Skip to content

Commit 990d725

Browse files
Make CancellationScopeImpl more deterministic
1 parent e5bb3a5 commit 990d725

3 files changed

Lines changed: 300 additions & 5 deletions

File tree

temporal-sdk/src/main/java/io/temporal/internal/sync/CancellationScopeImpl.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package io.temporal.internal.sync;
22

33
import io.temporal.workflow.*;
4-
import java.util.ArrayDeque;
5-
import java.util.Deque;
6-
import java.util.HashSet;
7-
import java.util.Set;
4+
import java.util.*;
85

96
class CancellationScopeImpl implements CancellationScope {
107

@@ -36,7 +33,8 @@ private static void popCurrent(CancellationScopeImpl expected) {
3633

3734
private final Runnable runnable;
3835
private CancellationScopeImpl parent;
39-
private final Set<CancellationScopeImpl> children = new HashSet<>();
36+
// We use a LinkedHashSet because we will iterate through the children, so we need to keep a deterministic order.
37+
private final Set<CancellationScopeImpl> children = new LinkedHashSet<>();
4038

4139
/**
4240
* When disconnected scope has no parent and thus doesn't receive cancellation requests from it.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package io.temporal.workflow.cancellationTests;
2+
3+
import io.temporal.activity.ActivityInterface;
4+
import io.temporal.activity.ActivityOptions;
5+
import io.temporal.client.WorkflowClient;
6+
import io.temporal.client.WorkflowStub;
7+
import io.temporal.common.WorkflowExecutionHistory;
8+
import io.temporal.testing.WorkflowReplayer;
9+
import io.temporal.testing.internal.SDKTestWorkflowRule;
10+
import io.temporal.workflow.*;
11+
import java.time.Duration;
12+
import org.junit.Rule;
13+
import org.junit.Test;
14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
16+
17+
public class WorkflowCancellationScopeDeterminism {
18+
private static final Logger log =
19+
LoggerFactory.getLogger(WorkflowCancellationScopeDeterminism.class);
20+
21+
@Rule
22+
public SDKTestWorkflowRule testWorkflowRule =
23+
SDKTestWorkflowRule.newBuilder()
24+
.setWorkflowTypes(TestWorkflowImpl.class)
25+
.setActivityImplementations(new TestActivityImpl())
26+
.setUseExternalService(true)
27+
.build();
28+
29+
@Test(timeout = 1000000)
30+
public void replayCanceledWorkflow() throws Exception {
31+
for (int i = 0; i < 1000; i++) {
32+
log.info("Running test iteration {}", i);
33+
TestWorkflow testWorkflow = testWorkflowRule.newWorkflowStub(TestWorkflow.class);
34+
35+
WorkflowClient.start(testWorkflow::start);
36+
37+
WorkflowStub stub = WorkflowStub.fromTyped(testWorkflow);
38+
stub.cancel();
39+
try {
40+
stub.getResult(Void.class);
41+
} catch (Exception e) {
42+
// ignore; just blocking to make sure workflow is actually finished
43+
}
44+
45+
WorkflowExecutionHistory history =
46+
testWorkflowRule
47+
.getWorkflowClient()
48+
.fetchHistory(stub.getExecution().getWorkflowId(), stub.getExecution().getRunId());
49+
WorkflowReplayer.replayWorkflowExecution(history, testWorkflowRule.getWorker());
50+
}
51+
}
52+
53+
@Test
54+
public void replayTest() throws Exception {
55+
WorkflowReplayer.replayWorkflowExecutionFromResource(
56+
"cancellationScopeDeterminism.json", TestWorkflowImpl.class);
57+
}
58+
59+
@WorkflowInterface
60+
public interface TestWorkflow {
61+
@WorkflowMethod
62+
void start();
63+
}
64+
65+
@ActivityInterface
66+
public interface TestActivity {
67+
void doActivity();
68+
}
69+
70+
public static class TestActivityImpl implements TestActivity {
71+
@Override
72+
public void doActivity() {
73+
try {
74+
Thread.sleep(5000);
75+
} catch (InterruptedException e) {
76+
throw new RuntimeException(e);
77+
}
78+
}
79+
}
80+
81+
public static class TestWorkflowImpl implements TestWorkflow {
82+
83+
TestActivity activity =
84+
Workflow.newActivityStub(
85+
TestActivity.class,
86+
ActivityOptions.newBuilder().setScheduleToCloseTimeout(Duration.ofSeconds(60)).build());
87+
88+
@Override
89+
public void start() {
90+
CancellationScope scope = Workflow.newCancellationScope(() -> activity.doActivity());
91+
92+
Async.procedure(
93+
() -> {
94+
Workflow.sleep(Duration.ofMinutes(5));
95+
});
96+
97+
scope.run();
98+
}
99+
}
100+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
{
2+
"events": [
3+
{
4+
"eventId": "1",
5+
"eventTime": "2025-05-07T16:31:45.402827Z",
6+
"eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED",
7+
"taskId": "4053782",
8+
"workflowExecutionStartedEventAttributes": {
9+
"workflowType": {
10+
"name": "TestWorkflow"
11+
},
12+
"taskQueue": {
13+
"name": "WorkflowTest-replayCanceledWorkflow-402be3d9-0bf9-41f3-a3df-301df10d22d8",
14+
"kind": "TASK_QUEUE_KIND_NORMAL"
15+
},
16+
"workflowExecutionTimeout": "0s",
17+
"workflowRunTimeout": "0s",
18+
"workflowTaskTimeout": "10s",
19+
"originalExecutionRunId": "0196ab96-befa-7c96-8adf-f3801ed7a09e",
20+
"identity": "93896@Quinn-Klassens-MacBook-Pro.local",
21+
"firstExecutionRunId": "0196ab96-befa-7c96-8adf-f3801ed7a09e",
22+
"attempt": 1,
23+
"firstWorkflowTaskBackoff": "0s",
24+
"header": {},
25+
"workflowId": "03e7349c-652f-4a32-a3b2-9015bc5cc7fe"
26+
}
27+
},
28+
{
29+
"eventId": "2",
30+
"eventTime": "2025-05-07T16:31:45.402919Z",
31+
"eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED",
32+
"taskId": "4053783",
33+
"workflowTaskScheduledEventAttributes": {
34+
"taskQueue": {
35+
"name": "WorkflowTest-replayCanceledWorkflow-402be3d9-0bf9-41f3-a3df-301df10d22d8",
36+
"kind": "TASK_QUEUE_KIND_NORMAL"
37+
},
38+
"startToCloseTimeout": "10s",
39+
"attempt": 1
40+
}
41+
},
42+
{
43+
"eventId": "3",
44+
"eventTime": "2025-05-07T16:31:45.412234Z",
45+
"eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED",
46+
"taskId": "4053788",
47+
"workflowTaskStartedEventAttributes": {
48+
"scheduledEventId": "2",
49+
"identity": "93896@Quinn-Klassens-MacBook-Pro.local",
50+
"requestId": "d5db764e-cb78-43f7-b0fb-3e2c2d085c83",
51+
"historySizeBytes": "407"
52+
}
53+
},
54+
{
55+
"eventId": "4",
56+
"eventTime": "2025-05-07T16:31:45.541892Z",
57+
"eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED",
58+
"taskId": "4053792",
59+
"workflowTaskCompletedEventAttributes": {
60+
"scheduledEventId": "2",
61+
"startedEventId": "3",
62+
"identity": "93896@Quinn-Klassens-MacBook-Pro.local",
63+
"workerVersion": {},
64+
"sdkMetadata": {
65+
"langUsedFlags": [
66+
1
67+
],
68+
"sdkName": "temporal-java",
69+
"sdkVersion": "1.23.0"
70+
},
71+
"meteringMetadata": {}
72+
}
73+
},
74+
{
75+
"eventId": "5",
76+
"eventTime": "2025-05-07T16:31:45.541956Z",
77+
"eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED",
78+
"taskId": "4053793",
79+
"activityTaskScheduledEventAttributes": {
80+
"activityId": "627e725e-c872-319c-a1d4-d03bb18bc572",
81+
"activityType": {
82+
"name": "DoActivity"
83+
},
84+
"taskQueue": {
85+
"name": "WorkflowTest-replayCanceledWorkflow-402be3d9-0bf9-41f3-a3df-301df10d22d8",
86+
"kind": "TASK_QUEUE_KIND_NORMAL"
87+
},
88+
"header": {},
89+
"scheduleToCloseTimeout": "60s",
90+
"scheduleToStartTimeout": "60s",
91+
"startToCloseTimeout": "60s",
92+
"heartbeatTimeout": "0s",
93+
"workflowTaskCompletedEventId": "4",
94+
"retryPolicy": {
95+
"initialInterval": "1s",
96+
"backoffCoefficient": 2,
97+
"maximumInterval": "100s"
98+
}
99+
}
100+
},
101+
{
102+
"eventId": "6",
103+
"eventTime": "2025-05-07T16:31:45.541989Z",
104+
"eventType": "EVENT_TYPE_TIMER_STARTED",
105+
"taskId": "4053794",
106+
"timerStartedEventAttributes": {
107+
"timerId": "47c34e95-d85f-3834-80ed-8b58cebe2289",
108+
"startToFireTimeout": "300s",
109+
"workflowTaskCompletedEventId": "4"
110+
}
111+
},
112+
{
113+
"eventId": "7",
114+
"eventTime": "2025-05-07T16:31:45.416574Z",
115+
"eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_CANCEL_REQUESTED",
116+
"taskId": "4053795",
117+
"workflowExecutionCancelRequestedEventAttributes": {
118+
"identity": "93896@Quinn-Klassens-MacBook-Pro.local"
119+
}
120+
},
121+
{
122+
"eventId": "8",
123+
"eventTime": "2025-05-07T16:31:45.542001Z",
124+
"eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED",
125+
"taskId": "4053796",
126+
"workflowTaskScheduledEventAttributes": {
127+
"taskQueue": {
128+
"name": "93896@Quinn-Klassens-MacBook-Pro.local:8bb44ec5-1202-4757-a16d-ef65d158c965",
129+
"kind": "TASK_QUEUE_KIND_STICKY",
130+
"normalName": "WorkflowTest-replayCanceledWorkflow-402be3d9-0bf9-41f3-a3df-301df10d22d8"
131+
},
132+
"startToCloseTimeout": "10s",
133+
"attempt": 1
134+
}
135+
},
136+
{
137+
"eventId": "9",
138+
"eventTime": "2025-05-07T16:31:45.547789Z",
139+
"eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED",
140+
"taskId": "4053804",
141+
"workflowTaskStartedEventAttributes": {
142+
"scheduledEventId": "8",
143+
"identity": "93896@Quinn-Klassens-MacBook-Pro.local",
144+
"requestId": "c2a474e4-b040-4a90-bb0d-b8bb81f1f827",
145+
"historySizeBytes": "1148"
146+
}
147+
},
148+
{
149+
"eventId": "10",
150+
"eventTime": "2025-05-07T16:31:45.572751Z",
151+
"eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED",
152+
"taskId": "4053810",
153+
"workflowTaskCompletedEventAttributes": {
154+
"scheduledEventId": "8",
155+
"startedEventId": "9",
156+
"identity": "93896@Quinn-Klassens-MacBook-Pro.local",
157+
"workerVersion": {},
158+
"sdkMetadata": {
159+
"sdkName": "temporal-java",
160+
"sdkVersion": "1.23.0"
161+
},
162+
"meteringMetadata": {}
163+
}
164+
},
165+
{
166+
"eventId": "11",
167+
"eventTime": "2025-05-07T16:31:45.572813Z",
168+
"eventType": "EVENT_TYPE_TIMER_CANCELED",
169+
"taskId": "4053811",
170+
"timerCanceledEventAttributes": {
171+
"timerId": "47c34e95-d85f-3834-80ed-8b58cebe2289",
172+
"startedEventId": "6",
173+
"workflowTaskCompletedEventId": "10",
174+
"identity": "93896@Quinn-Klassens-MacBook-Pro.local"
175+
}
176+
},
177+
{
178+
"eventId": "12",
179+
"eventTime": "2025-05-07T16:31:45.572824Z",
180+
"eventType": "EVENT_TYPE_ACTIVITY_TASK_CANCEL_REQUESTED",
181+
"taskId": "4053812",
182+
"activityTaskCancelRequestedEventAttributes": {
183+
"scheduledEventId": "5",
184+
"workflowTaskCompletedEventId": "10"
185+
}
186+
},
187+
{
188+
"eventId": "13",
189+
"eventTime": "2025-05-07T16:31:45.572835Z",
190+
"eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_CANCELED",
191+
"taskId": "4053813",
192+
"workflowExecutionCanceledEventAttributes": {
193+
"workflowTaskCompletedEventId": "10"
194+
}
195+
}
196+
]
197+
}

0 commit comments

Comments
 (0)