@hotmeshio/hotmesh 0.0.34 → 0.0.36
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 +30 -18
- package/build/modules/enums.d.ts +22 -0
- package/build/modules/enums.js +29 -0
- package/build/modules/errors.d.ts +10 -2
- package/build/modules/errors.js +14 -3
- package/build/modules/key.d.ts +16 -15
- package/build/modules/key.js +18 -15
- package/build/modules/utils.d.ts +1 -0
- package/build/modules/utils.js +6 -1
- package/build/package.json +3 -1
- package/build/services/activities/activity.d.ts +5 -0
- package/build/services/activities/activity.js +27 -6
- package/build/services/activities/await.js +11 -3
- package/build/services/activities/cycle.js +10 -2
- package/build/services/activities/hook.js +8 -2
- package/build/services/activities/index.d.ts +2 -2
- package/build/services/activities/index.js +2 -2
- package/build/services/activities/interrupt.d.ts +16 -0
- package/build/services/activities/interrupt.js +129 -0
- package/build/services/activities/signal.js +9 -2
- package/build/services/activities/trigger.d.ts +4 -0
- package/build/services/activities/trigger.js +14 -4
- package/build/services/activities/worker.js +10 -2
- package/build/services/collator/index.d.ts +4 -0
- package/build/services/collator/index.js +8 -0
- package/build/services/compiler/deployer.js +1 -3
- package/build/services/connector/index.js +2 -3
- package/build/services/durable/client.js +7 -3
- package/build/services/durable/factory.js +65 -284
- package/build/services/durable/handle.d.ts +37 -0
- package/build/services/durable/handle.js +52 -9
- package/build/services/durable/meshos.js +2 -2
- package/build/services/durable/worker.js +9 -2
- package/build/services/durable/workflow.d.ts +24 -0
- package/build/services/durable/workflow.js +56 -1
- package/build/services/engine/index.d.ts +14 -6
- package/build/services/engine/index.js +52 -27
- package/build/services/hotmesh/index.d.ts +3 -1
- package/build/services/hotmesh/index.js +11 -3
- package/build/services/quorum/index.d.ts +1 -0
- package/build/services/quorum/index.js +10 -0
- package/build/services/signaler/stream.js +25 -29
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +40 -4
- package/build/services/store/index.js +114 -9
- package/build/services/task/index.d.ts +5 -4
- package/build/services/task/index.js +12 -14
- package/build/types/activity.d.ts +35 -5
- package/build/types/durable.d.ts +4 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/job.d.ts +18 -1
- package/build/types/quorum.d.ts +11 -7
- package/build/types/stream.d.ts +4 -1
- package/build/types/stream.js +2 -0
- package/modules/enums.ts +32 -0
- package/modules/errors.ts +24 -9
- package/modules/key.ts +4 -1
- package/modules/utils.ts +5 -0
- package/package.json +3 -1
- package/services/activities/activity.ts +34 -8
- package/services/activities/await.ts +11 -4
- package/services/activities/cycle.ts +10 -3
- package/services/activities/hook.ts +8 -3
- package/services/activities/index.ts +2 -2
- package/services/activities/interrupt.ts +159 -0
- package/services/activities/signal.ts +9 -3
- package/services/activities/trigger.ts +21 -5
- package/services/activities/worker.ts +10 -3
- package/services/collator/index.ts +10 -1
- package/services/compiler/deployer.ts +1 -3
- package/services/connector/index.ts +3 -5
- package/services/durable/client.ts +8 -4
- package/services/durable/factory.ts +65 -284
- package/services/durable/handle.ts +55 -9
- package/services/durable/meshos.ts +2 -3
- package/services/durable/worker.ts +9 -2
- package/services/durable/workflow.ts +66 -2
- package/services/engine/index.ts +74 -26
- package/services/hotmesh/index.ts +14 -4
- package/services/quorum/index.ts +9 -0
- package/services/signaler/stream.ts +27 -24
- package/services/store/clients/ioredis.ts +1 -0
- package/services/store/index.ts +119 -11
- package/services/task/index.ts +18 -18
- package/types/activity.ts +38 -8
- package/types/durable.ts +8 -4
- package/types/index.ts +1 -1
- package/types/job.ts +30 -1
- package/types/quorum.ts +13 -8
- package/types/stream.ts +3 -0
- package/build/services/activities/iterate.d.ts +0 -9
- package/build/services/activities/iterate.js +0 -13
- package/services/activities/iterate.ts +0 -26
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WorkflowHandleService = void 0;
|
|
4
|
+
const enums_1 = require("../../modules/enums");
|
|
4
5
|
class WorkflowHandleService {
|
|
5
6
|
constructor(hotMesh, workflowTopic, workflowId) {
|
|
6
7
|
this.workflowTopic = workflowTopic;
|
|
7
8
|
this.workflowId = workflowId;
|
|
8
9
|
this.hotMesh = hotMesh;
|
|
9
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Sends a signal to the workflow. This is a way to send
|
|
13
|
+
* a message to a workflow that is paused due to having
|
|
14
|
+
* executed a `waitForSignal` workflow extension. Awakens
|
|
15
|
+
* the workflow if no other signals are pending.
|
|
16
|
+
*/
|
|
10
17
|
async signal(signalId, data) {
|
|
11
18
|
await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
|
|
12
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Returns the job state of the workflow. If the workflow has completed
|
|
22
|
+
* this is also the job output. If the workflow is still running, this
|
|
23
|
+
* is the current state of the job, but it may change depending upon
|
|
24
|
+
* the activities that remain.
|
|
25
|
+
*/
|
|
13
26
|
async state(metadata = false) {
|
|
14
27
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
15
28
|
if (!state.data && state.metadata.err) {
|
|
@@ -17,12 +30,38 @@ class WorkflowHandleService {
|
|
|
17
30
|
}
|
|
18
31
|
return metadata ? state : state.data;
|
|
19
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns the current search state of the workflow. This is
|
|
35
|
+
* different than the job state or individual activity state.
|
|
36
|
+
* Search state represents name/value pairs that were added
|
|
37
|
+
* to the workflow. As the workflow is stored in a Redis hash,
|
|
38
|
+
* this is a way to store additional data that is indexed
|
|
39
|
+
* and searchable using the RediSearch module.
|
|
40
|
+
*/
|
|
20
41
|
async queryState(fields) {
|
|
21
42
|
return await this.hotMesh.getQueryState(this.workflowId, fields);
|
|
22
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the current status of the workflow. This is a semaphore
|
|
46
|
+
* value that represents the current state of the workflow, where
|
|
47
|
+
* 0 is complete and a negative value represents that the flow was
|
|
48
|
+
* interrupted.
|
|
49
|
+
*/
|
|
23
50
|
async status() {
|
|
24
51
|
return await this.hotMesh.getStatus(this.workflowId);
|
|
25
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Interrupts a running workflow. Standard Job Completion tasks will
|
|
55
|
+
* run. Subscribers will be notified and the job hash will be expired.
|
|
56
|
+
*/
|
|
57
|
+
async interrupt(options) {
|
|
58
|
+
return await this.hotMesh.interrupt(`${this.hotMesh.appId}.execute`, this.workflowId, options);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Awaits for the workflow to complete and returns the result. If
|
|
62
|
+
* the workflow thows and error, this method will likewise throw
|
|
63
|
+
* an error.
|
|
64
|
+
*/
|
|
26
65
|
async result(loadState) {
|
|
27
66
|
if (loadState) {
|
|
28
67
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
@@ -50,30 +89,34 @@ class WorkflowHandleService {
|
|
|
50
89
|
}
|
|
51
90
|
else if (!response) {
|
|
52
91
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
53
|
-
if (
|
|
54
|
-
|
|
92
|
+
if (state.metadata.err) {
|
|
93
|
+
const error = JSON.parse(state.metadata.err);
|
|
94
|
+
if (error.code === enums_1.STATUS_CODE_INTERRUPT || !state.data) {
|
|
95
|
+
return reject({ ...error, job_id: this.workflowId });
|
|
96
|
+
}
|
|
55
97
|
}
|
|
56
98
|
response = state.data?.response;
|
|
57
99
|
}
|
|
58
100
|
resolve(response);
|
|
59
101
|
};
|
|
60
102
|
//check for done
|
|
61
|
-
if (status
|
|
103
|
+
if (status <= 0) {
|
|
62
104
|
return complete();
|
|
63
105
|
}
|
|
64
106
|
//subscribe to topic
|
|
65
107
|
this.hotMesh.sub(topic, async (topic, state) => {
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
108
|
+
if (state.metadata.err) {
|
|
109
|
+
const error = JSON.parse(state.metadata.err);
|
|
110
|
+
if (error.code === enums_1.STATUS_CODE_INTERRUPT || !state.data) {
|
|
111
|
+
return await complete(null, state.metadata.err);
|
|
112
|
+
}
|
|
71
113
|
}
|
|
114
|
+
await complete(state.data?.response);
|
|
72
115
|
});
|
|
73
116
|
//resolve for race condition
|
|
74
117
|
setTimeout(async () => {
|
|
75
118
|
status = await this.hotMesh.getStatus(this.workflowId);
|
|
76
|
-
if (status
|
|
119
|
+
if (status <= 0) {
|
|
77
120
|
await complete();
|
|
78
121
|
}
|
|
79
122
|
}, 0);
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MeshOSService = void 0;
|
|
4
|
-
const nanoid_1 = require("nanoid");
|
|
5
4
|
const _1 = require(".");
|
|
6
5
|
const asyncLocalStorage_1 = require("./asyncLocalStorage");
|
|
7
6
|
const client_1 = require("./client");
|
|
8
7
|
const search_1 = require("./search");
|
|
9
8
|
const worker_1 = require("./worker");
|
|
10
9
|
const workflow_1 = require("./workflow");
|
|
10
|
+
const utils_1 = require("../../modules/utils");
|
|
11
11
|
/**
|
|
12
12
|
* The base class for running MeshOS workflows.
|
|
13
13
|
* Extend this class, add your Redis config, and add functions to
|
|
@@ -31,7 +31,7 @@ class MeshOSService {
|
|
|
31
31
|
*/
|
|
32
32
|
static mintGuid() {
|
|
33
33
|
const my = new this();
|
|
34
|
-
return `${my.search?.prefix?.[0]}${(0,
|
|
34
|
+
return `${my.search?.prefix?.[0]}${(0, utils_1.guid)()}`;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Creates an FT search index
|
|
@@ -153,14 +153,21 @@ class WorkerService {
|
|
|
153
153
|
//incoming data payload has arguments and workflowId
|
|
154
154
|
const workflowInput = data.data;
|
|
155
155
|
const context = new Map();
|
|
156
|
+
context.set('raw', data);
|
|
156
157
|
context.set('namespace', config.namespace ?? factory_1.APP_ID);
|
|
157
158
|
context.set('counter', counter);
|
|
158
159
|
context.set('workflowId', workflowInput.workflowId);
|
|
159
|
-
|
|
160
|
+
context.set('workflowId', workflowInput.workflowId);
|
|
161
|
+
if (workflowInput.originJobId) {
|
|
162
|
+
//if present there is an origin job to which this job is subordinated;
|
|
163
|
+
// garbage collect (expire) this job when originJobId is expired
|
|
164
|
+
context.set('originJobId', workflowInput.originJobId);
|
|
165
|
+
}
|
|
166
|
+
if (workflowInput.workflowDimension) {
|
|
160
167
|
//every hook function runs in an isolated dimension controlled
|
|
161
168
|
//by the index assigned when the signal was received; even if the
|
|
162
169
|
//hook function re-runs, its scope will always remain constant
|
|
163
|
-
context.set('workflowDimension',
|
|
170
|
+
context.set('workflowDimension', workflowInput.workflowDimension);
|
|
164
171
|
}
|
|
165
172
|
context.set('workflowTopic', workflowTopic);
|
|
166
173
|
context.set('workflowName', workflowTopic.split('-').pop());
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Search } from './search';
|
|
2
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
3
|
import { ActivityConfig, HookOptions, ProxyType, WorkflowContext, WorkflowOptions } from "../../types/durable";
|
|
4
|
+
import { JobInterruptOptions } from '../../types/job';
|
|
4
5
|
export declare class WorkflowService {
|
|
5
6
|
/**
|
|
6
7
|
* Spawns a child workflow. await and return the result.
|
|
@@ -80,6 +81,29 @@ export declare class WorkflowService {
|
|
|
80
81
|
* @param {HookOptions} options - the hook options
|
|
81
82
|
*/
|
|
82
83
|
static hook(options: HookOptions): Promise<string>;
|
|
84
|
+
static getLocalState(): {
|
|
85
|
+
workflowId: any;
|
|
86
|
+
namespace: any;
|
|
87
|
+
workflowTopic: any;
|
|
88
|
+
workflowDimension: any;
|
|
89
|
+
counter: any;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Executes a function once and caches the result. If the function is called
|
|
93
|
+
* again, the cached result is returned. This is useful for wrapping
|
|
94
|
+
* expensive activity calls that should only be run once, but which might
|
|
95
|
+
* not require the configuration nuance/expense provided by proxyActivities.
|
|
96
|
+
* @template T - the result type
|
|
97
|
+
*/
|
|
98
|
+
static once<T>(fn: (...args: any[]) => Promise<T>, ...args: any[]): Promise<T>;
|
|
99
|
+
/**
|
|
100
|
+
* Interrupts a running job
|
|
101
|
+
*
|
|
102
|
+
* @param {string} jobId - the target job id
|
|
103
|
+
* @param {JobInterruptOptions} options - the interrupt options
|
|
104
|
+
* @returns {Promise<string>} - the stream id
|
|
105
|
+
*/
|
|
106
|
+
static interrupt(jobId: string, options?: JobInterruptOptions): Promise<string | void>;
|
|
83
107
|
/**
|
|
84
108
|
* Sleeps the workflow for a duration. As the function is reentrant,
|
|
85
109
|
* upon reentry, the function will traverse prior execution paths up
|
|
@@ -26,6 +26,7 @@ class WorkflowService {
|
|
|
26
26
|
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
27
27
|
const namespace = store.get('namespace');
|
|
28
28
|
const workflowId = store.get('workflowId');
|
|
29
|
+
const originJobId = store.get('originJobId');
|
|
29
30
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
30
31
|
const workflowTrace = store.get('workflowTrace');
|
|
31
32
|
const workflowSpan = store.get('workflowSpan');
|
|
@@ -49,6 +50,7 @@ class WorkflowService {
|
|
|
49
50
|
...options,
|
|
50
51
|
namespace,
|
|
51
52
|
workflowId: childJobId,
|
|
53
|
+
originJobId: originJobId ?? workflowId,
|
|
52
54
|
parentWorkflowId,
|
|
53
55
|
workflowTrace,
|
|
54
56
|
workflowSpan,
|
|
@@ -261,6 +263,57 @@ class WorkflowService {
|
|
|
261
263
|
return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
|
|
262
264
|
}
|
|
263
265
|
}
|
|
266
|
+
static getLocalState() {
|
|
267
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
268
|
+
return {
|
|
269
|
+
workflowId: store.get('workflowId'),
|
|
270
|
+
namespace: store.get('namespace'),
|
|
271
|
+
workflowTopic: store.get('workflowTopic'),
|
|
272
|
+
workflowDimension: store.get('workflowDimension') ?? '',
|
|
273
|
+
counter: store.get('counter'),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Executes a function once and caches the result. If the function is called
|
|
278
|
+
* again, the cached result is returned. This is useful for wrapping
|
|
279
|
+
* expensive activity calls that should only be run once, but which might
|
|
280
|
+
* not require the configuration nuance/expense provided by proxyActivities.
|
|
281
|
+
* @template T - the result type
|
|
282
|
+
*/
|
|
283
|
+
static async once(fn, ...args) {
|
|
284
|
+
const { workflowId, namespace, workflowTopic, workflowDimension, counter: COUNTER, } = WorkflowService.getLocalState();
|
|
285
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
286
|
+
const sessionId = `-once${workflowDimension}-${execIndex}-`;
|
|
287
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
288
|
+
const keyParams = {
|
|
289
|
+
appId: hotMeshClient.appId,
|
|
290
|
+
jobId: workflowId
|
|
291
|
+
};
|
|
292
|
+
const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
293
|
+
const value = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId);
|
|
294
|
+
if (value) {
|
|
295
|
+
return JSON.parse(value);
|
|
296
|
+
}
|
|
297
|
+
const response = await fn(...args);
|
|
298
|
+
await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, JSON.stringify(response));
|
|
299
|
+
return response;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Interrupts a running job
|
|
303
|
+
*
|
|
304
|
+
* @param {string} jobId - the target job id
|
|
305
|
+
* @param {JobInterruptOptions} options - the interrupt options
|
|
306
|
+
* @returns {Promise<string>} - the stream id
|
|
307
|
+
*/
|
|
308
|
+
static async interrupt(jobId, options = {}) {
|
|
309
|
+
const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
|
|
310
|
+
const workflowTopic = store.get('workflowTopic');
|
|
311
|
+
const namespace = store.get('namespace');
|
|
312
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
313
|
+
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'interrupt')) {
|
|
314
|
+
return await hotMeshClient.interrupt(`${hotMeshClient.appId}.execute`, jobId, options);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
264
317
|
/**
|
|
265
318
|
* Sleeps the workflow for a duration. As the function is reentrant,
|
|
266
319
|
* upon reentry, the function will traverse prior execution paths up
|
|
@@ -379,6 +432,7 @@ class WorkflowService {
|
|
|
379
432
|
//increment by state (not value) to avoid race conditions
|
|
380
433
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
381
434
|
const workflowId = store.get('workflowId');
|
|
435
|
+
const originJobId = store.get('originJobId');
|
|
382
436
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
383
437
|
const workflowTopic = store.get('workflowTopic');
|
|
384
438
|
const trc = store.get('workflowTrace');
|
|
@@ -412,7 +466,8 @@ class WorkflowService {
|
|
|
412
466
|
const duration = (0, ms_1.default)(options?.startToCloseTimeout || '1 minute');
|
|
413
467
|
const payload = {
|
|
414
468
|
arguments: Array.from(arguments),
|
|
415
|
-
//the
|
|
469
|
+
//when the origin job is removed
|
|
470
|
+
originJobId: originJobId ?? workflowId,
|
|
416
471
|
parentWorkflowId: `${workflowId}-a`,
|
|
417
472
|
workflowId: activityJobId,
|
|
418
473
|
workflowTopic: activityTopic,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Await } from '../activities/await';
|
|
2
2
|
import { Cycle } from '../activities/cycle';
|
|
3
3
|
import { Hook } from '../activities/hook';
|
|
4
|
+
import { Interrupt } from '../activities/interrupt';
|
|
4
5
|
import { Signal } from '../activities/signal';
|
|
5
6
|
import { Worker } from '../activities/worker';
|
|
6
7
|
import { Trigger } from '../activities/trigger';
|
|
@@ -14,7 +15,7 @@ import { TaskService } from '../task';
|
|
|
14
15
|
import { AppVID } from '../../types/app';
|
|
15
16
|
import { ActivityType } from '../../types/activity';
|
|
16
17
|
import { CacheMode } from '../../types/cache';
|
|
17
|
-
import { JobState, JobData, JobMetadata, JobOutput, JobStatus } from '../../types/job';
|
|
18
|
+
import { JobState, JobData, JobMetadata, JobOutput, JobStatus, JobInterruptOptions, JobCompletionOptions } from '../../types/job';
|
|
18
19
|
import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '../../types/hotmesh';
|
|
19
20
|
import { JobMessageCallback } from '../../types/quorum';
|
|
20
21
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
@@ -50,7 +51,7 @@ declare class EngineService {
|
|
|
50
51
|
processWebHooks(): Promise<void>;
|
|
51
52
|
processTimeHooks(): Promise<void>;
|
|
52
53
|
throttle(delayInMillis: number): Promise<void>;
|
|
53
|
-
initActivity(topic: string, data?: JobData, context?: JobState): Promise<Await | Cycle | Hook | Signal | Trigger | Worker>;
|
|
54
|
+
initActivity(topic: string, data?: JobData, context?: JobState): Promise<Await | Cycle | Hook | Signal | Trigger | Worker | Interrupt>;
|
|
54
55
|
getSchema(topic: string): Promise<[activityId: string, schema: ActivityType]>;
|
|
55
56
|
getSettings(): Promise<HotMeshSettings>;
|
|
56
57
|
isPrivate(topic: string): boolean;
|
|
@@ -63,9 +64,10 @@ declare class EngineService {
|
|
|
63
64
|
execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<string>;
|
|
64
65
|
hasParentJob(context: JobState): boolean;
|
|
65
66
|
resolveError(metadata: JobMetadata): StreamError | undefined;
|
|
67
|
+
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
|
|
66
68
|
scrub(jobId: string): Promise<void>;
|
|
67
69
|
hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
|
|
68
|
-
hookTime(jobId: string, activityId: string): Promise<
|
|
70
|
+
hookTime(jobId: string, activityId: string, type?: 'sleep' | 'expire' | 'interrupt'): Promise<string | void>;
|
|
69
71
|
hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
70
72
|
pub(topic: string, data: JobData, context?: JobState): Promise<string>;
|
|
71
73
|
sub(topic: string, callback: JobMessageCallback): Promise<void>;
|
|
@@ -73,14 +75,20 @@ declare class EngineService {
|
|
|
73
75
|
psub(wild: string, callback: JobMessageCallback): Promise<void>;
|
|
74
76
|
punsub(wild: string): Promise<void>;
|
|
75
77
|
pubsub(topic: string, data: JobData, context?: JobState | null, timeout?: number): Promise<JobOutput>;
|
|
76
|
-
|
|
78
|
+
pubOneTimeSubs(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
|
|
77
79
|
getPublishesTopic(context: JobState): Promise<string>;
|
|
78
|
-
|
|
80
|
+
pubPermSubs(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
|
|
79
81
|
add(streamData: StreamData | StreamDataResponse): Promise<string>;
|
|
80
82
|
registerJobCallback(jobId: string, jobCallback: JobMessageCallback): void;
|
|
81
83
|
delistJobCallback(jobId: string): void;
|
|
82
84
|
hasOneTimeSubscription(context: JobState): boolean;
|
|
83
|
-
runJobCompletionTasks(context: JobState,
|
|
85
|
+
runJobCompletionTasks(context: JobState, options?: JobCompletionOptions): Promise<string | void>;
|
|
86
|
+
/**
|
|
87
|
+
* Job hash expiration is typically reliant on the metadata field
|
|
88
|
+
* if the activity concludes normally. However, if the job is `interrupted`,
|
|
89
|
+
* it will be expired immediately.
|
|
90
|
+
*/
|
|
91
|
+
resolveExpires(context: JobState, options: JobCompletionOptions): number;
|
|
84
92
|
getStatus(jobId: string): Promise<JobStatus>;
|
|
85
93
|
getState(topic: string, jobId: string): Promise<JobOutput>;
|
|
86
94
|
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.EngineService = void 0;
|
|
7
7
|
const key_1 = require("../../modules/key");
|
|
8
|
+
const enums_1 = require("../../modules/enums");
|
|
8
9
|
const utils_1 = require("../../modules/utils");
|
|
9
10
|
const activities_1 = __importDefault(require("../activities"));
|
|
10
11
|
const compiler_1 = require("../compiler");
|
|
@@ -20,11 +21,6 @@ const ioredis_3 = require("../sub/clients/ioredis");
|
|
|
20
21
|
const redis_3 = require("../sub/clients/redis");
|
|
21
22
|
const task_1 = require("../task");
|
|
22
23
|
const stream_2 = require("../../types/stream");
|
|
23
|
-
//wait time to see if a job is complete
|
|
24
|
-
const OTT_WAIT_TIME = 1000;
|
|
25
|
-
const STATUS_CODE_SUCCESS = 200;
|
|
26
|
-
const STATUS_CODE_PENDING = 202;
|
|
27
|
-
const STATUS_CODE_TIMEOUT = 504;
|
|
28
24
|
class EngineService {
|
|
29
25
|
constructor() {
|
|
30
26
|
this.cacheMode = 'cache';
|
|
@@ -230,6 +226,7 @@ class EngineService {
|
|
|
230
226
|
aid: streamData.metadata.aid,
|
|
231
227
|
status: streamData.status || stream_2.StreamStatus.SUCCESS,
|
|
232
228
|
code: streamData.code || 200,
|
|
229
|
+
type: streamData.type,
|
|
233
230
|
});
|
|
234
231
|
const context = {
|
|
235
232
|
metadata: {
|
|
@@ -285,6 +282,7 @@ class EngineService {
|
|
|
285
282
|
const spn = context['$self']?.output?.metadata?.l2s || context['$self']?.output?.metadata?.l1s;
|
|
286
283
|
const streamData = {
|
|
287
284
|
metadata: {
|
|
285
|
+
guid: (0, utils_1.guid)(),
|
|
288
286
|
jid: context.metadata.pj,
|
|
289
287
|
dad: context.metadata.pd,
|
|
290
288
|
aid: context.metadata.pa,
|
|
@@ -301,17 +299,16 @@ class EngineService {
|
|
|
301
299
|
}
|
|
302
300
|
else if (emit) {
|
|
303
301
|
streamData.status = stream_2.StreamStatus.PENDING;
|
|
304
|
-
streamData.code = STATUS_CODE_PENDING;
|
|
302
|
+
streamData.code = enums_1.STATUS_CODE_PENDING;
|
|
305
303
|
}
|
|
306
304
|
else {
|
|
307
305
|
streamData.status = stream_2.StreamStatus.SUCCESS;
|
|
308
|
-
streamData.code = STATUS_CODE_SUCCESS;
|
|
306
|
+
streamData.code = enums_1.STATUS_CODE_SUCCESS;
|
|
309
307
|
}
|
|
310
308
|
return (await this.streamSignaler?.publishMessage(null, streamData));
|
|
311
309
|
}
|
|
312
310
|
}
|
|
313
311
|
hasParentJob(context) {
|
|
314
|
-
//todo: include the dimensional address (pd)
|
|
315
312
|
return Boolean(context.metadata.pj && context.metadata.pa);
|
|
316
313
|
}
|
|
317
314
|
resolveError(metadata) {
|
|
@@ -319,8 +316,19 @@ class EngineService {
|
|
|
319
316
|
return JSON.parse(metadata.err);
|
|
320
317
|
}
|
|
321
318
|
}
|
|
319
|
+
// ****************** `INTERRUPT` ACTIVE JOBS *****************
|
|
320
|
+
async interrupt(topic, jobId, options = {}) {
|
|
321
|
+
await this.store.interrupt(topic, jobId, options);
|
|
322
|
+
const context = await this.getState(topic, jobId);
|
|
323
|
+
const completionOpts = {
|
|
324
|
+
interrupt: options.descend,
|
|
325
|
+
expire: options.expire,
|
|
326
|
+
};
|
|
327
|
+
return await this.runJobCompletionTasks(context, completionOpts);
|
|
328
|
+
}
|
|
322
329
|
// ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
|
|
323
330
|
async scrub(jobId) {
|
|
331
|
+
//todo: do not allow scrubbing of non-existent or actively running job
|
|
324
332
|
await this.store.scrub(jobId);
|
|
325
333
|
}
|
|
326
334
|
// ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
|
|
@@ -332,6 +340,7 @@ class EngineService {
|
|
|
332
340
|
status,
|
|
333
341
|
code,
|
|
334
342
|
metadata: {
|
|
343
|
+
guid: (0, utils_1.guid)(),
|
|
335
344
|
aid,
|
|
336
345
|
topic
|
|
337
346
|
},
|
|
@@ -339,13 +348,21 @@ class EngineService {
|
|
|
339
348
|
};
|
|
340
349
|
return await this.streamSignaler.publishMessage(null, streamData);
|
|
341
350
|
}
|
|
342
|
-
async hookTime(jobId, activityId) {
|
|
343
|
-
|
|
351
|
+
async hookTime(jobId, activityId, type) {
|
|
352
|
+
if (type === 'interrupt') {
|
|
353
|
+
return await this.interrupt(activityId, //note: 'activityId' is the actually job topic
|
|
354
|
+
jobId, { suppress: true, expire: 1 });
|
|
355
|
+
}
|
|
356
|
+
else if (type === 'expire') {
|
|
357
|
+
return await this.store.expireJob(jobId, 1);
|
|
358
|
+
}
|
|
359
|
+
//'sleep': parse the activityId into parts
|
|
344
360
|
const [aid, ...dimensions] = activityId.split(',');
|
|
345
361
|
const dad = `,${dimensions.join(',')}`;
|
|
346
362
|
const streamData = {
|
|
347
363
|
type: stream_2.StreamDataType.TIMEHOOK,
|
|
348
364
|
metadata: {
|
|
365
|
+
guid: (0, utils_1.guid)(),
|
|
349
366
|
jid: jobId,
|
|
350
367
|
aid,
|
|
351
368
|
dad,
|
|
@@ -407,7 +424,7 @@ class EngineService {
|
|
|
407
424
|
return await this.subscribe.punsubscribe(key_1.KeyType.QUORUM, this.appId, wild);
|
|
408
425
|
}
|
|
409
426
|
//publish and await (returns the job and data (if ready)); throws error with jobid if not
|
|
410
|
-
async pubsub(topic, data, context, timeout = OTT_WAIT_TIME) {
|
|
427
|
+
async pubsub(topic, data, context, timeout = enums_1.OTT_WAIT_TIME) {
|
|
411
428
|
context = {
|
|
412
429
|
metadata: {
|
|
413
430
|
ngn: this.guid,
|
|
@@ -432,14 +449,14 @@ class EngineService {
|
|
|
432
449
|
setTimeout(() => {
|
|
433
450
|
this.delistJobCallback(jobId);
|
|
434
451
|
reject({
|
|
435
|
-
code: STATUS_CODE_TIMEOUT,
|
|
452
|
+
code: enums_1.STATUS_CODE_TIMEOUT,
|
|
436
453
|
message: 'timeout',
|
|
437
454
|
job_id: jobId
|
|
438
455
|
});
|
|
439
456
|
}, timeout);
|
|
440
457
|
});
|
|
441
458
|
}
|
|
442
|
-
async
|
|
459
|
+
async pubOneTimeSubs(context, jobOutput, emit = false) {
|
|
443
460
|
//todo: subscriber should query for the job...only publish minimum context needed
|
|
444
461
|
if (this.hasOneTimeSubscription(context)) {
|
|
445
462
|
const message = {
|
|
@@ -456,7 +473,7 @@ class EngineService {
|
|
|
456
473
|
const schema = await this.store.getSchema(activityId, config);
|
|
457
474
|
return schema.publishes;
|
|
458
475
|
}
|
|
459
|
-
async
|
|
476
|
+
async pubPermSubs(context, jobOutput, emit = false) {
|
|
460
477
|
const topic = await this.getPublishesTopic(context);
|
|
461
478
|
if (topic) {
|
|
462
479
|
const message = {
|
|
@@ -480,22 +497,30 @@ class EngineService {
|
|
|
480
497
|
return Boolean(context.metadata.ngn);
|
|
481
498
|
}
|
|
482
499
|
// ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
|
|
483
|
-
async runJobCompletionTasks(context,
|
|
484
|
-
//
|
|
500
|
+
async runJobCompletionTasks(context, options = {}) {
|
|
501
|
+
//'emit' indicates the job is still active
|
|
485
502
|
const isAwait = this.hasParentJob(context);
|
|
486
|
-
const
|
|
503
|
+
const isOneTimeSub = this.hasOneTimeSubscription(context);
|
|
487
504
|
const topic = await this.getPublishesTopic(context);
|
|
488
|
-
|
|
505
|
+
let msgId;
|
|
506
|
+
if (isAwait || isOneTimeSub || topic) {
|
|
489
507
|
const jobOutput = await this.getState(context.metadata.tpc, context.metadata.jid);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
508
|
+
msgId = await this.execAdjacentParent(context, jobOutput, options.emit);
|
|
509
|
+
this.pubOneTimeSubs(context, jobOutput, options.emit);
|
|
510
|
+
this.pubPermSubs(context, jobOutput, options.emit);
|
|
511
|
+
}
|
|
512
|
+
if (!options.emit) {
|
|
513
|
+
this.task.registerJobForCleanup(context.metadata.jid, this.resolveExpires(context, options), options);
|
|
514
|
+
}
|
|
515
|
+
return msgId;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Job hash expiration is typically reliant on the metadata field
|
|
519
|
+
* if the activity concludes normally. However, if the job is `interrupted`,
|
|
520
|
+
* it will be expired immediately.
|
|
521
|
+
*/
|
|
522
|
+
resolveExpires(context, options) {
|
|
523
|
+
return context.metadata.expire ?? options.expire ?? enums_1.DURABLE_EXPIRE_SECONDS;
|
|
499
524
|
}
|
|
500
525
|
// ****** GET JOB STATE/COLLATION STATUS BY ID *********
|
|
501
526
|
async getStatus(jobId) {
|
|
@@ -2,7 +2,7 @@ import { EngineService } from '../engine';
|
|
|
2
2
|
import { ILogger } from '../logger';
|
|
3
3
|
import { QuorumService } from '../quorum';
|
|
4
4
|
import { WorkerService } from '../worker';
|
|
5
|
-
import { JobState, JobData, JobOutput, JobStatus } from '../../types/job';
|
|
5
|
+
import { JobState, JobData, JobOutput, JobStatus, JobInterruptOptions } 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';
|
|
@@ -34,12 +34,14 @@ declare class HotMeshService {
|
|
|
34
34
|
plan(path: string): Promise<HotMeshManifest>;
|
|
35
35
|
deploy(pathOrYAML: string): Promise<HotMeshManifest>;
|
|
36
36
|
activate(version: string, delay?: number): Promise<boolean>;
|
|
37
|
+
inventory(version: string, delay?: number): Promise<number>;
|
|
37
38
|
getStats(topic: string, query: JobStatsInput): Promise<StatsResponse>;
|
|
38
39
|
getStatus(jobId: string): Promise<JobStatus>;
|
|
39
40
|
getState(topic: string, jobId: string): Promise<JobOutput>;
|
|
40
41
|
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
41
42
|
getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
|
|
42
43
|
resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
|
|
44
|
+
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
|
|
43
45
|
scrub(jobId: string): Promise<void>;
|
|
44
46
|
hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
|
|
45
47
|
hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HotMeshService = void 0;
|
|
4
|
-
const nanoid_1 = require("nanoid");
|
|
5
4
|
const key_1 = require("../../modules/key");
|
|
5
|
+
const utils_1 = require("../../modules/utils");
|
|
6
6
|
const redis_1 = require("../connector/clients/redis");
|
|
7
7
|
const ioredis_1 = require("../connector/clients/ioredis");
|
|
8
8
|
const engine_1 = require("../engine");
|
|
@@ -41,7 +41,7 @@ class HotMeshService {
|
|
|
41
41
|
}
|
|
42
42
|
static async init(config) {
|
|
43
43
|
const instance = new HotMeshService();
|
|
44
|
-
instance.guid = (0,
|
|
44
|
+
instance.guid = (0, utils_1.guid)();
|
|
45
45
|
instance.verifyAndSetNamespace(config.namespace);
|
|
46
46
|
instance.verifyAndSetAppId(config.appId);
|
|
47
47
|
instance.logger = new logger_1.LoggerService(config.appId, instance.guid, config.name || '', config.logLevel);
|
|
@@ -51,7 +51,7 @@ class HotMeshService {
|
|
|
51
51
|
return instance;
|
|
52
52
|
}
|
|
53
53
|
static guid() {
|
|
54
|
-
return (0,
|
|
54
|
+
return (0, utils_1.guid)();
|
|
55
55
|
}
|
|
56
56
|
async initEngine(config, logger) {
|
|
57
57
|
if (config.engine) {
|
|
@@ -100,6 +100,10 @@ class HotMeshService {
|
|
|
100
100
|
//activation is a quorum operation
|
|
101
101
|
return await this.quorum?.activate(version, delay);
|
|
102
102
|
}
|
|
103
|
+
async inventory(version, delay) {
|
|
104
|
+
//get count of all peers
|
|
105
|
+
return await this.quorum?.inventory(delay);
|
|
106
|
+
}
|
|
103
107
|
// ************* REPORTER METHODS *************
|
|
104
108
|
async getStats(topic, query) {
|
|
105
109
|
return await this.engine?.getStats(topic, query);
|
|
@@ -119,6 +123,10 @@ class HotMeshService {
|
|
|
119
123
|
async resolveQuery(topic, query) {
|
|
120
124
|
return await this.engine?.resolveQuery(topic, query);
|
|
121
125
|
}
|
|
126
|
+
// ****************** `INTERRUPT` ACTIVE JOBS *****************
|
|
127
|
+
async interrupt(topic, jobId, options = {}) {
|
|
128
|
+
return await this.engine?.interrupt(topic, jobId, options);
|
|
129
|
+
}
|
|
122
130
|
// ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
|
|
123
131
|
async scrub(jobId) {
|
|
124
132
|
await this.engine?.scrub(jobId);
|
|
@@ -29,6 +29,7 @@ declare class QuorumService {
|
|
|
29
29
|
pub(quorumMessage: ThrottleMessage): Promise<boolean>;
|
|
30
30
|
sub(callback: QuorumMessageCallback): Promise<void>;
|
|
31
31
|
unsub(callback: QuorumMessageCallback): Promise<void>;
|
|
32
|
+
inventory(delay?: number): Promise<number>;
|
|
32
33
|
activate(version: string, delay?: number): Promise<boolean>;
|
|
33
34
|
}
|
|
34
35
|
export { QuorumService };
|
|
@@ -82,6 +82,9 @@ class QuorumService {
|
|
|
82
82
|
else if (message.type === 'job') {
|
|
83
83
|
self.engine.routeToSubscribers(message.topic, message.job);
|
|
84
84
|
}
|
|
85
|
+
else if (message.type === 'cron') {
|
|
86
|
+
self.engine.processTimeHooks();
|
|
87
|
+
}
|
|
85
88
|
//if there are any callbacks, call them
|
|
86
89
|
if (self.callbacks.length > 0) {
|
|
87
90
|
self.callbacks.forEach(cb => cb(topic, message));
|
|
@@ -114,6 +117,13 @@ class QuorumService {
|
|
|
114
117
|
this.callbacks = this.callbacks.filter(cb => cb !== callback);
|
|
115
118
|
}
|
|
116
119
|
// ************* COMPILER METHODS *************
|
|
120
|
+
async inventory(delay = QUORUM_DELAY) {
|
|
121
|
+
await this.requestQuorum(delay);
|
|
122
|
+
const q1 = await this.requestQuorum(delay);
|
|
123
|
+
const q2 = await this.requestQuorum(delay);
|
|
124
|
+
const q3 = await this.requestQuorum(delay);
|
|
125
|
+
return Math.round((q1 + q2 + q3) / 3);
|
|
126
|
+
}
|
|
117
127
|
async activate(version, delay = QUORUM_DELAY) {
|
|
118
128
|
version = version.toString();
|
|
119
129
|
const config = await this.engine.getVID();
|