@hotmeshio/hotmesh 0.0.49 → 0.0.51

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.
Files changed (50) hide show
  1. package/build/modules/enums.d.ts +1 -0
  2. package/build/modules/enums.js +2 -1
  3. package/build/modules/key.d.ts +5 -1
  4. package/build/modules/key.js +10 -2
  5. package/build/package.json +2 -1
  6. package/build/services/activities/await.js +6 -0
  7. package/build/services/activities/hook.js +1 -1
  8. package/build/services/activities/trigger.d.ts +1 -0
  9. package/build/services/activities/trigger.js +23 -2
  10. package/build/services/durable/exporter.js +19 -5
  11. package/build/services/engine/index.d.ts +1 -1
  12. package/build/services/engine/index.js +12 -3
  13. package/build/services/exporter/index.js +3 -2
  14. package/build/services/hotmesh/index.js +4 -0
  15. package/build/services/quorum/index.d.ts +11 -2
  16. package/build/services/quorum/index.js +33 -0
  17. package/build/services/serializer/index.js +1 -1
  18. package/build/services/store/index.d.ts +5 -5
  19. package/build/services/store/index.js +44 -24
  20. package/build/services/task/index.d.ts +2 -1
  21. package/build/services/task/index.js +30 -13
  22. package/build/services/worker/index.d.ts +13 -2
  23. package/build/services/worker/index.js +44 -3
  24. package/build/types/activity.d.ts +1 -0
  25. package/build/types/exporter.d.ts +2 -0
  26. package/build/types/index.d.ts +1 -1
  27. package/build/types/job.d.ts +1 -0
  28. package/build/types/quorum.d.ts +22 -8
  29. package/build/types/stream.d.ts +1 -0
  30. package/modules/enums.ts +1 -0
  31. package/modules/key.ts +7 -2
  32. package/package.json +2 -1
  33. package/services/activities/await.ts +6 -0
  34. package/services/activities/hook.ts +1 -0
  35. package/services/activities/trigger.ts +25 -1
  36. package/services/durable/exporter.ts +18 -7
  37. package/services/engine/index.ts +13 -5
  38. package/services/exporter/index.ts +3 -2
  39. package/services/hotmesh/index.ts +4 -0
  40. package/services/quorum/index.ts +38 -2
  41. package/services/serializer/index.ts +1 -1
  42. package/services/store/index.ts +51 -25
  43. package/services/task/index.ts +31 -11
  44. package/services/worker/index.ts +49 -5
  45. package/types/activity.ts +1 -0
  46. package/types/exporter.ts +2 -0
  47. package/types/index.ts +2 -0
  48. package/types/job.ts +1 -0
  49. package/types/quorum.ts +28 -13
  50. package/types/stream.ts +1 -0
@@ -1,7 +1,9 @@
1
1
  import {
2
2
  HMSH_ACTIVATION_MAX_RETRY,
3
- HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
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: ThrottleMessage) {
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']
@@ -2,7 +2,9 @@ import {
2
2
  KeyService,
3
3
  KeyStoreParams,
4
4
  KeyType,
5
- HMNS} from '../../modules/key';
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
- //items listed as job dependencies have different relationships
442
- const expireTask = `${depType}::${topic}::${gId}::${jobId}`;
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 have '4' segments
461
- const delistTask = `delist::signal::${jobId}::${signalKey}`;
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
- const { topic, resolved, jobId} = hook; //`${activityId}::${dad}::${gId}::${jobId}`
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
- return await this.registerSignalDependency(jobId.split('::')[3], signalKey, multi);
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 = `::${context}::${depKey}`;
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
- const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
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
- //there are task types
913
- //1) sleep (awaken), 2) expire (OR expire-child), 3) interrupt, 4) delist, 5) child (just an index helper; no work to do)
914
- let [type, activityId, gId, ...jobId] = timeEvent.split('::');
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'; //use the same logic as 'expire'
947
+ type = 'expire';
921
948
  }
922
- return [listKey, jobId.join('::'), gId, activityId, pType];
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 - for example `::INTERRUPT::job123` or `job123`
963
+ * @param {string} listKey - composite key
937
964
  */
938
965
  resolveTaskKeyContext(listKey: string): [WorkListTaskType, string] {
939
- if (listKey.startsWith('::INTERRUPT')) {
940
- return ['interrupt', listKey.split('::')[2]];
941
- } else if (listKey.startsWith('::EXPIRE')) {
942
- return ['expire', listKey.split('::')[2]];
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,9 +1028,8 @@ 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, cursor = '0'): Promise<[string, string[]]> {
1005
1032
  const matchKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId: queryString });
1006
- let cursor = '0';
1007
1033
  let keys: string[];
1008
1034
  const matchingKeys: string[] = [];
1009
1035
  do {
@@ -1027,7 +1053,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
1027
1053
  break;
1028
1054
  }
1029
1055
  } while (cursor !== '0');
1030
- return matchingKeys;
1056
+ return [cursor, matchingKeys];
1031
1057
  }
1032
1058
 
1033
1059
  async findJobFields(jobId: string, fieldMatchPattern: string = '*', limit: number = 1000, batchSize: number = 1000, cursor = '0'): Promise<[string, StringStringType]> {
@@ -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
- //todo: retry connect to redis
146
- this.logger.error('task-process-timehooks-error', err);
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: `${activityId}::${dad}::${gId}::${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 composit key, but the hook `topic` is its public interface;
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 time as necessary.
208
- const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
209
- return [jid.join('::'), hookRule.to, dad, gid];
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
  }
@@ -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
- self.throttle(message.throttle);
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
- async sayPong(appId: string, guid: string, originator: string, details = false) {
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, originator,
235
+ guid,
236
+ originator,
193
237
  profile,
194
238
  },
195
239
  appId,
package/types/activity.ts CHANGED
@@ -77,6 +77,7 @@ interface AwaitActivity extends BaseActivity {
77
77
  type: 'await';
78
78
  eventName: string;
79
79
  timeout: number;
80
+ await?: boolean; //if exlicitly false do not await the response
80
81
  }
81
82
 
82
83
  interface WorkerActivity extends BaseActivity {
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/index.ts CHANGED
@@ -107,6 +107,7 @@ export {
107
107
  KeyType } from './hotmesh';
108
108
  export {
109
109
  ActivateMessage,
110
+ CronMessage,
110
111
  JobMessage,
111
112
  JobMessageCallback,
112
113
  PingMessage,
@@ -114,6 +115,7 @@ export {
114
115
  QuorumMessage,
115
116
  QuorumMessageCallback,
116
117
  QuorumProfile,
118
+ RollCallMessage,
117
119
  SubscriptionCallback,
118
120
  SystemHealth,
119
121
  ThrottleMessage,
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
- //used for coordination (like version activation)
56
- export interface PingMessage {
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
- //delay in ms between fetches from the buffered stream (speed/slow down entire network)
92
- export interface ThrottleMessage {
98
+ export interface ThrottleMessage extends QuorumMessageBase {
93
99
  type: 'throttle';
94
- guid?: string; //target the engine quorum
95
- topic?: string; //target a worker quorum
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>;