@hotmeshio/hotmesh 0.0.37 → 0.0.39

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 (78) hide show
  1. package/README.md +14 -8
  2. package/build/modules/enums.d.ts +29 -23
  3. package/build/modules/enums.js +38 -29
  4. package/build/modules/errors.d.ts +1 -1
  5. package/build/modules/errors.js +9 -7
  6. package/build/modules/key.d.ts +1 -34
  7. package/build/modules/key.js +24 -47
  8. package/build/package.json +1 -1
  9. package/build/services/activities/activity.js +1 -1
  10. package/build/services/activities/hook.js +4 -9
  11. package/build/services/activities/trigger.d.ts +3 -2
  12. package/build/services/activities/trigger.js +10 -6
  13. package/build/services/durable/client.d.ts +9 -1
  14. package/build/services/durable/client.js +30 -14
  15. package/build/services/durable/handle.js +2 -2
  16. package/build/services/durable/worker.js +4 -3
  17. package/build/services/engine/index.d.ts +2 -1
  18. package/build/services/engine/index.js +6 -6
  19. package/build/services/hotmesh/index.d.ts +2 -2
  20. package/build/services/hotmesh/index.js +3 -4
  21. package/build/services/quorum/index.d.ts +6 -6
  22. package/build/services/quorum/index.js +47 -11
  23. package/build/services/router/index.js +16 -14
  24. package/build/services/store/clients/ioredis.d.ts +1 -0
  25. package/build/services/store/clients/ioredis.js +9 -0
  26. package/build/services/store/clients/redis.d.ts +1 -0
  27. package/build/services/store/clients/redis.js +16 -0
  28. package/build/services/store/index.d.ts +15 -9
  29. package/build/services/store/index.js +46 -23
  30. package/build/services/stream/clients/ioredis.d.ts +1 -0
  31. package/build/services/stream/clients/ioredis.js +33 -24
  32. package/build/services/stream/clients/redis.d.ts +1 -0
  33. package/build/services/stream/clients/redis.js +15 -0
  34. package/build/services/stream/index.d.ts +1 -0
  35. package/build/services/task/index.d.ts +10 -3
  36. package/build/services/task/index.js +35 -17
  37. package/build/services/worker/index.d.ts +1 -0
  38. package/build/services/worker/index.js +24 -0
  39. package/build/types/durable.d.ts +3 -2
  40. package/build/types/hotmesh.d.ts +43 -2
  41. package/build/types/hotmesh.js +28 -0
  42. package/build/types/index.d.ts +3 -2
  43. package/build/types/index.js +3 -1
  44. package/build/types/logger.d.ts +1 -0
  45. package/build/types/logger.js +1 -0
  46. package/build/types/quorum.d.ts +11 -1
  47. package/build/types/redisclient.d.ts +1 -0
  48. package/build/types/task.d.ts +1 -0
  49. package/build/types/task.js +2 -0
  50. package/modules/enums.ts +49 -35
  51. package/modules/errors.ts +17 -8
  52. package/modules/key.ts +3 -40
  53. package/package.json +1 -1
  54. package/services/activities/activity.ts +2 -2
  55. package/services/activities/hook.ts +18 -9
  56. package/services/activities/trigger.ts +10 -6
  57. package/services/durable/client.ts +31 -15
  58. package/services/durable/handle.ts +3 -3
  59. package/services/durable/worker.ts +4 -3
  60. package/services/engine/index.ts +13 -12
  61. package/services/hotmesh/index.ts +4 -5
  62. package/services/quorum/index.ts +48 -12
  63. package/services/router/index.ts +26 -24
  64. package/services/store/clients/ioredis.ts +9 -0
  65. package/services/store/clients/redis.ts +16 -0
  66. package/services/store/index.ts +63 -25
  67. package/services/stream/clients/ioredis.ts +33 -24
  68. package/services/stream/clients/redis.ts +14 -0
  69. package/services/stream/index.ts +1 -0
  70. package/services/task/index.ts +66 -24
  71. package/services/worker/index.ts +30 -0
  72. package/types/durable.ts +6 -5
  73. package/types/hotmesh.ts +47 -2
  74. package/types/index.ts +8 -1
  75. package/types/logger.ts +3 -1
  76. package/types/quorum.ts +15 -4
  77. package/types/redisclient.ts +1 -0
  78. package/types/task.ts +1 -0
@@ -1,14 +1,14 @@
1
1
  import {
2
- BLOCK_TIME_MS,
3
- MAX_RETRIES,
4
- MAX_TIMEOUT_MS,
5
- GRADUATED_INTERVAL_MS,
6
- STATUS_CODE_UNACKED,
7
- STATUS_CODE_UNKNOWN,
8
- STATUS_MESSAGE_UNKNOWN,
9
- XCLAIM_COUNT,
10
- XCLAIM_DELAY_MS,
11
- XPENDING_COUNT } from '../../modules/enums';
2
+ HMSH_BLOCK_TIME_MS,
3
+ HMSH_MAX_RETRIES,
4
+ HMSH_MAX_TIMEOUT_MS,
5
+ HMSH_GRADUATED_INTERVAL_MS,
6
+ HMSH_CODE_UNACKED,
7
+ HMSH_CODE_UNKNOWN,
8
+ HMSH_STATUS_UNKNOWN,
9
+ HMSH_XCLAIM_COUNT,
10
+ HMSH_XCLAIM_DELAY_MS,
11
+ HMSH_XPENDING_COUNT } from '../../modules/enums';
12
12
  import { KeyType } from '../../modules/key';
13
13
  import { XSleepFor, guid, sleepFor } from '../../modules/utils';
14
14
  import { ILogger } from '../logger';
@@ -50,8 +50,8 @@ class Router {
50
50
  this.topic = config.topic;
51
51
  this.stream = stream;
52
52
  this.store = store;
53
- this.reclaimDelay = config.reclaimDelay || XCLAIM_DELAY_MS;
54
- this.reclaimCount = config.reclaimCount || XCLAIM_COUNT;
53
+ this.reclaimDelay = config.reclaimDelay || HMSH_XCLAIM_DELAY_MS;
54
+ this.reclaimCount = config.reclaimCount || HMSH_XCLAIM_COUNT;
55
55
  this.logger = logger;
56
56
  }
57
57
 
@@ -85,7 +85,9 @@ class Router {
85
85
  }
86
86
 
87
87
  try {
88
- const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', BLOCK_TIME_MS, 'STREAMS', stream, '>');
88
+ //randomizer that asymptotes at 150% of `HMSH_BLOCK_TIME_MS`
89
+ const streamDuration = HMSH_BLOCK_TIME_MS + Math.round((HMSH_BLOCK_TIME_MS * Math.random()));
90
+ const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', streamDuration, 'STREAMS', stream, '>');
89
91
  if (this.isStreamMessage(result)) {
90
92
  const [[, messages]] = result;
91
93
  for (const [id, message] of messages) {
@@ -107,7 +109,7 @@ class Router {
107
109
  if (this.shouldConsume && process.env.NODE_ENV !== 'test') {
108
110
  this.logger.error(`stream-consume-message-error`, { err, stream, group, consumer });
109
111
  this.errorCount++;
110
- const timeout = Math.min(GRADUATED_INTERVAL_MS * (2 ** this.errorCount), MAX_TIMEOUT_MS);
112
+ const timeout = Math.min(HMSH_GRADUATED_INTERVAL_MS * (2 ** this.errorCount), HMSH_MAX_TIMEOUT_MS);
111
113
  setTimeout(consume.bind(this), timeout);
112
114
  }
113
115
  }
@@ -129,7 +131,7 @@ class Router {
129
131
  telemetry.startStreamSpan(input, this.role);
130
132
  output = await this.execStreamLeg(input, stream, id, callback.bind(this));
131
133
  if (output?.status === StreamStatus.ERROR) {
132
- telemetry.setStreamError(`Function Status Code ${ output.code || STATUS_CODE_UNKNOWN }`);
134
+ telemetry.setStreamError(`Function Status Code ${ output.code || HMSH_CODE_UNKNOWN }`);
133
135
  }
134
136
  this.errorCount = 0;
135
137
  } catch (err) {
@@ -191,7 +193,7 @@ class Router {
191
193
  const errorCode = output.code.toString();
192
194
  const policy = policies?.[errorCode];
193
195
  const maxRetries = policy?.[0];
194
- const tryCount = Math.min(input.metadata.try || 0, MAX_RETRIES);
196
+ const tryCount = Math.min(input.metadata.try || 0, HMSH_MAX_RETRIES);
195
197
  //only possible values for maxRetries are 1, 2, 3
196
198
  //only possible values for tryCount are 0, 1, 2
197
199
  if (maxRetries > tryCount) {
@@ -206,7 +208,7 @@ class Router {
206
208
  if (typeof err.message === 'string') {
207
209
  error.message = err.message;
208
210
  } else {
209
- error.message = STATUS_MESSAGE_UNKNOWN;
211
+ error.message = HMSH_STATUS_UNKNOWN;
210
212
  }
211
213
  if (typeof err.stack === 'string') {
212
214
  error.stack = err.stack;
@@ -216,7 +218,7 @@ class Router {
216
218
  }
217
219
  return {
218
220
  status: 'error',
219
- code: STATUS_CODE_UNKNOWN,
221
+ code: HMSH_CODE_UNKNOWN,
220
222
  metadata: { ...input.metadata, guid: guid() },
221
223
  data: error as StreamError
222
224
  } as StreamDataResponse;
@@ -224,7 +226,7 @@ class Router {
224
226
 
225
227
  structureUnacknowledgedError(input: StreamData) {
226
228
  const message = 'stream message max delivery count exceeded';
227
- const code = STATUS_CODE_UNACKED;
229
+ const code = HMSH_CODE_UNACKED;
228
230
  const data: StreamError = { message, code };
229
231
  const output: StreamDataResponse = {
230
232
  metadata: { ...input.metadata, guid: guid() },
@@ -238,9 +240,9 @@ class Router {
238
240
  }
239
241
 
240
242
  structureError(input: StreamData, output: StreamDataResponse): StreamDataResponse {
241
- const message = output.data?.message ? output.data?.message.toString() : STATUS_MESSAGE_UNKNOWN;
243
+ const message = output.data?.message ? output.data?.message.toString() : HMSH_STATUS_UNKNOWN;
242
244
  const statusCode = output.code || output.data?.code;
243
- const code = isNaN(statusCode as number) ? STATUS_CODE_UNKNOWN : parseInt(statusCode.toString());
245
+ const code = isNaN(statusCode as number) ? HMSH_CODE_UNKNOWN : parseInt(statusCode.toString());
244
246
  const data: StreamError = { message, code };
245
247
  if (typeof output.data?.error === 'object') {
246
248
  data.error = { ...output.data.error };
@@ -257,7 +259,7 @@ class Router {
257
259
  for (const instance of [...Router.instances]) {
258
260
  instance.stopConsuming();
259
261
  }
260
- await sleepFor(BLOCK_TIME_MS);
262
+ await sleepFor(HMSH_BLOCK_TIME_MS * 2);
261
263
  }
262
264
 
263
265
  async stopConsuming() {
@@ -281,7 +283,7 @@ class Router {
281
283
  this.logger.info(`stream-throttle-reset`, { delay: this.throttle, topic: this.topic });
282
284
  }
283
285
 
284
- async claimUnacknowledged(stream: string, group: string, consumer: string, idleTimeMs = this.reclaimDelay, limit = XPENDING_COUNT): Promise<[string, [string, string]][]> {
286
+ async claimUnacknowledged(stream: string, group: string, consumer: string, idleTimeMs = this.reclaimDelay, limit = HMSH_XPENDING_COUNT): Promise<[string, [string, string]][]> {
285
287
  let pendingMessages = [];
286
288
  const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit); //[[ '1688768134881-0', 'testConsumer1', 1017, 1 ]]
287
289
  for (const pendingMessageInfo of pendingMessagesInfo) {
@@ -310,7 +312,7 @@ class Router {
310
312
  // ii) corrupt hardware/network/transport/etc
311
313
  // 3b) system error: Redis unable to accept `xadd` request
312
314
  // 4c) system error: Redis unable to accept `xdel`/`xack` request
313
- this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: STATUS_CODE_UNACKED, count });
315
+ this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: HMSH_CODE_UNACKED, count });
314
316
  const streamData = reclaimedMessage[0]?.[1]?.[1];
315
317
 
316
318
  //fatal risk point 1 of 3): json is corrupt
@@ -131,6 +131,15 @@ class IORedisStoreService extends StoreService<RedisClientType, RedisMultiType>
131
131
  throw error;
132
132
  }
133
133
  }
134
+
135
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
136
+ try {
137
+ return await (multi || this.redisClient).xlen(key);
138
+ } catch (error) {
139
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
140
+ throw error;
141
+ }
142
+ }
134
143
  }
135
144
 
136
145
  export { IORedisStoreService };
@@ -42,6 +42,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
42
42
  rpush: 'RPUSH',
43
43
  xack: 'XACK',
44
44
  xdel: 'XDEL',
45
+ xlen: 'XLEN',
45
46
  };
46
47
  }
47
48
 
@@ -167,6 +168,21 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
167
168
  throw error;
168
169
  }
169
170
  }
171
+
172
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
173
+ try {
174
+ if (multi) {
175
+ multi.XLEN(key);
176
+ return multi;
177
+ } else {
178
+ return await this.redisClient.XLEN(key);
179
+ }
180
+ } catch (error) {
181
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
182
+ throw error;
183
+ }
184
+ }
185
+
170
186
  }
171
187
 
172
188
  export { RedisStoreService };
@@ -29,8 +29,9 @@ import { Transitions } from '../../types/transition';
29
29
  import { formatISODate, getSymKey } from '../../modules/utils';
30
30
  import { ReclaimedMessageType } from '../../types/stream';
31
31
  import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
32
- import { SCOUT_INTERVAL_SECONDS, STATUS_CODE_INTERRUPT } from '../../modules/enums';
32
+ import { HMSH_SCOUT_INTERVAL_SECONDS, HMSH_CODE_INTERRUPT } from '../../modules/enums';
33
33
  import { GetStateError } from '../../modules/errors';
34
+ import { WorkListTaskType } from '../../types/task';
34
35
 
35
36
  interface AbstractRedisClient {
36
37
  exec(): any;
@@ -118,6 +119,10 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
118
119
  id: string,
119
120
  multi?: U
120
121
  ): Promise<number|U>;
122
+ abstract xlen(
123
+ key: string,
124
+ multi?: U
125
+ ): Promise<number|U>;
121
126
 
122
127
  constructor(redisClient: T) {
123
128
  this.redisClient = redisClient;
@@ -173,7 +178,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
173
178
  * check for and process work items in the
174
179
  * time and signal task queues.
175
180
  */
176
- async reserveScoutRole(scoutType: 'time' | 'signal', delay = SCOUT_INTERVAL_SECONDS): Promise<boolean> {
181
+ async reserveScoutRole(scoutType: 'time' | 'signal', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
177
182
  const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
178
183
  const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
179
184
  if (this.isSuccessful(success)) {
@@ -384,16 +389,45 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
384
389
  }
385
390
 
386
391
  /**
387
- * Registers jobId with the originJobId that spawned it. In the future,
388
- * when originJobId is interrupted or expired, the items in the
389
- * list (added via RPUSH) are LPOPed. If origin was expired, then
390
- * LPOPed items from the list are likewise expired;
392
+ * Registers the job, `jobId`, with `originJobId`. In the future,
393
+ * when `originJobId` is interrupted/expired, the items in the
394
+ * list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
391
395
  */
392
- async setDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
396
+ async registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
393
397
  const privateMulti = multi || this.getMulti();
394
- const depParams = { appId: this.appId, jobId: originJobId };
395
- const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
396
- privateMulti[this.commands.rpush](depKey, `expire::${topic}::${gId}::${jobId}`);
398
+ const dependencyParams = {
399
+ appId: this.appId,
400
+ jobId: originJobId,
401
+ };
402
+ const depKey = this.mintKey(
403
+ KeyType.JOB_DEPENDENTS,
404
+ dependencyParams,
405
+ );
406
+ //tasks have '4' segments
407
+ const expireTask = `expire::${topic}::${gId}::${jobId}`;
408
+ privateMulti[this.commands.rpush](depKey, expireTask);
409
+ if (!multi) {
410
+ return await privateMulti.exec();
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Ensures a `hook signal` is delisted when its parent activity/job
416
+ * is interrupted/expired.
417
+ */
418
+ async registerSignalDependency(jobId: string, signalKey: string, multi? : U): Promise<any> {
419
+ const privateMulti = multi || this.getMulti();
420
+ const dependencyParams = { appId: this.appId, jobId };
421
+ const dependencyKey = this.mintKey(
422
+ KeyType.JOB_DEPENDENTS,
423
+ dependencyParams,
424
+ );
425
+ //tasks have '4' segments
426
+ const delistTask = `delist::signal::${jobId}::${signalKey}`;
427
+ privateMulti[this.commands.rpush](
428
+ dependencyKey,
429
+ delistTask,
430
+ );
397
431
  if (!multi) {
398
432
  return await privateMulti.exec();
399
433
  }
@@ -706,11 +740,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
706
740
 
707
741
  async setHookSignal(hook: HookSignal, multi?: U): Promise<any> {
708
742
  const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
709
- const { topic, resolved, jobId} = hook;
710
- const payload = {
711
- [`${topic}:${resolved}`]: jobId
712
- };
713
- return await (multi || this.redisClient)[this.commands.hset](key, payload);
743
+ const { topic, resolved, jobId} = hook; //`${activityId}::${dad}::${gId}::${jobId}`
744
+ const signalKey = `${topic}:${resolved}`;
745
+ const payload = { [signalKey]: jobId };
746
+ await (multi || this.redisClient)[this.commands.hset](key, payload);
747
+ return await this.registerSignalDependency(jobId.split('::')[3], signalKey, multi);
714
748
  }
715
749
 
716
750
  async getHookSignal(topic: string, resolved: string): Promise<string | undefined> {
@@ -778,7 +812,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
778
812
  * expired at a future date; options indicate whether this
779
813
  * is a standard `expire` or an `interrupt`
780
814
  */
781
- async registerExpireJob(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
815
+ async registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
782
816
  const depParams = { appId: this.appId, jobId };
783
817
  const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
784
818
  const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
@@ -793,7 +827,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
793
827
  * for the given sleep group. Sleep groups are
794
828
  * organized into 'n'-second blocks (LISTS))
795
829
  */
796
- async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
830
+ async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, multi?: U): Promise<void> {
797
831
  const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
798
832
  const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
799
833
  const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
@@ -803,21 +837,25 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
803
837
  }
804
838
  }
805
839
 
806
- async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | boolean> {
807
- const existing = Boolean(listKey);
840
+ async getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean> {
808
841
  const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
809
842
  listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
810
843
  if (listKey) {
811
- const [pType, pKey] = this.resolveKeyContext(listKey);
844
+ let [pType, pKey] = this.resolveTaskKeyContext(listKey);
812
845
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
813
846
  if (timeEvent) {
814
- //there are 3 time-related event triggers: sleep, expire, interrupt
815
- const [_type, activityId, gId, ...jobId] = timeEvent.split('::');
847
+ //there are 4 time-related task
848
+ //1) sleep (awaken), 2) expire, 3) interrupt, 4) delist
849
+ const [type, activityId, gId, ...jobId] = timeEvent.split('::');
850
+ if (type === 'delist') {
851
+ pType = 'delist';
852
+ }
816
853
  return [listKey, jobId.join('::'), gId, activityId, pType];
817
854
  }
818
855
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
856
+ return true;
819
857
  }
820
- return existing;
858
+ return false;
821
859
  }
822
860
 
823
861
  /**
@@ -828,7 +866,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
828
866
  * generic LIST (lists typically contain target job ids)
829
867
  * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
830
868
  */
831
- resolveKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'), string] {
869
+ resolveTaskKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'|'delist'), string] {
832
870
  if (listKey.startsWith('::INTERRUPT')) {
833
871
  return ['interrupt', listKey.split('::')[2]];
834
872
  } else if (listKey.startsWith('::EXPIRE')) {
@@ -870,7 +908,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
870
908
 
871
909
  //persists the standard 410 error (job is `gone`)
872
910
  const err = JSON.stringify({
873
- code: STATUS_CODE_INTERRUPT,
911
+ code: HMSH_CODE_INTERRUPT,
874
912
  message: options.reason ?? `job [${jobId}] interrupted`,
875
913
  job_id: jobId
876
914
  });
@@ -33,16 +33,16 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
33
33
  if (mkStream === 'MKSTREAM') {
34
34
  try {
35
35
  return (await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK';
36
- } catch (err) {
36
+ } catch (error) {
37
37
  this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
38
- throw err;
38
+ throw error;
39
39
  }
40
40
  } else {
41
41
  try {
42
42
  return (await this.redisClient.xgroup(command, key, groupName, id)) === 'OK';
43
- } catch (err) {
43
+ } catch (error) {
44
44
  this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
45
- throw err;
45
+ throw error;
46
46
  }
47
47
  }
48
48
  }
@@ -50,9 +50,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
50
50
  async xadd(key: string, id: string, messageId: string, messageValue: string, multi?: RedisMultiType): Promise<string | RedisMultiType> {
51
51
  try {
52
52
  return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
53
- } catch (err) {
54
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
55
- throw err;
53
+ } catch (error) {
54
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
55
+ throw error;
56
56
  }
57
57
  }
58
58
 
@@ -79,9 +79,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
79
79
  streamName,
80
80
  id
81
81
  );
82
- } catch (err) {
83
- this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, err);
84
- throw err;
82
+ } catch (error) {
83
+ this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, { error });
84
+ throw error;
85
85
  }
86
86
  }
87
87
 
@@ -101,12 +101,12 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
101
101
  if (consumer) args.push(consumer);
102
102
  try {
103
103
  return await this.redisClient.call('XPENDING', ...args) as [string, string, number, number][];
104
- } catch (err) {
105
- this.logger.error('err, args', err, args);
104
+ } catch (error) {
105
+ this.logger.error('err, args', { error }, args);
106
106
  }
107
- } catch (err) {
108
- this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, err);
109
- throw err;
107
+ } catch (error) {
108
+ this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
109
+ throw error;
110
110
  }
111
111
  }
112
112
 
@@ -120,27 +120,36 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
120
120
  ): Promise<ReclaimedMessageType> {
121
121
  try {
122
122
  return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args) as unknown as ReclaimedMessageType;
123
- } catch (err) {
124
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
125
- throw err;
123
+ } catch (error) {
124
+ this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
125
+ throw error;
126
126
  }
127
127
  }
128
128
 
129
129
  async xack(key: string, group: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
130
130
  try {
131
131
  return await (multi || this.redisClient).xack(key, group, id);
132
- } catch (err) {
133
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
134
- throw err;
132
+ } catch (error) {
133
+ this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
134
+ throw error;
135
135
  }
136
136
  }
137
137
 
138
138
  async xdel(key: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
139
139
  try {
140
140
  return await (multi || this.redisClient).xdel(key, id);
141
- } catch (err) {
142
- this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, err);
143
- throw err;
141
+ } catch (error) {
142
+ this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
148
+ try {
149
+ return await (multi || this.redisClient).xlen(key);
150
+ } catch (error) {
151
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
152
+ throw error;
144
153
  }
145
154
  }
146
155
  }
@@ -139,6 +139,20 @@ class RedisStreamService extends StreamService<RedisClientType, RedisMultiType>
139
139
  throw err;
140
140
  }
141
141
  }
142
+
143
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
144
+ try {
145
+ if (multi) {
146
+ multi.XLEN(key);
147
+ return multi;
148
+ } else {
149
+ return await this.redisClient.XLEN(key);
150
+ }
151
+ } catch (error) {
152
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
153
+ throw error;
154
+ }
155
+ }
142
156
  }
143
157
 
144
158
  export { RedisStreamService };
@@ -52,6 +52,7 @@ abstract class StreamService<T, U> {
52
52
  ...args: string[]): Promise<ReclaimedMessageType>;
53
53
  abstract xack(key: string, group: string, id: string, multi?: U): Promise<number|U>;
54
54
  abstract xdel(key: string, id: string, multi?: U): Promise<number|U>;
55
+ abstract xlen(key: string, multi?: U): Promise<number|U>;
55
56
  }
56
57
 
57
58
  export { StreamService };
@@ -1,14 +1,16 @@
1
1
  import {
2
- EXPIRE_DURATION,
3
- FIDELITY_SECONDS,
4
- SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
2
+ HMSH_EXPIRE_DURATION,
3
+ HMSH_FIDELITY_SECONDS,
4
+ HMSH_SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
5
5
  import { XSleepFor, sleepFor } from '../../modules/utils';
6
6
  import { ILogger } from '../logger';
7
+ import { Pipe } from '../pipe';
7
8
  import { StoreService } from '../store';
8
9
  import { HookInterface, HookRule, HookSignal } from '../../types/hook';
10
+ import { KeyType } from '../../types/hotmesh';
9
11
  import { JobCompletionOptions, JobState } from '../../types/job';
10
12
  import { RedisClient, RedisMulti } from '../../types/redis';
11
- import { Pipe } from '../pipe';
13
+ import { WorkListTaskType } from '../../types/task';
12
14
 
13
15
  class TaskService {
14
16
  store: StoreService<RedisClient, RedisMulti>;
@@ -35,7 +37,12 @@ class TaskService {
35
37
  //todo: don't use 'id', make configurable using hook rule
36
38
  await hookEventCallback(topic, { ...data, id: jobId });
37
39
  } else {
38
- await this.store.deleteProcessedTaskQueue(workItemKey, sourceKey, destinationKey, scrub === 'true');
40
+ await this.store.deleteProcessedTaskQueue(
41
+ workItemKey,
42
+ sourceKey,
43
+ destinationKey,
44
+ scrub === 'true'
45
+ );
39
46
  }
40
47
  setImmediate(() => this.processWebHooks(hookEventCallback));
41
48
  }
@@ -45,21 +52,45 @@ class TaskService {
45
52
  await this.store.addTaskQueues(keys);
46
53
  }
47
54
 
48
- async registerJobForCleanup(jobId: string, inSeconds = EXPIRE_DURATION, options: JobCompletionOptions): Promise<void> {
55
+ async registerJobForCleanup(jobId: string, inSeconds = HMSH_EXPIRE_DURATION, options: JobCompletionOptions): Promise<void> {
49
56
  if (inSeconds > 0) {
50
57
  await this.store.expireJob(jobId, inSeconds);
51
- const expireTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
52
- await this.store.registerExpireJob(jobId, expireTimeSlot, options);
58
+ const fromNow = Date.now() + (inSeconds * 1000);
59
+ const fidelityMS = HMSH_FIDELITY_SECONDS * 1000;
60
+ const timeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
61
+ await this.store.registerDependenciesForCleanup(
62
+ jobId,
63
+ timeSlot,
64
+ options,
65
+ );
53
66
  }
54
67
  }
55
68
 
56
- async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
57
- const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
58
- await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
69
+ async registerTimeHook(
70
+ jobId: string,
71
+ gId: string,
72
+ activityId: string,
73
+ type: WorkListTaskType,
74
+ inSeconds = HMSH_FIDELITY_SECONDS,
75
+ multi?: RedisMulti,
76
+ ): Promise<void> {
77
+ const fromNow = Date.now() + (inSeconds * 1000);
78
+ const fidelityMS = HMSH_FIDELITY_SECONDS * 1000;
79
+ const awakenTimeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
80
+ await this.store.registerTimeHook(
81
+ jobId,
82
+ gId,
83
+ activityId,
84
+ type,
85
+ awakenTimeSlot,
86
+ multi,
87
+ );
59
88
  }
60
89
 
61
90
  /**
62
- * Should this engine instance play the role of 'scout' for the quorum.
91
+ * Should this engine instance play the role of 'scout' on behalf
92
+ * of the entire quorum? The scout role is responsible for processing
93
+ * task lists on behalf of the collective.
63
94
  */
64
95
  async shouldScout() {
65
96
  const wasScout = this.isScout;
@@ -68,30 +99,41 @@ class TaskService {
68
99
  if (!wasScout) {
69
100
  setTimeout(() => {
70
101
  this.isScout = false;
71
- }, SCOUT_INTERVAL_SECONDS * 1_000);
102
+ }, HMSH_SCOUT_INTERVAL_SECONDS * 1_000);
72
103
  }
73
104
  return true;
74
105
  }
75
106
  return false;
76
107
  }
77
108
 
78
- async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
109
+ /**
110
+ * Callback handler that takes an item from a work list and
111
+ * processes according to its type
112
+ */
113
+ async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>, listKey?: string): Promise<void> {
79
114
  if (await this.shouldScout()) {
80
115
  try {
81
- const timeJob = await this.store.getNextTimeJob(listKey);
82
- if (Array.isArray(timeJob)) {
83
- //a queue had a job; try again immediately
84
- const [listKey, jobId, gId, activityId, type] = timeJob;
85
- await timeEventCallback(jobId, gId, activityId, type);
116
+ const workListTask = await this.store.getNextTask(listKey);
117
+
118
+ if (Array.isArray(workListTask)) {
119
+ const [listKey, target, gId, activityId, type] = workListTask;
120
+ if (type === 'delist') {
121
+ //delist the signalKey (target)
122
+ const key = this.store.mintKey(KeyType.SIGNALS, { appId: this.store.appId });
123
+ await this.store.redisClient[this.store.commands.hdel](key, target);
124
+ } else {
125
+ //awaken/expire/interrupt
126
+ await timeEventCallback(target, gId, activityId, type);
127
+ }
86
128
  await sleepFor(0);
87
129
  this.processTimeHooks(timeEventCallback, listKey);
88
- } else if (timeJob) {
89
- //a queue was just emptied; try again immediately
130
+ } else if (workListTask) {
131
+ //a worklist was just emptied; try again immediately
90
132
  await sleepFor(0);
91
133
  this.processTimeHooks(timeEventCallback);
92
134
  } else {
93
- //all queues are empty; sleep before checking
94
- let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
135
+ //no worklists exist; sleep before checking
136
+ let sleep = XSleepFor(HMSH_FIDELITY_SECONDS * 1000);
95
137
  this.cleanupTimeout = sleep.timerId;
96
138
  await sleep.promise;
97
139
  this.processTimeHooks(timeEventCallback);
@@ -102,7 +144,7 @@ class TaskService {
102
144
  }
103
145
  } else {
104
146
  //didn't get the scout role; try again in 'one-ish' minutes
105
- let sleep = XSleepFor(SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
147
+ let sleep = XSleepFor(HMSH_SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
106
148
  this.cleanupTimeout = sleep.timerId;
107
149
  await sleep.promise;
108
150
  this.processTimeHooks(timeEventCallback);