You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/design-run-in-child-context.md
+23-24Lines changed: 23 additions & 24 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -31,19 +31,21 @@ This aligns the Java SDK with the TypeScript and Python reference implementation
31
31
A child context is a `CONTEXT` operation in the checkpoint log. Its lifecycle:
32
32
33
33
1.**START** (fire-and-forget) — marks the child context as in-progress
34
-
2. Inner operations checkpoint with `parentId` set to the child context's ID
34
+
2. Inner operations checkpoint with `parentId` set to the child context's operation ID
35
35
3.**SUCCEED** or **FAIL** (blocking) — finalizes the child context
36
36
37
37
```
38
38
Op ID | Parent ID | Type | Action | Payload
39
39
------|-----------|---------|---------|--------
40
40
3 | null | CONTEXT | START | —
41
-
1 | 3 | STEP | START | —
42
-
1 | 3 | STEP | SUCCEED | "result"
41
+
3-1 | 3 | STEP | START | —
42
+
3-1 | 3 | STEP | SUCCEED | "result"
43
43
3 | null | CONTEXT | SUCCEED | "final result"
44
44
```
45
45
46
-
For nested child contexts, the `parentId` uses the qualified context path (e.g., `"3:2"` for a child context created as operation `"2"` inside parent context `"3"`).
46
+
Inner operation IDs are prefixed with the parent context's operation ID using `-` as separator (e.g., `"3-1"`, `"3-2"`). This matches the JavaScript SDK's `stepPrefix` convention and ensures operation IDs are globally unique — the backend validates type consistency by operation ID, so bare sequential IDs inside child contexts would collide with root-level operations.
47
+
48
+
For nested child contexts, the prefix chains naturally (e.g., `"3-2-1"` for the first operation inside a nested child context that is operation `"2"` inside parent context `"3"`).
47
49
48
50
### Replay behavior
49
51
@@ -54,28 +56,24 @@ For nested child contexts, the `parentId` uses the qualified context path (e.g.,
54
56
| FAILED | Re-throw cached error |
55
57
| STARTED | Re-execute (was interrupted mid-flight) |
56
58
57
-
### Operation scoping
58
-
59
-
Child contexts restart their operation counter at 1. To avoid ID collisions, `ExecutionManager` uses a composite key:
60
-
61
-
```java
62
-
record OperationKey(String parentId, String operationId) { ... }
63
-
```
64
-
65
-
This applies to the `operations` map, `openPhasers` map, and all checkpoint completion handlers. The backend's `ParentId` field on each `Operation` is the source of truth for scoping.
59
+
### Operation ID prefixing
66
60
67
-
### Nested context ID qualification
61
+
To ensure global uniqueness, `DurableContext.nextOperationId()` prefixes operation IDs with the context's `parentId` when inside a child context:
68
62
69
-
To prevent `OperationKey` collisions when sibling contexts at different nesting levels share the same local operation ID, child contexts build a globally unique `contextId` by qualifying with the parent's context path:
70
-
71
-
- Root-level child contexts use just their operation ID (e.g., `"3"`)
72
-
- Nested child contexts include the parent path (e.g., `"3:2"` for operation `"2"` inside parent context `"3"`)
63
+
- Root context: IDs are `"1"`, `"2"`, `"3"` (no prefix)
64
+
- Child context `"1"`: IDs are `"1-1"`, `"1-2"`, `"1-3"`
65
+
- Nested child context `"1-2"`: IDs are `"1-2-1"`, `"1-2-2"`
73
66
74
67
```java
75
-
var contextId = getParentId() !=null? getParentId() +":"+ getOperationId() : getOperationId();
68
+
privateString nextOperationId() {
69
+
var counter =String.valueOf(operationCounter.incrementAndGet());
This qualified `contextId` is used as the `parentId` for all operations within the child context.
74
+
This matches the JavaScript SDK's `_stepPrefix` mechanism. The backend validates type consistency by operation ID alone, so without prefixing, a CONTEXT operation with ID `"1"` and an inner STEP with ID `"1"` (different `parentId`) would trigger an `InvalidParameterValueException`.
75
+
76
+
`ExecutionManager` still uses plain `String` keys (the globally unique operation ID) for its internal maps, since prefixed IDs are inherently unique across all contexts.
79
77
80
78
### Thread model
81
79
@@ -95,13 +93,14 @@ The `DurableContext` stores its context identity in a `parentId` field — `null
95
93
96
94
| File | Change |
97
95
|------|--------|
98
-
|`ChildContextOperation` (new) | Extends `BaseDurableOperation<T>`. Manages child context lifecycle, thread coordination, large result handling. Builds qualified `contextId`for nested contexts (e.g., `"3:2"`). |
96
+
|`ChildContextOperation` (new) | Extends `BaseDurableOperation<T>`. Manages child context lifecycle, thread coordination, large result handling. Uses `getOperationId()` directly as `contextId`(already globally unique via prefixed IDs). |
99
97
|`ChildContextFailedException` (new) | Extends `DurableOperationException`. Wraps the `Operation` object; extracts error from `contextDetails()`. |
100
-
|`DurableContext`| New `runInChildContext`/`runInChildContextAsync` methods. New `createChildContext` factory (skips thread registration). Stores `parentId` field (null for root, contextId for child). Per-context replay tracking via `isReplaying` field. |
98
+
|`DurableContext`| New `runInChildContext`/`runInChildContextAsync` methods. New `createChildContext` factory (skips thread registration). Stores `parentId` field (null for root, contextId for child). Per-context replay tracking via `isReplaying` field. `nextOperationId()` prefixes with `parentId` for child contexts (e.g., `"1-1"`). |
101
99
|`BaseDurableOperation`| New `parentId` constructor parameter. `sendOperationUpdateAsync` uses it instead of hardcoded `null`. Protected `getParentId()` getter. |
102
-
|`ExecutionManager`|`OperationKey` record for composite keys. All maps (`operations`, `openPhasers`) use composite keys. `getOperationAndUpdateReplayState`accepts `parentId`. `startPhaser`takes `parentId` +`operationId`. New `hasOperationsForContext(parentId)` method for per-context replay initialization. |
103
-
|`StepOperation`| Thread ID includes parent context: `(parentId != null ? parentId + ":" : "") + operationId + "-step"`|
100
+
|`ExecutionManager`| All maps (`operations`, `openPhasers`) use plain `String`keys (globally unique operation IDs). `getOperationAndUpdateReplayState`and `startPhaser`take a single`operationId` argument. New `hasOperationsForContext(parentId)` method for per-context replay initialization. |
101
+
|`StepOperation`| Thread ID uses `getOperationId() + "-step"` (operation IDs are globally unique via prefixing).|
104
102
|`LocalMemoryExecutionClient`| Handles `CONTEXT` operations (was `throw UnsupportedOperationException`). Propagates `parentId` for all operation types. |
0 commit comments