@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
@@ -7,7 +7,6 @@
7
7
  * ERROR CODES:
8
8
  * 594: waitforsignal
9
9
  * 592: sleepFor
10
- * 595: sleep (deprecated)
11
10
  * 596, 597, 598: fatal
12
11
  * 599: retry
13
12
  */
@@ -122,19 +121,6 @@ const getWorkflowYAML = (app: string, version: string) => {
122
121
  maps:
123
122
  index: '{$self.output.data.index}'
124
123
  signals: '{$self.output.data.signals}'
125
- 595:
126
- schema:
127
- type: object
128
- properties:
129
- duration:
130
- type: number
131
- description: sleep duration in seconds
132
- index:
133
- type: number
134
- description: the current index
135
- maps:
136
- duration: '{$self.output.data.duration}'
137
- index: '{$self.output.data.index}'
138
124
  592:
139
125
  schema:
140
126
  type: object
@@ -229,19 +215,6 @@ const getWorkflowYAML = (app: string, version: string) => {
229
215
  maps:
230
216
  index: '{$self.output.data.index}'
231
217
  signals: '{$self.output.data.signals}'
232
- 595:
233
- schema:
234
- type: object
235
- properties:
236
- duration:
237
- type: number
238
- description: sleep duration in seconds
239
- index:
240
- type: number
241
- description: the current index
242
- maps:
243
- duration: '{$self.output.data.duration}'
244
- index: '{$self.output.data.index}'
245
218
  592:
246
219
  schema:
247
220
  type: object
@@ -320,57 +293,6 @@ const getWorkflowYAML = (app: string, version: string) => {
320
293
  maps:
321
294
  duration: '{siga1.output.data.duration}'
322
295
 
323
- siga595:
324
- title: Signal In - Sleep before trying again
325
- type: await
326
- topic: ${app}.sleep.execute
327
- input:
328
- schema:
329
- type: object
330
- properties:
331
- duration:
332
- type: number
333
- index:
334
- type: number
335
- workflowId:
336
- type: string
337
- parentWorkflowId:
338
- type: string
339
- originJobId:
340
- type: string
341
- maps:
342
- duration: '{sigw1.output.data.duration}'
343
- index: '{sigw1.output.data.index}'
344
- parentWorkflowId:
345
- '@pipe':
346
- - ['{$job.metadata.jid}', '-s']
347
- - ['{@string.concat}']
348
- originJobId:
349
- '@pipe':
350
- - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
351
- - ['{@conditional.ternary}']
352
-
353
- workflowId:
354
- '@pipe':
355
- - ['-', '{$job.metadata.jid}', '-$sleep', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
356
- - ['{@string.concat}']
357
- output:
358
- schema:
359
- type: object
360
- properties:
361
- done:
362
- type: boolean
363
- maps:
364
- done: '{sigw1.output.data.done}'
365
-
366
- sigc595:
367
- title: Signal In - Goto Activity siga1
368
- type: cycle
369
- ancestor: siga1
370
- input:
371
- maps:
372
- duration: '{siga1.output.data.duration}'
373
-
374
296
  siga592:
375
297
  title: Signal In - Sleep For a duration and then cycle/goto
376
298
  type: hook
@@ -464,56 +386,6 @@ const getWorkflowYAML = (app: string, version: string) => {
464
386
  maps:
465
387
  duration: '{a1.output.data.duration}'
466
388
 
467
- a595:
468
- title: Sleep before trying again
469
- type: await
470
- topic: ${app}.sleep.execute
471
- input:
472
- schema:
473
- type: object
474
- properties:
475
- duration:
476
- type: number
477
- index:
478
- type: number
479
- workflowId:
480
- type: string
481
- parentWorkflowId:
482
- type: string
483
- originJobId:
484
- type: string
485
- maps:
486
- duration: '{w1.output.data.duration}'
487
- index: '{w1.output.data.index}'
488
- parentWorkflowId:
489
- '@pipe':
490
- - ['{$job.metadata.jid}', '-s']
491
- - ['{@string.concat}']
492
- originJobId:
493
- '@pipe':
494
- - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
495
- - ['{@conditional.ternary}']
496
- workflowId:
497
- '@pipe':
498
- - ['-', '{$job.metadata.jid}', '-$sleep-', '{w1.output.data.index}']
499
- - ['{@string.concat}']
500
- output:
501
- schema:
502
- type: object
503
- properties:
504
- done:
505
- type: boolean
506
- maps:
507
- done: '{w1.output.data.done}'
508
-
509
- c595:
510
- title: Goto Activity a1
511
- type: cycle
512
- ancestor: a1
513
- input:
514
- maps:
515
- duration: '{a1.output.data.duration}'
516
-
517
389
  a592:
518
390
  title: Sleep For a duration and then cycle/goto
519
391
  type: hook
@@ -571,9 +443,6 @@ const getWorkflowYAML = (app: string, version: string) => {
571
443
  - to: siga594
572
444
  conditions:
573
445
  code: 594
574
- - to: siga595
575
- conditions:
576
- code: 595
577
446
  - to: siga592
578
447
  conditions:
579
448
  code: 592
@@ -582,8 +451,6 @@ const getWorkflowYAML = (app: string, version: string) => {
582
451
  code: 599
583
452
  siga594:
584
453
  - to: sigc594
585
- siga595:
586
- - to: sigc595
587
454
  siga592:
588
455
  - to: sigc592
589
456
  siga599:
@@ -594,9 +461,6 @@ const getWorkflowYAML = (app: string, version: string) => {
594
461
  - to: a594
595
462
  conditions:
596
463
  code: 594
597
- - to: a595
598
- conditions:
599
- code: 595
600
464
  - to: a592
601
465
  conditions:
602
466
  code: 592
@@ -608,8 +472,6 @@ const getWorkflowYAML = (app: string, version: string) => {
608
472
  code: [200, 598, 597, 596]
609
473
  a594:
610
474
  - to: c594
611
- a595:
612
- - to: c595
613
475
  a592:
614
476
  - to: c592
615
477
  a599:
@@ -295,6 +295,9 @@ export class MeshOSService {
295
295
  }
296
296
 
297
297
  static generateSearchQuery(query: FindWhereQuery[]) {
298
+ if (!Array.isArray(query) || query.length === 0) {
299
+ return '*';
300
+ }
298
301
  const my = new this();
299
302
  let queryString = query.map(q => {
300
303
  const { field, is, value, type } = q;
@@ -3,7 +3,6 @@ import {
3
3
  DurableIncompleteSignalError,
4
4
  DurableMaxedError,
5
5
  DurableRetryError,
6
- DurableSleepError,
7
6
  DurableSleepForError,
8
7
  DurableTimeoutError,
9
8
  DurableWaitForSignalError} from '../../modules/errors';
@@ -249,21 +248,6 @@ export class WorkerService {
249
248
  }
250
249
  } as StreamDataResponse;
251
250
 
252
- //deprecated format; not an error...just a trigger to sleep
253
- } else if (err instanceof DurableSleepError) {
254
- return {
255
- status: StreamStatus.SUCCESS,
256
- code: err.code,
257
- metadata: { ...data.metadata },
258
- data: {
259
- code: err.code,
260
- message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
261
- duration: err.duration,
262
- index: err.index,
263
- dimension: err.dimension
264
- }
265
- } as StreamDataResponse;
266
-
267
251
  //not an error...just a trigger to wait for a signal
268
252
  } else if (err instanceof DurableWaitForSignalError) {
269
253
  return {
@@ -2,7 +2,6 @@ import ms from 'ms';
2
2
 
3
3
  import {
4
4
  DurableIncompleteSignalError,
5
- DurableSleepError,
6
5
  DurableSleepForError,
7
6
  DurableWaitForSignalError } from '../../modules/errors';
8
7
  import { KeyService, KeyType } from '../../modules/key';
@@ -376,37 +375,6 @@ export class WorkflowService {
376
375
  return seconds;
377
376
  }
378
377
 
379
- /**
380
- * Sleeps the workflow for a duration. As the function is reentrant,
381
- * upon reentry, the function will traverse prior execution paths up
382
- * until the sleep command and then resume execution from that point.
383
- * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
384
- * @returns {Promise<number>}
385
- * @deprecated - use `sleepFor` instead
386
- */
387
- static async sleep(duration: string): Promise<number> {
388
- const seconds = ms(duration) / 1000;
389
-
390
- const store = asyncLocalStorage.getStore();
391
- const COUNTER = store.get('counter');
392
- const execIndex = COUNTER.counter = COUNTER.counter + 1;
393
- const workflowId = store.get('workflowId');
394
- const workflowTopic = store.get('workflowTopic');
395
- const workflowDimension = store.get('workflowDimension') ?? '';
396
- const namespace = store.get('namespace');
397
- const sleepJobId = `-${workflowId}-$sleep${workflowDimension}-${execIndex}`;
398
-
399
- try {
400
- const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
401
- await hotMeshClient.getState(`${hotMeshClient.appId}.sleep.execute`, sleepJobId);
402
- //if no error is thrown, we've already slept, return the delay
403
- return seconds;
404
- } catch (e) {
405
- // spawn a new sleep job if error code 595 is thrown by the worker)
406
- throw new DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
407
- }
408
- }
409
-
410
378
  /**
411
379
  * Waits for a signal to awaken
412
380
  * @param {string[]} signals - the signals to wait for
@@ -332,6 +332,7 @@ class EngineService {
332
332
  });
333
333
  const context: PartialJobState = {
334
334
  metadata: {
335
+ guid: streamData.metadata.guid,
335
336
  jid: streamData.metadata.jid,
336
337
  gid: streamData.metadata.gid,
337
338
  dad: streamData.metadata.dad,
@@ -490,17 +491,15 @@ class EngineService {
490
491
  };
491
492
  return await this.router.publishMessage(null, streamData) as string;
492
493
  }
493
- async hookTime(jobId: string, gId: string, activityId: string, type?: WorkListTaskType): Promise<string | void> {
494
- if (type === 'interrupt') {
494
+ async hookTime(jobId: string, gId: string, topicOrActivity: string, type?: WorkListTaskType): Promise<string | void> {
495
+ if (type === 'interrupt' || type === 'expire') {
495
496
  return await this.interrupt(
496
- activityId, //note: 'activityId' is the actually job topic
497
+ topicOrActivity,
497
498
  jobId,
498
499
  { suppress: true, expire: 1 },
499
500
  );
500
- } else if (type === 'expire') {
501
- return await this.store.expireJob(jobId, 1);
502
501
  }
503
- const [aid, ...dimensions] = activityId.split(',');
502
+ const [aid, ...dimensions] = topicOrActivity.split(',');
504
503
  const dad = `,${dimensions.join(',')}`;
505
504
  const streamData: StreamData = {
506
505
  type: StreamDataType.TIMEHOOK,
@@ -1,4 +1,4 @@
1
- import { KeyType } from '../../modules/key';
1
+ import { HMSH_ACTIVATION_MAX_RETRY, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
2
2
  import { identifyRedisType, sleepFor } from '../../modules/utils';
3
3
  import { CompilerService } from '../compiler';
4
4
  import { EngineService } from '../engine';
@@ -10,6 +10,7 @@ import { SubService } from '../sub';
10
10
  import { IORedisSubService as IORedisSub } from '../sub/clients/ioredis';
11
11
  import { RedisSubService as RedisSub } from '../sub/clients/redis';
12
12
  import { CacheMode } from '../../types/cache';
13
+ import { HotMeshConfig, KeyType } from '../../types/hotmesh';
13
14
  import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
14
15
  import {
15
16
  QuorumMessage,
@@ -18,13 +19,9 @@ import {
18
19
  SubscriptionCallback,
19
20
  ThrottleMessage
20
21
  } from '../../types/quorum';
21
- import { HotMeshApps, HotMeshConfig } from '../../types/hotmesh';
22
22
  import { RedisClient, RedisMulti } from '../../types/redis';
23
23
  import { RedisClientType } from '../../types/redisclient';
24
24
 
25
- //wait time to see if quorum is reached
26
- const QUORUM_DELAY = 250;
27
-
28
25
  class QuorumService {
29
26
  namespace: string;
30
27
  appId: string;
@@ -59,8 +56,18 @@ class QuorumService {
59
56
  //note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
60
57
  await instance.initStoreChannel(config.engine.store);
61
58
  await instance.initSubChannel(config.engine.sub);
62
- await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId); //general quorum subscription
63
- await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid); //app-specific quorum subscription (used for pubsub one-time request/response)
59
+ //general quorum subscription
60
+ await instance.subscribe.subscribe(
61
+ KeyType.QUORUM,
62
+ instance.subscriptionHandler(),
63
+ appId
64
+ );
65
+ //app-specific quorum subscription (used for pubsub one-time request/response)
66
+ await instance.subscribe.subscribe(
67
+ KeyType.QUORUM,
68
+ instance.subscriptionHandler(),
69
+ appId, instance.guid
70
+ );
64
71
 
65
72
  instance.engine.processWebHooks();
66
73
  instance.engine.processTimeHooks();
@@ -152,7 +159,7 @@ class QuorumService {
152
159
  );
153
160
  }
154
161
 
155
- async requestQuorum(delay = QUORUM_DELAY, details = false): Promise<number> {
162
+ async requestQuorum(delay = HMSH_QUORUM_DELAY_MS, details = false): Promise<number> {
156
163
  const quorum = this.quorum;
157
164
  this.quorum = 0;
158
165
  this.profiles.length = 0;
@@ -188,7 +195,7 @@ class QuorumService {
188
195
 
189
196
 
190
197
  // ************* COMPILER METHODS *************
191
- async rollCall(delay = QUORUM_DELAY): Promise<QuorumProfile[]> {
198
+ async rollCall(delay = HMSH_QUORUM_DELAY_MS): Promise<QuorumProfile[]> {
192
199
  await this.requestQuorum(delay, true);
193
200
  const targetStreams = [];
194
201
  const multi = this.store.getMulti();
@@ -209,10 +216,20 @@ class QuorumService {
209
216
  });
210
217
  return this.profiles;
211
218
  }
212
- async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
219
+ /**
220
+ * request a quorum; if successful activate the app version
221
+ */
222
+ async activate(version: string, delay = HMSH_QUORUM_DELAY_MS, count = 0): Promise<boolean> {
213
223
  version = version.toString();
224
+ const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
225
+ if (!canActivate) {
226
+ //another engine is already activating the app version
227
+ this.logger.debug('quorum-activation-awaiting', { version });
228
+ await sleepFor(delay * 6);
229
+ const app = await this.store.getApp(this.appId, true);
230
+ return app?.active == true && app?.version === version;
231
+ }
214
232
  const config = await this.engine.getVID();
215
- //request a quorum to activate the version
216
233
  await this.requestQuorum(delay);
217
234
  const q1 = await this.requestQuorum(delay);
218
235
  const q2 = await this.requestQuorum(delay);
@@ -225,6 +242,7 @@ class QuorumService {
225
242
  this.appId
226
243
  );
227
244
  await new Promise(resolve => setTimeout(resolve, delay));
245
+ await this.store.releaseScoutRole('activate');
228
246
  //confirm we received the activation message
229
247
  if (this.engine.untilVersion === version) {
230
248
  this.logger.info('quorum-activation-succeeded', { version });
@@ -236,7 +254,12 @@ class QuorumService {
236
254
  throw new Error(`UntilVersion Not Received. Version ${version} not activated`);
237
255
  }
238
256
  } else {
239
- this.logger.info('quorum-rollcall-error', { q1, q2, q3 });
257
+ this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
258
+ this.store.releaseScoutRole('activate');
259
+ if (count < HMSH_ACTIVATION_MAX_RETRY) {
260
+ //increase the delay (give the quorum time to respond) and try again
261
+ return await this.activate(version, delay * 2, count + 1);
262
+ }
240
263
  throw new Error(`Quorum not reached. Version ${version} not activated.`);
241
264
  }
242
265
  }
@@ -18,6 +18,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
18
18
  constructor(redisClient: RedisClientType) {
19
19
  super(redisClient);
20
20
  this.commands = {
21
+ set: 'SET',
21
22
  setnx: 'SETNX',
22
23
  del: 'DEL',
23
24
  expire: 'EXPIRE',
@@ -45,6 +45,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
45
45
  appId: string
46
46
  logger: ILogger;
47
47
  commands: Record<string, string> = {
48
+ set: 'set',
48
49
  setnx: 'setnx',
49
50
  del: 'del',
50
51
  expire: 'expire',
@@ -178,14 +179,16 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
178
179
  * check for and process work items in the
179
180
  * time and signal task queues.
180
181
  */
181
- async reserveScoutRole(scoutType: 'time' | 'signal', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
182
+ async reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
182
183
  const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
183
- const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
184
- if (this.isSuccessful(success)) {
185
- await this.redisClient[this.commands.expire](key, delay - 1);
186
- return true;
187
- }
188
- return false;
184
+ const success = await this.exec('SET', key, `${scoutType}:${formatISODate(new Date())}`, 'NX', 'EX', `${delay - 1}`);
185
+ return this.isSuccessful(success);
186
+ }
187
+
188
+ async releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean> {
189
+ const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
190
+ const success = await this.exec('DEL', key);
191
+ return this.isSuccessful(success);
189
192
  }
190
193
 
191
194
  async getSettings(bCreate = false): Promise<HotMeshSettings> {
@@ -586,6 +589,10 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
586
589
  }
587
590
  }
588
591
 
592
+ /**
593
+ * collate is a generic method for incrementing a value in a hash
594
+ * in order to track their progress during processing.
595
+ */
589
596
  async collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi? : U): Promise<number> {
590
597
  const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
591
598
  const collationKey = `${activityId}/output/metadata/as`; //activity state
@@ -600,6 +607,17 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
600
607
  return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, targetId, amount);
601
608
  }
602
609
 
610
+ /**
611
+ * synthentic collation affects those activities in the graph
612
+ * that represent the synthetic DAG that was materialized during compilation;
613
+ * Synthetic targeting ensures that re-entry due to failure can be distinguished from
614
+ * purposeful re-entry.
615
+ */
616
+ async collateSynthetic(jobId: string, guid: string, amount: number, multi? : U): Promise<number> {
617
+ const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
618
+ return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, guid, amount);
619
+ }
620
+
603
621
  async setStateNX(jobId: string, appId: string): Promise<boolean> {
604
622
  const hashKey = this.mintKey(KeyType.JOB_STATE, { appId, jobId });
605
623
  const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', '1');
package/types/hotmesh.ts CHANGED
@@ -44,7 +44,7 @@ type KeyStoreParams = {
44
44
  facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
45
45
  topic?: string; //topic name (e.g., "foo" or "" for top-level)
46
46
  timeValue?: number; //time value (rounded to minute) (for delete range)
47
- scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
47
+ scoutType?: 'signal' | 'time' | 'activate'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
48
48
  };
49
49
 
50
50
  type HotMesh = typeof HotMeshService;
package/types/job.ts CHANGED
@@ -8,6 +8,7 @@ type ActivityData = {
8
8
 
9
9
  type JobMetadata = {
10
10
  key?: string; //job_key
11
+ guid?: string; //system assigned guid that corresponds to the transition message guid that spawned reentry
11
12
  gid: string; //system assigned guid; ensured created/deleted/created jobs are unique
12
13
  jid: string; //job_id (jid+dad+aid) is composite key for activity
13
14
  dad: string; //dimensional address for the activity (,0,0,1)