@hotmeshio/hotmesh 0.0.33 → 0.0.35

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 (94) hide show
  1. package/README.md +30 -18
  2. package/build/modules/enums.d.ts +22 -0
  3. package/build/modules/enums.js +29 -0
  4. package/build/modules/errors.d.ts +10 -2
  5. package/build/modules/errors.js +14 -3
  6. package/build/modules/key.d.ts +16 -15
  7. package/build/modules/key.js +18 -15
  8. package/build/modules/utils.d.ts +1 -0
  9. package/build/modules/utils.js +6 -1
  10. package/build/package.json +4 -1
  11. package/build/services/activities/activity.d.ts +5 -0
  12. package/build/services/activities/activity.js +27 -6
  13. package/build/services/activities/await.js +11 -3
  14. package/build/services/activities/cycle.js +10 -2
  15. package/build/services/activities/hook.js +8 -2
  16. package/build/services/activities/index.d.ts +2 -2
  17. package/build/services/activities/index.js +2 -2
  18. package/build/services/activities/interrupt.d.ts +16 -0
  19. package/build/services/activities/interrupt.js +129 -0
  20. package/build/services/activities/signal.js +9 -2
  21. package/build/services/activities/trigger.d.ts +4 -0
  22. package/build/services/activities/trigger.js +14 -4
  23. package/build/services/activities/worker.js +10 -2
  24. package/build/services/collator/index.d.ts +4 -0
  25. package/build/services/collator/index.js +8 -0
  26. package/build/services/compiler/deployer.js +1 -3
  27. package/build/services/connector/index.js +2 -3
  28. package/build/services/durable/client.js +9 -6
  29. package/build/services/durable/factory.js +65 -284
  30. package/build/services/durable/handle.d.ts +37 -0
  31. package/build/services/durable/handle.js +52 -9
  32. package/build/services/durable/index.d.ts +5 -0
  33. package/build/services/durable/index.js +10 -0
  34. package/build/services/durable/meshos.js +3 -6
  35. package/build/services/durable/worker.js +11 -5
  36. package/build/services/durable/workflow.d.ts +24 -0
  37. package/build/services/durable/workflow.js +56 -1
  38. package/build/services/engine/index.d.ts +14 -6
  39. package/build/services/engine/index.js +52 -27
  40. package/build/services/hotmesh/index.d.ts +6 -2
  41. package/build/services/hotmesh/index.js +23 -5
  42. package/build/services/quorum/index.d.ts +1 -0
  43. package/build/services/quorum/index.js +10 -0
  44. package/build/services/signaler/stream.js +25 -29
  45. package/build/services/store/index.d.ts +40 -4
  46. package/build/services/store/index.js +114 -9
  47. package/build/services/task/index.d.ts +5 -4
  48. package/build/services/task/index.js +12 -14
  49. package/build/types/activity.d.ts +35 -5
  50. package/build/types/durable.d.ts +4 -0
  51. package/build/types/index.d.ts +1 -1
  52. package/build/types/job.d.ts +18 -1
  53. package/build/types/quorum.d.ts +11 -7
  54. package/build/types/stream.d.ts +4 -1
  55. package/build/types/stream.js +2 -0
  56. package/modules/enums.ts +32 -0
  57. package/modules/errors.ts +24 -9
  58. package/modules/key.ts +4 -1
  59. package/modules/utils.ts +5 -0
  60. package/package.json +4 -1
  61. package/services/activities/activity.ts +34 -8
  62. package/services/activities/await.ts +11 -4
  63. package/services/activities/cycle.ts +10 -3
  64. package/services/activities/hook.ts +8 -3
  65. package/services/activities/index.ts +2 -2
  66. package/services/activities/interrupt.ts +159 -0
  67. package/services/activities/signal.ts +9 -3
  68. package/services/activities/trigger.ts +21 -5
  69. package/services/activities/worker.ts +10 -3
  70. package/services/collator/index.ts +10 -1
  71. package/services/compiler/deployer.ts +1 -3
  72. package/services/connector/index.ts +3 -5
  73. package/services/durable/client.ts +10 -7
  74. package/services/durable/factory.ts +65 -284
  75. package/services/durable/handle.ts +55 -9
  76. package/services/durable/index.ts +11 -0
  77. package/services/durable/meshos.ts +3 -7
  78. package/services/durable/worker.ts +11 -5
  79. package/services/durable/workflow.ts +66 -2
  80. package/services/engine/index.ts +74 -26
  81. package/services/hotmesh/index.ts +28 -6
  82. package/services/quorum/index.ts +9 -0
  83. package/services/signaler/stream.ts +28 -25
  84. package/services/store/index.ts +119 -11
  85. package/services/task/index.ts +18 -18
  86. package/types/activity.ts +38 -8
  87. package/types/durable.ts +8 -4
  88. package/types/index.ts +1 -1
  89. package/types/job.ts +30 -1
  90. package/types/quorum.ts +13 -8
  91. package/types/stream.ts +3 -0
  92. package/build/services/activities/iterate.d.ts +0 -9
  93. package/build/services/activities/iterate.js +0 -13
  94. package/services/activities/iterate.ts +0 -26
@@ -0,0 +1,159 @@
1
+ import { EngineService } from '../engine';
2
+ import { Activity, ActivityType } from './activity';
3
+ import {
4
+ ActivityData,
5
+ ActivityMetadata,
6
+ InterruptActivity } from '../../types/activity';
7
+ import { GetStateError, InactiveJobError } from '../../modules/errors';
8
+ import { MultiResponseFlags } from '../../types';
9
+ import { CollatorService } from '../collator';
10
+ import { JobInterruptOptions, JobState } from '../../types/job';
11
+ import { TelemetryService } from '../telemetry';
12
+ import { Pipe } from '../pipe';
13
+
14
+ class Interrupt extends Activity {
15
+ config: InterruptActivity;
16
+
17
+ constructor(
18
+ config: ActivityType,
19
+ data: ActivityData,
20
+ metadata: ActivityMetadata,
21
+ hook: ActivityData | null,
22
+ engine: EngineService,
23
+ context?: JobState
24
+ ) {
25
+ super(config, data, metadata, hook, engine, context);
26
+ }
27
+
28
+
29
+ //******** LEG 1 ENTRY ********//
30
+ async process(): Promise<string> {
31
+ this.logger.debug('interrupt-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
32
+ let telemetry: TelemetryService;
33
+ try {
34
+ this.setLeg(1);
35
+ await CollatorService.notarizeEntry(this);
36
+ await this.getState();
37
+ CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid); // Ensure job active
38
+ telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
39
+ telemetry.startActivitySpan(this.leg);
40
+
41
+ if (this.isInterruptingSelf()) {
42
+ return await this.interruptSelf(telemetry);
43
+ } else {
44
+ return await this.interruptAnother(telemetry);
45
+ }
46
+ } catch (error) {
47
+ if (error instanceof InactiveJobError) {
48
+ this.logger.error('interrupt-inactive-job-error', { error });
49
+ return;
50
+ } else if (error instanceof GetStateError) {
51
+ this.logger.error('interrupt-get-state-error', { error });
52
+ return;
53
+ } else {
54
+ this.logger.error('interrupt-process-error', { error });
55
+ }
56
+ telemetry.setActivityError(error.message);
57
+ throw error;
58
+ } finally {
59
+ telemetry?.endActivitySpan();
60
+ this.logger.debug('interrupt-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
61
+ }
62
+ }
63
+
64
+ async interruptSelf(telemetry: TelemetryService): Promise<string> {
65
+ // Apply final updates to THIS job's state
66
+ if (this.config.job?.maps) {
67
+ this.mapJobData();
68
+ await this.setState();
69
+ }
70
+
71
+ // Interrupt THIS job
72
+ const messageId = await this.interrupt();
73
+
74
+ // Notarize completion and log
75
+ telemetry.mapActivityAttributes();
76
+ const multi = this.store.getMulti();
77
+ await CollatorService.notarizeEarlyCompletion(this, multi);
78
+ await this.setStatus(-1, multi);
79
+ const multiResponse = await multi.exec() as MultiResponseFlags;
80
+ const jobStatus = this.resolveStatus(multiResponse);
81
+ telemetry.setActivityAttributes({
82
+ 'app.activity.mid': messageId,
83
+ 'app.job.jss': jobStatus
84
+ });
85
+
86
+ return this.context.metadata.aid;
87
+ }
88
+
89
+ async interruptAnother(telemetry: TelemetryService): Promise<string> {
90
+ // Interrupt ANOTHER job
91
+ const messageId = await this.interrupt();
92
+ const attrs = { 'app.activity.mid': messageId };
93
+
94
+ // Apply updates to THIS job's state
95
+ telemetry.mapActivityAttributes();
96
+ this.adjacencyList = await this.filterAdjacent();
97
+ if (this.config.job?.maps || this.config.output?.maps) {
98
+ this.mapOutputData();
99
+ this.mapJobData();
100
+ const multi = this.store.getMulti();
101
+ await this.setState(multi);
102
+ }
103
+
104
+ // Notarize completion
105
+ const multi = this.store.getMulti();
106
+ await CollatorService.notarizeEarlyCompletion(this, multi);
107
+ await this.setStatus(this.adjacencyList.length - 1, multi);
108
+ const multiResponse = await multi.exec() as MultiResponseFlags;
109
+ const jobStatus = this.resolveStatus(multiResponse);
110
+ attrs['app.job.jss'] = jobStatus;
111
+
112
+ // Transition next generation and log
113
+ const messageIds = await this.transition(this.adjacencyList, jobStatus);
114
+ if (messageIds.length) {
115
+ attrs['app.activity.mids'] = messageIds.join(',');
116
+ }
117
+ telemetry.setActivityAttributes(attrs);
118
+
119
+ return this.context.metadata.aid;
120
+ }
121
+
122
+
123
+ isInterruptingSelf(): boolean {
124
+ if (!this.config.target) {
125
+ return true;
126
+ }
127
+ const resolvedJob = Pipe.resolve(this.config.target, this.context);
128
+ return resolvedJob == this.context.metadata.jid;
129
+ }
130
+
131
+ resolveInterruptOptions(): JobInterruptOptions {
132
+ return {
133
+ reason: this.config.reason !== undefined
134
+ ? Pipe.resolve(this.config.reason, this.context)
135
+ : undefined,
136
+ throw: this.config.throw !== undefined
137
+ ? Pipe.resolve(this.config.throw, this.context)
138
+ : undefined,
139
+ descend: this.config.descend !== undefined
140
+ ? Pipe.resolve(this.config.descend, this.context)
141
+ : undefined,
142
+ };
143
+ }
144
+
145
+ async interrupt(): Promise<string> {
146
+ const options = this.resolveInterruptOptions();
147
+ return await this.engine.interrupt(
148
+ this.config.topic !== undefined
149
+ ? Pipe.resolve(this.config.topic, this.context)
150
+ : this.context.metadata.tpc,
151
+ this.config.target !== undefined
152
+ ? Pipe.resolve(this.config.target, this.context)
153
+ : this.context.metadata.jid,
154
+ options as JobInterruptOptions,
155
+ );
156
+ }
157
+ }
158
+
159
+ export { Interrupt };
@@ -1,4 +1,4 @@
1
- import { GetStateError } from '../../modules/errors';
1
+ import { GetStateError, InactiveJobError } from '../../modules/errors';
2
2
  import { Activity, ActivityType } from './activity';
3
3
  import { CollatorService } from '../collator';
4
4
  import { EngineService } from '../engine';
@@ -37,6 +37,7 @@ class Signal extends Activity {
37
37
  this.setLeg(1);
38
38
  await CollatorService.notarizeEntry(this);
39
39
  await this.getState();
40
+ CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
40
41
  telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
41
42
  telemetry.startActivitySpan(this.leg);
42
43
 
@@ -50,6 +51,7 @@ class Signal extends Activity {
50
51
  await this.setStatus(this.adjacencyList.length - 1, multi);
51
52
  const multiResponse = await multi.exec() as MultiResponseFlags;
52
53
 
54
+ //todo: this should execute BEFORE the status is decremented
53
55
  if (this.config.subtype === 'all') {
54
56
  await this.hookAll();
55
57
  } else {
@@ -68,15 +70,19 @@ class Signal extends Activity {
68
70
 
69
71
  return this.context.metadata.aid;
70
72
  } catch (error) {
71
- if (error instanceof GetStateError) {
73
+ if (error instanceof InactiveJobError) {
74
+ this.logger.error('signal-inactive-job-error', { error });
75
+ return;
76
+ } else if (error instanceof GetStateError) {
72
77
  this.logger.error('signal-get-state-error', { error });
78
+ return;
73
79
  } else {
74
80
  this.logger.error('signal-process-error', { error });
75
81
  }
76
82
  telemetry.setActivityError(error.message);
77
83
  throw error;
78
84
  } finally {
79
- telemetry.endActivitySpan();
85
+ telemetry?.endActivitySpan();
80
86
  this.logger.debug('signal-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
81
87
  }
82
88
  }
@@ -1,6 +1,5 @@
1
- import { nanoid } from 'nanoid';
2
1
  import { DuplicateJobError } from '../../modules/errors';
3
- import { formatISODate, getTimeSeries } from '../../modules/utils';
2
+ import { formatISODate, getTimeSeries, guid } from '../../modules/utils';
4
3
  import { Activity } from './activity';
5
4
  import { CollatorService } from '../collator';
6
5
  import { EngineService } from '../engine';
@@ -47,6 +46,7 @@ class Trigger extends Activity {
47
46
  const multi = this.store.getMulti();
48
47
  await this.setState(multi);
49
48
  await this.setStats(multi);
49
+ await this.setDependency(multi);
50
50
  await multi.exec();
51
51
 
52
52
  telemetry.mapActivityAttributes();
@@ -68,8 +68,8 @@ class Trigger extends Activity {
68
68
  telemetry.setActivityError(error.message);
69
69
  throw error;
70
70
  } finally {
71
- telemetry.endJobSpan();
72
- telemetry.endActivitySpan();
71
+ telemetry?.endJobSpan();
72
+ telemetry?.endActivitySpan();
73
73
  this.logger.debug('trigger-process-end', { subscribes: this.config.subscribes, jid: this.context.metadata.jid });
74
74
  }
75
75
  }
@@ -161,7 +161,7 @@ class Trigger extends Activity {
161
161
 
162
162
  resolveJobId(context: Partial<JobState>): string {
163
163
  const jobId = this.config.stats?.id;
164
- return jobId ? Pipe.resolve(jobId, context) : nanoid();
164
+ return jobId ? Pipe.resolve(jobId, context) : guid();
165
165
  }
166
166
 
167
167
  resolveJobKey(context: Partial<JobState>): string {
@@ -176,6 +176,22 @@ class Trigger extends Activity {
176
176
  }
177
177
  }
178
178
 
179
+ /**
180
+ * Registers this job as a dependent of the parent job
181
+ */
182
+ async setDependency(multi?: RedisMulti): Promise<void> {
183
+ const depKey = this.config.stats?.parent;
184
+ const resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
185
+ if (resolvedDepKey) {
186
+ await this.store.setDependency(
187
+ resolvedDepKey,
188
+ this.context.metadata.tpc,
189
+ this.context.metadata.jid,
190
+ multi,
191
+ );
192
+ }
193
+ }
194
+
179
195
  async setStats(multi?: RedisMulti): Promise<void> {
180
196
  const md = this.context.metadata;
181
197
  if (md.key && this.config.stats?.measures) {
@@ -1,4 +1,4 @@
1
- import { GetStateError } from '../../modules/errors';
1
+ import { GetStateError, InactiveJobError } from '../../modules/errors';
2
2
  import { Activity } from './activity';
3
3
  import { CollatorService } from '../collator';
4
4
  import { EngineService } from '../engine';
@@ -12,6 +12,7 @@ import { MultiResponseFlags, RedisMulti } from '../../types/redis';
12
12
  import { StreamData} from '../../types/stream';
13
13
  import { TelemetryService } from '../telemetry';
14
14
  import { Pipe } from '../pipe';
15
+ import { guid } from '../../modules/utils';
15
16
 
16
17
  class Worker extends Activity {
17
18
  config: WorkerActivity;
@@ -35,6 +36,7 @@ class Worker extends Activity {
35
36
  this.setLeg(1);
36
37
  await CollatorService.notarizeEntry(this);
37
38
  await this.getState();
39
+ CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
38
40
  telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
39
41
  telemetry.startActivitySpan(this.leg);
40
42
  this.mapInputData();
@@ -58,15 +60,19 @@ class Worker extends Activity {
58
60
 
59
61
  return this.context.metadata.aid;
60
62
  } catch (error) {
61
- if (error instanceof GetStateError) {
63
+ if (error instanceof InactiveJobError) {
64
+ this.logger.error('await-inactive-job-error', { error });
65
+ return;
66
+ } else if (error instanceof GetStateError) {
62
67
  this.logger.error('worker-get-state-error', { error });
68
+ return;
63
69
  } else {
64
70
  this.logger.error('worker-process-error', { error });
65
71
  }
66
72
  telemetry.setActivityError(error.message);
67
73
  throw error;
68
74
  } finally {
69
- telemetry.endActivitySpan();
75
+ telemetry?.endActivitySpan();
70
76
  this.logger.debug('worker-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
71
77
  }
72
78
  }
@@ -75,6 +81,7 @@ class Worker extends Activity {
75
81
  const topic = Pipe.resolve(this.config.subtype, this.context);
76
82
  const streamData: StreamData = {
77
83
  metadata: {
84
+ guid: guid(),
78
85
  jid: this.context.metadata.jid,
79
86
  dad: this.metadata.dad,
80
87
  aid: this.metadata.aid,
@@ -1,4 +1,4 @@
1
- import { CollationError } from '../../modules/errors';
1
+ import { CollationError, InactiveJobError } from '../../modules/errors';
2
2
  import { RedisMulti } from '../../types/redis';
3
3
  import { CollationFaultType, CollationStage } from '../../types/collator';
4
4
  import { ActivityDuplex } from '../../types/activity';
@@ -11,6 +11,15 @@ class CollatorService {
11
11
  //max int digit count that supports `hincrby`
12
12
  static targetLength = 15;
13
13
 
14
+ /**
15
+ * Upon re/entry, verify that the job status is active
16
+ */
17
+ static assertJobActive(status: number, jobId: string, activityId: string): void {
18
+ if (status <= 0) {
19
+ throw new InactiveJobError(jobId, status, activityId);
20
+ }
21
+ }
22
+
14
23
  /**
15
24
  * returns the dimensional address (dad) for the target; due
16
25
  * to the nature of the notary system, the dad for leg 2 entry
@@ -143,9 +143,7 @@ class Deployer {
143
143
  if (graph.publishes) {
144
144
  activities[activityKey].publishes = graph.publishes;
145
145
  }
146
- if (graph.expire) {
147
- activities[activityKey].expire = graph.expire;
148
- }
146
+ activities[activityKey].expire = graph.expire ?? undefined;
149
147
  }
150
148
  }
151
149
  }
@@ -1,6 +1,4 @@
1
- import { nanoid } from 'nanoid';
2
-
3
- import { identifyRedisTypeFromClass } from '../../modules/utils';
1
+ import { guid, identifyRedisTypeFromClass } from '../../modules/utils';
4
2
  import { RedisConnection as IORedisConnection } from '../connector/clients/ioredis';
5
3
  import { RedisConnection } from '../connector/clients/redis';
6
4
  import {
@@ -23,14 +21,14 @@ export class ConnectorService {
23
21
  if (identifyRedisTypeFromClass(Redis) === 'redis') {
24
22
  for (let i = 1; i <= 3; i++) {
25
23
  instances.push(RedisConnection.connect(
26
- nanoid(),
24
+ guid(),
27
25
  Redis as RedisClassType,
28
26
  options as RedisClientOptions));
29
27
  }
30
28
  } else {
31
29
  for (let i = 1; i <= 3; i++) {
32
30
  instances.push(IORedisConnection.connect(
33
- nanoid(),
31
+ guid(),
34
32
  Redis as IORedisClassType,
35
33
  options as IORedisClientOptions));
36
34
  }
@@ -11,6 +11,7 @@ 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 { DURABLE_EXPIRE_SECONDS } from '../../modules/enums';
14
15
 
15
16
  export class ClientService {
16
17
 
@@ -96,12 +97,15 @@ export class ClientService {
96
97
  const workflowName = options.entity ?? options.workflowName;
97
98
  const trc = options.workflowTrace;
98
99
  const spn = options.workflowSpan;
99
- //topic is concat of taskQueue and workflowName
100
+ //NOTE: HotMesh 'workflowTopic' is a created by concatenating
101
+ // the taskQueue and workflowName used by the Durable module
100
102
  const workflowTopic = `${taskQueueName}-${workflowName}`;
101
103
  const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
102
- this.configureSearchIndex(hotMeshClient, options.search)
104
+ this.configureSearchIndex(hotMeshClient, options.search);
103
105
  const payload = {
104
106
  arguments: [...options.args],
107
+ originJobId: options.originJobId,
108
+ expire: options.expire ?? DURABLE_EXPIRE_SECONDS,
105
109
  parentWorkflowId: options.parentWorkflowId,
106
110
  workflowId: options.workflowId || HotMesh.guid(),
107
111
  workflowTopic: workflowTopic,
@@ -113,12 +117,12 @@ export class ClientService {
113
117
  payload,
114
118
  context as JobState
115
119
  );
116
- // Seed search data if present
120
+ // Seed search data
117
121
  if (jobId && options.search?.data) {
118
122
  const searchSessionId = `-search-0`;
119
123
  const search = new Search(jobId, hotMeshClient, searchSessionId);
120
124
  const entries = Object.entries(options.search.data).flat();
121
- search.set(...entries);
125
+ await search.set(...entries);
122
126
  }
123
127
  return new WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
124
128
  },
@@ -198,9 +202,8 @@ export class ClientService {
198
202
  }
199
203
 
200
204
  static async shutdown(): Promise<void> {
201
- for (const [key, value] of ClientService.instances) {
202
- const hotMesh = await value;
203
- await hotMesh.stop();
205
+ for (const [_, hotMeshInstance] of ClientService.instances) {
206
+ (await hotMeshInstance).stop();
204
207
  }
205
208
  }
206
209
  }