@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/services/quorum/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HMSH_ACTIVATION_MAX_RETRY,
|
|
3
|
-
HMSH_QUORUM_DELAY_MS
|
|
3
|
+
HMSH_QUORUM_DELAY_MS,
|
|
4
|
+
HMSH_QUORUM_ROLLCALL_CYCLES} from '../../modules/enums';
|
|
4
5
|
import {
|
|
6
|
+
XSleepFor,
|
|
5
7
|
formatISODate,
|
|
6
8
|
getSystemHealth,
|
|
7
9
|
identifyRedisType,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
QuorumMessage,
|
|
23
25
|
QuorumMessageCallback,
|
|
24
26
|
QuorumProfile,
|
|
27
|
+
RollCallMessage,
|
|
25
28
|
SubscriptionCallback,
|
|
26
29
|
ThrottleMessage
|
|
27
30
|
} from '../../types/quorum';
|
|
@@ -41,6 +44,7 @@ class QuorumService {
|
|
|
41
44
|
untilVersion: string | null = null;
|
|
42
45
|
quorum: number | null = null;
|
|
43
46
|
callbacks: QuorumMessageCallback[] = [];
|
|
47
|
+
rollCallInterval: NodeJS.Timeout;
|
|
44
48
|
|
|
45
49
|
static async init(
|
|
46
50
|
namespace: string,
|
|
@@ -136,6 +140,8 @@ class QuorumService {
|
|
|
136
140
|
self.engine.routeToSubscribers(message.topic, message.job)
|
|
137
141
|
} else if (message.type === 'cron') {
|
|
138
142
|
self.engine.processTimeHooks();
|
|
143
|
+
} else if (message.type === 'rollcall') {
|
|
144
|
+
self.doRollCall(message);
|
|
139
145
|
}
|
|
140
146
|
//if there are any callbacks, call them
|
|
141
147
|
if (self.callbacks.length > 0) {
|
|
@@ -193,10 +199,40 @@ class QuorumService {
|
|
|
193
199
|
return quorum;
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
/**
|
|
203
|
+
* A quorum-wide command to broadcaset system details.
|
|
204
|
+
*
|
|
205
|
+
*/
|
|
206
|
+
async doRollCall(message: RollCallMessage) {
|
|
207
|
+
let iteration = 0;
|
|
208
|
+
let max = !isNaN(message.max) ? message.max : HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
209
|
+
if (this.rollCallInterval) clearTimeout(this.rollCallInterval);
|
|
210
|
+
const base = (message.interval / 2);
|
|
211
|
+
const amount = base + Math.ceil(Math.random() * base);
|
|
212
|
+
do {
|
|
213
|
+
await sleepFor(Math.ceil(Math.random() * 1000));
|
|
214
|
+
await this.sayPong(this.appId, this.guid, null, true);
|
|
215
|
+
if (!message.interval) return;
|
|
216
|
+
const { promise, timerId } = XSleepFor(amount * 1000);
|
|
217
|
+
this.rollCallInterval = timerId;
|
|
218
|
+
await promise;
|
|
219
|
+
} while (this.rollCallInterval && iteration++ < max - 1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
cancelRollCall() {
|
|
223
|
+
if (this.rollCallInterval) {
|
|
224
|
+
clearTimeout(this.rollCallInterval);
|
|
225
|
+
delete this.rollCallInterval;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
stop() {
|
|
230
|
+
this.cancelRollCall();
|
|
231
|
+
}
|
|
196
232
|
|
|
197
233
|
// ************* PUB/SUB METHODS *************
|
|
198
234
|
//publish a message to the quorum
|
|
199
|
-
async pub(quorumMessage:
|
|
235
|
+
async pub(quorumMessage: QuorumMessage) {
|
|
200
236
|
return await this.store.publish(KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
|
|
201
237
|
}
|
|
202
238
|
//subscribe user to quorum messages
|
|
@@ -19,7 +19,7 @@ export const MDATA_SYMBOLS = {
|
|
|
19
19
|
KEYS: ['au', 'err', 'l2s']
|
|
20
20
|
},
|
|
21
21
|
JOB: {
|
|
22
|
-
KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
22
|
+
KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'px', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
23
23
|
},
|
|
24
24
|
JOB_UPDATE: {
|
|
25
25
|
KEYS: ['ju', 'err']
|
package/services/store/index.ts
CHANGED
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
KeyService,
|
|
3
3
|
KeyStoreParams,
|
|
4
4
|
KeyType,
|
|
5
|
-
HMNS
|
|
5
|
+
HMNS,
|
|
6
|
+
VALSEP,
|
|
7
|
+
TYPSEP} from '../../modules/key';
|
|
6
8
|
import { ILogger } from '../logger';
|
|
7
9
|
import { MDATA_SYMBOLS, SerializerService as Serializer } from '../serializer';
|
|
8
10
|
import { Cache } from './cache';
|
|
@@ -428,7 +430,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
428
430
|
* when `originJobId` is interrupted/expired, the items in the
|
|
429
431
|
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
430
432
|
*/
|
|
431
|
-
async registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
433
|
+
async registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, pd = '', multi? : U): Promise<any> {
|
|
432
434
|
const privateMulti = multi || this.getMulti();
|
|
433
435
|
const dependencyParams = {
|
|
434
436
|
appId: this.appId,
|
|
@@ -438,8 +440,13 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
438
440
|
KeyType.JOB_DEPENDENTS,
|
|
439
441
|
dependencyParams,
|
|
440
442
|
);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
+
const expireTask = [
|
|
444
|
+
depType,
|
|
445
|
+
topic,
|
|
446
|
+
gId,
|
|
447
|
+
pd,
|
|
448
|
+
jobId,
|
|
449
|
+
].join(VALSEP);
|
|
443
450
|
privateMulti[this.commands.rpush](depKey, expireTask);
|
|
444
451
|
if (!multi) {
|
|
445
452
|
return await privateMulti.exec();
|
|
@@ -450,15 +457,20 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
450
457
|
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
451
458
|
* is interrupted/expired.
|
|
452
459
|
*/
|
|
453
|
-
async registerSignalDependency(jobId: string, signalKey: string, multi? : U): Promise<any> {
|
|
460
|
+
async registerSignalDependency(jobId: string, signalKey: string, dad: string, multi? : U): Promise<any> {
|
|
454
461
|
const privateMulti = multi || this.getMulti();
|
|
455
462
|
const dependencyParams = { appId: this.appId, jobId };
|
|
456
463
|
const dependencyKey = this.mintKey(
|
|
457
464
|
KeyType.JOB_DEPENDENTS,
|
|
458
465
|
dependencyParams,
|
|
459
466
|
);
|
|
460
|
-
//tasks
|
|
461
|
-
const delistTask =
|
|
467
|
+
//persiste dependency tasks as multi-segment composite keys
|
|
468
|
+
const delistTask = [
|
|
469
|
+
'delist',
|
|
470
|
+
'signal',
|
|
471
|
+
jobId,
|
|
472
|
+
dad,
|
|
473
|
+
signalKey].join(VALSEP);
|
|
462
474
|
privateMulti[this.commands.rpush](
|
|
463
475
|
dependencyKey,
|
|
464
476
|
delistTask,
|
|
@@ -799,11 +811,14 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
799
811
|
|
|
800
812
|
async setHookSignal(hook: HookSignal, multi?: U): Promise<any> {
|
|
801
813
|
const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
|
|
802
|
-
|
|
814
|
+
//destructure the hook key
|
|
815
|
+
const { topic, resolved, jobId} = hook;
|
|
803
816
|
const signalKey = `${topic}:${resolved}`;
|
|
804
817
|
const payload = { [signalKey]: jobId };
|
|
805
818
|
await (multi || this.redisClient)[this.commands.hset](key, payload);
|
|
806
|
-
|
|
819
|
+
//jobId needs even more destructuring
|
|
820
|
+
const [_aid, dad, _gid, jid] = jobId.split(VALSEP);
|
|
821
|
+
return await this.registerSignalDependency(jid, signalKey, dad, multi);
|
|
807
822
|
}
|
|
808
823
|
|
|
809
824
|
async getHookSignal(topic: string, resolved: string): Promise<string | undefined> {
|
|
@@ -875,7 +890,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
875
890
|
const depParams = { appId: this.appId, jobId };
|
|
876
891
|
const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
|
|
877
892
|
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
878
|
-
const depKeyContext =
|
|
893
|
+
const depKeyContext = `${TYPSEP}${context}${TYPSEP}${depKey}`;
|
|
879
894
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
880
895
|
await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
|
|
881
896
|
}
|
|
@@ -892,9 +907,15 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
892
907
|
* for the given sleep group. Sleep groups are
|
|
893
908
|
* organized into 'n'-second blocks (LISTS))
|
|
894
909
|
*/
|
|
895
|
-
async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, multi?: U): Promise<void> {
|
|
910
|
+
async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, dad: string, multi?: U): Promise<void> {
|
|
896
911
|
const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
897
|
-
|
|
912
|
+
//construct the composite key (the key has enough info to signal the hook)
|
|
913
|
+
const timeEvent = [
|
|
914
|
+
type,
|
|
915
|
+
activityId,
|
|
916
|
+
gId,
|
|
917
|
+
dad,
|
|
918
|
+
jobId].join(VALSEP);
|
|
898
919
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
899
920
|
if (multi || len === 1) {
|
|
900
921
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
@@ -909,17 +930,23 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
909
930
|
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
910
931
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
911
932
|
if (timeEvent) {
|
|
912
|
-
//
|
|
913
|
-
|
|
914
|
-
|
|
933
|
+
//deconstruct composite key
|
|
934
|
+
let [
|
|
935
|
+
type,
|
|
936
|
+
activityId,
|
|
937
|
+
gId,
|
|
938
|
+
_pd,
|
|
939
|
+
...jobId] = timeEvent.split(VALSEP);
|
|
940
|
+
const jid = jobId.join(VALSEP);
|
|
941
|
+
|
|
915
942
|
if (type === 'delist') {
|
|
916
943
|
pType = 'delist';
|
|
917
944
|
} else if (type === 'child') {
|
|
918
945
|
pType = 'child';
|
|
919
946
|
} else if (type === 'expire-child') {
|
|
920
|
-
type = 'expire';
|
|
947
|
+
type = 'expire';
|
|
921
948
|
}
|
|
922
|
-
return [listKey,
|
|
949
|
+
return [listKey, jid, gId, activityId, pType];
|
|
923
950
|
}
|
|
924
951
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
925
952
|
return true;
|
|
@@ -933,13 +960,13 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
933
960
|
* do with the work list. (not everything is known in advance,
|
|
934
961
|
* so the ZSET key defines HOW to approach the work in the
|
|
935
962
|
* generic LIST (lists typically contain target job ids)
|
|
936
|
-
* @param {string} listKey -
|
|
963
|
+
* @param {string} listKey - composite key
|
|
937
964
|
*/
|
|
938
965
|
resolveTaskKeyContext(listKey: string): [WorkListTaskType, string] {
|
|
939
|
-
if (listKey.startsWith(
|
|
940
|
-
return ['interrupt', listKey.split(
|
|
941
|
-
} else if (listKey.startsWith(
|
|
942
|
-
return ['expire', listKey.split(
|
|
966
|
+
if (listKey.startsWith(`${TYPSEP}INTERRUPT`)) {
|
|
967
|
+
return ['interrupt', listKey.split(TYPSEP)[2]];
|
|
968
|
+
} else if (listKey.startsWith(`${TYPSEP}EXPIRE`)) {
|
|
969
|
+
return ['expire', listKey.split(TYPSEP)[2]];
|
|
943
970
|
} else {
|
|
944
971
|
return ['sleep', listKey];
|
|
945
972
|
}
|
|
@@ -1001,7 +1028,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
1001
1028
|
await this.redisClient[this.commands.del](jobKey);
|
|
1002
1029
|
}
|
|
1003
1030
|
|
|
1004
|
-
async findJobs(queryString: string = '*', limit: number = 1000, batchSize: number = 1000): Promise<string[]> {
|
|
1031
|
+
async findJobs(queryString: string = '*', limit: number = 1000, batchSize: number = 1000): Promise<[string, string[]]> {
|
|
1005
1032
|
const matchKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId: queryString });
|
|
1006
1033
|
let cursor = '0';
|
|
1007
1034
|
let keys: string[];
|
|
@@ -1027,7 +1054,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
1027
1054
|
break;
|
|
1028
1055
|
}
|
|
1029
1056
|
} while (cursor !== '0');
|
|
1030
|
-
return matchingKeys;
|
|
1057
|
+
return [cursor, matchingKeys];
|
|
1031
1058
|
}
|
|
1032
1059
|
|
|
1033
1060
|
async findJobFields(jobId: string, fieldMatchPattern: string = '*', limit: number = 1000, batchSize: number = 1000, cursor = '0'): Promise<[string, StringStringType]> {
|
package/services/task/index.ts
CHANGED
|
@@ -11,12 +11,14 @@ import { KeyType } from '../../types/hotmesh';
|
|
|
11
11
|
import { JobCompletionOptions, JobState } from '../../types/job';
|
|
12
12
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
13
13
|
import { WorkListTaskType } from '../../types/task';
|
|
14
|
+
import { VALSEP, WEBSEP } from '../../modules/key';
|
|
14
15
|
|
|
15
16
|
class TaskService {
|
|
16
17
|
store: StoreService<RedisClient, RedisMulti>;
|
|
17
18
|
logger: ILogger;
|
|
18
19
|
cleanupTimeout: NodeJS.Timeout | null = null;
|
|
19
20
|
isScout: boolean = false;
|
|
21
|
+
errorCount = 0;
|
|
20
22
|
|
|
21
23
|
constructor(
|
|
22
24
|
store: StoreService<RedisClient, RedisMulti>,
|
|
@@ -29,8 +31,8 @@ class TaskService {
|
|
|
29
31
|
async processWebHooks(hookEventCallback: HookInterface): Promise<void> {
|
|
30
32
|
const workItemKey = await this.store.getActiveTaskQueue();
|
|
31
33
|
if (workItemKey) {
|
|
32
|
-
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(
|
|
33
|
-
const data = JSON.parse(sdata.join(
|
|
34
|
+
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(WEBSEP);
|
|
35
|
+
const data = JSON.parse(sdata.join(WEBSEP));
|
|
34
36
|
const destinationKey = `${sourceKey}:processed`;
|
|
35
37
|
const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
|
|
36
38
|
if (jobId) {
|
|
@@ -72,6 +74,7 @@ class TaskService {
|
|
|
72
74
|
activityId: string,
|
|
73
75
|
type: WorkListTaskType,
|
|
74
76
|
inSeconds = HMSH_FIDELITY_SECONDS,
|
|
77
|
+
dad: string,
|
|
75
78
|
multi?: RedisMulti,
|
|
76
79
|
): Promise<void> {
|
|
77
80
|
const fromNow = Date.now() + (inSeconds * 1000);
|
|
@@ -83,6 +86,7 @@ class TaskService {
|
|
|
83
86
|
activityId,
|
|
84
87
|
type,
|
|
85
88
|
awakenTimeSlot,
|
|
89
|
+
dad,
|
|
86
90
|
multi,
|
|
87
91
|
);
|
|
88
92
|
}
|
|
@@ -129,21 +133,29 @@ class TaskService {
|
|
|
129
133
|
await timeEventCallback(target, gId, activityId, type);
|
|
130
134
|
}
|
|
131
135
|
await sleepFor(0);
|
|
136
|
+
this.errorCount = 0;
|
|
132
137
|
this.processTimeHooks(timeEventCallback, listKey);
|
|
133
138
|
} else if (workListTask) {
|
|
134
139
|
//a worklist was just emptied; try again immediately
|
|
135
140
|
await sleepFor(0);
|
|
141
|
+
this.errorCount = 0;
|
|
136
142
|
this.processTimeHooks(timeEventCallback);
|
|
137
143
|
} else {
|
|
138
144
|
//no worklists exist; sleep before checking
|
|
139
145
|
let sleep = XSleepFor(HMSH_FIDELITY_SECONDS * 1000);
|
|
140
146
|
this.cleanupTimeout = sleep.timerId;
|
|
141
147
|
await sleep.promise;
|
|
148
|
+
this.errorCount = 0;
|
|
142
149
|
this.processTimeHooks(timeEventCallback);
|
|
143
150
|
}
|
|
144
151
|
} catch (err) {
|
|
145
|
-
//
|
|
146
|
-
|
|
152
|
+
//most common reasons: deleted job not found; container stopping; test stopping
|
|
153
|
+
//less common: redis/cluster down; retry with fallback (5s max main reassignment)
|
|
154
|
+
this.logger.warn('task-process-timehooks-error', err);
|
|
155
|
+
await sleepFor(1_000 * this.errorCount++);
|
|
156
|
+
if (this.errorCount < 5) {
|
|
157
|
+
this.processTimeHooks(timeEventCallback);
|
|
158
|
+
}
|
|
147
159
|
}
|
|
148
160
|
} else {
|
|
149
161
|
//didn't get the scout role; try again in 'one-ish' minutes
|
|
@@ -174,10 +186,18 @@ class TaskService {
|
|
|
174
186
|
const jobId = context.metadata.jid;
|
|
175
187
|
const gId = context.metadata.gid;
|
|
176
188
|
const activityId = hookRule.to;
|
|
189
|
+
//composite keys are used to fully describe the task target
|
|
190
|
+
const compositeJobKey = [
|
|
191
|
+
activityId,
|
|
192
|
+
dad,
|
|
193
|
+
gId,
|
|
194
|
+
jobId
|
|
195
|
+
].join(WEBSEP);
|
|
196
|
+
|
|
177
197
|
const hook: HookSignal = {
|
|
178
198
|
topic,
|
|
179
199
|
resolved,
|
|
180
|
-
jobId:
|
|
200
|
+
jobId: compositeJobKey,
|
|
181
201
|
}
|
|
182
202
|
await this.store.setHookSignal(hook, multi);
|
|
183
203
|
return jobId;
|
|
@@ -196,17 +216,17 @@ class TaskService {
|
|
|
196
216
|
const resolved = Pipe.resolve(mapExpression, context);
|
|
197
217
|
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
198
218
|
if (!hookSignalId) {
|
|
199
|
-
//messages can be double-processed; not an issue; return undefined
|
|
200
|
-
//users can also provide a bogus topic; not an issue; return undefined
|
|
219
|
+
//messages can be double-processed; not an issue; return `undefined`
|
|
220
|
+
//users can also provide a bogus topic; not an issue; return `undefined`
|
|
201
221
|
return undefined;
|
|
202
222
|
}
|
|
203
|
-
//`aid` is part of
|
|
223
|
+
//`aid` is part of composite key, but the hook `topic` is its public interface;
|
|
204
224
|
// this means that a new version of the graph can be deployed and the
|
|
205
225
|
// topic can be re-mapped to a different activity id. Outside callers
|
|
206
226
|
// can adhere to the unchanged contract (calling the same topic),
|
|
207
|
-
// while the internal system can be updated in real
|
|
208
|
-
const [_aid, dad, gid, ...jid] = hookSignalId.split(
|
|
209
|
-
return [jid.join(
|
|
227
|
+
// while the internal system can be updated in real-time as necessary.
|
|
228
|
+
const [_aid, dad, gid, ...jid] = hookSignalId.split(WEBSEP);
|
|
229
|
+
return [jid.join(WEBSEP), hookRule.to, dad, gid];
|
|
210
230
|
} else {
|
|
211
231
|
throw new Error('signal-not-found');
|
|
212
232
|
}
|
package/services/worker/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KeyType } from "../../modules/key";
|
|
2
|
-
import { formatISODate, getSystemHealth, identifyRedisType } from "../../modules/utils";
|
|
2
|
+
import { XSleepFor, formatISODate, getSystemHealth, identifyRedisType, sleepFor } from "../../modules/utils";
|
|
3
3
|
import { ConnectorService } from "../connector";
|
|
4
4
|
import { ILogger } from "../logger";
|
|
5
5
|
import { Router } from "../router";
|
|
@@ -17,10 +17,12 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
|
|
|
17
17
|
import {
|
|
18
18
|
QuorumMessage,
|
|
19
19
|
QuorumProfile,
|
|
20
|
+
RollCallMessage,
|
|
20
21
|
SubscriptionCallback } from "../../types/quorum";
|
|
21
22
|
import { RedisClient, RedisMulti } from "../../types/redis";
|
|
22
23
|
import { RedisClientType } from '../../types/redisclient';
|
|
23
|
-
import { StreamRole } from "../../types/stream";
|
|
24
|
+
import { StreamData, StreamRole, StreamDataResponse } from "../../types/stream";
|
|
25
|
+
import { HMSH_QUORUM_ROLLCALL_CYCLES } from "../../modules/enums";
|
|
24
26
|
|
|
25
27
|
class WorkerService {
|
|
26
28
|
namespace: string;
|
|
@@ -28,6 +30,7 @@ class WorkerService {
|
|
|
28
30
|
guid: string;
|
|
29
31
|
topic: string;
|
|
30
32
|
config: HotMeshConfig;
|
|
33
|
+
callback: (streamData: StreamData) => Promise<StreamDataResponse|void>;
|
|
31
34
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
32
35
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
33
36
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
@@ -35,6 +38,7 @@ class WorkerService {
|
|
|
35
38
|
logger: ILogger;
|
|
36
39
|
reporting = false;
|
|
37
40
|
inited: string;
|
|
41
|
+
rollCallInterval: NodeJS.Timeout;
|
|
38
42
|
|
|
39
43
|
static async init(
|
|
40
44
|
namespace: string,
|
|
@@ -58,6 +62,7 @@ class WorkerService {
|
|
|
58
62
|
service.namespace = namespace;
|
|
59
63
|
service.appId = appId;
|
|
60
64
|
service.guid = guid;
|
|
65
|
+
service.callback = worker.callback;
|
|
61
66
|
service.topic = worker.topic;
|
|
62
67
|
service.config = config;
|
|
63
68
|
service.logger = logger;
|
|
@@ -155,14 +160,51 @@ class WorkerService {
|
|
|
155
160
|
return async (topic: string, message: QuorumMessage) => {
|
|
156
161
|
self.logger.debug('worker-event-received', { topic, type: message.type });
|
|
157
162
|
if (message.type === 'throttle') {
|
|
158
|
-
|
|
163
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
164
|
+
self.throttle(message.throttle);
|
|
165
|
+
}
|
|
159
166
|
} else if(message.type === 'ping') {
|
|
160
167
|
self.sayPong(self.appId, self.guid, message.originator, message.details);
|
|
168
|
+
} else if(message.type === 'rollcall') {
|
|
169
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
170
|
+
self.doRollCall(message);
|
|
171
|
+
}
|
|
161
172
|
}
|
|
162
173
|
};
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
|
|
176
|
+
/**
|
|
177
|
+
* A quorum-wide command to broadcaset system details.
|
|
178
|
+
*
|
|
179
|
+
*/
|
|
180
|
+
async doRollCall(message: RollCallMessage) {
|
|
181
|
+
let iteration = 0;
|
|
182
|
+
let max = !isNaN(message.max) ? message.max : HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
183
|
+
if (this.rollCallInterval) clearTimeout(this.rollCallInterval);
|
|
184
|
+
const base = (message.interval / 2);
|
|
185
|
+
const amount = base + Math.ceil(Math.random() * base);
|
|
186
|
+
do {
|
|
187
|
+
await sleepFor(Math.ceil(Math.random() * 1000));
|
|
188
|
+
await this.sayPong(this.appId, this.guid, null, true, message.signature);
|
|
189
|
+
if (!message.interval) return;
|
|
190
|
+
const { promise, timerId } = XSleepFor(amount * 1000);
|
|
191
|
+
this.rollCallInterval = timerId;
|
|
192
|
+
await promise;
|
|
193
|
+
} while (this.rollCallInterval && iteration++ < max - 1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
cancelRollCall() {
|
|
197
|
+
if (this.rollCallInterval) {
|
|
198
|
+
clearTimeout(this.rollCallInterval);
|
|
199
|
+
delete this.rollCallInterval;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
stop() {
|
|
204
|
+
this.cancelRollCall();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async sayPong(appId: string, guid: string, originator?: string, details = false, signature = false) {
|
|
166
208
|
let profile: QuorumProfile;
|
|
167
209
|
if (details) {
|
|
168
210
|
const params = {
|
|
@@ -183,13 +225,15 @@ class WorkerService {
|
|
|
183
225
|
reclaimDelay: this.router.reclaimDelay,
|
|
184
226
|
reclaimCount: this.router.reclaimCount,
|
|
185
227
|
system: await getSystemHealth(),
|
|
228
|
+
signature: signature ? this.callback.toString() : undefined,
|
|
186
229
|
};
|
|
187
230
|
}
|
|
188
231
|
this.store.publish(
|
|
189
232
|
KeyType.QUORUM,
|
|
190
233
|
{
|
|
191
234
|
type: 'pong',
|
|
192
|
-
guid,
|
|
235
|
+
guid,
|
|
236
|
+
originator,
|
|
193
237
|
profile,
|
|
194
238
|
},
|
|
195
239
|
appId,
|
package/types/activity.ts
CHANGED
package/types/exporter.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface JobTimeline {
|
|
|
26
26
|
dimension: string; //dimensional isolate path
|
|
27
27
|
duplex: 'entry' | 'exit'; //activity entry or exit
|
|
28
28
|
timestamp: string; //actually a number but too many digits for JS
|
|
29
|
+
created?: string; //actually a number but too many digits for JS
|
|
30
|
+
updated?: string; //actually a number but too many digits for JS
|
|
29
31
|
actions?: ActivityAction[];
|
|
30
32
|
}
|
|
31
33
|
|
package/types/job.ts
CHANGED
|
@@ -17,6 +17,7 @@ type JobMetadata = {
|
|
|
17
17
|
pg?: string; //parent_generational_id (system assigned at trigger inception); pg is the parent job's gid (just in case user created/deleted/created a job with same jid)
|
|
18
18
|
pd?: string; //parent_dimensional_address
|
|
19
19
|
pa?: string; //parent_activity_id
|
|
20
|
+
px?: boolean; //sever the dependency chain if true (startChild/vs/executeChild)
|
|
20
21
|
ngn?: string; //engine guid (one time subscriptions)
|
|
21
22
|
app: string; //app_id
|
|
22
23
|
vrs: string; //app version
|
package/types/quorum.ts
CHANGED
|
@@ -50,50 +50,65 @@ export interface QuorumProfile {
|
|
|
50
50
|
reclaimDelay?: number;
|
|
51
51
|
reclaimCount?: number;
|
|
52
52
|
system?: SystemHealth;
|
|
53
|
+
signature?: string; //stringified function
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
interface QuorumMessageBase {
|
|
57
|
+
guid?: string;
|
|
58
|
+
topic?: string;
|
|
59
|
+
type?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Messages extending QuorumMessageBase
|
|
63
|
+
export interface PingMessage extends QuorumMessageBase {
|
|
57
64
|
type: 'ping';
|
|
58
65
|
originator: string; //guid
|
|
59
66
|
details?: boolean; //if true, all endpoints will include their profile
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
export interface WorkMessage {
|
|
69
|
+
export interface WorkMessage extends QuorumMessageBase {
|
|
63
70
|
type: 'work';
|
|
64
71
|
originator: string; //guid
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
export interface CronMessage {
|
|
74
|
+
export interface CronMessage extends QuorumMessageBase {
|
|
68
75
|
type: 'cron';
|
|
69
76
|
originator: string; //guid
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
export interface PongMessage {
|
|
79
|
+
export interface PongMessage extends QuorumMessageBase {
|
|
73
80
|
type: 'pong';
|
|
74
81
|
guid: string; //call initiator
|
|
75
82
|
originator: string; //clone of originator guid passed in ping
|
|
76
83
|
profile?: QuorumProfile; //contains details about the engine/worker
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
export interface ActivateMessage {
|
|
86
|
+
export interface ActivateMessage extends QuorumMessageBase {
|
|
80
87
|
type: 'activate';
|
|
81
88
|
cache_mode: 'nocache' | 'cache';
|
|
82
89
|
until_version: string;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
export interface JobMessage {
|
|
92
|
+
export interface JobMessage extends QuorumMessageBase {
|
|
86
93
|
type: 'job';
|
|
87
94
|
topic: string; //this comes from the 'publishes' field in the YAML
|
|
88
95
|
job: JobOutput
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
export interface ThrottleMessage {
|
|
98
|
+
export interface ThrottleMessage extends QuorumMessageBase {
|
|
93
99
|
type: 'throttle';
|
|
94
|
-
guid?: string; //target
|
|
95
|
-
topic?: string; //target
|
|
96
|
-
throttle: number; //0-n
|
|
100
|
+
guid?: string; //target engine AND workers with this guid
|
|
101
|
+
topic?: string; //target worker(s) matching this topic (pass null to only target the engine, pass undefined to target engine and workers)
|
|
102
|
+
throttle: number; //0-n; millis
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface RollCallMessage extends QuorumMessageBase {
|
|
106
|
+
type: 'rollcall';
|
|
107
|
+
guid?: string; //target the engine quorum
|
|
108
|
+
topic?: string | null; //target a worker if string; suppress if `null`;
|
|
109
|
+
interval: number; //every 'n' seconds
|
|
110
|
+
max?: number; //max broadcasts
|
|
111
|
+
signature?: boolean; //include bound worker function in broadcast
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
export interface JobMessageCallback {
|
|
@@ -114,4 +129,4 @@ export interface QuorumMessageCallback {
|
|
|
114
129
|
* These messages serve to coordinate the cache invalidation and switch-over
|
|
115
130
|
* to the new version without any downtime and a coordinating parent server.
|
|
116
131
|
*/
|
|
117
|
-
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
|
|
132
|
+
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | RollCallMessage | CronMessage;
|
package/types/stream.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface StreamData {
|
|
|
42
42
|
trc?: string; //trace id
|
|
43
43
|
spn?: string; //span id
|
|
44
44
|
try?: number; //current try count
|
|
45
|
+
await?: boolean; //(waitfor) if explicitly false, do not await; sever the connection
|
|
45
46
|
};
|
|
46
47
|
type?: StreamDataType;
|
|
47
48
|
data: Record<string, unknown>;
|