Repository navigation
Drain the worker on deploy and make interrupted or malformed runs fail cleanly - #155
Merged
Merged
Conversation
…l cleanly - SIGTERM now drains the Temporal worker (graceful_shutdown_timeout, default 300s via TIN_LITE_WORKER_GRACEFUL_SHUTDOWN_SECONDS) while HTTP and the Codex relay keep serving; uvicorn's own 20s shutdown follows. WorkerGroup shutdown is idempotent and no longer hangs on a worker that never started. install_switchboard.sh sets TimeoutStopSec=420. - Weekly brief, answer page, project memory and project scan receipts are marked failed even on cancellation, and a retry whose model request was already observed fails non-retryably instead of "External request already observed". begin_observation raises ObservationAlreadyRecorded. - brand.capture: code owns the assessment method; variants are normalized with a note, non-objects are rejected, and an invalid pair fails once. - visibility.audit: question IDs are normalized deterministically before validation and receipt keys; adjudication references follow. - commit_project_changes maps code.storage RefUpdateError to invalid (no changes), conflict (stale ref) or a recorded storage failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ua8Ysd7jtjWKHGg9DrUs4n
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.
P1 from the 09-23..09-25 prod log review: deploys no longer kill in-flight model calls, and interrupted or malformed runs now fail once with a clear reason instead of wedging or retrying three times.
4. Deploy drain (in-process)
Why: every deploy logged
will wait 0:00:00 before cancelling activitiesandCancel N running task(s). The Codex relay runs in the same FastAPI app and stopped before the worker drained. At 01:18 this killed aproject.weekly_briefin the middle of a model call.graceful_shutdown_timeout, set by the new settingworker_graceful_shutdown_seconds(TIN_LITE_WORKER_GRACEFUL_SHUTDOWN_SECONDS, default 300).tin_lite/serving.pywithDrainingServer(uvicorn.Server). uvicorn 0.52'shandle_exitis a synchronoussignal.signalhandler, so on the first SIGTERM/SIGINT it only schedules a task withloop.call_soon_threadsafe. That task awaitsruntime.worker.shutdown()(stop polling, then drain), bounded by the grace period + 60s. Only after that does it setshould_exit, and then uvicorn's existingtimeout_graceful_shutdown=20HTTP drain runs. HTTP and the relay keep serving during the drain.uv runparent can both deliver one. A second SIGINT stops waiting.WorkerGroup.shutdown()is now idempotent: one shielded task shared by every caller, so the laterworker.shutdown()in themain.pylifespan returns at once. It also no longer hangs on a worker whoserun()failed before it started. I checked this against temporalio 1.32:Worker.shutdown()waits on a completion event that only_runsets.infra/install_switchboard.shunit:TimeoutStopSec=420.5. Effect receipts survive cancellation and never wedge a retry
Why: attempts 2 and 3 failed with
External request already observed; recover its owning effect. The owning effect stayedstartedbecauseCancelledErrorskipsexcept Exception.memory_commit) and project scan (scan_commit) now catchBaseException. They recordfail_effectthrough a helper that finishes the write even if cancelled again, before the effect lock releases its connection, and then re-raise.ApplicationErrorof typeModelRequestInterrupted: "…: the model request was interrupted and was not repeated; start the run again". This follows the visibility recovery pattern.usage_capture.begin_observationraisesObservationAlreadyRecorded(RuntimeError), keeping the same message. These sites convert it to the same terminal error.startedby the current release recover the same way on retry.6. brand.capture method
Why:
persist_codex_procedure_artifactfailed 3x withUnsupported brand assessment method, andCONTRACT.mdinvited variation.brand_contract.assessmentnow ownsMETHOD. A missing or variant method is normalized, with a note (logged at capture; shown in the brand readdiagnostics). A method that isn't an object is rejected.ValueErrorfromvalidate_pairin_procedure_companionsbecomes a non-retryableBrandCaptureInvalid.CONTRACT.mdnow states the exact literal. Packet-only coverage goes inevidence_statusand the findings, not in the method. Package version bumped to 1.0.2.7. Visibility question IDs
Why: two runs failed with
VisibilityProtocolError: visibility question ID is invalid.idgetspattern: ^[a-z0-9_-]{1,32}$.prepare_panelnormalizes IDs before validation, beforepanel_hash, and before the activity derives answer receipt keys from the stored panel. It lowercases, replaces other characters with_, truncates to 32 and de-duplicates with-2,-3. The result is deterministic: replaying the same saved response gives the same IDs, so receipt keys stay stable.question_idandevidence_question_idswritten in the old spelling are mapped onto the panel IDs.8. commit_project_changes RefUpdateError
Why: the MCP tool raised an unmapped
pierre_storageRefUpdateError("no changes to commit").project_filesdidn't record a failure andmcp_errorsmasked it.code_storage.commit_project_changesstill runs the lost-ack check first. After that, aRefUpdateErroris mapped:ValueError("no changes: files already match expected_revision"), shown in MCP asinvalid:.RuntimeError("canonical project state changed before file commit"), which becomesStaleProjectRevisionError, shown asconflict:.storage_failedRuntimeError. These are transient failures, so they are not reported as a stale revision.ProjectFileService.commitnow also fails the change row onValueError, so rows no longer staystarted.Tests
tests/test_worker_drain.py: a fake worker proves the exit handler awaits the drain beforeshould_exit, a real uvicorn keeps serving HTTP during the drain, and repeated signals, timeouts and WorkerGroup idempotency behave.tests/test_effect_interruption.py(Postgres):CancelledErrorin the middle ofexecute()for weekly brief and answer page leaves the receiptfailed; the retry is non-retryable with the new message and never calls the model; a legacystartedreceipt recovers the same way.tests/test_project_commit_errors.py.tests/test_brand_capture.pyandtests/test_visibility_audit.py.tests/test_cli_serve.pyandtests/test_runtime.py(asserts the 5-minute grace period), and the fakeget_effect(conn=)intest_answer_page,test_project_memoryandtest_scan_report.uv run pytest -n autoagainst a disposable Postgres: 3375 passed, 49 skipped, 3 failed. The 3 failures aretests/test_delimited_viewer.py, a known Node 18 issue on this host; CI uses Node 22.test_temporal_replay.pypasses, and no workflow code changed.ruff format --check,ruff checkandlint-importsare clean.tin-lite validate-community: 18 of 18 packages valid.Not merged or deployed.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ua8Ysd7jtjWKHGg9DrUs4n