A node timeout in Orchestra is rearmed on every park: parkDeadline() anchors the deadline at the current request time each time a token parks, so re-entering a node (a retry loop) starts a fresh window. There is no way to express a timeout window that spans multiple tries. This proposes a per-node timeout anchor so a node can hold a single global window across re-entries.
Motivating use case
A payment step the user may retry as many times as they like, but with a hard 10-minute total budget: after 10 minutes from the first attempt, the payment opportunity expires regardless of how many tries happened. With the current per-park behavior the 10 minutes reset on every retry, so the total time is unbounded.
Current behavior
The park branch in advanceNode() always does setDeadline(parkDeadline(node)), and parkDeadline() returns Duration::deadline(getRequestTime(), timeout), i.e. measured from now. A loop re-entry is a new token that parks and gets a brand-new deadline; the previous visit's token is consumed and excluded from the cron sweep, so the window is rearmed every try.
Proposed: a timeout_anchor node setting
Let a node choose what its timeout is measured from:
- park (default, today's behavior): now + timeout, rearmed each park.
- instance: instance-created + timeout, a process-global budget. Trivial, because the anchor (the instance creation time) never moves, so a re-park naturally reuses the same absolute deadline; no extra storage.
- node: first-arrival-at-this-node + timeout, a per-step budget across tries (the payment case). The first-arrival time is stored once per (instance, node) and reused on every re-park.
Implementation sketch
- parkDeadline() branches on the node's timeout_anchor: for instance, return instanceCreated + timeout; for node, read-or-create a stored anchor for (instance, node) and return anchor + timeout; for park, the current now + timeout.
- The stored node anchor can be an instance-scoped process variable (a reserved key per node id), set on first park and reused; cleared when the step is finally passed (token consumed past the node) so a later independent visit starts fresh.
- On each park the token's deadline column is set to the resolved absolute value, so the cron sweep (which already fires parked/waiting tokens whose deadline has passed) needs no change and fires at the global deadline no matter how many tries occurred.
- Editable in the modeler via the existing node-feature mechanism (alongside the timeout/timers/retry settings), and typed in config schema as a node_setting.
Workaround available today
The same effect can be modeled now with a parallel deadline branch: split into the retrying task branch and a single wait-PT10M branch, joined by a discriminator that fires on first arrival and cancels the other branch. The wait branch parks once, so its window never resets; whichever branch arrives first wins, cancelling the loser (paid versus expired). This composes with everything but requires modeling the extra branch and join. timeout_anchor makes it a one-field node config instead.
Scope notes
- The single node timeout is the natural vehicle; making the staged escalation-timer ladder (node timers) global as well would need the same anchor applied to the spawned timer tokens, and can be a follow-up.
- When a global node/instance deadline fires while the user is mid-try (e.g. away at a payment gateway), the timeout action fires and the branch routes to its timeout outcome; handling a late callback after expiry is a business concern at the handler boundary, not the engine's.
Open questions
- Storage for the node anchor: a reserved instance-scoped variable per node id, versus a dedicated field/store.
- Anchor naming/values: timeout_anchor = park | instance | node, versus a boolean timeout_reset.
- Whether to ship "from instance start" as the simplest first increment (it needs no per-node storage) and add the per-node anchor second.
Issue fork orchestra-3604996
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
mably commented