@hotmeshio/hotmesh 0.0.18 → 0.0.20
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/modules/utils.js +7 -0
- 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 +4 -3
- package/build/services/activities/hook.js +15 -12
- 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 +45 -54
- 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 +34 -7
- package/build/services/durable/worker.d.ts +10 -7
- package/build/services/durable/worker.js +59 -49
- package/build/services/durable/workflow.d.ts +20 -2
- package/build/services/durable/workflow.js +97 -84
- 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 +17 -4
- package/build/types/hook.d.ts +0 -1
- package/build/types/index.d.ts +1 -1
- package/modules/errors.ts +4 -2
- package/modules/utils.ts +6 -0
- package/package.json +2 -1
- package/services/activities/activity.ts +10 -8
- package/services/activities/hook.ts +17 -14
- package/services/activities/signal.ts +17 -3
- package/services/durable/client.ts +48 -56
- package/services/durable/factory.ts +274 -46
- package/services/durable/handle.ts +18 -5
- package/services/durable/search.ts +36 -7
- package/services/durable/worker.ts +61 -51
- package/services/durable/workflow.ts +110 -84
- 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 +18 -3
- package/types/hook.ts +0 -1
- package/types/index.ts +1 -0
|
@@ -3,47 +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
|
-
/*
|
|
11
|
-
Here is an example of how the methods in this file are used:
|
|
12
|
-
|
|
13
|
-
./worker.ts
|
|
14
|
-
|
|
15
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
16
|
-
import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
|
|
17
|
-
|
|
18
|
-
import * as workflows from './workflows';
|
|
19
|
-
|
|
20
|
-
async function run() {
|
|
21
|
-
const worker = await Durable.Worker.create({
|
|
22
|
-
connection: {
|
|
23
|
-
class: Redis,
|
|
24
|
-
options: {
|
|
25
|
-
host: 'localhost',
|
|
26
|
-
port: 6379,
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
taskQueue: 'hello-world',
|
|
30
|
-
workflow: workflows.example,
|
|
31
|
-
});
|
|
32
|
-
await worker.run();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
run().catch((err) => {
|
|
36
|
-
console.error(err);
|
|
37
|
-
process.exit(1);
|
|
38
|
-
});
|
|
39
|
-
*/
|
|
40
11
|
class WorkerService {
|
|
41
12
|
static async activateWorkflow(hotMesh) {
|
|
42
|
-
const app = await hotMesh.engine.store.getApp(
|
|
13
|
+
const app = await hotMesh.engine.store.getApp(hotMesh.engine.appId);
|
|
43
14
|
const appVersion = app?.version;
|
|
44
15
|
if (!appVersion) {
|
|
45
16
|
try {
|
|
46
|
-
await hotMesh.deploy((0, factory_1.getWorkflowYAML)(
|
|
17
|
+
await hotMesh.deploy((0, factory_1.getWorkflowYAML)(hotMesh.engine.appId, factory_1.APP_VERSION));
|
|
47
18
|
await hotMesh.activate(factory_1.APP_VERSION);
|
|
48
19
|
}
|
|
49
20
|
catch (err) {
|
|
@@ -61,11 +32,6 @@ class WorkerService {
|
|
|
61
32
|
}
|
|
62
33
|
}
|
|
63
34
|
}
|
|
64
|
-
/**
|
|
65
|
-
* NOTE: Because the worker imports the workflows dynamically AFTER
|
|
66
|
-
* the activities are loaded, there will be items in the registry,
|
|
67
|
-
* allowing proxyActivities to succeed.
|
|
68
|
-
*/
|
|
69
35
|
static registerActivities(activities) {
|
|
70
36
|
if (typeof activities === 'function' && typeof WorkerService.activityRegistry[activities.name] !== 'function') {
|
|
71
37
|
WorkerService.activityRegistry[activities.name] = activities;
|
|
@@ -79,8 +45,40 @@ class WorkerService {
|
|
|
79
45
|
}
|
|
80
46
|
return WorkerService.activityRegistry;
|
|
81
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* For those deployments with a redis stack backend (with the FT module),
|
|
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.
|
|
54
|
+
*/
|
|
55
|
+
static async configureSearchIndex(hotMeshClient, search) {
|
|
56
|
+
if (search?.schema) {
|
|
57
|
+
const store = hotMeshClient.engine.store;
|
|
58
|
+
const schema = [];
|
|
59
|
+
for (const [key, value] of Object.entries(search.schema)) {
|
|
60
|
+
//prefix with a comma (avoids collisions with hotmesh reserved words)
|
|
61
|
+
schema.push(`_${key}`);
|
|
62
|
+
schema.push(value.type);
|
|
63
|
+
if (value.sortable) {
|
|
64
|
+
schema.push('SORTABLE');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const keyParams = {
|
|
69
|
+
appId: hotMeshClient.appId,
|
|
70
|
+
jobId: ''
|
|
71
|
+
};
|
|
72
|
+
const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
73
|
+
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
74
|
+
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
hotMeshClient.engine.logger.info('durable-client-search-err', { err });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
82
81
|
static async create(config) {
|
|
83
|
-
//always call `registerActivities` before `import`
|
|
84
82
|
WorkerService.connection = config.connection;
|
|
85
83
|
const workflow = config.workflow;
|
|
86
84
|
const [workflowFunctionName, workflowFunction] = WorkerService.resolveWorkflowTarget(workflow);
|
|
@@ -91,6 +89,7 @@ class WorkerService {
|
|
|
91
89
|
const worker = new WorkerService();
|
|
92
90
|
worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
|
|
93
91
|
worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
|
|
92
|
+
WorkerService.configureSearchIndex(worker.workflowRunner, config.search);
|
|
94
93
|
await WorkerService.activateWorkflow(worker.workflowRunner);
|
|
95
94
|
return worker;
|
|
96
95
|
}
|
|
@@ -115,7 +114,8 @@ class WorkerService {
|
|
|
115
114
|
options: config.connection.options
|
|
116
115
|
};
|
|
117
116
|
const hotMeshWorker = await hotmesh_1.HotMeshService.init({
|
|
118
|
-
|
|
117
|
+
logLevel: config.options?.logLevel ?? 'info',
|
|
118
|
+
appId: config.namespace ?? factory_1.APP_ID,
|
|
119
119
|
engine: { redis: redisConfig },
|
|
120
120
|
workers: [
|
|
121
121
|
{ topic: activityTopic,
|
|
@@ -163,26 +163,34 @@ class WorkerService {
|
|
|
163
163
|
options: config.connection.options
|
|
164
164
|
};
|
|
165
165
|
const hotMeshWorker = await hotmesh_1.HotMeshService.init({
|
|
166
|
-
|
|
166
|
+
logLevel: config.options?.logLevel ?? 'info',
|
|
167
|
+
appId: config.namespace ?? factory_1.APP_ID,
|
|
167
168
|
engine: { redis: redisConfig },
|
|
168
169
|
workers: [{
|
|
169
170
|
topic: workflowTopic,
|
|
170
171
|
redis: redisConfig,
|
|
171
|
-
callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic).bind(this)
|
|
172
|
+
callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic, config).bind(this)
|
|
172
173
|
}]
|
|
173
174
|
});
|
|
174
175
|
WorkerService.instances.set(workflowTopic, hotMeshWorker);
|
|
175
176
|
return hotMeshWorker;
|
|
176
177
|
}
|
|
177
|
-
wrapWorkflowFunction(workflowFunction, workflowTopic) {
|
|
178
|
+
wrapWorkflowFunction(workflowFunction, workflowTopic, config) {
|
|
178
179
|
return async (data) => {
|
|
179
180
|
const counter = { counter: 0 };
|
|
180
181
|
try {
|
|
181
182
|
//incoming data payload has arguments and workflowId
|
|
182
183
|
const workflowInput = data.data;
|
|
183
184
|
const context = new Map();
|
|
185
|
+
context.set('namespace', config.namespace ?? factory_1.APP_ID);
|
|
184
186
|
context.set('counter', counter);
|
|
185
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
|
+
}
|
|
186
194
|
context.set('workflowTopic', workflowTopic);
|
|
187
195
|
context.set('workflowName', workflowTopic.split('-').pop());
|
|
188
196
|
context.set('workflowTrace', data.metadata.trc);
|
|
@@ -206,9 +214,10 @@ class WorkerService {
|
|
|
206
214
|
metadata: { ...data.metadata },
|
|
207
215
|
data: {
|
|
208
216
|
code: err.code,
|
|
209
|
-
message: JSON.stringify({ duration: err.duration, index: err.index }),
|
|
217
|
+
message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
|
|
210
218
|
duration: err.duration,
|
|
211
|
-
index: err.index
|
|
219
|
+
index: err.index,
|
|
220
|
+
dimension: err.dimension
|
|
212
221
|
}
|
|
213
222
|
};
|
|
214
223
|
//not an error...just a trigger to wait for a signal
|
|
@@ -254,15 +263,16 @@ class WorkerService {
|
|
|
254
263
|
_a = WorkerService;
|
|
255
264
|
WorkerService.activityRegistry = {}; //user's activities
|
|
256
265
|
WorkerService.instances = new Map();
|
|
257
|
-
WorkerService.getHotMesh = async (
|
|
258
|
-
if (WorkerService.instances.has(
|
|
259
|
-
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);
|
|
260
269
|
}
|
|
261
270
|
const hotMeshClient = hotmesh_1.HotMeshService.init({
|
|
262
|
-
|
|
271
|
+
logLevel: options?.logLevel ?? 'info',
|
|
272
|
+
appId: config.namespace ?? factory_1.APP_ID,
|
|
263
273
|
engine: { redis: { ...WorkerService.connection } }
|
|
264
274
|
});
|
|
265
|
-
WorkerService.instances.set(
|
|
275
|
+
WorkerService.instances.set(workflowTopic, hotMeshClient);
|
|
266
276
|
await WorkerService.activateWorkflow(await hotMeshClient);
|
|
267
277
|
return hotMeshClient;
|
|
268
278
|
};
|
|
@@ -1,11 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Search } from './search';
|
|
2
|
+
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
+
import { ActivityConfig, HookOptions, ProxyType, WorkflowOptions } from "../../types/durable";
|
|
2
4
|
export declare class WorkflowService {
|
|
3
5
|
/**
|
|
4
6
|
* Spawn a child workflow. await the result.
|
|
5
7
|
*/
|
|
6
8
|
static executeChild<T>(options: WorkflowOptions): Promise<T>;
|
|
7
9
|
static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
|
|
8
|
-
static
|
|
10
|
+
static search(): Promise<Search>;
|
|
11
|
+
/**
|
|
12
|
+
* those methods that may only be called once must be protected by flagging
|
|
13
|
+
* their execution with a unique key (the key is stored in the workflow state)
|
|
14
|
+
*/
|
|
15
|
+
static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* send signal data into any other paused thread (which is paused and
|
|
18
|
+
* awaiting the signal) from within a hook-thread or the main-thread
|
|
19
|
+
*/
|
|
20
|
+
static signal(signalId: string, data: Record<any, any>): Promise<string>;
|
|
21
|
+
/**
|
|
22
|
+
* spawn a hook from either the main thread or a hook thread with
|
|
23
|
+
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
24
|
+
* default to the current workflowId/WorkflowTopic if not provided
|
|
25
|
+
*/
|
|
26
|
+
static hook(options: HookOptions): Promise<string>;
|
|
9
27
|
static sleep(duration: string): Promise<number>;
|
|
10
28
|
static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
|
|
11
29
|
static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
|
|
@@ -5,63 +5,35 @@ 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
|
-
|
|
16
|
-
|
|
17
|
-
target activity, so that when the workflow calls a
|
|
18
|
-
proxied activity, it is actually calling the proxy
|
|
19
|
-
function, which in turn calls the activity function.
|
|
20
|
-
|
|
21
|
-
Here is an example of how the methods in this file are used:
|
|
22
|
-
|
|
23
|
-
./workflows.ts
|
|
24
|
-
|
|
25
|
-
import { Durable } from '@hotmeshio/hotmesh';
|
|
26
|
-
import * as activities from './activities';
|
|
27
|
-
|
|
28
|
-
const { greet } = Durable.workflow.proxyActivities<typeof activities>({
|
|
29
|
-
activities: activities,
|
|
30
|
-
startToCloseTimeout: '1 minute',
|
|
31
|
-
retryPolicy: {
|
|
32
|
-
initialInterval: '5 seconds', // Initial delay between retries
|
|
33
|
-
maximumAttempts: 3, // Max number of retry attempts
|
|
34
|
-
backoffCoefficient: 2.0, // Backoff factor for delay between retries: delay = initialInterval * (backoffCoefficient ^ retry_attempt)
|
|
35
|
-
maximumInterval: '30 seconds', // Max delay between retries
|
|
36
|
-
},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
export async function example(name: string): Promise<string> {
|
|
40
|
-
return await greet(name);
|
|
41
|
-
}
|
|
42
|
-
*/
|
|
15
|
+
const worker_1 = require("./worker");
|
|
16
|
+
const stream_1 = require("../../types/stream");
|
|
43
17
|
class WorkflowService {
|
|
44
18
|
/**
|
|
45
19
|
* Spawn a child workflow. await the result.
|
|
46
20
|
*/
|
|
47
21
|
static async executeChild(options) {
|
|
48
22
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
49
|
-
if (!store) {
|
|
50
|
-
throw new Error('durable-store-not-found');
|
|
51
|
-
}
|
|
52
23
|
const workflowId = store.get('workflowId');
|
|
24
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
53
25
|
const workflowTrace = store.get('workflowTrace');
|
|
54
26
|
const workflowSpan = store.get('workflowSpan');
|
|
55
27
|
const COUNTER = store.get('counter');
|
|
56
28
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
57
|
-
const childJobId = `${workflowId}-$${options.workflowName}-${execIndex}`;
|
|
29
|
+
const childJobId = `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
|
|
58
30
|
const parentWorkflowId = `${workflowId}-f`;
|
|
59
31
|
const client = new client_1.ClientService({
|
|
60
32
|
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
61
33
|
});
|
|
62
34
|
let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId);
|
|
63
35
|
try {
|
|
64
|
-
return await handle.result();
|
|
36
|
+
return await handle.result(true);
|
|
65
37
|
}
|
|
66
38
|
catch (error) {
|
|
67
39
|
handle = await client.workflow.start({
|
|
@@ -89,53 +61,96 @@ class WorkflowService {
|
|
|
89
61
|
}
|
|
90
62
|
return proxy;
|
|
91
63
|
}
|
|
92
|
-
static async
|
|
64
|
+
static async search() {
|
|
93
65
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
94
|
-
if (!store) {
|
|
95
|
-
throw new Error('durable-store-not-found');
|
|
96
|
-
}
|
|
97
66
|
const workflowId = store.get('workflowId');
|
|
67
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
98
68
|
const workflowTopic = store.get('workflowTopic');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
69
|
+
const namespace = store.get('namespace');
|
|
70
|
+
const COUNTER = store.get('counter');
|
|
71
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
72
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
73
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
74
|
+
const searchSessionId = `-search${workflowDimension}-${execIndex}`;
|
|
75
|
+
return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* those methods that may only be called once must be protected by flagging
|
|
79
|
+
* their execution with a unique key (the key is stored in the workflow state)
|
|
80
|
+
*/
|
|
81
|
+
static async isSideEffectAllowed(hotMeshClient, prefix) {
|
|
82
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
83
|
+
const workflowId = store.get('workflowId');
|
|
84
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
85
|
+
const COUNTER = store.get('counter');
|
|
86
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
87
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
88
|
+
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
89
|
+
//this ID is used as a item key with a hash (dash prefix ensures no collision)
|
|
90
|
+
const keyParams = {
|
|
91
|
+
appId: hotMeshClient.appId,
|
|
92
|
+
jobId: ''
|
|
93
|
+
};
|
|
94
|
+
const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
95
|
+
const workflowGuid = `${hotMeshPrefix}${workflowId}`;
|
|
96
|
+
const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
|
|
97
|
+
return guidValue === 1;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* send signal data into any other paused thread (which is paused and
|
|
101
|
+
* awaiting the signal) from within a hook-thread or the main-thread
|
|
102
|
+
*/
|
|
103
|
+
static async signal(signalId, data) {
|
|
104
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
105
|
+
const namespace = store.get('namespace');
|
|
106
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.wfs.signal`, { namespace });
|
|
107
|
+
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'signal')) {
|
|
108
|
+
return await hotMeshClient.hook(`${namespace}.wfs.signal`, { id: signalId, data });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* spawn a hook from either the main thread or a hook thread with
|
|
113
|
+
* the provided options; worflowId/TaskQueue/Name are optional and will
|
|
114
|
+
* default to the current workflowId/WorkflowTopic if not provided
|
|
115
|
+
*/
|
|
116
|
+
static async hook(options) {
|
|
117
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
118
|
+
const namespace = store.get('namespace');
|
|
119
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
|
|
120
|
+
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
|
|
121
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
122
|
+
let workflowId;
|
|
123
|
+
let workflowTopic;
|
|
124
|
+
if (options.workflowId && options.taskQueue && options.workflowName) {
|
|
125
|
+
workflowId = options.workflowId;
|
|
126
|
+
workflowTopic = `${options.taskQueue}-${options.workflowName}`;
|
|
115
127
|
}
|
|
116
|
-
else
|
|
117
|
-
|
|
128
|
+
else {
|
|
129
|
+
workflowId = store.get('workflowId');
|
|
130
|
+
workflowTopic = store.get('workflowTopic');
|
|
118
131
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
const payload = {
|
|
133
|
+
arguments: [...options.args],
|
|
134
|
+
id: workflowId,
|
|
135
|
+
workflowTopic,
|
|
136
|
+
backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
|
|
137
|
+
};
|
|
138
|
+
return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
|
|
123
139
|
}
|
|
124
140
|
}
|
|
125
141
|
static async sleep(duration) {
|
|
126
142
|
const seconds = (0, ms_1.default)(duration) / 1000;
|
|
127
143
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
128
|
-
if (!store) {
|
|
129
|
-
throw new Error('durable-store-not-found');
|
|
130
|
-
}
|
|
131
144
|
const COUNTER = store.get('counter');
|
|
132
145
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
133
146
|
const workflowId = store.get('workflowId');
|
|
134
147
|
const workflowTopic = store.get('workflowTopic');
|
|
135
|
-
const
|
|
148
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
149
|
+
const namespace = store.get('namespace');
|
|
150
|
+
const sleepJobId = `${workflowId}-$sleep${workflowDimension}-${execIndex}`;
|
|
136
151
|
try {
|
|
137
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic);
|
|
138
|
-
await hotMeshClient.getState(
|
|
152
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
153
|
+
await hotMeshClient.getState(`${hotMeshClient.appId}.sleep.execute`, sleepJobId);
|
|
139
154
|
//if no error is thrown, we've already slept, return the delay
|
|
140
155
|
return seconds;
|
|
141
156
|
}
|
|
@@ -143,28 +158,27 @@ class WorkflowService {
|
|
|
143
158
|
//if an error, the sleep job was not found...rethrow error; sleep job
|
|
144
159
|
// will be automatically created according to the DAG rules (they
|
|
145
160
|
// spawn a new sleep job if error code 595 is thrown by the worker)
|
|
146
|
-
throw new errors_1.DurableSleepError(workflowId, seconds, execIndex);
|
|
161
|
+
throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
|
|
147
162
|
}
|
|
148
163
|
}
|
|
149
164
|
static async waitForSignal(signals, options) {
|
|
150
165
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
151
|
-
if (!store) {
|
|
152
|
-
throw new Error('durable-store-not-found');
|
|
153
|
-
}
|
|
154
166
|
const COUNTER = store.get('counter');
|
|
155
167
|
const workflowId = store.get('workflowId');
|
|
156
168
|
const workflowTopic = store.get('workflowTopic');
|
|
157
|
-
const
|
|
169
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
170
|
+
const namespace = store.get('namespace');
|
|
171
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
158
172
|
//iterate the list of signals and check for done
|
|
159
173
|
let allAreComplete = true;
|
|
160
174
|
let noneAreComplete = false;
|
|
161
175
|
const signalResults = [];
|
|
162
176
|
for (const signal of signals) {
|
|
163
177
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
164
|
-
const wfsJobId = `${workflowId}-$wfs-${execIndex}`;
|
|
178
|
+
const wfsJobId = `${workflowId}-$wfs${workflowDimension}-${execIndex}`;
|
|
165
179
|
try {
|
|
166
180
|
if (allAreComplete) {
|
|
167
|
-
const state = await hotMeshClient.getState(
|
|
181
|
+
const state = await hotMeshClient.getState(`${hotMeshClient.appId}.wfs.execute`, wfsJobId);
|
|
168
182
|
if (state.data?.signalData) {
|
|
169
183
|
//user data is nested to isolate from the signal id; unpackage it
|
|
170
184
|
const signalData = state.data.signalData;
|
|
@@ -202,22 +216,21 @@ class WorkflowService {
|
|
|
202
216
|
static wrapActivity(activityName, options) {
|
|
203
217
|
return async function () {
|
|
204
218
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
205
|
-
if (!store) {
|
|
206
|
-
throw new Error('durable-store-not-found');
|
|
207
|
-
}
|
|
208
219
|
const COUNTER = store.get('counter');
|
|
209
220
|
//increment by state (not value) to avoid race conditions
|
|
210
221
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
211
222
|
const workflowId = store.get('workflowId');
|
|
223
|
+
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
212
224
|
const workflowTopic = store.get('workflowTopic');
|
|
213
225
|
const trc = store.get('workflowTrace');
|
|
214
226
|
const spn = store.get('workflowSpan');
|
|
227
|
+
const namespace = store.get('namespace');
|
|
215
228
|
const activityTopic = `${workflowTopic}-activity`;
|
|
216
|
-
const activityJobId = `${workflowId}-$${activityName}-${execIndex}`;
|
|
229
|
+
const activityJobId = `${workflowId}-$${activityName}${workflowDimension}-${execIndex}`;
|
|
217
230
|
let activityState;
|
|
218
231
|
try {
|
|
219
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
|
|
220
|
-
activityState = await hotMeshClient.getState(
|
|
232
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
|
|
233
|
+
activityState = await hotMeshClient.getState(`${hotMeshClient.appId}.activity.execute`, activityJobId);
|
|
221
234
|
if (activityState.metadata.err) {
|
|
222
235
|
await hotMeshClient.scrub(activityJobId);
|
|
223
236
|
throw new Error(activityState.metadata.err);
|
|
@@ -227,9 +240,9 @@ class WorkflowService {
|
|
|
227
240
|
}
|
|
228
241
|
//one time subscription
|
|
229
242
|
return await new Promise((resolve, reject) => {
|
|
230
|
-
hotMeshClient.sub(`${
|
|
243
|
+
hotMeshClient.sub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`, async (topic, message) => {
|
|
231
244
|
const response = message.data?.response;
|
|
232
|
-
hotMeshClient.unsub(`${
|
|
245
|
+
hotMeshClient.unsub(`${hotMeshClient.appId}.activity.executed.${activityJobId}`);
|
|
233
246
|
// Resolve the Promise when the callback is triggered with a message
|
|
234
247
|
resolve(response);
|
|
235
248
|
});
|
|
@@ -247,9 +260,9 @@ class WorkflowService {
|
|
|
247
260
|
activityName,
|
|
248
261
|
};
|
|
249
262
|
//start the job
|
|
250
|
-
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic);
|
|
263
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(activityTopic, { namespace });
|
|
251
264
|
const context = { metadata: { trc, spn }, data: {} };
|
|
252
|
-
const jobOutput = await hotMeshClient.pubsub(
|
|
265
|
+
const jobOutput = await hotMeshClient.pubsub(`${hotMeshClient.appId}.activity.execute`, payload, context, duration);
|
|
253
266
|
return jobOutput.data.response;
|
|
254
267
|
}
|
|
255
268
|
};
|
|
@@ -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 };
|