Skip to content

Add opt-in deadline-triggered durable cleanup with an immutable total budget #863

Open

Description

@rmcdaniel

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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

  1. added
    kind:featureA public product capability or experience is requested
    priority:P2Normal-priority product work
    status:readyReady for implementation
    on Oct 20, 2026
  2. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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.

  3. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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.

  4. rmcdaniel commented on Nov 4, 2026

    @rmcdaniel
    MemberAuthor

    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.

  5. rmcdaniel commented on Nov 11, 2026

    @rmcdaniel
    MemberAuthor

    #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.

  6. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    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-time WorkflowTimeoutCleanupStarted transition reusing cooperative-cancellation delivery, the always-WorkflowTimedOut outcome with an independent timeout_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).

  7. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    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.

  8. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Implementation plan (embedded first), following the merged contract docs/architecture/timeout-cleanup.md. Branch: feature/863-timeout-cleanup-admission.

    1. Admission: StartOptions gets timeoutCleanupSeconds and withTimeoutCleanup(), validated before any write: 1–86,400 s, at least one hard timeout, and smaller than each one. WorkflowStarted records timeout_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.
    2. Transition: WorkflowExecutor::timeoutIfDeadlineExpired() runs before timeoutRun(). 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:
      • it records a system-sourced RequestCancellation command, using the same shape as WorkflowStub::attemptRequestCancellation, with a CancellationContext of cause: timeout, source system, and cleanup_deadline_at set to the contract's cleanup deadline;
      • it sets cancellation_request_command_id, cancellation_requested_at and cancellation_deadline_at, and records CooperativeCancellationRequested plus the new WorkflowTimeoutCleanupStarted;
      • it makes sure a workflow task exists.
        The watchdog finds the work deadline the same way.
    3. Delivery: the existing cooperative delivery throws WorkflowTimeoutCleanupRequested (a subclass of WorkflowCancellationRequestedException) when the context's cause is timeout.
    4. Outcome: finishCooperativeCancellation() and the cleanup-deadline-expiry path detect a timeout-cause request. They close the run with WorkflowTimedOut (closed_reason: timed_out, failure_category: timeout) plus timeout_cleanup {outcome: completed|failed|expired, started_at, cleanup_deadline_at, finished_at, failure_id}, instead of WorkflowCancelled. A preemption by terminate, a hard cancel or the hard deadline records outcome: preempted on the preempting terminal event. Run detail, describe, the export and the failure list show 'cleanup failed' plainly, and redrive eligibility follows the timeout category.
    5. Tests: contract vectors 1–12 as feature tests, plus a corpus fixture whose consumer fails on base. Replay readers (fiber runner, query replayer) must accept WorkflowTimeoutCleanupStarted.
    6. Service mode: a follow-up PR with the timeout_cleanup capability and admission refusal, then the server parity issue.

    Owner: Workflow (the workflow agent).

  9. 6 remaining items

  10. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Workflow is driving the remaining Security findings on this PR. The current head 14a4b944 is green in CI and fixes the scoped-descendant cause leak, but the other findings from the review at 5207d232 remain open.

    Next actions, owned by Workflow:

    • Preserve timeout_cleanup.outcome: expired at every hard-deadline boundary.
    • Preserve the activity abandon contract, and let replay-blocking errors keep the run repairable during cleanup.
    • Cover the missing contract vectors, cancellation interactions and task-path expiry.
    • Address bridge-driven cleanup admission/delivery and watchdog churn, and mark engine command attribution internal.
    • Run the repository quality cycle and counterfactual regression checks, then request Security review at the new exact head. Merge remains gated on that review and passing CI.

    The embedded implementation remains on this PR. Service-mode publication and consumer follow-through will stay explicit on #863.

  11. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    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.

  12. added
    status:in-progressApproved work is actively being implemented or validated
    and removed
    status:readyReady for implementation
    on Nov 13, 2026
  13. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    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.

  14. added a commit that references this issue on Nov 13, 2026
  15. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    Embedded source #1129 merged as 86b3de68c76e167669dcfcca2c889ff3026ace0b after 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.

  16. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    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.

  17. rmcdaniel commented on Nov 13, 2026

    @rmcdaniel
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:workflowkind:featureA public product capability or experience is requestedpriority:P2Normal-priority product workstatus:in-progressApproved work is actively being implemented or validated

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions