@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
@@ -5,18 +5,20 @@ const enums_1 = require("../../modules/enums");
5
5
  const utils_1 = require("../../modules/utils");
6
6
  const pipe_1 = require("../pipe");
7
7
  const hotmesh_1 = require("../../types/hotmesh");
8
+ const key_1 = require("../../modules/key");
8
9
  class TaskService {
9
10
  constructor(store, logger) {
10
11
  this.cleanupTimeout = null;
11
12
  this.isScout = false;
13
+ this.errorCount = 0;
12
14
  this.logger = logger;
13
15
  this.store = store;
14
16
  }
15
17
  async processWebHooks(hookEventCallback) {
16
18
  const workItemKey = await this.store.getActiveTaskQueue();
17
19
  if (workItemKey) {
18
- const [topic, sourceKey, scrub, ...sdata] = workItemKey.split('::');
19
- const data = JSON.parse(sdata.join('::'));
20
+ const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(key_1.WEBSEP);
21
+ const data = JSON.parse(sdata.join(key_1.WEBSEP));
20
22
  const destinationKey = `${sourceKey}:processed`;
21
23
  const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
22
24
  if (jobId) {
@@ -41,11 +43,11 @@ class TaskService {
41
43
  await this.store.registerDependenciesForCleanup(jobId, timeSlot, options);
42
44
  }
43
45
  }
44
- async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, multi) {
46
+ async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, dad, multi) {
45
47
  const fromNow = Date.now() + (inSeconds * 1000);
46
48
  const fidelityMS = enums_1.HMSH_FIDELITY_SECONDS * 1000;
47
49
  const awakenTimeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
48
- await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
50
+ await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, dad, multi);
49
51
  }
50
52
  /**
51
53
  * Should this engine instance play the role of 'scout' on behalf
@@ -89,11 +91,13 @@ class TaskService {
89
91
  await timeEventCallback(target, gId, activityId, type);
90
92
  }
91
93
  await (0, utils_1.sleepFor)(0);
94
+ this.errorCount = 0;
92
95
  this.processTimeHooks(timeEventCallback, listKey);
93
96
  }
94
97
  else if (workListTask) {
95
98
  //a worklist was just emptied; try again immediately
96
99
  await (0, utils_1.sleepFor)(0);
100
+ this.errorCount = 0;
97
101
  this.processTimeHooks(timeEventCallback);
98
102
  }
99
103
  else {
@@ -101,12 +105,18 @@ class TaskService {
101
105
  let sleep = (0, utils_1.XSleepFor)(enums_1.HMSH_FIDELITY_SECONDS * 1000);
102
106
  this.cleanupTimeout = sleep.timerId;
103
107
  await sleep.promise;
108
+ this.errorCount = 0;
104
109
  this.processTimeHooks(timeEventCallback);
105
110
  }
106
111
  }
107
112
  catch (err) {
108
- //todo: retry connect to redis
109
- this.logger.error('task-process-timehooks-error', err);
113
+ //most common reasons: deleted job not found; container stopping; test stopping
114
+ //less common: redis/cluster down; retry with fallback (5s max main reassignment)
115
+ this.logger.warn('task-process-timehooks-error', err);
116
+ await (0, utils_1.sleepFor)(1000 * this.errorCount++);
117
+ if (this.errorCount < 5) {
118
+ this.processTimeHooks(timeEventCallback);
119
+ }
110
120
  }
111
121
  }
112
122
  else {
@@ -135,10 +145,17 @@ class TaskService {
135
145
  const jobId = context.metadata.jid;
136
146
  const gId = context.metadata.gid;
137
147
  const activityId = hookRule.to;
148
+ //composite keys are used to fully describe the task target
149
+ const compositeJobKey = [
150
+ activityId,
151
+ dad,
152
+ gId,
153
+ jobId
154
+ ].join(key_1.WEBSEP);
138
155
  const hook = {
139
156
  topic,
140
157
  resolved,
141
- jobId: `${activityId}::${dad}::${gId}::${jobId}`,
158
+ jobId: compositeJobKey,
142
159
  };
143
160
  await this.store.setHookSignal(hook, multi);
144
161
  return jobId;
@@ -157,17 +174,17 @@ class TaskService {
157
174
  const resolved = pipe_1.Pipe.resolve(mapExpression, context);
158
175
  const hookSignalId = await this.store.getHookSignal(topic, resolved);
159
176
  if (!hookSignalId) {
160
- //messages can be double-processed; not an issue; return undefined
161
- //users can also provide a bogus topic; not an issue; return undefined
177
+ //messages can be double-processed; not an issue; return `undefined`
178
+ //users can also provide a bogus topic; not an issue; return `undefined`
162
179
  return undefined;
163
180
  }
164
- //`aid` is part of composit key, but the hook `topic` is its public interface;
181
+ //`aid` is part of composite key, but the hook `topic` is its public interface;
165
182
  // this means that a new version of the graph can be deployed and the
166
183
  // topic can be re-mapped to a different activity id. Outside callers
167
184
  // can adhere to the unchanged contract (calling the same topic),
168
- // while the internal system can be updated in real time as necessary.
169
- const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
170
- return [jid.join('::'), hookRule.to, dad, gid];
185
+ // while the internal system can be updated in real-time as necessary.
186
+ const [_aid, dad, gid, ...jid] = hookSignalId.split(key_1.WEBSEP);
187
+ return [jid.join(key_1.WEBSEP), hookRule.to, dad, gid];
171
188
  }
172
189
  else {
173
190
  throw new Error('signal-not-found');
@@ -1,17 +1,20 @@
1
+ /// <reference types="node" />
1
2
  import { ILogger } from "../logger";
2
3
  import { Router } from "../router";
3
4
  import { StoreService } from '../store';
4
5
  import { StreamService } from '../stream';
5
6
  import { SubService } from '../sub';
6
7
  import { HotMeshConfig, HotMeshWorker } from "../../types/hotmesh";
7
- import { SubscriptionCallback } from "../../types/quorum";
8
+ import { RollCallMessage, SubscriptionCallback } from "../../types/quorum";
8
9
  import { RedisClient, RedisMulti } from "../../types/redis";
10
+ import { StreamData, StreamDataResponse } from "../../types/stream";
9
11
  declare class WorkerService {
10
12
  namespace: string;
11
13
  appId: string;
12
14
  guid: string;
13
15
  topic: string;
14
16
  config: HotMeshConfig;
17
+ callback: (streamData: StreamData) => Promise<StreamDataResponse | void>;
15
18
  store: StoreService<RedisClient, RedisMulti> | null;
16
19
  stream: StreamService<RedisClient, RedisMulti> | null;
17
20
  subscribe: SubService<RedisClient, RedisMulti> | null;
@@ -19,6 +22,7 @@ declare class WorkerService {
19
22
  logger: ILogger;
20
23
  reporting: boolean;
21
24
  inited: string;
25
+ rollCallInterval: NodeJS.Timeout;
22
26
  static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<WorkerService[]>;
23
27
  verifyWorkerFields(worker: HotMeshWorker): void;
24
28
  initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
@@ -26,7 +30,14 @@ declare class WorkerService {
26
30
  initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
27
31
  initRouter(worker: HotMeshWorker, logger: ILogger): Router;
28
32
  subscriptionHandler(): SubscriptionCallback;
29
- sayPong(appId: string, guid: string, originator: string, details?: boolean): Promise<void>;
33
+ /**
34
+ * A quorum-wide command to broadcaset system details.
35
+ *
36
+ */
37
+ doRollCall(message: RollCallMessage): Promise<void>;
38
+ cancelRollCall(): void;
39
+ stop(): void;
40
+ sayPong(appId: string, guid: string, originator?: string, details?: boolean, signature?: boolean): Promise<void>;
30
41
  throttle(delayInMillis: number): Promise<void>;
31
42
  }
32
43
  export { WorkerService };
@@ -12,6 +12,7 @@ const redis_2 = require("../stream/clients/redis");
12
12
  const ioredis_3 = require("../sub/clients/ioredis");
13
13
  const redis_3 = require("../sub/clients/redis");
14
14
  const stream_1 = require("../../types/stream");
15
+ const enums_1 = require("../../modules/enums");
15
16
  class WorkerService {
16
17
  constructor() {
17
18
  this.reporting = false;
@@ -26,6 +27,7 @@ class WorkerService {
26
27
  service.namespace = namespace;
27
28
  service.appId = appId;
28
29
  service.guid = guid;
30
+ service.callback = worker.callback;
29
31
  service.topic = worker.topic;
30
32
  service.config = config;
31
33
  service.logger = logger;
@@ -95,14 +97,51 @@ class WorkerService {
95
97
  return async (topic, message) => {
96
98
  self.logger.debug('worker-event-received', { topic, type: message.type });
97
99
  if (message.type === 'throttle') {
98
- self.throttle(message.throttle);
100
+ if (message.topic !== null) { //undefined allows passthrough
101
+ self.throttle(message.throttle);
102
+ }
99
103
  }
100
104
  else if (message.type === 'ping') {
101
105
  self.sayPong(self.appId, self.guid, message.originator, message.details);
102
106
  }
107
+ else if (message.type === 'rollcall') {
108
+ if (message.topic !== null) { //undefined allows passthrough
109
+ self.doRollCall(message);
110
+ }
111
+ }
103
112
  };
104
113
  }
105
- async sayPong(appId, guid, originator, details = false) {
114
+ /**
115
+ * A quorum-wide command to broadcaset system details.
116
+ *
117
+ */
118
+ async doRollCall(message) {
119
+ let iteration = 0;
120
+ let max = !isNaN(message.max) ? message.max : enums_1.HMSH_QUORUM_ROLLCALL_CYCLES;
121
+ if (this.rollCallInterval)
122
+ clearTimeout(this.rollCallInterval);
123
+ const base = (message.interval / 2);
124
+ const amount = base + Math.ceil(Math.random() * base);
125
+ do {
126
+ await (0, utils_1.sleepFor)(Math.ceil(Math.random() * 1000));
127
+ await this.sayPong(this.appId, this.guid, null, true, message.signature);
128
+ if (!message.interval)
129
+ return;
130
+ const { promise, timerId } = (0, utils_1.XSleepFor)(amount * 1000);
131
+ this.rollCallInterval = timerId;
132
+ await promise;
133
+ } while (this.rollCallInterval && iteration++ < max - 1);
134
+ }
135
+ cancelRollCall() {
136
+ if (this.rollCallInterval) {
137
+ clearTimeout(this.rollCallInterval);
138
+ delete this.rollCallInterval;
139
+ }
140
+ }
141
+ stop() {
142
+ this.cancelRollCall();
143
+ }
144
+ async sayPong(appId, guid, originator, details = false, signature = false) {
106
145
  let profile;
107
146
  if (details) {
108
147
  const params = {
@@ -122,11 +161,13 @@ class WorkerService {
122
161
  reclaimDelay: this.router.reclaimDelay,
123
162
  reclaimCount: this.router.reclaimCount,
124
163
  system: await (0, utils_1.getSystemHealth)(),
164
+ signature: signature ? this.callback.toString() : undefined,
125
165
  };
126
166
  }
127
167
  this.store.publish(key_1.KeyType.QUORUM, {
128
168
  type: 'pong',
129
- guid, originator,
169
+ guid,
170
+ originator,
130
171
  profile,
131
172
  }, appId);
132
173
  }
@@ -74,6 +74,7 @@ interface AwaitActivity extends BaseActivity {
74
74
  type: 'await';
75
75
  eventName: string;
76
76
  timeout: number;
77
+ await?: boolean;
77
78
  }
78
79
  interface WorkerActivity extends BaseActivity {
79
80
  type: 'worker';
@@ -21,6 +21,8 @@ export interface JobTimeline {
21
21
  dimension: string;
22
22
  duplex: 'entry' | 'exit';
23
23
  timestamp: string;
24
+ created?: string;
25
+ updated?: string;
24
26
  actions?: ActivityAction[];
25
27
  }
26
28
  export interface DependencyExport {
@@ -15,6 +15,7 @@ type JobMetadata = {
15
15
  pg?: string;
16
16
  pd?: string;
17
17
  pa?: string;
18
+ px?: boolean;
18
19
  ngn?: string;
19
20
  app: string;
20
21
  vrs: string;
@@ -45,42 +45,56 @@ export interface QuorumProfile {
45
45
  reclaimDelay?: number;
46
46
  reclaimCount?: number;
47
47
  system?: SystemHealth;
48
+ signature?: string;
48
49
  }
49
- export interface PingMessage {
50
+ interface QuorumMessageBase {
51
+ guid?: string;
52
+ topic?: string;
53
+ type?: string;
54
+ }
55
+ export interface PingMessage extends QuorumMessageBase {
50
56
  type: 'ping';
51
57
  originator: string;
52
58
  details?: boolean;
53
59
  }
54
- export interface WorkMessage {
60
+ export interface WorkMessage extends QuorumMessageBase {
55
61
  type: 'work';
56
62
  originator: string;
57
63
  }
58
- export interface CronMessage {
64
+ export interface CronMessage extends QuorumMessageBase {
59
65
  type: 'cron';
60
66
  originator: string;
61
67
  }
62
- export interface PongMessage {
68
+ export interface PongMessage extends QuorumMessageBase {
63
69
  type: 'pong';
64
70
  guid: string;
65
71
  originator: string;
66
72
  profile?: QuorumProfile;
67
73
  }
68
- export interface ActivateMessage {
74
+ export interface ActivateMessage extends QuorumMessageBase {
69
75
  type: 'activate';
70
76
  cache_mode: 'nocache' | 'cache';
71
77
  until_version: string;
72
78
  }
73
- export interface JobMessage {
79
+ export interface JobMessage extends QuorumMessageBase {
74
80
  type: 'job';
75
81
  topic: string;
76
82
  job: JobOutput;
77
83
  }
78
- export interface ThrottleMessage {
84
+ export interface ThrottleMessage extends QuorumMessageBase {
79
85
  type: 'throttle';
80
86
  guid?: string;
81
87
  topic?: string;
82
88
  throttle: number;
83
89
  }
90
+ export interface RollCallMessage extends QuorumMessageBase {
91
+ type: 'rollcall';
92
+ guid?: string;
93
+ topic?: string | null;
94
+ interval: number;
95
+ max?: number;
96
+ signature?: boolean;
97
+ }
84
98
  export interface JobMessageCallback {
85
99
  (topic: string, message: JobOutput): void;
86
100
  }
@@ -96,5 +110,5 @@ export interface QuorumMessageCallback {
96
110
  * These messages serve to coordinate the cache invalidation and switch-over
97
111
  * to the new version without any downtime and a coordinating parent server.
98
112
  */
99
- export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
113
+ export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | RollCallMessage | CronMessage;
100
114
  export {};
@@ -37,6 +37,7 @@ export interface StreamData {
37
37
  trc?: string;
38
38
  spn?: string;
39
39
  try?: number;
40
+ await?: boolean;
40
41
  };
41
42
  type?: StreamDataType;
42
43
  data: Record<string, unknown>;
package/modules/enums.ts CHANGED
@@ -23,6 +23,7 @@ export const HMSH_CODE_DURABLE_RETRYABLE = 599;
23
23
  export const HMSH_STATUS_UNKNOWN = 'unknown';
24
24
 
25
25
  // QUORUM
26
+ export const HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
26
27
  export const HMSH_QUORUM_DELAY_MS = 250;
27
28
  export const HMSH_ACTIVATION_MAX_RETRY = 3;
28
29
 
package/modules/key.ts CHANGED
@@ -28,7 +28,12 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
28
28
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
29
29
  */
30
30
 
31
- const HMNS = "hmsh"; //default
31
+ const HMNS = "hmsh";
32
+
33
+ const KEYSEP = ':'; //default delimiter for keys
34
+ const VALSEP = '::'; //default delimiter for vals
35
+ const WEBSEP = '::'; //default delimiter for webhook vals
36
+ const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
32
37
 
33
38
  class KeyService {
34
39
 
@@ -93,4 +98,4 @@ class KeyService {
93
98
  }
94
99
  }
95
100
 
96
- export { KeyService, KeyType, KeyStoreParams, HMNS };
101
+ export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
package/package.json CHANGED
@@ -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",
@@ -95,6 +95,12 @@ class Await extends Activity {
95
95
  type: StreamDataType.AWAIT,
96
96
  data: this.context.data
97
97
  };
98
+ if (this.config.await !== true) {
99
+ const doAwait = Pipe.resolve(this.config.await, this.context);
100
+ if (doAwait === false) {
101
+ streamData.metadata.await = false;
102
+ }
103
+ }
98
104
  if (this.config.retry) {
99
105
  streamData.policies = {
100
106
  retry: this.config.retry
@@ -148,6 +148,7 @@ class Hook extends Activity {
148
148
  `${this.metadata.aid}${this.metadata.dad || ''}`,
149
149
  'sleep',
150
150
  duration,
151
+ this.metadata.dad || '',
151
152
  );
152
153
  return this.context.metadata.jid;
153
154
  }
@@ -15,6 +15,7 @@ import {
15
15
  import { JobState } from '../../types/job';
16
16
  import { RedisMulti } from '../../types/redis';
17
17
  import { StringScalarType } from '../../types/serializer';
18
+ import { WorkListTaskType } from '../../types/task';
18
19
 
19
20
  class Trigger extends Activity {
20
21
  config: TriggerActivity;
@@ -50,6 +51,10 @@ class Trigger extends Activity {
50
51
  await this.registerJobDependency(multi);
51
52
  await multi.exec();
52
53
 
54
+ //if the parent (spawner) chose not to await,
55
+ // emit the job_id as the data payload { job_id }
56
+ this.execAdjacentParent();
57
+
53
58
  telemetry.mapActivityAttributes();
54
59
  const jobStatus = Number(this.context.metadata.js);
55
60
  telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
@@ -79,6 +84,12 @@ class Trigger extends Activity {
79
84
  this.context.metadata.js = amount;
80
85
  }
81
86
 
87
+ async execAdjacentParent() {
88
+ if (this.context.metadata.px) {
89
+ await this.engine.execAdjacentParent(this.context, {metadata: this.context.metadata, data: { job_id: this.context.metadata.jid }});
90
+ }
91
+ }
92
+
82
93
  createInputContext(): Partial<JobState> {
83
94
  const input = {
84
95
  [this.metadata.aid]: {
@@ -115,6 +126,7 @@ class Trigger extends Activity {
115
126
  pg: this.context.metadata.pg,
116
127
  pd: this.context.metadata.pd,
117
128
  pa: this.context.metadata.pa,
129
+ px: this.context.metadata.px,
118
130
  app: id,
119
131
  vrs: version,
120
132
  tpc: this.config.subscribes,
@@ -193,12 +205,23 @@ class Trigger extends Activity {
193
205
  }
194
206
  if (resolvedDepKey) {
195
207
  const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
208
+ let type: WorkListTaskType;
209
+ if (isParentOrigin) {
210
+ if (this.context.metadata.px) {
211
+ type = 'child'
212
+ } else {
213
+ type = 'expire-child'
214
+ }
215
+ } else {
216
+ type = 'expire';
217
+ }
196
218
  await this.store.registerJobDependency(
197
- isParentOrigin ? 'expire-child' : 'expire',
219
+ type,
198
220
  resolvedDepKey,
199
221
  this.context.metadata.tpc,
200
222
  this.context.metadata.jid,
201
223
  this.context.metadata.gid,
224
+ this.context.metadata.pd,
202
225
  multi,
203
226
  );
204
227
  }
@@ -209,6 +232,7 @@ class Trigger extends Activity {
209
232
  this.context.metadata.tpc,
210
233
  this.context.metadata.jid,
211
234
  this.context.metadata.gid,
235
+ this.context.metadata.pd,
212
236
  multi,
213
237
  );
214
238
  }
@@ -13,6 +13,7 @@ import {
13
13
  JobTimeline } from '../../types/exporter';
14
14
  import { SerializerService } from '../serializer';
15
15
  import { restoreHierarchy } from '../../modules/utils';
16
+ import { VALSEP } from '../../modules/key';
16
17
 
17
18
  /**
18
19
  * Downloads job data from Redis (hscan, hmget, hgetall)
@@ -116,19 +117,31 @@ class ExporterService {
116
117
  const activityName = item[1].split('/')[0];
117
118
  const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
118
119
  const timestamp = item[2];
119
- const event: JobTimeline = {
120
+ let event: JobTimeline = {
120
121
  activity: activityName,
121
122
  duplex: duplex as 'entry' | 'exit',
122
123
  dimension: dimensions,
123
124
  timestamp,
125
+ created: timestamp,
126
+ updated: timestamp,
124
127
  };
125
- timeline.push(event);
128
+ const prior = timeline[timeline.length - 1];
129
+ if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
130
+ if (event.duplex === 'exit') {
131
+ prior.updated = event.timestamp;
132
+ } else {
133
+ prior.created = event.timestamp;
134
+ }
135
+ event = prior;
136
+ } else {
137
+ timeline.push(event);
138
+ }
126
139
 
127
140
  if (this.isMainEntry(item[1])) {
128
141
  event.actions = [] as ActivityAction[];
129
142
  this.interleaveActions(actions.main, event.actions);
130
143
  } else if (this.isHookEntry(item[1])) {
131
- const hookDimension = `/${parts[1]}/${parts[2]}`;
144
+ const hookDimension = `/${parts[1]}/${parts[2]}`;
132
145
  const hookActions = actions.hooks[hookDimension];
133
146
  event.actions = [] as ActivityAction[];
134
147
  this.interleaveActions(hookActions, event.actions);
@@ -191,17 +204,15 @@ class ExporterService {
191
204
  * @returns - the organized dependency data
192
205
  */
193
206
  inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
194
- //console.log('dependency data>', data);
195
207
  const hookReg = /([0-9,]+)-(\d+)$/;
196
208
  const flowReg = /-(\d+)$/;
197
209
  return data.map((dependency, index: number): DependencyExport => {
198
- const [action, topic, gid, ...jid] = dependency.split('::');
199
- const jobId = jid.join('::');
210
+ const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
211
+ const jobId = jid.join(VALSEP);
200
212
  const match = jobId.match(hookReg);
201
213
  let prefix: string;
202
214
  let type: 'hook' | 'flow' | 'other';
203
215
  let dimensionKey: string = '';
204
-
205
216
  if (match) {
206
217
  //hook-originating dependency
207
218
  const [_, dimension, counter] = match;
@@ -1,4 +1,4 @@
1
- import { KeyType } from '../../modules/key';
1
+ import { KeyType, VALSEP } from '../../modules/key';
2
2
  import {
3
3
  HMSH_OTT_WAIT_TIME,
4
4
  HMSH_CODE_SUCCESS,
@@ -386,9 +386,10 @@ class EngineService {
386
386
  pg: streamData.metadata.gid,
387
387
  pd: streamData.metadata.dad,
388
388
  pa: streamData.metadata.aid,
389
+ px: streamData.metadata.await === false, //sever the parent connection (px)
389
390
  trc: streamData.metadata.trc,
390
391
  spn: streamData.metadata.spn,
391
- };
392
+ };
392
393
  const activityHandler = await this.initActivity(
393
394
  streamData.metadata.topic,
394
395
  streamData.data,
@@ -459,7 +460,10 @@ class EngineService {
459
460
  return (await this.router?.publishMessage(null, streamData)) as string;
460
461
  }
461
462
  }
462
- hasParentJob(context: JobState): boolean {
463
+ hasParentJob(context: JobState, checkSevered = false): boolean {
464
+ if (checkSevered) {
465
+ return Boolean(context.metadata.pj && context.metadata.pa && !context.metadata.px);
466
+ }
463
467
  return Boolean(context.metadata.pj && context.metadata.pa);
464
468
  }
465
469
  resolveError(metadata: JobMetadata): StreamError | undefined {
@@ -537,7 +541,11 @@ class EngineService {
537
541
  const taskService = new TaskService(this.store, this.logger);
538
542
  await taskService.enqueueWorkItems(
539
543
  workItems.map(
540
- workItem => `${hookTopic}::${workItem}::${keyResolver.scrub || false}::${JSON.stringify(data)}`
544
+ workItem => [
545
+ hookTopic,
546
+ workItem,
547
+ keyResolver.scrub || false,
548
+ JSON.stringify(data)].join(VALSEP)
541
549
  ));
542
550
  this.store.publish(
543
551
  KeyType.QUORUM,
@@ -663,7 +671,7 @@ class EngineService {
663
671
  // ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
664
672
  async runJobCompletionTasks(context: JobState, options: JobCompletionOptions = {}): Promise<string | void> {
665
673
  //'emit' indicates the job is still active
666
- const isAwait = this.hasParentJob(context);
674
+ const isAwait = this.hasParentJob(context, true);
667
675
  const isOneTimeSub = this.hasOneTimeSubscription(context);
668
676
  const topic = await this.getPublishesTopic(context);
669
677
  let msgId: string;
@@ -12,6 +12,7 @@ import {
12
12
  JobExport } from '../../types/exporter';
13
13
  import { SerializerService } from '../serializer';
14
14
  import { restoreHierarchy } from '../../modules/utils';
15
+ import { VALSEP } from '../../modules/key';
15
16
 
16
17
  /**
17
18
  * Downloads job data from Redis (hscan, hmget, hgetall)
@@ -108,8 +109,8 @@ class ExporterService {
108
109
  const hookReg = /([0-9,]+)-(\d+)$/;
109
110
  const flowReg = /-(\d+)$/;
110
111
  return data.map((dependency, index: number): DependencyExport => {
111
- const [action, topic, gid, ...jid] = dependency.split('::');
112
- const jobId = jid.join('::');
112
+ const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
113
+ const jobId = jid.join(VALSEP);
113
114
  const match = jobId.match(hookReg);
114
115
  let prefix: string;
115
116
  let type: 'hook' | 'flow' | 'other';
@@ -222,6 +222,10 @@ class HotMeshService {
222
222
 
223
223
  stop() {
224
224
  this.engine?.taskService.cancelCleanup();
225
+ this.quorum?.stop();
226
+ this.workers?.forEach((worker: WorkerService) => {
227
+ worker.stop();
228
+ });
225
229
  }
226
230
 
227
231
  async compress(terms: string[]): Promise<boolean> {