Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit de44dba

Browse files
authored
fix(workflow): Propagate scope cancellation while waiting on ExternalWorkflowHandle cancellation (temporalio#633)
* fix(workflow)!: Propagate scope cancellation while waiting on ExternalWorkflowHandle cancellation * Avoid breaking history compatibility * Try to avoid some CI failures waiting for verdaccio startup * Don't use markers unnecessarily and address CR comments
1 parent 3705ba9 commit de44dba

File tree

5 files changed

+89
-6
lines changed

5 files changed

+89
-6
lines changed

packages/test/src/test-workflows.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1803,3 +1803,28 @@ test('waitOnUser', async (t) => {
18031803
compareCompletion(t, completion, makeSuccess());
18041804
}
18051805
});
1806+
1807+
test('scopeCancelledWhileWaitingOnExternalWorkflowCancellation', async (t) => {
1808+
const { workflowType } = t.context;
1809+
{
1810+
const completion = await activate(t, makeStartWorkflow(workflowType));
1811+
compareCompletion(
1812+
t,
1813+
completion,
1814+
makeSuccess([
1815+
{
1816+
requestCancelExternalWorkflowExecution: {
1817+
seq: 1,
1818+
workflowExecution: { namespace: 'default', workflowId: 'irrelevant' },
1819+
},
1820+
},
1821+
{
1822+
setPatchMarker: { deprecated: false, patchId: '__temporal_internal_connect_external_handle_cancel_to_scope' },
1823+
},
1824+
{
1825+
completeWorkflowExecution: { result: defaultPayloadConverter.toPayload(undefined) },
1826+
},
1827+
])
1828+
);
1829+
}
1830+
});

packages/test/src/workflows/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export * from './random';
6161
export * from './reject-promise';
6262
export * from './run-activity-in-different-task-queue';
6363
export * from './set-timeout-after-microtasks';
64+
export * from './scope-cancelled-while-waiting-on-external-workflow-cancellation';
6465
export * from './shared-promise-scopes';
6566
export * from './shield-awaited-in-root-scope';
6667
export * from './shield-in-shield';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Tests that scope cancellation is propagated to the workflow while waiting on
3+
* external workflow cancellation (`await ExternalWorkflowHandle.cancel()`).
4+
*
5+
* This test was added along with the behavior fix where waiting for external
6+
* workflow cancellation was the only framework generated promise that was not
7+
* connected to a a cancellation scope.
8+
*
9+
* @module
10+
*/
11+
import { CancelledFailure } from '@temporalio/common';
12+
import { CancellationScope, getExternalWorkflowHandle } from '@temporalio/workflow';
13+
14+
export async function scopeCancelledWhileWaitingOnExternalWorkflowCancellation(): Promise<void> {
15+
try {
16+
await CancellationScope.cancellable(async () => {
17+
const handle = getExternalWorkflowHandle('irrelevant');
18+
const promise = handle.cancel();
19+
CancellationScope.current().cancel();
20+
await promise;
21+
throw new Error('External cancellation was not cancelled');
22+
});
23+
throw new Error('Expected CancellationScope to throw CancelledFailure');
24+
} catch (err) {
25+
if (!(err instanceof CancelledFailure)) {
26+
throw err;
27+
}
28+
}
29+
}

packages/workflow/src/workflow.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,9 @@ export function proxyLocalActivities<A extends ActivityInterface>(options: Local
532532
) as any;
533533
}
534534

535+
// TODO: deprecate this patch after "enough" time has passed
536+
const EXTERNAL_WF_CANCEL_PATCH = '__temporal_internal_connect_external_handle_cancel_to_scope';
537+
535538
/**
536539
* Returns a client-side handle that can be used to signal and cancel an existing Workflow execution.
537540
* It takes a Workflow ID and optional run ID.
@@ -545,6 +548,28 @@ export function getExternalWorkflowHandle(workflowId: string, runId?: string): E
545548
if (state.info === undefined) {
546549
throw new IllegalStateError('Uninitialized workflow');
547550
}
551+
552+
// Connect this cancel operation to the current cancellation scope.
553+
// This is behavior was introduced after v0.22.0 and is incompatible
554+
// with histories generated with previous SDK versions and thus requires
555+
// patching.
556+
//
557+
// We try to delay patching as much as possible to avoid polluting
558+
// histories unless strictly required.
559+
const scope = CancellationScope.current();
560+
if (scope.cancellable) {
561+
scope.cancelRequested.catch((err) => {
562+
if (patched(EXTERNAL_WF_CANCEL_PATCH)) {
563+
reject(err);
564+
}
565+
});
566+
}
567+
if (scope.consideredCancelled) {
568+
if (patched(EXTERNAL_WF_CANCEL_PATCH)) {
569+
return;
570+
}
571+
}
572+
548573
const seq = state.nextSeqs.cancelWorkflow++;
549574
state.pushCommand({
550575
requestCancelExternalWorkflowExecution: {
@@ -657,15 +682,9 @@ export async function startChild<T extends Workflow>(
657682
workflowId: optionsWithDefaults.workflowId,
658683
originalRunId,
659684
result(): Promise<WorkflowResultType<T>> {
660-
if (completed === undefined) {
661-
throw new IllegalStateError('Child Workflow was not started');
662-
}
663685
return completed as any;
664686
},
665687
async signal<Args extends any[]>(def: SignalDefinition<Args> | string, ...args: Args): Promise<void> {
666-
if (started === undefined) {
667-
throw new IllegalStateError('Workflow execution not started');
668-
}
669688
return composeInterceptors(
670689
state.interceptors.outbound,
671690
'signalWorkflow',

scripts/registry.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,15 @@ class Registry {
5353
let contents;
5454
try {
5555
contents = await readFile(logPath, 'utf8');
56+
// Sometimes (mostly in Windows) tail can miss updates.
57+
// Use this workaround as a last resort to recover and avoid failing CI.
58+
const found = contents
59+
.split('\n')
60+
.map(JSON.parse)
61+
.find((parsed) => parsed.addr);
62+
if (found) {
63+
resolve();
64+
}
5665
} catch (e) {
5766
contents = `Error ${e}`;
5867
}

0 commit comments

Comments
 (0)