@hotmeshio/hotmesh 0.0.19 → 0.0.21
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 +4 -4
- package/build/modules/errors.d.ts +2 -1
- package/build/modules/errors.js +2 -1
- package/build/package.json +2 -1
- package/build/services/activities/activity.d.ts +2 -2
- package/build/services/activities/activity.js +10 -8
- package/build/services/activities/hook.d.ts +2 -1
- package/build/services/activities/hook.js +12 -9
- package/build/services/activities/signal.d.ts +4 -0
- package/build/services/activities/signal.js +16 -2
- package/build/services/durable/client.d.ts +15 -5
- package/build/services/durable/client.js +37 -14
- package/build/services/durable/factory.d.ts +2 -16
- package/build/services/durable/factory.js +276 -46
- package/build/services/durable/handle.d.ts +1 -1
- package/build/services/durable/handle.js +18 -5
- package/build/services/durable/search.d.ts +8 -1
- package/build/services/durable/search.js +36 -10
- package/build/services/durable/worker.d.ts +7 -9
- package/build/services/durable/worker.js +29 -23
- package/build/services/durable/workflow.d.ts +23 -2
- package/build/services/durable/workflow.js +143 -37
- package/build/services/engine/index.d.ts +2 -2
- package/build/services/engine/index.js +7 -12
- package/build/services/hotmesh/index.d.ts +2 -2
- package/build/services/hotmesh/index.js +2 -2
- package/build/services/signaler/store.d.ts +2 -2
- package/build/services/signaler/store.js +17 -7
- package/build/services/signaler/stream.js +1 -0
- package/build/services/store/clients/redis.js +1 -1
- package/build/services/store/index.js +3 -0
- package/build/services/telemetry/index.js +7 -1
- package/build/types/activity.d.ts +5 -3
- package/build/types/durable.d.ts +13 -2
- package/build/types/hook.d.ts +0 -1
- package/build/types/index.d.ts +1 -1
- package/modules/errors.ts +4 -2
- package/package.json +2 -1
- package/services/activities/activity.ts +10 -8
- package/services/activities/hook.ts +13 -10
- package/services/activities/signal.ts +17 -3
- package/services/durable/client.ts +40 -15
- package/services/durable/factory.ts +274 -46
- package/services/durable/handle.ts +18 -5
- package/services/durable/search.ts +38 -10
- package/services/durable/worker.ts +30 -24
- package/services/durable/workflow.ts +158 -40
- package/services/engine/index.ts +8 -12
- package/services/hotmesh/index.ts +3 -3
- package/services/signaler/store.ts +18 -8
- package/services/signaler/stream.ts +1 -0
- package/services/store/clients/redis.ts +1 -1
- package/services/store/index.ts +2 -0
- package/services/telemetry/index.ts +6 -1
- package/types/activity.ts +10 -8
- package/types/durable.ts +14 -1
- package/types/hook.ts +0 -1
- package/types/index.ts +1 -0
|
@@ -3,18 +3,18 @@ var _a;
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.WorkerService = void 0;
|
|
5
5
|
const errors_1 = require("../../modules/errors");
|
|
6
|
+
const key_1 = require("../../modules/key");
|
|
6
7
|
const asyncLocalStorage_1 = require("./asyncLocalStorage");
|
|
7
8
|
const factory_1 = require("./factory");
|
|
8
9
|
const hotmesh_1 = require("../hotmesh");
|
|
9
10
|
const stream_1 = require("../../types/stream");
|
|
10
|
-
const key_1 = require("../../modules/key");
|
|
11
11
|
class WorkerService {
|
|
12
12
|
static async activateWorkflow(hotMesh) {
|
|
13
|
-
const app = await hotMesh.engine.store.getApp(
|
|
13
|
+
const app = await hotMesh.engine.store.getApp(hotMesh.engine.appId);
|
|
14
14
|
const appVersion = app?.version;
|
|
15
15
|
if (!appVersion) {
|
|
16
16
|
try {
|
|
17
|
-
await hotMesh.deploy((0, factory_1.getWorkflowYAML)(
|
|
17
|
+
await hotMesh.deploy((0, factory_1.getWorkflowYAML)(hotMesh.engine.appId, factory_1.APP_VERSION));
|
|
18
18
|
await hotMesh.activate(factory_1.APP_VERSION);
|
|
19
19
|
}
|
|
20
20
|
catch (err) {
|
|
@@ -32,11 +32,6 @@ class WorkerService {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
-
/**
|
|
36
|
-
* NOTE: Because the worker imports the workflows dynamically AFTER
|
|
37
|
-
* the activities are loaded, there will be items in the registry,
|
|
38
|
-
* allowing proxyActivities to succeed.
|
|
39
|
-
*/
|
|
40
35
|
static registerActivities(activities) {
|
|
41
36
|
if (typeof activities === 'function' && typeof WorkerService.activityRegistry[activities.name] !== 'function') {
|
|
42
37
|
WorkerService.activityRegistry[activities.name] = activities;
|
|
@@ -52,10 +47,11 @@ class WorkerService {
|
|
|
52
47
|
}
|
|
53
48
|
/**
|
|
54
49
|
* For those deployments with a redis stack backend (with the FT module),
|
|
55
|
-
* this method will configure the search index for the workflow.
|
|
50
|
+
* this method will configure the search index for the workflow. For all
|
|
51
|
+
* others, this method will fail gracefully. In all cases, the values
|
|
52
|
+
* will be stored in the workflow's central HASH data structure, allowing
|
|
53
|
+
* for manual traversal and inspection as well.
|
|
56
54
|
*/
|
|
57
|
-
//todo: bind this to the Search service; update constructor to expect hotMeshClient as first param (id is optional
|
|
58
|
-
//refactor and delete other one as well)
|
|
59
55
|
static async configureSearchIndex(hotMeshClient, search) {
|
|
60
56
|
if (search?.schema) {
|
|
61
57
|
const store = hotMeshClient.engine.store;
|
|
@@ -83,7 +79,6 @@ class WorkerService {
|
|
|
83
79
|
}
|
|
84
80
|
}
|
|
85
81
|
static async create(config) {
|
|
86
|
-
//always call `registerActivities` before `import`
|
|
87
82
|
WorkerService.connection = config.connection;
|
|
88
83
|
const workflow = config.workflow;
|
|
89
84
|
const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
|
|
@@ -119,7 +114,8 @@ class WorkerService {
|
|
|
119
114
|
options: config.connection.options
|
|
120
115
|
};
|
|
121
116
|
const hotMeshWorker = await hotmesh_1.HotMeshService.init({
|
|
122
|
-
|
|
117
|
+
logLevel: config.options?.logLevel ?? 'info',
|
|
118
|
+
appId: config.namespace ?? factory_1.APP_ID,
|
|
123
119
|
engine: { redis: redisConfig },
|
|
124
120
|
workers: [
|
|
125
121
|
{ topic: activityTopic,
|
|
@@ -167,26 +163,34 @@ class WorkerService {
|
|
|
167
163
|
options: config.connection.options
|
|
168
164
|
};
|
|
169
165
|
const hotMeshWorker = await hotmesh_1.HotMeshService.init({
|
|
170
|
-
|
|
166
|
+
logLevel: config.options?.logLevel ?? 'info',
|
|
167
|
+
appId: config.namespace ?? factory_1.APP_ID,
|
|
171
168
|
engine: { redis: redisConfig },
|
|
172
169
|
workers: [{
|
|
173
170
|
topic: workflowTopic,
|
|
174
171
|
redis: redisConfig,
|
|
175
|
-
callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic).bind(this)
|
|
172
|
+
callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic, config).bind(this)
|
|
176
173
|
}]
|
|
177
174
|
});
|
|
178
175
|
WorkerService.instances.set(workflowTopic, hotMeshWorker);
|
|
179
176
|
return hotMeshWorker;
|
|
180
177
|
}
|
|
181
|
-
wrapWorkflowFunction(workflowFunction, workflowTopic) {
|
|
178
|
+
wrapWorkflowFunction(workflowFunction, workflowTopic, config) {
|
|
182
179
|
return async (data) => {
|
|
183
180
|
const counter = { counter: 0 };
|
|
184
181
|
try {
|
|
185
182
|
//incoming data payload has arguments and workflowId
|
|
186
183
|
const workflowInput = data.data;
|
|
187
184
|
const context = new Map();
|
|
185
|
+
context.set('namespace', config.namespace ?? factory_1.APP_ID);
|
|
188
186
|
context.set('counter', counter);
|
|
189
187
|
context.set('workflowId', workflowInput.workflowId);
|
|
188
|
+
if (data.data.workflowDimension) {
|
|
189
|
+
//every hook function runs in an isolated dimension controlled
|
|
190
|
+
//by the index assigned when the signal was received; even if the
|
|
191
|
+
//hook function re-runs, its scope will always remain constant
|
|
192
|
+
context.set('workflowDimension', data.data.workflowDimension);
|
|
193
|
+
}
|
|
190
194
|
context.set('workflowTopic', workflowTopic);
|
|
191
195
|
context.set('workflowName', workflowTopic.split('-').pop());
|
|
192
196
|
context.set('workflowTrace', data.metadata.trc);
|
|
@@ -210,9 +214,10 @@ class WorkerService {
|
|
|
210
214
|
metadata: { ...data.metadata },
|
|
211
215
|
data: {
|
|
212
216
|
code: err.code,
|
|
213
|
-
message: JSON.stringify({ duration: err.duration, index: err.index }),
|
|
217
|
+
message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
|
|
214
218
|
duration: err.duration,
|
|
215
|
-
index: err.index
|
|
219
|
+
index: err.index,
|
|
220
|
+
dimension: err.dimension
|
|
216
221
|
}
|
|
217
222
|
};
|
|
218
223
|
//not an error...just a trigger to wait for a signal
|
|
@@ -258,15 +263,16 @@ class WorkerService {
|
|
|
258
263
|
_a = WorkerService;
|
|
259
264
|
WorkerService.activityRegistry = {}; //user's activities
|
|
260
265
|
WorkerService.instances = new Map();
|
|
261
|
-
WorkerService.getHotMesh = async (
|
|
262
|
-
if (WorkerService.instances.has(
|
|
263
|
-
return await WorkerService.instances.get(
|
|
266
|
+
WorkerService.getHotMesh = async (workflowTopic, config, options) => {
|
|
267
|
+
if (WorkerService.instances.has(workflowTopic)) {
|
|
268
|
+
return await WorkerService.instances.get(workflowTopic);
|
|
264
269
|
}
|
|
265
270
|
const hotMeshClient = hotmesh_1.HotMeshService.init({
|
|
266
|
-
|
|
271
|
+
logLevel: options?.logLevel ?? 'info',
|
|
272
|
+
appId: config.namespace ?? factory_1.APP_ID,
|
|
267
273
|
engine: { redis: { ...WorkerService.connection } }
|
|
268
274
|
});
|
|
269
|
-
WorkerService.instances.set(
|
|
275
|
+
WorkerService.instances.set(workflowTopic, hotMeshClient);
|
|
270
276
|
await WorkerService.activateWorkflow(await hotMeshClient);
|
|
271
277
|
return hotMeshClient;
|
|
272
278
|
};
|
|
@@ -1,12 +1,33 @@
|
|
|
1
|
-
import { ActivityConfig, ProxyType, WorkflowOptions } from "../../types/durable";
|
|
2
1
|
import { Search } from './search';
|
|
2
|
+
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
+
import { ActivityConfig, HookOptions, ProxyType, WorkflowOptions } from "../../types/durable";
|
|
3
4
|
export declare class WorkflowService {
|
|
4
5
|
/**
|
|
5
|
-
* Spawn a child workflow. await the result.
|
|
6
|
+
* Spawn a child workflow. await and return the result.
|
|
6
7
|
*/
|
|
7
8
|
static executeChild<T>(options: WorkflowOptions): Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* spawn a child workflow. return the childJobId.
|
|
11
|
+
*/
|
|
12
|
+
static startChild<T>(options: WorkflowOptions): Promise<string>;
|
|
8
13
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
|
|
9
14
|
static search(): Promise<Search>;
|
|
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 workflow state)
|
|
18
|
+
*/
|
|
19
|
+
static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
20
|
+
/**
|
|
21
|
+
* send signal data into any other paused thread (which is paused and
|
|
22
|
+
* awaiting the signal) from within a hook-thread or the main-thread
|
|
23
|
+
*/
|
|
24
|
+
static signal(signalId: string, data: Record<any, any>): Promise<string>;
|
|
25
|
+
/**
|
|
26
|
+
* spawn a hook from either the main thread or a hook thread with
|
|
27
|
+
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
28
|
+
* default to the current workflowId/WorkflowTopic if not provided
|
|
29
|
+
*/
|
|
30
|
+
static hook(options: HookOptions): Promise<string>;
|
|
10
31
|
static sleep(duration: string): Promise<number>;
|
|
11
32
|
static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
|
|
12
33
|
static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
|
|
@@ -5,48 +5,90 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.WorkflowService = void 0;
|
|
7
7
|
const ms_1 = __importDefault(require("ms"));
|
|
8
|
+
const errors_1 = require("../../modules/errors");
|
|
9
|
+
const key_1 = require("../../modules/key");
|
|
8
10
|
const asyncLocalStorage_1 = require("./asyncLocalStorage");
|
|
9
|
-
const worker_1 = require("./worker");
|
|
10
11
|
const client_1 = require("./client");
|
|
11
12
|
const connection_1 = require("./connection");
|
|
12
13
|
const factory_1 = require("./factory");
|
|
13
|
-
const errors_1 = require("../../modules/errors");
|
|
14
14
|
const search_1 = require("./search");
|
|
15
|
+
const worker_1 = require("./worker");
|
|
16
|
+
const stream_1 = require("../../types/stream");
|
|
15
17
|
class WorkflowService {
|
|
16
18
|
/**
|
|
17
|
-
* Spawn a child workflow. await the result.
|
|
19
|
+
* Spawn a child workflow. await and return the result.
|
|
18
20
|
*/
|
|
19
21
|
static async executeChild(options) {
|
|
20
22
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
21
|
-
|
|
22
|
-
throw new Error('durable-store-not-found');
|
|
23
|
-
}
|
|
23
|
+
const namespace = store.get('namespace');
|
|
24
24
|
const workflowId = store.get('workflowId');
|
|
25
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
25
26
|
const workflowTrace = store.get('workflowTrace');
|
|
26
27
|
const workflowSpan = store.get('workflowSpan');
|
|
27
28
|
const COUNTER = store.get('counter');
|
|
28
29
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
29
|
-
|
|
30
|
+
//this is risky but MUST be allowed. Users MAY set the workflowId,
|
|
31
|
+
//but if there is a naming collision, the data from the target entity will be used
|
|
32
|
+
//as there is know way of knowing if the item was generated via a prior run of the workflow
|
|
33
|
+
const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
|
|
30
34
|
const parentWorkflowId = `${workflowId}-f`;
|
|
31
35
|
const client = new client_1.ClientService({
|
|
32
36
|
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
33
37
|
});
|
|
34
|
-
let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId);
|
|
38
|
+
let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId, namespace);
|
|
35
39
|
try {
|
|
36
|
-
return await handle.result();
|
|
40
|
+
return await handle.result(true);
|
|
37
41
|
}
|
|
38
42
|
catch (error) {
|
|
39
43
|
handle = await client.workflow.start({
|
|
40
44
|
...options,
|
|
45
|
+
namespace,
|
|
41
46
|
workflowId: childJobId,
|
|
42
47
|
parentWorkflowId,
|
|
43
48
|
workflowTrace,
|
|
44
49
|
workflowSpan,
|
|
45
50
|
});
|
|
51
|
+
//todo: options.startToCloseTimeout
|
|
46
52
|
const result = await handle.result();
|
|
47
53
|
return result;
|
|
48
54
|
}
|
|
49
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* spawn a child workflow. return the childJobId.
|
|
58
|
+
*/
|
|
59
|
+
static async startChild(options) {
|
|
60
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
61
|
+
const namespace = store.get('namespace');
|
|
62
|
+
const workflowId = store.get('workflowId');
|
|
63
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
64
|
+
const workflowTrace = store.get('workflowTrace');
|
|
65
|
+
const workflowSpan = store.get('workflowSpan');
|
|
66
|
+
const COUNTER = store.get('counter');
|
|
67
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
68
|
+
const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
|
|
69
|
+
const parentWorkflowId = `${workflowId}-f`;
|
|
70
|
+
const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
71
|
+
try {
|
|
72
|
+
//get the status; if there is no error, return childJobId (what was spawned)
|
|
73
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
74
|
+
await hotMeshClient.getStatus(childJobId);
|
|
75
|
+
return childJobId;
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const client = new client_1.ClientService({
|
|
79
|
+
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
80
|
+
});
|
|
81
|
+
await client.workflow.start({
|
|
82
|
+
...options,
|
|
83
|
+
namespace,
|
|
84
|
+
workflowId: childJobId,
|
|
85
|
+
parentWorkflowId,
|
|
86
|
+
workflowTrace,
|
|
87
|
+
workflowSpan,
|
|
88
|
+
});
|
|
89
|
+
return childJobId;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
50
92
|
static proxyActivities(options) {
|
|
51
93
|
if (options.activities) {
|
|
52
94
|
worker_1.WorkerService.registerActivities(options.activities);
|
|
@@ -63,28 +105,94 @@ class WorkflowService {
|
|
|
63
105
|
}
|
|
64
106
|
static async search() {
|
|
65
107
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
66
|
-
if (!store) {
|
|
67
|
-
throw new Error('durable-store-not-found');
|
|
68
|
-
}
|
|
69
108
|
const workflowId = store.get('workflowId');
|
|
109
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
70
110
|
const workflowTopic = store.get('workflowTopic');
|
|
71
|
-
const
|
|
72
|
-
|
|
111
|
+
const namespace = store.get('namespace');
|
|
112
|
+
const COUNTER = store.get('counter');
|
|
113
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
114
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
115
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
116
|
+
const searchSessionId = `-search${workflowDimension}-${execIndex}`;
|
|
117
|
+
return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* those methods that may only be called once must be protected by flagging
|
|
121
|
+
* their execution with a unique key (the key is stored in the workflow state)
|
|
122
|
+
*/
|
|
123
|
+
static async isSideEffectAllowed(hotMeshClient, prefix) {
|
|
124
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
125
|
+
const workflowId = store.get('workflowId');
|
|
126
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
127
|
+
const COUNTER = store.get('counter');
|
|
128
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
129
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
130
|
+
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
131
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
132
|
+
const keyParams = {
|
|
133
|
+
appId: hotMeshClient.appId,
|
|
134
|
+
jobId: ''
|
|
135
|
+
};
|
|
136
|
+
const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
137
|
+
const workflowGuid = `${hotMeshPrefix}${workflowId}`;
|
|
138
|
+
const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
|
|
139
|
+
return guidValue === 1;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* send signal data into any other paused thread (which is paused and
|
|
143
|
+
* awaiting the signal) from within a hook-thread or the main-thread
|
|
144
|
+
*/
|
|
145
|
+
static async signal(signalId, data) {
|
|
146
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
147
|
+
const namespace = store.get('namespace');
|
|
148
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.wfs.signal`, { namespace });
|
|
149
|
+
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
|
|
150
|
+
return await hotMeshClient.hook(`${namespace}.wfs.signal`, { id: signalId, data });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* spawn a hook from either the main thread or a hook thread with
|
|
155
|
+
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
156
|
+
* default to the current workflowId/WorkflowTopic if not provided
|
|
157
|
+
*/
|
|
158
|
+
static async hook(options) {
|
|
159
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
160
|
+
const namespace = store.get('namespace');
|
|
161
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
|
|
162
|
+
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
|
|
163
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
164
|
+
let workflowId;
|
|
165
|
+
let workflowTopic;
|
|
166
|
+
if (options.workflowId && options.taskQueue && options.workflowName) {
|
|
167
|
+
workflowId = options.workflowId;
|
|
168
|
+
workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
workflowId = store.get('workflowId');
|
|
172
|
+
workflowTopic = store.get('workflowTopic');
|
|
173
|
+
}
|
|
174
|
+
const payload = {
|
|
175
|
+
arguments: [...options.args],
|
|
176
|
+
id: workflowId,
|
|
177
|
+
workflowTopic,
|
|
178
|
+
backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
|
|
179
|
+
};
|
|
180
|
+
return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
|
|
181
|
+
}
|
|
73
182
|
}
|
|
74
183
|
static async sleep(duration) {
|
|
75
184
|
const seconds = (0, ms_1.default)(duration) / 1000;
|
|
76
185
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
77
|
-
if (!store) {
|
|
78
|
-
throw new Error('durable-store-not-found');
|
|
79
|
-
}
|
|
80
186
|
const COUNTER = store.get('counter');
|
|
81
187
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
82
188
|
const workflowId = store.get('workflowId');
|
|
83
189
|
const workflowTopic = store.get('workflowTopic');
|
|
84
|
-
const
|
|
190
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
191
|
+
const namespace = store.get('namespace');
|
|
192
|
+
const sleepJobId = `${workflowId}-$sleep${workflowDimension}-${execIndex}`;
|
|
85
193
|
try {
|
|
86
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
|
|
87
|
-
await hotMeshClient.getState(
|
|
194
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
195
|
+
await hotMeshClient.getState(`${hotMeshClient.appId}.sleep.execute`, sleepJobId);
|
|
88
196
|
//if no error is thrown, we've already slept, return the delay
|
|
89
197
|
return seconds;
|
|
90
198
|
}
|
|
@@ -92,28 +200,27 @@ class WorkflowService {
|
|
|
92
200
|
//if an error, the sleep job was not found...rethrow error; sleep job
|
|
93
201
|
// will be automatically created according to the DAG rules (they
|
|
94
202
|
// spawn a new sleep job if error code 595 is thrown by the worker)
|
|
95
|
-
throw new errors_1.DurableSleepError(workflowId, seconds, execIndex);
|
|
203
|
+
throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
|
|
96
204
|
}
|
|
97
205
|
}
|
|
98
206
|
static async waitForSignal(signals, options) {
|
|
99
207
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
100
|
-
if (!store) {
|
|
101
|
-
throw new Error('durable-store-not-found');
|
|
102
|
-
}
|
|
103
208
|
const COUNTER = store.get('counter');
|
|
104
209
|
const workflowId = store.get('workflowId');
|
|
105
210
|
const workflowTopic = store.get('workflowTopic');
|
|
106
|
-
const
|
|
211
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
212
|
+
const namespace = store.get('namespace');
|
|
213
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
107
214
|
//iterate the list of signals and check for done
|
|
108
215
|
let allAreComplete = true;
|
|
109
216
|
let noneAreComplete = false;
|
|
110
217
|
const signalResults = [];
|
|
111
218
|
for (const signal of signals) {
|
|
112
219
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
113
|
-
const wfsJobId = `${workflowId}-$wfs-${execIndex}`;
|
|
220
|
+
const wfsJobId = `${workflowId}-$wfs${workflowDimension}-${execIndex}`;
|
|
114
221
|
try {
|
|
115
222
|
if (allAreComplete) {
|
|
116
|
-
const state = await hotMeshClient.getState(
|
|
223
|
+
const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
|
|
117
224
|
if (state.data?.signalData) {
|
|
118
225
|
//user data is nested to isolate from the signal id; unpackage it
|
|
119
226
|
const signalData = state.data.signalData;
|
|
@@ -151,22 +258,21 @@ class WorkflowService {
|
|
|
151
258
|
static wrapActivity(activityName, options) {
|
|
152
259
|
return async function () {
|
|
153
260
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
154
|
-
if (!store) {
|
|
155
|
-
throw new Error('durable-store-not-found');
|
|
156
|
-
}
|
|
157
261
|
const COUNTER = store.get('counter');
|
|
158
262
|
//increment by state (not value) to avoid race conditions
|
|
159
263
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
160
264
|
const workflowId = store.get('workflowId');
|
|
265
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
161
266
|
const workflowTopic = store.get('workflowTopic');
|
|
162
267
|
const trc = store.get('workflowTrace');
|
|
163
268
|
const spn = store.get('workflowSpan');
|
|
269
|
+
const namespace = store.get('namespace');
|
|
164
270
|
const activityTopic = `${workflowTopic}-activity`;
|
|
165
|
-
const activityJobId = `${workflowId}-$${activityName}-${execIndex}`;
|
|
271
|
+
const activityJobId = `${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
|
|
166
272
|
let activityState;
|
|
167
273
|
try {
|
|
168
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
|
|
169
|
-
activityState = await hotMeshClient.getState(
|
|
274
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
|
|
275
|
+
activityState = await hotMeshClient.getState(`${hotMeshClient.appId}.activity.execute`, activityJobId);
|
|
170
276
|
if (activityState.metadata.err) {
|
|
171
277
|
await hotMeshClient.scrub(activityJobId);
|
|
172
278
|
throw new Error(activityState.metadata.err);
|
|
@@ -176,9 +282,9 @@ class WorkflowService {
|
|
|
176
282
|
}
|
|
177
283
|
//one time subscription
|
|
178
284
|
return await new Promise((resolve, reject) => {
|
|
179
|
-
hotMeshClient.sub(`${
|
|
285
|
+
hotMeshClient.sub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`, async (topic, message) => {
|
|
180
286
|
const response = message.data?.response;
|
|
181
|
-
hotMeshClient.unsub(`${
|
|
287
|
+
hotMeshClient.unsub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`);
|
|
182
288
|
// Resolve the Promise when the callback is triggered with a message
|
|
183
289
|
resolve(response);
|
|
184
290
|
});
|
|
@@ -196,9 +302,9 @@ class WorkflowService {
|
|
|
196
302
|
activityName,
|
|
197
303
|
};
|
|
198
304
|
//start the job
|
|
199
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
|
|
305
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
|
|
200
306
|
const context = { metadata: { trc, spn }, data: {} };
|
|
201
|
-
const jobOutput = await hotMeshClient.pubsub(
|
|
307
|
+
const jobOutput = await hotMeshClient.pubsub(`${hotMeshClient.appId}.activity.execute`, payload, context, duration);
|
|
202
308
|
return jobOutput.data.response;
|
|
203
309
|
}
|
|
204
310
|
};
|
|
@@ -19,7 +19,7 @@ import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '..
|
|
|
19
19
|
import { JobMessageCallback } from '../../types/quorum';
|
|
20
20
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
21
21
|
import { GetStatsOptions, IdsResponse, JobStatsInput, StatsResponse } from '../../types/stats';
|
|
22
|
-
import { StreamData, StreamDataResponse, StreamError } from '../../types/stream';
|
|
22
|
+
import { StreamCode, StreamData, StreamDataResponse, StreamError, StreamStatus } from '../../types/stream';
|
|
23
23
|
declare class EngineService {
|
|
24
24
|
namespace: string;
|
|
25
25
|
apps: HotMeshApps | null;
|
|
@@ -63,7 +63,7 @@ declare class EngineService {
|
|
|
63
63
|
hasParentJob(context: JobState): boolean;
|
|
64
64
|
resolveError(metadata: JobMetadata): StreamError | undefined;
|
|
65
65
|
scrub(jobId: string): Promise<void>;
|
|
66
|
-
hook(topic: string, data: JobData,
|
|
66
|
+
hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
|
|
67
67
|
hookTime(jobId: string, activityId: string): Promise<JobStatus | void>;
|
|
68
68
|
hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
69
69
|
pub(topic: string, data: JobData, context?: JobState): Promise<string>;
|
|
@@ -248,7 +248,8 @@ class EngineService {
|
|
|
248
248
|
await activityHandler.process();
|
|
249
249
|
}
|
|
250
250
|
else {
|
|
251
|
-
|
|
251
|
+
//a 202 code keeps the hook alive (hooks are single-use by default)
|
|
252
|
+
await activityHandler.processWebHookEvent(streamData.status, streamData.code);
|
|
252
253
|
}
|
|
253
254
|
}
|
|
254
255
|
else if (streamData.type === stream_2.StreamDataType.AWAIT) {
|
|
@@ -269,7 +270,7 @@ class EngineService {
|
|
|
269
270
|
}
|
|
270
271
|
else {
|
|
271
272
|
const activityHandler = await this.initActivity(`.${streamData.metadata.aid}`, streamData.data, context);
|
|
272
|
-
await activityHandler.processEvent(streamData.status, streamData.code);
|
|
273
|
+
await activityHandler.processEvent(streamData.status, streamData.code, 'output');
|
|
273
274
|
}
|
|
274
275
|
this.logger.debug('engine-process-stream-message-end', {
|
|
275
276
|
jid: streamData.metadata.jid,
|
|
@@ -323,21 +324,15 @@ class EngineService {
|
|
|
323
324
|
await this.store.scrub(jobId);
|
|
324
325
|
}
|
|
325
326
|
// ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
|
|
326
|
-
async hook(topic, data,
|
|
327
|
+
async hook(topic, data, status = stream_2.StreamStatus.SUCCESS, code = 200) {
|
|
327
328
|
const hookRule = await this.storeSignaler.getHookRule(topic);
|
|
328
|
-
const [aid
|
|
329
|
-
if (!dad) {
|
|
330
|
-
//assume dimensional address is singular (0)
|
|
331
|
-
// for ancestors and self if not provided
|
|
332
|
-
// todo: register
|
|
333
|
-
dad = ',0'.repeat(schema.ancestors.length + 1);
|
|
334
|
-
}
|
|
329
|
+
const [aid] = await this.getSchema(`.${hookRule.to}`);
|
|
335
330
|
const streamData = {
|
|
336
331
|
type: stream_2.StreamDataType.WEBHOOK,
|
|
332
|
+
status,
|
|
333
|
+
code,
|
|
337
334
|
metadata: {
|
|
338
|
-
//jid is unknown at this point; will be resolved using the data
|
|
339
335
|
aid,
|
|
340
|
-
dad,
|
|
341
336
|
topic
|
|
342
337
|
},
|
|
343
338
|
data,
|
|
@@ -6,7 +6,7 @@ import { JobState, JobData, JobOutput, JobStatus } from '../../types/job';
|
|
|
6
6
|
import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
|
|
7
7
|
import { JobMessageCallback } from '../../types/quorum';
|
|
8
8
|
import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
|
|
9
|
-
import { StreamData, StreamDataResponse } from '../../types/stream';
|
|
9
|
+
import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
|
|
10
10
|
declare class HotMeshService {
|
|
11
11
|
namespace: string;
|
|
12
12
|
appId: string;
|
|
@@ -38,7 +38,7 @@ declare class HotMeshService {
|
|
|
38
38
|
getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
|
|
39
39
|
resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
|
|
40
40
|
scrub(jobId: string): Promise<void>;
|
|
41
|
-
hook(topic: string, data: JobData,
|
|
41
|
+
hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
|
|
42
42
|
hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
43
43
|
stop(): Promise<void>;
|
|
44
44
|
compress(terms: string[]): Promise<boolean>;
|
|
@@ -119,8 +119,8 @@ class HotMeshService {
|
|
|
119
119
|
await this.engine?.scrub(jobId);
|
|
120
120
|
}
|
|
121
121
|
// ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
|
|
122
|
-
async hook(topic, data,
|
|
123
|
-
return await this.engine?.hook(topic, data,
|
|
122
|
+
async hook(topic, data, status, code) {
|
|
123
|
+
return await this.engine?.hook(topic, data, status, code);
|
|
124
124
|
}
|
|
125
125
|
async hookAll(hookTopic, data, query, queryFacets = []) {
|
|
126
126
|
return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
|
|
@@ -8,8 +8,8 @@ declare class StoreSignaler {
|
|
|
8
8
|
logger: ILogger;
|
|
9
9
|
constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
10
10
|
getHookRule(topic: string): Promise<HookRule | undefined>;
|
|
11
|
-
registerWebHook(topic: string, context: JobState, multi?: RedisMulti): Promise<string>;
|
|
12
|
-
processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<string>;
|
|
11
|
+
registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string>;
|
|
12
|
+
processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string] | undefined>;
|
|
13
13
|
deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number>;
|
|
14
14
|
}
|
|
15
15
|
export { StoreSignaler };
|
|
@@ -11,7 +11,7 @@ class StoreSignaler {
|
|
|
11
11
|
const rules = await this.store.getHookRules();
|
|
12
12
|
return rules?.[topic]?.[0];
|
|
13
13
|
}
|
|
14
|
-
async registerWebHook(topic, context, multi) {
|
|
14
|
+
async registerWebHook(topic, context, dad, multi) {
|
|
15
15
|
const hookRule = await this.getHookRule(topic);
|
|
16
16
|
if (hookRule) {
|
|
17
17
|
const mapExpression = hookRule.conditions.match[0].expected;
|
|
@@ -20,7 +20,8 @@ class StoreSignaler {
|
|
|
20
20
|
const hook = {
|
|
21
21
|
topic,
|
|
22
22
|
resolved,
|
|
23
|
-
|
|
23
|
+
//hookSignalId is composed of `<dad>::<jid>`
|
|
24
|
+
jobId: `${dad}::${jobId}`,
|
|
24
25
|
};
|
|
25
26
|
await this.store.setHookSignal(hook, multi);
|
|
26
27
|
return jobId;
|
|
@@ -36,18 +37,27 @@ class StoreSignaler {
|
|
|
36
37
|
const context = { $self: { hook: { data } }, $hook: { data } };
|
|
37
38
|
const mapExpression = hookRule.conditions.match[0].actual;
|
|
38
39
|
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
39
|
-
const
|
|
40
|
-
|
|
40
|
+
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
41
|
+
if (!hookSignalId) {
|
|
42
|
+
//messages can be double-processed; not an issue; return undefined
|
|
43
|
+
//users can also provide a bogus topic; not an issue; return undefined
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const [dad, jid] = hookSignalId.split('::');
|
|
47
|
+
//return [jid, aid, dad]
|
|
48
|
+
return [jid, hookRule.to, dad];
|
|
41
49
|
}
|
|
42
50
|
else {
|
|
43
|
-
throw new Error('
|
|
51
|
+
throw new Error('signal-not-found');
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
async deleteWebHookSignal(topic, data) {
|
|
47
55
|
const hookRule = await this.getHookRule(topic);
|
|
48
56
|
if (hookRule) {
|
|
49
|
-
//
|
|
50
|
-
const
|
|
57
|
+
//NOTE: both formats are supported: $self.hook.data OR $hook.data
|
|
58
|
+
const context = { $self: { hook: { data } }, $hook: { data } };
|
|
59
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
60
|
+
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
51
61
|
return await this.store.deleteHookSignal(topic, resolved);
|
|
52
62
|
}
|
|
53
63
|
else {
|
|
@@ -68,7 +68,7 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
68
68
|
}
|
|
69
69
|
catch (error) {
|
|
70
70
|
const streamType = mkStream === 'MKSTREAM' ? 'with MKSTREAM' : 'without MKSTREAM';
|
|
71
|
-
this.logger.
|
|
71
|
+
this.logger.info(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
|
|
72
72
|
throw error;
|
|
73
73
|
}
|
|
74
74
|
}
|