@hotmeshio/hotmesh 0.0.34 → 0.0.36

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 (93) 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 +3 -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 +7 -3
  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/meshos.js +2 -2
  33. package/build/services/durable/worker.js +9 -2
  34. package/build/services/durable/workflow.d.ts +24 -0
  35. package/build/services/durable/workflow.js +56 -1
  36. package/build/services/engine/index.d.ts +14 -6
  37. package/build/services/engine/index.js +52 -27
  38. package/build/services/hotmesh/index.d.ts +3 -1
  39. package/build/services/hotmesh/index.js +11 -3
  40. package/build/services/quorum/index.d.ts +1 -0
  41. package/build/services/quorum/index.js +10 -0
  42. package/build/services/signaler/stream.js +25 -29
  43. package/build/services/store/clients/ioredis.js +1 -0
  44. package/build/services/store/index.d.ts +40 -4
  45. package/build/services/store/index.js +114 -9
  46. package/build/services/task/index.d.ts +5 -4
  47. package/build/services/task/index.js +12 -14
  48. package/build/types/activity.d.ts +35 -5
  49. package/build/types/durable.d.ts +4 -0
  50. package/build/types/index.d.ts +1 -1
  51. package/build/types/job.d.ts +18 -1
  52. package/build/types/quorum.d.ts +11 -7
  53. package/build/types/stream.d.ts +4 -1
  54. package/build/types/stream.js +2 -0
  55. package/modules/enums.ts +32 -0
  56. package/modules/errors.ts +24 -9
  57. package/modules/key.ts +4 -1
  58. package/modules/utils.ts +5 -0
  59. package/package.json +3 -1
  60. package/services/activities/activity.ts +34 -8
  61. package/services/activities/await.ts +11 -4
  62. package/services/activities/cycle.ts +10 -3
  63. package/services/activities/hook.ts +8 -3
  64. package/services/activities/index.ts +2 -2
  65. package/services/activities/interrupt.ts +159 -0
  66. package/services/activities/signal.ts +9 -3
  67. package/services/activities/trigger.ts +21 -5
  68. package/services/activities/worker.ts +10 -3
  69. package/services/collator/index.ts +10 -1
  70. package/services/compiler/deployer.ts +1 -3
  71. package/services/connector/index.ts +3 -5
  72. package/services/durable/client.ts +8 -4
  73. package/services/durable/factory.ts +65 -284
  74. package/services/durable/handle.ts +55 -9
  75. package/services/durable/meshos.ts +2 -3
  76. package/services/durable/worker.ts +9 -2
  77. package/services/durable/workflow.ts +66 -2
  78. package/services/engine/index.ts +74 -26
  79. package/services/hotmesh/index.ts +14 -4
  80. package/services/quorum/index.ts +9 -0
  81. package/services/signaler/stream.ts +27 -24
  82. package/services/store/clients/ioredis.ts +1 -0
  83. package/services/store/index.ts +119 -11
  84. package/services/task/index.ts +18 -18
  85. package/types/activity.ts +38 -8
  86. package/types/durable.ts +8 -4
  87. package/types/index.ts +1 -1
  88. package/types/job.ts +30 -1
  89. package/types/quorum.ts +13 -8
  90. package/types/stream.ts +3 -0
  91. package/build/services/activities/iterate.d.ts +0 -9
  92. package/build/services/activities/iterate.js +0 -13
  93. package/services/activities/iterate.ts +0 -26
@@ -22,10 +22,13 @@ export declare enum StreamDataType {
22
22
  RESULT = "result",
23
23
  WORKER = "worker",
24
24
  RESPONSE = "response",
25
- TRANSITION = "transition"
25
+ TRANSITION = "transition",
26
+ SIGNAL = "signal",
27
+ INTERRUPT = "interrupt"
26
28
  }
27
29
  export interface StreamData {
28
30
  metadata: {
31
+ guid: string;
29
32
  topic?: string;
30
33
  jid?: string;
31
34
  dad?: string;
@@ -16,6 +16,8 @@ var StreamDataType;
16
16
  StreamDataType["WORKER"] = "worker";
17
17
  StreamDataType["RESPONSE"] = "response";
18
18
  StreamDataType["TRANSITION"] = "transition";
19
+ StreamDataType["SIGNAL"] = "signal";
20
+ StreamDataType["INTERRUPT"] = "interrupt";
19
21
  })(StreamDataType = exports.StreamDataType || (exports.StreamDataType = {}));
20
22
  var StreamRole;
21
23
  (function (StreamRole) {
@@ -0,0 +1,32 @@
1
+ // Engine Constants
2
+ export const STATUS_CODE_SUCCESS = 200;
3
+ export const STATUS_CODE_PENDING = 202;
4
+ export const STATUS_CODE_TIMEOUT = 504;
5
+ export const STATUS_CODE_INTERRUPT = 410;
6
+ export const OTT_WAIT_TIME = 1000;
7
+
8
+ // Stream Constants
9
+ export const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
10
+ export const MAX_TIMEOUT_MS = 60000;
11
+ export const GRADUATED_INTERVAL_MS = 5000;
12
+
13
+ export const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
14
+ export const TEST_BLOCK_DURATION = 1000; //Set to `1000` so tests can interrupt quickly
15
+ export const BLOCK_TIME_MS = process.env.NODE_ENV === 'test' ? TEST_BLOCK_DURATION : BLOCK_DURATION;
16
+
17
+ export const XCLAIM_DELAY_MS = 1000 * 60; //max time a message can be unacked before it is claimed by another
18
+ export const XCLAIM_COUNT = 3; //max number of times a message can be claimed by another before it is dead-lettered
19
+ export const XPENDING_COUNT = 10;
20
+
21
+ export const STATUS_CODE_UNACKED = 999;
22
+ export const STATUS_CODE_UNKNOWN = 500;
23
+ export const STATUS_MESSAGE_UNKNOWN = 'unknown';
24
+
25
+ // HotMesh Constants
26
+ export const EXPIRE_DURATION = 15; // default expire in seconds; once job state semaphore reaches '0', this is applied to set Redis to expire the job HASH
27
+ export const BASE_FIDELITY_SECONDS = 15; // granularity resolution window size
28
+ export const TEST_FIDELITY_SECONDS = 5;
29
+ export const FIDELITY_SECONDS = process.env.NODE_ENV === 'test' ? TEST_FIDELITY_SECONDS : BASE_FIDELITY_SECONDS
30
+
31
+ // DURABLE CONSTANTS
32
+ export const DURABLE_EXPIRE_SECONDS = 1;
package/modules/errors.ts CHANGED
@@ -2,8 +2,11 @@ import { ActivityDuplex } from "../types/activity";
2
2
  import { CollationFaultType, CollationStage } from "../types/collator";
3
3
 
4
4
  class GetStateError extends Error {
5
- constructor() {
6
- super("Error occurred while getting job state");
5
+ jobId: string;
6
+ code: 404;
7
+ constructor(jobId: string) {
8
+ super(`${jobId} Not Found`);
9
+ this.jobId = jobId;
7
10
  }
8
11
  }
9
12
  class SetStateError extends Error {
@@ -107,7 +110,18 @@ class DuplicateJobError extends Error {
107
110
  this.message = `Duplicate job: ${jobId}`;
108
111
  }
109
112
  }
110
-
113
+ class InactiveJobError extends Error {
114
+ jobId: string;
115
+ activityId: string;
116
+ status: number; //non-positive integer
117
+ constructor(jobId: string, status: number, activityId: string) {
118
+ super("Inactive job");
119
+ this.jobId = jobId;
120
+ this.activityId = activityId;
121
+ this.message = `Inactive job: ${jobId}`;
122
+ this.status = status;
123
+ }
124
+ }
111
125
  class ExecActivityError extends Error {
112
126
  constructor() {
113
127
  super("Error occurred while executing activity");
@@ -131,18 +145,19 @@ class CollationError extends Error {
131
145
 
132
146
  export {
133
147
  CollationError,
134
- DurableTimeoutError,
135
- DurableMaxedError,
136
148
  DurableFatalError,
137
- DurableRetryError,
138
- DurableWaitForSignalError,
139
149
  DurableIncompleteSignalError,
150
+ DurableMaxedError,
151
+ DurableRetryError,
140
152
  DurableSleepError,
141
153
  DurableSleepForError,
154
+ DurableTimeoutError,
155
+ DurableWaitForSignalError,
142
156
  DuplicateJobError,
157
+ ExecActivityError,
143
158
  GetStateError,
144
- SetStateError,
159
+ InactiveJobError,
145
160
  MapDataError,
146
161
  RegisterTimeoutError,
147
- ExecActivityError
162
+ SetStateError,
148
163
  };
package/modules/key.ts CHANGED
@@ -34,6 +34,7 @@ enum KeyType {
34
34
  APP,
35
35
  ENGINE_ID,
36
36
  HOOKS,
37
+ JOB_DEPENDENTS,
37
38
  JOB_STATE,
38
39
  JOB_STATS_GENERAL,
39
40
  JOB_STATS_MEDIAN,
@@ -93,7 +94,9 @@ class KeyService {
93
94
  case KeyType.QUORUM:
94
95
  return `${namespace}:${params.appId}:q:${params.engineId || ''}`;
95
96
  case KeyType.JOB_STATE:
96
- return `${namespace}:${params.appId}:j:${params.jobId}`;
97
+ return `${namespace}:${params.appId}:j:${params.jobId}`;
98
+ case KeyType.JOB_DEPENDENTS:
99
+ return `${namespace}:${params.appId}:d:${params.jobId}`;
97
100
  case KeyType.JOB_STATS_GENERAL:
98
101
  return `${namespace}:${params.appId}:s:${params.jobKey}:${params.dateTime}`;
99
102
  case KeyType.JOB_STATS_MEDIAN:
package/modules/utils.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { nanoid } from "nanoid";
1
2
  import { StoreService } from "../services/store";
2
3
  import { AppSubscriptions, AppTransitions, AppVID } from "../types/app";
3
4
  import { RedisClient, RedisMulti } from "../types/redis";
@@ -8,6 +9,10 @@ export async function sleepFor(ms: number) {
8
9
  return new Promise((resolve) => setTimeout(resolve, ms));
9
10
  }
10
11
 
12
+ export function guid(): string {
13
+ return nanoid();
14
+ }
15
+
11
16
  export function deterministicRandom(seed: number): number {
12
17
  let x = Math.sin(seed) * 10000;
13
18
  return x - Math.floor(x);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.34",
3
+ "version": "0.0.36",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -30,6 +30,7 @@
30
30
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
31
31
  "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
32
32
  "test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
33
+ "test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
33
34
  "test:parallel": "NODE_ENV=test jest ./tests/functional/parallel/index.test.ts --detectOpenHandles --forceExit --verbose",
34
35
  "test:sequence": "NODE_ENV=test jest ./tests/functional/sequence/index.test.ts --detectOpenHandles --forceExit --verbose",
35
36
  "test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -46,6 +47,7 @@
46
47
  "test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
47
48
  "test:durable:meshos": "NODE_ENV=test jest ./tests/durable/meshos/index.test.ts --detectOpenHandles --forceExit --verbose",
48
49
  "test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
50
+ "test:durable:interrupt": "NODE_ENV=test jest ./tests/durable/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
49
51
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
50
52
  "test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
51
53
  "test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -1,7 +1,8 @@
1
- import { CollationError } from '../../modules/errors';
1
+ import { CollationError, GetStateError, InactiveJobError } from '../../modules/errors';
2
2
  import {
3
3
  formatISODate,
4
4
  getValueByPath,
5
+ guid,
5
6
  restoreHierarchy } from '../../modules/utils';
6
7
  import { CollatorService } from '../collator';
7
8
  import { EngineService } from '../engine';
@@ -29,6 +30,7 @@ import {
29
30
  StreamDataType,
30
31
  StreamStatus } from '../../types/stream';
31
32
  import { TransitionRule } from '../../types/transition';
33
+ import { EXPIRE_DURATION } from '../../modules/enums';
32
34
 
33
35
  /**
34
36
  * The base class for all activities
@@ -84,6 +86,7 @@ class Activity {
84
86
  let telemetry: TelemetryService;
85
87
  try {
86
88
  await this.getState();
89
+ CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
87
90
  const aState = await CollatorService.notarizeReentry(this);
88
91
  this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
89
92
 
@@ -110,12 +113,18 @@ class Activity {
110
113
  if (error instanceof CollationError) {
111
114
  this.logger.info('process-event-inactive-error', { error });
112
115
  return;
116
+ } else if (error instanceof InactiveJobError) {
117
+ this.logger.info('process-event-inactive-job-error', { error });
118
+ return;
119
+ } else if (error instanceof GetStateError) {
120
+ this.logger.info('process-event-get-job-error', { error });
121
+ return;
113
122
  }
114
123
  this.logger.error('activity-process-event-error', { error });
115
124
  telemetry && telemetry.setActivityError(error.message);
116
125
  throw error;
117
126
  } finally {
118
- telemetry && telemetry.endActivitySpan();
127
+ telemetry?.endActivitySpan();
119
128
  this.logger.debug('activity-process-event-end', { jid, aid });
120
129
  }
121
130
  }
@@ -159,6 +168,7 @@ class Activity {
159
168
  telemetry.mapActivityAttributes();
160
169
  const jobStatus = this.resolveStatus(multiResponse);
161
170
  const attrs: StringScalarType = { 'app.job.jss': jobStatus };
171
+ //adjacencyList membership has already been set at this point (according to activity status)
162
172
  const messageIds = await this.transition(this.adjacencyList, jobStatus);
163
173
  if (messageIds.length) {
164
174
  attrs['app.activity.mids'] = messageIds.join(',')
@@ -380,12 +390,16 @@ class Activity {
380
390
  self.hook = { };
381
391
  }
382
392
  context['$self'] = self;
383
- context['$job'] = context; //NEVER call STRINGIFY! (circular)
393
+ context['$job'] = context; //NEVER call STRINGIFY! (now circular)
384
394
  return context as JobState;
385
395
  }
386
396
 
387
397
  initPolicies(context: JobState) {
388
- context.metadata.expire = this.config.expire;
398
+ const expire = Pipe.resolve(
399
+ this.config.expire ?? EXPIRE_DURATION,
400
+ context
401
+ );
402
+ context.metadata.expire = expire;
389
403
  }
390
404
 
391
405
  bindActivityData(type: 'output' | 'hook'): void {
@@ -418,6 +432,7 @@ class Activity {
418
432
  if (MapperService.evaluate(transitionRule, this.context, this.code)) {
419
433
  adjacencyList.push({
420
434
  metadata: {
435
+ guid: guid(),
421
436
  jid: this.context.metadata.jid,
422
437
  dad: adjacentDad,
423
438
  aid: toActivityId,
@@ -434,17 +449,20 @@ class Activity {
434
449
  }
435
450
 
436
451
  async transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]> {
452
+ if (this.jobWasInterrupted(jobStatus)) {
453
+ return;
454
+ }
437
455
  let mIds: string[] = [];
438
456
  let emit: boolean = false;
439
457
  if (this.config.emit) {
440
458
  emit = Pipe.resolve(this.config.emit, this.context);
441
459
  }
442
460
  if (jobStatus <= 0 || emit) {
443
- //activity should not send 'emit' if the job is truly over
444
- const isTrueEmit = jobStatus > 0;
445
- await this.engine.runJobCompletionTasks(this.context, isTrueEmit);
461
+ await this.engine.runJobCompletionTasks(
462
+ this.context,
463
+ { emit: jobStatus > 0 },
464
+ );
446
465
  }
447
-
448
466
  if (adjacencyList.length && jobStatus > 0) {
449
467
  const multi = this.store.getMulti();
450
468
  for (const execSignal of adjacencyList) {
@@ -454,6 +472,14 @@ class Activity {
454
472
  }
455
473
  return mIds;
456
474
  }
475
+
476
+ /**
477
+ * A job with a vale < -100_000_000 is considered interrupted,
478
+ * as the interruption event decrements the job status by 1billion.
479
+ */
480
+ jobWasInterrupted(jobStatus: JobStatus): boolean {
481
+ return jobStatus < -100_000_000;
482
+ }
457
483
  }
458
484
 
459
485
  export { Activity, ActivityType };
@@ -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, StreamDataType } 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 Await extends Activity {
17
18
  config: AwaitActivity;
@@ -35,6 +36,7 @@ class Await 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();
@@ -57,15 +59,19 @@ class Await extends Activity {
57
59
  });
58
60
  return this.context.metadata.aid;
59
61
  } catch (error) {
60
- telemetry.setActivityError(error.message);
61
- if (error instanceof GetStateError) {
62
+ if (error instanceof InactiveJobError) {
63
+ this.logger.error('await-inactive-job-error', { error });
64
+ return;
65
+ } else if (error instanceof GetStateError) {
62
66
  this.logger.error('await-get-state-error', { error });
67
+ return;
63
68
  } else {
64
69
  this.logger.error('await-process-error', { error });
65
70
  }
71
+ telemetry.setActivityError(error.message);
66
72
  throw error;
67
73
  } finally {
68
- telemetry.endActivitySpan();
74
+ telemetry?.endActivitySpan();
69
75
  this.logger.debug('await-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
70
76
  }
71
77
  }
@@ -74,6 +80,7 @@ class Await extends Activity {
74
80
  const topic = Pipe.resolve(this.config.subtype, this.context);
75
81
  const streamData: StreamData = {
76
82
  metadata: {
83
+ guid: guid(),
77
84
  jid: this.context.metadata.jid,
78
85
  dad: this.metadata.dad,
79
86
  aid: this.metadata.aid,
@@ -1,4 +1,4 @@
1
- import { GetStateError } from '../../modules/errors';
1
+ import { GetStateError, InactiveJobError } from '../../modules/errors';
2
2
  import { CollatorService } from '../collator';
3
3
  import { EngineService } from '../engine';
4
4
  import { Activity, ActivityType } from './activity';
@@ -10,6 +10,7 @@ import { JobState } from '../../types/job';
10
10
  import { MultiResponseFlags, RedisMulti } from '../../types/redis';
11
11
  import { StreamData } from '../../types/stream';
12
12
  import { TelemetryService } from '../telemetry';
13
+ import { guid } from '../../modules/utils';
13
14
 
14
15
  class Cycle extends Activity {
15
16
  config: CycleActivity;
@@ -34,6 +35,7 @@ class Cycle extends Activity {
34
35
  this.setLeg(1);
35
36
  await CollatorService.notarizeEntry(this);
36
37
  await this.getState();
38
+ CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
37
39
  telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
38
40
  telemetry.startActivitySpan(this.leg);
39
41
  this.mapInputData();
@@ -60,15 +62,19 @@ class Cycle extends Activity {
60
62
 
61
63
  return this.context.metadata.aid;
62
64
  } catch (error) {
63
- if (error instanceof GetStateError) {
65
+ if (error instanceof InactiveJobError) {
66
+ this.logger.error('cycle-inactive-job-error', { error });
67
+ return;
68
+ } else if (error instanceof GetStateError) {
64
69
  this.logger.error('cycle-get-state-error', { error });
70
+ return;
65
71
  } else {
66
72
  this.logger.error('cycle-process-error', { error });
67
73
  }
68
74
  telemetry.setActivityError(error.message);
69
75
  throw error;
70
76
  } finally {
71
- telemetry.endActivitySpan();
77
+ telemetry?.endActivitySpan();
72
78
  this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
73
79
  }
74
80
  }
@@ -88,6 +94,7 @@ class Cycle extends Activity {
88
94
  this.mapInputData();
89
95
  const streamData: StreamData = {
90
96
  metadata: {
97
+ guid: guid(),
91
98
  dad: CollatorService.resolveReentryDimension(this),
92
99
  jid: this.context.metadata.jid,
93
100
  aid: this.config.ancestor,
@@ -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';
@@ -43,6 +43,7 @@ class Hook extends Activity {
43
43
  await CollatorService.notarizeEntry(this);
44
44
 
45
45
  await this.getState();
46
+ CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
46
47
  telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
47
48
  telemetry.startActivitySpan(this.leg);
48
49
  let multiResponse: MultiResponseFlags;
@@ -81,15 +82,19 @@ class Hook extends Activity {
81
82
 
82
83
  return this.context.metadata.aid;
83
84
  } catch (error) {
84
- if (error instanceof GetStateError) {
85
+ if (error instanceof InactiveJobError) {
86
+ this.logger.error('hook-inactive-job-error', { error });
87
+ return;
88
+ } else if (error instanceof GetStateError) {
85
89
  this.logger.error('hook-get-state-error', { error });
90
+ return;
86
91
  } else {
87
92
  this.logger.error('hook-process-error', { error });
88
93
  }
89
94
  telemetry.setActivityError(error.message);
90
95
  throw error;
91
96
  } finally {
92
- telemetry.endActivitySpan();
97
+ telemetry?.endActivitySpan();
93
98
  this.logger.debug('hook-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
94
99
  }
95
100
  }
@@ -2,7 +2,7 @@ import { Activity } from './activity';
2
2
  import { Await } from './await';
3
3
  import { Cycle } from './cycle';
4
4
  import { Hook } from './hook';
5
- import { Iterate } from './iterate';
5
+ import { Interrupt } from './interrupt';
6
6
  import { Signal } from './signal';
7
7
  import { Trigger } from './trigger';
8
8
  import { Worker } from './worker';
@@ -12,7 +12,7 @@ export default {
12
12
  await: Await,
13
13
  cycle: Cycle,
14
14
  hook: Hook,
15
- iterate: Iterate,
15
+ interrupt: Interrupt,
16
16
  signal: Signal,
17
17
  trigger: Trigger,
18
18
  worker: Worker,
@@ -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
  }