@hotmeshio/hotmesh 0.0.48 → 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/README.md +1 -1
- 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/durable/meshos.js +11 -6
- package/build/services/durable/search.d.ts +20 -1
- package/build/services/durable/search.js +73 -25
- package/build/services/durable/worker.js +10 -0
- package/build/services/durable/workflow.d.ts +1 -0
- package/build/services/durable/workflow.js +17 -1
- 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/router/index.d.ts +15 -0
- package/build/services/router/index.js +55 -7
- package/build/services/serializer/index.js +1 -1
- package/build/services/store/clients/redis.js +2 -0
- package/build/services/store/index.d.ts +6 -4
- package/build/services/store/index.js +86 -21
- 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/durable.d.ts +9 -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/durable/meshos.ts +10 -6
- package/services/durable/search.ts +73 -26
- package/services/durable/worker.ts +13 -1
- package/services/durable/workflow.ts +18 -0
- 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/router/index.ts +59 -9
- package/services/serializer/index.ts +1 -1
- package/services/store/clients/redis.ts +2 -0
- package/services/store/index.ts +108 -22
- package/services/task/index.ts +31 -11
- package/services/worker/index.ts +49 -5
- package/types/activity.ts +1 -0
- package/types/durable.ts +11 -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
|
@@ -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) {
|
|
@@ -22,9 +22,24 @@ declare class Router {
|
|
|
22
22
|
};
|
|
23
23
|
currentTimerId: NodeJS.Timeout | null;
|
|
24
24
|
shouldConsume: boolean;
|
|
25
|
+
sleepPromiseResolve: (() => void) | null;
|
|
26
|
+
innerPromiseResolve: (() => void) | null;
|
|
27
|
+
isSleeping: boolean;
|
|
28
|
+
sleepTimout: NodeJS.Timeout | null;
|
|
25
29
|
constructor(config: StreamConfig, stream: StreamService<RedisClient, RedisMulti>, store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
30
|
+
private resetThrottleState;
|
|
26
31
|
createGroup(stream: string, group: string): Promise<void>;
|
|
27
32
|
publishMessage(topic: string, streamData: StreamData | StreamDataResponse, multi?: RedisMulti): Promise<string | RedisMulti>;
|
|
33
|
+
/**
|
|
34
|
+
* An adjustable throttle that will interrupt a sleeping
|
|
35
|
+
* router if the throttle is reduced and the sleep time
|
|
36
|
+
* has elapsed. If the throttle is increased, or if
|
|
37
|
+
* the sleep time has not elapsed, the router will continue
|
|
38
|
+
* to sleep until the new termination point. This
|
|
39
|
+
* allows for dynamic, elastic throttling with smooth
|
|
40
|
+
* acceleration and deceleration.
|
|
41
|
+
*/
|
|
42
|
+
customSleep(): Promise<void>;
|
|
28
43
|
consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<void>;
|
|
29
44
|
isStreamMessage(result: any): boolean;
|
|
30
45
|
consumeOne(stream: string, group: string, id: string, message: string[], callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<void>;
|
|
@@ -12,6 +12,10 @@ class Router {
|
|
|
12
12
|
this.errorCount = 0;
|
|
13
13
|
this.counts = {};
|
|
14
14
|
this.currentTimerId = null;
|
|
15
|
+
this.sleepPromiseResolve = null;
|
|
16
|
+
this.innerPromiseResolve = null;
|
|
17
|
+
this.isSleeping = false;
|
|
18
|
+
this.sleepTimout = null;
|
|
15
19
|
this.appId = config.appId;
|
|
16
20
|
this.guid = config.guid;
|
|
17
21
|
this.role = config.role;
|
|
@@ -21,6 +25,13 @@ class Router {
|
|
|
21
25
|
this.reclaimDelay = config.reclaimDelay || enums_1.HMSH_XCLAIM_DELAY_MS;
|
|
22
26
|
this.reclaimCount = config.reclaimCount || enums_1.HMSH_XCLAIM_COUNT;
|
|
23
27
|
this.logger = logger;
|
|
28
|
+
this.resetThrottleState();
|
|
29
|
+
}
|
|
30
|
+
resetThrottleState() {
|
|
31
|
+
this.sleepPromiseResolve = null;
|
|
32
|
+
this.innerPromiseResolve = null;
|
|
33
|
+
this.isSleeping = false;
|
|
34
|
+
this.sleepTimout = null;
|
|
24
35
|
}
|
|
25
36
|
async createGroup(stream, group) {
|
|
26
37
|
try {
|
|
@@ -36,6 +47,36 @@ class Router {
|
|
|
36
47
|
const stream = this.store.mintKey(key_1.KeyType.STREAMS, { appId: this.store.appId, topic });
|
|
37
48
|
return await this.store.xadd(stream, '*', 'message', JSON.stringify(streamData), multi);
|
|
38
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* An adjustable throttle that will interrupt a sleeping
|
|
52
|
+
* router if the throttle is reduced and the sleep time
|
|
53
|
+
* has elapsed. If the throttle is increased, or if
|
|
54
|
+
* the sleep time has not elapsed, the router will continue
|
|
55
|
+
* to sleep until the new termination point. This
|
|
56
|
+
* allows for dynamic, elastic throttling with smooth
|
|
57
|
+
* acceleration and deceleration.
|
|
58
|
+
*/
|
|
59
|
+
async customSleep() {
|
|
60
|
+
if (this.throttle === 0)
|
|
61
|
+
return;
|
|
62
|
+
if (this.isSleeping)
|
|
63
|
+
return;
|
|
64
|
+
this.isSleeping = true;
|
|
65
|
+
let startTime = Date.now(); //anchor the origin
|
|
66
|
+
await new Promise(async (outerResolve) => {
|
|
67
|
+
this.sleepPromiseResolve = outerResolve;
|
|
68
|
+
let elapsedTime = Date.now() - startTime;
|
|
69
|
+
while (elapsedTime < this.throttle) {
|
|
70
|
+
await new Promise((innerResolve) => {
|
|
71
|
+
this.innerPromiseResolve = innerResolve;
|
|
72
|
+
this.sleepTimout = setTimeout(innerResolve, this.throttle - elapsedTime);
|
|
73
|
+
});
|
|
74
|
+
elapsedTime = Date.now() - startTime;
|
|
75
|
+
}
|
|
76
|
+
this.resetThrottleState();
|
|
77
|
+
outerResolve();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
39
80
|
async consumeMessages(stream, group, consumer, callback) {
|
|
40
81
|
this.logger.info(`stream-consumer-starting`, { group, consumer, stream });
|
|
41
82
|
Router.instances.add(this);
|
|
@@ -43,9 +84,7 @@ class Router {
|
|
|
43
84
|
await this.createGroup(stream, group);
|
|
44
85
|
let lastCheckedPendingMessagesAt = Date.now();
|
|
45
86
|
async function consume() {
|
|
46
|
-
|
|
47
|
-
this.currentTimerId = sleep.timerId;
|
|
48
|
-
await sleep.promise;
|
|
87
|
+
await this.customSleep();
|
|
49
88
|
if (!this.shouldConsume) {
|
|
50
89
|
this.logger.info(`stream-consumer-stopping`, { group, consumer, stream });
|
|
51
90
|
return;
|
|
@@ -229,17 +268,26 @@ class Router {
|
|
|
229
268
|
this.cancelThrottle();
|
|
230
269
|
}
|
|
231
270
|
cancelThrottle() {
|
|
232
|
-
if (this.
|
|
233
|
-
clearTimeout(this.
|
|
234
|
-
this.currentTimerId = undefined;
|
|
271
|
+
if (this.sleepTimout) {
|
|
272
|
+
clearTimeout(this.sleepTimout);
|
|
235
273
|
}
|
|
274
|
+
this.resetThrottleState();
|
|
236
275
|
}
|
|
237
276
|
setThrottle(delayInMillis) {
|
|
238
277
|
if (!Number.isInteger(delayInMillis) || delayInMillis < 0) {
|
|
239
278
|
throw new Error('Throttle must be a non-negative integer');
|
|
240
279
|
}
|
|
280
|
+
const wasDecreased = delayInMillis < this.throttle;
|
|
241
281
|
this.throttle = delayInMillis;
|
|
242
|
-
|
|
282
|
+
// If the throttle was decreased, and we're in the middle of a sleep cycle, adjust immediately
|
|
283
|
+
if (wasDecreased) {
|
|
284
|
+
if (this.sleepTimout) {
|
|
285
|
+
clearTimeout(this.sleepTimout);
|
|
286
|
+
}
|
|
287
|
+
if (this.innerPromiseResolve) {
|
|
288
|
+
this.innerPromiseResolve();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
243
291
|
}
|
|
244
292
|
async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = enums_1.HMSH_XPENDING_COUNT) {
|
|
245
293
|
let pendingMessages = [];
|
|
@@ -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']
|
|
@@ -10,6 +10,7 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
10
10
|
setnx: 'SETNX',
|
|
11
11
|
del: 'DEL',
|
|
12
12
|
expire: 'EXPIRE',
|
|
13
|
+
hscan: 'HSCAN',
|
|
13
14
|
hset: 'HSET',
|
|
14
15
|
hsetnx: 'HSETNX',
|
|
15
16
|
hincrby: 'HINCRBY',
|
|
@@ -29,6 +30,7 @@ class RedisStoreService extends index_1.StoreService {
|
|
|
29
30
|
lpop: 'LPOP',
|
|
30
31
|
rename: 'RENAME',
|
|
31
32
|
rpush: 'RPUSH',
|
|
33
|
+
scan: 'SCAN',
|
|
32
34
|
xack: 'XACK',
|
|
33
35
|
xdel: 'XDEL',
|
|
34
36
|
xlen: 'XLEN',
|
|
@@ -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,5 +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, string[]]>;
|
|
156
|
+
findJobFields(jobId: string, fieldMatchPattern?: string, limit?: number, batchSize?: number, cursor?: string): Promise<[string, StringStringType]>;
|
|
155
157
|
}
|
|
156
158
|
export { StoreService };
|
|
@@ -38,6 +38,7 @@ class StoreService {
|
|
|
38
38
|
del: 'del',
|
|
39
39
|
expire: 'expire',
|
|
40
40
|
hset: 'hset',
|
|
41
|
+
hscan: 'hscan',
|
|
41
42
|
hsetnx: 'hsetnx',
|
|
42
43
|
hincrby: 'hincrby',
|
|
43
44
|
hdel: 'hdel',
|
|
@@ -56,6 +57,7 @@ class StoreService {
|
|
|
56
57
|
lrange: 'lrange',
|
|
57
58
|
rename: 'rename',
|
|
58
59
|
rpush: 'rpush',
|
|
60
|
+
scan: 'scan',
|
|
59
61
|
xack: 'xack',
|
|
60
62
|
xdel: 'xdel',
|
|
61
63
|
};
|
|
@@ -343,15 +345,20 @@ class StoreService {
|
|
|
343
345
|
* when `originJobId` is interrupted/expired, the items in the
|
|
344
346
|
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
345
347
|
*/
|
|
346
|
-
async registerJobDependency(depType, originJobId, topic, jobId, gId, multi) {
|
|
348
|
+
async registerJobDependency(depType, originJobId, topic, jobId, gId, pd = '', multi) {
|
|
347
349
|
const privateMulti = multi || this.getMulti();
|
|
348
350
|
const dependencyParams = {
|
|
349
351
|
appId: this.appId,
|
|
350
352
|
jobId: originJobId,
|
|
351
353
|
};
|
|
352
354
|
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
+
const expireTask = [
|
|
356
|
+
depType,
|
|
357
|
+
topic,
|
|
358
|
+
gId,
|
|
359
|
+
pd,
|
|
360
|
+
jobId,
|
|
361
|
+
].join(key_1.VALSEP);
|
|
355
362
|
privateMulti[this.commands.rpush](depKey, expireTask);
|
|
356
363
|
if (!multi) {
|
|
357
364
|
return await privateMulti.exec();
|
|
@@ -361,12 +368,18 @@ class StoreService {
|
|
|
361
368
|
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
362
369
|
* is interrupted/expired.
|
|
363
370
|
*/
|
|
364
|
-
async registerSignalDependency(jobId, signalKey, multi) {
|
|
371
|
+
async registerSignalDependency(jobId, signalKey, dad, multi) {
|
|
365
372
|
const privateMulti = multi || this.getMulti();
|
|
366
373
|
const dependencyParams = { appId: this.appId, jobId };
|
|
367
374
|
const dependencyKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
|
|
368
|
-
//tasks
|
|
369
|
-
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);
|
|
370
383
|
privateMulti[this.commands.rpush](dependencyKey, delistTask);
|
|
371
384
|
if (!multi) {
|
|
372
385
|
return await privateMulti.exec();
|
|
@@ -684,11 +697,14 @@ class StoreService {
|
|
|
684
697
|
}
|
|
685
698
|
async setHookSignal(hook, multi) {
|
|
686
699
|
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
687
|
-
|
|
700
|
+
//destructure the hook key
|
|
701
|
+
const { topic, resolved, jobId } = hook;
|
|
688
702
|
const signalKey = `${topic}:${resolved}`;
|
|
689
703
|
const payload = { [signalKey]: jobId };
|
|
690
704
|
await (multi || this.redisClient)[this.commands.hset](key, payload);
|
|
691
|
-
|
|
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);
|
|
692
708
|
}
|
|
693
709
|
async getHookSignal(topic, resolved) {
|
|
694
710
|
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
@@ -753,7 +769,7 @@ class StoreService {
|
|
|
753
769
|
const depParams = { appId: this.appId, jobId };
|
|
754
770
|
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
|
|
755
771
|
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
756
|
-
const depKeyContext =
|
|
772
|
+
const depKeyContext = `${key_1.TYPSEP}${context}${key_1.TYPSEP}${depKey}`;
|
|
757
773
|
const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
|
|
758
774
|
await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
|
|
759
775
|
}
|
|
@@ -768,9 +784,16 @@ class StoreService {
|
|
|
768
784
|
* for the given sleep group. Sleep groups are
|
|
769
785
|
* organized into 'n'-second blocks (LISTS))
|
|
770
786
|
*/
|
|
771
|
-
async registerTimeHook(jobId, gId, activityId, type, deletionTime, multi) {
|
|
787
|
+
async registerTimeHook(jobId, gId, activityId, type, deletionTime, dad, multi) {
|
|
772
788
|
const listKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
773
|
-
|
|
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);
|
|
774
797
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
775
798
|
if (multi || len === 1) {
|
|
776
799
|
const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
|
|
@@ -784,9 +807,9 @@ class StoreService {
|
|
|
784
807
|
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
785
808
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
786
809
|
if (timeEvent) {
|
|
787
|
-
//
|
|
788
|
-
|
|
789
|
-
|
|
810
|
+
//deconstruct composite key
|
|
811
|
+
let [type, activityId, gId, _pd, ...jobId] = timeEvent.split(key_1.VALSEP);
|
|
812
|
+
const jid = jobId.join(key_1.VALSEP);
|
|
790
813
|
if (type === 'delist') {
|
|
791
814
|
pType = 'delist';
|
|
792
815
|
}
|
|
@@ -794,9 +817,9 @@ class StoreService {
|
|
|
794
817
|
pType = 'child';
|
|
795
818
|
}
|
|
796
819
|
else if (type === 'expire-child') {
|
|
797
|
-
type = 'expire';
|
|
820
|
+
type = 'expire';
|
|
798
821
|
}
|
|
799
|
-
return [listKey,
|
|
822
|
+
return [listKey, jid, gId, activityId, pType];
|
|
800
823
|
}
|
|
801
824
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
802
825
|
return true;
|
|
@@ -809,14 +832,14 @@ class StoreService {
|
|
|
809
832
|
* do with the work list. (not everything is known in advance,
|
|
810
833
|
* so the ZSET key defines HOW to approach the work in the
|
|
811
834
|
* generic LIST (lists typically contain target job ids)
|
|
812
|
-
* @param {string} listKey -
|
|
835
|
+
* @param {string} listKey - composite key
|
|
813
836
|
*/
|
|
814
837
|
resolveTaskKeyContext(listKey) {
|
|
815
|
-
if (listKey.startsWith(
|
|
816
|
-
return ['interrupt', listKey.split(
|
|
838
|
+
if (listKey.startsWith(`${key_1.TYPSEP}INTERRUPT`)) {
|
|
839
|
+
return ['interrupt', listKey.split(key_1.TYPSEP)[2]];
|
|
817
840
|
}
|
|
818
|
-
else if (listKey.startsWith(
|
|
819
|
-
return ['expire', listKey.split(
|
|
841
|
+
else if (listKey.startsWith(`${key_1.TYPSEP}EXPIRE`)) {
|
|
842
|
+
return ['expire', listKey.split(key_1.TYPSEP)[2]];
|
|
820
843
|
}
|
|
821
844
|
else {
|
|
822
845
|
return ['sleep', listKey];
|
|
@@ -876,5 +899,47 @@ class StoreService {
|
|
|
876
899
|
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
877
900
|
await this.redisClient[this.commands.del](jobKey);
|
|
878
901
|
}
|
|
902
|
+
async findJobs(queryString = '*', limit = 1000, batchSize = 1000) {
|
|
903
|
+
const matchKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId: queryString });
|
|
904
|
+
let cursor = '0';
|
|
905
|
+
let keys;
|
|
906
|
+
const matchingKeys = [];
|
|
907
|
+
do {
|
|
908
|
+
const output = await this.exec('SCAN', cursor, 'MATCH', matchKey, 'COUNT', batchSize.toString());
|
|
909
|
+
if (Array.isArray(output)) {
|
|
910
|
+
[cursor, keys] = output;
|
|
911
|
+
for (let key of [...keys]) {
|
|
912
|
+
matchingKeys.push(key);
|
|
913
|
+
}
|
|
914
|
+
if (matchingKeys.length >= limit) {
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
break;
|
|
920
|
+
}
|
|
921
|
+
} while (cursor !== '0');
|
|
922
|
+
return [cursor, matchingKeys];
|
|
923
|
+
}
|
|
924
|
+
async findJobFields(jobId, fieldMatchPattern = '*', limit = 1000, batchSize = 1000, cursor = '0') {
|
|
925
|
+
let fields = [];
|
|
926
|
+
const matchingFields = {};
|
|
927
|
+
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
928
|
+
let len = 0;
|
|
929
|
+
do {
|
|
930
|
+
const output = await this.exec('HSCAN', jobKey, cursor, 'MATCH', fieldMatchPattern, 'COUNT', batchSize.toString());
|
|
931
|
+
if (Array.isArray(output)) {
|
|
932
|
+
[cursor, fields] = output;
|
|
933
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
934
|
+
len++;
|
|
935
|
+
matchingFields[fields[i]] = fields[i + 1];
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
break;
|
|
940
|
+
}
|
|
941
|
+
} while (cursor !== '0' && len < limit);
|
|
942
|
+
return [cursor, matchingFields];
|
|
943
|
+
}
|
|
879
944
|
}
|
|
880
945
|
exports.StoreService = StoreService;
|
|
@@ -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
|
|
@@ -5,18 +5,20 @@ const enums_1 = require("../../modules/enums");
|
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
6
|
const pipe_1 = require("../pipe");
|
|
7
7
|
const hotmesh_1 = require("../../types/hotmesh");
|
|
8
|
+
const key_1 = require("../../modules/key");
|
|
8
9
|
class TaskService {
|
|
9
10
|
constructor(store, logger) {
|
|
10
11
|
this.cleanupTimeout = null;
|
|
11
12
|
this.isScout = false;
|
|
13
|
+
this.errorCount = 0;
|
|
12
14
|
this.logger = logger;
|
|
13
15
|
this.store = store;
|
|
14
16
|
}
|
|
15
17
|
async processWebHooks(hookEventCallback) {
|
|
16
18
|
const workItemKey = await this.store.getActiveTaskQueue();
|
|
17
19
|
if (workItemKey) {
|
|
18
|
-
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(
|
|
19
|
-
const data = JSON.parse(sdata.join(
|
|
20
|
+
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(key_1.WEBSEP);
|
|
21
|
+
const data = JSON.parse(sdata.join(key_1.WEBSEP));
|
|
20
22
|
const destinationKey = `${sourceKey}:processed`;
|
|
21
23
|
const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
|
|
22
24
|
if (jobId) {
|
|
@@ -41,11 +43,11 @@ class TaskService {
|
|
|
41
43
|
await this.store.registerDependenciesForCleanup(jobId, timeSlot, options);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
|
-
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, multi) {
|
|
46
|
+
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, dad, multi) {
|
|
45
47
|
const fromNow = Date.now() + (inSeconds * 1000);
|
|
46
48
|
const fidelityMS = enums_1.HMSH_FIDELITY_SECONDS * 1000;
|
|
47
49
|
const awakenTimeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
|
|
48
|
-
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
|
|
50
|
+
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, dad, multi);
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Should this engine instance play the role of 'scout' on behalf
|
|
@@ -89,11 +91,13 @@ class TaskService {
|
|
|
89
91
|
await timeEventCallback(target, gId, activityId, type);
|
|
90
92
|
}
|
|
91
93
|
await (0, utils_1.sleepFor)(0);
|
|
94
|
+
this.errorCount = 0;
|
|
92
95
|
this.processTimeHooks(timeEventCallback, listKey);
|
|
93
96
|
}
|
|
94
97
|
else if (workListTask) {
|
|
95
98
|
//a worklist was just emptied; try again immediately
|
|
96
99
|
await (0, utils_1.sleepFor)(0);
|
|
100
|
+
this.errorCount = 0;
|
|
97
101
|
this.processTimeHooks(timeEventCallback);
|
|
98
102
|
}
|
|
99
103
|
else {
|
|
@@ -101,12 +105,18 @@ class TaskService {
|
|
|
101
105
|
let sleep = (0, utils_1.XSleepFor)(enums_1.HMSH_FIDELITY_SECONDS * 1000);
|
|
102
106
|
this.cleanupTimeout = sleep.timerId;
|
|
103
107
|
await sleep.promise;
|
|
108
|
+
this.errorCount = 0;
|
|
104
109
|
this.processTimeHooks(timeEventCallback);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
catch (err) {
|
|
108
|
-
//
|
|
109
|
-
|
|
113
|
+
//most common reasons: deleted job not found; container stopping; test stopping
|
|
114
|
+
//less common: redis/cluster down; retry with fallback (5s max main reassignment)
|
|
115
|
+
this.logger.warn('task-process-timehooks-error', err);
|
|
116
|
+
await (0, utils_1.sleepFor)(1000 * this.errorCount++);
|
|
117
|
+
if (this.errorCount < 5) {
|
|
118
|
+
this.processTimeHooks(timeEventCallback);
|
|
119
|
+
}
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
else {
|
|
@@ -135,10 +145,17 @@ class TaskService {
|
|
|
135
145
|
const jobId = context.metadata.jid;
|
|
136
146
|
const gId = context.metadata.gid;
|
|
137
147
|
const activityId = hookRule.to;
|
|
148
|
+
//composite keys are used to fully describe the task target
|
|
149
|
+
const compositeJobKey = [
|
|
150
|
+
activityId,
|
|
151
|
+
dad,
|
|
152
|
+
gId,
|
|
153
|
+
jobId
|
|
154
|
+
].join(key_1.WEBSEP);
|
|
138
155
|
const hook = {
|
|
139
156
|
topic,
|
|
140
157
|
resolved,
|
|
141
|
-
jobId:
|
|
158
|
+
jobId: compositeJobKey,
|
|
142
159
|
};
|
|
143
160
|
await this.store.setHookSignal(hook, multi);
|
|
144
161
|
return jobId;
|
|
@@ -157,17 +174,17 @@ class TaskService {
|
|
|
157
174
|
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
158
175
|
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
159
176
|
if (!hookSignalId) {
|
|
160
|
-
//messages can be double-processed; not an issue; return undefined
|
|
161
|
-
//users can also provide a bogus topic; not an issue; return undefined
|
|
177
|
+
//messages can be double-processed; not an issue; return `undefined`
|
|
178
|
+
//users can also provide a bogus topic; not an issue; return `undefined`
|
|
162
179
|
return undefined;
|
|
163
180
|
}
|
|
164
|
-
//`aid` is part of
|
|
181
|
+
//`aid` is part of composite key, but the hook `topic` is its public interface;
|
|
165
182
|
// this means that a new version of the graph can be deployed and the
|
|
166
183
|
// topic can be re-mapped to a different activity id. Outside callers
|
|
167
184
|
// can adhere to the unchanged contract (calling the same topic),
|
|
168
|
-
// while the internal system can be updated in real
|
|
169
|
-
const [_aid, dad, gid, ...jid] = hookSignalId.split(
|
|
170
|
-
return [jid.join(
|
|
185
|
+
// while the internal system can be updated in real-time as necessary.
|
|
186
|
+
const [_aid, dad, gid, ...jid] = hookSignalId.split(key_1.WEBSEP);
|
|
187
|
+
return [jid.join(key_1.WEBSEP), hookRule.to, dad, gid];
|
|
171
188
|
}
|
|
172
189
|
else {
|
|
173
190
|
throw new Error('signal-not-found');
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { ILogger } from "../logger";
|
|
2
3
|
import { Router } from "../router";
|
|
3
4
|
import { StoreService } from '../store';
|
|
4
5
|
import { StreamService } from '../stream';
|
|
5
6
|
import { SubService } from '../sub';
|
|
6
7
|
import { HotMeshConfig, HotMeshWorker } from "../../types/hotmesh";
|
|
7
|
-
import { SubscriptionCallback } from "../../types/quorum";
|
|
8
|
+
import { RollCallMessage, SubscriptionCallback } from "../../types/quorum";
|
|
8
9
|
import { RedisClient, RedisMulti } from "../../types/redis";
|
|
10
|
+
import { StreamData, StreamDataResponse } from "../../types/stream";
|
|
9
11
|
declare class WorkerService {
|
|
10
12
|
namespace: string;
|
|
11
13
|
appId: string;
|
|
12
14
|
guid: string;
|
|
13
15
|
topic: string;
|
|
14
16
|
config: HotMeshConfig;
|
|
17
|
+
callback: (streamData: StreamData) => Promise<StreamDataResponse | void>;
|
|
15
18
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
16
19
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
17
20
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
@@ -19,6 +22,7 @@ declare class WorkerService {
|
|
|
19
22
|
logger: ILogger;
|
|
20
23
|
reporting: boolean;
|
|
21
24
|
inited: string;
|
|
25
|
+
rollCallInterval: NodeJS.Timeout;
|
|
22
26
|
static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<WorkerService[]>;
|
|
23
27
|
verifyWorkerFields(worker: HotMeshWorker): void;
|
|
24
28
|
initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
|
|
@@ -26,7 +30,14 @@ declare class WorkerService {
|
|
|
26
30
|
initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
|
|
27
31
|
initRouter(worker: HotMeshWorker, logger: ILogger): Router;
|
|
28
32
|
subscriptionHandler(): SubscriptionCallback;
|
|
29
|
-
|
|
33
|
+
/**
|
|
34
|
+
* A quorum-wide command to broadcaset system details.
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
doRollCall(message: RollCallMessage): Promise<void>;
|
|
38
|
+
cancelRollCall(): void;
|
|
39
|
+
stop(): void;
|
|
40
|
+
sayPong(appId: string, guid: string, originator?: string, details?: boolean, signature?: boolean): Promise<void>;
|
|
30
41
|
throttle(delayInMillis: number): Promise<void>;
|
|
31
42
|
}
|
|
32
43
|
export { WorkerService };
|