@hotmeshio/hotmesh 0.0.41 → 0.0.43

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 (44) hide show
  1. package/build/modules/enums.d.ts +2 -0
  2. package/build/modules/enums.js +4 -1
  3. package/build/modules/errors.d.ts +1 -8
  4. package/build/modules/errors.js +1 -12
  5. package/build/modules/utils.js +1 -1
  6. package/build/package.json +1 -1
  7. package/build/services/activities/activity.d.ts +8 -1
  8. package/build/services/activities/activity.js +17 -12
  9. package/build/services/collator/index.d.ts +20 -2
  10. package/build/services/collator/index.js +41 -7
  11. package/build/services/durable/client.d.ts +2 -1
  12. package/build/services/durable/client.js +17 -3
  13. package/build/services/durable/factory.d.ts +0 -1
  14. package/build/services/durable/factory.js +0 -138
  15. package/build/services/durable/meshos.js +3 -0
  16. package/build/services/durable/worker.js +0 -15
  17. package/build/services/durable/workflow.d.ts +0 -9
  18. package/build/services/durable/workflow.js +0 -29
  19. package/build/services/engine/index.d.ts +1 -1
  20. package/build/services/engine/index.js +5 -8
  21. package/build/services/quorum/index.d.ts +5 -2
  22. package/build/services/quorum/index.js +32 -15
  23. package/build/services/store/clients/redis.js +1 -0
  24. package/build/services/store/index.d.ts +13 -1
  25. package/build/services/store/index.js +22 -6
  26. package/build/types/hotmesh.d.ts +1 -1
  27. package/build/types/job.d.ts +1 -0
  28. package/modules/enums.ts +4 -0
  29. package/modules/errors.ts +0 -15
  30. package/modules/utils.ts +1 -1
  31. package/package.json +1 -1
  32. package/services/activities/activity.ts +30 -15
  33. package/services/collator/index.ts +41 -8
  34. package/services/durable/client.ts +19 -4
  35. package/services/durable/factory.ts +0 -138
  36. package/services/durable/meshos.ts +3 -0
  37. package/services/durable/worker.ts +0 -16
  38. package/services/durable/workflow.ts +0 -32
  39. package/services/engine/index.ts +5 -6
  40. package/services/quorum/index.ts +35 -12
  41. package/services/store/clients/redis.ts +1 -0
  42. package/services/store/index.ts +25 -7
  43. package/types/hotmesh.ts +1 -1
  44. package/types/job.ts +1 -0
@@ -226,6 +226,7 @@ class EngineService {
226
226
  });
227
227
  const context = {
228
228
  metadata: {
229
+ guid: streamData.metadata.guid,
229
230
  jid: streamData.metadata.jid,
230
231
  gid: streamData.metadata.gid,
231
232
  dad: streamData.metadata.dad,
@@ -353,15 +354,11 @@ class EngineService {
353
354
  };
354
355
  return await this.router.publishMessage(null, streamData);
355
356
  }
356
- async hookTime(jobId, gId, activityId, type) {
357
- if (type === 'interrupt') {
358
- return await this.interrupt(activityId, //note: 'activityId' is the actually job topic
359
- jobId, { suppress: true, expire: 1 });
357
+ async hookTime(jobId, gId, topicOrActivity, type) {
358
+ if (type === 'interrupt' || type === 'expire') {
359
+ return await this.interrupt(topicOrActivity, jobId, { suppress: true, expire: 1 });
360
360
  }
361
- else if (type === 'expire') {
362
- return await this.store.expireJob(jobId, 1);
363
- }
364
- const [aid, ...dimensions] = activityId.split(',');
361
+ const [aid, ...dimensions] = topicOrActivity.split(',');
365
362
  const dad = `,${dimensions.join(',')}`;
366
363
  const streamData = {
367
364
  type: stream_1.StreamDataType.TIMEHOOK,
@@ -3,8 +3,8 @@ import { ILogger } from '../logger';
3
3
  import { StoreService } from '../store';
4
4
  import { SubService } from '../sub';
5
5
  import { CacheMode } from '../../types/cache';
6
- import { QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
7
6
  import { HotMeshConfig } from '../../types/hotmesh';
7
+ import { QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
8
8
  import { RedisClient, RedisMulti } from '../../types/redis';
9
9
  declare class QuorumService {
10
10
  namespace: string;
@@ -30,6 +30,9 @@ declare class QuorumService {
30
30
  sub(callback: QuorumMessageCallback): Promise<void>;
31
31
  unsub(callback: QuorumMessageCallback): Promise<void>;
32
32
  rollCall(delay?: number): Promise<QuorumProfile[]>;
33
- activate(version: string, delay?: number): Promise<boolean>;
33
+ /**
34
+ * request a quorum; if successful activate the app version
35
+ */
36
+ activate(version: string, delay?: number, count?: number): Promise<boolean>;
34
37
  }
35
38
  export { QuorumService };
@@ -1,15 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.QuorumService = void 0;
4
- const key_1 = require("../../modules/key");
4
+ const enums_1 = require("../../modules/enums");
5
5
  const utils_1 = require("../../modules/utils");
6
6
  const compiler_1 = require("../compiler");
7
7
  const redis_1 = require("../store/clients/redis");
8
8
  const ioredis_1 = require("../store/clients/ioredis");
9
9
  const ioredis_2 = require("../sub/clients/ioredis");
10
10
  const redis_2 = require("../sub/clients/redis");
11
- //wait time to see if quorum is reached
12
- const QUORUM_DELAY = 250;
11
+ const hotmesh_1 = require("../../types/hotmesh");
13
12
  class QuorumService {
14
13
  constructor() {
15
14
  this.profiles = [];
@@ -30,8 +29,10 @@ class QuorumService {
30
29
  //note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
31
30
  await instance.initStoreChannel(config.engine.store);
32
31
  await instance.initSubChannel(config.engine.sub);
33
- await instance.subscribe.subscribe(key_1.KeyType.QUORUM, instance.subscriptionHandler(), appId); //general quorum subscription
34
- await instance.subscribe.subscribe(key_1.KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid); //app-specific quorum subscription (used for pubsub one-time request/response)
32
+ //general quorum subscription
33
+ await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId);
34
+ //app-specific quorum subscription (used for pubsub one-time request/response)
35
+ await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid);
35
36
  instance.engine.processWebHooks();
36
37
  instance.engine.processTimeHooks();
37
38
  return instance;
@@ -102,20 +103,20 @@ class QuorumService {
102
103
  engine_id: this.guid,
103
104
  namespace: this.namespace,
104
105
  app_id: this.appId,
105
- stream: this.engine.stream.mintKey(key_1.KeyType.STREAMS, { appId: this.appId })
106
+ stream: this.engine.stream.mintKey(hotmesh_1.KeyType.STREAMS, { appId: this.appId })
106
107
  };
107
108
  }
108
- this.store.publish(key_1.KeyType.QUORUM, {
109
+ this.store.publish(hotmesh_1.KeyType.QUORUM, {
109
110
  type: 'pong',
110
111
  guid, originator,
111
112
  profile,
112
113
  }, appId);
113
114
  }
114
- async requestQuorum(delay = QUORUM_DELAY, details = false) {
115
+ async requestQuorum(delay = enums_1.HMSH_QUORUM_DELAY_MS, details = false) {
115
116
  const quorum = this.quorum;
116
117
  this.quorum = 0;
117
118
  this.profiles.length = 0;
118
- await this.store.publish(key_1.KeyType.QUORUM, {
119
+ await this.store.publish(hotmesh_1.KeyType.QUORUM, {
119
120
  type: 'ping',
120
121
  originator: this.guid,
121
122
  details,
@@ -126,7 +127,7 @@ class QuorumService {
126
127
  // ************* PUB/SUB METHODS *************
127
128
  //publish a message to the quorum
128
129
  async pub(quorumMessage) {
129
- return await this.store.publish(key_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
130
+ return await this.store.publish(hotmesh_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
130
131
  }
131
132
  //subscribe user to quorum messages
132
133
  async sub(callback) {
@@ -139,7 +140,7 @@ class QuorumService {
139
140
  this.callbacks = this.callbacks.filter(cb => cb !== callback);
140
141
  }
141
142
  // ************* COMPILER METHODS *************
142
- async rollCall(delay = QUORUM_DELAY) {
143
+ async rollCall(delay = enums_1.HMSH_QUORUM_DELAY_MS) {
143
144
  await this.requestQuorum(delay, true);
144
145
  const targetStreams = [];
145
146
  const multi = this.store.getMulti();
@@ -160,18 +161,29 @@ class QuorumService {
160
161
  });
161
162
  return this.profiles;
162
163
  }
163
- async activate(version, delay = QUORUM_DELAY) {
164
+ /**
165
+ * request a quorum; if successful activate the app version
166
+ */
167
+ async activate(version, delay = enums_1.HMSH_QUORUM_DELAY_MS, count = 0) {
164
168
  version = version.toString();
169
+ const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
170
+ if (!canActivate) {
171
+ //another engine is already activating the app version
172
+ this.logger.debug('quorum-activation-awaiting', { version });
173
+ await (0, utils_1.sleepFor)(delay * 6);
174
+ const app = await this.store.getApp(this.appId, true);
175
+ return app?.active == true && app?.version === version;
176
+ }
165
177
  const config = await this.engine.getVID();
166
- //request a quorum to activate the version
167
178
  await this.requestQuorum(delay);
168
179
  const q1 = await this.requestQuorum(delay);
169
180
  const q2 = await this.requestQuorum(delay);
170
181
  const q3 = await this.requestQuorum(delay);
171
182
  if (q1 && q1 === q2 && q2 === q3) {
172
183
  this.logger.info('quorum-rollcall-succeeded', { q1, q2, q3 });
173
- this.store.publish(key_1.KeyType.QUORUM, { type: 'activate', cache_mode: 'nocache', until_version: version }, this.appId);
184
+ this.store.publish(hotmesh_1.KeyType.QUORUM, { type: 'activate', cache_mode: 'nocache', until_version: version }, this.appId);
174
185
  await new Promise(resolve => setTimeout(resolve, delay));
186
+ await this.store.releaseScoutRole('activate');
175
187
  //confirm we received the activation message
176
188
  if (this.engine.untilVersion === version) {
177
189
  this.logger.info('quorum-activation-succeeded', { version });
@@ -185,7 +197,12 @@ class QuorumService {
185
197
  }
186
198
  }
187
199
  else {
188
- this.logger.info('quorum-rollcall-error', { q1, q2, q3 });
200
+ this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
201
+ this.store.releaseScoutRole('activate');
202
+ if (count < enums_1.HMSH_ACTIVATION_MAX_RETRY) {
203
+ //increase the delay (give the quorum time to respond) and try again
204
+ return await this.activate(version, delay * 2, count + 1);
205
+ }
189
206
  throw new Error(`Quorum not reached. Version ${version} not activated.`);
190
207
  }
191
208
  }
@@ -6,6 +6,7 @@ class RedisStoreService extends index_1.StoreService {
6
6
  constructor(redisClient) {
7
7
  super(redisClient);
8
8
  this.commands = {
9
+ set: 'SET',
9
10
  setnx: 'SETNX',
10
11
  del: 'DEL',
11
12
  expire: 'EXPIRE',
@@ -46,7 +46,8 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
46
46
  * check for and process work items in the
47
47
  * time and signal task queues.
48
48
  */
49
- reserveScoutRole(scoutType: 'time' | 'signal', delay?: number): Promise<boolean>;
49
+ reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay?: number): Promise<boolean>;
50
+ releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean>;
50
51
  getSettings(bCreate?: boolean): Promise<HotMeshSettings>;
51
52
  setSettings(manifest: HotMeshSettings): Promise<any>;
52
53
  reserveSymbolRange(target: string, size: number, type: 'JOB' | 'ACTIVITY'): Promise<[number, number, Symbols]>;
@@ -86,7 +87,18 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
86
87
  */
87
88
  getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
88
89
  getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
90
+ /**
91
+ * collate is a generic method for incrementing a value in a hash
92
+ * in order to track their progress during processing.
93
+ */
89
94
  collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi?: U): Promise<number>;
95
+ /**
96
+ * synthentic collation affects those activities in the graph
97
+ * that represent the synthetic DAG that was materialized during compilation;
98
+ * Synthetic targeting ensures that re-entry due to failure can be distinguished from
99
+ * purposeful re-entry.
100
+ */
101
+ collateSynthetic(jobId: string, guid: string, amount: number, multi?: U): Promise<number>;
90
102
  setStateNX(jobId: string, appId: string): Promise<boolean>;
91
103
  getSchema(activityId: string, appVersion: AppVID): Promise<ActivityType>;
92
104
  getSchemas(appVersion: AppVID): Promise<Record<string, ActivityType>>;
@@ -33,6 +33,7 @@ const errors_1 = require("../../modules/errors");
33
33
  class StoreService {
34
34
  constructor(redisClient) {
35
35
  this.commands = {
36
+ set: 'set',
36
37
  setnx: 'setnx',
37
38
  del: 'del',
38
39
  expire: 'expire',
@@ -106,12 +107,13 @@ class StoreService {
106
107
  */
107
108
  async reserveScoutRole(scoutType, delay = enums_1.HMSH_SCOUT_INTERVAL_SECONDS) {
108
109
  const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
109
- const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`);
110
- if (this.isSuccessful(success)) {
111
- await this.redisClient[this.commands.expire](key, delay - 1);
112
- return true;
113
- }
114
- return false;
110
+ const success = await this.exec('SET', key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`, 'NX', 'EX', `${delay - 1}`);
111
+ return this.isSuccessful(success);
112
+ }
113
+ async releaseScoutRole(scoutType) {
114
+ const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
115
+ const success = await this.exec('DEL', key);
116
+ return this.isSuccessful(success);
115
117
  }
116
118
  async getSettings(bCreate = false) {
117
119
  let settings = this.cache?.getSettings();
@@ -485,6 +487,10 @@ class StoreService {
485
487
  throw new errors_1.GetStateError(jobId);
486
488
  }
487
489
  }
490
+ /**
491
+ * collate is a generic method for incrementing a value in a hash
492
+ * in order to track their progress during processing.
493
+ */
488
494
  async collate(jobId, activityId, amount, dIds, multi) {
489
495
  const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
490
496
  const collationKey = `${activityId}/output/metadata/as`; //activity state
@@ -497,6 +503,16 @@ class StoreService {
497
503
  const targetId = Object.keys(hashData)[0];
498
504
  return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, targetId, amount);
499
505
  }
506
+ /**
507
+ * synthentic collation affects those activities in the graph
508
+ * that represent the synthetic DAG that was materialized during compilation;
509
+ * Synthetic targeting ensures that re-entry due to failure can be distinguished from
510
+ * purposeful re-entry.
511
+ */
512
+ async collateSynthetic(jobId, guid, amount, multi) {
513
+ const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
514
+ return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, guid, amount);
515
+ }
500
516
  async setStateNX(jobId, appId) {
501
517
  const hashKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId, jobId });
502
518
  const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', '1');
@@ -42,7 +42,7 @@ type KeyStoreParams = {
42
42
  facet?: string;
43
43
  topic?: string;
44
44
  timeValue?: number;
45
- scoutType?: 'signal' | 'time';
45
+ scoutType?: 'signal' | 'time' | 'activate';
46
46
  };
47
47
  type HotMesh = typeof HotMeshService;
48
48
  type RedisConfig = {
@@ -6,6 +6,7 @@ type ActivityData = {
6
6
  };
7
7
  type JobMetadata = {
8
8
  key?: string;
9
+ guid?: string;
9
10
  gid: string;
10
11
  jid: string;
11
12
  dad: string;
package/modules/enums.ts CHANGED
@@ -22,6 +22,10 @@ export const HMSH_CODE_DURABLE_RETRYABLE = 599;
22
22
 
23
23
  export const HMSH_STATUS_UNKNOWN = 'unknown';
24
24
 
25
+ // QUORUM
26
+ export const HMSH_QUORUM_DELAY_MS = 250;
27
+ export const HMSH_ACTIVATION_MAX_RETRY = 3;
28
+
25
29
  // ENGINE
26
30
  export const HMSH_OTT_WAIT_TIME = parseInt(process.env.HMSH_OTT_WAIT_TIME, 10) || 1000;
27
31
  export const HMSH_EXPIRE_JOB_SECONDS = parseInt(process.env.HMSH_EXPIRE_JOB_SECONDS, 10) || 1;
package/modules/errors.ts CHANGED
@@ -45,20 +45,6 @@ class DurableWaitForSignalError extends Error {
45
45
  }
46
46
  }
47
47
 
48
- /* @deprecated */
49
- class DurableSleepError extends Error {
50
- code: number;
51
- duration: number; //seconds
52
- index: number; //execution order in the workflow
53
- dimension: string; //hook dimension (e.g., ',0,1,0') (uses empty string for `null`)
54
- constructor(message: string, duration: number, index: number, dimension: string) {
55
- super(message);
56
- this.duration = duration;
57
- this.index = index;
58
- this.dimension = dimension;
59
- this.code = 595;
60
- }
61
- }
62
48
  class DurableSleepForError extends Error {
63
49
  code: number;
64
50
  duration: number; //seconds
@@ -175,7 +161,6 @@ export {
175
161
  DurableIncompleteSignalError,
176
162
  DurableMaxedError,
177
163
  DurableRetryError,
178
- DurableSleepError,
179
164
  DurableSleepForError,
180
165
  DurableTimeoutError,
181
166
  DurableWaitForSignalError,
package/modules/utils.ts CHANGED
@@ -10,7 +10,7 @@ export async function sleepFor(ms: number) {
10
10
  }
11
11
 
12
12
  export function guid(): string {
13
- return nanoid();
13
+ return nanoid().replace(/[_-]/g, '0');
14
14
  }
15
15
 
16
16
  export function deterministicRandom(seed: number): number {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.41",
3
+ "version": "0.0.43",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -90,6 +90,22 @@ class Activity {
90
90
  await CollatorService.notarizeEntry(this);
91
91
  }
92
92
 
93
+ /**
94
+ * Upon entering leg 2 of a duplexed activty, verify
95
+ * all aspects of the re-entry including job and activty state
96
+ */
97
+ async verifyReentry(): Promise<number> {
98
+ const guid = this.context.metadata.guid;
99
+ this.setLeg(2);
100
+ await this.getState();
101
+ CollatorService.assertJobActive(
102
+ this.context.metadata.js,
103
+ this.context.metadata.jid,
104
+ this.metadata.aid
105
+ );
106
+ return await CollatorService.notarizeReentry(this, guid);
107
+ }
108
+
93
109
  //******** DUPLEX RE-ENTRY POINT ********//
94
110
  async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output'): Promise<void> {
95
111
  this.setLeg(2);
@@ -103,23 +119,20 @@ class Activity {
103
119
  this.code = code;
104
120
  this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
105
121
  let telemetry: TelemetryService;
106
- try {
107
- await this.getState();
108
- CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
109
- const aState = await CollatorService.notarizeReentry(this);
110
- this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
111
-
112
- telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
113
- let isComplete = CollatorService.isActivityComplete(this.context.metadata.js);
114
122
 
115
- if (isComplete) {
116
- this.logger.warn('activity-process-event-duplicate', { jid, aid });
117
- this.logger.debug('activity-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
118
- return;
119
- }
123
+ try {
124
+ const collationKey = await this.verifyReentry();
120
125
 
126
+ this.adjacentIndex = CollatorService.getDimensionalIndex(collationKey);
127
+ telemetry = new TelemetryService(
128
+ this.engine.appId,
129
+ this.config,
130
+ this.metadata,
131
+ this.context,
132
+ );
121
133
  telemetry.startActivitySpan(this.leg);
122
134
  let multiResponse: MultiResponseFlags;
135
+
123
136
  if (status === StreamStatus.PENDING) {
124
137
  multiResponse = await this.processPending(telemetry, type);
125
138
  } else if (status === StreamStatus.SUCCESS) {
@@ -384,7 +397,7 @@ class Activity {
384
397
  TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
385
398
  let { dad, jid } = this.context.metadata;
386
399
  const dIds = CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad || '');
387
- //`state` is a flat hash; context is a tree
400
+ //`state` is a unidimensional hash; context is a tree
388
401
  const [state, status] = await this.store.getState(jid, consumes, dIds);
389
402
  this.context = restoreHierarchy(state) as JobState;
390
403
  this.assertGenerationalId(this.context.metadata.gid, gid);
@@ -395,7 +408,9 @@ class Activity {
395
408
 
396
409
  /**
397
410
  * if the job is created/deleted/created with the same key,
398
- * the 'gid' ensures no stale messages enter the stream
411
+ * the 'gid' ensures no stale messages (such as sleep delays)
412
+ * enter the workstream. Any message with a mismatched gid
413
+ * belongs to a prior job and can safely be ignored/dropped.
399
414
  */
400
415
  assertGenerationalId(jobGID: string, msgGID?: string) {
401
416
  if (msgGID !== jobGID) {
@@ -78,11 +78,28 @@ class CollatorService {
78
78
  return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_001 - decrement, this.getDimensionalAddress(activity), multi);
79
79
  };
80
80
 
81
- static async notarizeReentry(activity: Activity, multi?: RedisMulti): Promise<number> {
81
+ /**
82
+ * verifies both the concrete and synthetic keys for the activity; concrete keys
83
+ * exist in the original model and are effectively the 'real' keys. In reality,
84
+ * hook activities are atomized during compilation to create a synthetic DAG that
85
+ * is used to track the status of the graph in a distributed environment. The
86
+ * synthetic key represents different dimensional realities and is used to
87
+ * track re-entry overages (it distinguishes between the original and re-entry).
88
+ * The essential challenge is: is this a re-entry that is purposeful in
89
+ * order to induce cycles, or is the re-entry due to a failure in the system?
90
+ */
91
+ static async notarizeReentry(activity: Activity, guid: string, multi?: RedisMulti): Promise<number> {
92
+ const jid = activity.context.metadata.jid;
93
+ const localMulti = multi || activity.store.getMulti();
82
94
  //increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
83
- const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity, true), multi);
84
- this.verifyInteger(amount, 2, 'enter');
85
- return amount;
95
+ await activity.store.collate(jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity, true), localMulti);
96
+ await activity.store.collateSynthetic(jid, guid, 1_000_000, localMulti);
97
+ const [_amountConcrete, _amountSynthetic] = await localMulti.exec();
98
+ const amountConcrete = Array.isArray(_amountConcrete) ? _amountConcrete[1] : _amountConcrete;
99
+ const amountSynthetic = Array.isArray(_amountSynthetic) ? _amountSynthetic[1] : _amountSynthetic;
100
+ this.verifyInteger(amountConcrete as number, 2, 'enter');
101
+ this.verifySyntheticInteger(amountSynthetic as number);
102
+ return amountConcrete as number;
86
103
  };
87
104
 
88
105
  static async notarizeContinuation(activity: Activity, multi?: RedisMulti): Promise<number> {
@@ -134,6 +151,26 @@ class CollatorService {
134
151
  }
135
152
  }
136
153
 
154
+ /**
155
+ * During compilation, the graphs are compiled into structures necessary
156
+ * for distributed processing; these are referred to as 'synthetic DAGs',
157
+ * because they are not part of the original graph, but are used to track
158
+ * the status of the graph in a distributed environment. This check ensures
159
+ * that the 'synthetic key' is not a duplicate. (which is different than
160
+ * saying the 'key' is not a duplicate)
161
+ */
162
+ static verifySyntheticInteger(amount: number): void {
163
+ const samount = amount.toString();
164
+ const isCompletedValue = parseInt(samount[samount.length - 1], 10);
165
+ if (isCompletedValue > 0) {
166
+ //already done error (ack/delete clearly failed; this is a duplicate)
167
+ throw new CollationError(amount, 2, 'enter', CollationFaultType.INACTIVE);
168
+ } else if (amount >= 2_000_000) {
169
+ //duplicate synthetic key (todo: need to resolve/fix this!!)
170
+ throw new CollationError(amount, 2, 'enter', CollationFaultType.DUPLICATE);
171
+ }
172
+ }
173
+
137
174
  static verifyInteger(amount: number, leg: ActivityDuplex, stage: CollationStage): void {
138
175
  let faultType: CollationFaultType | undefined;
139
176
  if (leg === 1 && stage === 'enter') {
@@ -239,10 +276,6 @@ class CollatorService {
239
276
  });
240
277
  }
241
278
 
242
- static isActivityComplete(status: number): boolean {
243
- return (status - 0) <= 0;
244
- }
245
-
246
279
  /**
247
280
  * All activities exist on a dimensional plane. Zero
248
281
  * is the default. A value of
@@ -11,13 +11,14 @@ import { JobState } from '../../types/job';
11
11
  import { KeyService, KeyType } from '../../modules/key';
12
12
  import { Search } from './search';
13
13
  import { StreamStatus } from '../../types';
14
- import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
14
+ import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
15
+ import { sleepFor } from '../../modules/utils';
15
16
 
16
17
  export class ClientService {
17
18
 
18
19
  connection: Connection;
19
- topics: string[] = [];
20
20
  options: WorkflowOptions;
21
+ static topics: string[] = [];
21
22
  static instances = new Map<string, HotMesh | Promise<HotMesh>>();
22
23
 
23
24
  constructor(config: ClientConfig) {
@@ -29,8 +30,9 @@ export class ClientService {
29
30
  const instanceId = 'SINGLETON';
30
31
  if (ClientService.instances.has(instanceId)) {
31
32
  const hotMeshClient = await ClientService.instances.get(instanceId);
32
- if (!this.topics.includes(workflowTopic)) {
33
- this.topics.push(workflowTopic);
33
+ await this.verifyWorkflowActive(hotMeshClient, namespace ?? APP_ID);
34
+ if (!ClientService.topics.includes(workflowTopic)) {
35
+ ClientService.topics.push(workflowTopic);
34
36
  await this.createStream(hotMeshClient, workflowTopic, namespace);
35
37
  }
36
38
  return hotMeshClient;
@@ -196,6 +198,19 @@ export class ClientService {
196
198
  }
197
199
  }
198
200
 
201
+ async verifyWorkflowActive(hotMesh: HotMesh, appId = APP_ID, count = 0): Promise<boolean> {
202
+ const app = await hotMesh.engine.store.getApp(appId);
203
+ const appVersion = app?.version as unknown as number;
204
+ if(isNaN(appVersion)) {
205
+ if (count > 10) {
206
+ throw new Error('Workflow failed to activate');
207
+ }
208
+ await sleepFor(HMSH_QUORUM_DELAY_MS * 2);
209
+ return await this.verifyWorkflowActive(hotMesh, appId, count + 1);
210
+ }
211
+ return true;
212
+ }
213
+
199
214
  async activateWorkflow(hotMesh: HotMesh, appId = APP_ID, version = APP_VERSION): Promise<void> {
200
215
  const app = await hotMesh.engine.store.getApp(appId);
201
216
  const appVersion = app?.version as unknown as number;