Skip to content

Commit f39bb27

Browse files
docs: add structural limit failure taxonomy and constraints guide
1 parent 9f84207 commit f39bb27

File tree

3 files changed

+128
-1
lines changed

3 files changed

+128
-1
lines changed
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
---
2+
sidebar_position: 5
3+
---
4+
5+
# Structural Limits
6+
7+
Structural limits cap the resource consumption of a single workflow run. When an operation would exceed a configured limit, the engine records a typed failure with a machine-readable `structural_limit` failure category and the specific limit kind, then fails the run. This protects the system from unbounded fan-out, oversized payloads, and metadata bloat.
8+
9+
## Limit kinds
10+
11+
| Limit kind | Default | What it caps |
12+
|---|---|---|
13+
| `pending_activity_count` | 2,000 | Non-terminal activity executions open simultaneously |
14+
| `pending_child_count` | 1,000 | Non-terminal child workflows open simultaneously |
15+
| `pending_timer_count` | 2,000 | Pending timers open simultaneously |
16+
| `pending_signal_count` | 5,000 | Unprocessed signals pending simultaneously |
17+
| `pending_update_count` | 500 | Unresolved updates pending simultaneously |
18+
| `command_batch_size` | 1,000 | Items in a single parallel fan-out (`all()` / `parallel()`) |
19+
| `payload_size_bytes` | 2 MiB | Serialized size of a single argument payload |
20+
| `memo_size_bytes` | 256 KiB | Serialized size of non-indexed memo metadata |
21+
| `search_attribute_size_bytes` | 40 KiB | Serialized size of indexed search-attribute metadata |
22+
23+
All limits are enforced at the point of scheduling or recording. A value of `0` disables the check for that limit kind.
24+
25+
## Configuration
26+
27+
Override any limit through `workflows.v2.structural_limits` in your config or via environment variables:
28+
29+
```php
30+
// config/workflows.php
31+
'v2' => [
32+
'structural_limits' => [
33+
'pending_activity_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_ACTIVITIES', 2000),
34+
'pending_child_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_CHILDREN', 1000),
35+
'pending_timer_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_TIMERS', 2000),
36+
'pending_signal_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_SIGNALS', 5000),
37+
'pending_update_count' => (int) env('WORKFLOW_V2_LIMIT_PENDING_UPDATES', 500),
38+
'command_batch_size' => (int) env('WORKFLOW_V2_LIMIT_COMMAND_BATCH_SIZE', 1000),
39+
'payload_size_bytes' => (int) env('WORKFLOW_V2_LIMIT_PAYLOAD_SIZE_BYTES', 2097152),
40+
'memo_size_bytes' => (int) env('WORKFLOW_V2_LIMIT_MEMO_SIZE_BYTES', 262144),
41+
'search_attribute_size_bytes' => (int) env('WORKFLOW_V2_LIMIT_SEARCH_ATTRIBUTE_SIZE_BYTES', 40960),
42+
],
43+
],
44+
```
45+
46+
## Enforcement points
47+
48+
### Pending count limits
49+
50+
Before the executor schedules an activity, child workflow, or timer, it counts the currently non-terminal items of that type on the run. If the count is already at or above the configured limit, the run fails immediately with a `StructuralLimitExceededException`.
51+
52+
This protects against patterns like unbounded parallel fan-out loops that accumulate thousands of pending operations:
53+
54+
```php
55+
// This will fail if $items exceeds the pending_activity_count limit
56+
$calls = [];
57+
foreach ($items as $item) {
58+
$calls[] = startActivity(ProcessItemActivity::class, $item);
59+
}
60+
return all($calls); // Also checked against command_batch_size
61+
```
62+
63+
To handle large batches within the limits, process items in bounded chunks:
64+
65+
```php
66+
foreach (array_chunk($items, 500) as $chunk) {
67+
$calls = [];
68+
foreach ($chunk as $item) {
69+
$calls[] = startActivity(ProcessItemActivity::class, $item);
70+
}
71+
all($calls);
72+
}
73+
```
74+
75+
### Command batch size
76+
77+
The `all()` and `parallel()` functions check the total number of leaf operations in a single fan-out group against `command_batch_size`. This is checked before any individual activities or children are scheduled, so the run fails cleanly rather than partially scheduling a batch.
78+
79+
### Payload size limits
80+
81+
Payload size checks apply to serialized argument data. When a serialized payload exceeds the configured `payload_size_bytes`, the engine rejects the operation.
82+
83+
### Metadata size limits
84+
85+
Memo and search-attribute metadata are checked against `memo_size_bytes` and `search_attribute_size_bytes` respectively when upserting metadata.
86+
87+
## Failure taxonomy
88+
89+
When a structural limit is exceeded, the engine records:
90+
91+
- A `WorkflowFailure` row with `failure_category = structural_limit`
92+
- A `WorkflowFailed` history event with:
93+
- `failure_category = structural_limit`
94+
- `structural_limit_kind` — the specific limit that was exceeded (e.g. `pending_activity_count`, `command_batch_size`)
95+
- `structural_limit_value` — the current count or size that triggered the limit
96+
- `structural_limit_configured` — the configured ceiling
97+
98+
This metadata is machine-readable, so operators, Waterline, and external tooling can identify the root cause without parsing free-text messages.
99+
100+
## Health check
101+
102+
The current structural limits configuration is included in the v2 health check snapshot under `structural_limits`, making the active ceilings visible to operators:
103+
104+
```json
105+
{
106+
"structural_limits": {
107+
"pending_activity_count": 2000,
108+
"pending_child_count": 1000,
109+
"pending_timer_count": 2000,
110+
"pending_signal_count": 5000,
111+
"pending_update_count": 500,
112+
"command_batch_size": 1000,
113+
"payload_size_bytes": 2097152,
114+
"memo_size_bytes": 262144,
115+
"search_attribute_size_bytes": 40960
116+
}
117+
}
118+
```
119+
120+
## Waterline
121+
122+
Waterline surfaces structural-limit failures in the exceptions table with the `structural_limit` failure category. The timeline failure details include the limit kind, current value, and configured ceiling.

docs/failures-and-recovery.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Every failure recorded by the engine carries a `failure_category` that classifie
8585
| Timeout | `timeout` | Failure caused by a timeout expiration — enforced by the engine when a workflow execution or run deadline passes. |
8686
| Task Failure | `task_failure` | Workflow-task execution failure such as replay errors, determinism violations, or invalid command shapes. |
8787
| Internal | `internal` | Server or infrastructure failure (database, queue, worker crash). |
88+
| Structural Limit | `structural_limit` | Failure caused by exceeding a structural limit (payload size, pending fan-out count, command batch size, metadata size). See [Structural Limits](/constraints/structural-limits). |
8889

8990
The category is determined automatically when the failure is recorded:
9091

@@ -95,6 +96,7 @@ The category is determined automatically when the failure is recorded:
9596
- Determinism violations (`UnsupportedWorkflowYieldException`, `StraightLineWorkflowRequiredException`) classify as `task_failure`.
9697
- Infrastructure exceptions (database/PDO errors, queue max-attempts exceeded) classify as `internal`.
9798
- Timeout-indicating exceptions (messages containing "timed out", "timeout exceeded", "execution deadline", or "run deadline") classify as `timeout`.
99+
- Structural-limit exceptions (`StructuralLimitExceededException`) classify as `structural_limit`. The history event also carries `structural_limit_kind`, `structural_limit_value`, and `structural_limit_configured` metadata.
98100
- All other business-logic exceptions default to `application`.
99101
- External worker failures (submitted through the workflow task bridge HTTP protocol) use the same classification rules based on the exception class name and message strings, even though the original throwable is not available in the host process.
100102

docs/features/timeouts.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,7 @@ Waterline displays the retry policy including all configured timeout types in th
178178
The following are planned but not yet implemented:
179179

180180
- Retry policies at the workflow level
181-
- Typed structural-limit failures for payload size, history size, and pending fan-out
181+
182+
### Structural limits
183+
184+
Typed structural-limit failures for payload size, pending fan-out counts, and metadata size ceilings are enforced by the engine. See [Structural Limits](/constraints/structural-limits) for the full limit contract, configuration, and failure taxonomy.

0 commit comments

Comments
 (0)