@hotmeshio/hotmesh 0.0.49 → 0.0.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/build/modules/enums.d.ts +1 -0
  2. package/build/modules/enums.js +2 -1
  3. package/build/modules/key.d.ts +5 -1
  4. package/build/modules/key.js +10 -2
  5. package/build/package.json +2 -1
  6. package/build/services/activities/await.js +6 -0
  7. package/build/services/activities/hook.js +1 -1
  8. package/build/services/activities/trigger.d.ts +1 -0
  9. package/build/services/activities/trigger.js +23 -2
  10. package/build/services/durable/exporter.js +19 -5
  11. package/build/services/engine/index.d.ts +1 -1
  12. package/build/services/engine/index.js +12 -3
  13. package/build/services/exporter/index.js +3 -2
  14. package/build/services/hotmesh/index.js +4 -0
  15. package/build/services/quorum/index.d.ts +11 -2
  16. package/build/services/quorum/index.js +33 -0
  17. package/build/services/serializer/index.js +1 -1
  18. package/build/services/store/index.d.ts +5 -5
  19. package/build/services/store/index.js +43 -22
  20. package/build/services/task/index.d.ts +2 -1
  21. package/build/services/task/index.js +30 -13
  22. package/build/services/worker/index.d.ts +13 -2
  23. package/build/services/worker/index.js +44 -3
  24. package/build/types/activity.d.ts +1 -0
  25. package/build/types/exporter.d.ts +2 -0
  26. package/build/types/job.d.ts +1 -0
  27. package/build/types/quorum.d.ts +22 -8
  28. package/build/types/stream.d.ts +1 -0
  29. package/modules/enums.ts +1 -0
  30. package/modules/key.ts +7 -2
  31. package/package.json +2 -1
  32. package/services/activities/await.ts +6 -0
  33. package/services/activities/hook.ts +1 -0
  34. package/services/activities/trigger.ts +25 -1
  35. package/services/durable/exporter.ts +18 -7
  36. package/services/engine/index.ts +13 -5
  37. package/services/exporter/index.ts +3 -2
  38. package/services/hotmesh/index.ts +4 -0
  39. package/services/quorum/index.ts +38 -2
  40. package/services/serializer/index.ts +1 -1
  41. package/services/store/index.ts +51 -24
  42. package/services/task/index.ts +31 -11
  43. package/services/worker/index.ts +49 -5
  44. package/types/activity.ts +1 -0
  45. package/types/exporter.ts +2 -0
  46. package/types/job.ts +1 -0
  47. package/types/quorum.ts +28 -13
  48. package/types/stream.ts +1 -0
@@ -15,6 +15,7 @@ export declare const HMSH_CODE_DURABLE_MAXED = 597;
15
15
  export declare const HMSH_CODE_DURABLE_FATAL = 598;
16
16
  export declare const HMSH_CODE_DURABLE_RETRYABLE = 599;
17
17
  export declare const HMSH_STATUS_UNKNOWN = "unknown";
18
+ export declare const HMSH_QUORUM_ROLLCALL_CYCLES = 12;
18
19
  export declare const HMSH_QUORUM_DELAY_MS = 250;
19
20
  export declare const HMSH_ACTIVATION_MAX_RETRY = 3;
20
21
  export declare const HMSH_OTT_WAIT_TIME: number;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
3
+ exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
4
4
  // HOTMESH SYSTEM
5
5
  exports.HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL || 'info';
6
6
  // STATUS CODES AND MESSAGES
@@ -20,6 +20,7 @@ exports.HMSH_CODE_DURABLE_FATAL = 598;
20
20
  exports.HMSH_CODE_DURABLE_RETRYABLE = 599;
21
21
  exports.HMSH_STATUS_UNKNOWN = 'unknown';
22
22
  // QUORUM
23
+ exports.HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
23
24
  exports.HMSH_QUORUM_DELAY_MS = 250;
24
25
  exports.HMSH_ACTIVATION_MAX_RETRY = 3;
25
26
  // ENGINE
@@ -27,6 +27,10 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
27
27
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
28
28
  */
29
29
  declare const HMNS = "hmsh";
30
+ declare const KEYSEP = ":";
31
+ declare const VALSEP = "::";
32
+ declare const WEBSEP = "::";
33
+ declare const TYPSEP = "::";
30
34
  declare class KeyService {
31
35
  /**
32
36
  * returns a key that can be used to access a value in the key/value store
@@ -41,4 +45,4 @@ declare class KeyService {
41
45
  */
42
46
  static mintKey(namespace: string, keyType: KeyType, params: KeyStoreParams): string;
43
47
  }
44
- export { KeyService, KeyType, KeyStoreParams, HMNS };
48
+ export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HMNS = exports.KeyType = exports.KeyService = void 0;
3
+ exports.VALSEP = exports.WEBSEP = exports.TYPSEP = exports.KEYSEP = exports.HMNS = exports.KeyType = exports.KeyService = void 0;
4
4
  const hotmesh_1 = require("../types/hotmesh");
5
5
  Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () { return hotmesh_1.KeyType; } });
6
6
  /**
@@ -30,8 +30,16 @@ Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () {
30
30
  * hmsh:<appid>:sym:keys:<activityid|$subscribes> -> {hash} list of symbols based upon schema enums (initially) and adaptively optimized (later) during runtime; if '$subscribes' is used as the activityid, it is a top-level `job` symbol set (for job keys)
31
31
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
32
32
  */
33
- const HMNS = "hmsh"; //default
33
+ const HMNS = "hmsh";
34
34
  exports.HMNS = HMNS;
35
+ const KEYSEP = ':'; //default delimiter for keys
36
+ exports.KEYSEP = KEYSEP;
37
+ const VALSEP = '::'; //default delimiter for vals
38
+ exports.VALSEP = VALSEP;
39
+ const WEBSEP = '::'; //default delimiter for webhook vals
40
+ exports.WEBSEP = WEBSEP;
41
+ const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
42
+ exports.TYPSEP = TYPSEP;
35
43
  class KeyService {
36
44
  /**
37
45
  * returns a key that can be used to access a value in the key/value store
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.49",
3
+ "version": "0.0.50",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -28,6 +28,7 @@
28
28
  "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
29
29
  "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
30
30
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
31
+ "test:await": "NODE_ENV=test jest ./tests/functional/awaiter/index.test.ts --detectOpenHandles --forceExit --verbose",
31
32
  "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
32
33
  "test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
33
34
  "test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -78,6 +78,12 @@ class Await extends activity_1.Activity {
78
78
  type: stream_1.StreamDataType.AWAIT,
79
79
  data: this.context.data
80
80
  };
81
+ if (this.config.await !== true) {
82
+ const doAwait = pipe_1.Pipe.resolve(this.config.await, this.context);
83
+ if (doAwait === false) {
84
+ streamData.metadata.await = false;
85
+ }
86
+ }
81
87
  if (this.config.retry) {
82
88
  streamData.policies = {
83
89
  retry: this.config.retry
@@ -107,7 +107,7 @@ class Hook extends activity_1.Activity {
107
107
  }
108
108
  else if (this.config.sleep) {
109
109
  const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
110
- await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration);
110
+ await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '');
111
111
  return this.context.metadata.jid;
112
112
  }
113
113
  }
@@ -8,6 +8,7 @@ declare class Trigger extends Activity {
8
8
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
9
9
  process(): Promise<string>;
10
10
  setStatus(amount: number): Promise<void>;
11
+ execAdjacentParent(): Promise<void>;
11
12
  createInputContext(): Partial<JobState>;
12
13
  getState(): Promise<void>;
13
14
  bindJobMetadataPaths(): string[];
@@ -31,6 +31,9 @@ class Trigger extends activity_1.Activity {
31
31
  await this.setStats(multi);
32
32
  await this.registerJobDependency(multi);
33
33
  await multi.exec();
34
+ //if the parent (spawner) chose not to await,
35
+ // emit the job_id as the data payload { job_id }
36
+ this.execAdjacentParent();
34
37
  telemetry.mapActivityAttributes();
35
38
  const jobStatus = Number(this.context.metadata.js);
36
39
  telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
@@ -61,6 +64,11 @@ class Trigger extends activity_1.Activity {
61
64
  async setStatus(amount) {
62
65
  this.context.metadata.js = amount;
63
66
  }
67
+ async execAdjacentParent() {
68
+ if (this.context.metadata.px) {
69
+ await this.engine.execAdjacentParent(this.context, { metadata: this.context.metadata, data: { job_id: this.context.metadata.jid } });
70
+ }
71
+ }
64
72
  createInputContext() {
65
73
  const input = {
66
74
  [this.metadata.aid]: {
@@ -95,6 +103,7 @@ class Trigger extends activity_1.Activity {
95
103
  pg: this.context.metadata.pg,
96
104
  pd: this.context.metadata.pd,
97
105
  pa: this.context.metadata.pa,
106
+ px: this.context.metadata.px,
98
107
  app: id,
99
108
  vrs: version,
100
109
  tpc: this.config.subscribes,
@@ -165,10 +174,22 @@ class Trigger extends activity_1.Activity {
165
174
  }
166
175
  if (resolvedDepKey) {
167
176
  const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
168
- await this.store.registerJobDependency(isParentOrigin ? 'expire-child' : 'expire', resolvedDepKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, multi);
177
+ let type;
178
+ if (isParentOrigin) {
179
+ if (this.context.metadata.px) {
180
+ type = 'child';
181
+ }
182
+ else {
183
+ type = 'expire-child';
184
+ }
185
+ }
186
+ else {
187
+ type = 'expire';
188
+ }
189
+ await this.store.registerJobDependency(type, resolvedDepKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
169
190
  }
170
191
  if (resolvedAdjKey && resolvedAdjKey !== resolvedDepKey) {
171
- await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, multi);
192
+ await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
172
193
  }
173
194
  }
174
195
  async setStats(multi) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExporterService = void 0;
4
4
  const serializer_1 = require("../serializer");
5
5
  const utils_1 = require("../../modules/utils");
6
+ const key_1 = require("../../modules/key");
6
7
  /**
7
8
  * Downloads job data from Redis (hscan, hmget, hgetall)
8
9
  * Splits, Inflates, and Sorts the job data for use in durable contexts
@@ -94,13 +95,27 @@ class ExporterService {
94
95
  const activityName = item[1].split('/')[0];
95
96
  const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
96
97
  const timestamp = item[2];
97
- const event = {
98
+ let event = {
98
99
  activity: activityName,
99
100
  duplex: duplex,
100
101
  dimension: dimensions,
101
102
  timestamp,
103
+ created: timestamp,
104
+ updated: timestamp,
102
105
  };
103
- timeline.push(event);
106
+ const prior = timeline[timeline.length - 1];
107
+ if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
108
+ if (event.duplex === 'exit') {
109
+ prior.updated = event.timestamp;
110
+ }
111
+ else {
112
+ prior.created = event.timestamp;
113
+ }
114
+ event = prior;
115
+ }
116
+ else {
117
+ timeline.push(event);
118
+ }
104
119
  if (this.isMainEntry(item[1])) {
105
120
  event.actions = [];
106
121
  this.interleaveActions(actions.main, event.actions);
@@ -163,12 +178,11 @@ class ExporterService {
163
178
  * @returns - the organized dependency data
164
179
  */
165
180
  inflateDependencyData(data, actions) {
166
- //console.log('dependency data>', data);
167
181
  const hookReg = /([0-9,]+)-(\d+)$/;
168
182
  const flowReg = /-(\d+)$/;
169
183
  return data.map((dependency, index) => {
170
- const [action, topic, gid, ...jid] = dependency.split('::');
171
- const jobId = jid.join('::');
184
+ const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
185
+ const jobId = jid.join(key_1.VALSEP);
172
186
  const match = jobId.match(hookReg);
173
187
  let prefix;
174
188
  let type;
@@ -65,7 +65,7 @@ declare class EngineService {
65
65
  resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
66
66
  processStreamMessage(streamData: StreamDataResponse): Promise<void>;
67
67
  execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<string>;
68
- hasParentJob(context: JobState): boolean;
68
+ hasParentJob(context: JobState, checkSevered?: boolean): boolean;
69
69
  resolveError(metadata: JobMetadata): StreamError | undefined;
70
70
  interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
71
71
  scrub(jobId: string): Promise<void>;
@@ -259,6 +259,7 @@ class EngineService {
259
259
  pg: streamData.metadata.gid,
260
260
  pd: streamData.metadata.dad,
261
261
  pa: streamData.metadata.aid,
262
+ px: streamData.metadata.await === false,
262
263
  trc: streamData.metadata.trc,
263
264
  spn: streamData.metadata.spn,
264
265
  };
@@ -316,7 +317,10 @@ class EngineService {
316
317
  return (await this.router?.publishMessage(null, streamData));
317
318
  }
318
319
  }
319
- hasParentJob(context) {
320
+ hasParentJob(context, checkSevered = false) {
321
+ if (checkSevered) {
322
+ return Boolean(context.metadata.pj && context.metadata.pa && !context.metadata.px);
323
+ }
320
324
  return Boolean(context.metadata.pj && context.metadata.pa);
321
325
  }
322
326
  resolveError(metadata) {
@@ -385,7 +389,12 @@ class EngineService {
385
389
  const workItems = await reporter.getWorkItems(resolvedQuery, queryFacets);
386
390
  if (workItems.length) {
387
391
  const taskService = new task_1.TaskService(this.store, this.logger);
388
- await taskService.enqueueWorkItems(workItems.map(workItem => `${hookTopic}::${workItem}::${keyResolver.scrub || false}::${JSON.stringify(data)}`));
392
+ await taskService.enqueueWorkItems(workItems.map(workItem => [
393
+ hookTopic,
394
+ workItem,
395
+ keyResolver.scrub || false,
396
+ JSON.stringify(data)
397
+ ].join(key_1.VALSEP)));
389
398
  this.store.publish(key_1.KeyType.QUORUM, { type: 'work', originator: this.guid }, this.appId);
390
399
  }
391
400
  return workItems;
@@ -504,7 +513,7 @@ class EngineService {
504
513
  // ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
505
514
  async runJobCompletionTasks(context, options = {}) {
506
515
  //'emit' indicates the job is still active
507
- const isAwait = this.hasParentJob(context);
516
+ const isAwait = this.hasParentJob(context, true);
508
517
  const isOneTimeSub = this.hasOneTimeSubscription(context);
509
518
  const topic = await this.getPublishesTopic(context);
510
519
  let msgId;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExporterService = void 0;
4
4
  const serializer_1 = require("../serializer");
5
5
  const utils_1 = require("../../modules/utils");
6
+ const key_1 = require("../../modules/key");
6
7
  /**
7
8
  * Downloads job data from Redis (hscan, hmget, hgetall)
8
9
  * Expands process data and includes dependency list
@@ -87,8 +88,8 @@ class ExporterService {
87
88
  const hookReg = /([0-9,]+)-(\d+)$/;
88
89
  const flowReg = /-(\d+)$/;
89
90
  return data.map((dependency, index) => {
90
- const [action, topic, gid, ...jid] = dependency.split('::');
91
- const jobId = jid.join('::');
91
+ const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
92
+ const jobId = jid.join(key_1.VALSEP);
92
93
  const match = jobId.match(hookReg);
93
94
  let prefix;
94
95
  let type;
@@ -167,6 +167,10 @@ class HotMeshService {
167
167
  }
168
168
  stop() {
169
169
  this.engine?.taskService.cancelCleanup();
170
+ this.quorum?.stop();
171
+ this.workers?.forEach((worker) => {
172
+ worker.stop();
173
+ });
170
174
  }
171
175
  async compress(terms) {
172
176
  return await this.engine?.compress(terms);
@@ -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) {
@@ -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']
@@ -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,7 +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[]>;
155
+ findJobs(queryString?: string, limit?: number, batchSize?: number): Promise<[string, string[]]>;
156
156
  findJobFields(jobId: string, fieldMatchPattern?: string, limit?: number, batchSize?: number, cursor?: string): Promise<[string, StringStringType]>;
157
157
  }
158
158
  export { StoreService };
@@ -345,15 +345,20 @@ class StoreService {
345
345
  * when `originJobId` is interrupted/expired, the items in the
346
346
  * list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
347
347
  */
348
- async registerJobDependency(depType, originJobId, topic, jobId, gId, multi) {
348
+ async registerJobDependency(depType, originJobId, topic, jobId, gId, pd = '', multi) {
349
349
  const privateMulti = multi || this.getMulti();
350
350
  const dependencyParams = {
351
351
  appId: this.appId,
352
352
  jobId: originJobId,
353
353
  };
354
354
  const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
355
- //items listed as job dependencies have different relationships
356
- const expireTask = `${depType}::${topic}::${gId}::${jobId}`;
355
+ const expireTask = [
356
+ depType,
357
+ topic,
358
+ gId,
359
+ pd,
360
+ jobId,
361
+ ].join(key_1.VALSEP);
357
362
  privateMulti[this.commands.rpush](depKey, expireTask);
358
363
  if (!multi) {
359
364
  return await privateMulti.exec();
@@ -363,12 +368,18 @@ class StoreService {
363
368
  * Ensures a `hook signal` is delisted when its parent activity/job
364
369
  * is interrupted/expired.
365
370
  */
366
- async registerSignalDependency(jobId, signalKey, multi) {
371
+ async registerSignalDependency(jobId, signalKey, dad, multi) {
367
372
  const privateMulti = multi || this.getMulti();
368
373
  const dependencyParams = { appId: this.appId, jobId };
369
374
  const dependencyKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
370
- //tasks have '4' segments
371
- 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);
372
383
  privateMulti[this.commands.rpush](dependencyKey, delistTask);
373
384
  if (!multi) {
374
385
  return await privateMulti.exec();
@@ -686,11 +697,14 @@ class StoreService {
686
697
  }
687
698
  async setHookSignal(hook, multi) {
688
699
  const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
689
- const { topic, resolved, jobId } = hook; //`${activityId}::${dad}::${gId}::${jobId}`
700
+ //destructure the hook key
701
+ const { topic, resolved, jobId } = hook;
690
702
  const signalKey = `${topic}:${resolved}`;
691
703
  const payload = { [signalKey]: jobId };
692
704
  await (multi || this.redisClient)[this.commands.hset](key, payload);
693
- 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);
694
708
  }
695
709
  async getHookSignal(topic, resolved) {
696
710
  const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
@@ -755,7 +769,7 @@ class StoreService {
755
769
  const depParams = { appId: this.appId, jobId };
756
770
  const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
757
771
  const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
758
- const depKeyContext = `::${context}::${depKey}`;
772
+ const depKeyContext = `${key_1.TYPSEP}${context}${key_1.TYPSEP}${depKey}`;
759
773
  const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
760
774
  await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
761
775
  }
@@ -770,9 +784,16 @@ class StoreService {
770
784
  * for the given sleep group. Sleep groups are
771
785
  * organized into 'n'-second blocks (LISTS))
772
786
  */
773
- async registerTimeHook(jobId, gId, activityId, type, deletionTime, multi) {
787
+ async registerTimeHook(jobId, gId, activityId, type, deletionTime, dad, multi) {
774
788
  const listKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
775
- 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);
776
797
  const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
777
798
  if (multi || len === 1) {
778
799
  const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
@@ -786,9 +807,9 @@ class StoreService {
786
807
  let [pType, pKey] = this.resolveTaskKeyContext(listKey);
787
808
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
788
809
  if (timeEvent) {
789
- //there are task types
790
- //1) sleep (awaken), 2) expire (OR expire-child), 3) interrupt, 4) delist, 5) child (just an index helper; no work to do)
791
- 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);
792
813
  if (type === 'delist') {
793
814
  pType = 'delist';
794
815
  }
@@ -796,9 +817,9 @@ class StoreService {
796
817
  pType = 'child';
797
818
  }
798
819
  else if (type === 'expire-child') {
799
- type = 'expire'; //use the same logic as 'expire'
820
+ type = 'expire';
800
821
  }
801
- return [listKey, jobId.join('::'), gId, activityId, pType];
822
+ return [listKey, jid, gId, activityId, pType];
802
823
  }
803
824
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
804
825
  return true;
@@ -811,14 +832,14 @@ class StoreService {
811
832
  * do with the work list. (not everything is known in advance,
812
833
  * so the ZSET key defines HOW to approach the work in the
813
834
  * generic LIST (lists typically contain target job ids)
814
- * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
835
+ * @param {string} listKey - composite key
815
836
  */
816
837
  resolveTaskKeyContext(listKey) {
817
- if (listKey.startsWith('::INTERRUPT')) {
818
- return ['interrupt', listKey.split('::')[2]];
838
+ if (listKey.startsWith(`${key_1.TYPSEP}INTERRUPT`)) {
839
+ return ['interrupt', listKey.split(key_1.TYPSEP)[2]];
819
840
  }
820
- else if (listKey.startsWith('::EXPIRE')) {
821
- return ['expire', listKey.split('::')[2]];
841
+ else if (listKey.startsWith(`${key_1.TYPSEP}EXPIRE`)) {
842
+ return ['expire', listKey.split(key_1.TYPSEP)[2]];
822
843
  }
823
844
  else {
824
845
  return ['sleep', listKey];
@@ -898,7 +919,7 @@ class StoreService {
898
919
  break;
899
920
  }
900
921
  } while (cursor !== '0');
901
- return matchingKeys;
922
+ return [cursor, matchingKeys];
902
923
  }
903
924
  async findJobFields(jobId, fieldMatchPattern = '*', limit = 1000, batchSize = 1000, cursor = '0') {
904
925
  let fields = [];
@@ -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