@hotmeshio/hotmesh 0.0.49 → 0.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/modules/enums.d.ts +1 -0
- package/build/modules/enums.js +2 -1
- package/build/modules/key.d.ts +5 -1
- package/build/modules/key.js +10 -2
- package/build/package.json +2 -1
- package/build/services/activities/await.js +6 -0
- package/build/services/activities/hook.js +1 -1
- package/build/services/activities/trigger.d.ts +1 -0
- package/build/services/activities/trigger.js +23 -2
- package/build/services/durable/exporter.js +19 -5
- package/build/services/engine/index.d.ts +1 -1
- package/build/services/engine/index.js +12 -3
- package/build/services/exporter/index.js +3 -2
- package/build/services/hotmesh/index.js +4 -0
- package/build/services/quorum/index.d.ts +11 -2
- package/build/services/quorum/index.js +33 -0
- package/build/services/serializer/index.js +1 -1
- package/build/services/store/index.d.ts +5 -5
- package/build/services/store/index.js +43 -22
- package/build/services/task/index.d.ts +2 -1
- package/build/services/task/index.js +30 -13
- package/build/services/worker/index.d.ts +13 -2
- package/build/services/worker/index.js +44 -3
- package/build/types/activity.d.ts +1 -0
- package/build/types/exporter.d.ts +2 -0
- package/build/types/job.d.ts +1 -0
- package/build/types/quorum.d.ts +22 -8
- package/build/types/stream.d.ts +1 -0
- package/modules/enums.ts +1 -0
- package/modules/key.ts +7 -2
- package/package.json +2 -1
- package/services/activities/await.ts +6 -0
- package/services/activities/hook.ts +1 -0
- package/services/activities/trigger.ts +25 -1
- package/services/durable/exporter.ts +18 -7
- package/services/engine/index.ts +13 -5
- package/services/exporter/index.ts +3 -2
- package/services/hotmesh/index.ts +4 -0
- package/services/quorum/index.ts +38 -2
- package/services/serializer/index.ts +1 -1
- package/services/store/index.ts +51 -24
- package/services/task/index.ts +31 -11
- package/services/worker/index.ts +49 -5
- package/types/activity.ts +1 -0
- package/types/exporter.ts +2 -0
- package/types/job.ts +1 -0
- package/types/quorum.ts +28 -13
- package/types/stream.ts +1 -0
package/build/modules/enums.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const HMSH_CODE_DURABLE_MAXED = 597;
|
|
|
15
15
|
export declare const HMSH_CODE_DURABLE_FATAL = 598;
|
|
16
16
|
export declare const HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
17
17
|
export declare const HMSH_STATUS_UNKNOWN = "unknown";
|
|
18
|
+
export declare const HMSH_QUORUM_ROLLCALL_CYCLES = 12;
|
|
18
19
|
export declare const HMSH_QUORUM_DELAY_MS = 250;
|
|
19
20
|
export declare const HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
20
21
|
export declare const HMSH_OTT_WAIT_TIME: number;
|
package/build/modules/enums.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
|
|
3
|
+
exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
|
|
4
4
|
// HOTMESH SYSTEM
|
|
5
5
|
exports.HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL || 'info';
|
|
6
6
|
// STATUS CODES AND MESSAGES
|
|
@@ -20,6 +20,7 @@ exports.HMSH_CODE_DURABLE_FATAL = 598;
|
|
|
20
20
|
exports.HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
21
21
|
exports.HMSH_STATUS_UNKNOWN = 'unknown';
|
|
22
22
|
// QUORUM
|
|
23
|
+
exports.HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
|
|
23
24
|
exports.HMSH_QUORUM_DELAY_MS = 250;
|
|
24
25
|
exports.HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
25
26
|
// ENGINE
|
package/build/modules/key.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
|
|
|
27
27
|
* hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
|
|
28
28
|
*/
|
|
29
29
|
declare const HMNS = "hmsh";
|
|
30
|
+
declare const KEYSEP = ":";
|
|
31
|
+
declare const VALSEP = "::";
|
|
32
|
+
declare const WEBSEP = "::";
|
|
33
|
+
declare const TYPSEP = "::";
|
|
30
34
|
declare class KeyService {
|
|
31
35
|
/**
|
|
32
36
|
* returns a key that can be used to access a value in the key/value store
|
|
@@ -41,4 +45,4 @@ declare class KeyService {
|
|
|
41
45
|
*/
|
|
42
46
|
static mintKey(namespace: string, keyType: KeyType, params: KeyStoreParams): string;
|
|
43
47
|
}
|
|
44
|
-
export { KeyService, KeyType, KeyStoreParams, HMNS };
|
|
48
|
+
export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
|
package/build/modules/key.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HMNS = exports.KeyType = exports.KeyService = void 0;
|
|
3
|
+
exports.VALSEP = exports.WEBSEP = exports.TYPSEP = exports.KEYSEP = exports.HMNS = exports.KeyType = exports.KeyService = void 0;
|
|
4
4
|
const hotmesh_1 = require("../types/hotmesh");
|
|
5
5
|
Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () { return hotmesh_1.KeyType; } });
|
|
6
6
|
/**
|
|
@@ -30,8 +30,16 @@ Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () {
|
|
|
30
30
|
* hmsh:<appid>:sym:keys:<activityid|$subscribes> -> {hash} list of symbols based upon schema enums (initially) and adaptively optimized (later) during runtime; if '$subscribes' is used as the activityid, it is a top-level `job` symbol set (for job keys)
|
|
31
31
|
* hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
|
|
32
32
|
*/
|
|
33
|
-
const HMNS = "hmsh";
|
|
33
|
+
const HMNS = "hmsh";
|
|
34
34
|
exports.HMNS = HMNS;
|
|
35
|
+
const KEYSEP = ':'; //default delimiter for keys
|
|
36
|
+
exports.KEYSEP = KEYSEP;
|
|
37
|
+
const VALSEP = '::'; //default delimiter for vals
|
|
38
|
+
exports.VALSEP = VALSEP;
|
|
39
|
+
const WEBSEP = '::'; //default delimiter for webhook vals
|
|
40
|
+
exports.WEBSEP = WEBSEP;
|
|
41
|
+
const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
|
|
42
|
+
exports.TYPSEP = TYPSEP;
|
|
35
43
|
class KeyService {
|
|
36
44
|
/**
|
|
37
45
|
* returns a key that can be used to access a value in the key/value store
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
29
29
|
"test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
30
30
|
"test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
|
+
"test:await": "NODE_ENV=test jest ./tests/functional/awaiter/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
32
|
"test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
32
33
|
"test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
33
34
|
"test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -78,6 +78,12 @@ class Await extends activity_1.Activity {
|
|
|
78
78
|
type: stream_1.StreamDataType.AWAIT,
|
|
79
79
|
data: this.context.data
|
|
80
80
|
};
|
|
81
|
+
if (this.config.await !== true) {
|
|
82
|
+
const doAwait = pipe_1.Pipe.resolve(this.config.await, this.context);
|
|
83
|
+
if (doAwait === false) {
|
|
84
|
+
streamData.metadata.await = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
81
87
|
if (this.config.retry) {
|
|
82
88
|
streamData.policies = {
|
|
83
89
|
retry: this.config.retry
|
|
@@ -107,7 +107,7 @@ class Hook extends activity_1.Activity {
|
|
|
107
107
|
}
|
|
108
108
|
else if (this.config.sleep) {
|
|
109
109
|
const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
|
|
110
|
-
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration);
|
|
110
|
+
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '');
|
|
111
111
|
return this.context.metadata.jid;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -8,6 +8,7 @@ declare class Trigger extends Activity {
|
|
|
8
8
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
9
9
|
process(): Promise<string>;
|
|
10
10
|
setStatus(amount: number): Promise<void>;
|
|
11
|
+
execAdjacentParent(): Promise<void>;
|
|
11
12
|
createInputContext(): Partial<JobState>;
|
|
12
13
|
getState(): Promise<void>;
|
|
13
14
|
bindJobMetadataPaths(): string[];
|
|
@@ -31,6 +31,9 @@ class Trigger extends activity_1.Activity {
|
|
|
31
31
|
await this.setStats(multi);
|
|
32
32
|
await this.registerJobDependency(multi);
|
|
33
33
|
await multi.exec();
|
|
34
|
+
//if the parent (spawner) chose not to await,
|
|
35
|
+
// emit the job_id as the data payload { job_id }
|
|
36
|
+
this.execAdjacentParent();
|
|
34
37
|
telemetry.mapActivityAttributes();
|
|
35
38
|
const jobStatus = Number(this.context.metadata.js);
|
|
36
39
|
telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
|
|
@@ -61,6 +64,11 @@ class Trigger extends activity_1.Activity {
|
|
|
61
64
|
async setStatus(amount) {
|
|
62
65
|
this.context.metadata.js = amount;
|
|
63
66
|
}
|
|
67
|
+
async execAdjacentParent() {
|
|
68
|
+
if (this.context.metadata.px) {
|
|
69
|
+
await this.engine.execAdjacentParent(this.context, { metadata: this.context.metadata, data: { job_id: this.context.metadata.jid } });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
64
72
|
createInputContext() {
|
|
65
73
|
const input = {
|
|
66
74
|
[this.metadata.aid]: {
|
|
@@ -95,6 +103,7 @@ class Trigger extends activity_1.Activity {
|
|
|
95
103
|
pg: this.context.metadata.pg,
|
|
96
104
|
pd: this.context.metadata.pd,
|
|
97
105
|
pa: this.context.metadata.pa,
|
|
106
|
+
px: this.context.metadata.px,
|
|
98
107
|
app: id,
|
|
99
108
|
vrs: version,
|
|
100
109
|
tpc: this.config.subscribes,
|
|
@@ -165,10 +174,22 @@ class Trigger extends activity_1.Activity {
|
|
|
165
174
|
}
|
|
166
175
|
if (resolvedDepKey) {
|
|
167
176
|
const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
|
|
168
|
-
|
|
177
|
+
let type;
|
|
178
|
+
if (isParentOrigin) {
|
|
179
|
+
if (this.context.metadata.px) {
|
|
180
|
+
type = 'child';
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
type = 'expire-child';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
type = 'expire';
|
|
188
|
+
}
|
|
189
|
+
await this.store.registerJobDependency(type, resolvedDepKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
|
|
169
190
|
}
|
|
170
191
|
if (resolvedAdjKey && resolvedAdjKey !== resolvedDepKey) {
|
|
171
|
-
await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, multi);
|
|
192
|
+
await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
|
|
172
193
|
}
|
|
173
194
|
}
|
|
174
195
|
async setStats(multi) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ExporterService = void 0;
|
|
4
4
|
const serializer_1 = require("../serializer");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
|
+
const key_1 = require("../../modules/key");
|
|
6
7
|
/**
|
|
7
8
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
8
9
|
* Splits, Inflates, and Sorts the job data for use in durable contexts
|
|
@@ -94,13 +95,27 @@ class ExporterService {
|
|
|
94
95
|
const activityName = item[1].split('/')[0];
|
|
95
96
|
const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
|
|
96
97
|
const timestamp = item[2];
|
|
97
|
-
|
|
98
|
+
let event = {
|
|
98
99
|
activity: activityName,
|
|
99
100
|
duplex: duplex,
|
|
100
101
|
dimension: dimensions,
|
|
101
102
|
timestamp,
|
|
103
|
+
created: timestamp,
|
|
104
|
+
updated: timestamp,
|
|
102
105
|
};
|
|
103
|
-
timeline.
|
|
106
|
+
const prior = timeline[timeline.length - 1];
|
|
107
|
+
if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
|
|
108
|
+
if (event.duplex === 'exit') {
|
|
109
|
+
prior.updated = event.timestamp;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
prior.created = event.timestamp;
|
|
113
|
+
}
|
|
114
|
+
event = prior;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
timeline.push(event);
|
|
118
|
+
}
|
|
104
119
|
if (this.isMainEntry(item[1])) {
|
|
105
120
|
event.actions = [];
|
|
106
121
|
this.interleaveActions(actions.main, event.actions);
|
|
@@ -163,12 +178,11 @@ class ExporterService {
|
|
|
163
178
|
* @returns - the organized dependency data
|
|
164
179
|
*/
|
|
165
180
|
inflateDependencyData(data, actions) {
|
|
166
|
-
//console.log('dependency data>', data);
|
|
167
181
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
168
182
|
const flowReg = /-(\d+)$/;
|
|
169
183
|
return data.map((dependency, index) => {
|
|
170
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
171
|
-
const jobId = jid.join(
|
|
184
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
|
|
185
|
+
const jobId = jid.join(key_1.VALSEP);
|
|
172
186
|
const match = jobId.match(hookReg);
|
|
173
187
|
let prefix;
|
|
174
188
|
let type;
|
|
@@ -65,7 +65,7 @@ declare class EngineService {
|
|
|
65
65
|
resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
|
|
66
66
|
processStreamMessage(streamData: StreamDataResponse): Promise<void>;
|
|
67
67
|
execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<string>;
|
|
68
|
-
hasParentJob(context: JobState): boolean;
|
|
68
|
+
hasParentJob(context: JobState, checkSevered?: boolean): boolean;
|
|
69
69
|
resolveError(metadata: JobMetadata): StreamError | undefined;
|
|
70
70
|
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
|
|
71
71
|
scrub(jobId: string): Promise<void>;
|
|
@@ -259,6 +259,7 @@ class EngineService {
|
|
|
259
259
|
pg: streamData.metadata.gid,
|
|
260
260
|
pd: streamData.metadata.dad,
|
|
261
261
|
pa: streamData.metadata.aid,
|
|
262
|
+
px: streamData.metadata.await === false,
|
|
262
263
|
trc: streamData.metadata.trc,
|
|
263
264
|
spn: streamData.metadata.spn,
|
|
264
265
|
};
|
|
@@ -316,7 +317,10 @@ class EngineService {
|
|
|
316
317
|
return (await this.router?.publishMessage(null, streamData));
|
|
317
318
|
}
|
|
318
319
|
}
|
|
319
|
-
hasParentJob(context) {
|
|
320
|
+
hasParentJob(context, checkSevered = false) {
|
|
321
|
+
if (checkSevered) {
|
|
322
|
+
return Boolean(context.metadata.pj && context.metadata.pa && !context.metadata.px);
|
|
323
|
+
}
|
|
320
324
|
return Boolean(context.metadata.pj && context.metadata.pa);
|
|
321
325
|
}
|
|
322
326
|
resolveError(metadata) {
|
|
@@ -385,7 +389,12 @@ class EngineService {
|
|
|
385
389
|
const workItems = await reporter.getWorkItems(resolvedQuery, queryFacets);
|
|
386
390
|
if (workItems.length) {
|
|
387
391
|
const taskService = new task_1.TaskService(this.store, this.logger);
|
|
388
|
-
await taskService.enqueueWorkItems(workItems.map(workItem =>
|
|
392
|
+
await taskService.enqueueWorkItems(workItems.map(workItem => [
|
|
393
|
+
hookTopic,
|
|
394
|
+
workItem,
|
|
395
|
+
keyResolver.scrub || false,
|
|
396
|
+
JSON.stringify(data)
|
|
397
|
+
].join(key_1.VALSEP)));
|
|
389
398
|
this.store.publish(key_1.KeyType.QUORUM, { type: 'work', originator: this.guid }, this.appId);
|
|
390
399
|
}
|
|
391
400
|
return workItems;
|
|
@@ -504,7 +513,7 @@ class EngineService {
|
|
|
504
513
|
// ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
|
|
505
514
|
async runJobCompletionTasks(context, options = {}) {
|
|
506
515
|
//'emit' indicates the job is still active
|
|
507
|
-
const isAwait = this.hasParentJob(context);
|
|
516
|
+
const isAwait = this.hasParentJob(context, true);
|
|
508
517
|
const isOneTimeSub = this.hasOneTimeSubscription(context);
|
|
509
518
|
const topic = await this.getPublishesTopic(context);
|
|
510
519
|
let msgId;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ExporterService = void 0;
|
|
4
4
|
const serializer_1 = require("../serializer");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
|
+
const key_1 = require("../../modules/key");
|
|
6
7
|
/**
|
|
7
8
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
8
9
|
* Expands process data and includes dependency list
|
|
@@ -87,8 +88,8 @@ class ExporterService {
|
|
|
87
88
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
88
89
|
const flowReg = /-(\d+)$/;
|
|
89
90
|
return data.map((dependency, index) => {
|
|
90
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
91
|
-
const jobId = jid.join(
|
|
91
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
|
|
92
|
+
const jobId = jid.join(key_1.VALSEP);
|
|
92
93
|
const match = jobId.match(hookReg);
|
|
93
94
|
let prefix;
|
|
94
95
|
let type;
|
|
@@ -167,6 +167,10 @@ class HotMeshService {
|
|
|
167
167
|
}
|
|
168
168
|
stop() {
|
|
169
169
|
this.engine?.taskService.cancelCleanup();
|
|
170
|
+
this.quorum?.stop();
|
|
171
|
+
this.workers?.forEach((worker) => {
|
|
172
|
+
worker.stop();
|
|
173
|
+
});
|
|
170
174
|
}
|
|
171
175
|
async compress(terms) {
|
|
172
176
|
return await this.engine?.compress(terms);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { EngineService } from '../engine';
|
|
2
3
|
import { ILogger } from '../logger';
|
|
3
4
|
import { StoreService } from '../store';
|
|
4
5
|
import { SubService } from '../sub';
|
|
5
6
|
import { CacheMode } from '../../types/cache';
|
|
6
7
|
import { HotMeshConfig } from '../../types/hotmesh';
|
|
7
|
-
import { QuorumMessageCallback, QuorumProfile,
|
|
8
|
+
import { QuorumMessage, QuorumMessageCallback, QuorumProfile, RollCallMessage, SubscriptionCallback } from '../../types/quorum';
|
|
8
9
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
9
10
|
declare class QuorumService {
|
|
10
11
|
namespace: string;
|
|
@@ -19,6 +20,7 @@ declare class QuorumService {
|
|
|
19
20
|
untilVersion: string | null;
|
|
20
21
|
quorum: number | null;
|
|
21
22
|
callbacks: QuorumMessageCallback[];
|
|
23
|
+
rollCallInterval: NodeJS.Timeout;
|
|
22
24
|
static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, engine: EngineService, logger: ILogger): Promise<QuorumService>;
|
|
23
25
|
verifyQuorumFields(config: HotMeshConfig): void;
|
|
24
26
|
initStoreChannel(store: RedisClient): Promise<void>;
|
|
@@ -26,7 +28,14 @@ declare class QuorumService {
|
|
|
26
28
|
subscriptionHandler(): SubscriptionCallback;
|
|
27
29
|
sayPong(appId: string, guid: string, originator: string, details?: boolean): Promise<void>;
|
|
28
30
|
requestQuorum(delay?: number, details?: boolean): Promise<number>;
|
|
29
|
-
|
|
31
|
+
/**
|
|
32
|
+
* A quorum-wide command to broadcaset system details.
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
doRollCall(message: RollCallMessage): Promise<void>;
|
|
36
|
+
cancelRollCall(): void;
|
|
37
|
+
stop(): void;
|
|
38
|
+
pub(quorumMessage: QuorumMessage): Promise<boolean>;
|
|
30
39
|
sub(callback: QuorumMessageCallback): Promise<void>;
|
|
31
40
|
unsub(callback: QuorumMessageCallback): Promise<void>;
|
|
32
41
|
rollCall(delay?: number): Promise<QuorumProfile[]>;
|
|
@@ -90,6 +90,9 @@ class QuorumService {
|
|
|
90
90
|
else if (message.type === 'cron') {
|
|
91
91
|
self.engine.processTimeHooks();
|
|
92
92
|
}
|
|
93
|
+
else if (message.type === 'rollcall') {
|
|
94
|
+
self.doRollCall(message);
|
|
95
|
+
}
|
|
93
96
|
//if there are any callbacks, call them
|
|
94
97
|
if (self.callbacks.length > 0) {
|
|
95
98
|
self.callbacks.forEach(cb => cb(topic, message));
|
|
@@ -132,6 +135,36 @@ class QuorumService {
|
|
|
132
135
|
await (0, utils_1.sleepFor)(delay);
|
|
133
136
|
return quorum;
|
|
134
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* A quorum-wide command to broadcaset system details.
|
|
140
|
+
*
|
|
141
|
+
*/
|
|
142
|
+
async doRollCall(message) {
|
|
143
|
+
let iteration = 0;
|
|
144
|
+
let max = !isNaN(message.max) ? message.max : enums_1.HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
145
|
+
if (this.rollCallInterval)
|
|
146
|
+
clearTimeout(this.rollCallInterval);
|
|
147
|
+
const base = (message.interval / 2);
|
|
148
|
+
const amount = base + Math.ceil(Math.random() * base);
|
|
149
|
+
do {
|
|
150
|
+
await (0, utils_1.sleepFor)(Math.ceil(Math.random() * 1000));
|
|
151
|
+
await this.sayPong(this.appId, this.guid, null, true);
|
|
152
|
+
if (!message.interval)
|
|
153
|
+
return;
|
|
154
|
+
const { promise, timerId } = (0, utils_1.XSleepFor)(amount * 1000);
|
|
155
|
+
this.rollCallInterval = timerId;
|
|
156
|
+
await promise;
|
|
157
|
+
} while (this.rollCallInterval && iteration++ < max - 1);
|
|
158
|
+
}
|
|
159
|
+
cancelRollCall() {
|
|
160
|
+
if (this.rollCallInterval) {
|
|
161
|
+
clearTimeout(this.rollCallInterval);
|
|
162
|
+
delete this.rollCallInterval;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
stop() {
|
|
166
|
+
this.cancelRollCall();
|
|
167
|
+
}
|
|
135
168
|
// ************* PUB/SUB METHODS *************
|
|
136
169
|
//publish a message to the quorum
|
|
137
170
|
async pub(quorumMessage) {
|
|
@@ -12,7 +12,7 @@ exports.MDATA_SYMBOLS = {
|
|
|
12
12
|
KEYS: ['au', 'err', 'l2s']
|
|
13
13
|
},
|
|
14
14
|
JOB: {
|
|
15
|
-
KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
15
|
+
KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'px', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
16
16
|
},
|
|
17
17
|
JOB_UPDATE: {
|
|
18
18
|
KEYS: ['ju', 'err']
|
|
@@ -69,12 +69,12 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
69
69
|
* when `originJobId` is interrupted/expired, the items in the
|
|
70
70
|
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
71
71
|
*/
|
|
72
|
-
registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, multi?: U): Promise<any>;
|
|
72
|
+
registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, pd?: string, multi?: U): Promise<any>;
|
|
73
73
|
/**
|
|
74
74
|
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
75
75
|
* is interrupted/expired.
|
|
76
76
|
*/
|
|
77
|
-
registerSignalDependency(jobId: string, signalKey: string, multi?: U): Promise<any>;
|
|
77
|
+
registerSignalDependency(jobId: string, signalKey: string, dad: string, multi?: U): Promise<any>;
|
|
78
78
|
setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, multi?: U): Promise<any>;
|
|
79
79
|
hGetAllResult(result: any): any;
|
|
80
80
|
getJobStats(jobKeys: string[]): Promise<JobStatsRange>;
|
|
@@ -133,7 +133,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
133
133
|
* for the given sleep group. Sleep groups are
|
|
134
134
|
* organized into 'n'-second blocks (LISTS))
|
|
135
135
|
*/
|
|
136
|
-
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, multi?: U): Promise<void>;
|
|
136
|
+
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, dad: string, multi?: U): Promise<void>;
|
|
137
137
|
getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean>;
|
|
138
138
|
/**
|
|
139
139
|
* when processing time jobs, the target LIST ID returned
|
|
@@ -141,7 +141,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
141
141
|
* do with the work list. (not everything is known in advance,
|
|
142
142
|
* so the ZSET key defines HOW to approach the work in the
|
|
143
143
|
* generic LIST (lists typically contain target job ids)
|
|
144
|
-
* @param {string} listKey -
|
|
144
|
+
* @param {string} listKey - composite key
|
|
145
145
|
*/
|
|
146
146
|
resolveTaskKeyContext(listKey: string): [WorkListTaskType, string];
|
|
147
147
|
/**
|
|
@@ -152,7 +152,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
152
152
|
*/
|
|
153
153
|
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<void>;
|
|
154
154
|
scrub(jobId: string): Promise<void>;
|
|
155
|
-
findJobs(queryString?: string, limit?: number, batchSize?: number): Promise<string[]>;
|
|
155
|
+
findJobs(queryString?: string, limit?: number, batchSize?: number): Promise<[string, string[]]>;
|
|
156
156
|
findJobFields(jobId: string, fieldMatchPattern?: string, limit?: number, batchSize?: number, cursor?: string): Promise<[string, StringStringType]>;
|
|
157
157
|
}
|
|
158
158
|
export { StoreService };
|
|
@@ -345,15 +345,20 @@ class StoreService {
|
|
|
345
345
|
* when `originJobId` is interrupted/expired, the items in the
|
|
346
346
|
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
347
347
|
*/
|
|
348
|
-
async registerJobDependency(depType, originJobId, topic, jobId, gId, multi) {
|
|
348
|
+
async registerJobDependency(depType, originJobId, topic, jobId, gId, pd = '', multi) {
|
|
349
349
|
const privateMulti = multi || this.getMulti();
|
|
350
350
|
const dependencyParams = {
|
|
351
351
|
appId: this.appId,
|
|
352
352
|
jobId: originJobId,
|
|
353
353
|
};
|
|
354
354
|
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
const expireTask = [
|
|
356
|
+
depType,
|
|
357
|
+
topic,
|
|
358
|
+
gId,
|
|
359
|
+
pd,
|
|
360
|
+
jobId,
|
|
361
|
+
].join(key_1.VALSEP);
|
|
357
362
|
privateMulti[this.commands.rpush](depKey, expireTask);
|
|
358
363
|
if (!multi) {
|
|
359
364
|
return await privateMulti.exec();
|
|
@@ -363,12 +368,18 @@ class StoreService {
|
|
|
363
368
|
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
364
369
|
* is interrupted/expired.
|
|
365
370
|
*/
|
|
366
|
-
async registerSignalDependency(jobId, signalKey, multi) {
|
|
371
|
+
async registerSignalDependency(jobId, signalKey, dad, multi) {
|
|
367
372
|
const privateMulti = multi || this.getMulti();
|
|
368
373
|
const dependencyParams = { appId: this.appId, jobId };
|
|
369
374
|
const dependencyKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
|
|
370
|
-
//tasks
|
|
371
|
-
const delistTask =
|
|
375
|
+
//persiste dependency tasks as multi-segment composite keys
|
|
376
|
+
const delistTask = [
|
|
377
|
+
'delist',
|
|
378
|
+
'signal',
|
|
379
|
+
jobId,
|
|
380
|
+
dad,
|
|
381
|
+
signalKey
|
|
382
|
+
].join(key_1.VALSEP);
|
|
372
383
|
privateMulti[this.commands.rpush](dependencyKey, delistTask);
|
|
373
384
|
if (!multi) {
|
|
374
385
|
return await privateMulti.exec();
|
|
@@ -686,11 +697,14 @@ class StoreService {
|
|
|
686
697
|
}
|
|
687
698
|
async setHookSignal(hook, multi) {
|
|
688
699
|
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
689
|
-
|
|
700
|
+
//destructure the hook key
|
|
701
|
+
const { topic, resolved, jobId } = hook;
|
|
690
702
|
const signalKey = `${topic}:${resolved}`;
|
|
691
703
|
const payload = { [signalKey]: jobId };
|
|
692
704
|
await (multi || this.redisClient)[this.commands.hset](key, payload);
|
|
693
|
-
|
|
705
|
+
//jobId needs even more destructuring
|
|
706
|
+
const [_aid, dad, _gid, jid] = jobId.split(key_1.VALSEP);
|
|
707
|
+
return await this.registerSignalDependency(jid, signalKey, dad, multi);
|
|
694
708
|
}
|
|
695
709
|
async getHookSignal(topic, resolved) {
|
|
696
710
|
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
@@ -755,7 +769,7 @@ class StoreService {
|
|
|
755
769
|
const depParams = { appId: this.appId, jobId };
|
|
756
770
|
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
|
|
757
771
|
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
758
|
-
const depKeyContext =
|
|
772
|
+
const depKeyContext = `${key_1.TYPSEP}${context}${key_1.TYPSEP}${depKey}`;
|
|
759
773
|
const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
|
|
760
774
|
await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
|
|
761
775
|
}
|
|
@@ -770,9 +784,16 @@ class StoreService {
|
|
|
770
784
|
* for the given sleep group. Sleep groups are
|
|
771
785
|
* organized into 'n'-second blocks (LISTS))
|
|
772
786
|
*/
|
|
773
|
-
async registerTimeHook(jobId, gId, activityId, type, deletionTime, multi) {
|
|
787
|
+
async registerTimeHook(jobId, gId, activityId, type, deletionTime, dad, multi) {
|
|
774
788
|
const listKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
775
|
-
|
|
789
|
+
//construct the composite key (the key has enough info to signal the hook)
|
|
790
|
+
const timeEvent = [
|
|
791
|
+
type,
|
|
792
|
+
activityId,
|
|
793
|
+
gId,
|
|
794
|
+
dad,
|
|
795
|
+
jobId
|
|
796
|
+
].join(key_1.VALSEP);
|
|
776
797
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
777
798
|
if (multi || len === 1) {
|
|
778
799
|
const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
|
|
@@ -786,9 +807,9 @@ class StoreService {
|
|
|
786
807
|
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
787
808
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
788
809
|
if (timeEvent) {
|
|
789
|
-
//
|
|
790
|
-
|
|
791
|
-
|
|
810
|
+
//deconstruct composite key
|
|
811
|
+
let [type, activityId, gId, _pd, ...jobId] = timeEvent.split(key_1.VALSEP);
|
|
812
|
+
const jid = jobId.join(key_1.VALSEP);
|
|
792
813
|
if (type === 'delist') {
|
|
793
814
|
pType = 'delist';
|
|
794
815
|
}
|
|
@@ -796,9 +817,9 @@ class StoreService {
|
|
|
796
817
|
pType = 'child';
|
|
797
818
|
}
|
|
798
819
|
else if (type === 'expire-child') {
|
|
799
|
-
type = 'expire';
|
|
820
|
+
type = 'expire';
|
|
800
821
|
}
|
|
801
|
-
return [listKey,
|
|
822
|
+
return [listKey, jid, gId, activityId, pType];
|
|
802
823
|
}
|
|
803
824
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
804
825
|
return true;
|
|
@@ -811,14 +832,14 @@ class StoreService {
|
|
|
811
832
|
* do with the work list. (not everything is known in advance,
|
|
812
833
|
* so the ZSET key defines HOW to approach the work in the
|
|
813
834
|
* generic LIST (lists typically contain target job ids)
|
|
814
|
-
* @param {string} listKey -
|
|
835
|
+
* @param {string} listKey - composite key
|
|
815
836
|
*/
|
|
816
837
|
resolveTaskKeyContext(listKey) {
|
|
817
|
-
if (listKey.startsWith(
|
|
818
|
-
return ['interrupt', listKey.split(
|
|
838
|
+
if (listKey.startsWith(`${key_1.TYPSEP}INTERRUPT`)) {
|
|
839
|
+
return ['interrupt', listKey.split(key_1.TYPSEP)[2]];
|
|
819
840
|
}
|
|
820
|
-
else if (listKey.startsWith(
|
|
821
|
-
return ['expire', listKey.split(
|
|
841
|
+
else if (listKey.startsWith(`${key_1.TYPSEP}EXPIRE`)) {
|
|
842
|
+
return ['expire', listKey.split(key_1.TYPSEP)[2]];
|
|
822
843
|
}
|
|
823
844
|
else {
|
|
824
845
|
return ['sleep', listKey];
|
|
@@ -898,7 +919,7 @@ class StoreService {
|
|
|
898
919
|
break;
|
|
899
920
|
}
|
|
900
921
|
} while (cursor !== '0');
|
|
901
|
-
return matchingKeys;
|
|
922
|
+
return [cursor, matchingKeys];
|
|
902
923
|
}
|
|
903
924
|
async findJobFields(jobId, fieldMatchPattern = '*', limit = 1000, batchSize = 1000, cursor = '0') {
|
|
904
925
|
let fields = [];
|
|
@@ -10,11 +10,12 @@ declare class TaskService {
|
|
|
10
10
|
logger: ILogger;
|
|
11
11
|
cleanupTimeout: NodeJS.Timeout | null;
|
|
12
12
|
isScout: boolean;
|
|
13
|
+
errorCount: number;
|
|
13
14
|
constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
14
15
|
processWebHooks(hookEventCallback: HookInterface): Promise<void>;
|
|
15
16
|
enqueueWorkItems(keys: string[]): Promise<void>;
|
|
16
17
|
registerJobForCleanup(jobId: string, inSeconds: number, options: JobCompletionOptions): Promise<void>;
|
|
17
|
-
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, inSeconds
|
|
18
|
+
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, inSeconds: number, dad: string, multi?: RedisMulti): Promise<void>;
|
|
18
19
|
/**
|
|
19
20
|
* Should this engine instance play the role of 'scout' on behalf
|
|
20
21
|
* of the entire quorum? The scout role is responsible for processing
|