@temporalio/workflow 1.11.8 → 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/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
+ );
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- import { IllegalStateError } from '@temporalio/common';
6
+ import { encodeVersioningBehavior, IllegalStateError, WorkflowFunctionWithOptions } from '@temporalio/common';
7
7
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
8
8
  import { coresdk } from '@temporalio/proto';
9
9
  import { disableStorage } from './cancellation-scope';
@@ -37,54 +37,68 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
37
37
  // as well as Date and Math.random.
38
38
  setActivatorUntyped(activator);
39
39
 
40
- // webpack alias to payloadConverterPath
41
- // eslint-disable-next-line @typescript-eslint/no-require-imports
42
- const customPayloadConverter = require('__temporal_custom_payload_converter').payloadConverter;
43
- // The `payloadConverter` export is validated in the Worker
44
- if (customPayloadConverter != null) {
45
- activator.payloadConverter = customPayloadConverter;
46
- }
47
- // webpack alias to failureConverterPath
48
- // eslint-disable-next-line @typescript-eslint/no-require-imports
49
- const customFailureConverter = require('__temporal_custom_failure_converter').failureConverter;
50
- // The `failureConverter` export is validated in the Worker
51
- if (customFailureConverter != null) {
52
- activator.failureConverter = customFailureConverter;
53
- }
40
+ activator.rethrowSynchronously = true;
41
+ try {
42
+ // webpack alias to payloadConverterPath
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
44
+ const customPayloadConverter = require('__temporal_custom_payload_converter').payloadConverter;
45
+ // The `payloadConverter` export is validated in the Worker
46
+ if (customPayloadConverter != null) {
47
+ activator.payloadConverter = customPayloadConverter;
48
+ }
49
+ // webpack alias to failureConverterPath
50
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
51
+ const customFailureConverter = require('__temporal_custom_failure_converter').failureConverter;
52
+ // The `failureConverter` export is validated in the Worker
53
+ if (customFailureConverter != null) {
54
+ activator.failureConverter = customFailureConverter;
55
+ }
54
56
 
55
- const { importWorkflows, importInterceptors } = global.__TEMPORAL__;
56
- if (importWorkflows === undefined || importInterceptors === undefined) {
57
- throw new IllegalStateError('Workflow bundle did not register import hooks');
58
- }
57
+ const { importWorkflows, importInterceptors } = global.__TEMPORAL__;
58
+ if (importWorkflows === undefined || importInterceptors === undefined) {
59
+ throw new IllegalStateError('Workflow bundle did not register import hooks');
60
+ }
59
61
 
60
- const interceptors = importInterceptors();
61
- for (const mod of interceptors) {
62
- const factory: WorkflowInterceptorsFactory = mod.interceptors;
63
- if (factory !== undefined) {
64
- if (typeof factory !== 'function') {
65
- throw new TypeError(`Failed to initialize workflows interceptors: expected a function, but got: '${factory}'`);
62
+ const interceptors = importInterceptors();
63
+ for (const mod of interceptors) {
64
+ const factory: WorkflowInterceptorsFactory = mod.interceptors;
65
+ if (factory !== undefined) {
66
+ if (typeof factory !== 'function') {
67
+ throw new TypeError(
68
+ `Failed to initialize workflows interceptors: expected a function, but got: '${factory}'`
69
+ );
70
+ }
71
+ const interceptors = factory();
72
+ activator.interceptors.inbound.push(...(interceptors.inbound ?? []));
73
+ activator.interceptors.outbound.push(...(interceptors.outbound ?? []));
74
+ activator.interceptors.internals.push(...(interceptors.internals ?? []));
66
75
  }
67
- const interceptors = factory();
68
- activator.interceptors.inbound.push(...(interceptors.inbound ?? []));
69
- activator.interceptors.outbound.push(...(interceptors.outbound ?? []));
70
- activator.interceptors.internals.push(...(interceptors.internals ?? []));
71
76
  }
72
- }
73
77
 
74
- const mod = importWorkflows();
75
- const workflowFn = mod[activator.info.workflowType];
76
- const defaultWorkflowFn = mod['default'];
77
-
78
- if (typeof workflowFn === 'function') {
79
- activator.workflow = workflowFn;
80
- } else if (typeof defaultWorkflowFn === 'function') {
81
- activator.workflow = defaultWorkflowFn;
82
- } else {
83
- const details =
84
- workflowFn === undefined
85
- ? 'no such function is exported by the workflow bundle'
86
- : `expected a function, but got: '${typeof workflowFn}'`;
87
- throw new TypeError(`Failed to initialize workflow of type '${activator.info.workflowType}': ${details}`);
78
+ const mod = importWorkflows();
79
+ const workflowFn = mod[activator.info.workflowType];
80
+ const defaultWorkflowFn = mod['default'];
81
+
82
+ if (typeof workflowFn === 'function') {
83
+ activator.workflow = workflowFn;
84
+ } else if (typeof defaultWorkflowFn === 'function') {
85
+ activator.workflow = defaultWorkflowFn;
86
+ } else {
87
+ const details =
88
+ workflowFn === undefined
89
+ ? 'no such function is exported by the workflow bundle'
90
+ : `expected a function, but got: '${typeof workflowFn}'`;
91
+ throw new TypeError(`Failed to initialize workflow of type '${activator.info.workflowType}': ${details}`);
92
+ }
93
+ if (isWorkflowFunctionWithOptions(activator.workflow)) {
94
+ if (typeof activator.workflow.workflowDefinitionOptions === 'object') {
95
+ activator.versioningBehavior = activator.workflow.workflowDefinitionOptions.versioningBehavior;
96
+ } else {
97
+ activator.workflowDefinitionOptionsGetter = activator.workflow.workflowDefinitionOptions;
98
+ }
99
+ }
100
+ } finally {
101
+ activator.rethrowSynchronously = false;
88
102
  }
89
103
  }
90
104
 
@@ -111,53 +125,70 @@ function fixPrototypes<X>(obj: X): X {
111
125
  * Initialize the workflow. Or to be exact, _complete_ initialization, as most part has been done in constructor).
112
126
  */
113
127
  export function initialize(initializeWorkflowJob: coresdk.workflow_activation.IInitializeWorkflow): void {
114
- getActivator().initializeWorkflow(initializeWorkflowJob);
128
+ const activator = getActivator();
129
+ activator.rethrowSynchronously = true;
130
+ try {
131
+ activator.initializeWorkflow(initializeWorkflowJob);
132
+ } finally {
133
+ activator.rethrowSynchronously = false;
134
+ }
115
135
  }
116
136
 
117
137
  /**
118
- * Run a chunk of activation jobs
138
+ * Run a chunk of activation jobs.
139
+ *
140
+ * Notice that this function is not async and runs _inside_ the VM context. Therefore, no microtask
141
+ * will get executed _while_ this function is active; they will however get executed _after_ this
142
+ * function returns (i.e. all outstanding microtasks in the VM will get executed before execution
143
+ * resumes out of the VM, in `vm-shared.ts:activate()`).
119
144
  */
120
145
  export function activate(activation: coresdk.workflow_activation.IWorkflowActivation, batchIndex = 0): void {
121
146
  const activator = getActivator();
122
- const intercept = composeInterceptors(activator.interceptors.internals, 'activate', ({ activation }) => {
123
- // Cast from the interface to the class which has the `variant` attribute.
124
- // This is safe because we know that activation is a proto class.
125
- const jobs = activation.jobs as coresdk.workflow_activation.WorkflowActivationJob[];
147
+ activator.rethrowSynchronously = true;
148
+ try {
149
+ const intercept = composeInterceptors(activator.interceptors.internals, 'activate', ({ activation }) => {
150
+ // Cast from the interface to the class which has the `variant` attribute.
151
+ // This is safe because we know that activation is a proto class.
152
+ const jobs = activation.jobs as coresdk.workflow_activation.WorkflowActivationJob[];
126
153
 
127
- // Initialization will have been handled already, but we might still need to start the workflow function
128
- const startWorkflowJob = jobs[0].variant === 'initializeWorkflow' ? jobs.shift()?.initializeWorkflow : undefined;
154
+ // Initialization will have been handled already, but we might still need to start the workflow function
155
+ const startWorkflowJob = jobs[0].variant === 'initializeWorkflow' ? jobs.shift()?.initializeWorkflow : undefined;
129
156
 
130
- for (const job of jobs) {
131
- if (job.variant === undefined) throw new TypeError('Expected job.variant to be defined');
157
+ for (const job of jobs) {
158
+ if (job.variant === undefined) throw new TypeError('Expected job.variant to be defined');
132
159
 
133
- const variant = job[job.variant];
134
- if (!variant) throw new TypeError(`Expected job.${job.variant} to be set`);
160
+ const variant = job[job.variant];
161
+ if (!variant) throw new TypeError(`Expected job.${job.variant} to be set`);
135
162
 
136
- activator[job.variant](variant as any /* TS can't infer this type */);
137
-
138
- if (job.variant !== 'queryWorkflow') tryUnblockConditions();
139
- }
163
+ activator[job.variant](variant as any /* TS can't infer this type */);
140
164
 
141
- if (startWorkflowJob) {
142
- const safeJobTypes: coresdk.workflow_activation.WorkflowActivationJob['variant'][] = [
143
- 'initializeWorkflow',
144
- 'signalWorkflow',
145
- 'doUpdate',
146
- 'cancelWorkflow',
147
- 'updateRandomSeed',
148
- ];
149
- if (jobs.some((job) => !safeJobTypes.includes(job.variant))) {
150
- throw new TypeError(
151
- 'Received both initializeWorkflow and non-signal/non-update jobs in the same activation: ' +
152
- JSON.stringify(jobs.map((job) => job.variant))
153
- );
165
+ if (job.variant !== 'queryWorkflow') tryUnblockConditions();
154
166
  }
155
167
 
156
- activator.startWorkflow(startWorkflowJob);
157
- tryUnblockConditions();
158
- }
159
- });
160
- intercept({ activation, batchIndex });
168
+ if (startWorkflowJob) {
169
+ const safeJobTypes: coresdk.workflow_activation.WorkflowActivationJob['variant'][] = [
170
+ 'initializeWorkflow',
171
+ 'signalWorkflow',
172
+ 'doUpdate',
173
+ 'cancelWorkflow',
174
+ 'updateRandomSeed',
175
+ ];
176
+ if (jobs.some((job) => !safeJobTypes.includes(job.variant))) {
177
+ throw new TypeError(
178
+ 'Received both initializeWorkflow and non-signal/non-update jobs in the same activation: ' +
179
+ JSON.stringify(jobs.map((job) => job.variant))
180
+ );
181
+ }
182
+
183
+ activator.startWorkflow(startWorkflowJob);
184
+
185
+ tryUnblockConditions();
186
+ }
187
+ });
188
+ intercept({ activation, batchIndex });
189
+ } finally {
190
+ activator.rethrowSynchronously = false;
191
+ }
161
192
  }
162
193
 
163
194
  /**
@@ -168,18 +199,26 @@ export function activate(activation: coresdk.workflow_activation.IWorkflowActiva
168
199
  */
169
200
  export function concludeActivation(): coresdk.workflow_completion.IWorkflowActivationCompletion {
170
201
  const activator = getActivator();
171
- activator.rejectBufferedUpdates();
172
- const intercept = composeInterceptors(activator.interceptors.internals, 'concludeActivation', (input) => input);
173
- const activationCompletion = activator.concludeActivation();
174
- const { commands } = intercept({ commands: activationCompletion.commands });
175
- if (activator.completed) {
176
- activator.warnIfUnfinishedHandlers();
202
+ activator.rethrowSynchronously = true;
203
+ try {
204
+ activator.rejectBufferedUpdates();
205
+ const intercept = composeInterceptors(activator.interceptors.internals, 'concludeActivation', (input) => input);
206
+ const activationCompletion = activator.concludeActivation();
207
+ const { commands } = intercept({ commands: activationCompletion.commands });
208
+ if (activator.completed) {
209
+ activator.warnIfUnfinishedHandlers();
210
+ }
211
+ return {
212
+ runId: activator.info.runId,
213
+ successful: {
214
+ ...activationCompletion,
215
+ commands,
216
+ versioningBehavior: encodeVersioningBehavior(activationCompletion.versioningBehavior),
217
+ },
218
+ };
219
+ } finally {
220
+ activator.rethrowSynchronously = false;
177
221
  }
178
-
179
- return {
180
- runId: activator.info.runId,
181
- successful: { ...activationCompletion, commands },
182
- };
183
222
  }
184
223
 
185
224
  /**
@@ -188,28 +227,46 @@ export function concludeActivation(): coresdk.workflow_completion.IWorkflowActiv
188
227
  * @returns number of unblocked conditions.
189
228
  */
190
229
  export function tryUnblockConditions(): number {
191
- let numUnblocked = 0;
192
- for (;;) {
193
- const prevUnblocked = numUnblocked;
194
- for (const [seq, cond] of getActivator().blockedConditions.entries()) {
195
- if (cond.fn()) {
196
- cond.resolve();
197
- numUnblocked++;
198
- // It is safe to delete elements during map iteration
199
- getActivator().blockedConditions.delete(seq);
230
+ const activator = getActivator();
231
+ activator.rethrowSynchronously = true;
232
+ try {
233
+ let numUnblocked = 0;
234
+ for (;;) {
235
+ activator.maybeRethrowWorkflowTaskError();
236
+ const prevUnblocked = numUnblocked;
237
+ for (const [seq, cond] of activator.blockedConditions.entries()) {
238
+ if (cond.fn()) {
239
+ cond.resolve();
240
+ numUnblocked++;
241
+ // It is safe to delete elements during map iteration
242
+ activator.blockedConditions.delete(seq);
243
+ }
244
+ }
245
+ if (prevUnblocked === numUnblocked) {
246
+ break;
200
247
  }
201
248
  }
202
- if (prevUnblocked === numUnblocked) {
203
- break;
204
- }
249
+ return numUnblocked;
250
+ } finally {
251
+ activator.rethrowSynchronously = false;
205
252
  }
206
- return numUnblocked;
207
253
  }
208
254
 
209
255
  export function dispose(): void {
210
- const dispose = composeInterceptors(getActivator().interceptors.internals, 'dispose', async () => {
211
- disableStorage();
212
- disableUpdateStorage();
213
- });
214
- dispose({});
256
+ const activator = getActivator();
257
+ activator.rethrowSynchronously = true;
258
+ try {
259
+ const dispose = composeInterceptors(activator.interceptors.internals, 'dispose', async () => {
260
+ disableStorage();
261
+ disableUpdateStorage();
262
+ });
263
+ dispose({});
264
+ } finally {
265
+ activator.rethrowSynchronously = false;
266
+ }
267
+ }
268
+
269
+ function isWorkflowFunctionWithOptions(obj: any): obj is WorkflowFunctionWithOptions<any[], any> {
270
+ if (obj == null) return false;
271
+ return Object.hasOwn(obj, 'workflowDefinitionOptions');
215
272
  }