@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.
Files changed (66) hide show
  1. package/README.md +1 -1
  2. package/build/modules/enums.d.ts +1 -0
  3. package/build/modules/enums.js +2 -1
  4. package/build/modules/key.d.ts +5 -1
  5. package/build/modules/key.js +10 -2
  6. package/build/package.json +2 -1
  7. package/build/services/activities/await.js +6 -0
  8. package/build/services/activities/hook.js +1 -1
  9. package/build/services/activities/trigger.d.ts +1 -0
  10. package/build/services/activities/trigger.js +23 -2
  11. package/build/services/durable/exporter.js +19 -5
  12. package/build/services/durable/meshos.js +11 -6
  13. package/build/services/durable/search.d.ts +20 -1
  14. package/build/services/durable/search.js +73 -25
  15. package/build/services/durable/worker.js +10 -0
  16. package/build/services/durable/workflow.d.ts +1 -0
  17. package/build/services/durable/workflow.js +17 -1
  18. package/build/services/engine/index.d.ts +1 -1
  19. package/build/services/engine/index.js +12 -3
  20. package/build/services/exporter/index.js +3 -2
  21. package/build/services/hotmesh/index.js +4 -0
  22. package/build/services/quorum/index.d.ts +11 -2
  23. package/build/services/quorum/index.js +33 -0
  24. package/build/services/router/index.d.ts +15 -0
  25. package/build/services/router/index.js +55 -7
  26. package/build/services/serializer/index.js +1 -1
  27. package/build/services/store/clients/redis.js +2 -0
  28. package/build/services/store/index.d.ts +6 -4
  29. package/build/services/store/index.js +86 -21
  30. package/build/services/task/index.d.ts +2 -1
  31. package/build/services/task/index.js +30 -13
  32. package/build/services/worker/index.d.ts +13 -2
  33. package/build/services/worker/index.js +44 -3
  34. package/build/types/activity.d.ts +1 -0
  35. package/build/types/durable.d.ts +9 -0
  36. package/build/types/exporter.d.ts +2 -0
  37. package/build/types/job.d.ts +1 -0
  38. package/build/types/quorum.d.ts +22 -8
  39. package/build/types/stream.d.ts +1 -0
  40. package/modules/enums.ts +1 -0
  41. package/modules/key.ts +7 -2
  42. package/package.json +2 -1
  43. package/services/activities/await.ts +6 -0
  44. package/services/activities/hook.ts +1 -0
  45. package/services/activities/trigger.ts +25 -1
  46. package/services/durable/exporter.ts +18 -7
  47. package/services/durable/meshos.ts +10 -6
  48. package/services/durable/search.ts +73 -26
  49. package/services/durable/worker.ts +13 -1
  50. package/services/durable/workflow.ts +18 -0
  51. package/services/engine/index.ts +13 -5
  52. package/services/exporter/index.ts +3 -2
  53. package/services/hotmesh/index.ts +4 -0
  54. package/services/quorum/index.ts +38 -2
  55. package/services/router/index.ts +59 -9
  56. package/services/serializer/index.ts +1 -1
  57. package/services/store/clients/redis.ts +2 -0
  58. package/services/store/index.ts +108 -22
  59. package/services/task/index.ts +31 -11
  60. package/services/worker/index.ts +49 -5
  61. package/types/activity.ts +1 -0
  62. package/types/durable.ts +11 -0
  63. package/types/exporter.ts +2 -0
  64. package/types/job.ts +1 -0
  65. package/types/quorum.ts +28 -13
  66. 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, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
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
- pub(quorumMessage: ThrottleMessage): Promise<boolean>;
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
- let sleep = (0, utils_1.XSleepFor)(this.throttle);
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.currentTimerId !== undefined) {
233
- clearTimeout(this.currentTimerId);
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
- this.logger.info(`stream-throttle-reset`, { delay: this.throttle, topic: this.topic });
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 - for example `::INTERRUPT::job123` or `job123`
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
- //items listed as job dependencies have different relationships
354
- const expireTask = `${depType}::${topic}::${gId}::${jobId}`;
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 have '4' segments
369
- const delistTask = `delist::signal::${jobId}::${signalKey}`;
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
- const { topic, resolved, jobId } = hook; //`${activityId}::${dad}::${gId}::${jobId}`
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
- return await this.registerSignalDependency(jobId.split('::')[3], signalKey, multi);
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 = `::${context}::${depKey}`;
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
- const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
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
- //there are task types
788
- //1) sleep (awaken), 2) expire (OR expire-child), 3) interrupt, 4) delist, 5) child (just an index helper; no work to do)
789
- let [type, activityId, gId, ...jobId] = timeEvent.split('::');
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'; //use the same logic as 'expire'
820
+ type = 'expire';
798
821
  }
799
- return [listKey, jobId.join('::'), gId, activityId, pType];
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 - for example `::INTERRUPT::job123` or `job123`
835
+ * @param {string} listKey - composite key
813
836
  */
814
837
  resolveTaskKeyContext(listKey) {
815
- if (listKey.startsWith('::INTERRUPT')) {
816
- return ['interrupt', listKey.split('::')[2]];
838
+ if (listKey.startsWith(`${key_1.TYPSEP}INTERRUPT`)) {
839
+ return ['interrupt', listKey.split(key_1.TYPSEP)[2]];
817
840
  }
818
- else if (listKey.startsWith('::EXPIRE')) {
819
- return ['expire', listKey.split('::')[2]];
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?: number, multi?: RedisMulti): Promise<void>;
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
- //todo: retry connect to redis
109
- this.logger.error('task-process-timehooks-error', err);
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: `${activityId}::${dad}::${gId}::${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 composit key, but the hook `topic` is its public interface;
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 time as necessary.
169
- const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
170
- return [jid.join('::'), hookRule.to, dad, gid];
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
- sayPong(appId: string, guid: string, originator: string, details?: boolean): Promise<void>;
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 };