@hotmeshio/hotmesh 0.0.15 → 0.0.17
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 +1 -1
- package/build/package.json +1 -1
- package/build/services/activities/activity.js +11 -3
- package/build/services/activities/trigger.js +1 -1
- package/build/services/durable/client.d.ts +1 -0
- package/build/services/durable/client.js +6 -0
- package/build/services/durable/factory.d.ts +5 -0
- package/build/services/durable/factory.js +83 -2
- package/build/services/durable/workflow.js +20 -9
- package/build/services/store/index.js +3 -0
- package/build/types/durable.d.ts +1 -0
- package/package.json +1 -1
- package/services/activities/activity.ts +11 -3
- package/services/activities/trigger.ts +1 -1
- package/services/durable/client.ts +7 -0
- package/services/durable/factory.ts +83 -2
- package/services/durable/workflow.ts +25 -10
- package/services/store/index.ts +3 -0
- package/types/durable.ts +1 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ The HotMesh SDK is designed to keep your code front-and-center. Write functions
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function saludar(nombre: string): Promise<string> {
|
|
25
|
-
Math.random() > 0.5
|
|
25
|
+
if (Math.random() > 0.5) throw new Error('Random error');
|
|
26
26
|
return `¡Hola, ${nombre}!`;
|
|
27
27
|
}
|
|
28
28
|
```
|
package/build/package.json
CHANGED
|
@@ -159,8 +159,7 @@ class Activity {
|
|
|
159
159
|
return jobStatus;
|
|
160
160
|
}
|
|
161
161
|
catch (error) {
|
|
162
|
-
if (error instanceof errors_1.CollationError) {
|
|
163
|
-
//caused by external over-signaling; the job is complete
|
|
162
|
+
if (error instanceof errors_1.CollationError && error.fault === 'inactive') {
|
|
164
163
|
this.logger.info('process-hook-event-inactive-error', { error });
|
|
165
164
|
return;
|
|
166
165
|
}
|
|
@@ -206,6 +205,10 @@ class Activity {
|
|
|
206
205
|
this.transitionAdjacent(multiResponse, telemetry);
|
|
207
206
|
}
|
|
208
207
|
catch (error) {
|
|
208
|
+
if (error instanceof errors_1.CollationError && error.fault === 'inactive') {
|
|
209
|
+
this.logger.info('process-event-inactive-error', { error });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
209
212
|
this.logger.error('activity-process-event-error', { error });
|
|
210
213
|
telemetry && telemetry.setActivityError(error.message);
|
|
211
214
|
throw error;
|
|
@@ -493,7 +496,12 @@ class Activity {
|
|
|
493
496
|
}
|
|
494
497
|
async transition(adjacencyList, jobStatus) {
|
|
495
498
|
let mIds = [];
|
|
496
|
-
|
|
499
|
+
//emit can be a mapping (allows emissions to be driven by the job state)
|
|
500
|
+
let emit = false;
|
|
501
|
+
if (this.config.emit) {
|
|
502
|
+
emit = pipe_1.Pipe.resolve(this.config.emit, this.context);
|
|
503
|
+
}
|
|
504
|
+
if (jobStatus <= 0 || emit) {
|
|
497
505
|
//activity should not send 'emit' if the job is truly over
|
|
498
506
|
const isTrueEmit = jobStatus > 0;
|
|
499
507
|
await this.engine.runJobCompletionTasks(this.context, isTrueEmit);
|
|
@@ -151,7 +151,7 @@ class Trigger extends activity_1.Activity {
|
|
|
151
151
|
}
|
|
152
152
|
async setStats(multi) {
|
|
153
153
|
const md = this.context.metadata;
|
|
154
|
-
if (this.config.stats?.measures) {
|
|
154
|
+
if (md.key && this.config.stats?.measures) {
|
|
155
155
|
const config = await this.engine.getVID();
|
|
156
156
|
const reporter = new reporter_1.ReporterService(config, this.store, this.logger);
|
|
157
157
|
await this.store.setStats(md.key, md.jid, md.ts, reporter.resolveTriggerStatistics(this.config, this.context), config, multi);
|
|
@@ -10,6 +10,7 @@ export declare class ClientService {
|
|
|
10
10
|
workflow: {
|
|
11
11
|
start: (options: WorkflowOptions) => Promise<WorkflowHandleService>;
|
|
12
12
|
signal: (signalId: string, data: Record<any, any>) => Promise<string>;
|
|
13
|
+
getHandle: (taskQueue: string, workflowName: string, workflowId: string) => Promise<WorkflowHandleService>;
|
|
13
14
|
};
|
|
14
15
|
activateWorkflow(hotMesh: HotMesh, appId?: string, version?: string): Promise<void>;
|
|
15
16
|
static shutdown(): Promise<void>;
|
|
@@ -87,6 +87,7 @@ class ClientService {
|
|
|
87
87
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
88
88
|
const payload = {
|
|
89
89
|
arguments: [...options.args],
|
|
90
|
+
parentWorkflowId: options.parentWorkflowId,
|
|
90
91
|
workflowId: options.workflowId || (0, nanoid_1.nanoid)(),
|
|
91
92
|
workflowTopic: workflowTopic,
|
|
92
93
|
backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
|
|
@@ -97,6 +98,11 @@ class ClientService {
|
|
|
97
98
|
},
|
|
98
99
|
signal: async (signalId, data) => {
|
|
99
100
|
return await (await this.getHotMeshClient('durable.wfs.signal')).hook('durable.wfs.signal', { id: signalId, data });
|
|
101
|
+
},
|
|
102
|
+
getHandle: async (taskQueue, workflowName, workflowId) => {
|
|
103
|
+
const workflowTopic = `${taskQueue}-${workflowName}`;
|
|
104
|
+
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
105
|
+
return new handle_1.WorkflowHandleService(hotMeshClient, workflowTopic, workflowId);
|
|
100
106
|
}
|
|
101
107
|
};
|
|
102
108
|
this.connection = config.connection;
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* NOTE: Using `maxSystemRetries = 3` and `backoffCoefficient = 10`, errant
|
|
3
3
|
* workflows will be retried on the following schedule (8 times in 27 hours):
|
|
4
4
|
* => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
|
|
5
|
+
* 593:
|
|
6
|
+
* 594: waitforsignal
|
|
7
|
+
* 595: sleep
|
|
8
|
+
* 596, 597, 598: fatal
|
|
9
|
+
* 599: retry
|
|
5
10
|
*/
|
|
6
11
|
declare const getWorkflowYAML: (app: string, version: string) => string;
|
|
7
12
|
declare const APP_VERSION = "1";
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* NOTE: Using `maxSystemRetries = 3` and `backoffCoefficient = 10`, errant
|
|
4
4
|
* workflows will be retried on the following schedule (8 times in 27 hours):
|
|
5
5
|
* => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
|
|
6
|
+
* 593:
|
|
7
|
+
* 594: waitforsignal
|
|
8
|
+
* 595: sleep
|
|
9
|
+
* 596, 597, 598: fatal
|
|
10
|
+
* 599: retry
|
|
6
11
|
*/
|
|
7
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
13
|
exports.WFS_HOOK_ID = exports.WFSC_PUBLISHES_TOPIC = exports.WFSC_SUBSCRIBES_TOPIC = exports.WFS_PUBLISHES_TOPIC = exports.WFS_SUBSCRIBES_TOPIC = exports.DEFAULT_COEFFICIENT = exports.SLEEP_HOOK_ID = exports.ACTIVITY_HOOK_ID = exports.HOOK_ID = exports.PUBLISHES_TOPIC = exports.SUBSCRIBES_TOPIC = exports.SLEEP_PUBLISHES_TOPIC = exports.SLEEP_SUBSCRIBES_TOPIC = exports.ACTIVITY_PUBLISHES_TOPIC = exports.ACTIVITY_SUBSCRIBES_TOPIC = exports.APP_ID = exports.APP_VERSION = exports.getWorkflowYAML = void 0;
|
|
@@ -20,6 +25,8 @@ const getWorkflowYAML = (app, version) => {
|
|
|
20
25
|
schema:
|
|
21
26
|
type: object
|
|
22
27
|
properties:
|
|
28
|
+
parentWorkflowId:
|
|
29
|
+
type: string
|
|
23
30
|
workflowId:
|
|
24
31
|
type: string
|
|
25
32
|
arguments:
|
|
@@ -42,6 +49,11 @@ const getWorkflowYAML = (app, version) => {
|
|
|
42
49
|
type: trigger
|
|
43
50
|
stats:
|
|
44
51
|
id: '{$self.input.data.workflowId}'
|
|
52
|
+
key: '{$self.input.data.parentWorkflowId}'
|
|
53
|
+
granularity: infinity
|
|
54
|
+
measures:
|
|
55
|
+
- measure: index
|
|
56
|
+
target: '{$self.input.data.parentWorkflowId}'
|
|
45
57
|
job:
|
|
46
58
|
maps:
|
|
47
59
|
done: false
|
|
@@ -61,6 +73,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
61
73
|
w1:
|
|
62
74
|
type: worker
|
|
63
75
|
topic: '{t1.output.data.workflowTopic}'
|
|
76
|
+
emit: '{$job.data.done}'
|
|
64
77
|
retry:
|
|
65
78
|
'599': [2]
|
|
66
79
|
input:
|
|
@@ -118,6 +131,19 @@ const getWorkflowYAML = (app, version) => {
|
|
|
118
131
|
response: '{$self.output.data.response}'
|
|
119
132
|
done: '{$self.output.data.done}'
|
|
120
133
|
|
|
134
|
+
a2:
|
|
135
|
+
type: activity
|
|
136
|
+
title: Wait for cleanup signal
|
|
137
|
+
hook:
|
|
138
|
+
type: object
|
|
139
|
+
properties:
|
|
140
|
+
done:
|
|
141
|
+
type: boolean
|
|
142
|
+
job:
|
|
143
|
+
maps:
|
|
144
|
+
workflowId: '{t1.output.data.workflowId}'
|
|
145
|
+
|
|
146
|
+
|
|
121
147
|
a594:
|
|
122
148
|
title: Wait for signals
|
|
123
149
|
type: await
|
|
@@ -275,7 +301,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
275
301
|
done: true
|
|
276
302
|
|
|
277
303
|
s2:
|
|
278
|
-
title: Awaken
|
|
304
|
+
title: Awaken sleeping flows so they end and self-clean
|
|
279
305
|
type: signal
|
|
280
306
|
subtype: all
|
|
281
307
|
key_name: parentWorkflowId
|
|
@@ -347,10 +373,55 @@ const getWorkflowYAML = (app, version) => {
|
|
|
347
373
|
type: boolean
|
|
348
374
|
maps:
|
|
349
375
|
done: true
|
|
376
|
+
|
|
377
|
+
s4:
|
|
378
|
+
title: Awaken child FLOWS so they end and self-clean
|
|
379
|
+
type: signal
|
|
380
|
+
subtype: all
|
|
381
|
+
key_name: parentWorkflowId
|
|
382
|
+
key_value:
|
|
383
|
+
'@pipe':
|
|
384
|
+
- ['{$job.metadata.jid}', '-f']
|
|
385
|
+
- ['{@string.concat}']
|
|
386
|
+
topic: ${app}.childflow.awaken
|
|
387
|
+
resolver:
|
|
388
|
+
schema:
|
|
389
|
+
type: object
|
|
390
|
+
properties:
|
|
391
|
+
data:
|
|
392
|
+
type: object
|
|
393
|
+
properties:
|
|
394
|
+
parentWorkflowId:
|
|
395
|
+
type: string
|
|
396
|
+
scrub:
|
|
397
|
+
type: boolean
|
|
398
|
+
maps:
|
|
399
|
+
data:
|
|
400
|
+
parentWorkflowId:
|
|
401
|
+
'@pipe':
|
|
402
|
+
- ['{$job.metadata.jid}', '-f']
|
|
403
|
+
- ['{@string.concat}']
|
|
404
|
+
scrub: true
|
|
405
|
+
signal:
|
|
406
|
+
schema:
|
|
407
|
+
type: object
|
|
408
|
+
properties:
|
|
409
|
+
done:
|
|
410
|
+
type: boolean
|
|
411
|
+
maps:
|
|
412
|
+
done: true
|
|
350
413
|
|
|
351
414
|
transitions:
|
|
352
415
|
t1:
|
|
353
416
|
- to: a1
|
|
417
|
+
- to: a2
|
|
418
|
+
conditions:
|
|
419
|
+
match:
|
|
420
|
+
- expected: true
|
|
421
|
+
actual:
|
|
422
|
+
'@pipe':
|
|
423
|
+
- ['{$job.metadata.key}', true, false]
|
|
424
|
+
- ['{@conditional.ternary}']
|
|
354
425
|
a1:
|
|
355
426
|
- to: w1
|
|
356
427
|
w1:
|
|
@@ -372,11 +443,13 @@ const getWorkflowYAML = (app, version) => {
|
|
|
372
443
|
- to: s3
|
|
373
444
|
conditions:
|
|
374
445
|
code: [200, 598, 597, 596]
|
|
446
|
+
- to: s4
|
|
447
|
+
conditions:
|
|
448
|
+
code: [200, 598, 597, 596]
|
|
375
449
|
a594:
|
|
376
450
|
- to: c594
|
|
377
451
|
conditions:
|
|
378
452
|
code: 202
|
|
379
|
-
|
|
380
453
|
a595:
|
|
381
454
|
- to: c595
|
|
382
455
|
conditions:
|
|
@@ -384,6 +457,14 @@ const getWorkflowYAML = (app, version) => {
|
|
|
384
457
|
a599:
|
|
385
458
|
- to: c599
|
|
386
459
|
|
|
460
|
+
hooks:
|
|
461
|
+
${app}.childflow.awaken:
|
|
462
|
+
- to: a2
|
|
463
|
+
conditions:
|
|
464
|
+
match:
|
|
465
|
+
- expected: '{t1.output.data.workflowId}'
|
|
466
|
+
actual: '{$self.hook.data.id}'
|
|
467
|
+
|
|
387
468
|
- subscribes: ${app}.activity.execute
|
|
388
469
|
publishes: ${app}.activity.executed
|
|
389
470
|
|
|
@@ -51,17 +51,28 @@ class WorkflowService {
|
|
|
51
51
|
const workflowId = store.get('workflowId');
|
|
52
52
|
const workflowTrace = store.get('workflowTrace');
|
|
53
53
|
const workflowSpan = store.get('workflowSpan');
|
|
54
|
+
const COUNTER = store.get('counter');
|
|
55
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
56
|
+
const childJobId = `${workflowId}-$${options.workflowName}-${execIndex}`;
|
|
57
|
+
const parentWorkflowId = `${workflowId}-f`;
|
|
54
58
|
const client = new client_1.ClientService({
|
|
55
59
|
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
56
60
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId);
|
|
62
|
+
try {
|
|
63
|
+
return await handle.result();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
handle = await client.workflow.start({
|
|
67
|
+
...options,
|
|
68
|
+
workflowId: childJobId,
|
|
69
|
+
parentWorkflowId,
|
|
70
|
+
workflowTrace,
|
|
71
|
+
workflowSpan,
|
|
72
|
+
});
|
|
73
|
+
const result = await handle.result();
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
65
76
|
}
|
|
66
77
|
static proxyActivities(options) {
|
|
67
78
|
if (options.activities) {
|
|
@@ -168,7 +179,7 @@ class WorkflowService {
|
|
|
168
179
|
const trc = store.get('workflowTrace');
|
|
169
180
|
const spn = store.get('workflowSpan');
|
|
170
181
|
const activityTopic = `${workflowTopic}-activity`;
|
|
171
|
-
const activityJobId = `${workflowId}
|
|
182
|
+
const activityJobId = `${workflowId}-$${activityName}-${execIndex}`;
|
|
172
183
|
let activityState;
|
|
173
184
|
try {
|
|
174
185
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
|
|
@@ -373,6 +373,9 @@ class StoreService {
|
|
|
373
373
|
async getStatus(jobId, appId) {
|
|
374
374
|
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId, jobId });
|
|
375
375
|
const status = await this.redisClient[this.commands.hget](jobKey, ':');
|
|
376
|
+
if (status === null) {
|
|
377
|
+
throw new Error(`Job ${jobId} not found`);
|
|
378
|
+
}
|
|
376
379
|
return Number(status);
|
|
377
380
|
}
|
|
378
381
|
async setState({ ...state }, status, jobId, symbolNames, dIds, multi) {
|
package/build/types/durable.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -209,8 +209,7 @@ class Activity {
|
|
|
209
209
|
telemetry.setActivityAttributes(attrs);
|
|
210
210
|
return jobStatus as number;
|
|
211
211
|
} catch (error) {
|
|
212
|
-
if (error instanceof CollationError) {
|
|
213
|
-
//caused by external over-signaling; the job is complete
|
|
212
|
+
if (error instanceof CollationError && error.fault === 'inactive') {
|
|
214
213
|
this.logger.info('process-hook-event-inactive-error', { error });
|
|
215
214
|
return;
|
|
216
215
|
}
|
|
@@ -256,6 +255,10 @@ class Activity {
|
|
|
256
255
|
}
|
|
257
256
|
this.transitionAdjacent(multiResponse, telemetry);
|
|
258
257
|
} catch (error) {
|
|
258
|
+
if (error instanceof CollationError && error.fault === 'inactive') {
|
|
259
|
+
this.logger.info('process-event-inactive-error', { error });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
259
262
|
this.logger.error('activity-process-event-error', { error });
|
|
260
263
|
telemetry && telemetry.setActivityError(error.message);
|
|
261
264
|
throw error;
|
|
@@ -581,7 +584,12 @@ class Activity {
|
|
|
581
584
|
|
|
582
585
|
async transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]> {
|
|
583
586
|
let mIds: string[] = [];
|
|
584
|
-
|
|
587
|
+
//emit can be a mapping (allows emissions to be driven by the job state)
|
|
588
|
+
let emit: boolean = false;
|
|
589
|
+
if (this.config.emit) {
|
|
590
|
+
emit = Pipe.resolve(this.config.emit, this.context);
|
|
591
|
+
}
|
|
592
|
+
if (jobStatus <= 0 || emit) {
|
|
585
593
|
//activity should not send 'emit' if the job is truly over
|
|
586
594
|
const isTrueEmit = jobStatus > 0;
|
|
587
595
|
await this.engine.runJobCompletionTasks(this.context, isTrueEmit);
|
|
@@ -178,7 +178,7 @@ class Trigger extends Activity {
|
|
|
178
178
|
|
|
179
179
|
async setStats(multi?: RedisMulti): Promise<void> {
|
|
180
180
|
const md = this.context.metadata;
|
|
181
|
-
if (this.config.stats?.measures) {
|
|
181
|
+
if (md.key && this.config.stats?.measures) {
|
|
182
182
|
const config = await this.engine.getVID();
|
|
183
183
|
const reporter = new ReporterService(config, this.store, this.logger);
|
|
184
184
|
await this.store.setStats(
|
|
@@ -101,6 +101,7 @@ export class ClientService {
|
|
|
101
101
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
102
102
|
const payload = {
|
|
103
103
|
arguments: [...options.args],
|
|
104
|
+
parentWorkflowId: options.parentWorkflowId,
|
|
104
105
|
workflowId: options.workflowId || nanoid(),
|
|
105
106
|
workflowTopic: workflowTopic,
|
|
106
107
|
backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
|
|
@@ -115,6 +116,12 @@ export class ClientService {
|
|
|
115
116
|
|
|
116
117
|
signal: async (signalId: string, data: Record<any, any>): Promise<string> => {
|
|
117
118
|
return await (await this.getHotMeshClient('durable.wfs.signal')).hook('durable.wfs.signal', { id: signalId, data });
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
getHandle: async (taskQueue: string, workflowName: string, workflowId: string): Promise<WorkflowHandleService> => {
|
|
122
|
+
const workflowTopic = `${taskQueue}-${workflowName}`;
|
|
123
|
+
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
124
|
+
return new WorkflowHandleService(hotMeshClient, workflowTopic, workflowId);
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
* NOTE: Using `maxSystemRetries = 3` and `backoffCoefficient = 10`, errant
|
|
3
3
|
* workflows will be retried on the following schedule (8 times in 27 hours):
|
|
4
4
|
* => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
|
|
5
|
+
* 593:
|
|
6
|
+
* 594: waitforsignal
|
|
7
|
+
* 595: sleep
|
|
8
|
+
* 596, 597, 598: fatal
|
|
9
|
+
* 599: retry
|
|
5
10
|
*/
|
|
6
11
|
|
|
7
12
|
//todo: getChildWorkflowYAML (includes key, so flow will cleanup)
|
|
@@ -19,6 +24,8 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
19
24
|
schema:
|
|
20
25
|
type: object
|
|
21
26
|
properties:
|
|
27
|
+
parentWorkflowId:
|
|
28
|
+
type: string
|
|
22
29
|
workflowId:
|
|
23
30
|
type: string
|
|
24
31
|
arguments:
|
|
@@ -41,6 +48,11 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
41
48
|
type: trigger
|
|
42
49
|
stats:
|
|
43
50
|
id: '{$self.input.data.workflowId}'
|
|
51
|
+
key: '{$self.input.data.parentWorkflowId}'
|
|
52
|
+
granularity: infinity
|
|
53
|
+
measures:
|
|
54
|
+
- measure: index
|
|
55
|
+
target: '{$self.input.data.parentWorkflowId}'
|
|
44
56
|
job:
|
|
45
57
|
maps:
|
|
46
58
|
done: false
|
|
@@ -60,6 +72,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
60
72
|
w1:
|
|
61
73
|
type: worker
|
|
62
74
|
topic: '{t1.output.data.workflowTopic}'
|
|
75
|
+
emit: '{$job.data.done}'
|
|
63
76
|
retry:
|
|
64
77
|
'599': [2]
|
|
65
78
|
input:
|
|
@@ -117,6 +130,19 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
117
130
|
response: '{$self.output.data.response}'
|
|
118
131
|
done: '{$self.output.data.done}'
|
|
119
132
|
|
|
133
|
+
a2:
|
|
134
|
+
type: activity
|
|
135
|
+
title: Wait for cleanup signal
|
|
136
|
+
hook:
|
|
137
|
+
type: object
|
|
138
|
+
properties:
|
|
139
|
+
done:
|
|
140
|
+
type: boolean
|
|
141
|
+
job:
|
|
142
|
+
maps:
|
|
143
|
+
workflowId: '{t1.output.data.workflowId}'
|
|
144
|
+
|
|
145
|
+
|
|
120
146
|
a594:
|
|
121
147
|
title: Wait for signals
|
|
122
148
|
type: await
|
|
@@ -274,7 +300,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
274
300
|
done: true
|
|
275
301
|
|
|
276
302
|
s2:
|
|
277
|
-
title: Awaken
|
|
303
|
+
title: Awaken sleeping flows so they end and self-clean
|
|
278
304
|
type: signal
|
|
279
305
|
subtype: all
|
|
280
306
|
key_name: parentWorkflowId
|
|
@@ -346,10 +372,55 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
346
372
|
type: boolean
|
|
347
373
|
maps:
|
|
348
374
|
done: true
|
|
375
|
+
|
|
376
|
+
s4:
|
|
377
|
+
title: Awaken child FLOWS so they end and self-clean
|
|
378
|
+
type: signal
|
|
379
|
+
subtype: all
|
|
380
|
+
key_name: parentWorkflowId
|
|
381
|
+
key_value:
|
|
382
|
+
'@pipe':
|
|
383
|
+
- ['{$job.metadata.jid}', '-f']
|
|
384
|
+
- ['{@string.concat}']
|
|
385
|
+
topic: ${app}.childflow.awaken
|
|
386
|
+
resolver:
|
|
387
|
+
schema:
|
|
388
|
+
type: object
|
|
389
|
+
properties:
|
|
390
|
+
data:
|
|
391
|
+
type: object
|
|
392
|
+
properties:
|
|
393
|
+
parentWorkflowId:
|
|
394
|
+
type: string
|
|
395
|
+
scrub:
|
|
396
|
+
type: boolean
|
|
397
|
+
maps:
|
|
398
|
+
data:
|
|
399
|
+
parentWorkflowId:
|
|
400
|
+
'@pipe':
|
|
401
|
+
- ['{$job.metadata.jid}', '-f']
|
|
402
|
+
- ['{@string.concat}']
|
|
403
|
+
scrub: true
|
|
404
|
+
signal:
|
|
405
|
+
schema:
|
|
406
|
+
type: object
|
|
407
|
+
properties:
|
|
408
|
+
done:
|
|
409
|
+
type: boolean
|
|
410
|
+
maps:
|
|
411
|
+
done: true
|
|
349
412
|
|
|
350
413
|
transitions:
|
|
351
414
|
t1:
|
|
352
415
|
- to: a1
|
|
416
|
+
- to: a2
|
|
417
|
+
conditions:
|
|
418
|
+
match:
|
|
419
|
+
- expected: true
|
|
420
|
+
actual:
|
|
421
|
+
'@pipe':
|
|
422
|
+
- ['{$job.metadata.key}', true, false]
|
|
423
|
+
- ['{@conditional.ternary}']
|
|
353
424
|
a1:
|
|
354
425
|
- to: w1
|
|
355
426
|
w1:
|
|
@@ -371,11 +442,13 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
371
442
|
- to: s3
|
|
372
443
|
conditions:
|
|
373
444
|
code: [200, 598, 597, 596]
|
|
445
|
+
- to: s4
|
|
446
|
+
conditions:
|
|
447
|
+
code: [200, 598, 597, 596]
|
|
374
448
|
a594:
|
|
375
449
|
- to: c594
|
|
376
450
|
conditions:
|
|
377
451
|
code: 202
|
|
378
|
-
|
|
379
452
|
a595:
|
|
380
453
|
- to: c595
|
|
381
454
|
conditions:
|
|
@@ -383,6 +456,14 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
383
456
|
a599:
|
|
384
457
|
- to: c599
|
|
385
458
|
|
|
459
|
+
hooks:
|
|
460
|
+
${app}.childflow.awaken:
|
|
461
|
+
- to: a2
|
|
462
|
+
conditions:
|
|
463
|
+
match:
|
|
464
|
+
- expected: '{t1.output.data.workflowId}'
|
|
465
|
+
actual: '{$self.hook.data.id}'
|
|
466
|
+
|
|
386
467
|
- subscribes: ${app}.activity.execute
|
|
387
468
|
publishes: ${app}.activity.executed
|
|
388
469
|
|
|
@@ -8,7 +8,6 @@ import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable"
|
|
|
8
8
|
import { JobOutput, JobState } from '../../types';
|
|
9
9
|
import { ACTIVITY_PUBLISHES_TOPIC, ACTIVITY_SUBSCRIBES_TOPIC, SLEEP_SUBSCRIBES_TOPIC, WFS_SUBSCRIBES_TOPIC } from './factory';
|
|
10
10
|
import { DurableIncompleteSignalError, DurableSleepError, DurableWaitForSignalError } from '../../modules/errors';
|
|
11
|
-
import { stringify } from 'querystring';
|
|
12
11
|
|
|
13
12
|
/*
|
|
14
13
|
`proxyActivities` returns a wrapped instance of the
|
|
@@ -52,18 +51,34 @@ export class WorkflowService {
|
|
|
52
51
|
const workflowId = store.get('workflowId');
|
|
53
52
|
const workflowTrace = store.get('workflowTrace');
|
|
54
53
|
const workflowSpan = store.get('workflowSpan');
|
|
54
|
+
const COUNTER = store.get('counter');
|
|
55
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
56
|
+
const childJobId = `${workflowId}-$${options.workflowName}-${execIndex}`;
|
|
57
|
+
const parentWorkflowId = `${workflowId}-f`;
|
|
55
58
|
|
|
56
59
|
const client = new Client({
|
|
57
60
|
connection: await Connection.connect(WorkerService.connection),
|
|
58
61
|
});
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
|
|
63
|
+
let handle = await client.workflow.getHandle(
|
|
64
|
+
options.taskQueue,
|
|
65
|
+
options.workflowName,
|
|
66
|
+
childJobId
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return await handle.result() as T;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
handle = await client.workflow.start({
|
|
73
|
+
...options,
|
|
74
|
+
workflowId: childJobId,
|
|
75
|
+
parentWorkflowId,
|
|
76
|
+
workflowTrace,
|
|
77
|
+
workflowSpan,
|
|
78
|
+
});
|
|
79
|
+
const result = await handle.result();
|
|
80
|
+
return result as T;
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
|
|
69
84
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT> {
|
|
@@ -172,7 +187,7 @@ export class WorkflowService {
|
|
|
172
187
|
const trc = store.get('workflowTrace');
|
|
173
188
|
const spn = store.get('workflowSpan');
|
|
174
189
|
const activityTopic = `${workflowTopic}-activity`;
|
|
175
|
-
const activityJobId = `${workflowId}
|
|
190
|
+
const activityJobId = `${workflowId}-$${activityName}-${execIndex}`;
|
|
176
191
|
|
|
177
192
|
let activityState: JobOutput
|
|
178
193
|
try {
|
package/services/store/index.ts
CHANGED
|
@@ -452,6 +452,9 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
452
452
|
async getStatus(jobId: string, appId: string): Promise<number> {
|
|
453
453
|
const jobKey = this.mintKey(KeyType.JOB_STATE, { appId, jobId });
|
|
454
454
|
const status = await this.redisClient[this.commands.hget](jobKey, ':');
|
|
455
|
+
if (status === null) {
|
|
456
|
+
throw new Error(`Job ${jobId} not found`);
|
|
457
|
+
}
|
|
455
458
|
return Number(status);
|
|
456
459
|
}
|
|
457
460
|
|
package/types/durable.ts
CHANGED
|
@@ -12,6 +12,7 @@ type WorkflowOptions = {
|
|
|
12
12
|
args: any[]; //input arguments to pass in
|
|
13
13
|
workflowId: string; //execution id (the job id)
|
|
14
14
|
workflowName?: string; //the name of the user's workflow function
|
|
15
|
+
parentWorkflowId?: string; //system reserved; the id of the parent; if present the flow will not self-clean until the parent that spawned it self-cleans
|
|
15
16
|
workflowTrace?: string;
|
|
16
17
|
workflowSpan?: string;
|
|
17
18
|
config?: WorkflowConfig;
|