@hotmeshio/hotmesh 0.0.7 → 0.0.8
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/build/cjs/package.json +3 -1
- package/build/cjs/services/activities/activity.d.ts +6 -0
- package/build/cjs/services/activities/activity.js +83 -7
- package/build/cjs/services/activities/await.d.ts +2 -2
- package/build/cjs/services/activities/await.js +5 -5
- package/build/cjs/services/activities/cycle.d.ts +19 -0
- package/build/cjs/services/activities/cycle.js +77 -0
- package/build/cjs/services/activities/index.d.ts +4 -2
- package/build/cjs/services/activities/index.js +4 -2
- package/build/cjs/services/activities/worker.d.ts +0 -8
- package/build/cjs/services/activities/worker.js +1 -87
- package/build/cjs/services/collator/index.d.ts +18 -1
- package/build/cjs/services/collator/index.js +41 -11
- package/build/cjs/services/durable/factory.js +17 -1
- package/build/cjs/services/durable/worker.d.ts +1 -0
- package/build/cjs/services/durable/worker.js +9 -14
- package/build/cjs/services/durable/workflow.js +5 -1
- package/build/cjs/services/mapper/index.js +3 -0
- package/build/cjs/services/signaler/stream.js +0 -1
- package/build/cjs/types/activity.d.ts +7 -2
- package/build/cjs/types/index.d.ts +1 -1
- package/build/esm/package.json +3 -1
- package/build/esm/services/activities/activity.d.ts +6 -0
- package/build/esm/services/activities/activity.js +83 -7
- package/build/esm/services/activities/await.d.ts +2 -2
- package/build/esm/services/activities/await.js +5 -5
- package/build/esm/services/activities/cycle.d.ts +19 -0
- package/build/esm/services/activities/cycle.js +74 -0
- package/build/esm/services/activities/index.d.ts +4 -2
- package/build/esm/services/activities/index.js +4 -2
- package/build/esm/services/activities/worker.d.ts +0 -8
- package/build/esm/services/activities/worker.js +1 -87
- package/build/esm/services/collator/index.d.ts +18 -1
- package/build/esm/services/collator/index.js +41 -11
- package/build/esm/services/durable/factory.js +17 -1
- package/build/esm/services/durable/worker.d.ts +1 -0
- package/build/esm/services/durable/worker.js +9 -14
- package/build/esm/services/durable/workflow.js +5 -1
- package/build/esm/services/mapper/index.js +3 -0
- package/build/esm/services/signaler/stream.js +0 -1
- package/build/esm/types/activity.d.ts +7 -2
- package/build/esm/types/index.d.ts +1 -1
- package/package.json +3 -1
- package/services/activities/activity.ts +90 -7
- package/services/activities/await.ts +5 -5
- package/services/activities/cycle.ts +96 -0
- package/services/activities/index.ts +4 -2
- package/services/activities/worker.ts +2 -93
- package/services/collator/index.ts +43 -11
- package/services/durable/factory.ts +17 -1
- package/services/durable/worker.ts +10 -13
- package/services/durable/workflow.ts +4 -1
- package/services/mapper/index.ts +3 -0
- package/services/signaler/stream.ts +0 -1
- package/types/activity.ts +8 -1
- package/types/index.ts +1 -0
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
import { CollationError } from '../../modules/errors';
|
|
2
2
|
import { CollationFaultType } from '../../types/collator';
|
|
3
3
|
class CollatorService {
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* returns the dimensional address (dad) for the target; due
|
|
6
|
+
* to the nature of the notary system, the dad for leg 2 entry
|
|
7
|
+
* must target the `0` index while leg 2 exit must target the
|
|
8
|
+
* current index (0)
|
|
9
|
+
*/
|
|
10
|
+
static getDimensionalAddress(activity, isEntry = false) {
|
|
5
11
|
let dad = activity.context.metadata.dad || activity.metadata.dad;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
|
|
10
|
-
// }
|
|
12
|
+
if (isEntry && dad && activity.leg === 2) {
|
|
13
|
+
dad = `${dad.substring(0, dad.lastIndexOf(','))},0`;
|
|
14
|
+
}
|
|
11
15
|
return CollatorService.getDimensionsById([...activity.config.ancestors, activity.metadata.aid], dad);
|
|
12
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* resolves the dimensional address for the
|
|
19
|
+
* ancestor in the graph to go back to. this address
|
|
20
|
+
* is determined by trimming the last digits from
|
|
21
|
+
* the `dad` (including the target).
|
|
22
|
+
* the target activity index is then set to `0`, so that
|
|
23
|
+
* the origin node can be queried for approval/entry.
|
|
24
|
+
*/
|
|
25
|
+
static resolveReentryDimension(activity) {
|
|
26
|
+
const targetActivityId = activity.config.ancestor;
|
|
27
|
+
const ancestors = activity.config.ancestors;
|
|
28
|
+
const ancestorIndex = ancestors.indexOf(targetActivityId);
|
|
29
|
+
const dimensions = activity.metadata.dad.split(','); //e.g., `,0,0,1,0`
|
|
30
|
+
dimensions.length = ancestorIndex + 1;
|
|
31
|
+
dimensions.push('0');
|
|
32
|
+
return dimensions.join(',');
|
|
33
|
+
}
|
|
13
34
|
static async notarizeEntry(activity, multi) {
|
|
14
35
|
//decrement by -100_000_000_000_000
|
|
15
36
|
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -100000000000000, this.getDimensionalAddress(activity), multi);
|
|
@@ -24,14 +45,21 @@ class CollatorService {
|
|
|
24
45
|
//this.verifyInteger(amount, 1, 'exit');
|
|
25
46
|
return amount;
|
|
26
47
|
}
|
|
48
|
+
static async notarizeEarlyExit(activity, multi) {
|
|
49
|
+
//decrement the 2nd and 3rd digits to fully deactivate (`cycle` activities use this command to fully exit after leg 1) (should result in `888000000000000`)
|
|
50
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, -11000000000000, this.getDimensionalAddress(activity), multi);
|
|
51
|
+
}
|
|
52
|
+
;
|
|
27
53
|
static async notarizeEarlyCompletion(activity, multi) {
|
|
28
|
-
//initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd
|
|
29
|
-
|
|
54
|
+
//initialize both `possible` (1m) and `actualized` (1) zero dimension, while decrementing the 2nd
|
|
55
|
+
//3rd digit is optionally kept open if the activity might be used in a cycle
|
|
56
|
+
const decrement = activity.config.cycle ? 10000000000000 : 11000000000000;
|
|
57
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - decrement, this.getDimensionalAddress(activity), multi);
|
|
30
58
|
}
|
|
31
59
|
;
|
|
32
60
|
static async notarizeReentry(activity, multi) {
|
|
33
61
|
//increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
|
|
34
|
-
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity), multi);
|
|
62
|
+
const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity, true), multi);
|
|
35
63
|
this.verifyInteger(amount, 2, 'enter');
|
|
36
64
|
return amount;
|
|
37
65
|
}
|
|
@@ -42,8 +70,10 @@ class CollatorService {
|
|
|
42
70
|
}
|
|
43
71
|
;
|
|
44
72
|
static async notarizeCompletion(activity, multi) {
|
|
45
|
-
//
|
|
46
|
-
|
|
73
|
+
//1) ALWAYS actualize leg2 dimension (+1)
|
|
74
|
+
//2) IF the activity is used in a cycle, don't close leg 2!
|
|
75
|
+
const decrement = activity.config.cycle ? 0 : -1000000000000;
|
|
76
|
+
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), multi);
|
|
47
77
|
}
|
|
48
78
|
;
|
|
49
79
|
static getDigitAtIndex(num, targetDigitIndex) {
|
|
@@ -26,7 +26,12 @@ const getWorkflowYAML = (topic, version = '1') => {
|
|
|
26
26
|
type: trigger
|
|
27
27
|
stats:
|
|
28
28
|
id: '{$self.input.data.workflowId}'
|
|
29
|
+
|
|
29
30
|
a1:
|
|
31
|
+
type: activity
|
|
32
|
+
cycle: true
|
|
33
|
+
|
|
34
|
+
w1:
|
|
30
35
|
type: worker
|
|
31
36
|
topic: ${topic}
|
|
32
37
|
input:
|
|
@@ -49,9 +54,20 @@ const getWorkflowYAML = (topic, version = '1') => {
|
|
|
49
54
|
job:
|
|
50
55
|
maps:
|
|
51
56
|
response: '{$self.output.data.response}'
|
|
57
|
+
|
|
58
|
+
c1:
|
|
59
|
+
type: cycle
|
|
60
|
+
ancestor: a1
|
|
52
61
|
transitions:
|
|
53
62
|
t1:
|
|
54
|
-
- to: a1
|
|
63
|
+
- to: a1
|
|
64
|
+
a1:
|
|
65
|
+
- to: w1
|
|
66
|
+
w1:
|
|
67
|
+
- to: c1
|
|
68
|
+
conditions:
|
|
69
|
+
code: 500
|
|
70
|
+
`;
|
|
55
71
|
};
|
|
56
72
|
const getActivityYAML = (topic, version = '1') => {
|
|
57
73
|
return `app:
|
|
@@ -5,6 +5,7 @@ export declare class WorkerService {
|
|
|
5
5
|
static connection: Connection;
|
|
6
6
|
static instances: Map<string, HotMesh | Promise<HotMesh>>;
|
|
7
7
|
workflowRunner: HotMesh;
|
|
8
|
+
activityRunner: HotMesh;
|
|
8
9
|
static getHotMesh: (worflowTopic: string) => Promise<HotMesh>;
|
|
9
10
|
static activateWorkflow(hotMesh: HotMesh, topic: string, factory: Function): Promise<void>;
|
|
10
11
|
/**
|
|
@@ -94,8 +94,8 @@ class WorkerService {
|
|
|
94
94
|
const workflowTopic = `${baseTopic}`;
|
|
95
95
|
//initialize supporting workflows
|
|
96
96
|
const worker = new WorkerService();
|
|
97
|
-
|
|
98
|
-
await WorkerService.activateWorkflow(activityRunner, activityTopic, getActivityYAML);
|
|
97
|
+
worker.activityRunner = await worker.initActivityWorkflow(config, activityTopic);
|
|
98
|
+
await WorkerService.activateWorkflow(worker.activityRunner, activityTopic, getActivityYAML);
|
|
99
99
|
worker.workflowRunner = await worker.initWorkerWorkflow(config, workflowTopic, workflowFunction);
|
|
100
100
|
await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML);
|
|
101
101
|
return worker;
|
|
@@ -113,12 +113,7 @@ class WorkerService {
|
|
|
113
113
|
return [workflowFunction.name, workflowFunction];
|
|
114
114
|
}
|
|
115
115
|
async run() {
|
|
116
|
-
|
|
117
|
-
this.workflowRunner.engine.logger.info('WorkerService is running');
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
console.log('WorkerService is running');
|
|
121
|
-
}
|
|
116
|
+
this.workflowRunner.engine.logger.info('WorkerService is running');
|
|
122
117
|
}
|
|
123
118
|
async initActivityWorkflow(config, activityTopic) {
|
|
124
119
|
const redisConfig = {
|
|
@@ -153,10 +148,11 @@ class WorkerService {
|
|
|
153
148
|
};
|
|
154
149
|
}
|
|
155
150
|
catch (err) {
|
|
156
|
-
|
|
157
|
-
//todo (make retry configurable)
|
|
151
|
+
this.activityRunner.engine.logger.error('durable-worker-activity-err', err);
|
|
158
152
|
return {
|
|
159
|
-
status: StreamStatus.
|
|
153
|
+
status: StreamStatus.ERROR,
|
|
154
|
+
code: 500,
|
|
155
|
+
message: err.message,
|
|
160
156
|
metadata: { ...data.metadata },
|
|
161
157
|
data: { error: err }
|
|
162
158
|
};
|
|
@@ -173,7 +169,7 @@ class WorkerService {
|
|
|
173
169
|
await hotMesh.activate(version);
|
|
174
170
|
}
|
|
175
171
|
catch (err) {
|
|
176
|
-
|
|
172
|
+
hotMesh.engine.logger.error('durable-worker-activity-deploy-activate-error', err);
|
|
177
173
|
throw err;
|
|
178
174
|
}
|
|
179
175
|
}
|
|
@@ -229,10 +225,9 @@ class WorkerService {
|
|
|
229
225
|
};
|
|
230
226
|
}
|
|
231
227
|
catch (err) {
|
|
232
|
-
//todo: (retryable error types)
|
|
233
228
|
return {
|
|
229
|
+
status: StreamStatus.ERROR,
|
|
234
230
|
code: 500,
|
|
235
|
-
status: StreamStatus.PENDING,
|
|
236
231
|
metadata: { ...data.metadata },
|
|
237
232
|
data: { error: err }
|
|
238
233
|
};
|
|
@@ -94,7 +94,11 @@ export class WorkflowService {
|
|
|
94
94
|
try {
|
|
95
95
|
const hmshInstance = await WorkerService.getHotMesh(activityTopic);
|
|
96
96
|
activityState = await hmshInstance.getState(activityTopic, activityJobId);
|
|
97
|
-
if (activityState.metadata.
|
|
97
|
+
if (activityState.metadata.err) {
|
|
98
|
+
await hmshInstance.scrub(activityJobId);
|
|
99
|
+
throw new Error(activityState.metadata.err);
|
|
100
|
+
}
|
|
101
|
+
else if (activityState.metadata.js === 0) {
|
|
98
102
|
//return immediately
|
|
99
103
|
return activityState.data?.response;
|
|
100
104
|
}
|
|
@@ -47,6 +47,9 @@ class MapperService {
|
|
|
47
47
|
return transitionRule;
|
|
48
48
|
}
|
|
49
49
|
if (code.toString() === (transitionRule.code || 200).toString()) {
|
|
50
|
+
if (!transitionRule.match) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
50
53
|
const orGate = transitionRule.gate === 'or';
|
|
51
54
|
let allAreTrue = true;
|
|
52
55
|
let someAreTrue = false;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MetricTypes } from "./stats";
|
|
2
2
|
import { StreamRetryPolicy } from "./stream";
|
|
3
|
-
type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate';
|
|
3
|
+
type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle';
|
|
4
4
|
type Consumes = Record<string, string[]>;
|
|
5
5
|
interface BaseActivity {
|
|
6
6
|
title?: string;
|
|
@@ -15,6 +15,7 @@ interface BaseActivity {
|
|
|
15
15
|
sleep?: number;
|
|
16
16
|
expire?: number;
|
|
17
17
|
retry?: StreamRetryPolicy;
|
|
18
|
+
cycle?: boolean;
|
|
18
19
|
collationInt?: number;
|
|
19
20
|
consumes?: Consumes;
|
|
20
21
|
PRODUCES?: string[];
|
|
@@ -55,6 +56,10 @@ interface WorkerActivity extends BaseActivity {
|
|
|
55
56
|
interface EmitActivity extends BaseActivity {
|
|
56
57
|
type: 'emit';
|
|
57
58
|
}
|
|
59
|
+
interface CycleActivity extends BaseActivity {
|
|
60
|
+
type: 'cycle';
|
|
61
|
+
ancestor: string;
|
|
62
|
+
}
|
|
58
63
|
interface IterateActivity extends BaseActivity {
|
|
59
64
|
type: 'iterate';
|
|
60
65
|
}
|
|
@@ -84,4 +89,4 @@ type ActivityDataType = {
|
|
|
84
89
|
hook?: Record<string, unknown>;
|
|
85
90
|
};
|
|
86
91
|
type ActivityLeg = 1 | 2;
|
|
87
|
-
export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, BaseActivity, EmitActivity, IterateActivity, TriggerActivity, WorkerActivity };
|
|
92
|
+
export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, BaseActivity, EmitActivity, IterateActivity, TriggerActivity, WorkerActivity };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, EmitActivity, WorkerActivity, IterateActivity, TriggerActivity, TriggerActivityStats } from './activity';
|
|
1
|
+
export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, EmitActivity, WorkerActivity, IterateActivity, TriggerActivity, TriggerActivityStats } from './activity';
|
|
2
2
|
export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
|
|
3
3
|
export { AsyncSignal } from './async';
|
|
4
4
|
export { CacheMode } from './cache';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "Durable Workflows",
|
|
5
5
|
"main": "build/cjs/index.js",
|
|
6
6
|
"module": "build/esm/index.js",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"test": "NODE_ENV=test jest --detectOpenHandles --forceExit --verbose",
|
|
30
30
|
"test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
|
|
31
31
|
"test:compile": "NODE_ENV=test jest ./tests/functional/compile/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
32
|
+
"test:cycle": "NODE_ENV=test jest ./tests/functional/cycle/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
32
33
|
"test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
33
34
|
"test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
34
35
|
"test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
49
50
|
"test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
50
51
|
"test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
52
|
+
"test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
51
53
|
"test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
52
54
|
"test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
|
|
53
55
|
},
|
|
@@ -129,7 +129,7 @@ class Activity {
|
|
|
129
129
|
this.leg = leg;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
//********
|
|
132
|
+
//******** SIGNAL RE-ENTRY POINT ********//
|
|
133
133
|
doesHook(): boolean {
|
|
134
134
|
return !!(this.config.hook?.topic || this.config.sleep);
|
|
135
135
|
}
|
|
@@ -170,11 +170,6 @@ class Activity {
|
|
|
170
170
|
return await this.processHookEvent(jobId);
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
//todo: hooks are currently singletons. but they can support
|
|
174
|
-
// dimensional threads like `await` and `worker` do.
|
|
175
|
-
// Copy code from those activities to support cyclical
|
|
176
|
-
// timehook and eventhook inputs by adding a 'pending'
|
|
177
|
-
// flag to hooks that allows for repeated signals
|
|
178
173
|
async processHookEvent(jobId: string): Promise<JobStatus | void> {
|
|
179
174
|
this.logger.debug('activity-process-hook-event', { jobId });
|
|
180
175
|
let telemetry: TelemetryService;
|
|
@@ -207,7 +202,6 @@ class Activity {
|
|
|
207
202
|
telemetry.setActivityAttributes(attrs);
|
|
208
203
|
return jobStatus as number;
|
|
209
204
|
} catch (error) {
|
|
210
|
-
console.error('this error?', error);
|
|
211
205
|
this.logger.error('engine-process-hook-event-error', error);
|
|
212
206
|
telemetry.setActivityError(error.message);
|
|
213
207
|
throw error;
|
|
@@ -216,6 +210,95 @@ class Activity {
|
|
|
216
210
|
}
|
|
217
211
|
}
|
|
218
212
|
|
|
213
|
+
//******** DUPLEX RE-ENTRY POINT ********//
|
|
214
|
+
async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<void> {
|
|
215
|
+
this.setLeg(2);
|
|
216
|
+
const jid = this.context.metadata.jid;
|
|
217
|
+
const aid = this.metadata.aid;
|
|
218
|
+
this.status = status;
|
|
219
|
+
this.code = code;
|
|
220
|
+
this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
|
|
221
|
+
let telemetry: TelemetryService;
|
|
222
|
+
try {
|
|
223
|
+
await this.getState();
|
|
224
|
+
const aState = await CollatorService.notarizeReentry(this);
|
|
225
|
+
this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
|
|
226
|
+
|
|
227
|
+
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
228
|
+
let isComplete = CollatorService.isActivityComplete(this.context.metadata.js);
|
|
229
|
+
|
|
230
|
+
if (isComplete) {
|
|
231
|
+
this.logger.warn('activity-process-event-duplicate', { jid, aid });
|
|
232
|
+
this.logger.debug('activity-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
telemetry.startActivitySpan(this.leg);
|
|
237
|
+
let multiResponse: MultiResponseFlags;
|
|
238
|
+
if (status === StreamStatus.PENDING) {
|
|
239
|
+
multiResponse = await this.processPending(telemetry);
|
|
240
|
+
} else if (status === StreamStatus.SUCCESS) {
|
|
241
|
+
multiResponse = await this.processSuccess(telemetry);
|
|
242
|
+
} else {
|
|
243
|
+
multiResponse = await this.processError(telemetry);
|
|
244
|
+
}
|
|
245
|
+
this.transitionAdjacent(multiResponse, telemetry);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
this.logger.error('activity-process-event-error', error);
|
|
248
|
+
telemetry.setActivityError(error.message);
|
|
249
|
+
throw error;
|
|
250
|
+
} finally {
|
|
251
|
+
telemetry.endActivitySpan();
|
|
252
|
+
this.logger.debug('activity-process-event-end', { jid, aid });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async processPending(telemetry: TelemetryService): Promise<MultiResponseFlags> {
|
|
257
|
+
this.bindActivityData('output');
|
|
258
|
+
this.adjacencyList = await this.filterAdjacent();
|
|
259
|
+
this.mapJobData();
|
|
260
|
+
const multi = this.store.getMulti();
|
|
261
|
+
await this.setState(multi);
|
|
262
|
+
await CollatorService.notarizeContinuation(this, multi);
|
|
263
|
+
|
|
264
|
+
await this.setStatus(this.adjacencyList.length, multi);
|
|
265
|
+
return await multi.exec() as MultiResponseFlags;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async processSuccess(telemetry: TelemetryService): Promise<MultiResponseFlags> {
|
|
269
|
+
this.bindActivityData('output');
|
|
270
|
+
this.adjacencyList = await this.filterAdjacent();
|
|
271
|
+
this.mapJobData();
|
|
272
|
+
const multi = this.store.getMulti();
|
|
273
|
+
await this.setState(multi);
|
|
274
|
+
await CollatorService.notarizeCompletion(this, multi);
|
|
275
|
+
|
|
276
|
+
await this.setStatus(this.adjacencyList.length - 1, multi);
|
|
277
|
+
return await multi.exec() as MultiResponseFlags;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async processError(telemetry: TelemetryService): Promise<MultiResponseFlags> {
|
|
281
|
+
this.bindActivityError(this.data);
|
|
282
|
+
this.adjacencyList = await this.filterAdjacent();
|
|
283
|
+
const multi = this.store.getMulti();
|
|
284
|
+
await this.setState(multi);
|
|
285
|
+
await CollatorService.notarizeCompletion(this, multi);
|
|
286
|
+
|
|
287
|
+
await this.setStatus(this.adjacencyList.length - 1, multi);
|
|
288
|
+
return await multi.exec() as MultiResponseFlags;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async transitionAdjacent(multiResponse: MultiResponseFlags, telemetry: TelemetryService): Promise<void> {
|
|
292
|
+
telemetry.mapActivityAttributes();
|
|
293
|
+
const jobStatus = this.resolveStatus(multiResponse);
|
|
294
|
+
const attrs: StringScalarType = { 'app.job.jss': jobStatus };
|
|
295
|
+
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
296
|
+
if (messageIds.length) {
|
|
297
|
+
attrs['app.activity.mids'] = messageIds.join(',')
|
|
298
|
+
}
|
|
299
|
+
telemetry.setActivityAttributes(attrs);
|
|
300
|
+
}
|
|
301
|
+
|
|
219
302
|
resolveStatus(multiResponse: MultiResponseFlags): number {
|
|
220
303
|
const activityStatus = multiResponse[multiResponse.length - 1];
|
|
221
304
|
if (Array.isArray(activityStatus)) {
|
|
@@ -44,7 +44,7 @@ class Await extends Activity {
|
|
|
44
44
|
this.mapInputData();
|
|
45
45
|
|
|
46
46
|
const multi = this.store.getMulti();
|
|
47
|
-
//await this.registerTimeout();
|
|
47
|
+
//todo: await this.registerTimeout();
|
|
48
48
|
await CollatorService.authorizeReentry(this, multi);
|
|
49
49
|
await this.setState(multi);
|
|
50
50
|
await this.setStatus(0, multi);
|
|
@@ -121,11 +121,11 @@ class Await extends Activity {
|
|
|
121
121
|
if (status === StreamStatus.SUCCESS) {
|
|
122
122
|
this.bindActivityData('output');
|
|
123
123
|
this.adjacencyList = await this.filterAdjacent();
|
|
124
|
-
multiResponse = await this.
|
|
124
|
+
multiResponse = await this.processSuccessResponse(this.adjacencyList);
|
|
125
125
|
} else {
|
|
126
126
|
this.bindActivityError(this.data);
|
|
127
127
|
this.adjacencyList = await this.filterAdjacent();
|
|
128
|
-
multiResponse = await this.
|
|
128
|
+
multiResponse = await this.processErrorResponse(this.adjacencyList);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
telemetry.mapActivityAttributes();
|
|
@@ -146,7 +146,7 @@ class Await extends Activity {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
async
|
|
149
|
+
async processSuccessResponse(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
|
|
150
150
|
this.mapJobData();
|
|
151
151
|
const multi = this.store.getMulti();
|
|
152
152
|
await this.setState(multi);
|
|
@@ -156,7 +156,7 @@ class Await extends Activity {
|
|
|
156
156
|
return await multi.exec() as MultiResponseFlags;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
async
|
|
159
|
+
async processErrorResponse(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
|
|
160
160
|
//todo: if adjacencyList.length == 0, then map to the job output
|
|
161
161
|
// this method would be added to Base activity class
|
|
162
162
|
//this.mapJobData();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { GetStateError } from '../../modules/errors';
|
|
2
|
+
import { CollatorService } from '../collator';
|
|
3
|
+
import { EngineService } from '../engine';
|
|
4
|
+
import { Activity, ActivityType } from './activity';
|
|
5
|
+
import {
|
|
6
|
+
ActivityData,
|
|
7
|
+
ActivityMetadata,
|
|
8
|
+
CycleActivity } from '../../types/activity';
|
|
9
|
+
import { JobState } from '../../types/job';
|
|
10
|
+
import { MultiResponseFlags, RedisMulti } from '../../types/redis';
|
|
11
|
+
import { StreamData } from '../../types/stream';
|
|
12
|
+
import { TelemetryService } from '../telemetry';
|
|
13
|
+
|
|
14
|
+
class Cycle extends Activity {
|
|
15
|
+
config: CycleActivity;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
config: ActivityType,
|
|
19
|
+
data: ActivityData,
|
|
20
|
+
metadata: ActivityMetadata,
|
|
21
|
+
hook: ActivityData | null,
|
|
22
|
+
engine: EngineService,
|
|
23
|
+
context?: JobState) {
|
|
24
|
+
super(config, data, metadata, hook, engine, context);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
//******** LEG 1 ENTRY ********//
|
|
29
|
+
async process(): Promise<string> {
|
|
30
|
+
this.logger.debug('cycle-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
31
|
+
let telemetry: TelemetryService;
|
|
32
|
+
try {
|
|
33
|
+
//verify entry is allowed
|
|
34
|
+
this.setLeg(1);
|
|
35
|
+
await CollatorService.notarizeEntry(this);
|
|
36
|
+
await this.getState();
|
|
37
|
+
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
38
|
+
telemetry.startActivitySpan(this.leg);
|
|
39
|
+
this.mapInputData();
|
|
40
|
+
|
|
41
|
+
//set state/status
|
|
42
|
+
let multi = this.store.getMulti();
|
|
43
|
+
await this.setState(multi);
|
|
44
|
+
await this.setStatus(0, multi); //leg 1 never changes job status
|
|
45
|
+
const multiResponse = await multi.exec() as MultiResponseFlags;
|
|
46
|
+
telemetry.mapActivityAttributes();
|
|
47
|
+
const jobStatus = this.resolveStatus(multiResponse);
|
|
48
|
+
|
|
49
|
+
//cycle the target ancestor
|
|
50
|
+
multi = this.store.getMulti();
|
|
51
|
+
const messageId = await this.cycleAncestorActivity(multi);
|
|
52
|
+
telemetry.setActivityAttributes({
|
|
53
|
+
'app.activity.mid': messageId,
|
|
54
|
+
'app.job.jss': jobStatus
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
//exit early (`Cycle` activities only execute Leg 1)
|
|
58
|
+
await CollatorService.notarizeEarlyExit(this, multi);
|
|
59
|
+
await multi.exec() as MultiResponseFlags;
|
|
60
|
+
|
|
61
|
+
return this.context.metadata.aid;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error instanceof GetStateError) {
|
|
64
|
+
this.logger.error('cycle-get-state-error', error);
|
|
65
|
+
} else {
|
|
66
|
+
this.logger.error('cycle-process-error', error);
|
|
67
|
+
}
|
|
68
|
+
telemetry.setActivityError(error.message);
|
|
69
|
+
throw error;
|
|
70
|
+
} finally {
|
|
71
|
+
telemetry.endActivitySpan();
|
|
72
|
+
this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Trigger the target ancestor to execute in a cycle,
|
|
78
|
+
* without violating the constraints of the DAG. Immutable
|
|
79
|
+
* `individual activity state` will execute in a new dimensional
|
|
80
|
+
* thread while `shared job state` can change. This
|
|
81
|
+
* pattern allows for retries without violating the DAG.
|
|
82
|
+
*/
|
|
83
|
+
async cycleAncestorActivity(multi: RedisMulti): Promise<string> {
|
|
84
|
+
const streamData: StreamData = {
|
|
85
|
+
metadata: {
|
|
86
|
+
dad: CollatorService.resolveReentryDimension(this),
|
|
87
|
+
jid: this.context.metadata.jid,
|
|
88
|
+
aid: this.config.ancestor,
|
|
89
|
+
},
|
|
90
|
+
data: {} //todo: verify immutability, before enabling: `this.context.data`
|
|
91
|
+
};
|
|
92
|
+
return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export { Cycle };
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { Activity } from './activity';
|
|
2
2
|
import { Await } from './await';
|
|
3
|
-
import {
|
|
4
|
-
import { Iterate } from './iterate';
|
|
3
|
+
import { Cycle } from './cycle';
|
|
5
4
|
import { Emit } from './emit';
|
|
5
|
+
import { Iterate } from './iterate';
|
|
6
6
|
import { Trigger } from './trigger';
|
|
7
|
+
import { Worker } from './worker';
|
|
7
8
|
|
|
8
9
|
export default {
|
|
9
10
|
activity: Activity,
|
|
10
11
|
await: Await,
|
|
12
|
+
cycle: Cycle,
|
|
11
13
|
iterate: Iterate,
|
|
12
14
|
emit: Emit,
|
|
13
15
|
trigger: Trigger,
|