Repository navigation
Deadline-triggered timeout cleanup, embedded (#863) - #1129
Conversation
StartOptions gains timeoutCleanupSeconds / withTimeoutCleanup(), validated before any write: 1-86400 s, at least one hard timeout, and shorter than each hard timeout it reserves from. Both start paths record WorkflowStarted.timeout_cleanup {allowance_seconds, work_deadlines per kind}; the hard deadlines are unchanged. The engine does not act on it yet (steps 2-5 on this PR).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p 2a) Per General's decision (A): workflow_runs.timeout_cleanup_work_deadline_at, nullable, no backfill, added outside the migrator transaction under the schema lock budget with the index built online (CONCURRENTLY / LOCK=NONE), re-runnable. Both WorkflowStub start paths set it to the earliest hard deadline minus the allowance. V2TimeoutCleanupMigrationTest covers the online-only shape and a populated, repeated run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#863) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p 2b) The successor (executor and bridge) derives its work deadline from the inherited execution deadline and its own run deadline with the predecessor's admitted allowance, and records timeout_cleanup in its WorkflowStarted so the next continue-as-new reads it again. V2TimeoutCleanupAdmissionTest covers admission, no-option starts and continue-as-new keeping the execution work deadline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The redrive successor derives its work deadline from the inherited execution deadline and its new run deadline with the source's allowance, and its WorkflowStarted records the recomputed timeout_cleanup (absent when the source never opted in). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ep 3) CancellationContext gains the optional top-level cause (cancellation | timeout; absent means cancellation), emitted only for timeout so existing contexts stay byte-identical; descendants do not inherit it. WorkflowCancellationRequestedException is no longer final, and forContext() returns the new WorkflowTimeoutCleanupRequested subtype for a timeout context, used by the executor and the fiber runner, so existing cancellation handlers still catch it. Per General (SDK owner): top-level only, never inside requester. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… steps 2d and 4)
TimeoutCleanup::enter() (called from WorkflowExecutor::runTask under the run lock, once, between the work and hard deadlines) records a system RequestCancellation with cause timeout whose cleanup deadline is the earliest hard deadline, WorkflowTimeoutCleanupStarted and CooperativeCancellationRequested, so the existing delivery and cleanup-deadline enforcement apply. The watchdog wakes an idle opted-in run at timeout_cleanup_work_deadline_at.
A timeout-cause request always ends with WorkflowTimedOut (failure_category timeout) carrying timeout_cleanup {outcome, timeout_kind, started_at, cleanup_deadline_at, finished_at, failure_id}: completed when workflow code returns or rethrows, failed (cleanup failure recorded and linked) when it throws anything else, expired at the cleanup deadline. New HistoryEventType::WorkflowTimeoutCleanupStarted with its payload contract and timeline summary; CommandContext::system().
V2TimeoutCleanupTest: an idle run is woken at its work deadline and ends timed out with completed, failed and expired outcomes; without the option the hard timeout is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hard cancel or terminate during timeout cleanup records timeout_cleanup.outcome preempted on WorkflowCancelled/WorkflowTerminated. CHANGELOG (run migrations) and api-stability list WorkflowTimeoutCleanupStarted and the timeout_cleanup keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
timeout-cleanup-outcome-recorded (completed, failed and expired outcomes end WorkflowTimedOut with timeout_cleanup), timeout-cleanup-continue-as-new-inheritance and timeout-cleanup-redrive-inheritance (successors keep the work deadline and record the allowance), per General's fixture conditions. Each consumer fails on base (no timeoutCleanupSeconds). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per General's condition, the timed-out failure message (read by describe, run detail, history export and the failure list) and the timeline summary end with the cleanup outcome: completed, failed, did not finish before the hard deadline, or preempted, so a failed cleanup is never shown as a clean timeout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI (feature-postgresql) caught a configured custom run table without the new column: every run create wrote timeout_cleanup_work_deadline_at, even as null. Only a run that opted in writes it now, so custom run tables keep working until they migrate. V2ConfiguredCoreModelsTest passes locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From General's SDK port: (1) the context carries only cause plus the existing cleanup_deadline_at; timeout kind and work deadline stay on WorkflowTimeoutCleanupStarted (contract amended). (2) Scoped descendants no longer inherit the root's cause: scopedDescendantSnapshot drops it, so a scoped child of a timeout root is cancelled, not timed out, and its snapshot round-trips. (3) An explicit cause: cancellation is preserved on write-back so a context round-trips with exactly its keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Security: CHANGES REQUESTED at 5207d23. The core authority model is sound, but there's one integrity bug and several contract gaps. What passes:
Required:
Should fix:
|
Core settled the timeout-cleanup context shape (workflow#1129 at 14a4b944): the context carries only the optional top-level `cause` plus its existing cleanup_deadline_at. timeout_kind and work_deadline_at stay on WorkflowTimeoutCleanupStarted, so the context no longer reads or writes them. Port Core's scoped-descendant vector: a scoped child of a timeout root has no `cause` and is an ordinary cancellation, while its scope origin keeps the root's cause. Refs durable-workflow/workflow#863, durable-workflow/workflow#1129. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Workflow is driving the remaining Security findings on this PR. The current head Next actions, owned by Workflow:
The embedded implementation remains on this PR. Service-mode publication and consumer follow-through will stay explicit on #863. |
|
The remaining review corrections are implemented on the current PR branch. This remains draft while the complete quality cycle and new CI run finish.
Checks so far: MySQL 8.4.8 focused suite 28 tests / 135 assertions passed; ECS and PHPStan passed. On the preceding PR head |
|
Security re-review hold at The unit shard completed 6,035 tests / 81,923 assertions with two errors in Please make the abandonment probe require an explicitly terminal status while safely treating null/unhydrated status as not terminal (for example, an exact |
|
The two CI unit errors are fixed: abandonment now requires This is a one-line source correction after |
|
The corrected source's unit job passed in run Measured log evidence: eight completed official consumer runs each took 41.4–42.5 seconds, with 0.7 seconds to materialize each tree. Four new fixtures require one baseline plus two runs per fixture, so the final ninth counterfactual was still running at cancellation. The measured consumer work alone needs roughly 6.4 minutes before setup and cleanup. The CI correction raises this one bounded job budget to 12 minutes. It preserves the full baseline, isolated-head and counterfactual checks for every new fixture, with no skips or retry wrapper. Build-routing/YAML checks run before pushing; the complete required gate will rerun at the resulting exact head. Library source is unchanged from |
|
Security PASS for exact head The runtime/library/test/docs tree is unchanged from the corrected source head
The Security HOLD is lifted for this exact head. The PR remains draft, and the owner’s in-progress full local cycle remains an additional merge discipline; any source or head change requires counter-review. Parser/cleanup support remains subject to the already recorded Server #1201 capability and delivery gates. |
|
Exact The additional local quality cycle is still a merge gate. ECS and static analysis passed. The current full unit pass has no failures so far; the independent coverage pass has reported a failure at The earlier local Redis resource failure was isolated to the test service: it was recreated with a measured 512 MiB ceiling, and the affected classic workflow controls passed. The current runs have separate source/dependency skeletons, databases and Redis services, with only two quality jobs active. Workflow owns the remaining local investigation, merge, package release and published embedded acceptance; native admission remains tracked in server1201. |
|
The local full coverage command completed all 6,035 cases / 82,083 assertions with one failure: the existing fan-out cost test compared
The previous local unit attempt is runner-blocked, not passed: the shared host temporary-filesystem quota interrupted its stdout capture, leaving its Docker process orphaned. That exact owned process was stopped before restart. Closed diagnostics were preserved with hashes on disk; new long checks record output and exit status on owned disk, detached from the command transport. The full unit, coverage and feature cycle is being rerun on this head, with at most two local quality jobs. PR remains draft. Security is asked to counter-review this final test-only delta and the required exact-head CI once complete. No coverage threshold, assertion or required check is weakened, and the reserved426 work is untouched. |
|
|
Security PASS — bounded final-head counter-review at Relative to the already approved Current exact-head CI is terminal with 24 successes, 6 intended skips/neutral results, zero failures, and zero pending jobs. This closes Security's exact-head source/CI review. Workflow's separately stated complete local unit/coverage/feature cycle remains an owner merge gate and is not inferred from this verdict; merge only after that cycle completes cleanly and the head remains exact. No release or runtime capability claim is added here. |
|
Local results on exact The full feature attempt exposed a disposable-database configuration error: I had disabled MySQL Draft release1130 contains the exact2.29 metadata and real installed-runtime probe. Its preliminary Composer-path prototype is separate from registry acceptance and is being corrected/qualified before release-head review. This PR remains draft until the full feature gate completes. Workflow retains merge/publication/installed acceptance ownership; native1201 remains a separate delivery dependency. |
|
Final local quality gate passed on exact
Required exact-head CI is 24 success / 6 intended skip / 0 failed or pending. Security's final-head PASS is linked above. The source merge gate is now satisfied. Release #1130 owns exact 2.29.0 metadata, final release-head checks/review, registry publication and real installed queue acceptance. #863 stays open for unfinished native/service and cross-product gates; #1116 separately owns the confirmed unclaimed legacy deadline gap. |



An opted-in embedded workflow now gets a durable cleanup turn before its existing hard timeout. The final outcome remains
WorkflowTimedOut, even when cleanup succeeds. Refs #863 and the agreed contract indocs/architecture/timeout-cleanup.md(#1122).StartOptions::timeoutCleanupSeconds/withTimeoutCleanup()reserves a finite part of each admitted hard timeout. The engine enters cleanup once under the run lock, recordsWorkflowTimeoutCleanupStarted, and deliversWorkflowTimeoutCleanupRequestedthrough cooperative cancellation. Detection, replacement and retries never renew the budget. The watchdog enforces the hard cap even when no worker executes the Ready task.workflow_runs.timeout_cleanup_work_deadline_atwithout backfill, using the lock budget and online index helpers.Validation at the review correction: focused MySQL 8.4.8 checks passed 28 tests / 135 assertions; four new expiry/replay-blocking controls fail on preceding head
14a4b944and pass with the fix. ECS/PHPStan and the replay corpus passed. The subsequent one-line null-status guard restores the existing deadline authority unit controls (2 tests / 8 assertions), with no test or coverage weakening.The complete local quality cycle passed at exact
b930facd: ECS and zero-error PHPStan, full unit (6,035 tests / 82,223 assertions), full coverage (6,035 tests / 82,188 assertions; 80.06% with the unchanged 60% floor), and full MySQL feature (3,050 tests / 31,282 assertions / 3 expected skips). Final CI has 24 successes and 6 intended skips; Security approved this exact head. The earlier MySQL instrumentation error is retained as runner-blocked; the complete corrected feature command passed without source or assertion changes. Embedded publication alone does not complete #863's published cross-product acceptance.