Skip to content

Commit e3d9b5e

Browse files
durable-workflow.github.io: update v2 changes
1 parent 178607e commit e3d9b5e

File tree

3 files changed

+73
-4
lines changed

3 files changed

+73
-4
lines changed

docs/defining-workflows/activities.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,10 @@ $handle = startActivity(
9898
| `queue` | `string` | Queue name override |
9999
| `maxAttempts` | `int` | Override the activity class `$tries` |
100100
| `backoff` | `int\|list<int>` | Override the activity class `backoff()` |
101-
| `startToCloseTimeout` | `int` | Seconds from activity start to required completion |
101+
| `startToCloseTimeout` | `int` | Seconds from activity start to required completion (per attempt) |
102102
| `scheduleToStartTimeout` | `int` | Seconds from scheduling to first claim |
103+
| `scheduleToCloseTimeout` | `int` | Total wall-clock seconds from scheduling to completion across all retries |
104+
| `heartbeatTimeout` | `int` | Seconds between heartbeats before the activity is considered unresponsive |
103105

104106
### Resolution order
105107

docs/defining-workflows/workflow-id.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ $sameSelectedRun = WorkflowStub::loadRun($runId);
4040

4141
That instance-scoped selector is useful when another system already stored the public instance id and later wants to pin a historical run without changing the outer workflow address.
4242

43-
All three loaders accept an optional `namespace` parameter. When provided, loading is scoped to the given namespace at the query level — a workflow belonging to a different namespace will not be found. See [Namespace Scoping](/configuration/options#namespace-scoping) for details.
43+
All three loaders accept an optional `namespace` parameter. When provided, loading is scoped to the given namespace at the query level — a workflow belonging to a different namespace will not be found. See [Namespace](../configuration/options.md#namespace) for details.
4444

4545
Instance-scoped current-run selection no longer trusts only the mutable `workflow_instances.current_run_id` pointer. `load($instanceId)`, `currentRunId()`, Waterline instance routes, and other instance-targeted current-run actions resolve the newest durable run in the instance chain, so continue-as-new navigation still lands on the right run even if that pointer is temporarily stale or null.
4646

docs/features/timeouts.md

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,78 @@ When a workflow continues as new:
104104

105105
This means the execution timeout always measures from the original start, while each new run gets its own fresh run-timeout window.
106106

107+
## Activity timeouts
108+
109+
Activity timeouts let you bound how long individual activity executions are allowed to take. There are four activity timeout scopes:
110+
111+
- **Schedule-to-start** — caps the time from scheduling to the first worker claim. Enforced while the activity is `Pending`.
112+
- **Start-to-close** — caps the time from when a worker claims the activity to when it must complete. Resets on each retry attempt.
113+
- **Schedule-to-close** — caps the total wall-clock time from scheduling to completion across all retry attempts. This is always terminal — retrying would not help because the overall deadline has passed.
114+
- **Heartbeat** — caps the time between heartbeats. For long-running activities that call `$this->heartbeat()`, the engine requires a heartbeat within the configured interval or the activity is considered unresponsive.
115+
116+
All are optional and can be combined. Configure them through `ActivityOptions` when calling an activity:
117+
118+
```php
119+
use function Workflow\V2\activity;
120+
use Workflow\V2\Support\ActivityOptions;
121+
122+
$result = activity(
123+
LongRunningActivity::class,
124+
new ActivityOptions(
125+
scheduleToStartTimeout: 30, // must be claimed within 30s
126+
startToCloseTimeout: 300, // each attempt has 5 minutes
127+
scheduleToCloseTimeout: 600, // total 10 minutes across all retries
128+
heartbeatTimeout: 15, // must heartbeat every 15 seconds
129+
maxAttempts: 3,
130+
),
131+
$input,
132+
);
133+
```
134+
135+
### Schedule-to-start timeout
136+
137+
The deadline is computed when the activity is scheduled. If no worker claims the activity before the deadline, the `TaskWatchdog` enforces the timeout. If retry attempts remain, a new activity task is scheduled with the snapped backoff; otherwise a terminal `ActivityTimedOut` history event is recorded and the workflow is woken.
138+
139+
### Start-to-close timeout
140+
141+
The deadline is computed when a worker claims the activity task. Each retry gets a fresh deadline. If the activity does not complete before the deadline, the current attempt is closed and the same retry-or-terminal decision applies.
142+
143+
### Schedule-to-close timeout
144+
145+
The deadline is computed once at scheduling time and never resets. When it expires, the activity is immediately failed as terminal — even if retry attempts remain, because the total allowed wall-clock time has passed. This is useful for bounding the overall cost of a flaky activity that might otherwise retry indefinitely within its per-attempt limits.
146+
147+
### Heartbeat timeout
148+
149+
The initial deadline is computed when a worker claims the activity task. Each successful `$this->heartbeat()` call extends the deadline by the configured interval. If the activity does not call `heartbeat()` before the deadline expires, the engine assumes the worker is unresponsive. If retry attempts remain, a new attempt is scheduled; otherwise a terminal timeout is recorded.
150+
151+
```php
152+
use Workflow\V2\Activity;
153+
154+
class LongRunningActivity extends Activity
155+
{
156+
public function handle($input)
157+
{
158+
foreach ($items as $item) {
159+
$this->heartbeat(['processed' => $count]);
160+
// ... process item ...
161+
}
162+
}
163+
}
164+
```
165+
166+
### Enforcement
167+
168+
Activity timeouts are enforced by the `TaskWatchdog` on each worker-loop pass. The watchdog scans for executions whose deadline columns have passed and delegates to `ActivityTimeoutEnforcer`. Each enforcement records:
169+
170+
- A terminal `ActivityTimedOut` history event with `timeout_kind` set to `schedule_to_start`, `start_to_close`, `schedule_to_close`, or `heartbeat`
171+
- A `WorkflowFailure` row with `failure_category = timeout` and `propagation_kind = timeout`
172+
- A workflow resume task to wake the parent workflow so it can observe the failure
173+
174+
Waterline displays the retry policy including all configured timeout types in the activity detail view. The timeline shows the timeout kind in the activity timed-out event message.
175+
107176
### What is not yet covered
108177

109178
The following are planned but not yet implemented:
110179

111-
- Task-level timeouts (schedule-to-start, start-to-close, schedule-to-close, heartbeat)
112-
- Automatic timeout enforcement (firing `WorkflowTimedOut` events and terminating the run when a deadline passes)
113180
- Retry policies at the workflow level
114181
- Typed structural-limit failures for payload size, history size, and pending fan-out

0 commit comments

Comments
 (0)