@hotmeshio/hotmesh 0.0.53 → 0.0.55
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/README.md +0 -3
- package/build/modules/errors.d.ts +6 -48
- package/build/modules/errors.js +5 -5
- package/build/package.json +1 -1
- package/build/services/activities/hook.js +5 -1
- package/build/services/activities/trigger.d.ts +5 -2
- package/build/services/activities/trigger.js +22 -1
- package/build/services/durable/client.js +1 -8
- package/build/services/durable/exporter.d.ts +24 -13
- package/build/services/durable/exporter.js +145 -127
- package/build/services/durable/handle.d.ts +2 -2
- package/build/services/durable/handle.js +2 -2
- package/build/services/durable/worker.js +1 -1
- package/build/services/durable/workflow.d.ts +29 -17
- package/build/services/durable/workflow.js +117 -97
- package/build/services/engine/index.d.ts +2 -2
- package/build/services/engine/index.js +2 -2
- package/build/services/hotmesh/index.d.ts +2 -2
- package/build/services/hotmesh/index.js +2 -2
- package/build/types/durable.d.ts +15 -3
- package/build/types/error.d.ts +48 -0
- package/build/types/error.js +2 -0
- package/build/types/exporter.d.ts +26 -20
- package/build/types/index.d.ts +2 -1
- package/build/types/job.d.ts +24 -1
- package/modules/errors.ts +18 -55
- package/package.json +1 -1
- package/services/activities/hook.ts +8 -1
- package/services/activities/trigger.ts +27 -2
- package/services/durable/client.ts +2 -8
- package/services/durable/exporter.ts +149 -128
- package/services/durable/handle.ts +3 -3
- package/services/durable/worker.ts +1 -1
- package/services/durable/workflow.ts +137 -104
- package/services/engine/index.ts +4 -3
- package/services/hotmesh/index.ts +4 -3
- package/types/durable.ts +18 -3
- package/types/error.ts +52 -0
- package/types/exporter.ts +31 -23
- package/types/index.ts +8 -1
- package/types/job.ts +27 -0
|
@@ -9,8 +9,8 @@ class WorkflowHandleService {
|
|
|
9
9
|
this.hotMesh = hotMesh;
|
|
10
10
|
this.exporter = new exporter_1.ExporterService(this.hotMesh.appId, this.hotMesh.engine.store, this.hotMesh.engine.logger);
|
|
11
11
|
}
|
|
12
|
-
async export() {
|
|
13
|
-
return this.exporter.export(this.workflowId);
|
|
12
|
+
async export(options) {
|
|
13
|
+
return this.exporter.export(this.workflowId, options);
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Sends a signal to the workflow. This is a way to send
|
|
@@ -231,7 +231,7 @@ class WorkerService {
|
|
|
231
231
|
const workflowInput = data.data;
|
|
232
232
|
const execIndex = counter.counter - interruptionRegistry.length + 1;
|
|
233
233
|
const { workflowId, workflowTopic, workflowDimension, originJobId } = workflowInput;
|
|
234
|
-
const collatorFlowId = `-${workflowId}
|
|
234
|
+
const collatorFlowId = `-${workflowId}-$${workflowDimension || ''}-$${execIndex}`;
|
|
235
235
|
return {
|
|
236
236
|
status: stream_1.StreamStatus.SUCCESS,
|
|
237
237
|
code: enums_1.HMSH_CODE_DURABLE_ALL,
|
|
@@ -2,17 +2,33 @@ import { Search } from './search';
|
|
|
2
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
3
|
import { ActivityConfig, HookOptions, ProxyType, WorkflowContext, WorkflowOptions } from "../../types/durable";
|
|
4
4
|
import { JobInterruptOptions } from '../../types/job';
|
|
5
|
+
import { DurableChildErrorType, DurableProxyErrorType } from '../../types/error';
|
|
5
6
|
export declare class WorkflowService {
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Returns the synchronous output from the activity (replay)
|
|
9
|
+
* if available locally, revealing whether or not the activity already
|
|
10
|
+
* ran during a prior execution cycle
|
|
11
|
+
* @param {string} prefix - one of: proxy, child, start, wait etc
|
|
12
|
+
* @returns
|
|
9
13
|
*/
|
|
10
|
-
static
|
|
14
|
+
static didRun(prefix: string): Promise<[boolean, number, any]>;
|
|
15
|
+
/**
|
|
16
|
+
* Those methods that may only be called once must be protected by flagging
|
|
17
|
+
* their execution with a unique key (the key is stored in the HASH alongside
|
|
18
|
+
* process state and job state)
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
11
22
|
/**
|
|
12
23
|
* Returns the current workflow context restored
|
|
13
24
|
* from Redis
|
|
14
25
|
*/
|
|
15
26
|
static getContext(): WorkflowContext;
|
|
27
|
+
/**
|
|
28
|
+
* Return a handle to the hotmesh client hosting the workflow execution
|
|
29
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
30
|
+
*/
|
|
31
|
+
static getHotMesh(): Promise<HotMesh>;
|
|
16
32
|
/**
|
|
17
33
|
* Spawns a child workflow and awaits the return.
|
|
18
34
|
* @template T - the result type
|
|
@@ -22,6 +38,11 @@ export declare class WorkflowService {
|
|
|
22
38
|
* const result = await Durable.workflow.execChild<typeof resultType>({ ...options });
|
|
23
39
|
*/
|
|
24
40
|
static execChild<T>(options: WorkflowOptions): Promise<T>;
|
|
41
|
+
/**
|
|
42
|
+
* constructs the payload necessary to spawn a child job
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
static getChildInterruptPayload(context: WorkflowContext, options: WorkflowOptions, execIndex: number): DurableChildErrorType;
|
|
25
46
|
/**
|
|
26
47
|
* Spawns a child workflow and returns the child Job ID.
|
|
27
48
|
* This method guarantees the spawned child has reserved the Job ID,
|
|
@@ -53,26 +74,17 @@ export declare class WorkflowService {
|
|
|
53
74
|
*/
|
|
54
75
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
|
|
55
76
|
static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
|
|
77
|
+
/**
|
|
78
|
+
* constructs the payload necessary to spawn a proxyActivity job
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
static getProxyInterruptPayload(context: WorkflowContext, activityName: string, execIndex: number, args: any[], options?: ActivityConfig): DurableProxyErrorType;
|
|
56
82
|
/**
|
|
57
83
|
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
58
84
|
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
59
85
|
* @returns {Promise<Search>} - a search session
|
|
60
86
|
*/
|
|
61
87
|
static search(): Promise<Search>;
|
|
62
|
-
/**
|
|
63
|
-
* Returns the synchronous output from the activity (replay)
|
|
64
|
-
* if available locally
|
|
65
|
-
* @param {string} prefix - one of: proxy, child, start, wait etc
|
|
66
|
-
* @returns
|
|
67
|
-
*/
|
|
68
|
-
static didRun(prefix: string): Promise<[boolean, number, any]>;
|
|
69
|
-
/**
|
|
70
|
-
* Those methods that may only be called once must be protected by flagging
|
|
71
|
-
* their execution with a unique key (the key is stored in the HASH alongside
|
|
72
|
-
* process state and job state)
|
|
73
|
-
* @private
|
|
74
|
-
*/
|
|
75
|
-
static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
76
88
|
/**
|
|
77
89
|
* Returns a random number between 0 and 1. This number is deterministic
|
|
78
90
|
* and will never vary for a given seed. This is useful for randomizing
|
|
@@ -11,19 +11,51 @@ const storage_1 = require("../../modules/storage");
|
|
|
11
11
|
const utils_1 = require("../../modules/utils");
|
|
12
12
|
const search_1 = require("./search");
|
|
13
13
|
const worker_1 = require("./worker");
|
|
14
|
+
const serializer_1 = require("../serializer");
|
|
14
15
|
const stream_1 = require("../../types/stream");
|
|
15
16
|
const enums_1 = require("../../modules/enums");
|
|
16
|
-
const serializer_1 = require("../serializer");
|
|
17
17
|
class WorkflowService {
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Returns the synchronous output from the activity (replay)
|
|
20
|
+
* if available locally, revealing whether or not the activity already
|
|
21
|
+
* ran during a prior execution cycle
|
|
22
|
+
* @param {string} prefix - one of: proxy, child, start, wait etc
|
|
23
|
+
* @returns
|
|
21
24
|
*/
|
|
22
|
-
static async
|
|
25
|
+
static async didRun(prefix) {
|
|
26
|
+
const { COUNTER, replay, workflowDimension, } = WorkflowService.getContext();
|
|
27
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
28
|
+
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
29
|
+
if (sessionId in replay) {
|
|
30
|
+
const restored = serializer_1.SerializerService.fromString(replay[sessionId]);
|
|
31
|
+
return [true, execIndex, restored];
|
|
32
|
+
}
|
|
33
|
+
return [false, execIndex, null];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Those methods that may only be called once must be protected by flagging
|
|
37
|
+
* their execution with a unique key (the key is stored in the HASH alongside
|
|
38
|
+
* process state and job state)
|
|
39
|
+
* @private
|
|
40
|
+
*/
|
|
41
|
+
static async isSideEffectAllowed(hotMeshClient, prefix) {
|
|
23
42
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
43
|
+
const workflowId = store.get('workflowId');
|
|
44
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
45
|
+
const COUNTER = store.get('counter');
|
|
46
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
47
|
+
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
48
|
+
const replay = store.get('replay');
|
|
49
|
+
if (sessionId in replay) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const keyParams = {
|
|
53
|
+
appId: hotMeshClient.appId,
|
|
54
|
+
jobId: workflowId
|
|
55
|
+
};
|
|
56
|
+
const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
57
|
+
const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
|
|
58
|
+
return guidValue === 1;
|
|
27
59
|
}
|
|
28
60
|
/**
|
|
29
61
|
* Returns the current workflow context restored
|
|
@@ -34,9 +66,11 @@ class WorkflowService {
|
|
|
34
66
|
const workflowId = store.get('workflowId');
|
|
35
67
|
const replay = store.get('replay');
|
|
36
68
|
const cursor = store.get('cursor');
|
|
69
|
+
const interruptionRegistry = store.get('interruptionRegistry');
|
|
37
70
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
38
71
|
const workflowTopic = store.get('workflowTopic');
|
|
39
72
|
const namespace = store.get('namespace');
|
|
73
|
+
const originJobId = store.get('originJobId');
|
|
40
74
|
const workflowTrace = store.get('workflowTrace');
|
|
41
75
|
const canRetry = store.get('canRetry');
|
|
42
76
|
const workflowSpan = store.get('workflowSpan');
|
|
@@ -47,7 +81,9 @@ class WorkflowService {
|
|
|
47
81
|
COUNTER,
|
|
48
82
|
counter: COUNTER.counter,
|
|
49
83
|
cursor,
|
|
84
|
+
interruptionRegistry,
|
|
50
85
|
namespace,
|
|
86
|
+
originJobId,
|
|
51
87
|
raw,
|
|
52
88
|
replay,
|
|
53
89
|
workflowId,
|
|
@@ -57,6 +93,16 @@ class WorkflowService {
|
|
|
57
93
|
workflowSpan,
|
|
58
94
|
};
|
|
59
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Return a handle to the hotmesh client hosting the workflow execution
|
|
98
|
+
* @returns {Promise<HotMesh>} - a hotmesh client
|
|
99
|
+
*/
|
|
100
|
+
static async getHotMesh() {
|
|
101
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
102
|
+
const workflowTopic = store.get('workflowTopic');
|
|
103
|
+
const namespace = store.get('namespace');
|
|
104
|
+
return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
105
|
+
}
|
|
60
106
|
/**
|
|
61
107
|
* Spawns a child workflow and awaits the return.
|
|
62
108
|
* @template T - the result type
|
|
@@ -69,13 +115,14 @@ class WorkflowService {
|
|
|
69
115
|
//SYNC
|
|
70
116
|
//check if the activity already ran (check $error/done)
|
|
71
117
|
const isStartChild = options.await === false;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
const
|
|
118
|
+
const prefix = isStartChild ? 'start' : 'child';
|
|
119
|
+
const [didRun, execIndex, result] = await WorkflowService.didRun(prefix);
|
|
120
|
+
const context = WorkflowService.getContext();
|
|
121
|
+
const { canRetry, interruptionRegistry } = context;
|
|
75
122
|
if (didRun) {
|
|
76
123
|
if (result?.$error && (!result.$error.is_stream_error || (result.$error.is_stream_error && !canRetry))) {
|
|
77
124
|
if (options?.config?.throwOnError !== false) {
|
|
78
|
-
//rethrow remote execution error (simulates
|
|
125
|
+
//rethrow remote execution error (simulates local failure)
|
|
79
126
|
const code = result.$error.code;
|
|
80
127
|
const message = result.$error.message;
|
|
81
128
|
const stack = result.$error.stack;
|
|
@@ -98,18 +145,38 @@ class WorkflowService {
|
|
|
98
145
|
return result.data;
|
|
99
146
|
}
|
|
100
147
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
148
|
+
const interruptionMessage = WorkflowService.getChildInterruptPayload(context, options, execIndex);
|
|
149
|
+
//push the packaged inputs to the registry
|
|
150
|
+
interruptionRegistry.push({
|
|
151
|
+
code: enums_1.HMSH_CODE_DURABLE_CHILD,
|
|
152
|
+
...interruptionMessage,
|
|
153
|
+
});
|
|
154
|
+
//ASYNC
|
|
155
|
+
//sleep (allow others to be packaged / registered) and throw the error
|
|
156
|
+
await (0, utils_1.sleepFor)(0);
|
|
157
|
+
throw new errors_1.DurableChildError(interruptionMessage);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* constructs the payload necessary to spawn a child job
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
static getChildInterruptPayload(context, options, execIndex) {
|
|
164
|
+
const { workflowId, originJobId, workflowDimension } = context;
|
|
165
|
+
let childJobId;
|
|
166
|
+
if (options.workflowId) {
|
|
167
|
+
childJobId = options.workflowId;
|
|
168
|
+
}
|
|
169
|
+
else if (options.entity) {
|
|
170
|
+
childJobId = `${options.entity}-${workflowId.substring(0, 7)}-${(0, utils_1.guid)()}-${workflowDimension}-${execIndex}`;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
childJobId = `-${options.workflowName}-${(0, utils_1.guid)()}-${workflowDimension}-${execIndex}`;
|
|
174
|
+
}
|
|
108
175
|
const parentWorkflowId = workflowId;
|
|
109
176
|
const taskQueueName = options.entity ?? options.taskQueue;
|
|
110
177
|
const workflowName = options.entity ?? options.workflowName;
|
|
111
178
|
const workflowTopic = `${taskQueueName}-${workflowName}`;
|
|
112
|
-
|
|
179
|
+
return {
|
|
113
180
|
arguments: [...(options.args || [])],
|
|
114
181
|
await: options?.await ?? true,
|
|
115
182
|
backoffCoefficient: options?.config?.backoffCoefficient ?? enums_1.HMSH_DURABLE_EXP_BACKOFF,
|
|
@@ -122,15 +189,6 @@ class WorkflowService {
|
|
|
122
189
|
workflowId: childJobId,
|
|
123
190
|
workflowTopic,
|
|
124
191
|
};
|
|
125
|
-
//push the packaged inputs to the registry
|
|
126
|
-
interruptionRegistry.push({
|
|
127
|
-
code: enums_1.HMSH_CODE_DURABLE_CHILD,
|
|
128
|
-
...interruptionMessage,
|
|
129
|
-
});
|
|
130
|
-
//ASYNC
|
|
131
|
-
//sleep (allow others to be packaged / registered) and throw the error
|
|
132
|
-
await (0, utils_1.sleepFor)(0);
|
|
133
|
-
throw new errors_1.DurableChildError(interruptionMessage);
|
|
134
192
|
}
|
|
135
193
|
/**
|
|
136
194
|
* Spawns a child workflow and returns the child Job ID.
|
|
@@ -144,7 +202,7 @@ class WorkflowService {
|
|
|
144
202
|
* const childJobId = await Durable.workflow.startChild({ ...options });
|
|
145
203
|
*/
|
|
146
204
|
static async startChild(options) {
|
|
147
|
-
return
|
|
205
|
+
return WorkflowService.execChild({ ...options, await: false });
|
|
148
206
|
}
|
|
149
207
|
/**
|
|
150
208
|
* Wraps activities in a proxy that durably runs/re-runs them to completion.
|
|
@@ -204,31 +262,9 @@ class WorkflowService {
|
|
|
204
262
|
return result.data;
|
|
205
263
|
}
|
|
206
264
|
//package the interruption inputs
|
|
207
|
-
const
|
|
208
|
-
const interruptionRegistry =
|
|
209
|
-
const
|
|
210
|
-
const workflowId = store.get('workflowId');
|
|
211
|
-
const originJobId = store.get('originJobId');
|
|
212
|
-
const workflowTopic = store.get('workflowTopic');
|
|
213
|
-
const activityTopic = `${workflowTopic}-activity`;
|
|
214
|
-
const activityJobId = `-${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
|
|
215
|
-
let maximumInterval;
|
|
216
|
-
if (options.retryPolicy?.maximumInterval) {
|
|
217
|
-
maximumInterval = (0, ms_1.default)(options.retryPolicy.maximumInterval) / 1000;
|
|
218
|
-
}
|
|
219
|
-
const interruptionMessage = {
|
|
220
|
-
arguments: Array.from(arguments),
|
|
221
|
-
workflowDimension: workflowDimension,
|
|
222
|
-
index: execIndex,
|
|
223
|
-
originJobId: originJobId || workflowId,
|
|
224
|
-
parentWorkflowId: workflowId,
|
|
225
|
-
workflowId: activityJobId,
|
|
226
|
-
workflowTopic: activityTopic,
|
|
227
|
-
activityName,
|
|
228
|
-
backoffCoefficient: options?.retryPolicy?.backoffCoefficient ?? undefined,
|
|
229
|
-
maximumAttempts: options?.retryPolicy?.maximumAttempts ?? undefined,
|
|
230
|
-
maximumInterval: maximumInterval ?? undefined,
|
|
231
|
-
};
|
|
265
|
+
const context = WorkflowService.getContext();
|
|
266
|
+
const { interruptionRegistry } = context;
|
|
267
|
+
const interruptionMessage = WorkflowService.getProxyInterruptPayload(context, activityName, execIndex, Array.from(arguments), options);
|
|
232
268
|
//push the packaged inputs to the registry
|
|
233
269
|
interruptionRegistry.push({
|
|
234
270
|
code: enums_1.HMSH_CODE_DURABLE_PROXY,
|
|
@@ -240,6 +276,32 @@ class WorkflowService {
|
|
|
240
276
|
throw new errors_1.DurableProxyError(interruptionMessage);
|
|
241
277
|
};
|
|
242
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* constructs the payload necessary to spawn a proxyActivity job
|
|
281
|
+
* @private
|
|
282
|
+
*/
|
|
283
|
+
static getProxyInterruptPayload(context, activityName, execIndex, args, options) {
|
|
284
|
+
const { workflowDimension, workflowId, originJobId, workflowTopic } = context;
|
|
285
|
+
const activityTopic = `${workflowTopic}-activity`;
|
|
286
|
+
const activityJobId = `-${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
|
|
287
|
+
let maximumInterval;
|
|
288
|
+
if (options.retryPolicy?.maximumInterval) {
|
|
289
|
+
maximumInterval = (0, ms_1.default)(options.retryPolicy.maximumInterval) / 1000;
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
arguments: args,
|
|
293
|
+
workflowDimension: workflowDimension,
|
|
294
|
+
index: execIndex,
|
|
295
|
+
originJobId: originJobId || workflowId,
|
|
296
|
+
parentWorkflowId: workflowId,
|
|
297
|
+
workflowId: activityJobId,
|
|
298
|
+
workflowTopic: activityTopic,
|
|
299
|
+
activityName,
|
|
300
|
+
backoffCoefficient: options?.retryPolicy?.backoffCoefficient ?? undefined,
|
|
301
|
+
maximumAttempts: options?.retryPolicy?.maximumAttempts ?? undefined,
|
|
302
|
+
maximumInterval: maximumInterval ?? undefined,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
243
305
|
/**
|
|
244
306
|
* Returns a search session for use when reading/writing to the workflow HASH.
|
|
245
307
|
* The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
|
|
@@ -258,46 +320,6 @@ class WorkflowService {
|
|
|
258
320
|
const searchSessionId = `-search${workflowDimension}-${execIndex}`;
|
|
259
321
|
return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
|
|
260
322
|
}
|
|
261
|
-
/**
|
|
262
|
-
* Returns the synchronous output from the activity (replay)
|
|
263
|
-
* if available locally
|
|
264
|
-
* @param {string} prefix - one of: proxy, child, start, wait etc
|
|
265
|
-
* @returns
|
|
266
|
-
*/
|
|
267
|
-
static async didRun(prefix) {
|
|
268
|
-
const { COUNTER, replay, workflowDimension, } = WorkflowService.getContext();
|
|
269
|
-
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
270
|
-
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
271
|
-
if (sessionId in replay) {
|
|
272
|
-
return [true, execIndex, serializer_1.SerializerService.fromString(replay[sessionId])];
|
|
273
|
-
}
|
|
274
|
-
return [false, execIndex, null];
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Those methods that may only be called once must be protected by flagging
|
|
278
|
-
* their execution with a unique key (the key is stored in the HASH alongside
|
|
279
|
-
* process state and job state)
|
|
280
|
-
* @private
|
|
281
|
-
*/
|
|
282
|
-
static async isSideEffectAllowed(hotMeshClient, prefix) {
|
|
283
|
-
const store = storage_1.asyncLocalStorage.getStore();
|
|
284
|
-
const workflowId = store.get('workflowId');
|
|
285
|
-
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
286
|
-
const COUNTER = store.get('counter');
|
|
287
|
-
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
288
|
-
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
289
|
-
const replay = store.get('replay');
|
|
290
|
-
if (sessionId in replay) {
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
const keyParams = {
|
|
294
|
-
appId: hotMeshClient.appId,
|
|
295
|
-
jobId: workflowId
|
|
296
|
-
};
|
|
297
|
-
const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
298
|
-
const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
|
|
299
|
-
return guidValue === 1;
|
|
300
|
-
}
|
|
301
323
|
/**
|
|
302
324
|
* Returns a random number between 0 and 1. This number is deterministic
|
|
303
325
|
* and will never vary for a given seed. This is useful for randomizing
|
|
@@ -388,9 +410,7 @@ class WorkflowService {
|
|
|
388
410
|
* Interrupts a running job
|
|
389
411
|
*/
|
|
390
412
|
static async interrupt(jobId, options = {}) {
|
|
391
|
-
const
|
|
392
|
-
const workflowTopic = store.get('workflowTopic');
|
|
393
|
-
const namespace = store.get('namespace');
|
|
413
|
+
const { workflowTopic, namespace } = WorkflowService.getContext();
|
|
394
414
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
395
415
|
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'interrupt')) {
|
|
396
416
|
return await hotMeshClient.interrupt(`${hotMeshClient.appId}.execute`, jobId, options);
|
|
@@ -15,7 +15,7 @@ import { TaskService } from '../task';
|
|
|
15
15
|
import { AppVID } from '../../types/app';
|
|
16
16
|
import { ActivityType } from '../../types/activity';
|
|
17
17
|
import { CacheMode } from '../../types/cache';
|
|
18
|
-
import { JobState, JobData, JobMetadata, JobOutput, JobStatus, JobInterruptOptions, JobCompletionOptions } from '../../types/job';
|
|
18
|
+
import { JobState, JobData, JobMetadata, JobOutput, JobStatus, JobInterruptOptions, JobCompletionOptions, ExtensionType } from '../../types/job';
|
|
19
19
|
import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '../../types/hotmesh';
|
|
20
20
|
import { JobMessageCallback } from '../../types/quorum';
|
|
21
21
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
@@ -72,7 +72,7 @@ declare class EngineService {
|
|
|
72
72
|
hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
|
|
73
73
|
hookTime(jobId: string, gId: string, topicOrActivity: string, type?: WorkListTaskType): Promise<string | void>;
|
|
74
74
|
hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
75
|
-
pub(topic: string, data: JobData, context?: JobState): Promise<string>;
|
|
75
|
+
pub(topic: string, data: JobData, context?: JobState, extended?: ExtensionType): Promise<string>;
|
|
76
76
|
sub(topic: string, callback: JobMessageCallback): Promise<void>;
|
|
77
77
|
unsub(topic: string): Promise<void>;
|
|
78
78
|
psub(wild: string, callback: JobMessageCallback): Promise<void>;
|
|
@@ -408,10 +408,10 @@ class EngineService {
|
|
|
408
408
|
}
|
|
409
409
|
// ********************** PUB/SUB ENTRY POINT **********************
|
|
410
410
|
//publish (returns just the job id)
|
|
411
|
-
async pub(topic, data, context) {
|
|
411
|
+
async pub(topic, data, context, extended) {
|
|
412
412
|
const activityHandler = await this.initActivity(topic, data, context);
|
|
413
413
|
if (activityHandler) {
|
|
414
|
-
return await activityHandler.process();
|
|
414
|
+
return await activityHandler.process(extended);
|
|
415
415
|
}
|
|
416
416
|
else {
|
|
417
417
|
throw new Error(`unable to process activity for topic ${topic}`);
|
|
@@ -2,7 +2,7 @@ import { EngineService } from '../engine';
|
|
|
2
2
|
import { ILogger } from '../logger';
|
|
3
3
|
import { QuorumService } from '../quorum';
|
|
4
4
|
import { WorkerService } from '../worker';
|
|
5
|
-
import { JobState, JobData, JobOutput, JobStatus, JobInterruptOptions } from '../../types/job';
|
|
5
|
+
import { JobState, JobData, JobOutput, JobStatus, JobInterruptOptions, ExtensionType } from '../../types/job';
|
|
6
6
|
import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
|
|
7
7
|
import { JobMessageCallback, QuorumProfile, ThrottleOptions } from '../../types/quorum';
|
|
8
8
|
import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
|
|
@@ -25,7 +25,7 @@ declare class HotMeshService {
|
|
|
25
25
|
initEngine(config: HotMeshConfig, logger: ILogger): Promise<void>;
|
|
26
26
|
initQuorum(config: HotMeshConfig, engine: EngineService, logger: ILogger): Promise<void>;
|
|
27
27
|
doWork(config: HotMeshConfig, logger: ILogger): Promise<void>;
|
|
28
|
-
pub(topic: string, data?: JobData, context?: JobState): Promise<string>;
|
|
28
|
+
pub(topic: string, data?: JobData, context?: JobState, extended?: ExtensionType): Promise<string>;
|
|
29
29
|
sub(topic: string, callback: JobMessageCallback): Promise<void>;
|
|
30
30
|
unsub(topic: string): Promise<void>;
|
|
31
31
|
psub(wild: string, callback: JobMessageCallback): Promise<void>;
|
|
@@ -68,8 +68,8 @@ class HotMeshService {
|
|
|
68
68
|
this.workers = await worker_1.WorkerService.init(this.namespace, this.appId, this.guid, config, logger);
|
|
69
69
|
}
|
|
70
70
|
// ************* PUB/SUB METHODS *************
|
|
71
|
-
async pub(topic, data = {}, context) {
|
|
72
|
-
return await this.engine?.pub(topic, data, context);
|
|
71
|
+
async pub(topic, data = {}, context, extended) {
|
|
72
|
+
return await this.engine?.pub(topic, data, context, extended);
|
|
73
73
|
}
|
|
74
74
|
async sub(topic, callback) {
|
|
75
75
|
return await this.engine?.sub(topic, callback);
|
package/build/types/durable.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LogLevel } from './logger';
|
|
2
2
|
import { RedisClass, RedisOptions } from './redis';
|
|
3
|
-
import { StringStringType } from './serializer';
|
|
3
|
+
import { StringAnyType, StringStringType } from './serializer';
|
|
4
4
|
import { StreamData, StreamError } from './stream';
|
|
5
5
|
/**
|
|
6
6
|
* Type definition for workflow configuration.
|
|
@@ -56,6 +56,14 @@ type WorkflowContext = {
|
|
|
56
56
|
* the HotMesh App namespace. `durable` is the default.
|
|
57
57
|
*/
|
|
58
58
|
namespace: string;
|
|
59
|
+
/**
|
|
60
|
+
* holds list of interruption payloads; if list is longer than 1 when the error is thrown, a `collator` subflow will be used
|
|
61
|
+
*/
|
|
62
|
+
interruptionRegistry: any[];
|
|
63
|
+
/**
|
|
64
|
+
* entry point ancestor flow; might be the parent; will never be self
|
|
65
|
+
*/
|
|
66
|
+
originJobId: string;
|
|
59
67
|
/**
|
|
60
68
|
* the workflow/job ID
|
|
61
69
|
*/
|
|
@@ -94,7 +102,7 @@ type WorkflowSearchOptions = {
|
|
|
94
102
|
sortable?: boolean;
|
|
95
103
|
}>;
|
|
96
104
|
/** Additional data as a key-value record */
|
|
97
|
-
data?:
|
|
105
|
+
data?: StringStringType;
|
|
98
106
|
};
|
|
99
107
|
type WorkflowOptions = {
|
|
100
108
|
/**
|
|
@@ -141,6 +149,10 @@ type WorkflowOptions = {
|
|
|
141
149
|
* the full-text-search (RediSearch) options for the workflow
|
|
142
150
|
*/
|
|
143
151
|
search?: WorkflowSearchOptions;
|
|
152
|
+
/**
|
|
153
|
+
* marker data (begins with a -)
|
|
154
|
+
*/
|
|
155
|
+
marker?: StringStringType;
|
|
144
156
|
/**
|
|
145
157
|
* the workflow configuration object
|
|
146
158
|
*/
|
|
@@ -191,7 +203,7 @@ type SignalOptions = {
|
|
|
191
203
|
/**
|
|
192
204
|
* Input data for the signal (any serializable object)
|
|
193
205
|
*/
|
|
194
|
-
data:
|
|
206
|
+
data: StringAnyType;
|
|
195
207
|
/**
|
|
196
208
|
* Execution ID, also known as the job ID
|
|
197
209
|
*/
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type DurableChildErrorType = {
|
|
2
|
+
arguments: string[];
|
|
3
|
+
await?: boolean;
|
|
4
|
+
backoffCoefficient?: number;
|
|
5
|
+
index: number;
|
|
6
|
+
maximumAttempts?: number;
|
|
7
|
+
maximumInterval?: number;
|
|
8
|
+
originJobId: string | null;
|
|
9
|
+
parentWorkflowId: string;
|
|
10
|
+
workflowDimension: string;
|
|
11
|
+
workflowId: string;
|
|
12
|
+
workflowTopic: string;
|
|
13
|
+
};
|
|
14
|
+
export type DurableWaitForAllErrorType = {
|
|
15
|
+
items: string[];
|
|
16
|
+
workflowId: string;
|
|
17
|
+
workflowTopic: string;
|
|
18
|
+
parentWorkflowId: string;
|
|
19
|
+
originJobId: string | null;
|
|
20
|
+
size: number;
|
|
21
|
+
index: number;
|
|
22
|
+
workflowDimension: string;
|
|
23
|
+
};
|
|
24
|
+
export type DurableProxyErrorType = {
|
|
25
|
+
arguments: string[];
|
|
26
|
+
activityName: string;
|
|
27
|
+
backoffCoefficient?: number;
|
|
28
|
+
index: number;
|
|
29
|
+
maximumAttempts?: number;
|
|
30
|
+
maximumInterval?: number;
|
|
31
|
+
originJobId: string | null;
|
|
32
|
+
parentWorkflowId: string;
|
|
33
|
+
workflowDimension: string;
|
|
34
|
+
workflowId: string;
|
|
35
|
+
workflowTopic: string;
|
|
36
|
+
};
|
|
37
|
+
export type DurableWaitForErrorType = {
|
|
38
|
+
signalId: string;
|
|
39
|
+
index: number;
|
|
40
|
+
workflowDimension: string;
|
|
41
|
+
workflowId: string;
|
|
42
|
+
};
|
|
43
|
+
export type DurableSleepErrorType = {
|
|
44
|
+
duration: number;
|
|
45
|
+
index: number;
|
|
46
|
+
workflowDimension: string;
|
|
47
|
+
workflowId: string;
|
|
48
|
+
};
|
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { StringAnyType } from "./serializer";
|
|
2
2
|
export type ExportItem = [(string | null), string, any];
|
|
3
|
+
/**
|
|
4
|
+
* job export data can be large, particularly transitions the timeline
|
|
5
|
+
*/
|
|
6
|
+
export type ExportFields = 'data' | 'state' | 'status' | 'timeline' | 'transitions';
|
|
3
7
|
export interface ExportOptions {
|
|
8
|
+
/**
|
|
9
|
+
* limit the export byte size by specifying an allowlist
|
|
10
|
+
*/
|
|
11
|
+
allow?: Array<ExportFields>;
|
|
12
|
+
/**
|
|
13
|
+
* limit the export byte size by specifying a block list
|
|
14
|
+
*/
|
|
15
|
+
block?: Array<ExportFields>;
|
|
16
|
+
/**
|
|
17
|
+
* If false, do not return timeline values (like child job response, proxy activity response, etc)
|
|
18
|
+
* @default true
|
|
19
|
+
*/
|
|
20
|
+
values?: boolean;
|
|
4
21
|
}
|
|
5
22
|
export type JobAction = {
|
|
6
23
|
cursor: number;
|
|
@@ -37,36 +54,25 @@ export interface ExportTransitions {
|
|
|
37
54
|
export interface ExportCycles {
|
|
38
55
|
[key: string]: string[];
|
|
39
56
|
}
|
|
40
|
-
export type
|
|
57
|
+
export type TimelineType = {
|
|
58
|
+
key: string;
|
|
59
|
+
value: Record<string, any> | string | number | null;
|
|
41
60
|
index: number;
|
|
42
61
|
secondary?: number;
|
|
43
62
|
dimension?: string;
|
|
44
63
|
};
|
|
45
|
-
export
|
|
46
|
-
key: string;
|
|
47
|
-
value: string;
|
|
48
|
-
parts: IdemParts;
|
|
49
|
-
};
|
|
50
|
-
export interface TimelineEntry {
|
|
51
|
-
activity: string;
|
|
52
|
-
dimensions: string;
|
|
53
|
-
created: string;
|
|
54
|
-
updated: string;
|
|
55
|
-
}
|
|
56
|
-
export interface TimestampParts {
|
|
64
|
+
export interface TransitionType {
|
|
57
65
|
activity: string;
|
|
58
66
|
dimensions: string;
|
|
59
67
|
created: string;
|
|
60
68
|
updated: string;
|
|
61
69
|
}
|
|
62
70
|
export interface DurableJobExport {
|
|
63
|
-
data
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
idempotents: IdemType[];
|
|
69
|
-
replay: TimestampParts[];
|
|
71
|
+
data?: StringAnyType;
|
|
72
|
+
state?: StringAnyType;
|
|
73
|
+
status?: number;
|
|
74
|
+
timeline?: TimelineType[];
|
|
75
|
+
transitions?: TransitionType[];
|
|
70
76
|
}
|
|
71
77
|
export interface JobExport {
|
|
72
78
|
dependencies: DependencyExport[];
|