Repository navigation
Require policyengine-observability 3.0.7 so Modal preemptions are recorded at WARNING - #1033
Merged
Merged
Conversation
…NING They fail with policyengine-observability 3.0.4, which records an operation that ends in KeyboardInterrupt at ERROR. They pass with the change in PolicyEngine/policyengine-observability#44; the executor's uv.lock moves to that release once it is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at WARNING 3.0.7 records an operation, request or span that ends in KeyboardInterrupt with the outcome "interrupted" at WARNING and leaves it out of the error count metric. Modal delivers a preemption as KeyboardInterrupt, so a preempted worker attempt no longer writes an ERROR record. The tests added in the previous commit now pass. Raises the floor to >=3.0.7,<4 in the observability library, the entrypoint, the gateway and both executor dependency lists, and relocks the six projects whose uv.lock holds the package with uv 0.13.6 (uv lock --upgrade-package policyengine-observability); only that package moves in each lock. Updates the type-checking and observability skills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…docs The recorded_observability_runtime fixture now sets capture_standard_library=True, as the deployed workers' runtime does, so the tests' "no ERROR records" checks also see ERROR records written through logging. runtime.shutdown() removes the handler it installs. The observability skill no longer says a span is recorded at WARNING: a span that ends in KeyboardInterrupt keeps an unset status and gets policyengine.outcome=interrupted. The type-checking skill says the scope typing test fails only below 3.0.4. The changelog fragment adds the other 3.0.7 changes: the new outcome value on the request and operation metrics, the span status, and FastAPI requests that end in KeyboardInterrupt, which 3.0.4 recorded from their status code alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #992, a preempted Stage 12 segment runner cancels the segment calls it started, and a preempted coordinator with a report run cancels its unfinished simulation calls. Modal ends a cancelled call with InputCancellation, which is not a KeyboardInterrupt, so policyengine-observability 3.0.7 still records it at ERROR. The observability skill and the changelog fragment now say so, and the fragment no longer says every span ending in KeyboardInterrupt was set to error: 3.0.4 did that only for operation spans and runtime.span spans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A segmented US simulation that a preempted coordinator cancels cancels its own started segments, and each running segment writes an ERROR record too; the observability skill and the changelog fragment now say so. The skill says Modal delivers a preemption as KeyboardInterrupt only to synchronous functions: in the gateway's ASGI app it cancels the running task, so a request ends in asyncio.CancelledError. The fragment drops the sentence about requests that end in KeyboardInterrupt, which no preemption in these services reaches, names the observability library among the packages that moved, and says older versioned executor apps keep the earlier behaviour until they are redeployed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
3 tasks done
anth-volk
marked this pull request as ready for review
November 10, 2026 14:40
This was referenced Nov 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1026
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Changelog
changelog.d/<name>.<type>.md, whose type is breaking, added, changed, fixed or removed.Summary
KeyboardInterruptand then runs the same input again, so the job succeeds. policyengine-observability 3.0.4 recorded every operation that ended inKeyboardInterruptwith outcomeerrorat ERROR, and the Public API's daily telemetry report opened issues for them ([telemetry] policyengine-simulation-py: KeyboardInterrupt (b659eaa2004ca456) policyengine-api#4151, #4153, #4154, #4227, #4228).KeyboardInterruptwith outcomeinterruptedat WARNING, leaves it out of the error count metric, leaves the span status unset withpolicyengine.outcome=interrupted, and lets the exception propagate unchanged.>=3.0.7,<4(was>=3.0.4,<4) inlibs/policyengine-simulation-observability,projects/policyengine-simulation-entry,projects/policyengine-simulation-executor(both dependency lists) andprojects/policyengine-simulation-gateway, so a later relock cannot drop the fix.uv.lockholds the package withuv lock --upgrade-package policyengine-observability(uv 0.13.6, the repository's pinned release). Each lock diff changes only that package's version, file hashes and specifier; the file hashes match PyPI's.policyengine-simulation-contractandpolicyengine-stage12-persistenceget the package throughpolicyengine-simulation-observabilityand change only their lock.docs/engineering/skills/observability.mdand updates the floor indocs/engineering/skills/type-checking.md.What still writes ERROR after a preemption
Since #992, a preempted Stage 12 segment runner cancels the segment calls it started (
stage12_runtime/fanout.py), and a preempted Stage 12 coordinator with a report run cancels its unfinished simulation calls (report_coordination.py,stop_children()); a segmented US simulation cancelled that way cancels its own started segments in turn. Modal 1.4.2 ends a running sync call it cancels by raisingmodal.exception.InputCancellationin it, which is aBaseExceptionbut not aKeyboardInterrupt, so 3.0.7 records each cancelled call's operation with outcomeerrorat ERROR. One coordinator preemption can therefore write an ERROR record for each simulation and each of its running segments. This PR doesn't change that; the observability skill and the changelog fragment say so. Recording a cancelled call differently needs a change in policyengine-observability or in how these workers end a cancelled call; no issue is open for it yet.What else changes from 3.0.4 to 3.0.7
record_exception,event,log) carries the matched route template ashttp.route, such as/jobs/{job_id}, instead of<unmatched>. This affects the gateway, the entrypoint and the executor's local FastAPI app. The changelog fragment says so. The Public API's daily telemetry report excludes health-check routes byhttp.route(.github/scripts/telemetry_error_issues/bigquery.py,HEALTH_CHECK_ROUTES), so an ERROR record written inside a health-check handler, which carried<unmatched>and was counted, now carries the health-check route and is left out of the report.uv.lock; the package's requirements are unchanged.interruptedas a new value of theiroutcomeattribute.KeyboardInterruptkeeps an unset status (3.0.4 set it to error).KeyboardInterruptto the request record. 3.0.4 recorded such a request from its status code alone, assuccessat INFO when no status had been sent; it is nowinterruptedat WARNING. No preemption in these services reaches that code: the gateway is a Modal ASGI app, and Modal cancels an async app's running task on the first SIGINT (modal/_runtime/user_code_event_loop.py), so a preempted gateway request ends inasyncio.CancelledError, which 3.0.7 handles as 3.0.4 did. The changelog fragment leaves this out for that reason.otel.span_annotate, andotel.span_endcounts only failures to close a span. Nothing in this repository reads either diagnostic name.Overlap with #1019
Open draft #1019 relocks the same six lock files to 3.0.6 and raises the same floors to
>=3.0.6. Whichever of the two merges second needs a relock (uv lock --upgrade-package policyengine-observabilityin each of the six projects) and its floors and docs adjusted to the other's; resolving those conflicts must keep>=3.0.7, or the preemption fix can be dropped by a later relock. If this one merges first, #1019's lock and floor changes are already onmain, and it reduces to its route tests, its documentation paragraph and its changelog fragment.Deployment
The gateway's image and the executor's simulation, Stage 12 and Stage 12 precompute images install from these locks with
uv_sync(frozen=True), and the entrypoint's Dockerfile runsuv sync --frozen, so 3.0.7 reaches all of them on the next release.A release deploys only the versioned executor app for the current policyengine.py version,
policyengine-simulation-py<version>(simulation-deploy.reusable.yml, step "Select versioned executor app"). Executor apps for older policyengine.py versions stay deployed with 3.0.4 and still serve requests that pin those versions, so their preemptions are still recorded at ERROR until they are redeployed. Unpinned requests reach the current release. 3.0.5 to 3.0.7 read no new environment variable; the variables the observability runtime reads (OBSERVABILITY_*,OTEL_*,POLICYENGINE_OTEL_*) are the same as in 3.0.4, and this PR changes none of their providers or wiring.Testing
Red then green. The first commit,
0f5fde26, adds the tests; they fail on 3.0.4 withassert 'ERROR' == 'WARNING'(and[('ERROR', 'error')] == [('WARNING', 'interrupted')]). With this PR's locks they pass (6 passed):projects/policyengine-simulation-executor:tests/test_job_progress.py::TestModalRunSimulation::test__preempted_job_is_recorded_at_warning:run_simulationpreempted during the calculation.tests/test_job_progress.py::test_preempted_segment_is_recorded_at_warning:run_simulation_segmentpreempted.tests/test_stage12_modal_app.py::test_stage12_functions_record_a_preemption_at_warning, forrun_single_simulation_us,run_single_simulation_uk,run_single_simulation_segment_usandcoordinate_report.recorded_observability_runtimeintests/conftest.py, which keeps every structured record and, like the deployed workers' runtime, also records standard-library log records), raisesKeyboardInterruptinside it, checks the exception propagates, checks theoperation.completedrecord has severityWARNINGand outcomeinterrupted, and checks no ERROR record was written.mainat0576797f, which includes Record report runs and simulation runs from the v2 Modal coordinator and workers #992.uv lock --check --project <dir>passes for all six relocked projects after that merge.python -m scripts.rules pyright --sync <package>passes with no Pyright errors for all six on193ff0a8(before the second merge frommain).checkandformat --checkpass on the changed test files.BASE_SHA=origin/main .github/scripts/check-changelog-fragment.shpasses.🤖 Generated with Claude Code