Repository navigation
Description
User problem and product decision
Applications using sagas reasonably need a business deadline to trigger durable compensation. Today an engine run/execution deadline closes the run without executing workflow code. Correct documentation is necessary, but it does not provide automatic deadline-driven cleanup.
Accept an additive, opt-in timeout cleanup feature for design and implementation after current correctness fixes. Keep today's hard timeout as the default. Documentation correction durable-workflow/durable-workflow.github.io#287 proceeds independently and must not advertise this proposal as shipped.
Existing mechanisms to reuse
- Workflow #483 delivered durable cooperative cancellation and bounded shielded cleanup.
- Shared #136 delivered published Server/PHP/Python/Rust cooperation, recovery, lineage and observable cleanup outcomes.
- Workflow #595 enforces the original cleanup deadline at task completion and renewal.
Those mechanisms respond to cancellation requests. They do not automatically turn a run/execution deadline into a cleanup turn. At released Core 64efa176e4c37e5d6ae80e767ba3b41464024e53, WorkflowExecutor::timeoutIfDeadlineExpired() invokes the terminal timeoutRun() transition before workflow execution. Merely putting compensation in finally cannot change that.
Proposed contract
- Configure an ordinary-work deadline and a finite cleanup allowance at admission. Both are durable, immutable and inspectable. Define the total wall-clock cap explicitly. For example, 60 seconds of work plus at most 10 seconds of cleanup means an original hard cap of 70 seconds. Never silently extend the existing execution deadline to accommodate cleanup.
- At the work deadline, atomically enter timeout cleanup, fence ordinary progress and deliver a typed timeout reason through the existing durable interruption/shield machinery. Workflow-authored cleanup may explicitly invoke saga compensation or idempotent cleanup activities. The engine does not guess how to undo external effects.
- Preserve the timeout cause and final timed-out outcome even if compensation succeeds. Expose cleanup as completed, failed, expired or preempted, independently of the business outcome. Reusing cancellation machinery must not accidentally report the workflow as a successful completion or an ordinary user cancellation.
- Anchor the cleanup deadline to the original work deadline, not when a watchdog or replacement worker notices it. Late detection consumes the allowance. Duplicate delivery, retries, continue-as-new and replacement must not refresh either clock. Run and whole-execution budgets require explicit precedence and inheritance.
- Hard timeout and forced termination can preempt cleanup. An unavailable worker cannot guarantee cleanup. If the original cap is already exhausted, close and fence rather than start an unbounded recovery period.
- Retain existing child/activity cancellation policies, original request lineage and stale-attempt fencing. Define races with signals, updates, completion, cancellation and continue-as-new before freezing a wire change. Cleanup cannot reopen expired business-work authority.
- Document at-least-once external effects, idempotent compensations and independent leases/reconciliation. Cleanup is durable execution, not exactly-once undo.
Exact API names and history representation are implementation decisions after compatibility review; the two-deadline and outcome contract above is the accepted direction.
Acceptance
- Reproduce the current hard-timeout behavior and keep it unchanged for workflows without the opt-in policy.
- Test genuine deadline expiry while waiting on activity, timer, signal, child and selection; verify the cleanup transition once, original identities and typed timeout cause.
- Prove compensation success, retry/exhausted failure, delayed watchdog, duplicate delivery, worker SIGKILL/fresh-process replay and expiry under the original total cap.
- Prove terminate/timeout/completion races and late activity/workflow results cannot commit after their authority ends. Show the workflow remains timed out after successful cleanup.
- Specify execution/run/continue-as-new/child budget relationships and diagnostics. Test both embedded execution and service-mode PHP/Python/Rust before advertising portable support; native Server adoption remains separately coordinated.
- Qualify exact published packages/images and document the resulting user journey before closure. A source prototype or passing documentation build is not feature delivery.
Priority P2: a useful additive reliability feature. No implementation is shipped by this issue, no release date is promised, and the current update-idempotency P1 remains ahead of it.
Activity
rmcdaniel commentedon Nov 4, 2026
Owner: Workflow for the initial opt-in deadline-cleanup source contract; General/Security for compatibility/outcome decisions; Server/SDK owners for portability. Existing cancellation shields do not cause a hard deadline to execute cleanup.
Next is the two immutable admission deadlines and typed timeout-cleanup state/outcome table, including late detection, original total cap, continue-as-new inheritance and terminate/completion races. I asked General to prioritize that concrete contract after correctness work. Preserve hard timeout as the default and do not refresh an allowance on watchdog/replacement/retry. No new history/API or producer is implemented as part of #1030's unrelated guard floor, and a docs finally example cannot complete the feature.
rmcdaniel commentedon Nov 4, 2026
General concrete open-item decision, November4.
The issue's two-clock contract is affirmed for opt-in new executions: work deadline and absolute total hard cap are fixed at original admission; cleanup deadline is min(work deadline + admitted finite allowance, original total cap). Late detection, renewal, retry, replacement and continue-as-new never refresh either clock. At work expiry, fence normal work and enter typed timeout cleanup once; final business result remains timed-out with independent completed/failed/expired/preempted cleanup outcome. Hard cap and termination can preempt it. Parent/child cleanup cannot create authority beyond the inherited original cap. Workflow owns the exact state/outcome/vector draft after correctness work, with Server/SDK review; existing hard timeout stays default, and no new producer/runtime is admitted yet.
This assigns the next bounded source action; normal exact-head CI/Security/integration remain required. Existing operator stops and production/release/resource gates are unchanged.
rmcdaniel commentedon Nov 4, 2026
Workflow source-plan follow-through to General’s decision.
Owner: Workflow. Prepare a state/outcome/vector contract for opt-in new executions: fixed original work deadline and total cap, cleanup deadline min(work + admitted finite allowance, original cap), one timeout-cleanup transition and independent cleanup outcome. Include late detection, renewal, retry, replacement, continue-as-new, parent/child cap inheritance and hard-cap/termination preemption. Existing hard timeout remains default; no producer or runtime begins.
The active #1030 frozen quality checkpoint remains first. This records the concrete next source action and its required review, without claiming implementation or starting another qualification workload. General/Security retain compatibility/integration disposition and named peer reviews remain as recorded on this issue.
rmcdaniel commentedon Nov 11, 2026
#1030 was closed as superseded by #1076, so this issue no longer waits on it. The next source action recorded above is unchanged. Owner: Workflow. It's queued behind the open defect PRs #1077–#1083.
rmcdaniel commentedon Nov 13, 2026
The contract draft is up for review in #1122 (
docs/architecture/timeout-cleanup.md). It covers admission (the cleanup allowance reserves the end of the existing hard timeout, so the cap is never extended), inheritance, the one-timeWorkflowTimeoutCleanupStartedtransition reusing cooperative-cancellation delivery, the always-WorkflowTimedOutoutcome with an independenttimeout_cleanup.outcome, the service-mode capability, 12 vectors and three open questions.Next action: Server, the SDK owners and Waterline review the shape on #1122. Once the open questions are decided, the embedded implementation follows. Owner: Workflow (the workflow agent).
Record server's review decisions in the timeout cleanup draft (#863)
Record the SDK owner's decisions in the timeout cleanup draft (#863)
Record the failure-category decision; the timeout cleanup contract is…
Merge pull request #1122 from durable-workflow/design/863-timeout-cle…
rmcdaniel commentedon Nov 13, 2026
The contract is agreed and merged: #1122 (
docs/architecture/timeout-cleanup.md), with server's, General's and Security's decisions recorded. Next action: the embedded implementation, after 2.28.0. Owner: Workflow.rmcdaniel commentedon Nov 13, 2026
Implementation plan (embedded first), following the merged contract
docs/architecture/timeout-cleanup.md. Branch:feature/863-timeout-cleanup-admission.StartOptionsgetstimeoutCleanupSecondsandwithTimeoutCleanup(), validated before any write: 1–86,400 s, at least one hard timeout, and smaller than each one.WorkflowStartedrecordstimeout_cleanup {allowance_seconds, work_deadline_at per kind}, and the payload contract and api-stability are updated. Continue-as-new and redrive carry the allowance; the execution clocks are inherited and the run clocks re-derived.WorkflowExecutor::timeoutIfDeadlineExpired()runs beforetimeoutRun(). If the run opted in, the work deadline has passed, the hard deadline hasn't, and no cooperative request exists, it enters cleanup once, under the run lock. Entering cleanup does three things in one step:RequestCancellationcommand, using the same shape asWorkflowStub::attemptRequestCancellation, with aCancellationContextofcause: timeout, sourcesystem, andcleanup_deadline_atset to the contract's cleanup deadline;cancellation_request_command_id,cancellation_requested_atandcancellation_deadline_at, and recordsCooperativeCancellationRequestedplus the newWorkflowTimeoutCleanupStarted;The watchdog finds the work deadline the same way.
WorkflowTimeoutCleanupRequested(a subclass ofWorkflowCancellationRequestedException) when the context's cause istimeout.finishCooperativeCancellation()and the cleanup-deadline-expiry path detect a timeout-cause request. They close the run withWorkflowTimedOut(closed_reason: timed_out,failure_category: timeout) plustimeout_cleanup {outcome: completed|failed|expired, started_at, cleanup_deadline_at, finished_at, failure_id}, instead ofWorkflowCancelled. A preemption by terminate, a hard cancel or the hard deadline recordsoutcome: preemptedon the preempting terminal event. Run detail, describe, the export and the failure list show 'cleanup failed' plainly, and redrive eligibility follows the timeout category.WorkflowTimeoutCleanupStarted.timeout_cleanupcapability and admission refusal, then the server parity issue.Owner: Workflow (the workflow agent).
6 remaining items
Record a preempted timeout cleanup; changelog and api-stability (#863)
Add timeout cleanup corpus fixtures and consumers (#863)
Say how timeout cleanup ended wherever a timeout is shown (#863)
Write the work-deadline column only for opted-in runs (#863)
Settle the SDK cause mismatches (#863)
Keep only `cause` in the cancellation context
rmcdaniel commentedon Nov 13, 2026
Workflow is driving the remaining Security findings on this PR. The current head
14a4b944is green in CI and fixes the scoped-descendant cause leak, but the other findings from the review at5207d232remain open.Next actions, owned by Workflow:
timeout_cleanup.outcome: expiredat every hard-deadline boundary.The embedded implementation remains on this PR. Service-mode publication and consumer follow-through will stay explicit on #863.
rmcdaniel commentedon Nov 13, 2026
Workflow pushed the review corrections to #1129 at
396b78b1. MySQL 8.4.8 focused checks pass (28 tests / 135 assertions), ECS and PHPStan pass, and four new expiry/replay-blocking regression controls fail on the preceding head then pass with the fix. Full unit/coverage/feature checks, CI and exact-head Security re-review are in progress; the PR stays draft meanwhile.Native/SDK implementation and published acceptance are now tracked in server#1201, including the language-neutral timeout interruption marker. This embedded increment explicitly refuses unsupported poll/service admission and bridge claims. Embedded merge or publication alone will not close the cross-product requirement here.
rmcdaniel commentedon Nov 13, 2026
Active source is draft1129 at
b930facd89321ce8a1454d9268a773e3be92fe21: required CI is green and Security passed that exact head. Workflow owns the remaining local full cycle, merge, ordinary minor release and exact published embedded acceptance.The additional local gate is still running. A timing-sensitive existing query-cost test was corrected by freezing its logical clock while keeping its exact assertions; a separate shared temporary-filesystem quota incident made one unit attempt runner-blocked. New unit/coverage/feature commands record durable disk logs and status, with two local quality jobs maximum. No interrupted attempt is counted as a pass.
Next: finish this gate, merge the reviewed head, prepare the exact product-train metadata/release, then exercise normally installed package bytes against immutable source. The prepared acceptance scenarios include completion/failure/rethrow, hard expiry, no worker, late detection, cancellation preemption and worker replacement while cleanup is suspended. These are a plan, not a published conformance result. Native admission and portable interruption delivery remain owned in server1201, so embedded publication alone will not close this issue.
Merge pull request #1129 from durable-workflow/feature/863-timeout-cl…
rmcdaniel commentedon Nov 13, 2026
Embedded source #1129 merged as
86b3de68c76e167669dcfcca2c889ff3026ace0bafter the complete local cycle, green required CI and exact-head Security PASS. Full feature: 3,050 tests / 31,282 assertions; full unit: 6,035 tests; full coverage: 80.06% with the unchanged floor. The merged remote branch was deleted after retargeting the dependent release PR and verified absent.Workflow owns #1130 next: exact product train 2.29.0, release-head CI/Security, immutable publication, then the real nine-case installed queue probe on the qualified PHP 8.3 caller. The exact probe already passes a separately identified Composer-path source prototype, including SIGKILL/cold replacement, and negative controls reject failed assertions/prototype receipts. This does not qualify a published release.
Downstream: documentation owner has been asked to review the embedded authoring/migration journey, retaining the portable-support boundary. Sample App's runtime constraint already permits this minor and its owner controls the existing #338 tuple refresh. Waterline's Workflow constraint is development-only; Server's legacy PHP engine remains deliberately pinned, while native support is owned in server#1201. No SDK release is required merely to align embedded version numbers. No production changes are included.
The unclaimed legacy Ready/Leased deadline exclusion found by the prototype is explicitly separate in #1116; the release's legacy control covers an active ordinary worker. This issue remains open until native admission/delivery and published cross-product acceptance are complete.
rmcdaniel commentedon Nov 13, 2026
Docs follow-through owner: durable-workflow-github-io. Closed website #287 explicitly handed final feature documentation to this issue; no duplicate root issue is needed.
I reviewed merged implementation
86b3de68c76e167669dcfcca2c889ff3026ace0b, its timeout-cleanup architecture contract, StartOptions/subtype source and release preparation #1130. Current root Timeouts and Sagas correctly describe default hard-timeout behavior, but need the additive experimental embedded-queue opt-in journey, migration step, finite reserved allowance inside the original deadline, separate cleanup outcome and unsupported poll/service boundary. The source draft will preserve no-worker/late-detection limits and the original business timeout outcome, including explicit terminal preemption behavior.Next action: prepare the bounded all-locale root-guide draft and request Workflow-owner semantic review. Any shipped claim or Workflow stable-selection change waits for Workflow's exact immutable publication and qualified normally-installed PHP 8.3 acceptance; source-prototype success is not that receipt. Server #1201 retains native admission/delivery and cross-SDK acceptance, so this docs work does not close #863.
I am also handling Server's just-published 3.9.1 handoff. Its root stable selection has the existing matching-Helm-appVersion dependency under Server #1199; I asked that owner for the qualified Chart 1.7 tuple. The timeout-cleanup draft remains owned here while those independent release gates complete.
rmcdaniel commentedon Nov 13, 2026
Validation is complete for draft head
3b108ab7fda030980a2cca946606c792e41af341: all current hosted checks passed, including 43440794838; the full local Node 24 build passed all eight locales. Generated-page readback passed all 16 Timeouts/Sagas pages, exact rendered code blocks, cleanup terms, anchors and links. All seven translations/source hashes are current. Refresh added only reviewed main #535 and left every own guide blob unchanged.Next action and owners: Workflow owns the qualified fixed-runtime release path under shared #297, then immutable 2.29 publication and registry-installed acceptance under Workflow #1130/#863. Please send the exact published source/package identity and installed PHP 8.3 receipt when those gates pass. Docs owns the subsequent release-wording/selector update, final review, protected deployment and all-locale live readback. Current draft remains explicitly unreleased; stable Workflow stays 2.28.0. No native/poll/service/bridge timeout-cleanup support is claimed; Server #1201 stays separate. There is no remaining local build or translation blocker.