@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,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { STATUS_CODE_INTERRUPT } from '../../modules/enums';
|
|
2
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
+
import { JobInterruptOptions, JobOutput } from '../../types/job';
|
|
4
|
+
import { StreamError } from '../../types/stream';
|
|
3
5
|
|
|
4
6
|
export class WorkflowHandleService {
|
|
5
7
|
hotMesh: HotMesh;
|
|
@@ -12,10 +14,22 @@ export class WorkflowHandleService {
|
|
|
12
14
|
this.hotMesh = hotMesh;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Sends a signal to the workflow. This is a way to send
|
|
19
|
+
* a message to a workflow that is paused due to having
|
|
20
|
+
* executed a `waitForSignal` workflow extension. Awakens
|
|
21
|
+
* the workflow if no other signals are pending.
|
|
22
|
+
*/
|
|
15
23
|
async signal(signalId: string, data: Record<any, any>): Promise<void> {
|
|
16
24
|
await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
|
|
17
25
|
}
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Returns the job state of the workflow. If the workflow has completed
|
|
29
|
+
* this is also the job output. If the workflow is still running, this
|
|
30
|
+
* is the current state of the job, but it may change depending upon
|
|
31
|
+
* the activities that remain.
|
|
32
|
+
*/
|
|
19
33
|
async state(metadata = false): Promise<Record<string, any>> {
|
|
20
34
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
21
35
|
if (!state.data && state.metadata.err) {
|
|
@@ -24,14 +38,41 @@ export class WorkflowHandleService {
|
|
|
24
38
|
return metadata ? state : state.data;
|
|
25
39
|
}
|
|
26
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Returns the current search state of the workflow. This is
|
|
43
|
+
* different than the job state or individual activity state.
|
|
44
|
+
* Search state represents name/value pairs that were added
|
|
45
|
+
* to the workflow. As the workflow is stored in a Redis hash,
|
|
46
|
+
* this is a way to store additional data that is indexed
|
|
47
|
+
* and searchable using the RediSearch module.
|
|
48
|
+
*/
|
|
27
49
|
async queryState(fields: string[]): Promise<Record<string, any>> {
|
|
28
50
|
return await this.hotMesh.getQueryState(this.workflowId, fields);
|
|
29
51
|
}
|
|
30
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Returns the current status of the workflow. This is a semaphore
|
|
55
|
+
* value that represents the current state of the workflow, where
|
|
56
|
+
* 0 is complete and a negative value represents that the flow was
|
|
57
|
+
* interrupted.
|
|
58
|
+
*/
|
|
31
59
|
async status(): Promise<number> {
|
|
32
60
|
return await this.hotMesh.getStatus(this.workflowId);
|
|
33
61
|
}
|
|
34
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Interrupts a running workflow. Standard Job Completion tasks will
|
|
65
|
+
* run. Subscribers will be notified and the job hash will be expired.
|
|
66
|
+
*/
|
|
67
|
+
async interrupt(options?: JobInterruptOptions): Promise<string> {
|
|
68
|
+
return await this.hotMesh.interrupt(`${this.hotMesh.appId}.execute`, this.workflowId, options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Awaits for the workflow to complete and returns the result. If
|
|
73
|
+
* the workflow thows and error, this method will likewise throw
|
|
74
|
+
* an error.
|
|
75
|
+
*/
|
|
35
76
|
async result(loadState?: boolean): Promise<any> {
|
|
36
77
|
if (loadState) {
|
|
37
78
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
@@ -58,29 +99,34 @@ export class WorkflowHandleService {
|
|
|
58
99
|
return reject(JSON.parse(err));
|
|
59
100
|
} else if (!response) {
|
|
60
101
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
61
|
-
if (
|
|
62
|
-
|
|
102
|
+
if (state.metadata.err) {
|
|
103
|
+
const error = JSON.parse(state.metadata.err) as StreamError;
|
|
104
|
+
if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
|
|
105
|
+
return reject({ ...error, job_id: this.workflowId });
|
|
106
|
+
}
|
|
63
107
|
}
|
|
64
108
|
response = state.data?.response;
|
|
65
109
|
}
|
|
66
110
|
resolve(response);
|
|
67
111
|
};
|
|
68
112
|
//check for done
|
|
69
|
-
if (status
|
|
113
|
+
if (status <= 0) {
|
|
70
114
|
return complete();
|
|
71
115
|
}
|
|
72
116
|
//subscribe to topic
|
|
73
117
|
this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
118
|
+
if (state.metadata.err) {
|
|
119
|
+
const error = JSON.parse(state.metadata.err) as StreamError;
|
|
120
|
+
if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
|
|
121
|
+
return await complete(null, state.metadata.err);
|
|
122
|
+
}
|
|
78
123
|
}
|
|
124
|
+
await complete(state.data?.response);
|
|
79
125
|
});
|
|
80
126
|
//resolve for race condition
|
|
81
127
|
setTimeout(async () => {
|
|
82
128
|
status = await this.hotMesh.getStatus(this.workflowId);
|
|
83
|
-
if (status
|
|
129
|
+
if (status <= 0) {
|
|
84
130
|
await complete();
|
|
85
131
|
}
|
|
86
132
|
}, 0);
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { nanoid } from 'nanoid';
|
|
2
|
-
|
|
3
1
|
import { Durable } from '.';
|
|
4
2
|
import { asyncLocalStorage } from './asyncLocalStorage';
|
|
5
3
|
import { ClientService as Client } from './client';
|
|
@@ -18,6 +16,7 @@ import {
|
|
|
18
16
|
WorkflowSearchOptions } from '../../types/durable';
|
|
19
17
|
import { RedisOptions, RedisClass } from '../../types/redis';
|
|
20
18
|
import { StringAnyType } from '../../types/serializer';
|
|
19
|
+
import { guid } from '../../modules/utils';
|
|
21
20
|
|
|
22
21
|
/**
|
|
23
22
|
* The base class for running MeshOS workflows.
|
|
@@ -117,7 +116,7 @@ export class MeshOSService {
|
|
|
117
116
|
*/
|
|
118
117
|
static mintGuid(): string {
|
|
119
118
|
const my = new this();
|
|
120
|
-
return `${my.search?.prefix?.[0]}${
|
|
119
|
+
return `${my.search?.prefix?.[0]}${guid()}`;
|
|
121
120
|
}
|
|
122
121
|
|
|
123
122
|
/**
|
|
@@ -201,14 +201,21 @@ export class WorkerService {
|
|
|
201
201
|
//incoming data payload has arguments and workflowId
|
|
202
202
|
const workflowInput = data.data as unknown as WorkflowDataType;
|
|
203
203
|
const context = new Map();
|
|
204
|
+
context.set('raw', data);
|
|
204
205
|
context.set('namespace', config.namespace ?? APP_ID);
|
|
205
206
|
context.set('counter', counter);
|
|
206
207
|
context.set('workflowId', workflowInput.workflowId);
|
|
207
|
-
|
|
208
|
+
context.set('workflowId', workflowInput.workflowId);
|
|
209
|
+
if (workflowInput.originJobId) {
|
|
210
|
+
//if present there is an origin job to which this job is subordinated;
|
|
211
|
+
// garbage collect (expire) this job when originJobId is expired
|
|
212
|
+
context.set('originJobId', workflowInput.originJobId);
|
|
213
|
+
}
|
|
214
|
+
if (workflowInput.workflowDimension) {
|
|
208
215
|
//every hook function runs in an isolated dimension controlled
|
|
209
216
|
//by the index assigned when the signal was received; even if the
|
|
210
217
|
//hook function re-runs, its scope will always remain constant
|
|
211
|
-
context.set('workflowDimension',
|
|
218
|
+
context.set('workflowDimension', workflowInput.workflowDimension);
|
|
212
219
|
}
|
|
213
220
|
context.set('workflowTopic', workflowTopic);
|
|
214
221
|
context.set('workflowName', workflowTopic.split('-').pop());
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
ProxyType,
|
|
20
20
|
WorkflowContext,
|
|
21
21
|
WorkflowOptions } from "../../types/durable";
|
|
22
|
-
import { JobOutput, JobState } from '../../types/job';
|
|
22
|
+
import { JobInterruptOptions, JobOutput, JobState } from '../../types/job';
|
|
23
23
|
import { StreamStatus } from '../../types/stream';
|
|
24
24
|
import { deterministicRandom } from '../../modules/utils';
|
|
25
25
|
|
|
@@ -35,6 +35,7 @@ export class WorkflowService {
|
|
|
35
35
|
const store = asyncLocalStorage.getStore();
|
|
36
36
|
const namespace = store.get('namespace');
|
|
37
37
|
const workflowId = store.get('workflowId');
|
|
38
|
+
const originJobId = store.get('originJobId');
|
|
38
39
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
39
40
|
const workflowTrace = store.get('workflowTrace');
|
|
40
41
|
const workflowSpan = store.get('workflowSpan');
|
|
@@ -65,6 +66,7 @@ export class WorkflowService {
|
|
|
65
66
|
...options,
|
|
66
67
|
namespace,
|
|
67
68
|
workflowId: childJobId,
|
|
69
|
+
originJobId: originJobId ?? workflowId,
|
|
68
70
|
parentWorkflowId,
|
|
69
71
|
workflowTrace,
|
|
70
72
|
workflowSpan,
|
|
@@ -289,6 +291,66 @@ export class WorkflowService {
|
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
293
|
|
|
294
|
+
static getLocalState() {
|
|
295
|
+
const store = asyncLocalStorage.getStore();
|
|
296
|
+
return {
|
|
297
|
+
workflowId: store.get('workflowId'),
|
|
298
|
+
namespace: store.get('namespace'),
|
|
299
|
+
workflowTopic: store.get('workflowTopic'),
|
|
300
|
+
workflowDimension: store.get('workflowDimension') ?? '',
|
|
301
|
+
counter: store.get('counter'),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Executes a function once and caches the result. If the function is called
|
|
307
|
+
* again, the cached result is returned. This is useful for wrapping
|
|
308
|
+
* expensive activity calls that should only be run once, but which might
|
|
309
|
+
* not require the configuration nuance/expense provided by proxyActivities.
|
|
310
|
+
* @template T - the result type
|
|
311
|
+
*/
|
|
312
|
+
static async once<T>(fn: (...args: any[]) => Promise<T>, ...args: any[]): Promise<T> {
|
|
313
|
+
const {
|
|
314
|
+
workflowId,
|
|
315
|
+
namespace,
|
|
316
|
+
workflowTopic,
|
|
317
|
+
workflowDimension,
|
|
318
|
+
counter: COUNTER,
|
|
319
|
+
} = WorkflowService.getLocalState();
|
|
320
|
+
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
321
|
+
const sessionId = `-once${workflowDimension}-${execIndex}-`;
|
|
322
|
+
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
323
|
+
const keyParams = {
|
|
324
|
+
appId: hotMeshClient.appId,
|
|
325
|
+
jobId: workflowId
|
|
326
|
+
}
|
|
327
|
+
const workflowGuid = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
|
|
328
|
+
const value = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId) as string;
|
|
329
|
+
if (value) {
|
|
330
|
+
return JSON.parse(value) as T;
|
|
331
|
+
}
|
|
332
|
+
const response = await fn(...args);
|
|
333
|
+
await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, JSON.stringify(response));
|
|
334
|
+
return response;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Interrupts a running job
|
|
339
|
+
*
|
|
340
|
+
* @param {string} jobId - the target job id
|
|
341
|
+
* @param {JobInterruptOptions} options - the interrupt options
|
|
342
|
+
* @returns {Promise<string>} - the stream id
|
|
343
|
+
*/
|
|
344
|
+
static async interrupt(jobId: string, options: JobInterruptOptions = {}): Promise<string | void> {
|
|
345
|
+
const store = asyncLocalStorage.getStore();
|
|
346
|
+
const workflowTopic = store.get('workflowTopic');
|
|
347
|
+
const namespace = store.get('namespace');
|
|
348
|
+
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
349
|
+
if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'interrupt')) {
|
|
350
|
+
return await hotMeshClient.interrupt(`${hotMeshClient.appId}.execute`, jobId, options);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
292
354
|
/**
|
|
293
355
|
* Sleeps the workflow for a duration. As the function is reentrant,
|
|
294
356
|
* upon reentry, the function will traverse prior execution paths up
|
|
@@ -407,6 +469,7 @@ export class WorkflowService {
|
|
|
407
469
|
//increment by state (not value) to avoid race conditions
|
|
408
470
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
409
471
|
const workflowId = store.get('workflowId');
|
|
472
|
+
const originJobId = store.get('originJobId');
|
|
410
473
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
411
474
|
const workflowTopic = store.get('workflowTopic');
|
|
412
475
|
const trc = store.get('workflowTrace');
|
|
@@ -439,7 +502,8 @@ export class WorkflowService {
|
|
|
439
502
|
const duration = ms(options?.startToCloseTimeout || '1 minute');
|
|
440
503
|
const payload = {
|
|
441
504
|
arguments: Array.from(arguments),
|
|
442
|
-
//the
|
|
505
|
+
//when the origin job is removed
|
|
506
|
+
originJobId: originJobId ?? workflowId,
|
|
443
507
|
parentWorkflowId: `${workflowId}-a`,
|
|
444
508
|
workflowId: activityJobId,
|
|
445
509
|
workflowTopic: activityTopic,
|
package/services/engine/index.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { KeyType } from '../../modules/key';
|
|
2
|
+
import {
|
|
3
|
+
OTT_WAIT_TIME,
|
|
4
|
+
STATUS_CODE_SUCCESS,
|
|
5
|
+
STATUS_CODE_PENDING,
|
|
6
|
+
STATUS_CODE_TIMEOUT,
|
|
7
|
+
DURABLE_EXPIRE_SECONDS} from '../../modules/enums';
|
|
2
8
|
import {
|
|
3
9
|
formatISODate,
|
|
4
10
|
getSubscriptionTopic,
|
|
11
|
+
guid,
|
|
5
12
|
identifyRedisType,
|
|
6
13
|
polyfill,
|
|
7
14
|
restoreHierarchy } from '../../modules/utils';
|
|
@@ -9,6 +16,7 @@ import Activities from '../activities';
|
|
|
9
16
|
import { Await } from '../activities/await';
|
|
10
17
|
import { Cycle } from '../activities/cycle';
|
|
11
18
|
import { Hook } from '../activities/hook';
|
|
19
|
+
import { Interrupt } from '../activities/interrupt';
|
|
12
20
|
import { Signal } from '../activities/signal';
|
|
13
21
|
import { Worker } from '../activities/worker';
|
|
14
22
|
import { Trigger } from '../activities/trigger';
|
|
@@ -41,7 +49,9 @@ import {
|
|
|
41
49
|
JobMetadata,
|
|
42
50
|
JobOutput,
|
|
43
51
|
PartialJobState,
|
|
44
|
-
JobStatus
|
|
52
|
+
JobStatus,
|
|
53
|
+
JobInterruptOptions,
|
|
54
|
+
JobCompletionOptions } from '../../types/job';
|
|
45
55
|
import {
|
|
46
56
|
HotMeshApps,
|
|
47
57
|
HotMeshConfig,
|
|
@@ -69,12 +79,6 @@ import {
|
|
|
69
79
|
StreamRole,
|
|
70
80
|
StreamStatus } from '../../types/stream';
|
|
71
81
|
|
|
72
|
-
//wait time to see if a job is complete
|
|
73
|
-
const OTT_WAIT_TIME = 1000;
|
|
74
|
-
const STATUS_CODE_SUCCESS = 200;
|
|
75
|
-
const STATUS_CODE_PENDING = 202;
|
|
76
|
-
const STATUS_CODE_TIMEOUT = 504;
|
|
77
|
-
|
|
78
82
|
class EngineService {
|
|
79
83
|
namespace: string;
|
|
80
84
|
apps: HotMeshApps | null;
|
|
@@ -239,7 +243,7 @@ class EngineService {
|
|
|
239
243
|
}
|
|
240
244
|
|
|
241
245
|
// ************* METADATA/MODEL METHODS *************
|
|
242
|
-
async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Await|Cycle|Hook|Signal|Trigger|Worker> {
|
|
246
|
+
async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Await|Cycle|Hook|Signal|Trigger|Worker|Interrupt> {
|
|
243
247
|
const [activityId, schema] = await this.getSchema(topic);
|
|
244
248
|
polyfill
|
|
245
249
|
const ActivityHandler = Activities[polyfill.resolveActivityType(schema.type)];
|
|
@@ -329,6 +333,7 @@ class EngineService {
|
|
|
329
333
|
aid: streamData.metadata.aid,
|
|
330
334
|
status: streamData.status || StreamStatus.SUCCESS,
|
|
331
335
|
code: streamData.code || 200,
|
|
336
|
+
type: streamData.type,
|
|
332
337
|
});
|
|
333
338
|
const context: PartialJobState = {
|
|
334
339
|
metadata: {
|
|
@@ -380,6 +385,7 @@ class EngineService {
|
|
|
380
385
|
const spn = context['$self']?.output?.metadata?.l2s || context['$self']?.output?.metadata?.l1s;
|
|
381
386
|
const streamData: StreamData = {
|
|
382
387
|
metadata: {
|
|
388
|
+
guid: guid(),
|
|
383
389
|
jid: context.metadata.pj,
|
|
384
390
|
dad: context.metadata.pd,
|
|
385
391
|
aid: context.metadata.pa,
|
|
@@ -404,17 +410,28 @@ class EngineService {
|
|
|
404
410
|
}
|
|
405
411
|
}
|
|
406
412
|
hasParentJob(context: JobState): boolean {
|
|
407
|
-
//todo: include the dimensional address (pd)
|
|
408
413
|
return Boolean(context.metadata.pj && context.metadata.pa);
|
|
409
414
|
}
|
|
410
415
|
resolveError(metadata: JobMetadata): StreamError | undefined {
|
|
411
416
|
if (metadata && metadata.err) {
|
|
412
417
|
return JSON.parse(metadata.err) as StreamError;
|
|
413
|
-
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ****************** `INTERRUPT` ACTIVE JOBS *****************
|
|
422
|
+
async interrupt(topic: string, jobId: string, options: JobInterruptOptions = {}): Promise<string> {
|
|
423
|
+
await this.store.interrupt(topic, jobId, options);
|
|
424
|
+
const context = await this.getState(topic, jobId) as JobState;
|
|
425
|
+
const completionOpts: JobCompletionOptions = {
|
|
426
|
+
interrupt: options.descend,
|
|
427
|
+
expire: options.expire,
|
|
428
|
+
};
|
|
429
|
+
return await this.runJobCompletionTasks(context, completionOpts) as string;
|
|
414
430
|
}
|
|
415
431
|
|
|
416
432
|
// ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
|
|
417
433
|
async scrub(jobId: string) {
|
|
434
|
+
//todo: do not allow scrubbing of non-existent or actively running job
|
|
418
435
|
await this.store.scrub(jobId);
|
|
419
436
|
}
|
|
420
437
|
|
|
@@ -427,6 +444,7 @@ class EngineService {
|
|
|
427
444
|
status,
|
|
428
445
|
code,
|
|
429
446
|
metadata: {
|
|
447
|
+
guid: guid(),
|
|
430
448
|
aid,
|
|
431
449
|
topic
|
|
432
450
|
},
|
|
@@ -434,13 +452,23 @@ class EngineService {
|
|
|
434
452
|
};
|
|
435
453
|
return await this.streamSignaler.publishMessage(null, streamData) as string;
|
|
436
454
|
}
|
|
437
|
-
async hookTime(jobId: string, activityId: string): Promise<
|
|
438
|
-
|
|
455
|
+
async hookTime(jobId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
|
|
456
|
+
if (type === 'interrupt') {
|
|
457
|
+
return await this.interrupt(
|
|
458
|
+
activityId, //note: 'activityId' is the actually job topic
|
|
459
|
+
jobId,
|
|
460
|
+
{ suppress: true, expire: 1 },
|
|
461
|
+
);
|
|
462
|
+
} else if (type === 'expire') {
|
|
463
|
+
return await this.store.expireJob(jobId, 1);
|
|
464
|
+
}
|
|
465
|
+
//'sleep': parse the activityId into parts
|
|
439
466
|
const [aid, ...dimensions] = activityId.split(',');
|
|
440
467
|
const dad = `,${dimensions.join(',')}`;
|
|
441
468
|
const streamData: StreamData = {
|
|
442
469
|
type: StreamDataType.TIMEHOOK,
|
|
443
470
|
metadata: {
|
|
471
|
+
guid: guid(),
|
|
444
472
|
jid: jobId,
|
|
445
473
|
aid,
|
|
446
474
|
dad,
|
|
@@ -540,7 +568,7 @@ class EngineService {
|
|
|
540
568
|
}, timeout);
|
|
541
569
|
});
|
|
542
570
|
}
|
|
543
|
-
async
|
|
571
|
+
async pubOneTimeSubs(context: JobState, jobOutput: JobOutput, emit = false) {
|
|
544
572
|
//todo: subscriber should query for the job...only publish minimum context needed
|
|
545
573
|
if (this.hasOneTimeSubscription(context)) {
|
|
546
574
|
const message: JobMessage = {
|
|
@@ -557,7 +585,7 @@ class EngineService {
|
|
|
557
585
|
const schema = await this.store.getSchema(activityId, config);
|
|
558
586
|
return schema.publishes;
|
|
559
587
|
}
|
|
560
|
-
async
|
|
588
|
+
async pubPermSubs(context: JobState, jobOutput: JobOutput, emit = false) {
|
|
561
589
|
const topic = await this.getPublishesTopic(context);
|
|
562
590
|
if (topic) {
|
|
563
591
|
const message: JobMessage = {
|
|
@@ -584,22 +612,42 @@ class EngineService {
|
|
|
584
612
|
|
|
585
613
|
|
|
586
614
|
// ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
|
|
587
|
-
async runJobCompletionTasks(context: JobState,
|
|
588
|
-
//
|
|
615
|
+
async runJobCompletionTasks(context: JobState, options: JobCompletionOptions = {}): Promise<string | void> {
|
|
616
|
+
//'emit' indicates the job is still active
|
|
589
617
|
const isAwait = this.hasParentJob(context);
|
|
590
|
-
const
|
|
618
|
+
const isOneTimeSub = this.hasOneTimeSubscription(context);
|
|
591
619
|
const topic = await this.getPublishesTopic(context);
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
this.
|
|
620
|
+
let msgId: string;
|
|
621
|
+
if (isAwait || isOneTimeSub || topic) {
|
|
622
|
+
const jobOutput = await this.getState(
|
|
623
|
+
context.metadata.tpc,
|
|
624
|
+
context.metadata.jid,
|
|
625
|
+
);
|
|
626
|
+
msgId = await this.execAdjacentParent(
|
|
627
|
+
context,
|
|
628
|
+
jobOutput,
|
|
629
|
+
options.emit,
|
|
630
|
+
);
|
|
631
|
+
this.pubOneTimeSubs(context, jobOutput, options.emit);
|
|
632
|
+
this.pubPermSubs(context, jobOutput, options.emit);
|
|
599
633
|
}
|
|
600
|
-
if (!emit) {
|
|
601
|
-
this.task.registerJobForCleanup(
|
|
634
|
+
if (!options.emit) {
|
|
635
|
+
this.task.registerJobForCleanup(
|
|
636
|
+
context.metadata.jid,
|
|
637
|
+
this.resolveExpires(context, options),
|
|
638
|
+
options,
|
|
639
|
+
);
|
|
602
640
|
}
|
|
641
|
+
return msgId;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Job hash expiration is typically reliant on the metadata field
|
|
646
|
+
* if the activity concludes normally. However, if the job is `interrupted`,
|
|
647
|
+
* it will be expired immediately.
|
|
648
|
+
*/
|
|
649
|
+
resolveExpires(context: JobState, options: JobCompletionOptions): number {
|
|
650
|
+
return context.metadata.expire ?? options.expire ?? DURABLE_EXPIRE_SECONDS;
|
|
603
651
|
}
|
|
604
652
|
|
|
605
653
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { nanoid } from 'nanoid';
|
|
2
1
|
import { HMNS } from '../../modules/key';
|
|
2
|
+
import { guid } from '../../modules/utils';
|
|
3
3
|
import { RedisConnection } from '../connector/clients/redis';
|
|
4
4
|
import { RedisConnection as IORedisConnection } from '../connector/clients/ioredis';
|
|
5
5
|
import { EngineService } from '../engine';
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
JobState,
|
|
12
12
|
JobData,
|
|
13
13
|
JobOutput,
|
|
14
|
-
JobStatus
|
|
14
|
+
JobStatus,
|
|
15
|
+
JobInterruptOptions} from '../../types/job';
|
|
15
16
|
import {
|
|
16
17
|
HotMeshConfig,
|
|
17
18
|
HotMeshManifest } from '../../types/hotmesh';
|
|
@@ -58,7 +59,7 @@ class HotMeshService {
|
|
|
58
59
|
|
|
59
60
|
static async init(config: HotMeshConfig) {
|
|
60
61
|
const instance = new HotMeshService();
|
|
61
|
-
instance.guid =
|
|
62
|
+
instance.guid = guid();
|
|
62
63
|
instance.verifyAndSetNamespace(config.namespace);
|
|
63
64
|
instance.verifyAndSetAppId(config.appId);
|
|
64
65
|
instance.logger = new LoggerService(config.appId, instance.guid, config.name || '', config.logLevel);
|
|
@@ -69,7 +70,7 @@ class HotMeshService {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
static guid(): string {
|
|
72
|
-
return
|
|
73
|
+
return guid();
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
async initEngine(config: HotMeshConfig, logger: ILogger): Promise<void> {
|
|
@@ -146,6 +147,10 @@ class HotMeshService {
|
|
|
146
147
|
//activation is a quorum operation
|
|
147
148
|
return await this.quorum?.activate(version, delay);
|
|
148
149
|
}
|
|
150
|
+
async inventory(version: string, delay?: number): Promise<number> {
|
|
151
|
+
//get count of all peers
|
|
152
|
+
return await this.quorum?.inventory(delay);
|
|
153
|
+
}
|
|
149
154
|
|
|
150
155
|
// ************* REPORTER METHODS *************
|
|
151
156
|
async getStats(topic: string, query: JobStatsInput): Promise<StatsResponse> {
|
|
@@ -167,6 +172,11 @@ class HotMeshService {
|
|
|
167
172
|
return await this.engine?.resolveQuery(topic, query);
|
|
168
173
|
}
|
|
169
174
|
|
|
175
|
+
// ****************** `INTERRUPT` ACTIVE JOBS *****************
|
|
176
|
+
async interrupt(topic: string, jobId: string, options: JobInterruptOptions = {}): Promise<string> {
|
|
177
|
+
return await this.engine?.interrupt(topic, jobId, options);
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
// ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
|
|
171
181
|
async scrub(jobId: string) {
|
|
172
182
|
await this.engine?.scrub(jobId);
|
package/services/quorum/index.ts
CHANGED
|
@@ -117,6 +117,8 @@ class QuorumService {
|
|
|
117
117
|
self.engine.processWebHooks()
|
|
118
118
|
} else if (message.type === 'job') {
|
|
119
119
|
self.engine.routeToSubscribers(message.topic, message.job)
|
|
120
|
+
} else if (message.type === 'cron') {
|
|
121
|
+
self.engine.processTimeHooks();
|
|
120
122
|
}
|
|
121
123
|
//if there are any callbacks, call them
|
|
122
124
|
if (self.callbacks.length > 0) {
|
|
@@ -164,6 +166,13 @@ class QuorumService {
|
|
|
164
166
|
|
|
165
167
|
|
|
166
168
|
// ************* COMPILER METHODS *************
|
|
169
|
+
async inventory(delay = QUORUM_DELAY): Promise<number> {
|
|
170
|
+
await this.requestQuorum(delay);
|
|
171
|
+
const q1 = await this.requestQuorum(delay);
|
|
172
|
+
const q2 = await this.requestQuorum(delay);
|
|
173
|
+
const q3 = await this.requestQuorum(delay);
|
|
174
|
+
return Math.round((q1 + q2 + q3) / 3);
|
|
175
|
+
}
|
|
167
176
|
async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
|
|
168
177
|
version = version.toString();
|
|
169
178
|
const config = await this.engine.getVID();
|