@temporalio/workflow 1.10.2 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/flags.d.ts +18 -0
- package/lib/flags.js +19 -1
- package/lib/flags.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/interfaces.d.ts +35 -13
- package/lib/interfaces.js.map +1 -1
- package/lib/internals.d.ts +58 -20
- package/lib/internals.js +151 -83
- package/lib/internals.js.map +1 -1
- package/lib/update-scope.d.ts +43 -0
- package/lib/update-scope.js +41 -0
- package/lib/update-scope.js.map +1 -0
- package/lib/worker-interface.d.ts +1 -8
- package/lib/worker-interface.js +13 -58
- package/lib/worker-interface.js.map +1 -1
- package/lib/workflow.d.ts +67 -8
- package/lib/workflow.js +108 -13
- package/lib/workflow.js.map +1 -1
- package/package.json +4 -4
- package/src/flags.ts +20 -1
- package/src/index.ts +3 -3
- package/src/interfaces.ts +41 -14
- package/src/internals.ts +192 -88
- package/src/update-scope.ts +67 -0
- package/src/worker-interface.ts +15 -64
- package/src/workflow.ts +118 -13
package/src/internals.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
arrayFromPayloads,
|
|
7
7
|
defaultPayloadConverter,
|
|
8
8
|
ensureTemporalFailure,
|
|
9
|
+
HandlerUnfinishedPolicy,
|
|
9
10
|
IllegalStateError,
|
|
10
11
|
TemporalFailure,
|
|
11
12
|
Workflow,
|
|
@@ -15,21 +16,24 @@ import {
|
|
|
15
16
|
WorkflowUpdateAnnotatedType,
|
|
16
17
|
ProtoFailure,
|
|
17
18
|
ApplicationFailure,
|
|
19
|
+
WorkflowUpdateType,
|
|
20
|
+
WorkflowUpdateValidatorType,
|
|
18
21
|
} from '@temporalio/common';
|
|
19
22
|
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
|
|
20
23
|
import { checkExtends } from '@temporalio/common/lib/type-helpers';
|
|
21
24
|
import type { coresdk, temporal } from '@temporalio/proto';
|
|
22
25
|
import { alea, RNG } from './alea';
|
|
23
26
|
import { RootCancellationScope } from './cancellation-scope';
|
|
27
|
+
import { UpdateScope } from './update-scope';
|
|
24
28
|
import { DeterminismViolationError, LocalActivityDoBackoff, isCancellation } from './errors';
|
|
25
29
|
import { QueryInput, SignalInput, UpdateInput, WorkflowExecuteInput, WorkflowInterceptors } from './interceptors';
|
|
26
30
|
import {
|
|
27
31
|
ContinueAsNew,
|
|
28
32
|
DefaultSignalHandler,
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
StackTraceSDKInfo,
|
|
34
|
+
StackTraceFileSlice,
|
|
31
35
|
EnhancedStackTrace,
|
|
32
|
-
|
|
36
|
+
StackTraceFileLocation,
|
|
33
37
|
WorkflowInfo,
|
|
34
38
|
WorkflowCreateOptionsInternal,
|
|
35
39
|
ActivationCompletion,
|
|
@@ -37,8 +41,8 @@ import {
|
|
|
37
41
|
import { type SinkCall } from './sinks';
|
|
38
42
|
import { untrackPromise } from './stack-helpers';
|
|
39
43
|
import pkg from './pkg';
|
|
40
|
-
import { executeWithLifecycleLogging } from './logs';
|
|
41
44
|
import { SdkFlag, assertValidFlag } from './flags';
|
|
45
|
+
import { executeWithLifecycleLogging, log } from './logs';
|
|
42
46
|
|
|
43
47
|
enum StartChildWorkflowExecutionFailedCause {
|
|
44
48
|
START_CHILD_WORKFLOW_EXECUTION_FAILED_CAUSE_UNSPECIFIED = 0,
|
|
@@ -50,7 +54,7 @@ checkExtends<StartChildWorkflowExecutionFailedCause, coresdk.child_workflow.Star
|
|
|
50
54
|
|
|
51
55
|
export interface Stack {
|
|
52
56
|
formatted: string;
|
|
53
|
-
structured:
|
|
57
|
+
structured: StackTraceFileLocation[];
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
/**
|
|
@@ -63,11 +67,13 @@ export interface PromiseStackStore {
|
|
|
63
67
|
|
|
64
68
|
export interface Completion {
|
|
65
69
|
resolve(val: unknown): unknown;
|
|
70
|
+
|
|
66
71
|
reject(reason: unknown): unknown;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
export interface Condition {
|
|
70
75
|
fn(): boolean;
|
|
76
|
+
|
|
71
77
|
resolve(): void;
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -82,10 +88,32 @@ export type ActivationHandler = {
|
|
|
82
88
|
[P in keyof coresdk.workflow_activation.IWorkflowActivationJob]: ActivationHandlerFunction<P>;
|
|
83
89
|
};
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Information about an update or signal handler execution.
|
|
93
|
+
*/
|
|
94
|
+
interface MessageHandlerExecution {
|
|
95
|
+
name: string;
|
|
96
|
+
unfinishedPolicy: HandlerUnfinishedPolicy;
|
|
97
|
+
id?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
/**
|
|
86
101
|
* Keeps all of the Workflow runtime state like pending completions for activities and timers.
|
|
87
102
|
*
|
|
88
103
|
* Implements handlers for all workflow activation jobs.
|
|
104
|
+
*
|
|
105
|
+
* Note that most methods in this class are meant to be called only from within the VM.
|
|
106
|
+
*
|
|
107
|
+
* However, a few methods may be called directly from outside the VM (essentially from `vm-shared.ts`).
|
|
108
|
+
* These methods are specifically marked with a comment and require careful consideration, as the
|
|
109
|
+
* execution context may not properly reflect that of the target workflow execution (e.g.: with Reusable
|
|
110
|
+
* VMs, the `global` may not have been swapped to those of that workflow execution; the active microtask
|
|
111
|
+
* queue may be that of the thread/process, rather than the queue of that VM context; etc). Consequently,
|
|
112
|
+
* methods that are meant to be called from outside of the VM must not do any of the following:
|
|
113
|
+
*
|
|
114
|
+
* - Access any global variable;
|
|
115
|
+
* - Create Promise objects, use async/await, or otherwise schedule microtasks;
|
|
116
|
+
* - Call user-defined functions, including any form of interceptor.
|
|
89
117
|
*/
|
|
90
118
|
export class Activator implements ActivationHandler {
|
|
91
119
|
/**
|
|
@@ -114,15 +142,6 @@ export class Activator implements ActivationHandler {
|
|
|
114
142
|
*/
|
|
115
143
|
readonly bufferedSignals = Array<coresdk.workflow_activation.ISignalWorkflow>();
|
|
116
144
|
|
|
117
|
-
/**
|
|
118
|
-
* Holds buffered query calls until a handler is registered.
|
|
119
|
-
*
|
|
120
|
-
* **IMPORTANT** queries are only buffered until workflow is started.
|
|
121
|
-
* This is required because async interceptors might block workflow function invocation
|
|
122
|
-
* which delays query handler registration.
|
|
123
|
-
*/
|
|
124
|
-
protected readonly bufferedQueries = Array<coresdk.workflow_activation.IQueryWorkflow>();
|
|
125
|
-
|
|
126
145
|
/**
|
|
127
146
|
* Mapping of update name to handler and validator
|
|
128
147
|
*/
|
|
@@ -133,6 +152,21 @@ export class Activator implements ActivationHandler {
|
|
|
133
152
|
*/
|
|
134
153
|
readonly signalHandlers = new Map<string, WorkflowSignalAnnotatedType>();
|
|
135
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Mapping of in-progress updates to handler execution information.
|
|
157
|
+
*/
|
|
158
|
+
readonly inProgressUpdates = new Map<string, MessageHandlerExecution>();
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Mapping of in-progress signals to handler execution information.
|
|
162
|
+
*/
|
|
163
|
+
readonly inProgressSignals = new Map<number, MessageHandlerExecution>();
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A sequence number providing unique identifiers for signal handler executions.
|
|
167
|
+
*/
|
|
168
|
+
protected signalHandlerExecutionSeq = 0;
|
|
169
|
+
|
|
136
170
|
/**
|
|
137
171
|
* A signal handler that catches calls for non-registered signal names.
|
|
138
172
|
*/
|
|
@@ -175,19 +209,19 @@ export class Activator implements ActivationHandler {
|
|
|
175
209
|
{
|
|
176
210
|
handler: (): EnhancedStackTrace => {
|
|
177
211
|
const { sourceMap } = this;
|
|
178
|
-
const sdk:
|
|
212
|
+
const sdk: StackTraceSDKInfo = { name: 'typescript', version: pkg.version };
|
|
179
213
|
const stacks = this.getStackTraces().map(({ structured: locations }) => ({ locations }));
|
|
180
|
-
const sources: Record<string,
|
|
214
|
+
const sources: Record<string, StackTraceFileSlice[]> = {};
|
|
181
215
|
if (this.showStackTraceSources) {
|
|
182
216
|
for (const { locations } of stacks) {
|
|
183
|
-
for (const {
|
|
184
|
-
if (!
|
|
185
|
-
const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(
|
|
217
|
+
for (const { file_path } of locations) {
|
|
218
|
+
if (!file_path) continue;
|
|
219
|
+
const content = sourceMap?.sourcesContent?.[sourceMap?.sources.indexOf(file_path)];
|
|
186
220
|
if (!content) continue;
|
|
187
|
-
sources[
|
|
221
|
+
sources[file_path] = [
|
|
188
222
|
{
|
|
223
|
+
line_offset: 0,
|
|
189
224
|
content,
|
|
190
|
-
lineOffset: 0,
|
|
191
225
|
},
|
|
192
226
|
];
|
|
193
227
|
}
|
|
@@ -218,7 +252,6 @@ export class Activator implements ActivationHandler {
|
|
|
218
252
|
return {
|
|
219
253
|
definition: {
|
|
220
254
|
type: workflowType,
|
|
221
|
-
description: null, // For now, do not set the workflow description in the TS SDK.
|
|
222
255
|
queryDefinitions,
|
|
223
256
|
signalDefinitions,
|
|
224
257
|
updateDefinitions,
|
|
@@ -233,7 +266,11 @@ export class Activator implements ActivationHandler {
|
|
|
233
266
|
/**
|
|
234
267
|
* Loaded in {@link initRuntime}
|
|
235
268
|
*/
|
|
236
|
-
public readonly interceptors: Required<WorkflowInterceptors> = {
|
|
269
|
+
public readonly interceptors: Required<WorkflowInterceptors> = {
|
|
270
|
+
inbound: [],
|
|
271
|
+
outbound: [],
|
|
272
|
+
internals: [],
|
|
273
|
+
};
|
|
237
274
|
|
|
238
275
|
/**
|
|
239
276
|
* Buffer that stores all generated commands, reset after each activation
|
|
@@ -259,13 +296,6 @@ export class Activator implements ActivationHandler {
|
|
|
259
296
|
*/
|
|
260
297
|
protected cancelled = false;
|
|
261
298
|
|
|
262
|
-
/**
|
|
263
|
-
* This is tracked to allow buffering queries until a workflow function is called.
|
|
264
|
-
* TODO(bergundy): I don't think this makes sense since queries run last in an activation and must be responded to in
|
|
265
|
-
* the same activation.
|
|
266
|
-
*/
|
|
267
|
-
protected workflowFunctionWasCalled = false;
|
|
268
|
-
|
|
269
299
|
/**
|
|
270
300
|
* The next (incremental) sequence to assign when generating completable commands
|
|
271
301
|
*/
|
|
@@ -282,6 +312,7 @@ export class Activator implements ActivationHandler {
|
|
|
282
312
|
|
|
283
313
|
/**
|
|
284
314
|
* This is set every time the workflow executes an activation
|
|
315
|
+
* May be accessed and modified from outside the VM.
|
|
285
316
|
*/
|
|
286
317
|
now: number;
|
|
287
318
|
|
|
@@ -292,6 +323,7 @@ export class Activator implements ActivationHandler {
|
|
|
292
323
|
|
|
293
324
|
/**
|
|
294
325
|
* Information about the current Workflow
|
|
326
|
+
* May be accessed from outside the VM.
|
|
295
327
|
*/
|
|
296
328
|
public info: WorkflowInfo;
|
|
297
329
|
|
|
@@ -333,6 +365,7 @@ export class Activator implements ActivationHandler {
|
|
|
333
365
|
showStackTraceSources,
|
|
334
366
|
sourceMap,
|
|
335
367
|
getTimeOfDay,
|
|
368
|
+
sdkFlags,
|
|
336
369
|
randomnessSeed,
|
|
337
370
|
patches,
|
|
338
371
|
registeredActivityNames,
|
|
@@ -345,13 +378,15 @@ export class Activator implements ActivationHandler {
|
|
|
345
378
|
this.random = alea(randomnessSeed);
|
|
346
379
|
this.registeredActivityNames = registeredActivityNames;
|
|
347
380
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
381
|
+
this.addKnownFlags(sdkFlags);
|
|
382
|
+
for (const patchId of patches) {
|
|
383
|
+
this.notifyHasPatch({ patchId });
|
|
352
384
|
}
|
|
353
385
|
}
|
|
354
386
|
|
|
387
|
+
/**
|
|
388
|
+
* May be invoked from outside the VM.
|
|
389
|
+
*/
|
|
355
390
|
mutateWorkflowInfo(fn: (info: WorkflowInfo) => WorkflowInfo): void {
|
|
356
391
|
this.info = fn(this.info);
|
|
357
392
|
}
|
|
@@ -378,6 +413,9 @@ export class Activator implements ActivationHandler {
|
|
|
378
413
|
return [...stacks].map(([_, stack]) => stack);
|
|
379
414
|
}
|
|
380
415
|
|
|
416
|
+
/**
|
|
417
|
+
* May be invoked from outside the VM.
|
|
418
|
+
*/
|
|
381
419
|
getAndResetSinkCalls(): SinkCall[] {
|
|
382
420
|
const { sinkCalls } = this;
|
|
383
421
|
this.sinkCalls = [];
|
|
@@ -390,8 +428,6 @@ export class Activator implements ActivationHandler {
|
|
|
390
428
|
* Prevents commands from being added after Workflow completion.
|
|
391
429
|
*/
|
|
392
430
|
pushCommand(cmd: coresdk.workflow_commands.IWorkflowCommand, complete = false): void {
|
|
393
|
-
// Only query responses may be sent after completion
|
|
394
|
-
if (this.completed && !cmd.respondToQuery) return;
|
|
395
431
|
this.commands.push(cmd);
|
|
396
432
|
if (complete) {
|
|
397
433
|
this.completed = true;
|
|
@@ -410,19 +446,7 @@ export class Activator implements ActivationHandler {
|
|
|
410
446
|
if (workflow === undefined) {
|
|
411
447
|
throw new IllegalStateError('Workflow uninitialized');
|
|
412
448
|
}
|
|
413
|
-
|
|
414
|
-
try {
|
|
415
|
-
promise = workflow(...args);
|
|
416
|
-
} finally {
|
|
417
|
-
// Queries must be handled even if there was an exception when invoking the Workflow function.
|
|
418
|
-
this.workflowFunctionWasCalled = true;
|
|
419
|
-
// Empty the buffer
|
|
420
|
-
const buffer = this.bufferedQueries.splice(0);
|
|
421
|
-
for (const activation of buffer) {
|
|
422
|
-
this.queryWorkflow(activation);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
return await promise;
|
|
449
|
+
return await workflow(...args);
|
|
426
450
|
}
|
|
427
451
|
|
|
428
452
|
public startWorkflow(activation: coresdk.workflow_activation.IStartWorkflow): void {
|
|
@@ -553,11 +577,6 @@ export class Activator implements ActivationHandler {
|
|
|
553
577
|
}
|
|
554
578
|
|
|
555
579
|
public queryWorkflow(activation: coresdk.workflow_activation.IQueryWorkflow): void {
|
|
556
|
-
if (!this.workflowFunctionWasCalled) {
|
|
557
|
-
this.bufferedQueries.push(activation);
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
580
|
const { queryType, queryId, headers } = activation;
|
|
562
581
|
if (!(queryType && queryId)) {
|
|
563
582
|
throw new TypeError('Missing query activation attributes');
|
|
@@ -590,7 +609,8 @@ export class Activator implements ActivationHandler {
|
|
|
590
609
|
if (!protocolInstanceId) {
|
|
591
610
|
throw new TypeError('Missing activation update protocolInstanceId');
|
|
592
611
|
}
|
|
593
|
-
|
|
612
|
+
const entry = this.updateHandlers.get(name);
|
|
613
|
+
if (!entry) {
|
|
594
614
|
this.bufferedUpdates.push(activation);
|
|
595
615
|
return;
|
|
596
616
|
}
|
|
@@ -629,25 +649,31 @@ export class Activator implements ActivationHandler {
|
|
|
629
649
|
//
|
|
630
650
|
// Note that there is a deliberately unhandled promise rejection below.
|
|
631
651
|
// These are caught elsewhere and fail the corresponding activation.
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
652
|
+
const doUpdateImpl = async () => {
|
|
653
|
+
let input: UpdateInput;
|
|
654
|
+
try {
|
|
655
|
+
if (runValidator && entry.validator) {
|
|
656
|
+
const validate = composeInterceptors(
|
|
657
|
+
this.interceptors.inbound,
|
|
658
|
+
'validateUpdate',
|
|
659
|
+
this.validateUpdateNextHandler.bind(this, entry.validator)
|
|
660
|
+
);
|
|
661
|
+
validate(makeInput());
|
|
662
|
+
}
|
|
663
|
+
input = makeInput();
|
|
664
|
+
} catch (error) {
|
|
665
|
+
this.rejectUpdate(protocolInstanceId, error);
|
|
666
|
+
return;
|
|
641
667
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
execute(input)
|
|
668
|
+
this.acceptUpdate(protocolInstanceId);
|
|
669
|
+
const execute = composeInterceptors(
|
|
670
|
+
this.interceptors.inbound,
|
|
671
|
+
'handleUpdate',
|
|
672
|
+
this.updateNextHandler.bind(this, entry.handler)
|
|
673
|
+
);
|
|
674
|
+
const { unfinishedPolicy } = entry;
|
|
675
|
+
this.inProgressUpdates.set(updateId, { name, unfinishedPolicy, id: updateId });
|
|
676
|
+
const res = execute(input)
|
|
651
677
|
.then((result) => this.completeUpdate(protocolInstanceId, result))
|
|
652
678
|
.catch((error) => {
|
|
653
679
|
if (error instanceof TemporalFailure) {
|
|
@@ -656,20 +682,18 @@ export class Activator implements ActivationHandler {
|
|
|
656
682
|
throw error;
|
|
657
683
|
}
|
|
658
684
|
})
|
|
659
|
-
|
|
685
|
+
.finally(() => this.inProgressUpdates.delete(updateId));
|
|
686
|
+
untrackPromise(res);
|
|
687
|
+
return res;
|
|
688
|
+
};
|
|
689
|
+
untrackPromise(UpdateScope.updateWithInfo(updateId, name, doUpdateImpl));
|
|
660
690
|
}
|
|
661
691
|
|
|
662
|
-
protected async updateNextHandler(
|
|
663
|
-
const entry = this.updateHandlers.get(name);
|
|
664
|
-
if (!entry) {
|
|
665
|
-
return Promise.reject(new IllegalStateError(`No registered update handler for update: ${name}`));
|
|
666
|
-
}
|
|
667
|
-
const { handler } = entry;
|
|
692
|
+
protected async updateNextHandler(handler: WorkflowUpdateType, { args }: UpdateInput): Promise<unknown> {
|
|
668
693
|
return await handler(...args);
|
|
669
694
|
}
|
|
670
695
|
|
|
671
|
-
protected validateUpdateNextHandler(
|
|
672
|
-
const { validator } = this.updateHandlers.get(name) ?? {};
|
|
696
|
+
protected validateUpdateNextHandler(validator: WorkflowUpdateValidatorType | undefined, { args }: UpdateInput): void {
|
|
673
697
|
if (validator) {
|
|
674
698
|
validator(...args);
|
|
675
699
|
}
|
|
@@ -723,6 +747,14 @@ export class Activator implements ActivationHandler {
|
|
|
723
747
|
return;
|
|
724
748
|
}
|
|
725
749
|
|
|
750
|
+
// If we fall through to the default signal handler then the unfinished
|
|
751
|
+
// policy is WARN_AND_ABANDON; users currently have no way to silence any
|
|
752
|
+
// ensuing warnings.
|
|
753
|
+
const unfinishedPolicy =
|
|
754
|
+
this.signalHandlers.get(signalName)?.unfinishedPolicy ?? HandlerUnfinishedPolicy.WARN_AND_ABANDON;
|
|
755
|
+
|
|
756
|
+
const signalExecutionNum = this.signalHandlerExecutionSeq++;
|
|
757
|
+
this.inProgressSignals.set(signalExecutionNum, { name: signalName, unfinishedPolicy });
|
|
726
758
|
const execute = composeInterceptors(
|
|
727
759
|
this.interceptors.inbound,
|
|
728
760
|
'handleSignal',
|
|
@@ -732,7 +764,9 @@ export class Activator implements ActivationHandler {
|
|
|
732
764
|
args: arrayFromPayloads(this.payloadConverter, activation.input),
|
|
733
765
|
signalName,
|
|
734
766
|
headers: headers ?? {},
|
|
735
|
-
})
|
|
767
|
+
})
|
|
768
|
+
.catch(this.handleWorkflowFailure.bind(this))
|
|
769
|
+
.finally(() => this.inProgressSignals.delete(signalExecutionNum));
|
|
736
770
|
}
|
|
737
771
|
|
|
738
772
|
public dispatchBufferedSignals(): void {
|
|
@@ -771,6 +805,24 @@ export class Activator implements ActivationHandler {
|
|
|
771
805
|
}
|
|
772
806
|
}
|
|
773
807
|
|
|
808
|
+
public warnIfUnfinishedHandlers(): void {
|
|
809
|
+
const getWarnable = (handlerExecutions: Iterable<MessageHandlerExecution>): MessageHandlerExecution[] => {
|
|
810
|
+
return Array.from(handlerExecutions).filter(
|
|
811
|
+
(ex) => ex.unfinishedPolicy === HandlerUnfinishedPolicy.WARN_AND_ABANDON
|
|
812
|
+
);
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
const warnableUpdates = getWarnable(this.inProgressUpdates.values());
|
|
816
|
+
if (warnableUpdates.length > 0) {
|
|
817
|
+
log.warn(makeUnfinishedUpdateHandlerMessage(warnableUpdates));
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const warnableSignals = getWarnable(this.inProgressSignals.values());
|
|
821
|
+
if (warnableSignals.length > 0) {
|
|
822
|
+
log.warn(makeUnfinishedSignalHandlerMessage(warnableSignals));
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
774
826
|
public updateRandomSeed(activation: coresdk.workflow_activation.IUpdateRandomSeed): void {
|
|
775
827
|
if (!activation.randomnessSeed) {
|
|
776
828
|
throw new TypeError('Expected activation with randomnessSeed attribute');
|
|
@@ -779,9 +831,9 @@ export class Activator implements ActivationHandler {
|
|
|
779
831
|
}
|
|
780
832
|
|
|
781
833
|
public notifyHasPatch(activation: coresdk.workflow_activation.INotifyHasPatch): void {
|
|
782
|
-
if (!
|
|
783
|
-
throw new
|
|
784
|
-
|
|
834
|
+
if (!this.info.unsafe.isReplaying)
|
|
835
|
+
throw new IllegalStateError('Unexpected notifyHasPatch job on non-replay activation');
|
|
836
|
+
if (!activation.patchId) throw new TypeError('notifyHasPatch missing patch id');
|
|
785
837
|
this.knownPresentPatches.add(activation.patchId);
|
|
786
838
|
}
|
|
787
839
|
|
|
@@ -801,7 +853,10 @@ export class Activator implements ActivationHandler {
|
|
|
801
853
|
return usePatch;
|
|
802
854
|
}
|
|
803
855
|
|
|
804
|
-
|
|
856
|
+
/**
|
|
857
|
+
* Called early while handling an activation to register known flags.
|
|
858
|
+
* May be invoked from outside the VM.
|
|
859
|
+
*/
|
|
805
860
|
public addKnownFlags(flags: number[]): void {
|
|
806
861
|
for (const flag of flags) {
|
|
807
862
|
assertValidFlag(flag);
|
|
@@ -809,6 +864,11 @@ export class Activator implements ActivationHandler {
|
|
|
809
864
|
}
|
|
810
865
|
}
|
|
811
866
|
|
|
867
|
+
/**
|
|
868
|
+
* Check if a flag is known to the Workflow Execution; if not, enable the flag if workflow
|
|
869
|
+
* is not replaying and the flag is configured to be enabled by default.
|
|
870
|
+
* May be invoked from outside the VM.
|
|
871
|
+
*/
|
|
812
872
|
public hasFlag(flag: SdkFlag): boolean {
|
|
813
873
|
if (this.knownFlags.has(flag.id)) {
|
|
814
874
|
return true;
|
|
@@ -839,7 +899,10 @@ export class Activator implements ActivationHandler {
|
|
|
839
899
|
// preventing it from completing.
|
|
840
900
|
throw error;
|
|
841
901
|
}
|
|
842
|
-
|
|
902
|
+
// Fail the workflow. We do not want to issue unfinishedHandlers warnings. To achieve that, we
|
|
903
|
+
// mark all handlers as completed now.
|
|
904
|
+
this.inProgressSignals.clear();
|
|
905
|
+
this.inProgressUpdates.clear();
|
|
843
906
|
this.pushCommand(
|
|
844
907
|
{
|
|
845
908
|
failWorkflowExecution: {
|
|
@@ -930,3 +993,44 @@ function getSeq<T extends { seq?: number | null }>(activation: T): number {
|
|
|
930
993
|
}
|
|
931
994
|
return seq;
|
|
932
995
|
}
|
|
996
|
+
|
|
997
|
+
function makeUnfinishedUpdateHandlerMessage(handlerExecutions: MessageHandlerExecution[]): string {
|
|
998
|
+
const message = `
|
|
999
|
+
[TMPRL1102] Workflow finished while an update handler was still running. This may have interrupted work that the
|
|
1000
|
+
update handler was doing, and the client that sent the update will receive a 'workflow execution
|
|
1001
|
+
already completed' RPCError instead of the update result. You can wait for all update and signal
|
|
1002
|
+
handlers to complete by using \`await workflow.condition(workflow.allHandlersFinished)\`.
|
|
1003
|
+
Alternatively, if both you and the clients sending the update are okay with interrupting running handlers
|
|
1004
|
+
when the workflow finishes, and causing clients to receive errors, then you can disable this warning by
|
|
1005
|
+
passing an option when setting the handler:
|
|
1006
|
+
\`workflow.setHandler(myUpdate, myUpdateHandler, {unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON});\`.`
|
|
1007
|
+
.replace(/\n/g, ' ')
|
|
1008
|
+
.trim();
|
|
1009
|
+
|
|
1010
|
+
return `${message} The following updates were unfinished (and warnings were not disabled for their handler): ${JSON.stringify(
|
|
1011
|
+
handlerExecutions.map((ex) => ({ name: ex.name, id: ex.id }))
|
|
1012
|
+
)}`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function makeUnfinishedSignalHandlerMessage(handlerExecutions: MessageHandlerExecution[]): string {
|
|
1016
|
+
const message = `
|
|
1017
|
+
[TMPRL1102] Workflow finished while a signal handler was still running. This may have interrupted work that the
|
|
1018
|
+
signal handler was doing. You can wait for all update and signal handlers to complete by using
|
|
1019
|
+
\`await workflow.condition(workflow.allHandlersFinished)\`. Alternatively, if both you and the
|
|
1020
|
+
clients sending the update are okay with interrupting running handlers when the workflow finishes,
|
|
1021
|
+
then you can disable this warning by passing an option when setting the handler:
|
|
1022
|
+
\`workflow.setHandler(mySignal, mySignalHandler, {unfinishedPolicy: HandlerUnfinishedPolicy.ABANDON});\`.`
|
|
1023
|
+
|
|
1024
|
+
.replace(/\n/g, ' ')
|
|
1025
|
+
.trim();
|
|
1026
|
+
|
|
1027
|
+
const names = new Map<string, number>();
|
|
1028
|
+
for (const ex of handlerExecutions) {
|
|
1029
|
+
const count = names.get(ex.name) || 0;
|
|
1030
|
+
names.set(ex.name, count + 1);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return `${message} The following signals were unfinished (and warnings were not disabled for their handler): ${JSON.stringify(
|
|
1034
|
+
Array.from(names.entries()).map(([name, count]) => ({ name, count }))
|
|
1035
|
+
)}`;
|
|
1036
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AsyncLocalStorage as ALS } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Option for constructing a UpdateScope
|
|
5
|
+
*/
|
|
6
|
+
export interface UpdateScopeOptions {
|
|
7
|
+
/**
|
|
8
|
+
* A workflow-unique identifier for this update.
|
|
9
|
+
*/
|
|
10
|
+
id: string;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The update type name.
|
|
14
|
+
*/
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// AsyncLocalStorage is injected via vm module into global scope.
|
|
19
|
+
// In case Workflow code is imported in Node.js context, replace with an empty class.
|
|
20
|
+
export const AsyncLocalStorage: new <T>() => ALS<T> = (globalThis as any).AsyncLocalStorage ?? class {};
|
|
21
|
+
|
|
22
|
+
export class UpdateScope {
|
|
23
|
+
/**
|
|
24
|
+
* A workflow-unique identifier for this update.
|
|
25
|
+
*/
|
|
26
|
+
public readonly id: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The update type name.
|
|
30
|
+
*/
|
|
31
|
+
public readonly name: string;
|
|
32
|
+
|
|
33
|
+
constructor(options: UpdateScopeOptions) {
|
|
34
|
+
this.id = options.id;
|
|
35
|
+
this.name = options.name;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Activate the scope as current and run the update handler `fn`.
|
|
40
|
+
*
|
|
41
|
+
* @return the result of `fn`
|
|
42
|
+
*/
|
|
43
|
+
run<T>(fn: () => Promise<T>): Promise<T> {
|
|
44
|
+
return storage.run(this, fn);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the current "active" update scope.
|
|
49
|
+
*/
|
|
50
|
+
static current(): UpdateScope | undefined {
|
|
51
|
+
return storage.getStore();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Alias to `new UpdateScope({ id, name }).run(fn)` */
|
|
55
|
+
static updateWithInfo<T>(id: string, name: string, fn: () => Promise<T>): Promise<T> {
|
|
56
|
+
return new this({ id, name }).run(fn);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const storage = new AsyncLocalStorage<UpdateScope>();
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Disable the async local storage for updates.
|
|
64
|
+
*/
|
|
65
|
+
export function disableUpdateStorage(): void {
|
|
66
|
+
storage.disable();
|
|
67
|
+
}
|