@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
@@ -1,6 +1,6 @@
1
1
  import { MetricTypes } from "./stats";
2
2
  import { StreamRetryPolicy } from "./stream";
3
- type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle' | 'signal' | 'hook';
3
+ type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'interrupt' | 'cycle' | 'signal' | 'hook';
4
4
  type Consumes = Record<string, string[]>;
5
5
  interface BaseActivity {
6
6
  title?: string;
@@ -32,13 +32,33 @@ interface Measure {
32
32
  target: string;
33
33
  }
34
34
  interface TriggerActivityStats {
35
+ /**
36
+ * parent job; including this allows the parent's
37
+ * expiration/interruption events to cascade; set
38
+ * `expire` in the YAML for the dependent graph
39
+ * to 0 and provide the parent for dependent,
40
+ * cascading interruption and cleanup
41
+ */
42
+ parent?: string;
35
43
  id?: {
36
44
  [key: string]: unknown;
37
45
  } | string;
38
46
  key?: {
39
47
  [key: string]: unknown;
40
48
  } | string;
49
+ /**
50
+ * @deprecated
51
+ * return 'infinity' to disable; default behavior
52
+ * is to always segment keys by time to ensure
53
+ * indexes (Redis LIST) never grow unbounded
54
+ * as a default behavior; for now, 5m is default
55
+ * and infinity can be set to override
56
+ */
41
57
  granularity?: string;
58
+ /**
59
+ * @deprecated
60
+ * what to capture
61
+ */
42
62
  measures?: Measure[];
43
63
  }
44
64
  interface TriggerActivity extends BaseActivity {
@@ -74,10 +94,20 @@ interface SignalActivity extends BaseActivity {
74
94
  status?: string;
75
95
  code?: number;
76
96
  }
77
- interface IterateActivity extends BaseActivity {
78
- type: 'iterate';
97
+ interface InterruptActivity extends BaseActivity {
98
+ type: 'interrupt';
99
+ /** Optional Reason; will be used as the error `message` when thrown; NOTE: 410 is the error `code` */
100
+ reason?: string;
101
+ /** default is `true` (throw JobInterrupted error upon interrupting) */
102
+ throw?: boolean;
103
+ /** TODO: // default is `false` (do not interrupt child jobs) */
104
+ descend?: boolean;
105
+ /** target job id (if not present the current job will be targeted) */
106
+ target?: string;
107
+ /** topic to publish the interrupt message (if not present the current job topic will be used) */
108
+ topic?: string;
79
109
  }
80
- type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | IterateActivity | HookActivity;
110
+ type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | InterruptActivity | HookActivity | SignalActivity | CycleActivity;
81
111
  type ActivityData = Record<string, any>;
82
112
  type ActivityMetadata = {
83
113
  aid: string;
@@ -103,4 +133,4 @@ type ActivityDataType = {
103
133
  hook?: Record<string, unknown>;
104
134
  };
105
135
  type ActivityLeg = 1 | 2;
106
- export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, HookActivity, SignalActivity, BaseActivity, IterateActivity, TriggerActivity, WorkerActivity };
136
+ export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, HookActivity, SignalActivity, BaseActivity, InterruptActivity, TriggerActivity, WorkerActivity };
@@ -52,10 +52,12 @@ type WorkflowOptions = {
52
52
  entity?: string;
53
53
  workflowName?: string;
54
54
  parentWorkflowId?: string;
55
+ originJobId?: string;
55
56
  workflowTrace?: string;
56
57
  workflowSpan?: string;
57
58
  search?: WorkflowSearchOptions;
58
59
  config?: WorkflowConfig;
60
+ expire?: number;
59
61
  };
60
62
  type HookOptions = {
61
63
  namespace?: string;
@@ -83,6 +85,8 @@ type WorkflowDataType = {
83
85
  arguments: any[];
84
86
  workflowId: string;
85
87
  workflowTopic: string;
88
+ workflowDimension?: string;
89
+ originJobId?: string;
86
90
  };
87
91
  type MeshOSClassConfig = {
88
92
  namespace: string;
@@ -1,4 +1,4 @@
1
- export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, HookActivity, WorkerActivity, IterateActivity, SignalActivity, TriggerActivity, TriggerActivityStats } from './activity';
1
+ export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, HookActivity, WorkerActivity, InterruptActivity, SignalActivity, TriggerActivity, TriggerActivityStats } from './activity';
2
2
  export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
3
3
  export { AsyncSignal } from './async';
4
4
  export { CacheMode } from './cache';
@@ -39,6 +39,18 @@ type JobState = {
39
39
  errors: ActivityData;
40
40
  };
41
41
  };
42
+ type JobInterruptOptions = {
43
+ /** Optional reason when throwing the error */
44
+ reason?: string;
45
+ /** default is `true` when `undefined` (throw JobInterrupted/410 error) */
46
+ throw?: boolean;
47
+ /** default behavior is `false` when `undefined` (do NOT interrupt child jobs) */
48
+ descend?: boolean;
49
+ /** default is false; if true, errors related to inactivation (like overage...already inactive) are suppressed/ignored */
50
+ suppress?: boolean;
51
+ /** how long to wait in seconds before fully expiring/removing the hash from Redis; the job is inactive, but can remain in the cache indefinitely. minimum 1 second.*/
52
+ expire?: number;
53
+ };
42
54
  type JobOutput = {
43
55
  metadata: JobMetadata;
44
56
  data: JobData;
@@ -47,4 +59,9 @@ type PartialJobState = {
47
59
  metadata: JobMetadata | Pick<JobMetadata, 'jid' | 'dad' | 'aid'>;
48
60
  data: JobData;
49
61
  };
50
- export { JobState, JobStatus, JobData, JobsData, JobMetadata, PartialJobState, JobOutput };
62
+ type JobCompletionOptions = {
63
+ emit?: boolean;
64
+ interrupt?: boolean;
65
+ expire?: number;
66
+ };
67
+ export { JobCompletionOptions, JobInterruptOptions, JobData, JobsData, JobMetadata, JobOutput, JobState, JobStatus, PartialJobState, };
@@ -1,11 +1,4 @@
1
1
  import { JobOutput } from "./job";
2
- /**
3
- * The types in this file are used to define those messages that are sent
4
- * to hotmesh client instances when a new version is about to be activated.
5
- * These messages serve to coordinate the cache invalidation and switch-over
6
- * to the new version without any downtime and a coordinating parent server.
7
- */
8
- export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage;
9
2
  export interface PingMessage {
10
3
  type: 'ping';
11
4
  originator: string;
@@ -14,6 +7,10 @@ export interface WorkMessage {
14
7
  type: 'work';
15
8
  originator: string;
16
9
  }
10
+ export interface CronMessage {
11
+ type: 'cron';
12
+ originator: string;
13
+ }
17
14
  export interface PongMessage {
18
15
  type: 'pong';
19
16
  originator: string;
@@ -44,3 +41,10 @@ export interface SubscriptionCallback {
44
41
  export interface QuorumMessageCallback {
45
42
  (topic: string, message: QuorumMessage): void;
46
43
  }
44
+ /**
45
+ * The types in this file are used to define those messages that are sent
46
+ * to hotmesh client instances when a new version is about to be activated.
47
+ * These messages serve to coordinate the cache invalidation and switch-over
48
+ * to the new version without any downtime and a coordinating parent server.
49
+ */
50
+ export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
@@ -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.33",
3
+ "version": "0.0.35",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -23,12 +23,14 @@
23
23
  "test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
24
24
  "test:compile": "NODE_ENV=test jest ./tests/functional/compile/index.test.ts --detectOpenHandles --forceExit --verbose",
25
25
  "test:cycle": "NODE_ENV=test jest ./tests/functional/cycle/index.test.ts --detectOpenHandles --forceExit --verbose",
26
+ "test:trigger": "NODE_ENV=test jest ./tests/unit/services/activities/trigger.test.ts --detectOpenHandles --forceExit --verbose",
26
27
  "test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/index.test.ts --detectOpenHandles --forceExit --verbose",
27
28
  "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
28
29
  "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
29
30
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
30
31
  "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
31
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",
32
34
  "test:parallel": "NODE_ENV=test jest ./tests/functional/parallel/index.test.ts --detectOpenHandles --forceExit --verbose",
33
35
  "test:sequence": "NODE_ENV=test jest ./tests/functional/sequence/index.test.ts --detectOpenHandles --forceExit --verbose",
34
36
  "test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -45,6 +47,7 @@
45
47
  "test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
46
48
  "test:durable:meshos": "NODE_ENV=test jest ./tests/durable/meshos/index.test.ts --detectOpenHandles --forceExit --verbose",
47
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",
48
51
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
49
52
  "test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
50
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,