@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.
- package/README.md +30 -18
- package/build/modules/enums.d.ts +22 -0
- package/build/modules/enums.js +29 -0
- package/build/modules/errors.d.ts +10 -2
- package/build/modules/errors.js +14 -3
- package/build/modules/key.d.ts +16 -15
- package/build/modules/key.js +18 -15
- package/build/modules/utils.d.ts +1 -0
- package/build/modules/utils.js +6 -1
- package/build/package.json +3 -1
- package/build/services/activities/activity.d.ts +5 -0
- package/build/services/activities/activity.js +27 -6
- package/build/services/activities/await.js +11 -3
- package/build/services/activities/cycle.js +10 -2
- package/build/services/activities/hook.js +8 -2
- package/build/services/activities/index.d.ts +2 -2
- package/build/services/activities/index.js +2 -2
- package/build/services/activities/interrupt.d.ts +16 -0
- package/build/services/activities/interrupt.js +129 -0
- package/build/services/activities/signal.js +9 -2
- package/build/services/activities/trigger.d.ts +4 -0
- package/build/services/activities/trigger.js +14 -4
- package/build/services/activities/worker.js +10 -2
- package/build/services/collator/index.d.ts +4 -0
- package/build/services/collator/index.js +8 -0
- package/build/services/compiler/deployer.js +1 -3
- package/build/services/connector/index.js +2 -3
- package/build/services/durable/client.js +7 -3
- package/build/services/durable/factory.js +65 -284
- package/build/services/durable/handle.d.ts +37 -0
- package/build/services/durable/handle.js +52 -9
- package/build/services/durable/meshos.js +2 -2
- package/build/services/durable/worker.js +9 -2
- package/build/services/durable/workflow.d.ts +24 -0
- package/build/services/durable/workflow.js +56 -1
- package/build/services/engine/index.d.ts +14 -6
- package/build/services/engine/index.js +52 -27
- package/build/services/hotmesh/index.d.ts +3 -1
- package/build/services/hotmesh/index.js +11 -3
- package/build/services/quorum/index.d.ts +1 -0
- package/build/services/quorum/index.js +10 -0
- package/build/services/signaler/stream.js +25 -29
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +40 -4
- package/build/services/store/index.js +114 -9
- package/build/services/task/index.d.ts +5 -4
- package/build/services/task/index.js +12 -14
- package/build/types/activity.d.ts +35 -5
- package/build/types/durable.d.ts +4 -0
- package/build/types/index.d.ts +1 -1
- package/build/types/job.d.ts +18 -1
- package/build/types/quorum.d.ts +11 -7
- package/build/types/stream.d.ts +4 -1
- package/build/types/stream.js +2 -0
- package/modules/enums.ts +32 -0
- package/modules/errors.ts +24 -9
- package/modules/key.ts +4 -1
- package/modules/utils.ts +5 -0
- package/package.json +3 -1
- package/services/activities/activity.ts +34 -8
- package/services/activities/await.ts +11 -4
- package/services/activities/cycle.ts +10 -3
- package/services/activities/hook.ts +8 -3
- package/services/activities/index.ts +2 -2
- package/services/activities/interrupt.ts +159 -0
- package/services/activities/signal.ts +9 -3
- package/services/activities/trigger.ts +21 -5
- package/services/activities/worker.ts +10 -3
- package/services/collator/index.ts +10 -1
- package/services/compiler/deployer.ts +1 -3
- package/services/connector/index.ts +3 -5
- package/services/durable/client.ts +8 -4
- package/services/durable/factory.ts +65 -284
- package/services/durable/handle.ts +55 -9
- package/services/durable/meshos.ts +2 -3
- package/services/durable/worker.ts +9 -2
- package/services/durable/workflow.ts +66 -2
- package/services/engine/index.ts +74 -26
- package/services/hotmesh/index.ts +14 -4
- package/services/quorum/index.ts +9 -0
- package/services/signaler/stream.ts +27 -24
- package/services/store/clients/ioredis.ts +1 -0
- package/services/store/index.ts +119 -11
- package/services/task/index.ts +18 -18
- package/types/activity.ts +38 -8
- package/types/durable.ts +8 -4
- package/types/index.ts +1 -1
- package/types/job.ts +30 -1
- package/types/quorum.ts +13 -8
- package/types/stream.ts +3 -0
- package/build/services/activities/iterate.d.ts +0 -9
- package/build/services/activities/iterate.js +0 -13
- package/services/activities/iterate.ts +0 -26
package/build/types/stream.d.ts
CHANGED
|
@@ -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;
|
package/build/types/stream.js
CHANGED
|
@@ -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) {
|
package/modules/enums.ts
ADDED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
-
|
|
159
|
+
InactiveJobError,
|
|
145
160
|
MapDataError,
|
|
146
161
|
RegisterTimeoutError,
|
|
147
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
85
|
+
telemetry?.endActivitySpan();
|
|
80
86
|
this.logger.debug('signal-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
81
87
|
}
|
|
82
88
|
}
|