@hotmeshio/hotmesh 0.0.42 → 0.0.44

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 (61) hide show
  1. package/build/modules/enums.d.ts +2 -0
  2. package/build/modules/enums.js +4 -1
  3. package/build/modules/utils.js +1 -1
  4. package/build/package.json +1 -1
  5. package/build/services/activities/trigger.js +7 -1
  6. package/build/services/durable/client.d.ts +2 -1
  7. package/build/services/durable/client.js +17 -3
  8. package/build/services/durable/exporter.d.ts +105 -0
  9. package/build/services/durable/exporter.js +374 -0
  10. package/build/services/durable/factory.js +6 -63
  11. package/build/services/durable/handle.d.ts +4 -0
  12. package/build/services/durable/handle.js +5 -0
  13. package/build/services/durable/meshos.js +3 -0
  14. package/build/services/durable/workflow.js +24 -21
  15. package/build/services/engine/index.d.ts +6 -1
  16. package/build/services/engine/index.js +9 -2
  17. package/build/services/exporter/index.d.ts +46 -0
  18. package/build/services/exporter/index.js +126 -0
  19. package/build/services/hotmesh/index.d.ts +4 -1
  20. package/build/services/hotmesh/index.js +6 -0
  21. package/build/services/quorum/index.d.ts +5 -2
  22. package/build/services/quorum/index.js +33 -15
  23. package/build/services/router/index.d.ts +3 -0
  24. package/build/services/router/index.js +3 -0
  25. package/build/services/store/clients/redis.js +1 -0
  26. package/build/services/store/index.d.ts +7 -3
  27. package/build/services/store/index.js +62 -12
  28. package/build/services/task/index.js +5 -1
  29. package/build/services/worker/index.js +5 -4
  30. package/build/types/activity.d.ts +6 -1
  31. package/build/types/exporter.d.ts +51 -0
  32. package/build/types/exporter.js +8 -0
  33. package/build/types/hotmesh.d.ts +1 -1
  34. package/build/types/index.d.ts +1 -0
  35. package/build/types/quorum.d.ts +1 -0
  36. package/build/types/task.d.ts +1 -1
  37. package/modules/enums.ts +4 -0
  38. package/modules/utils.ts +1 -1
  39. package/package.json +1 -1
  40. package/services/activities/trigger.ts +14 -0
  41. package/services/durable/client.ts +19 -4
  42. package/services/durable/exporter.ts +408 -0
  43. package/services/durable/factory.ts +6 -63
  44. package/services/durable/handle.ts +12 -0
  45. package/services/durable/meshos.ts +3 -0
  46. package/services/durable/workflow.ts +24 -22
  47. package/services/engine/index.ts +20 -5
  48. package/services/exporter/index.ts +147 -0
  49. package/services/hotmesh/index.ts +8 -1
  50. package/services/quorum/index.ts +37 -13
  51. package/services/router/index.ts +3 -0
  52. package/services/store/clients/redis.ts +1 -0
  53. package/services/store/index.ts +66 -14
  54. package/services/task/index.ts +4 -1
  55. package/services/worker/index.ts +6 -5
  56. package/types/activity.ts +6 -1
  57. package/types/exporter.ts +61 -0
  58. package/types/hotmesh.ts +1 -1
  59. package/types/index.ts +13 -1
  60. package/types/quorum.ts +1 -0
  61. package/types/task.ts +1 -1
@@ -60,6 +60,7 @@ const getWorkflowYAML = (app, version) => {
60
60
  id: '{$self.input.data.workflowId}'
61
61
  key: '{$self.input.data.parentWorkflowId}'
62
62
  parent: '{$self.input.data.originJobId}'
63
+ adjacent: '{$self.input.data.parentWorkflowId}'
63
64
  job:
64
65
  maps:
65
66
  done: false
@@ -263,10 +264,7 @@ const getWorkflowYAML = (app, version) => {
263
264
  description: index will be appended later
264
265
  maps:
265
266
  signals: '{sigw1.output.data.signals}'
266
- parentWorkflowId:
267
- '@pipe':
268
- - ['{$job.metadata.jid}', '-w']
269
- - ['{@string.concat}']
267
+ parentWorkflowId: '{$job.metadata.jid}'
270
268
  originJobId:
271
269
  '@pipe':
272
270
  - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
@@ -356,10 +354,7 @@ const getWorkflowYAML = (app, version) => {
356
354
  description: index will be appended later
357
355
  maps:
358
356
  signals: '{w1.output.data.signals}'
359
- parentWorkflowId:
360
- '@pipe':
361
- - ['{$job.metadata.jid}', '-w']
362
- - ['{@string.concat}']
357
+ parentWorkflowId: '{$job.metadata.jid}'
363
358
  originJobId:
364
359
  '@pipe':
365
360
  - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
@@ -526,6 +521,7 @@ const getWorkflowYAML = (app, version) => {
526
521
  id: '{$self.input.data.workflowId}'
527
522
  key: '{$self.input.data.parentWorkflowId}'
528
523
  parent: '{$self.input.data.originJobId}'
524
+ adjacent: '{$self.input.data.parentWorkflowId}'
529
525
 
530
526
  w1a:
531
527
  title: Activity Worker - Calls Activity Functions
@@ -566,61 +562,6 @@ const getWorkflowYAML = (app, version) => {
566
562
  t1a:
567
563
  - to: w1a
568
564
 
569
- - subscribes: ${app}.sleep.execute
570
- publishes: ${app}.sleep.executed
571
-
572
- expire: 0
573
-
574
- input:
575
- schema:
576
- type: object
577
- properties:
578
- parentWorkflowId:
579
- type: string
580
- originJobId:
581
- type: string
582
- workflowId:
583
- type: string
584
- duration:
585
- type: number
586
- description: in seconds
587
- index:
588
- type: number
589
- output:
590
- schema:
591
- type: object
592
- properties:
593
- done:
594
- type: boolean
595
- duration:
596
- type: number
597
- index:
598
- type: number
599
-
600
- activities:
601
- t1s:
602
- title: Sleep Flow Trigger
603
- type: trigger
604
- stats:
605
- id: '{$self.input.data.workflowId}'
606
- key: '{$self.input.data.parentWorkflowId}'
607
- parent: '{$self.input.data.originJobId}'
608
-
609
- a1s:
610
- title: Sleep for a duration
611
- type: hook
612
- sleep: '{t1s.output.data.duration}'
613
- job:
614
- maps:
615
- done: true
616
- duration: '{t1s.output.data.duration}'
617
- index: '{t1s.output.data.index}'
618
- workflowId: '{t1s.output.data.workflowId}'
619
-
620
- transitions:
621
- t1s:
622
- - to: a1s
623
-
624
565
  - subscribes: ${app}.wfsc.execute
625
566
  publishes: ${app}.wfsc.executed
626
567
 
@@ -665,6 +606,7 @@ const getWorkflowYAML = (app, version) => {
665
606
  stats:
666
607
  id: '{$self.input.data.cycleWorkflowId}'
667
608
  parent: '{$self.input.data.originJobId}'
609
+ adjacent: '{$self.input.data.parentWorkflowId}'
668
610
 
669
611
  a1wc:
670
612
  title: Pivot - All Cycling Descendants Point Here
@@ -836,6 +778,7 @@ const getWorkflowYAML = (app, version) => {
836
778
  id: '{$self.input.data.workflowId}'
837
779
  key: '{$self.input.data.parentWorkflowId}'
838
780
  parent: '{$self.input.data.originJobId}'
781
+ adjacent: '{$self.input.data.parentWorkflowId}'
839
782
 
840
783
  a1ww:
841
784
  title: WFS - signal entry point
@@ -1,10 +1,14 @@
1
+ import { ExporterService } from './exporter';
1
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
+ import { DurableJobExport } from '../../types/exporter';
2
4
  import { JobInterruptOptions } from '../../types/job';
3
5
  export declare class WorkflowHandleService {
6
+ exporter: ExporterService;
4
7
  hotMesh: HotMesh;
5
8
  workflowTopic: string;
6
9
  workflowId: string;
7
10
  constructor(hotMesh: HotMesh, workflowTopic: string, workflowId: string);
11
+ export(): Promise<DurableJobExport>;
8
12
  /**
9
13
  * Sends a signal to the workflow. This is a way to send
10
14
  * a message to a workflow that is paused due to having
@@ -2,11 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkflowHandleService = void 0;
4
4
  const enums_1 = require("../../modules/enums");
5
+ const exporter_1 = require("./exporter");
5
6
  class WorkflowHandleService {
6
7
  constructor(hotMesh, workflowTopic, workflowId) {
7
8
  this.workflowTopic = workflowTopic;
8
9
  this.workflowId = workflowId;
9
10
  this.hotMesh = hotMesh;
11
+ this.exporter = new exporter_1.ExporterService(this.hotMesh.appId, this.hotMesh.engine.store, this.hotMesh.engine.logger);
12
+ }
13
+ async export() {
14
+ return this.exporter.export(this.workflowId);
10
15
  }
11
16
  /**
12
17
  * Sends a signal to the workflow. This is a way to send
@@ -194,6 +194,9 @@ class MeshOSService {
194
194
  return await this.find(options.options ?? {}, ...args);
195
195
  }
196
196
  static generateSearchQuery(query) {
197
+ if (!Array.isArray(query) || query.length === 0) {
198
+ return '*';
199
+ }
197
200
  const my = new this();
198
201
  let queryString = query.map(q => {
199
202
  const { field, is, value, type } = q;
@@ -37,7 +37,7 @@ class WorkflowService {
37
37
  const entityOrEmptyString = options.entity ?? '';
38
38
  //If the workflowId is not provided, it is generated from the entity and the workflow name
39
39
  const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
40
- const parentWorkflowId = `${workflowId}-f`;
40
+ const parentWorkflowId = workflowId;
41
41
  const client = new client_1.ClientService({
42
42
  connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
43
43
  });
@@ -75,32 +75,35 @@ class WorkflowService {
75
75
  const workflowSpan = store.get('workflowSpan');
76
76
  const COUNTER = store.get('counter');
77
77
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
78
+ const sessionId = `-start${workflowDimension}-${execIndex}-`;
78
79
  //NOTE: this is the hash prefix; necessary for the search index to locate the entity
79
80
  const entityOrEmptyString = options.entity ?? '';
80
81
  //If the workflowId is not provided, it is generated from the entity and the workflow name
81
- const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
82
- const parentWorkflowId = `${workflowId}-f`;
82
+ const parentWorkflowId = workflowId;
83
83
  const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
84
- try {
85
- //get the status; if there is no error, return childJobId (what was spawned)
86
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
87
- await hotMeshClient.getStatus(childJobId);
84
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
85
+ const keyParams = { appId: hotMeshClient.appId, jobId: workflowId };
86
+ const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
87
+ let childJobId = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId);
88
+ if (childJobId) {
88
89
  return childJobId;
89
90
  }
90
- catch (error) {
91
- const client = new client_1.ClientService({
92
- connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
93
- });
94
- await client.workflow.start({
95
- ...options,
96
- namespace,
97
- workflowId: childJobId,
98
- parentWorkflowId,
99
- workflowTrace,
100
- workflowSpan,
101
- });
102
- return childJobId;
91
+ else {
92
+ childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
103
93
  }
94
+ const client = new client_1.ClientService({
95
+ connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
96
+ });
97
+ await client.workflow.start({
98
+ ...options,
99
+ namespace,
100
+ workflowId: childJobId,
101
+ parentWorkflowId,
102
+ workflowTrace,
103
+ workflowSpan,
104
+ });
105
+ await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, childJobId);
106
+ return childJobId;
104
107
  }
105
108
  /**
106
109
  * Wraps activities in a proxy that will durably run them
@@ -439,7 +442,7 @@ class WorkflowService {
439
442
  arguments: Array.from(arguments),
440
443
  //when the origin job is removed
441
444
  originJobId: originJobId ?? workflowId,
442
- parentWorkflowId: `${workflowId}-a`,
445
+ parentWorkflowId: workflowId,
443
446
  workflowId: activityJobId,
444
447
  workflowTopic: activityTopic,
445
448
  activityName,
@@ -5,6 +5,7 @@ import { Interrupt } from '../activities/interrupt';
5
5
  import { Signal } from '../activities/signal';
6
6
  import { Worker } from '../activities/worker';
7
7
  import { Trigger } from '../activities/trigger';
8
+ import { ExporterService } from '../exporter';
8
9
  import { ILogger } from '../logger';
9
10
  import { Router } from '../router';
10
11
  import { StoreService } from '../store';
@@ -18,15 +19,17 @@ import { JobState, JobData, JobMetadata, JobOutput, JobStatus, JobInterruptOptio
18
19
  import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '../../types/hotmesh';
19
20
  import { JobMessageCallback } from '../../types/quorum';
20
21
  import { RedisClient, RedisMulti } from '../../types/redis';
21
- import { StringAnyType } from '../../types/serializer';
22
+ import { StringAnyType, StringStringType } from '../../types/serializer';
22
23
  import { GetStatsOptions, IdsResponse, JobStatsInput, StatsResponse } from '../../types/stats';
23
24
  import { StreamCode, StreamData, StreamDataResponse, StreamError, StreamStatus } from '../../types/stream';
24
25
  import { WorkListTaskType } from '../../types/task';
26
+ import { JobExport } from '../../types/exporter';
25
27
  declare class EngineService {
26
28
  namespace: string;
27
29
  apps: HotMeshApps | null;
28
30
  appId: string;
29
31
  guid: string;
32
+ exporter: ExporterService | null;
30
33
  router: Router | null;
31
34
  store: StoreService<RedisClient, RedisMulti> | null;
32
35
  stream: StreamService<RedisClient, RedisMulti> | null;
@@ -88,6 +91,8 @@ declare class EngineService {
88
91
  * it will be expired immediately.
89
92
  */
90
93
  resolveExpires(context: JobState, options: JobCompletionOptions): number;
94
+ export(jobId: string): Promise<JobExport>;
95
+ getRaw(jobId: string): Promise<StringStringType>;
91
96
  getStatus(jobId: string): Promise<JobStatus>;
92
97
  getState(topic: string, jobId: string): Promise<JobOutput>;
93
98
  getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
@@ -9,6 +9,7 @@ const enums_1 = require("../../modules/enums");
9
9
  const utils_1 = require("../../modules/utils");
10
10
  const activities_1 = __importDefault(require("../activities"));
11
11
  const compiler_1 = require("../compiler");
12
+ const exporter_1 = require("../exporter");
12
13
  const reporter_1 = require("../reporter");
13
14
  const router_1 = require("../router");
14
15
  const serializer_1 = require("../serializer");
@@ -41,8 +42,8 @@ class EngineService {
41
42
  await instance.initStreamChannel(config.engine.stream);
42
43
  instance.router = instance.initRouter(config);
43
44
  instance.router.consumeMessages(instance.stream.mintKey(key_1.KeyType.STREAMS, { appId: instance.appId }), 'ENGINE', instance.guid, instance.processStreamMessage.bind(instance));
44
- //the task service is used by the engine to process `webhooks` and `timehooks`
45
45
  instance.taskService = new task_1.TaskService(instance.store, logger);
46
+ instance.exporter = new exporter_1.ExporterService(instance.appId, instance.store, logger);
46
47
  return instance;
47
48
  }
48
49
  }
@@ -526,9 +527,15 @@ class EngineService {
526
527
  return options.expire ?? context.metadata.expire ?? enums_1.HMSH_EXPIRE_JOB_SECONDS;
527
528
  }
528
529
  // ****** GET JOB STATE/COLLATION STATUS BY ID *********
530
+ async export(jobId) {
531
+ return await this.exporter.export(jobId);
532
+ }
533
+ async getRaw(jobId) {
534
+ return await this.store.getRaw(jobId);
535
+ }
529
536
  async getStatus(jobId) {
530
537
  const { id: appId } = await this.getVID();
531
- return this.store.getStatus(jobId, appId);
538
+ return await this.store.getStatus(jobId, appId);
532
539
  }
533
540
  //todo: add 'options' parameter;
534
541
  // (e.g, if {dimensions:true}, use hscan to deliver
@@ -0,0 +1,46 @@
1
+ import { ILogger } from '../logger';
2
+ import { StoreService } from '../store';
3
+ import { StringStringType, Symbols } from "../../types/serializer";
4
+ import { RedisClient, RedisMulti } from '../../types/redis';
5
+ import { DependencyExport, ExportOptions, JobActionExport, JobExport } from '../../types/exporter';
6
+ import { SerializerService } from '../serializer';
7
+ /**
8
+ * Downloads job data from Redis (hscan, hmget, hgetall)
9
+ * Expands process data and includes dependency list
10
+ */
11
+ declare class ExporterService {
12
+ appId: string;
13
+ logger: ILogger;
14
+ serializer: SerializerService;
15
+ store: StoreService<RedisClient, RedisMulti>;
16
+ symbols: Promise<Symbols> | Symbols;
17
+ constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
18
+ /**
19
+ * Convert the job hash and dependency list into a JobExport object.
20
+ * This object contains various facets that describe the interaction
21
+ * in terms relevant to narrative storytelling.
22
+ */
23
+ export(jobId: string, options?: ExportOptions): Promise<JobExport>;
24
+ /**
25
+ * Inflates the key from Redis, 3-character symbol
26
+ * into a human-readable JSON path, reflecting the
27
+ * tree-like structure of the unidimensional Hash
28
+ */
29
+ inflateKey(key: string): string;
30
+ /**
31
+ * Inflates the job data from Redis into a JobExport object
32
+ * @param jobHash - the job data from Redis
33
+ * @param dependencyList - the list of dependencies for the job
34
+ * @returns - the inflated job data
35
+ */
36
+ inflate(jobHash: StringStringType, dependencyList: string[]): JobExport;
37
+ /**
38
+ * Inflates the dependency data from Redis into a JobExport object by
39
+ * organizing the dimensional isolate in sch a way asto interleave
40
+ * into a story
41
+ * @param data - the dependency data from Redis
42
+ * @returns - the organized dependency data
43
+ */
44
+ inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[];
45
+ }
46
+ export { ExporterService };
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExporterService = void 0;
4
+ const serializer_1 = require("../serializer");
5
+ const utils_1 = require("../../modules/utils");
6
+ /**
7
+ * Downloads job data from Redis (hscan, hmget, hgetall)
8
+ * Expands process data and includes dependency list
9
+ */
10
+ class ExporterService {
11
+ constructor(appId, store, logger) {
12
+ this.appId = appId;
13
+ this.logger = logger;
14
+ this.store = store;
15
+ this.serializer = new serializer_1.SerializerService();
16
+ }
17
+ /**
18
+ * Convert the job hash and dependency list into a JobExport object.
19
+ * This object contains various facets that describe the interaction
20
+ * in terms relevant to narrative storytelling.
21
+ */
22
+ async export(jobId, options = {}) {
23
+ if (!this.symbols) {
24
+ this.symbols = this.store.getAllSymbols();
25
+ this.symbols = await this.symbols;
26
+ }
27
+ const depData = await this.store.getDependencies(jobId);
28
+ const jobData = await this.store.getRaw(jobId);
29
+ const jobExport = this.inflate(jobData, depData);
30
+ return jobExport;
31
+ }
32
+ /**
33
+ * Inflates the key from Redis, 3-character symbol
34
+ * into a human-readable JSON path, reflecting the
35
+ * tree-like structure of the unidimensional Hash
36
+ */
37
+ inflateKey(key) {
38
+ return (key in this.symbols) ? this.symbols[key] : key;
39
+ }
40
+ /**
41
+ * Inflates the job data from Redis into a JobExport object
42
+ * @param jobHash - the job data from Redis
43
+ * @param dependencyList - the list of dependencies for the job
44
+ * @returns - the inflated job data
45
+ */
46
+ inflate(jobHash, dependencyList) {
47
+ //the list of actions taken in the workflow and hook functions
48
+ const actions = {
49
+ hooks: {},
50
+ main: {
51
+ cursor: -1,
52
+ items: []
53
+ }
54
+ };
55
+ const process = {};
56
+ const dependencies = this.inflateDependencyData(dependencyList, actions);
57
+ const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
58
+ Object.entries(jobHash).forEach(([key, value]) => {
59
+ const match = key.match(regex);
60
+ if (match) {
61
+ //activity process state
62
+ const [_, letters, numbers] = match;
63
+ const path = this.inflateKey(letters);
64
+ const dimensions = `${numbers.replace(/,/g, '/')}`;
65
+ const resolved = this.serializer.fromString(value);
66
+ process[`${dimensions}/${path}`] = resolved;
67
+ }
68
+ else if (key.length === 3) {
69
+ //job state
70
+ process[this.inflateKey(key)] = this.serializer.fromString(value);
71
+ }
72
+ });
73
+ return {
74
+ dependencies,
75
+ process: (0, utils_1.restoreHierarchy)(process),
76
+ status: jobHash[':'],
77
+ };
78
+ }
79
+ /**
80
+ * Inflates the dependency data from Redis into a JobExport object by
81
+ * organizing the dimensional isolate in sch a way asto interleave
82
+ * into a story
83
+ * @param data - the dependency data from Redis
84
+ * @returns - the organized dependency data
85
+ */
86
+ inflateDependencyData(data, actions) {
87
+ const hookReg = /([0-9,]+)-(\d+)$/;
88
+ const flowReg = /-(\d+)$/;
89
+ return data.map((dependency, index) => {
90
+ const [action, topic, gid, ...jid] = dependency.split('::');
91
+ const jobId = jid.join('::');
92
+ const match = jobId.match(hookReg);
93
+ let prefix;
94
+ let type;
95
+ let dimensionKey = '';
96
+ if (match) {
97
+ //hook-originating dependency
98
+ const [_, dimension, counter] = match;
99
+ dimensionKey = dimension.split(',').join('/');
100
+ prefix = `${dimensionKey}[${counter}]`;
101
+ type = 'hook';
102
+ }
103
+ else {
104
+ const match = jobId.match(flowReg);
105
+ if (match) {
106
+ //main workflow-originating dependency
107
+ const [_, counter] = match;
108
+ prefix = `[${counter}]`;
109
+ type = 'flow';
110
+ }
111
+ else {
112
+ //'other' types like signal cleanup
113
+ prefix = '/';
114
+ type = 'other';
115
+ }
116
+ }
117
+ return {
118
+ type: action,
119
+ topic,
120
+ gid,
121
+ jid: jobId,
122
+ };
123
+ });
124
+ }
125
+ }
126
+ exports.ExporterService = ExporterService;
@@ -7,7 +7,8 @@ import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
7
7
  import { JobMessageCallback, QuorumProfile } from '../../types/quorum';
8
8
  import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
9
9
  import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
10
- import { StringAnyType } from '../../types/serializer';
10
+ import { StringAnyType, StringStringType } from '../../types/serializer';
11
+ import { JobExport } from '../../types/exporter';
11
12
  declare class HotMeshService {
12
13
  namespace: string;
13
14
  appId: string;
@@ -35,6 +36,8 @@ declare class HotMeshService {
35
36
  plan(path: string): Promise<HotMeshManifest>;
36
37
  deploy(pathOrYAML: string): Promise<HotMeshManifest>;
37
38
  activate(version: string, delay?: number): Promise<boolean>;
39
+ export(jobId: string): Promise<JobExport>;
40
+ getRaw(jobId: string): Promise<StringStringType>;
38
41
  getStats(topic: string, query: JobStatsInput): Promise<StatsResponse>;
39
42
  getStatus(jobId: string): Promise<JobStatus>;
40
43
  getState(topic: string, jobId: string): Promise<JobOutput>;
@@ -104,6 +104,12 @@ class HotMeshService {
104
104
  return await this.quorum?.activate(version, delay);
105
105
  }
106
106
  // ************* REPORTER METHODS *************
107
+ async export(jobId) {
108
+ return await this.engine?.export(jobId);
109
+ }
110
+ async getRaw(jobId) {
111
+ return await this.engine?.getRaw(jobId);
112
+ }
107
113
  async getStats(topic, query) {
108
114
  return await this.engine?.getStats(topic, query);
109
115
  }
@@ -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,21 @@ 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 }),
107
+ counts: this.engine.router.counts,
106
108
  };
107
109
  }
108
- this.store.publish(key_1.KeyType.QUORUM, {
110
+ this.store.publish(hotmesh_1.KeyType.QUORUM, {
109
111
  type: 'pong',
110
112
  guid, originator,
111
113
  profile,
112
114
  }, appId);
113
115
  }
114
- async requestQuorum(delay = QUORUM_DELAY, details = false) {
116
+ async requestQuorum(delay = enums_1.HMSH_QUORUM_DELAY_MS, details = false) {
115
117
  const quorum = this.quorum;
116
118
  this.quorum = 0;
117
119
  this.profiles.length = 0;
118
- await this.store.publish(key_1.KeyType.QUORUM, {
120
+ await this.store.publish(hotmesh_1.KeyType.QUORUM, {
119
121
  type: 'ping',
120
122
  originator: this.guid,
121
123
  details,
@@ -126,7 +128,7 @@ class QuorumService {
126
128
  // ************* PUB/SUB METHODS *************
127
129
  //publish a message to the quorum
128
130
  async pub(quorumMessage) {
129
- return await this.store.publish(key_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
131
+ return await this.store.publish(hotmesh_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
130
132
  }
131
133
  //subscribe user to quorum messages
132
134
  async sub(callback) {
@@ -139,7 +141,7 @@ class QuorumService {
139
141
  this.callbacks = this.callbacks.filter(cb => cb !== callback);
140
142
  }
141
143
  // ************* COMPILER METHODS *************
142
- async rollCall(delay = QUORUM_DELAY) {
144
+ async rollCall(delay = enums_1.HMSH_QUORUM_DELAY_MS) {
143
145
  await this.requestQuorum(delay, true);
144
146
  const targetStreams = [];
145
147
  const multi = this.store.getMulti();
@@ -160,18 +162,29 @@ class QuorumService {
160
162
  });
161
163
  return this.profiles;
162
164
  }
163
- async activate(version, delay = QUORUM_DELAY) {
165
+ /**
166
+ * request a quorum; if successful activate the app version
167
+ */
168
+ async activate(version, delay = enums_1.HMSH_QUORUM_DELAY_MS, count = 0) {
164
169
  version = version.toString();
170
+ const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
171
+ if (!canActivate) {
172
+ //another engine is already activating the app version
173
+ this.logger.debug('quorum-activation-awaiting', { version });
174
+ await (0, utils_1.sleepFor)(delay * 6);
175
+ const app = await this.store.getApp(this.appId, true);
176
+ return app?.active == true && app?.version === version;
177
+ }
165
178
  const config = await this.engine.getVID();
166
- //request a quorum to activate the version
167
179
  await this.requestQuorum(delay);
168
180
  const q1 = await this.requestQuorum(delay);
169
181
  const q2 = await this.requestQuorum(delay);
170
182
  const q3 = await this.requestQuorum(delay);
171
183
  if (q1 && q1 === q2 && q2 === q3) {
172
184
  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);
185
+ this.store.publish(hotmesh_1.KeyType.QUORUM, { type: 'activate', cache_mode: 'nocache', until_version: version }, this.appId);
174
186
  await new Promise(resolve => setTimeout(resolve, delay));
187
+ await this.store.releaseScoutRole('activate');
175
188
  //confirm we received the activation message
176
189
  if (this.engine.untilVersion === version) {
177
190
  this.logger.info('quorum-activation-succeeded', { version });
@@ -185,7 +198,12 @@ class QuorumService {
185
198
  }
186
199
  }
187
200
  else {
188
- this.logger.info('quorum-rollcall-error', { q1, q2, q3 });
201
+ this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
202
+ this.store.releaseScoutRole('activate');
203
+ if (count < enums_1.HMSH_ACTIVATION_MAX_RETRY) {
204
+ //increase the delay (give the quorum time to respond) and try again
205
+ return await this.activate(version, delay * 2, count + 1);
206
+ }
189
207
  throw new Error(`Quorum not reached. Version ${version} not activated.`);
190
208
  }
191
209
  }
@@ -17,6 +17,9 @@ declare class Router {
17
17
  logger: ILogger;
18
18
  throttle: number;
19
19
  errorCount: number;
20
+ counts: {
21
+ [key: string]: number;
22
+ };
20
23
  currentTimerId: NodeJS.Timeout | null;
21
24
  shouldConsume: boolean;
22
25
  constructor(config: StreamConfig, stream: StreamService<RedisClient, RedisMulti>, store: StoreService<RedisClient, RedisMulti>, logger: ILogger);