@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
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskService = void 0;
4
4
  const enums_1 = require("../../modules/enums");
5
5
  const utils_1 = require("../../modules/utils");
6
+ const pipe_1 = require("../pipe");
6
7
  class TaskService {
7
8
  constructor(store, logger) {
8
9
  this.cleanupTimeout = null;
10
+ this.isScout = false;
9
11
  this.logger = logger;
10
12
  this.store = store;
11
13
  }
@@ -36,29 +38,61 @@ class TaskService {
36
38
  await this.store.registerExpireJob(jobId, expireTimeSlot, options);
37
39
  }
38
40
  }
39
- async registerTimeHook(jobId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
41
+ async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
40
42
  const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (enums_1.FIDELITY_SECONDS * 1000)) * (enums_1.FIDELITY_SECONDS * 1000); //n second awaken groups
41
- await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
43
+ await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
44
+ }
45
+ /**
46
+ * Should this engine instance play the role of 'scout' for the quorum.
47
+ */
48
+ async shouldScout() {
49
+ const wasScout = this.isScout;
50
+ const isScout = wasScout || (this.isScout = await this.store.reserveScoutRole('time'));
51
+ if (isScout) {
52
+ if (!wasScout) {
53
+ setTimeout(() => {
54
+ this.isScout = false;
55
+ }, enums_1.SCOUT_INTERVAL_SECONDS * 1000);
56
+ }
57
+ return true;
58
+ }
59
+ return false;
42
60
  }
43
61
  async processTimeHooks(timeEventCallback, listKey) {
44
- try {
45
- const timeJob = await this.store.getNextTimeJob(listKey);
46
- if (timeJob) {
47
- const [listKey, jobId, activityId, type] = timeJob;
48
- await timeEventCallback(jobId, activityId, type);
49
- await (0, utils_1.sleepFor)(0);
50
- this.processTimeHooks(timeEventCallback, listKey);
62
+ if (await this.shouldScout()) {
63
+ try {
64
+ const timeJob = await this.store.getNextTimeJob(listKey);
65
+ if (Array.isArray(timeJob)) {
66
+ //a queue had a job; try again immediately
67
+ const [listKey, jobId, gId, activityId, type] = timeJob;
68
+ await timeEventCallback(jobId, gId, activityId, type);
69
+ await (0, utils_1.sleepFor)(0);
70
+ this.processTimeHooks(timeEventCallback, listKey);
71
+ }
72
+ else if (timeJob) {
73
+ //a queue was just emptied; try again immediately
74
+ await (0, utils_1.sleepFor)(0);
75
+ this.processTimeHooks(timeEventCallback);
76
+ }
77
+ else {
78
+ //all queues are empty; sleep before checking
79
+ let sleep = (0, utils_1.XSleepFor)(enums_1.FIDELITY_SECONDS * 1000);
80
+ this.cleanupTimeout = sleep.timerId;
81
+ await sleep.promise;
82
+ this.processTimeHooks(timeEventCallback);
83
+ }
51
84
  }
52
- else {
53
- let sleep = (0, utils_1.XSleepFor)(enums_1.FIDELITY_SECONDS * 1000);
54
- this.cleanupTimeout = sleep.timerId;
55
- await sleep.promise;
56
- this.processTimeHooks(timeEventCallback);
85
+ catch (err) {
86
+ //todo: retry connect to redis
87
+ this.logger.error('task-process-timehooks-error', err);
57
88
  }
58
89
  }
59
- catch (err) {
60
- //todo: retry connect to redis
61
- this.logger.error('task-process-timehooks-error', err);
90
+ else {
91
+ //didn't get the scout role; try again in 'one-ish' minutes
92
+ let sleep = (0, utils_1.XSleepFor)(enums_1.SCOUT_INTERVAL_SECONDS * 1000 * 2 * Math.random());
93
+ this.cleanupTimeout = sleep.timerId;
94
+ await sleep.promise;
95
+ this.processTimeHooks(timeEventCallback);
62
96
  }
63
97
  }
64
98
  cancelCleanup() {
@@ -67,5 +101,69 @@ class TaskService {
67
101
  this.cleanupTimeout = undefined;
68
102
  }
69
103
  }
104
+ async getHookRule(topic) {
105
+ const rules = await this.store.getHookRules();
106
+ return rules?.[topic]?.[0];
107
+ }
108
+ async registerWebHook(topic, context, dad, multi) {
109
+ const hookRule = await this.getHookRule(topic);
110
+ if (hookRule) {
111
+ const mapExpression = hookRule.conditions.match[0].expected;
112
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
113
+ const jobId = context.metadata.jid;
114
+ const gId = context.metadata.gid;
115
+ const activityId = hookRule.to;
116
+ const hook = {
117
+ topic,
118
+ resolved,
119
+ jobId: `${activityId}::${dad}::${gId}::${jobId}`,
120
+ };
121
+ await this.store.setHookSignal(hook, multi);
122
+ return jobId;
123
+ }
124
+ else {
125
+ throw new Error('signaler.registerWebHook:error: hook rule not found');
126
+ }
127
+ }
128
+ async processWebHookSignal(topic, data) {
129
+ const hookRule = await this.getHookRule(topic);
130
+ if (hookRule) {
131
+ //NOTE: both formats are supported by the mapping engine:
132
+ // `$self.hook.data` OR `$hook.data`
133
+ const context = { $self: { hook: { data } }, $hook: { data } };
134
+ const mapExpression = hookRule.conditions.match[0].actual;
135
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
136
+ const hookSignalId = await this.store.getHookSignal(topic, resolved);
137
+ if (!hookSignalId) {
138
+ //messages can be double-processed; not an issue; return undefined
139
+ //users can also provide a bogus topic; not an issue; return undefined
140
+ return undefined;
141
+ }
142
+ //`aid` is part of composit key, but the hook `topic` is its public interface;
143
+ // this means that a new version of the graph can be deployed and the
144
+ // topic can be re-mapped to a different activity id. Outside callers
145
+ // can adhere to the unchanged contract (calling the same topic),
146
+ // while the internal system can be updated in real time as necessary.
147
+ const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
148
+ return [jid.join('::'), hookRule.to, dad, gid];
149
+ }
150
+ else {
151
+ throw new Error('signal-not-found');
152
+ }
153
+ }
154
+ async deleteWebHookSignal(topic, data) {
155
+ const hookRule = await this.getHookRule(topic);
156
+ if (hookRule) {
157
+ //NOTE: both formats are supported by the mapping engine:
158
+ // `$self.hook.data` OR `$hook.data`
159
+ const context = { $self: { hook: { data } }, $hook: { data } };
160
+ const mapExpression = hookRule.conditions.match[0].actual;
161
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
162
+ return await this.store.deleteHookSignal(topic, resolved);
163
+ }
164
+ else {
165
+ throw new Error('signaler.process:error: hook rule not found');
166
+ }
167
+ }
70
168
  }
71
169
  exports.TaskService = TaskService;
@@ -93,17 +93,17 @@ class TelemetryService {
93
93
  return result;
94
94
  }, {})
95
95
  };
96
- this.span.setAttributes(namespacedAtts);
96
+ this.span?.setAttributes(namespacedAtts);
97
97
  }
98
98
  }
99
99
  setActivityAttributes(attributes) {
100
- this.span.setAttributes(attributes);
100
+ this.span?.setAttributes(attributes);
101
101
  }
102
102
  setStreamAttributes(attributes) {
103
- this.span.setAttributes(attributes);
103
+ this.span?.setAttributes(attributes);
104
104
  }
105
105
  setJobAttributes(attributes) {
106
- this.jobSpan.setAttributes(attributes);
106
+ this.jobSpan?.setAttributes(attributes);
107
107
  }
108
108
  endJobSpan() {
109
109
  this.endSpan(this.jobSpan);
@@ -174,10 +174,10 @@ class TelemetryService {
174
174
  }
175
175
  }
176
176
  setActivityError(message) {
177
- this.span.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
177
+ this.span?.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
178
178
  }
179
179
  setStreamError(message) {
180
- this.span.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
180
+ this.span?.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
181
181
  }
182
182
  /**
183
183
  * Adds the paths (HGET) necessary to restore telemetry state for an activity
@@ -1,5 +1,5 @@
1
1
  import { ILogger } from "../logger";
2
- import { StreamSignaler } from "../signaler/stream";
2
+ import { Router } from "../router";
3
3
  import { StoreService } from '../store';
4
4
  import { StreamService } from '../stream';
5
5
  import { SubService } from '../sub';
@@ -15,7 +15,7 @@ declare class WorkerService {
15
15
  store: StoreService<RedisClient, RedisMulti> | null;
16
16
  stream: StreamService<RedisClient, RedisMulti> | null;
17
17
  subscribe: SubService<RedisClient, RedisMulti> | null;
18
- streamSignaler: StreamSignaler | null;
18
+ router: Router | null;
19
19
  logger: ILogger;
20
20
  reporting: boolean;
21
21
  static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<WorkerService[]>;
@@ -23,7 +23,7 @@ declare class WorkerService {
23
23
  initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
24
24
  initSubChannel(service: WorkerService, sub: RedisClient): Promise<void>;
25
25
  initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
26
- initStreamSignaler(worker: HotMeshWorker, logger: ILogger): StreamSignaler;
26
+ initRouter(worker: HotMeshWorker, logger: ILogger): Router;
27
27
  subscriptionHandler(): SubscriptionCallback;
28
28
  throttle(delayInMillis: number): Promise<void>;
29
29
  }
@@ -2,14 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkerService = void 0;
4
4
  const key_1 = require("../../modules/key");
5
- const stream_1 = require("../signaler/stream");
5
+ const router_1 = require("../router");
6
6
  const redis_1 = require("../store/clients/redis");
7
7
  const ioredis_1 = require("../store/clients/ioredis");
8
8
  const redis_2 = require("../stream/clients/redis");
9
9
  const ioredis_2 = require("../stream/clients/ioredis");
10
10
  const ioredis_3 = require("../sub/clients/ioredis");
11
11
  const redis_3 = require("../sub/clients/redis");
12
- const stream_2 = require("../../types/stream");
12
+ const stream_1 = require("../../types/stream");
13
13
  const utils_1 = require("../../modules/utils");
14
14
  const connector_1 = require("../connector");
15
15
  class WorkerService {
@@ -35,9 +35,9 @@ class WorkerService {
35
35
  await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.topic);
36
36
  await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.guid);
37
37
  await service.initStreamChannel(service, worker.stream);
38
- service.streamSignaler = service.initStreamSignaler(worker, logger);
38
+ service.router = service.initRouter(worker, logger);
39
39
  const key = service.stream.mintKey(key_1.KeyType.STREAMS, { appId: service.appId, topic: worker.topic });
40
- await service.streamSignaler.consumeMessages(key, 'WORKER', service.guid, worker.callback);
40
+ await service.router.consumeMessages(key, 'WORKER', service.guid, worker.callback);
41
41
  services.push(service);
42
42
  }
43
43
  }
@@ -78,12 +78,12 @@ class WorkerService {
78
78
  }
79
79
  await service.stream.init(service.namespace, service.appId, service.logger);
80
80
  }
81
- initStreamSignaler(worker, logger) {
82
- return new stream_1.StreamSignaler({
81
+ initRouter(worker, logger) {
82
+ return new router_1.Router({
83
83
  namespace: this.namespace,
84
84
  appId: this.appId,
85
85
  guid: this.guid,
86
- role: stream_2.StreamRole.WORKER,
86
+ role: stream_1.StreamRole.WORKER,
87
87
  topic: worker.topic,
88
88
  reclaimDelay: worker.reclaimDelay,
89
89
  reclaimCount: worker.reclaimCount,
@@ -99,7 +99,7 @@ class WorkerService {
99
99
  };
100
100
  }
101
101
  async throttle(delayInMillis) {
102
- this.streamSignaler.setThrottle(delayInMillis);
102
+ this.router.setThrottle(delayInMillis);
103
103
  }
104
104
  }
105
105
  exports.WorkerService = WorkerService;
@@ -6,10 +6,12 @@ type ActivityData = {
6
6
  };
7
7
  type JobMetadata = {
8
8
  key?: string;
9
+ gid: string;
9
10
  jid: string;
10
11
  dad: string;
11
12
  aid: string;
12
13
  pj?: string;
14
+ pg?: string;
13
15
  pd?: string;
14
16
  pa?: string;
15
17
  ngn?: string;
@@ -31,6 +31,7 @@ export interface StreamData {
31
31
  guid: string;
32
32
  topic?: string;
33
33
  jid?: string;
34
+ gid?: string;
34
35
  dad?: string;
35
36
  aid: string;
36
37
  trc?: string;
package/modules/enums.ts CHANGED
@@ -30,3 +30,6 @@ export const FIDELITY_SECONDS = process.env.NODE_ENV === 'test' ? TEST_FIDELITY_
30
30
 
31
31
  // DURABLE CONSTANTS
32
32
  export const DURABLE_EXPIRE_SECONDS = 1;
33
+
34
+ // TASK CONSTANTS
35
+ export const SCOUT_INTERVAL_SECONDS = 60;
package/modules/errors.ts CHANGED
@@ -122,6 +122,23 @@ class InactiveJobError extends Error {
122
122
  this.status = status;
123
123
  }
124
124
  }
125
+ class GenerationalError extends Error {
126
+ expected: string;
127
+ actual: string;
128
+ jobId: string;
129
+ activityId: string;
130
+ dimensionalAddress: string;
131
+
132
+ constructor(expected: string, actual: string, jobId: string, activityId: string, dimensionalAddress: string) {
133
+ super("Generational Error");
134
+ this.expected = expected;
135
+ this.actual = actual;
136
+ this.jobId = jobId;
137
+ this.activityId = activityId;
138
+ this.dimensionalAddress = dimensionalAddress;
139
+ }
140
+ }
141
+
125
142
  class ExecActivityError extends Error {
126
143
  constructor() {
127
144
  super("Error occurred while executing activity");
@@ -155,6 +172,7 @@ export {
155
172
  DurableWaitForSignalError,
156
173
  DuplicateJobError,
157
174
  ExecActivityError,
175
+ GenerationalError,
158
176
  GetStateError,
159
177
  InactiveJobError,
160
178
  MapDataError,
package/modules/key.ts CHANGED
@@ -31,25 +31,25 @@ const HMNS = "hmsh";
31
31
 
32
32
  //these are the entity types that are stored in the key/value store
33
33
  enum KeyType {
34
- APP,
35
- ENGINE_ID,
36
- HOOKS,
37
- JOB_DEPENDENTS,
38
- JOB_STATE,
39
- JOB_STATS_GENERAL,
40
- JOB_STATS_MEDIAN,
41
- JOB_STATS_INDEX,
42
- HOTMESH,
43
- QUORUM,
44
- SCHEMAS,
45
- SIGNALS,
46
- STREAMS,
47
- SUBSCRIPTIONS,
48
- SUBSCRIPTION_PATTERNS,
49
- SYMKEYS,
50
- SYMVALS,
51
- TIME_RANGE,
52
- WORK_ITEMS,
34
+ APP = 'APP',
35
+ ENGINE_ID = 'ENGINE',
36
+ HOOKS = 'HOOKS',
37
+ JOB_DEPENDENTS = 'JOB_DEPENDENTS',
38
+ JOB_STATE = 'JOB_STATE',
39
+ JOB_STATS_GENERAL = 'JOB_STATS_GENERAL',
40
+ JOB_STATS_MEDIAN = 'JOB_STATS_MEDIAN',
41
+ JOB_STATS_INDEX = 'JOB_STATS_INDEX',
42
+ HOTMESH = 'HOTMESH',
43
+ QUORUM = 'QUORUM',
44
+ SCHEMAS = 'SCHEMAS',
45
+ SIGNALS = 'SIGNALS',
46
+ STREAMS = 'STREAMS',
47
+ SUBSCRIPTIONS = 'SUBSCRIPTIONS',
48
+ SUBSCRIPTION_PATTERNS = 'SUBSCRIPTION_PATTERNS',
49
+ SYMKEYS = 'SYMKEYS',
50
+ SYMVALS = 'SYMVALS',
51
+ TIME_RANGE = 'TIME_RANGE',
52
+ WORK_ITEMS = 'WORK_ITEMS',
53
53
  }
54
54
 
55
55
  //when minting a key, the following parameters are used to create a unique key per entity
@@ -64,6 +64,7 @@ type KeyStoreParams = {
64
64
  facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
65
65
  topic?: string; //topic name (e.g., "foo" or "" for top-level)
66
66
  timeValue?: number; //time value (rounded to minute) (for delete range)
67
+ scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
67
68
  };
68
69
 
69
70
  class KeyService {
@@ -86,7 +87,7 @@ class KeyService {
86
87
  case KeyType.ENGINE_ID:
87
88
  return `${namespace}:${params.appId}:e:${params.engineId}`;
88
89
  case KeyType.WORK_ITEMS:
89
- return `${namespace}:${params.appId}:w:`;
90
+ return `${namespace}:${params.appId}:w:${params.scoutType || ''}`;
90
91
  case KeyType.TIME_RANGE:
91
92
  return `${namespace}:${params.appId}:t:${params.timeValue || ''}`;
92
93
  case KeyType.APP:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.36",
3
+ "version": "0.0.37",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,4 +1,9 @@
1
- import { CollationError, GetStateError, InactiveJobError } from '../../modules/errors';
1
+ import { EXPIRE_DURATION } from '../../modules/enums';
2
+ import {
3
+ CollationError,
4
+ GenerationalError,
5
+ GetStateError,
6
+ InactiveJobError } from '../../modules/errors';
2
7
  import {
3
8
  formatISODate,
4
9
  getValueByPath,
@@ -30,7 +35,6 @@ import {
30
35
  StreamDataType,
31
36
  StreamStatus } from '../../types/stream';
32
37
  import { TransitionRule } from '../../types/transition';
33
- import { EXPIRE_DURATION } from '../../modules/enums';
34
38
 
35
39
  /**
36
40
  * The base class for all activities
@@ -71,6 +75,21 @@ class Activity {
71
75
  this.leg = leg;
72
76
  }
73
77
 
78
+ /**
79
+ * Upon entering leg 1 of a duplexed activty, verify
80
+ * all aspects of the entry including job and activty state
81
+ */
82
+ async verifyEntry() {
83
+ this.setLeg(1);
84
+ await this.getState();
85
+ CollatorService.assertJobActive(
86
+ this.context.metadata.js,
87
+ this.context.metadata.jid,
88
+ this.metadata.aid
89
+ );
90
+ await CollatorService.notarizeEntry(this);
91
+ }
92
+
74
93
  //******** DUPLEX RE-ENTRY POINT ********//
75
94
  async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output'): Promise<void> {
76
95
  this.setLeg(2);
@@ -116,6 +135,9 @@ class Activity {
116
135
  } else if (error instanceof InactiveJobError) {
117
136
  this.logger.info('process-event-inactive-job-error', { error });
118
137
  return;
138
+ } else if (error instanceof GenerationalError) {
139
+ this.logger.info('process-event-generational-job-error', { error });
140
+ return;
119
141
  } else if (error instanceof GetStateError) {
120
142
  this.logger.info('process-event-get-job-error', { error });
121
143
  return;
@@ -337,7 +359,7 @@ class Activity {
337
359
  }
338
360
 
339
361
  async getState() {
340
- //assemble list of paths necessary to create 'job state' from the 'symbol hash'
362
+ const gid = this.context.metadata.gid;
341
363
  const jobSymbolHashName = `$${this.config.subscribes}`;
342
364
  const consumes: Consumes = {
343
365
  [jobSymbolHashName]: MDATA_SYMBOLS.JOB.KEYS.map((key) => `metadata/${key}`)
@@ -365,11 +387,28 @@ class Activity {
365
387
  //`state` is a flat hash; context is a tree
366
388
  const [state, status] = await this.store.getState(jid, consumes, dIds);
367
389
  this.context = restoreHierarchy(state) as JobState;
390
+ this.assertGenerationalId(this.context.metadata.gid, gid);
368
391
  this.initDimensionalAddress(dad);
369
392
  this.initSelf(this.context);
370
393
  this.initPolicies(this.context);
371
394
  }
372
395
 
396
+ /**
397
+ * if the job is created/deleted/created with the same key,
398
+ * the 'gid' ensures no stale messages enter the stream
399
+ */
400
+ assertGenerationalId(jobGID: string, msgGID?: string) {
401
+ if (msgGID !== jobGID) {
402
+ throw new GenerationalError(
403
+ jobGID,
404
+ msgGID,
405
+ this.context.metadata.jid,
406
+ this.context.metadata.aid,
407
+ this.context.metadata.dad
408
+ );
409
+ }
410
+ }
411
+
373
412
  initDimensionalAddress(dad: string): void {
374
413
  this.metadata.dad = dad;
375
414
  }
@@ -434,6 +473,7 @@ class Activity {
434
473
  metadata: {
435
474
  guid: guid(),
436
475
  jid: this.context.metadata.jid,
476
+ gid: this.context.metadata.gid,
437
477
  dad: adjacentDad,
438
478
  aid: toActivityId,
439
479
  spn: this.context['$self'].output.metadata?.l2s,
@@ -466,7 +506,7 @@ class Activity {
466
506
  if (adjacencyList.length && jobStatus > 0) {
467
507
  const multi = this.store.getMulti();
468
508
  for (const execSignal of adjacencyList) {
469
- await this.engine.streamSignaler?.publishMessage(null, execSignal, multi);
509
+ await this.engine.router?.publishMessage(null, execSignal, multi);
470
510
  }
471
511
  mIds = (await multi.exec()) as string[];
472
512
  }
@@ -1,7 +1,11 @@
1
- import { GetStateError, InactiveJobError } from '../../modules/errors';
1
+ import {
2
+ GenerationalError,
3
+ GetStateError,
4
+ InactiveJobError } from '../../modules/errors';
2
5
  import { Activity } from './activity';
3
6
  import { CollatorService } from '../collator';
4
7
  import { EngineService } from '../engine';
8
+ import { TelemetryService } from '../telemetry';
5
9
  import {
6
10
  ActivityData,
7
11
  ActivityMetadata,
@@ -10,7 +14,6 @@ import {
10
14
  import { JobState } from '../../types/job';
11
15
  import { MultiResponseFlags, RedisMulti } from '../../types/redis';
12
16
  import { StreamData, StreamDataType } from '../../types/stream';
13
- import { TelemetryService } from '../telemetry';
14
17
  import { Pipe } from '../pipe';
15
18
  import { guid } from '../../modules/utils';
16
19
 
@@ -29,14 +32,11 @@ class Await extends Activity {
29
32
 
30
33
  //******** INITIAL ENTRY POINT (A) ********//
31
34
  async process(): Promise<string> {
32
- this.logger.debug('await-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
35
+ this.logger.debug('await-process', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
33
36
  let telemetry: TelemetryService;
34
37
  try {
35
- //confirm entry is allowed and restore state
36
- this.setLeg(1);
37
- await CollatorService.notarizeEntry(this);
38
- await this.getState();
39
- CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
38
+ await this.verifyEntry();
39
+
40
40
  telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
41
41
  telemetry.startActivitySpan(this.leg);
42
42
  this.mapInputData();
@@ -62,6 +62,9 @@ class Await extends Activity {
62
62
  if (error instanceof InactiveJobError) {
63
63
  this.logger.error('await-inactive-job-error', { error });
64
64
  return;
65
+ } else if (error instanceof GenerationalError) {
66
+ this.logger.info('process-event-generational-job-error', { error });
67
+ return;
65
68
  } else if (error instanceof GetStateError) {
66
69
  this.logger.error('await-get-state-error', { error });
67
70
  return;
@@ -72,7 +75,7 @@ class Await extends Activity {
72
75
  throw error;
73
76
  } finally {
74
77
  telemetry?.endActivitySpan();
75
- this.logger.debug('await-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
78
+ this.logger.debug('await-process-end', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
76
79
  }
77
80
  }
78
81
 
@@ -82,6 +85,7 @@ class Await extends Activity {
82
85
  metadata: {
83
86
  guid: guid(),
84
87
  jid: this.context.metadata.jid,
88
+ gid: this.context.metadata.gid,
85
89
  dad: this.metadata.dad,
86
90
  aid: this.metadata.aid,
87
91
  topic,
@@ -96,7 +100,7 @@ class Await extends Activity {
96
100
  retry: this.config.retry
97
101
  };
98
102
  }
99
- return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
103
+ return (await this.engine.router?.publishMessage(null, streamData, multi)) as string;
100
104
  }
101
105
  }
102
106
 
@@ -1,4 +1,7 @@
1
- import { GetStateError, InactiveJobError } from '../../modules/errors';
1
+ import {
2
+ GenerationalError,
3
+ GetStateError,
4
+ InactiveJobError } from '../../modules/errors';
2
5
  import { CollatorService } from '../collator';
3
6
  import { EngineService } from '../engine';
4
7
  import { Activity, ActivityType } from './activity';
@@ -28,14 +31,11 @@ class Cycle extends Activity {
28
31
 
29
32
  //******** LEG 1 ENTRY ********//
30
33
  async process(): Promise<string> {
31
- this.logger.debug('cycle-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
34
+ this.logger.debug('cycle-process', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
32
35
  let telemetry: TelemetryService;
33
36
  try {
34
- //verify entry is allowed
35
- this.setLeg(1);
36
- await CollatorService.notarizeEntry(this);
37
- await this.getState();
38
- CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
37
+ await this.verifyEntry();
38
+
39
39
  telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
40
40
  telemetry.startActivitySpan(this.leg);
41
41
  this.mapInputData();
@@ -65,6 +65,9 @@ class Cycle extends Activity {
65
65
  if (error instanceof InactiveJobError) {
66
66
  this.logger.error('cycle-inactive-job-error', { error });
67
67
  return;
68
+ } else if (error instanceof GenerationalError) {
69
+ this.logger.info('process-event-generational-job-error', { error });
70
+ return;
68
71
  } else if (error instanceof GetStateError) {
69
72
  this.logger.error('cycle-get-state-error', { error });
70
73
  return;
@@ -75,7 +78,7 @@ class Cycle extends Activity {
75
78
  throw error;
76
79
  } finally {
77
80
  telemetry?.endActivitySpan();
78
- this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
81
+ this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
79
82
  }
80
83
  }
81
84
 
@@ -95,15 +98,16 @@ class Cycle extends Activity {
95
98
  const streamData: StreamData = {
96
99
  metadata: {
97
100
  guid: guid(),
98
- dad: CollatorService.resolveReentryDimension(this),
99
101
  jid: this.context.metadata.jid,
102
+ gid: this.context.metadata.gid,
103
+ dad: CollatorService.resolveReentryDimension(this),
100
104
  aid: this.config.ancestor,
101
105
  spn: this.context['$self'].output.metadata?.l1s,
102
106
  trc: this.context.metadata.trc,
103
107
  },
104
108
  data: this.context.data
105
109
  };
106
- return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
110
+ return (await this.engine.router?.publishMessage(null, streamData, multi)) as string;
107
111
  }
108
112
  }
109
113