@hotmeshio/hotmesh 0.0.36 → 0.0.37

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 (67) hide show
  1. package/README.md +11 -11
  2. package/build/modules/enums.d.ts +1 -0
  3. package/build/modules/enums.js +3 -1
  4. package/build/modules/errors.d.ts +9 -1
  5. package/build/modules/errors.js +12 -1
  6. package/build/modules/key.d.ts +20 -19
  7. package/build/modules/key.js +20 -20
  8. package/build/package.json +1 -1
  9. package/build/services/activities/activity.d.ts +10 -0
  10. package/build/services/activities/activity.js +28 -3
  11. package/build/services/activities/await.js +10 -9
  12. package/build/services/activities/cycle.js +10 -9
  13. package/build/services/activities/hook.d.ts +7 -1
  14. package/build/services/activities/hook.js +61 -44
  15. package/build/services/activities/interrupt.js +10 -9
  16. package/build/services/activities/signal.js +7 -7
  17. package/build/services/activities/trigger.js +4 -2
  18. package/build/services/activities/worker.js +9 -8
  19. package/build/services/durable/meshos.js +2 -2
  20. package/build/services/durable/worker.js +2 -2
  21. package/build/services/durable/workflow.js +17 -17
  22. package/build/services/engine/index.d.ts +5 -7
  23. package/build/services/engine/index.js +53 -47
  24. package/build/services/hotmesh/index.js +3 -3
  25. package/build/services/{signaler/stream.d.ts → router/index.d.ts} +3 -3
  26. package/build/services/{signaler/stream.js → router/index.js} +6 -6
  27. package/build/services/serializer/index.js +1 -1
  28. package/build/services/store/index.d.ts +9 -4
  29. package/build/services/store/index.js +21 -10
  30. package/build/services/task/index.d.ts +13 -4
  31. package/build/services/task/index.js +115 -17
  32. package/build/services/telemetry/index.js +6 -6
  33. package/build/services/worker/index.d.ts +3 -3
  34. package/build/services/worker/index.js +8 -8
  35. package/build/types/job.d.ts +2 -0
  36. package/build/types/stream.d.ts +1 -0
  37. package/modules/enums.ts +3 -0
  38. package/modules/errors.ts +18 -0
  39. package/modules/key.ts +21 -20
  40. package/package.json +1 -1
  41. package/services/activities/activity.ts +44 -4
  42. package/services/activities/await.ts +14 -10
  43. package/services/activities/cycle.ts +14 -10
  44. package/services/activities/hook.ts +70 -47
  45. package/services/activities/interrupt.ts +13 -10
  46. package/services/activities/signal.ts +11 -8
  47. package/services/activities/trigger.ts +5 -1
  48. package/services/activities/worker.ts +13 -9
  49. package/services/durable/meshos.ts +1 -1
  50. package/services/durable/worker.ts +1 -1
  51. package/services/durable/workflow.ts +1 -1
  52. package/services/engine/index.ts +82 -44
  53. package/services/hotmesh/index.ts +3 -3
  54. package/services/{signaler/stream.ts → router/index.ts} +5 -5
  55. package/services/serializer/index.ts +1 -1
  56. package/services/store/index.ts +23 -12
  57. package/services/task/index.ts +120 -21
  58. package/services/telemetry/index.ts +6 -6
  59. package/services/worker/index.ts +7 -7
  60. package/types/job.ts +2 -0
  61. package/types/stream.ts +6 -5
  62. package/build/services/signaler/store.d.ts +0 -15
  63. package/build/services/signaler/store.js +0 -68
  64. package/services/signaler/store.ts +0 -76
  65. /package/build/{services/durable/asyncLocalStorage.d.ts → modules/storage.d.ts} +0 -0
  66. /package/build/{services/durable/asyncLocalStorage.js → modules/storage.js} +0 -0
  67. /package/{services/durable/asyncLocalStorage.ts → modules/storage.ts} +0 -0
@@ -4,7 +4,7 @@ import {
4
4
  STATUS_CODE_SUCCESS,
5
5
  STATUS_CODE_PENDING,
6
6
  STATUS_CODE_TIMEOUT,
7
- DURABLE_EXPIRE_SECONDS} from '../../modules/enums';
7
+ DURABLE_EXPIRE_SECONDS } from '../../modules/enums';
8
8
  import {
9
9
  formatISODate,
10
10
  getSubscriptionTopic,
@@ -23,9 +23,8 @@ import { Trigger } from '../activities/trigger';
23
23
  import { CompilerService } from '../compiler';
24
24
  import { ILogger } from '../logger';
25
25
  import { ReporterService } from '../reporter';
26
+ import { Router } from '../router';
26
27
  import { SerializerService } from '../serializer';
27
- import { StoreSignaler } from '../signaler/store';
28
- import { StreamSignaler } from '../signaler/stream';
29
28
  import { StoreService } from '../store';
30
29
  import { RedisStoreService as RedisStore } from '../store/clients/redis';
31
30
  import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
@@ -84,12 +83,11 @@ class EngineService {
84
83
  apps: HotMeshApps | null;
85
84
  appId: string;
86
85
  guid: string;
86
+ router: Router | null;
87
87
  store: StoreService<RedisClient, RedisMulti> | null;
88
88
  stream: StreamService<RedisClient, RedisMulti> | null;
89
89
  subscribe: SubService<RedisClient, RedisMulti> | null;
90
- storeSignaler: StoreSignaler | null;
91
- streamSignaler: StreamSignaler | null;
92
- task: TaskService | null;
90
+ taskService: TaskService | null;
93
91
  logger: ILogger;
94
92
  cacheMode: CacheMode = 'cache';
95
93
  untilVersion: string | null = null;
@@ -110,9 +108,9 @@ class EngineService {
110
108
  await instance.initStoreChannel(config.engine.store);
111
109
  await instance.initSubChannel(config.engine.sub);
112
110
  await instance.initStreamChannel(config.engine.stream);
113
- instance.streamSignaler = instance.initStreamSignaler(config);
111
+ instance.router = instance.initRouter(config);
114
112
 
115
- instance.streamSignaler.consumeMessages(
113
+ instance.router.consumeMessages(
116
114
  instance.stream.mintKey(
117
115
  KeyType.STREAMS,
118
116
  { appId: instance.appId },
@@ -122,12 +120,8 @@ class EngineService {
122
120
  instance.processStreamMessage.bind(instance)
123
121
  );
124
122
 
125
- //the storeSignaler service is used by the engine to create `webhooks`
126
- //todo: unify/move to the task service (it manages all `signal` types)
127
- instance.storeSignaler = new StoreSignaler(instance.store, logger);
128
-
129
123
  //the task service is used by the engine to process `webhooks` and `timehooks`
130
- instance.task = new TaskService(instance.store, logger);
124
+ instance.taskService = new TaskService(instance.store, logger);
131
125
 
132
126
  return instance;
133
127
  }
@@ -181,8 +175,8 @@ class EngineService {
181
175
  );
182
176
  }
183
177
 
184
- initStreamSignaler(config: HotMeshConfig): StreamSignaler {
185
- return new StreamSignaler(
178
+ initRouter(config: HotMeshConfig): Router {
179
+ return new Router(
186
180
  {
187
181
  namespace: this.namespace,
188
182
  appId: this.appId,
@@ -231,21 +225,20 @@ class EngineService {
231
225
  }
232
226
 
233
227
  async processWebHooks() {
234
- this.task.processWebHooks((this.hook).bind(this));
228
+ this.taskService.processWebHooks((this.hook).bind(this));
235
229
  }
236
230
 
237
231
  async processTimeHooks() {
238
- this.task.processTimeHooks((this.hookTime).bind(this));
232
+ this.taskService.processTimeHooks((this.hookTime).bind(this));
239
233
  }
240
234
 
241
235
  async throttle(delayInMillis: number) {
242
- this.streamSignaler.setThrottle(delayInMillis);
236
+ this.router.setThrottle(delayInMillis);
243
237
  }
244
238
 
245
239
  // ************* METADATA/MODEL METHODS *************
246
240
  async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Await|Cycle|Hook|Signal|Trigger|Worker|Interrupt> {
247
241
  const [activityId, schema] = await this.getSchema(topic);
248
- polyfill
249
242
  const ActivityHandler = Activities[polyfill.resolveActivityType(schema.type)];
250
243
  if (ActivityHandler) {
251
244
  const utc = formatISODate(new Date());
@@ -329,6 +322,7 @@ class EngineService {
329
322
  async processStreamMessage(streamData: StreamDataResponse): Promise<void> {
330
323
  this.logger.debug('engine-process-stream-message', {
331
324
  jid: streamData.metadata.jid,
325
+ gid: streamData.metadata.gid,
332
326
  dad: streamData.metadata.dad,
333
327
  aid: streamData.metadata.aid,
334
328
  status: streamData.status || StreamStatus.SUCCESS,
@@ -338,41 +332,83 @@ class EngineService {
338
332
  const context: PartialJobState = {
339
333
  metadata: {
340
334
  jid: streamData.metadata.jid,
335
+ gid: streamData.metadata.gid,
341
336
  dad: streamData.metadata.dad,
342
337
  aid: streamData.metadata.aid,
343
338
  },
344
339
  data: streamData.data,
345
340
  };
346
- if (streamData.type === StreamDataType.TIMEHOOK || streamData.type === StreamDataType.WEBHOOK || streamData.type === StreamDataType.TRANSITION) {
347
- const activityHandler = await this.initActivity(`.${streamData.metadata.aid}`, context.data, context as JobState) as Hook;
348
- if (streamData.type === StreamDataType.TIMEHOOK) {
349
- await activityHandler.processTimeHookEvent(streamData.metadata.jid);
350
- } else if (streamData.type === StreamDataType.TRANSITION) {
351
- await activityHandler.process();
352
- } else {
353
- //a 202 code keeps the hook alive (hooks are single-use by default)
354
- await activityHandler.processWebHookEvent(streamData.status, streamData.code);
355
- }
341
+ if (streamData.type === StreamDataType.TIMEHOOK) {
342
+ //TIMEHOOK AWAKEN
343
+ const activityHandler = await this.initActivity(
344
+ `.${streamData.metadata.aid}`,
345
+ context.data,
346
+ context as JobState,
347
+ ) as Hook;
348
+ await activityHandler.processTimeHookEvent(streamData.metadata.jid);
349
+ } else if (streamData.type === StreamDataType.WEBHOOK) {
350
+ //WEBHOOK AWAKEN (SIGNAL IN)
351
+ const activityHandler = await this.initActivity(
352
+ `.${streamData.metadata.aid}`,
353
+ context.data,
354
+ context as JobState,
355
+ ) as Hook;
356
+ await activityHandler.processWebHookEvent(
357
+ streamData.status,
358
+ streamData.code
359
+ );
360
+ } else if (streamData.type === StreamDataType.TRANSITION) {
361
+ //TRANSITION (ADJACENT ACTIVITY)
362
+ const activityHandler = await this.initActivity(
363
+ `.${streamData.metadata.aid}`,
364
+ context.data,
365
+ context as JobState,
366
+ ) as Hook; //todo: `as Activity` (type is more generic)
367
+ await activityHandler.process();
356
368
  } else if (streamData.type === StreamDataType.AWAIT) {
369
+ //TRIGGER JOB
357
370
  context.metadata = {
358
371
  ...context.metadata,
359
372
  pj: streamData.metadata.jid,
373
+ pg: streamData.metadata.gid,
360
374
  pd: streamData.metadata.dad,
361
375
  pa: streamData.metadata.aid,
362
376
  trc: streamData.metadata.trc,
363
377
  spn: streamData.metadata.spn,
364
378
  };
365
- const activityHandler = await this.initActivity(streamData.metadata.topic, streamData.data, context as JobState) as Trigger;
379
+ const activityHandler = await this.initActivity(
380
+ streamData.metadata.topic,
381
+ streamData.data,
382
+ context as JobState
383
+ ) as Trigger;
366
384
  await activityHandler.process();
367
385
  } else if (streamData.type === StreamDataType.RESULT) {
368
- const activityHandler = await this.initActivity(`.${context.metadata.aid}`, streamData.data, context as JobState) as Await;
369
- await activityHandler.processEvent(streamData.status, streamData.code);
386
+ //AWAIT RESULT
387
+ const activityHandler = await this.initActivity(
388
+ `.${context.metadata.aid}`,
389
+ streamData.data,
390
+ context as JobState,
391
+ ) as Await;
392
+ await activityHandler.processEvent(
393
+ streamData.status,
394
+ streamData.code,
395
+ );
370
396
  } else {
371
- const activityHandler = await this.initActivity(`.${streamData.metadata.aid}`, streamData.data, context as JobState) as Worker;
372
- await activityHandler.processEvent(streamData.status, streamData.code, 'output');
397
+ //WORKER RESULT
398
+ const activityHandler = await this.initActivity(
399
+ `.${streamData.metadata.aid}`,
400
+ streamData.data,
401
+ context as JobState,
402
+ ) as Worker;
403
+ await activityHandler.processEvent(
404
+ streamData.status,
405
+ streamData.code,
406
+ 'output'
407
+ );
373
408
  }
374
409
  this.logger.debug('engine-process-stream-message-end', {
375
410
  jid: streamData.metadata.jid,
411
+ gid: streamData.metadata.gid,
376
412
  aid: streamData.metadata.aid
377
413
  });
378
414
  }
@@ -387,6 +423,7 @@ class EngineService {
387
423
  metadata: {
388
424
  guid: guid(),
389
425
  jid: context.metadata.pj,
426
+ gid: context.metadata.pg,
390
427
  dad: context.metadata.pd,
391
428
  aid: context.metadata.pa,
392
429
  trc: context.metadata.trc,
@@ -406,7 +443,7 @@ class EngineService {
406
443
  streamData.status = StreamStatus.SUCCESS;
407
444
  streamData.code = STATUS_CODE_SUCCESS;
408
445
  }
409
- return (await this.streamSignaler?.publishMessage(null, streamData)) as string;
446
+ return (await this.router?.publishMessage(null, streamData)) as string;
410
447
  }
411
448
  }
412
449
  hasParentJob(context: JobState): boolean {
@@ -437,7 +474,7 @@ class EngineService {
437
474
 
438
475
  // ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
439
476
  async hook(topic: string, data: JobData, status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<string> {
440
- const hookRule = await this.storeSignaler.getHookRule(topic);
477
+ const hookRule = await this.taskService.getHookRule(topic);
441
478
  const [aid] = await this.getSchema(`.${hookRule.to}`);
442
479
  const streamData: StreamData = {
443
480
  type: StreamDataType.WEBHOOK,
@@ -450,9 +487,9 @@ class EngineService {
450
487
  },
451
488
  data,
452
489
  };
453
- return await this.streamSignaler.publishMessage(null, streamData) as string;
490
+ return await this.router.publishMessage(null, streamData) as string;
454
491
  }
455
- async hookTime(jobId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
492
+ async hookTime(jobId: string, gId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
456
493
  if (type === 'interrupt') {
457
494
  return await this.interrupt(
458
495
  activityId, //note: 'activityId' is the actually job topic
@@ -470,16 +507,17 @@ class EngineService {
470
507
  metadata: {
471
508
  guid: guid(),
472
509
  jid: jobId,
473
- aid,
510
+ gid: gId,
474
511
  dad,
512
+ aid,
475
513
  },
476
514
  data: { timestamp: Date.now() },
477
515
  };
478
- await this.streamSignaler.publishMessage(null, streamData);
516
+ await this.router.publishMessage(null, streamData);
479
517
  }
480
518
  async hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
481
519
  const config = await this.getVID();
482
- const hookRule = await this.storeSignaler.getHookRule(hookTopic);
520
+ const hookRule = await this.taskService.getHookRule(hookTopic);
483
521
  if (hookRule) {
484
522
  const subscriptionTopic = await getSubscriptionTopic(hookRule.to, this.store, config)
485
523
  const resolvedQuery = await this.resolveQuery(subscriptionTopic, keyResolver);
@@ -597,7 +635,7 @@ class EngineService {
597
635
  }
598
636
  }
599
637
  async add(streamData: StreamData|StreamDataResponse): Promise<string> {
600
- return await this.streamSignaler.publishMessage(null, streamData) as string;
638
+ return await this.router.publishMessage(null, streamData) as string;
601
639
  }
602
640
 
603
641
  registerJobCallback(jobId: string, jobCallback: JobMessageCallback) {
@@ -632,7 +670,7 @@ class EngineService {
632
670
  this.pubPermSubs(context, jobOutput, options.emit);
633
671
  }
634
672
  if (!options.emit) {
635
- this.task.registerJobForCleanup(
673
+ this.taskService.registerJobForCleanup(
636
674
  context.metadata.jid,
637
675
  this.resolveExpires(context, options),
638
676
  options,
@@ -647,7 +685,7 @@ class EngineService {
647
685
  * it will be expired immediately.
648
686
  */
649
687
  resolveExpires(context: JobState, options: JobCompletionOptions): number {
650
- return context.metadata.expire ?? options.expire ?? DURABLE_EXPIRE_SECONDS;
688
+ return options.expire ?? context.metadata.expire ?? DURABLE_EXPIRE_SECONDS;
651
689
  }
652
690
 
653
691
 
@@ -4,8 +4,8 @@ import { RedisConnection } from '../connector/clients/redis';
4
4
  import { RedisConnection as IORedisConnection } from '../connector/clients/ioredis';
5
5
  import { EngineService } from '../engine';
6
6
  import { LoggerService, ILogger } from '../logger';
7
- import { StreamSignaler } from '../signaler/stream';
8
7
  import { QuorumService } from '../quorum';
8
+ import { Router } from '../router';
9
9
  import { WorkerService } from '../worker';
10
10
  import {
11
11
  JobState,
@@ -193,14 +193,14 @@ class HotMeshService {
193
193
  static async stop() {
194
194
  if (!this.disconnecting) {
195
195
  this.disconnecting = true;
196
- await StreamSignaler.stopConsuming();
196
+ await Router.stopConsuming();
197
197
  await RedisConnection.disconnectAll();
198
198
  await IORedisConnection.disconnectAll();
199
199
  }
200
200
  }
201
201
 
202
202
  stop() {
203
- this.engine?.task.cancelCleanup();
203
+ this.engine?.taskService.cancelCleanup();
204
204
  }
205
205
 
206
206
  async compress(terms: string[]): Promise<boolean> {
@@ -27,8 +27,8 @@ import {
27
27
  StreamStatus
28
28
  } from '../../types/stream';
29
29
 
30
- class StreamSignaler {
31
- static signalers: Set<StreamSignaler> = new Set();
30
+ class Router {
31
+ static instances: Set<Router> = new Set();
32
32
  appId: string;
33
33
  guid: string;
34
34
  role: StreamRole;
@@ -70,7 +70,7 @@ class StreamSignaler {
70
70
 
71
71
  async consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse|void>): Promise<void> {
72
72
  this.logger.info(`stream-consumer-starting`, { group, consumer, stream });
73
- StreamSignaler.signalers.add(this);
73
+ Router.instances.add(this);
74
74
  this.shouldConsume = true;
75
75
  await this.createGroup(stream, group);
76
76
  let lastCheckedPendingMessagesAt = Date.now();
@@ -254,7 +254,7 @@ class StreamSignaler {
254
254
  }
255
255
 
256
256
  static async stopConsuming() {
257
- for (const instance of [...StreamSignaler.signalers]) {
257
+ for (const instance of [...Router.instances]) {
258
258
  instance.stopConsuming();
259
259
  }
260
260
  await sleepFor(BLOCK_TIME_MS);
@@ -356,4 +356,4 @@ class StreamSignaler {
356
356
  }
357
357
  }
358
358
 
359
- export { StreamSignaler };
359
+ export { Router };
@@ -19,7 +19,7 @@ export const MDATA_SYMBOLS = {
19
19
  KEYS: ['au', 'err', 'l2s']
20
20
  },
21
21
  JOB: {
22
- KEYS: ['ngn', 'tpc', 'pj', 'pd', 'pa', 'key', 'app', 'vrs', 'jid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
22
+ KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
23
23
  },
24
24
  JOB_UPDATE: {
25
25
  KEYS: ['ju', 'err']
@@ -29,7 +29,7 @@ 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 { STATUS_CODE_INTERRUPT } from '../../modules/enums';
32
+ import { SCOUT_INTERVAL_SECONDS, STATUS_CODE_INTERRUPT } from '../../modules/enums';
33
33
  import { GetStateError } from '../../modules/errors';
34
34
 
35
35
  interface AbstractRedisClient {
@@ -168,10 +168,19 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
168
168
  this.cache.invalidate();
169
169
  }
170
170
 
171
- async reserveEngineId(engineId: string): Promise<boolean> {
172
- const key = this.mintKey(KeyType.ENGINE_ID, { engineId });
173
- const success = await this.redisClient[this.commands.setnx](key, 'id', 1);
174
- return this.isSuccessful(success);
171
+ /**
172
+ * At any given time only a single engine will
173
+ * check for and process work items in the
174
+ * time and signal task queues.
175
+ */
176
+ async reserveScoutRole(scoutType: 'time' | 'signal', delay = SCOUT_INTERVAL_SECONDS): Promise<boolean> {
177
+ const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
178
+ const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
179
+ if (this.isSuccessful(success)) {
180
+ await this.redisClient[this.commands.expire](key, delay - 1);
181
+ return true;
182
+ }
183
+ return false;
175
184
  }
176
185
 
177
186
  async getSettings(bCreate = false): Promise<HotMeshSettings> {
@@ -380,11 +389,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
380
389
  * list (added via RPUSH) are LPOPed. If origin was expired, then
381
390
  * LPOPed items from the list are likewise expired;
382
391
  */
383
- async setDependency(originJobId: string, topic: string, jobId: string, multi? : U): Promise<any> {
392
+ async setDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
384
393
  const privateMulti = multi || this.getMulti();
385
394
  const depParams = { appId: this.appId, jobId: originJobId };
386
395
  const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
387
- privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
396
+ privateMulti[this.commands.rpush](depKey, `expire::${topic}::${gId}::${jobId}`);
388
397
  if (!multi) {
389
398
  return await privateMulti.exec();
390
399
  }
@@ -784,9 +793,9 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
784
793
  * for the given sleep group. Sleep groups are
785
794
  * organized into 'n'-second blocks (LISTS))
786
795
  */
787
- async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
796
+ async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
788
797
  const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
789
- const timeEvent = `${type}::${activityId}::${jobId}`
798
+ const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
790
799
  const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
791
800
  if (multi || len === 1) {
792
801
  const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
@@ -794,7 +803,8 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
794
803
  }
795
804
  }
796
805
 
797
- async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | void> {
806
+ async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | boolean> {
807
+ const existing = Boolean(listKey);
798
808
  const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
799
809
  listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
800
810
  if (listKey) {
@@ -802,11 +812,12 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
802
812
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
803
813
  if (timeEvent) {
804
814
  //there are 3 time-related event triggers: sleep, expire, interrupt
805
- const [_type, activityId, ...jobId] = timeEvent.split('::');
806
- return [listKey, jobId.join('::'), activityId, pType];
815
+ const [_type, activityId, gId, ...jobId] = timeEvent.split('::');
816
+ return [listKey, jobId.join('::'), gId, activityId, pType];
807
817
  }
808
818
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
809
819
  }
820
+ return existing;
810
821
  }
811
822
 
812
823
  /**
@@ -1,17 +1,20 @@
1
1
  import {
2
2
  EXPIRE_DURATION,
3
- FIDELITY_SECONDS } from '../../modules/enums';
3
+ FIDELITY_SECONDS,
4
+ SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
4
5
  import { XSleepFor, sleepFor } from '../../modules/utils';
5
6
  import { ILogger } from '../logger';
6
7
  import { StoreService } from '../store';
7
- import { HookInterface } from '../../types/hook';
8
- import { JobCompletionOptions } from '../../types/job';
8
+ import { HookInterface, HookRule, HookSignal } from '../../types/hook';
9
+ import { JobCompletionOptions, JobState } from '../../types/job';
9
10
  import { RedisClient, RedisMulti } from '../../types/redis';
11
+ import { Pipe } from '../pipe';
10
12
 
11
13
  class TaskService {
12
14
  store: StoreService<RedisClient, RedisMulti>;
13
15
  logger: ILogger;
14
16
  cleanupTimeout: NodeJS.Timeout | null = null;
17
+ isScout: boolean = false;
15
18
 
16
19
  constructor(
17
20
  store: StoreService<RedisClient, RedisMulti>,
@@ -50,28 +53,59 @@ class TaskService {
50
53
  }
51
54
  }
52
55
 
53
- async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
56
+ async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
54
57
  const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
55
- await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
58
+ await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
56
59
  }
57
60
 
58
- async processTimeHooks(timeEventCallback: (jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
59
- try {
60
- const timeJob = await this.store.getNextTimeJob(listKey);
61
- if (timeJob) {
62
- const [listKey, jobId, activityId, type] = timeJob;
63
- await timeEventCallback(jobId, activityId, type);
64
- await sleepFor(0);
65
- this.processTimeHooks(timeEventCallback, listKey);
66
- } else {
67
- let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
68
- this.cleanupTimeout = sleep.timerId;
69
- await sleep.promise;
70
- this.processTimeHooks(timeEventCallback)
61
+ /**
62
+ * Should this engine instance play the role of 'scout' for the quorum.
63
+ */
64
+ async shouldScout() {
65
+ const wasScout = this.isScout;
66
+ const isScout = wasScout || (this.isScout = await this.store.reserveScoutRole('time'));
67
+ if (isScout) {
68
+ if (!wasScout) {
69
+ setTimeout(() => {
70
+ this.isScout = false;
71
+ }, SCOUT_INTERVAL_SECONDS * 1_000);
72
+ }
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+
78
+ async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
79
+ if (await this.shouldScout()) {
80
+ 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);
86
+ await sleepFor(0);
87
+ this.processTimeHooks(timeEventCallback, listKey);
88
+ } else if (timeJob) {
89
+ //a queue was just emptied; try again immediately
90
+ await sleepFor(0);
91
+ this.processTimeHooks(timeEventCallback);
92
+ } else {
93
+ //all queues are empty; sleep before checking
94
+ let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
95
+ this.cleanupTimeout = sleep.timerId;
96
+ await sleep.promise;
97
+ this.processTimeHooks(timeEventCallback);
98
+ }
99
+ } catch (err) {
100
+ //todo: retry connect to redis
101
+ this.logger.error('task-process-timehooks-error', err);
71
102
  }
72
- } catch (err) {
73
- //todo: retry connect to redis
74
- this.logger.error('task-process-timehooks-error', err);
103
+ } else {
104
+ //didn't get the scout role; try again in 'one-ish' minutes
105
+ let sleep = XSleepFor(SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
106
+ this.cleanupTimeout = sleep.timerId;
107
+ await sleep.promise;
108
+ this.processTimeHooks(timeEventCallback);
75
109
  }
76
110
  }
77
111
 
@@ -81,6 +115,71 @@ class TaskService {
81
115
  this.cleanupTimeout = undefined;
82
116
  }
83
117
  }
118
+
119
+ async getHookRule(topic: string): Promise<HookRule | undefined> {
120
+ const rules = await this.store.getHookRules();
121
+ return rules?.[topic]?.[0] as HookRule;
122
+ }
123
+
124
+ async registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string> {
125
+ const hookRule = await this.getHookRule(topic);
126
+ if (hookRule) {
127
+ const mapExpression = hookRule.conditions.match[0].expected;
128
+ const resolved = Pipe.resolve(mapExpression, context);
129
+ const jobId = context.metadata.jid;
130
+ const gId = context.metadata.gid;
131
+ const activityId = hookRule.to;
132
+ const hook: HookSignal = {
133
+ topic,
134
+ resolved,
135
+ jobId: `${activityId}::${dad}::${gId}::${jobId}`,
136
+ }
137
+ await this.store.setHookSignal(hook, multi);
138
+ return jobId;
139
+ } else {
140
+ throw new Error('signaler.registerWebHook:error: hook rule not found');
141
+ }
142
+ }
143
+
144
+ async processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string, string] | undefined> {
145
+ const hookRule = await this.getHookRule(topic);
146
+ if (hookRule) {
147
+ //NOTE: both formats are supported by the mapping engine:
148
+ // `$self.hook.data` OR `$hook.data`
149
+ const context = { $self: { hook: { data }}, $hook: { data }};
150
+ const mapExpression = hookRule.conditions.match[0].actual;
151
+ const resolved = Pipe.resolve(mapExpression, context);
152
+ const hookSignalId = await this.store.getHookSignal(topic, resolved);
153
+ if (!hookSignalId) {
154
+ //messages can be double-processed; not an issue; return undefined
155
+ //users can also provide a bogus topic; not an issue; return undefined
156
+ return undefined;
157
+ }
158
+ //`aid` is part of composit key, but the hook `topic` is its public interface;
159
+ // this means that a new version of the graph can be deployed and the
160
+ // topic can be re-mapped to a different activity id. Outside callers
161
+ // can adhere to the unchanged contract (calling the same topic),
162
+ // while the internal system can be updated in real time as necessary.
163
+ const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
164
+ return [jid.join('::'), hookRule.to, dad, gid];
165
+ } else {
166
+ throw new Error('signal-not-found');
167
+ }
168
+ }
169
+
170
+ async deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number> {
171
+ const hookRule = await this.getHookRule(topic);
172
+ if (hookRule) {
173
+ //NOTE: both formats are supported by the mapping engine:
174
+ // `$self.hook.data` OR `$hook.data`
175
+ const context = { $self: { hook: { data }}, $hook: { data }};
176
+ const mapExpression = hookRule.conditions.match[0].actual;
177
+ const resolved = Pipe.resolve(mapExpression, context);
178
+ return await this.store.deleteHookSignal(topic, resolved);
179
+ } else {
180
+ throw new Error('signaler.process:error: hook rule not found');
181
+ }
182
+ }
84
183
  }
85
184
 
86
185
  export { TaskService };
@@ -126,20 +126,20 @@ class TelemetryService {
126
126
  return result;
127
127
  }, {})
128
128
  };
129
- this.span.setAttributes(namespacedAtts as StringScalarType);
129
+ this.span?.setAttributes(namespacedAtts as StringScalarType);
130
130
  }
131
131
  }
132
132
 
133
133
  setActivityAttributes(attributes: StringScalarType): void {
134
- this.span.setAttributes(attributes);
134
+ this.span?.setAttributes(attributes);
135
135
  }
136
136
 
137
137
  setStreamAttributes(attributes: StringScalarType): void {
138
- this.span.setAttributes(attributes);
138
+ this.span?.setAttributes(attributes);
139
139
  }
140
140
 
141
141
  setJobAttributes(attributes: StringScalarType): void {
142
- this.jobSpan.setAttributes(attributes);
142
+ this.jobSpan?.setAttributes(attributes);
143
143
  }
144
144
 
145
145
  endJobSpan(): void {
@@ -216,11 +216,11 @@ class TelemetryService {
216
216
  }
217
217
 
218
218
  setActivityError(message: string) {
219
- this.span.setStatus({ code: SpanStatusCode.ERROR, message });
219
+ this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
220
220
  }
221
221
 
222
222
  setStreamError(message: string) {
223
- this.span.setStatus({ code: SpanStatusCode.ERROR, message });
223
+ this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
224
224
  }
225
225
 
226
226
  /**