@temporalio/workflow 1.11.7 → 1.12.0-rc.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/alea.js +5 -3
- package/lib/alea.js.map +1 -1
- package/lib/cancellation-scope.js +36 -18
- package/lib/cancellation-scope.js.map +1 -1
- package/lib/errors.js +1 -0
- package/lib/errors.js.map +1 -1
- package/lib/flags.js +1 -1
- package/lib/flags.js.map +1 -1
- package/lib/index.d.ts +6 -3
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/interceptors.d.ts +11 -1
- package/lib/interfaces.d.ts +68 -5
- package/lib/interfaces.js +1 -0
- package/lib/interfaces.js.map +1 -1
- package/lib/internals.d.ts +55 -5
- package/lib/internals.js +323 -193
- package/lib/internals.js.map +1 -1
- package/lib/logs.js +11 -1
- package/lib/logs.js.map +1 -1
- package/lib/metrics.d.ts +32 -0
- package/lib/metrics.js +118 -0
- package/lib/metrics.js.map +1 -0
- package/lib/trigger.js +8 -0
- package/lib/trigger.js.map +1 -1
- package/lib/update-scope.js +8 -0
- package/lib/update-scope.js.map +1 -1
- package/lib/worker-interface.d.ts +6 -1
- package/lib/worker-interface.js +165 -103
- package/lib/worker-interface.js.map +1 -1
- package/lib/workflow.d.ts +75 -17
- package/lib/workflow.js +204 -31
- package/lib/workflow.js.map +1 -1
- package/package.json +7 -4
- package/src/flags.ts +1 -1
- package/src/index.ts +4 -2
- package/src/interceptors.ts +21 -1
- package/src/interfaces.ts +81 -7
- package/src/internals.ts +133 -23
- package/src/logs.ts +11 -1
- package/src/metrics.ts +191 -0
- package/src/worker-interface.ts +164 -107
- package/src/workflow.ts +229 -35
package/src/interfaces.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { RawSourceMap } from 'source-map';
|
|
2
2
|
import {
|
|
3
3
|
RetryPolicy,
|
|
4
|
-
TemporalFailure,
|
|
5
4
|
CommonWorkflowOptions,
|
|
6
5
|
HandlerUnfinishedPolicy,
|
|
7
6
|
SearchAttributes,
|
|
@@ -10,6 +9,11 @@ import {
|
|
|
10
9
|
QueryDefinition,
|
|
11
10
|
Duration,
|
|
12
11
|
VersioningIntent,
|
|
12
|
+
TypedSearchAttributes,
|
|
13
|
+
SearchAttributePair,
|
|
14
|
+
Priority,
|
|
15
|
+
WorkerDeploymentVersion,
|
|
16
|
+
VersioningBehavior,
|
|
13
17
|
} from '@temporalio/common';
|
|
14
18
|
import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers';
|
|
15
19
|
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow/enums-helpers';
|
|
@@ -39,8 +43,16 @@ export interface WorkflowInfo {
|
|
|
39
43
|
* Indexed information attached to the Workflow Execution
|
|
40
44
|
*
|
|
41
45
|
* This value may change during the lifetime of an Execution.
|
|
46
|
+
* @deprecated Use {@link typedSearchAttributes} instead.
|
|
42
47
|
*/
|
|
43
|
-
readonly searchAttributes: SearchAttributes;
|
|
48
|
+
readonly searchAttributes: SearchAttributes; // eslint-disable-line deprecation/deprecation
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Indexed information attached to the Workflow Execution, exposed through an interface.
|
|
52
|
+
*
|
|
53
|
+
* This value may change during the lifetime of an Execution.
|
|
54
|
+
*/
|
|
55
|
+
readonly typedSearchAttributes: TypedSearchAttributes;
|
|
44
56
|
|
|
45
57
|
/**
|
|
46
58
|
* Non-indexed information attached to the Workflow Execution
|
|
@@ -52,6 +64,22 @@ export interface WorkflowInfo {
|
|
|
52
64
|
*/
|
|
53
65
|
readonly parent?: ParentWorkflowInfo;
|
|
54
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The root workflow execution, defined as follows:
|
|
69
|
+
* 1. A workflow without a parent workflow is its own root workflow.
|
|
70
|
+
* 2. A workflow with a parent workflow has the same root workflow as
|
|
71
|
+
* its parent.
|
|
72
|
+
*
|
|
73
|
+
* When there is no parent workflow, i.e., the workflow is its own root workflow,
|
|
74
|
+
* this field is `undefined`.
|
|
75
|
+
*
|
|
76
|
+
* Note that Continue-as-New (or reset) propagates the workflow parentage relationship,
|
|
77
|
+
* and therefore, whether the new workflow has the same root workflow as the original one
|
|
78
|
+
* depends on whether it had a parent.
|
|
79
|
+
*
|
|
80
|
+
*/
|
|
81
|
+
readonly root?: RootWorkflowInfo;
|
|
82
|
+
|
|
55
83
|
/**
|
|
56
84
|
* Result from the previous Run (present if this is a Cron Workflow or was Continued As New).
|
|
57
85
|
*
|
|
@@ -62,7 +90,7 @@ export interface WorkflowInfo {
|
|
|
62
90
|
/**
|
|
63
91
|
* Failure from the previous Run (present when this Run is a retry, or the last Run of a Cron Workflow failed)
|
|
64
92
|
*/
|
|
65
|
-
readonly lastFailure?:
|
|
93
|
+
readonly lastFailure?: Error;
|
|
66
94
|
|
|
67
95
|
/**
|
|
68
96
|
* Length of Workflow history up until the current Workflow Task.
|
|
@@ -169,10 +197,28 @@ export interface WorkflowInfo {
|
|
|
169
197
|
* task was completed by a worker without a Build ID. If this worker is the one executing this
|
|
170
198
|
* task for the first time and has a Build ID set, then its ID will be used. This value may change
|
|
171
199
|
* over the lifetime of the workflow run, but is deterministic and safe to use for branching.
|
|
200
|
+
*
|
|
201
|
+
* @deprecated Use `currentDeploymentVersion` instead
|
|
172
202
|
*/
|
|
173
203
|
readonly currentBuildId?: string;
|
|
174
204
|
|
|
205
|
+
/**
|
|
206
|
+
* The Deployment Version of the worker which executed the current Workflow Task. May be undefined
|
|
207
|
+
* if the task was completed by a worker without a Deployment Version. If this worker is the one
|
|
208
|
+
* executing this task for the first time and has a Deployment Version set, then its ID will be
|
|
209
|
+
* used. This value may change over the lifetime of the workflow run, but is deterministic and
|
|
210
|
+
* safe to use for branching.
|
|
211
|
+
*
|
|
212
|
+
* @experimental Deployment based versioning is experimental and may change in the future.
|
|
213
|
+
*/
|
|
214
|
+
readonly currentDeploymentVersion?: WorkerDeploymentVersion;
|
|
215
|
+
|
|
175
216
|
readonly unsafe: UnsafeWorkflowInfo;
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Priority of this workflow
|
|
220
|
+
*/
|
|
221
|
+
readonly priority?: Priority;
|
|
176
222
|
}
|
|
177
223
|
|
|
178
224
|
/**
|
|
@@ -213,6 +259,11 @@ export interface ParentWorkflowInfo {
|
|
|
213
259
|
namespace: string;
|
|
214
260
|
}
|
|
215
261
|
|
|
262
|
+
export interface RootWorkflowInfo {
|
|
263
|
+
workflowId: string;
|
|
264
|
+
runId: string;
|
|
265
|
+
}
|
|
266
|
+
|
|
216
267
|
/**
|
|
217
268
|
* Not an actual error, used by the Workflow runtime to abort execution when {@link continueAsNew} is called
|
|
218
269
|
*/
|
|
@@ -251,15 +302,27 @@ export interface ContinueAsNewOptions {
|
|
|
251
302
|
memo?: Record<string, unknown>;
|
|
252
303
|
/**
|
|
253
304
|
* Searchable attributes to attach to next Workflow run
|
|
305
|
+
* @deprecated Use {@link typedSearchAttributes} instead.
|
|
306
|
+
*/
|
|
307
|
+
searchAttributes?: SearchAttributes; // eslint-disable-line deprecation/deprecation
|
|
308
|
+
/**
|
|
309
|
+
* Specifies additional indexed information to attach to the Workflow Execution. More info:
|
|
310
|
+
* https://docs.temporal.io/docs/typescript/search-attributes
|
|
311
|
+
*
|
|
312
|
+
* Values are always converted using {@link JsonPayloadConverter}, even when a custom data converter is provided.
|
|
313
|
+
* Note that search attributes are not encoded, as such, do not include any sensitive information.
|
|
314
|
+
*
|
|
315
|
+
* If both {@link searchAttributes} and {@link typedSearchAttributes} are provided, conflicting keys will be overwritten
|
|
316
|
+
* by {@link typedSearchAttributes}.
|
|
254
317
|
*/
|
|
255
|
-
|
|
318
|
+
typedSearchAttributes?: SearchAttributePair[] | TypedSearchAttributes;
|
|
256
319
|
/**
|
|
257
320
|
* When using the Worker Versioning feature, specifies whether this Workflow should
|
|
258
321
|
* Continue-as-New onto a worker with a compatible Build Id or not. See {@link VersioningIntent}.
|
|
259
322
|
*
|
|
260
323
|
* @default 'COMPATIBLE'
|
|
261
324
|
*
|
|
262
|
-
* @experimental
|
|
325
|
+
* @experimental The Worker Versioning API is still being designed. Major changes are expected.
|
|
263
326
|
*/
|
|
264
327
|
versioningIntent?: VersioningIntent;
|
|
265
328
|
}
|
|
@@ -390,7 +453,7 @@ export const [encodeParentClosePolicy, decodeParentClosePolicy] = makeProtoEnumC
|
|
|
390
453
|
'PARENT_CLOSE_POLICY_'
|
|
391
454
|
);
|
|
392
455
|
|
|
393
|
-
export interface ChildWorkflowOptions extends CommonWorkflowOptions {
|
|
456
|
+
export interface ChildWorkflowOptions extends Omit<CommonWorkflowOptions, 'workflowIdConflictPolicy'> {
|
|
394
457
|
/**
|
|
395
458
|
* Workflow id to use when starting. If not specified a UUID is generated. Note that it is
|
|
396
459
|
* dangerous as in case of client side retries no deduplication will happen based on the
|
|
@@ -429,7 +492,7 @@ export interface ChildWorkflowOptions extends CommonWorkflowOptions {
|
|
|
429
492
|
*
|
|
430
493
|
* @default 'COMPATIBLE'
|
|
431
494
|
*
|
|
432
|
-
* @experimental
|
|
495
|
+
* @experimental The Worker Versioning API is still being designed. Major changes are expected.
|
|
433
496
|
*/
|
|
434
497
|
versioningIntent?: VersioningIntent;
|
|
435
498
|
}
|
|
@@ -538,6 +601,16 @@ export type Handler<
|
|
|
538
601
|
*/
|
|
539
602
|
export type DefaultSignalHandler = (signalName: string, ...args: unknown[]) => void | Promise<void>;
|
|
540
603
|
|
|
604
|
+
/**
|
|
605
|
+
* A handler function accepting update calls for non-registered update names.
|
|
606
|
+
*/
|
|
607
|
+
export type DefaultUpdateHandler = (updateName: string, ...args: unknown[]) => Promise<unknown> | unknown;
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* A handler function accepting query calls for non-registered query names.
|
|
611
|
+
*/
|
|
612
|
+
export type DefaultQueryHandler = (queryName: string, ...args: unknown[]) => unknown;
|
|
613
|
+
|
|
541
614
|
/**
|
|
542
615
|
* A validation function capable of accepting the arguments for a given UpdateDefinition.
|
|
543
616
|
*/
|
|
@@ -565,4 +638,5 @@ export type UpdateHandlerOptions<Args extends any[]> = {
|
|
|
565
638
|
export interface ActivationCompletion {
|
|
566
639
|
commands: coresdk.workflow_commands.IWorkflowCommand[];
|
|
567
640
|
usedInternalFlags: number[];
|
|
641
|
+
versioningBehavior?: VersioningBehavior;
|
|
568
642
|
}
|
package/src/internals.ts
CHANGED
|
@@ -19,10 +19,15 @@ import {
|
|
|
19
19
|
WorkflowUpdateType,
|
|
20
20
|
WorkflowUpdateValidatorType,
|
|
21
21
|
mapFromPayloads,
|
|
22
|
-
searchAttributePayloadConverter,
|
|
23
22
|
fromPayloadsAtIndex,
|
|
24
|
-
|
|
23
|
+
WorkflowFunctionWithOptions,
|
|
24
|
+
VersioningBehavior,
|
|
25
|
+
WorkflowDefinitionOptions,
|
|
25
26
|
} from '@temporalio/common';
|
|
27
|
+
import {
|
|
28
|
+
decodeSearchAttributes,
|
|
29
|
+
decodeTypedSearchAttributes,
|
|
30
|
+
} from '@temporalio/common/lib/converter/payload-search-attributes';
|
|
26
31
|
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
|
|
27
32
|
import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
|
|
28
33
|
import type { coresdk, temporal } from '@temporalio/proto';
|
|
@@ -41,6 +46,8 @@ import {
|
|
|
41
46
|
WorkflowInfo,
|
|
42
47
|
WorkflowCreateOptionsInternal,
|
|
43
48
|
ActivationCompletion,
|
|
49
|
+
DefaultUpdateHandler,
|
|
50
|
+
DefaultQueryHandler,
|
|
44
51
|
} from './interfaces';
|
|
45
52
|
import { type SinkCall } from './sinks';
|
|
46
53
|
import { untrackPromise } from './stack-helpers';
|
|
@@ -189,6 +196,16 @@ export class Activator implements ActivationHandler {
|
|
|
189
196
|
*/
|
|
190
197
|
defaultSignalHandler?: DefaultSignalHandler;
|
|
191
198
|
|
|
199
|
+
/**
|
|
200
|
+
* A update handler that catches calls for non-registered update names.
|
|
201
|
+
*/
|
|
202
|
+
defaultUpdateHandler?: DefaultUpdateHandler;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* A query handler that catches calls for non-registered query names.
|
|
206
|
+
*/
|
|
207
|
+
defaultQueryHandler?: DefaultQueryHandler;
|
|
208
|
+
|
|
192
209
|
/**
|
|
193
210
|
* Source map file for looking up the source files in response to __enhanced_stack_trace
|
|
194
211
|
*/
|
|
@@ -204,6 +221,38 @@ export class Activator implements ActivationHandler {
|
|
|
204
221
|
childToParent: new Map(),
|
|
205
222
|
};
|
|
206
223
|
|
|
224
|
+
/**
|
|
225
|
+
* The error that caused the current Workflow Task to fail. Sets if a non-`TemporalFailure`
|
|
226
|
+
* error bubbles up out of the Workflow function, or out of a Signal or Update handler. We
|
|
227
|
+
* capture errors this way because those functions are not technically awaited when started,
|
|
228
|
+
* but left to run asynchronously. There is therefore no real "parent" function that can
|
|
229
|
+
* directly handle those errors, and not capturing it would result in an Unhandled Promise
|
|
230
|
+
* Rejection. So instead, we buffer the error here, to then be processed in the context
|
|
231
|
+
* of our own synchronous Activation handling event loop.
|
|
232
|
+
*
|
|
233
|
+
* Our code does a best effort to stop processing the current activation as soon as possible
|
|
234
|
+
* after this field is set:
|
|
235
|
+
* - If an error is thrown while executing code synchronously (e.g. anything before the
|
|
236
|
+
* first `await` statement in a Workflow function or a signal/update handler), the error
|
|
237
|
+
* will be _immediately_ rethrown, which will prevent execution of further jobs in the
|
|
238
|
+
* current activation. We know we're currently running code synchronously thanks to the
|
|
239
|
+
* `rethrowSynchronously` flag below.
|
|
240
|
+
* - It an error is thrown while executing microtasks, then the error will be rethrown on
|
|
241
|
+
* the next call to `tryUnblockConditions()`.
|
|
242
|
+
*
|
|
243
|
+
* Unfortunately, there's no way for us to prevent further execution of microtasks that have
|
|
244
|
+
* already been scheduled, nor those that will be recursively scheduled from those microtasks.
|
|
245
|
+
* Should more errors get thrown while settling microtasks, those will be ignored (i.e. only
|
|
246
|
+
* the first captured error is preserved).
|
|
247
|
+
*/
|
|
248
|
+
public workflowTaskError: unknown;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Set to true when running synchronous code (e.g. while processing activation jobs and when calling
|
|
252
|
+
* `tryUnblockConditions()`). While this flag is set, it is safe to let errors bubble up.
|
|
253
|
+
*/
|
|
254
|
+
public rethrowSynchronously = false;
|
|
255
|
+
|
|
207
256
|
public readonly rootScope = new RootCancellationScope();
|
|
208
257
|
|
|
209
258
|
/**
|
|
@@ -336,7 +385,7 @@ export class Activator implements ActivationHandler {
|
|
|
336
385
|
/**
|
|
337
386
|
* Reference to the current Workflow, initialized when a Workflow is started
|
|
338
387
|
*/
|
|
339
|
-
public workflow?: Workflow
|
|
388
|
+
public workflow?: Workflow | WorkflowFunctionWithOptions<any[], any>;
|
|
340
389
|
|
|
341
390
|
/**
|
|
342
391
|
* Information about the current Workflow
|
|
@@ -370,12 +419,17 @@ export class Activator implements ActivationHandler {
|
|
|
370
419
|
sinkCalls = Array<SinkCall>();
|
|
371
420
|
|
|
372
421
|
/**
|
|
373
|
-
* A nanosecond resolution time function, externally injected
|
|
422
|
+
* A nanosecond resolution time function, externally injected. This is used to
|
|
423
|
+
* precisely sort logs entries emitted from the Workflow Context vs those emitted
|
|
424
|
+
* from other sources (e.g. main thread, Core, etc).
|
|
374
425
|
*/
|
|
375
426
|
public readonly getTimeOfDay: () => bigint;
|
|
376
427
|
|
|
377
428
|
public readonly registeredActivityNames: Set<string>;
|
|
378
429
|
|
|
430
|
+
public versioningBehavior?: VersioningBehavior;
|
|
431
|
+
public workflowDefinitionOptionsGetter?: () => WorkflowDefinitionOptions;
|
|
432
|
+
|
|
379
433
|
constructor({
|
|
380
434
|
info,
|
|
381
435
|
now,
|
|
@@ -448,6 +502,7 @@ export class Activator implements ActivationHandler {
|
|
|
448
502
|
return {
|
|
449
503
|
commands: this.commands.splice(0),
|
|
450
504
|
usedInternalFlags: [...this.knownFlags],
|
|
505
|
+
versioningBehavior: this.versioningBehavior,
|
|
451
506
|
};
|
|
452
507
|
}
|
|
453
508
|
|
|
@@ -478,8 +533,10 @@ export class Activator implements ActivationHandler {
|
|
|
478
533
|
// Most things related to initialization have already been handled in the constructor
|
|
479
534
|
this.mutateWorkflowInfo((info) => ({
|
|
480
535
|
...info,
|
|
481
|
-
|
|
482
|
-
|
|
536
|
+
|
|
537
|
+
searchAttributes: decodeSearchAttributes(searchAttributes?.indexedFields),
|
|
538
|
+
typedSearchAttributes: decodeTypedSearchAttributes(searchAttributes?.indexedFields),
|
|
539
|
+
|
|
483
540
|
memo: mapFromPayloads(this.payloadConverter, memo?.fields),
|
|
484
541
|
lastResult: fromPayloadsAtIndex(this.payloadConverter, 0, lastCompletionResult?.payloads),
|
|
485
542
|
lastFailure:
|
|
@@ -487,6 +544,9 @@ export class Activator implements ActivationHandler {
|
|
|
487
544
|
? this.failureConverter.failureToError(continuedFailure, this.payloadConverter)
|
|
488
545
|
: undefined,
|
|
489
546
|
}));
|
|
547
|
+
if (this.workflowDefinitionOptionsGetter) {
|
|
548
|
+
this.versioningBehavior = this.workflowDefinitionOptionsGetter().versioningBehavior;
|
|
549
|
+
}
|
|
490
550
|
}
|
|
491
551
|
|
|
492
552
|
public cancelWorkflow(_activation: coresdk.workflow_activation.ICancelWorkflow): void {
|
|
@@ -577,9 +637,21 @@ export class Activator implements ActivationHandler {
|
|
|
577
637
|
}
|
|
578
638
|
}
|
|
579
639
|
|
|
640
|
+
public resolveNexusOperationStart(_: coresdk.workflow_activation.IResolveNexusOperationStart): void {
|
|
641
|
+
throw new Error('TODO');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
public resolveNexusOperation(_: coresdk.workflow_activation.IResolveNexusOperation): void {
|
|
645
|
+
throw new Error('TODO');
|
|
646
|
+
}
|
|
647
|
+
|
|
580
648
|
// Intentionally non-async function so this handler doesn't show up in the stack trace
|
|
581
649
|
protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
|
|
582
|
-
|
|
650
|
+
let fn = this.queryHandlers.get(queryName)?.handler;
|
|
651
|
+
if (fn === undefined && this.defaultQueryHandler !== undefined) {
|
|
652
|
+
fn = this.defaultQueryHandler.bind(undefined, queryName);
|
|
653
|
+
}
|
|
654
|
+
// No handler or default registered, fail.
|
|
583
655
|
if (fn === undefined) {
|
|
584
656
|
const knownQueryTypes = [...this.queryHandlers.keys()].join(' ');
|
|
585
657
|
// Fail the query
|
|
@@ -589,6 +661,7 @@ export class Activator implements ActivationHandler {
|
|
|
589
661
|
)
|
|
590
662
|
);
|
|
591
663
|
}
|
|
664
|
+
// Execute handler.
|
|
592
665
|
try {
|
|
593
666
|
const ret = fn(...args);
|
|
594
667
|
if (ret instanceof Promise) {
|
|
@@ -633,8 +706,20 @@ export class Activator implements ActivationHandler {
|
|
|
633
706
|
if (!protocolInstanceId) {
|
|
634
707
|
throw new TypeError('Missing activation update protocolInstanceId');
|
|
635
708
|
}
|
|
636
|
-
|
|
637
|
-
|
|
709
|
+
|
|
710
|
+
const entry =
|
|
711
|
+
this.updateHandlers.get(name) ??
|
|
712
|
+
(this.defaultUpdateHandler
|
|
713
|
+
? {
|
|
714
|
+
handler: this.defaultUpdateHandler.bind(undefined, name),
|
|
715
|
+
validator: undefined,
|
|
716
|
+
// Default to a warning policy.
|
|
717
|
+
unfinishedPolicy: HandlerUnfinishedPolicy.WARN_AND_ABANDON,
|
|
718
|
+
}
|
|
719
|
+
: null);
|
|
720
|
+
|
|
721
|
+
// If we don't have an entry from either source, buffer and return
|
|
722
|
+
if (entry === null) {
|
|
638
723
|
this.bufferedUpdates.push(activation);
|
|
639
724
|
return;
|
|
640
725
|
}
|
|
@@ -703,7 +788,7 @@ export class Activator implements ActivationHandler {
|
|
|
703
788
|
if (error instanceof TemporalFailure) {
|
|
704
789
|
this.rejectUpdate(protocolInstanceId, error);
|
|
705
790
|
} else {
|
|
706
|
-
|
|
791
|
+
this.handleWorkflowFailure(error);
|
|
707
792
|
}
|
|
708
793
|
})
|
|
709
794
|
.finally(() => this.inProgressUpdates.delete(updateId));
|
|
@@ -726,13 +811,21 @@ export class Activator implements ActivationHandler {
|
|
|
726
811
|
public dispatchBufferedUpdates(): void {
|
|
727
812
|
const bufferedUpdates = this.bufferedUpdates;
|
|
728
813
|
while (bufferedUpdates.length) {
|
|
729
|
-
|
|
730
|
-
if (
|
|
731
|
-
|
|
732
|
-
|
|
814
|
+
// We have a default update handler, so all updates are dispatchable.
|
|
815
|
+
if (this.defaultUpdateHandler) {
|
|
816
|
+
const update = bufferedUpdates.shift();
|
|
817
|
+
// Logically, this must be defined as we're in the loop.
|
|
818
|
+
// But Typescript doesn't know that so we use a non-null assertion (!).
|
|
819
|
+
this.doUpdate(update!);
|
|
820
|
+
} else {
|
|
821
|
+
const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name as string));
|
|
822
|
+
if (foundIndex === -1) {
|
|
823
|
+
// No buffered Updates have a handler yet.
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
const [update] = bufferedUpdates.splice(foundIndex, 1);
|
|
827
|
+
this.doUpdate(update);
|
|
733
828
|
}
|
|
734
|
-
const [update] = bufferedUpdates.splice(foundIndex, 1);
|
|
735
|
-
this.doUpdate(update);
|
|
736
829
|
}
|
|
737
830
|
}
|
|
738
831
|
|
|
@@ -830,6 +923,8 @@ export class Activator implements ActivationHandler {
|
|
|
830
923
|
}
|
|
831
924
|
|
|
832
925
|
public warnIfUnfinishedHandlers(): void {
|
|
926
|
+
if (this.workflowTaskError) return;
|
|
927
|
+
|
|
833
928
|
const getWarnable = (handlerExecutions: Iterable<MessageHandlerExecution>): MessageHandlerExecution[] => {
|
|
834
929
|
return Array.from(handlerExecutions).filter(
|
|
835
930
|
(ex) => ex.unfinishedPolicy === HandlerUnfinishedPolicy.WARN_AND_ABANDON
|
|
@@ -941,17 +1036,12 @@ export class Activator implements ActivationHandler {
|
|
|
941
1036
|
* Transforms failures into a command to be sent to the server.
|
|
942
1037
|
* Used to handle any failure emitted by the Workflow.
|
|
943
1038
|
*/
|
|
944
|
-
|
|
1039
|
+
handleWorkflowFailure(error: unknown): void {
|
|
945
1040
|
if (this.cancelled && isCancellation(error)) {
|
|
946
1041
|
this.pushCommand({ cancelWorkflowExecution: {} }, true);
|
|
947
1042
|
} else if (error instanceof ContinueAsNew) {
|
|
948
1043
|
this.pushCommand({ continueAsNewWorkflowExecution: error.command }, true);
|
|
949
|
-
} else {
|
|
950
|
-
if (!(error instanceof TemporalFailure)) {
|
|
951
|
-
// This results in an unhandled rejection which will fail the activation
|
|
952
|
-
// preventing it from completing.
|
|
953
|
-
throw error;
|
|
954
|
-
}
|
|
1044
|
+
} else if (error instanceof TemporalFailure) {
|
|
955
1045
|
// Fail the workflow. We do not want to issue unfinishedHandlers warnings. To achieve that, we
|
|
956
1046
|
// mark all handlers as completed now.
|
|
957
1047
|
this.inProgressSignals.clear();
|
|
@@ -964,9 +1054,29 @@ export class Activator implements ActivationHandler {
|
|
|
964
1054
|
},
|
|
965
1055
|
true
|
|
966
1056
|
);
|
|
1057
|
+
} else {
|
|
1058
|
+
this.recordWorkflowTaskError(error);
|
|
967
1059
|
}
|
|
968
1060
|
}
|
|
969
1061
|
|
|
1062
|
+
recordWorkflowTaskError(error: unknown): void {
|
|
1063
|
+
// Only keep the first error that bubbles up; subsequent errors will be ignored.
|
|
1064
|
+
if (this.workflowTaskError === undefined) this.workflowTaskError = error;
|
|
1065
|
+
|
|
1066
|
+
// Immediately rethrow the error if we know it is safe to do so (i.e. we are not running async
|
|
1067
|
+
// microtasks). Otherwise, the error will be rethrown whenever we get an opportunity to do so,
|
|
1068
|
+
// e.g. the next time `tryUnblockConditions()` is called.
|
|
1069
|
+
if (this.rethrowSynchronously) this.maybeRethrowWorkflowTaskError();
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* If a Workflow Task error was captured, and we are running in synchronous mode,
|
|
1074
|
+
* then bubble it up now. This is safe to call even if there is no error to rethrow.
|
|
1075
|
+
*/
|
|
1076
|
+
maybeRethrowWorkflowTaskError(): void {
|
|
1077
|
+
if (this.workflowTaskError) throw this.workflowTaskError;
|
|
1078
|
+
}
|
|
1079
|
+
|
|
970
1080
|
private completeQuery(queryId: string, result: unknown): void {
|
|
971
1081
|
this.pushCommand({
|
|
972
1082
|
respondToQuery: { queryId, succeeded: { response: this.payloadConverter.toPayload(result) } },
|
package/src/logs.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { type Sink, type Sinks, proxySinks } from './sinks';
|
|
|
5
5
|
import { isCancellation } from './errors';
|
|
6
6
|
import { WorkflowInfo, ContinueAsNew } from './interfaces';
|
|
7
7
|
import { assertInWorkflowContext } from './global-attributes';
|
|
8
|
+
import { currentUpdateInfo, inWorkflowContext } from './workflow';
|
|
8
9
|
|
|
9
10
|
export interface WorkflowLogger extends Sink {
|
|
10
11
|
trace(message: string, attrs?: Record<string, unknown>): void;
|
|
@@ -117,11 +118,20 @@ export function executeWithLifecycleLogging(fn: () => Promise<unknown>): Promise
|
|
|
117
118
|
* Note that this function may be called from outside of the Workflow context (eg. by the worker itself).
|
|
118
119
|
*/
|
|
119
120
|
export function workflowLogAttributes(info: WorkflowInfo): Record<string, unknown> {
|
|
120
|
-
|
|
121
|
+
const attributes: { [key: string]: string } = {
|
|
121
122
|
namespace: info.namespace,
|
|
122
123
|
taskQueue: info.taskQueue,
|
|
123
124
|
workflowId: info.workflowId,
|
|
124
125
|
runId: info.runId,
|
|
125
126
|
workflowType: info.workflowType,
|
|
126
127
|
};
|
|
128
|
+
if (inWorkflowContext()) {
|
|
129
|
+
const updateInfo = currentUpdateInfo();
|
|
130
|
+
if (updateInfo) {
|
|
131
|
+
// Add update info if it exists
|
|
132
|
+
attributes['updateId'] = updateInfo.id;
|
|
133
|
+
attributes['updateName'] = updateInfo.name;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return attributes;
|
|
127
137
|
}
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MetricCounter,
|
|
3
|
+
MetricGauge,
|
|
4
|
+
MetricHistogram,
|
|
5
|
+
MetricMeter,
|
|
6
|
+
MetricMeterWithComposedTags,
|
|
7
|
+
MetricTags,
|
|
8
|
+
NumericMetricValueType,
|
|
9
|
+
} from '@temporalio/common';
|
|
10
|
+
import { composeInterceptors } from '@temporalio/common/lib/interceptors';
|
|
11
|
+
import { proxySinks, Sink, Sinks } from './sinks';
|
|
12
|
+
import { workflowInfo } from './workflow';
|
|
13
|
+
import { assertInWorkflowContext } from './global-attributes';
|
|
14
|
+
|
|
15
|
+
class WorkflowMetricMeterImpl implements MetricMeter {
|
|
16
|
+
constructor() {}
|
|
17
|
+
|
|
18
|
+
createCounter(name: string, unit?: string, description?: string): MetricCounter {
|
|
19
|
+
assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
|
|
20
|
+
return new WorkflowMetricCounter(name, unit, description);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
createHistogram(
|
|
24
|
+
name: string,
|
|
25
|
+
valueType: NumericMetricValueType = 'int',
|
|
26
|
+
unit?: string,
|
|
27
|
+
description?: string
|
|
28
|
+
): MetricHistogram {
|
|
29
|
+
assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
|
|
30
|
+
return new WorkflowMetricHistogram(name, valueType, unit, description);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
createGauge(
|
|
34
|
+
name: string,
|
|
35
|
+
valueType: NumericMetricValueType = 'int',
|
|
36
|
+
unit?: string,
|
|
37
|
+
description?: string
|
|
38
|
+
): MetricGauge {
|
|
39
|
+
assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
|
|
40
|
+
return new WorkflowMetricGauge(name, valueType, unit, description);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
withTags(_tags: MetricTags): MetricMeter {
|
|
44
|
+
assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
|
|
45
|
+
// Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
|
|
46
|
+
throw new Error(`withTags is not supported directly on WorkflowMetricMeter`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class WorkflowMetricCounter implements MetricCounter {
|
|
51
|
+
constructor(
|
|
52
|
+
public readonly name: string,
|
|
53
|
+
public readonly unit: string | undefined,
|
|
54
|
+
public readonly description: string | undefined
|
|
55
|
+
) {}
|
|
56
|
+
|
|
57
|
+
add(value: number, extraTags: MetricTags = {}): void {
|
|
58
|
+
if (value < 0) {
|
|
59
|
+
throw new Error(`MetricCounter value must be non-negative (got ${value})`);
|
|
60
|
+
}
|
|
61
|
+
if (!workflowInfo().unsafe.isReplaying) {
|
|
62
|
+
metricSink.addMetricCounterValue(this.name, this.unit, this.description, value, extraTags);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
withTags(_tags: MetricTags): MetricCounter {
|
|
67
|
+
// Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
|
|
68
|
+
throw new Error(`withTags is not supported directly on WorkflowMetricCounter`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class WorkflowMetricHistogram implements MetricHistogram {
|
|
73
|
+
constructor(
|
|
74
|
+
public readonly name: string,
|
|
75
|
+
public readonly valueType: NumericMetricValueType,
|
|
76
|
+
public readonly unit: string | undefined,
|
|
77
|
+
public readonly description: string | undefined
|
|
78
|
+
) {}
|
|
79
|
+
|
|
80
|
+
record(value: number, extraTags: MetricTags = {}): void {
|
|
81
|
+
if (value < 0) {
|
|
82
|
+
throw new Error(`MetricHistogram value must be non-negative (got ${value})`);
|
|
83
|
+
}
|
|
84
|
+
if (!workflowInfo().unsafe.isReplaying) {
|
|
85
|
+
metricSink.recordMetricHistogramValue(this.name, this.valueType, this.unit, this.description, value, extraTags);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
withTags(_tags: MetricTags): MetricHistogram {
|
|
90
|
+
// Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
|
|
91
|
+
throw new Error(`withTags is not supported directly on WorkflowMetricHistogram`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class WorkflowMetricGauge implements MetricGauge {
|
|
96
|
+
constructor(
|
|
97
|
+
public readonly name: string,
|
|
98
|
+
public readonly valueType: NumericMetricValueType,
|
|
99
|
+
public readonly unit: string | undefined,
|
|
100
|
+
public readonly description: string | undefined
|
|
101
|
+
) {}
|
|
102
|
+
|
|
103
|
+
set(value: number, tags?: MetricTags): void {
|
|
104
|
+
if (value < 0) {
|
|
105
|
+
throw new Error(`MetricGauge value must be non-negative (got ${value})`);
|
|
106
|
+
}
|
|
107
|
+
if (!workflowInfo().unsafe.isReplaying) {
|
|
108
|
+
metricSink.setMetricGaugeValue(this.name, this.valueType, this.unit, this.description, value, tags ?? {});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
withTags(_tags: MetricTags): MetricGauge {
|
|
113
|
+
// Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
|
|
114
|
+
throw new Error(`withTags is not supported directly on WorkflowMetricGauge`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
119
|
+
|
|
120
|
+
// Note: given that forwarding metrics outside of the sanbox can be quite chatty and add non
|
|
121
|
+
// negligeable overhead, we eagerly check for `isReplaying` and completely skip doing sink
|
|
122
|
+
// calls if we are replaying.
|
|
123
|
+
const metricSink = proxySinks<MetricSinks>().__temporal_metrics;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sink interface for forwarding metrics from the Workflow sandbox to the Worker.
|
|
127
|
+
*
|
|
128
|
+
* These sink functions are not intended to be called directly from workflow code; instead,
|
|
129
|
+
* developers should use the `metricMeter` object exposed to workflow code by the SDK, which
|
|
130
|
+
* provides an API that is easier to work with.
|
|
131
|
+
*
|
|
132
|
+
* This sink interface is also not meant to be implemented by user.
|
|
133
|
+
*
|
|
134
|
+
* @hidden
|
|
135
|
+
* @internal Users should not implement this interface, nor use it directly. Use `metricMeter` instead.
|
|
136
|
+
*/
|
|
137
|
+
export interface MetricSinks extends Sinks {
|
|
138
|
+
__temporal_metrics: WorkflowMetricMeter;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @hidden
|
|
143
|
+
* @internal Users should not implement this interface, nor use it directly. Use `metricMeter` instead.
|
|
144
|
+
*/
|
|
145
|
+
export interface WorkflowMetricMeter extends Sink {
|
|
146
|
+
addMetricCounterValue(
|
|
147
|
+
metricName: string,
|
|
148
|
+
unit: string | undefined,
|
|
149
|
+
description: string | undefined,
|
|
150
|
+
value: number,
|
|
151
|
+
attrs: MetricTags
|
|
152
|
+
): void;
|
|
153
|
+
|
|
154
|
+
recordMetricHistogramValue(
|
|
155
|
+
metricName: string,
|
|
156
|
+
valueType: NumericMetricValueType,
|
|
157
|
+
unit: string | undefined,
|
|
158
|
+
description: string | undefined,
|
|
159
|
+
value: number,
|
|
160
|
+
attrs: MetricTags
|
|
161
|
+
): void;
|
|
162
|
+
|
|
163
|
+
setMetricGaugeValue(
|
|
164
|
+
metricName: string,
|
|
165
|
+
valueType: NumericMetricValueType,
|
|
166
|
+
unit: string | undefined,
|
|
167
|
+
description: string | undefined,
|
|
168
|
+
value: number,
|
|
169
|
+
attrs: MetricTags
|
|
170
|
+
): void;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* A MetricMeter that can be used to emit metrics from within a Workflow.
|
|
175
|
+
*
|
|
176
|
+
* @experimental The Metric API is an experimental feature and may be subject to change.
|
|
177
|
+
*/
|
|
178
|
+
export const metricMeter: MetricMeter = MetricMeterWithComposedTags.compose(
|
|
179
|
+
new WorkflowMetricMeterImpl(),
|
|
180
|
+
() => {
|
|
181
|
+
const activator = assertInWorkflowContext('Workflow.metricMeter may only be used from workflow context.');
|
|
182
|
+
const getMetricTags = composeInterceptors(activator.interceptors.outbound, 'getMetricTags', (a) => a);
|
|
183
|
+
|
|
184
|
+
const info = activator.info;
|
|
185
|
+
return getMetricTags({
|
|
186
|
+
// namespace and taskQueue will be added by the Worker
|
|
187
|
+
workflowType: info.workflowType,
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
true
|
|
191
|
+
);
|