@hotmeshio/hotmesh 0.0.48 → 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 (66) hide show
  1. package/README.md +1 -1
  2. package/build/modules/enums.d.ts +1 -0
  3. package/build/modules/enums.js +2 -1
  4. package/build/modules/key.d.ts +5 -1
  5. package/build/modules/key.js +10 -2
  6. package/build/package.json +2 -1
  7. package/build/services/activities/await.js +6 -0
  8. package/build/services/activities/hook.js +1 -1
  9. package/build/services/activities/trigger.d.ts +1 -0
  10. package/build/services/activities/trigger.js +23 -2
  11. package/build/services/durable/exporter.js +19 -5
  12. package/build/services/durable/meshos.js +11 -6
  13. package/build/services/durable/search.d.ts +20 -1
  14. package/build/services/durable/search.js +73 -25
  15. package/build/services/durable/worker.js +10 -0
  16. package/build/services/durable/workflow.d.ts +1 -0
  17. package/build/services/durable/workflow.js +17 -1
  18. package/build/services/engine/index.d.ts +1 -1
  19. package/build/services/engine/index.js +12 -3
  20. package/build/services/exporter/index.js +3 -2
  21. package/build/services/hotmesh/index.js +4 -0
  22. package/build/services/quorum/index.d.ts +11 -2
  23. package/build/services/quorum/index.js +33 -0
  24. package/build/services/router/index.d.ts +15 -0
  25. package/build/services/router/index.js +55 -7
  26. package/build/services/serializer/index.js +1 -1
  27. package/build/services/store/clients/redis.js +2 -0
  28. package/build/services/store/index.d.ts +6 -4
  29. package/build/services/store/index.js +86 -21
  30. package/build/services/task/index.d.ts +2 -1
  31. package/build/services/task/index.js +30 -13
  32. package/build/services/worker/index.d.ts +13 -2
  33. package/build/services/worker/index.js +44 -3
  34. package/build/types/activity.d.ts +1 -0
  35. package/build/types/durable.d.ts +9 -0
  36. package/build/types/exporter.d.ts +2 -0
  37. package/build/types/job.d.ts +1 -0
  38. package/build/types/quorum.d.ts +22 -8
  39. package/build/types/stream.d.ts +1 -0
  40. package/modules/enums.ts +1 -0
  41. package/modules/key.ts +7 -2
  42. package/package.json +2 -1
  43. package/services/activities/await.ts +6 -0
  44. package/services/activities/hook.ts +1 -0
  45. package/services/activities/trigger.ts +25 -1
  46. package/services/durable/exporter.ts +18 -7
  47. package/services/durable/meshos.ts +10 -6
  48. package/services/durable/search.ts +73 -26
  49. package/services/durable/worker.ts +13 -1
  50. package/services/durable/workflow.ts +18 -0
  51. package/services/engine/index.ts +13 -5
  52. package/services/exporter/index.ts +3 -2
  53. package/services/hotmesh/index.ts +4 -0
  54. package/services/quorum/index.ts +38 -2
  55. package/services/router/index.ts +59 -9
  56. package/services/serializer/index.ts +1 -1
  57. package/services/store/clients/redis.ts +2 -0
  58. package/services/store/index.ts +108 -22
  59. package/services/task/index.ts +31 -11
  60. package/services/worker/index.ts +49 -5
  61. package/types/activity.ts +1 -0
  62. package/types/durable.ts +11 -0
  63. package/types/exporter.ts +2 -0
  64. package/types/job.ts +1 -0
  65. package/types/quorum.ts +28 -13
  66. package/types/stream.ts +1 -0
@@ -11,12 +11,14 @@ import { KeyType } from '../../types/hotmesh';
11
11
  import { JobCompletionOptions, JobState } from '../../types/job';
12
12
  import { RedisClient, RedisMulti } from '../../types/redis';
13
13
  import { WorkListTaskType } from '../../types/task';
14
+ import { VALSEP, WEBSEP } from '../../modules/key';
14
15
 
15
16
  class TaskService {
16
17
  store: StoreService<RedisClient, RedisMulti>;
17
18
  logger: ILogger;
18
19
  cleanupTimeout: NodeJS.Timeout | null = null;
19
20
  isScout: boolean = false;
21
+ errorCount = 0;
20
22
 
21
23
  constructor(
22
24
  store: StoreService<RedisClient, RedisMulti>,
@@ -29,8 +31,8 @@ class TaskService {
29
31
  async processWebHooks(hookEventCallback: HookInterface): Promise<void> {
30
32
  const workItemKey = await this.store.getActiveTaskQueue();
31
33
  if (workItemKey) {
32
- const [topic, sourceKey, scrub, ...sdata] = workItemKey.split('::');
33
- const data = JSON.parse(sdata.join('::'));
34
+ const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(WEBSEP);
35
+ const data = JSON.parse(sdata.join(WEBSEP));
34
36
  const destinationKey = `${sourceKey}:processed`;
35
37
  const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
36
38
  if (jobId) {
@@ -72,6 +74,7 @@ class TaskService {
72
74
  activityId: string,
73
75
  type: WorkListTaskType,
74
76
  inSeconds = HMSH_FIDELITY_SECONDS,
77
+ dad: string,
75
78
  multi?: RedisMulti,
76
79
  ): Promise<void> {
77
80
  const fromNow = Date.now() + (inSeconds * 1000);
@@ -83,6 +86,7 @@ class TaskService {
83
86
  activityId,
84
87
  type,
85
88
  awakenTimeSlot,
89
+ dad,
86
90
  multi,
87
91
  );
88
92
  }
@@ -129,21 +133,29 @@ class TaskService {
129
133
  await timeEventCallback(target, gId, activityId, type);
130
134
  }
131
135
  await sleepFor(0);
136
+ this.errorCount = 0;
132
137
  this.processTimeHooks(timeEventCallback, listKey);
133
138
  } else if (workListTask) {
134
139
  //a worklist was just emptied; try again immediately
135
140
  await sleepFor(0);
141
+ this.errorCount = 0;
136
142
  this.processTimeHooks(timeEventCallback);
137
143
  } else {
138
144
  //no worklists exist; sleep before checking
139
145
  let sleep = XSleepFor(HMSH_FIDELITY_SECONDS * 1000);
140
146
  this.cleanupTimeout = sleep.timerId;
141
147
  await sleep.promise;
148
+ this.errorCount = 0;
142
149
  this.processTimeHooks(timeEventCallback);
143
150
  }
144
151
  } catch (err) {
145
- //todo: retry connect to redis
146
- this.logger.error('task-process-timehooks-error', err);
152
+ //most common reasons: deleted job not found; container stopping; test stopping
153
+ //less common: redis/cluster down; retry with fallback (5s max main reassignment)
154
+ this.logger.warn('task-process-timehooks-error', err);
155
+ await sleepFor(1_000 * this.errorCount++);
156
+ if (this.errorCount < 5) {
157
+ this.processTimeHooks(timeEventCallback);
158
+ }
147
159
  }
148
160
  } else {
149
161
  //didn't get the scout role; try again in 'one-ish' minutes
@@ -174,10 +186,18 @@ class TaskService {
174
186
  const jobId = context.metadata.jid;
175
187
  const gId = context.metadata.gid;
176
188
  const activityId = hookRule.to;
189
+ //composite keys are used to fully describe the task target
190
+ const compositeJobKey = [
191
+ activityId,
192
+ dad,
193
+ gId,
194
+ jobId
195
+ ].join(WEBSEP);
196
+
177
197
  const hook: HookSignal = {
178
198
  topic,
179
199
  resolved,
180
- jobId: `${activityId}::${dad}::${gId}::${jobId}`,
200
+ jobId: compositeJobKey,
181
201
  }
182
202
  await this.store.setHookSignal(hook, multi);
183
203
  return jobId;
@@ -196,17 +216,17 @@ class TaskService {
196
216
  const resolved = Pipe.resolve(mapExpression, context);
197
217
  const hookSignalId = await this.store.getHookSignal(topic, resolved);
198
218
  if (!hookSignalId) {
199
- //messages can be double-processed; not an issue; return undefined
200
- //users can also provide a bogus topic; not an issue; return undefined
219
+ //messages can be double-processed; not an issue; return `undefined`
220
+ //users can also provide a bogus topic; not an issue; return `undefined`
201
221
  return undefined;
202
222
  }
203
- //`aid` is part of composit key, but the hook `topic` is its public interface;
223
+ //`aid` is part of composite key, but the hook `topic` is its public interface;
204
224
  // this means that a new version of the graph can be deployed and the
205
225
  // topic can be re-mapped to a different activity id. Outside callers
206
226
  // can adhere to the unchanged contract (calling the same topic),
207
- // while the internal system can be updated in real time as necessary.
208
- const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
209
- return [jid.join('::'), hookRule.to, dad, gid];
227
+ // while the internal system can be updated in real-time as necessary.
228
+ const [_aid, dad, gid, ...jid] = hookSignalId.split(WEBSEP);
229
+ return [jid.join(WEBSEP), hookRule.to, dad, gid];
210
230
  } else {
211
231
  throw new Error('signal-not-found');
212
232
  }
@@ -1,5 +1,5 @@
1
1
  import { KeyType } from "../../modules/key";
2
- import { formatISODate, getSystemHealth, identifyRedisType } from "../../modules/utils";
2
+ import { XSleepFor, formatISODate, getSystemHealth, identifyRedisType, sleepFor } from "../../modules/utils";
3
3
  import { ConnectorService } from "../connector";
4
4
  import { ILogger } from "../logger";
5
5
  import { Router } from "../router";
@@ -17,10 +17,12 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
17
17
  import {
18
18
  QuorumMessage,
19
19
  QuorumProfile,
20
+ RollCallMessage,
20
21
  SubscriptionCallback } from "../../types/quorum";
21
22
  import { RedisClient, RedisMulti } from "../../types/redis";
22
23
  import { RedisClientType } from '../../types/redisclient';
23
- import { StreamRole } from "../../types/stream";
24
+ import { StreamData, StreamRole, StreamDataResponse } from "../../types/stream";
25
+ import { HMSH_QUORUM_ROLLCALL_CYCLES } from "../../modules/enums";
24
26
 
25
27
  class WorkerService {
26
28
  namespace: string;
@@ -28,6 +30,7 @@ class WorkerService {
28
30
  guid: string;
29
31
  topic: string;
30
32
  config: HotMeshConfig;
33
+ callback: (streamData: StreamData) => Promise<StreamDataResponse|void>;
31
34
  store: StoreService<RedisClient, RedisMulti> | null;
32
35
  stream: StreamService<RedisClient, RedisMulti> | null;
33
36
  subscribe: SubService<RedisClient, RedisMulti> | null;
@@ -35,6 +38,7 @@ class WorkerService {
35
38
  logger: ILogger;
36
39
  reporting = false;
37
40
  inited: string;
41
+ rollCallInterval: NodeJS.Timeout;
38
42
 
39
43
  static async init(
40
44
  namespace: string,
@@ -58,6 +62,7 @@ class WorkerService {
58
62
  service.namespace = namespace;
59
63
  service.appId = appId;
60
64
  service.guid = guid;
65
+ service.callback = worker.callback;
61
66
  service.topic = worker.topic;
62
67
  service.config = config;
63
68
  service.logger = logger;
@@ -155,14 +160,51 @@ class WorkerService {
155
160
  return async (topic: string, message: QuorumMessage) => {
156
161
  self.logger.debug('worker-event-received', { topic, type: message.type });
157
162
  if (message.type === 'throttle') {
158
- self.throttle(message.throttle);
163
+ if (message.topic !== null) { //undefined allows passthrough
164
+ self.throttle(message.throttle);
165
+ }
159
166
  } else if(message.type === 'ping') {
160
167
  self.sayPong(self.appId, self.guid, message.originator, message.details);
168
+ } else if(message.type === 'rollcall') {
169
+ if (message.topic !== null) { //undefined allows passthrough
170
+ self.doRollCall(message);
171
+ }
161
172
  }
162
173
  };
163
174
  }
164
175
 
165
- async sayPong(appId: string, guid: string, originator: string, details = false) {
176
+ /**
177
+ * A quorum-wide command to broadcaset system details.
178
+ *
179
+ */
180
+ async doRollCall(message: RollCallMessage) {
181
+ let iteration = 0;
182
+ let max = !isNaN(message.max) ? message.max : HMSH_QUORUM_ROLLCALL_CYCLES;
183
+ if (this.rollCallInterval) clearTimeout(this.rollCallInterval);
184
+ const base = (message.interval / 2);
185
+ const amount = base + Math.ceil(Math.random() * base);
186
+ do {
187
+ await sleepFor(Math.ceil(Math.random() * 1000));
188
+ await this.sayPong(this.appId, this.guid, null, true, message.signature);
189
+ if (!message.interval) return;
190
+ const { promise, timerId } = XSleepFor(amount * 1000);
191
+ this.rollCallInterval = timerId;
192
+ await promise;
193
+ } while (this.rollCallInterval && iteration++ < max - 1);
194
+ }
195
+
196
+ cancelRollCall() {
197
+ if (this.rollCallInterval) {
198
+ clearTimeout(this.rollCallInterval);
199
+ delete this.rollCallInterval;
200
+ }
201
+ }
202
+
203
+ stop() {
204
+ this.cancelRollCall();
205
+ }
206
+
207
+ async sayPong(appId: string, guid: string, originator?: string, details = false, signature = false) {
166
208
  let profile: QuorumProfile;
167
209
  if (details) {
168
210
  const params = {
@@ -183,13 +225,15 @@ class WorkerService {
183
225
  reclaimDelay: this.router.reclaimDelay,
184
226
  reclaimCount: this.router.reclaimCount,
185
227
  system: await getSystemHealth(),
228
+ signature: signature ? this.callback.toString() : undefined,
186
229
  };
187
230
  }
188
231
  this.store.publish(
189
232
  KeyType.QUORUM,
190
233
  {
191
234
  type: 'pong',
192
- guid, originator,
235
+ guid,
236
+ originator,
193
237
  profile,
194
238
  },
195
239
  appId,
package/types/activity.ts CHANGED
@@ -77,6 +77,7 @@ interface AwaitActivity extends BaseActivity {
77
77
  type: 'await';
78
78
  eventName: string;
79
79
  timeout: number;
80
+ await?: boolean; //if exlicitly false do not await the response
80
81
  }
81
82
 
82
83
  interface WorkerActivity extends BaseActivity {
package/types/durable.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { LogLevel } from './logger';
2
2
  import { RedisClass, RedisOptions } from './redis';
3
+ import { StringStringType } from './serializer';
3
4
 
4
5
  type WorkflowConfig = {
5
6
  backoffCoefficient?: number; //default 10
@@ -15,6 +16,16 @@ type WorkflowContext = {
15
16
  */
16
17
  counter: number;
17
18
 
19
+ /**
20
+ * number as string for the replay cursor
21
+ */
22
+ cursor: string;
23
+
24
+ /**
25
+ * the replay hash of name/value pairs representing prior executions
26
+ */
27
+ replay: StringStringType;
28
+
18
29
  /**
19
30
  * the HotMesh App namespace. `durable` is the default.
20
31
  */
package/types/exporter.ts CHANGED
@@ -26,6 +26,8 @@ export interface JobTimeline {
26
26
  dimension: string; //dimensional isolate path
27
27
  duplex: 'entry' | 'exit'; //activity entry or exit
28
28
  timestamp: string; //actually a number but too many digits for JS
29
+ created?: string; //actually a number but too many digits for JS
30
+ updated?: string; //actually a number but too many digits for JS
29
31
  actions?: ActivityAction[];
30
32
  }
31
33
 
package/types/job.ts CHANGED
@@ -17,6 +17,7 @@ type JobMetadata = {
17
17
  pg?: string; //parent_generational_id (system assigned at trigger inception); pg is the parent job's gid (just in case user created/deleted/created a job with same jid)
18
18
  pd?: string; //parent_dimensional_address
19
19
  pa?: string; //parent_activity_id
20
+ px?: boolean; //sever the dependency chain if true (startChild/vs/executeChild)
20
21
  ngn?: string; //engine guid (one time subscriptions)
21
22
  app: string; //app_id
22
23
  vrs: string; //app version
package/types/quorum.ts CHANGED
@@ -50,50 +50,65 @@ export interface QuorumProfile {
50
50
  reclaimDelay?: number;
51
51
  reclaimCount?: number;
52
52
  system?: SystemHealth;
53
+ signature?: string; //stringified function
53
54
  }
54
55
 
55
- //used for coordination (like version activation)
56
- export interface PingMessage {
56
+ interface QuorumMessageBase {
57
+ guid?: string;
58
+ topic?: string;
59
+ type?: string;
60
+ }
61
+
62
+ // Messages extending QuorumMessageBase
63
+ export interface PingMessage extends QuorumMessageBase {
57
64
  type: 'ping';
58
65
  originator: string; //guid
59
66
  details?: boolean; //if true, all endpoints will include their profile
60
67
  }
61
68
 
62
- export interface WorkMessage {
69
+ export interface WorkMessage extends QuorumMessageBase {
63
70
  type: 'work';
64
71
  originator: string; //guid
65
72
  }
66
73
 
67
- export interface CronMessage {
74
+ export interface CronMessage extends QuorumMessageBase {
68
75
  type: 'cron';
69
76
  originator: string; //guid
70
77
  }
71
78
 
72
- export interface PongMessage {
79
+ export interface PongMessage extends QuorumMessageBase {
73
80
  type: 'pong';
74
81
  guid: string; //call initiator
75
82
  originator: string; //clone of originator guid passed in ping
76
83
  profile?: QuorumProfile; //contains details about the engine/worker
77
84
  }
78
85
 
79
- export interface ActivateMessage {
86
+ export interface ActivateMessage extends QuorumMessageBase {
80
87
  type: 'activate';
81
88
  cache_mode: 'nocache' | 'cache';
82
89
  until_version: string;
83
90
  }
84
91
 
85
- export interface JobMessage {
92
+ export interface JobMessage extends QuorumMessageBase {
86
93
  type: 'job';
87
94
  topic: string; //this comes from the 'publishes' field in the YAML
88
95
  job: JobOutput
89
96
  }
90
97
 
91
- //delay in ms between fetches from the buffered stream (speed/slow down entire network)
92
- export interface ThrottleMessage {
98
+ export interface ThrottleMessage extends QuorumMessageBase {
93
99
  type: 'throttle';
94
- guid?: string; //target the engine quorum
95
- topic?: string; //target a worker quorum
96
- throttle: number; //0-n
100
+ guid?: string; //target engine AND workers with this guid
101
+ topic?: string; //target worker(s) matching this topic (pass null to only target the engine, pass undefined to target engine and workers)
102
+ throttle: number; //0-n; millis
103
+ }
104
+
105
+ export interface RollCallMessage extends QuorumMessageBase {
106
+ type: 'rollcall';
107
+ guid?: string; //target the engine quorum
108
+ topic?: string | null; //target a worker if string; suppress if `null`;
109
+ interval: number; //every 'n' seconds
110
+ max?: number; //max broadcasts
111
+ signature?: boolean; //include bound worker function in broadcast
97
112
  }
98
113
 
99
114
  export interface JobMessageCallback {
@@ -114,4 +129,4 @@ export interface QuorumMessageCallback {
114
129
  * These messages serve to coordinate the cache invalidation and switch-over
115
130
  * to the new version without any downtime and a coordinating parent server.
116
131
  */
117
- export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
132
+ export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | RollCallMessage | CronMessage;
package/types/stream.ts CHANGED
@@ -42,6 +42,7 @@ export interface StreamData {
42
42
  trc?: string; //trace id
43
43
  spn?: string; //span id
44
44
  try?: number; //current try count
45
+ await?: boolean; //(waitfor) if explicitly false, do not await; sever the connection
45
46
  };
46
47
  type?: StreamDataType;
47
48
  data: Record<string, unknown>;