@hotmeshio/hotmesh 0.0.10 → 0.0.11
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 +5 -4
- package/build/modules/errors.d.ts +17 -1
- package/build/modules/errors.js +29 -1
- package/build/package.json +2 -1
- package/build/services/activities/activity.d.ts +1 -0
- package/build/services/activities/activity.js +13 -2
- package/build/services/activities/cycle.js +6 -1
- package/build/services/activities/trigger.js +2 -3
- package/build/services/collator/index.d.ts +8 -0
- package/build/services/collator/index.js +11 -1
- package/build/services/durable/factory.d.ts +18 -1
- package/build/services/durable/factory.js +46 -4
- package/build/services/durable/handle.js +25 -7
- package/build/services/durable/worker.d.ts +3 -3
- package/build/services/durable/worker.js +16 -10
- package/build/services/durable/workflow.js +1 -1
- package/build/services/pipe/functions/math.d.ts +4 -0
- package/build/services/pipe/functions/math.js +73 -0
- package/build/services/pipe/functions/number.d.ts +0 -4
- package/build/services/pipe/functions/number.js +0 -73
- package/build/services/signaler/stream.js +6 -3
- package/build/types/durable.d.ts +7 -2
- package/build/types/index.d.ts +1 -0
- package/modules/errors.ts +42 -1
- package/package.json +2 -1
- package/services/activities/activity.ts +14 -2
- package/services/activities/cycle.ts +6 -1
- package/services/activities/trigger.ts +2 -3
- package/services/collator/index.ts +12 -1
- package/services/durable/factory.ts +46 -4
- package/services/durable/handle.ts +23 -8
- package/services/durable/worker.ts +27 -12
- package/services/durable/workflow.ts +1 -1
- package/services/pipe/functions/math.ts +74 -0
- package/services/pipe/functions/number.ts +0 -75
- package/services/signaler/stream.ts +6 -3
- package/types/durable.ts +11 -4
- package/types/index.ts +15 -0
- package/build/services/dimension/index.d.ts +0 -29
- package/build/services/dimension/index.js +0 -35
- package/services/dimension/README.md +0 -73
- package/services/dimension/index.ts +0 -39
|
@@ -56,78 +56,5 @@ class NumberHandler {
|
|
|
56
56
|
round(input) {
|
|
57
57
|
return Math.round(input);
|
|
58
58
|
}
|
|
59
|
-
add(...operands) {
|
|
60
|
-
// @ts-ignore
|
|
61
|
-
return operands.reduce((a, b) => {
|
|
62
|
-
if (Array.isArray(b)) {
|
|
63
|
-
return a + this.add(...b);
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
return a + b;
|
|
67
|
-
}
|
|
68
|
-
}, 0);
|
|
69
|
-
}
|
|
70
|
-
subtract(...operands) {
|
|
71
|
-
if (operands.length === 0) {
|
|
72
|
-
throw new Error('At least one operand is required.');
|
|
73
|
-
}
|
|
74
|
-
let flatOperands = [];
|
|
75
|
-
operands.forEach((op) => {
|
|
76
|
-
if (Array.isArray(op)) {
|
|
77
|
-
flatOperands = [...flatOperands, ...op];
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
flatOperands.push(op);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
if (flatOperands.length === 0) {
|
|
84
|
-
throw new Error('At least one operand is required after flattening.');
|
|
85
|
-
}
|
|
86
|
-
const result = flatOperands.reduce((a, b, i) => {
|
|
87
|
-
return i === 0 ? a : a - b;
|
|
88
|
-
});
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
multiply(...operands) {
|
|
92
|
-
if (operands.length === 0) {
|
|
93
|
-
throw new Error('At least one operand is required.');
|
|
94
|
-
}
|
|
95
|
-
// @ts-ignore
|
|
96
|
-
return operands.reduce((a, b) => {
|
|
97
|
-
if (Array.isArray(b)) {
|
|
98
|
-
return a * this.multiply(...b);
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
return a * b;
|
|
102
|
-
}
|
|
103
|
-
}, 1);
|
|
104
|
-
}
|
|
105
|
-
divide(...operands) {
|
|
106
|
-
if (operands.length === 0) {
|
|
107
|
-
throw new Error('At least one operand is required.');
|
|
108
|
-
}
|
|
109
|
-
let flatOperands = [];
|
|
110
|
-
operands.forEach((op) => {
|
|
111
|
-
if (Array.isArray(op)) {
|
|
112
|
-
flatOperands = [...flatOperands, ...op];
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
flatOperands.push(op);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
if (flatOperands.length === 0) {
|
|
119
|
-
throw new Error('At least one operand is required after flattening.');
|
|
120
|
-
}
|
|
121
|
-
const result = flatOperands.reduce((a, b, i) => {
|
|
122
|
-
if (b === 0) {
|
|
123
|
-
return NaN;
|
|
124
|
-
}
|
|
125
|
-
return i === 0 ? a : a / b;
|
|
126
|
-
});
|
|
127
|
-
if (isNaN(result)) {
|
|
128
|
-
return NaN;
|
|
129
|
-
}
|
|
130
|
-
return result;
|
|
131
|
-
}
|
|
132
59
|
}
|
|
133
60
|
exports.NumberHandler = NumberHandler;
|
|
@@ -5,7 +5,7 @@ const key_1 = require("../../modules/key");
|
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
6
|
const telemetry_1 = require("../telemetry");
|
|
7
7
|
const stream_1 = require("../../types/stream");
|
|
8
|
-
const MAX_RETRIES =
|
|
8
|
+
const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
|
|
9
9
|
const MAX_TIMEOUT_MS = 60000;
|
|
10
10
|
const GRADUATED_INTERVAL_MS = 5000;
|
|
11
11
|
const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
|
|
@@ -158,8 +158,11 @@ class StreamSignaler {
|
|
|
158
158
|
const policy = policies?.[errorCode];
|
|
159
159
|
const maxRetries = policy?.[0];
|
|
160
160
|
const tryCount = Math.min(input.metadata.try || 0, MAX_RETRIES);
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
//only possible values for maxRetries are 1, 2, 3
|
|
162
|
+
//only possible values for tryCount are 0, 1, 2
|
|
163
|
+
if (maxRetries > tryCount) {
|
|
164
|
+
// 10ms, 100ms, or 1000ms delays between system retries
|
|
165
|
+
return [true, Math.pow(10, tryCount + 1)];
|
|
163
166
|
}
|
|
164
167
|
return [false, 0];
|
|
165
168
|
}
|
package/build/types/durable.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ type WorkflowOptions = {
|
|
|
7
7
|
workflowTrace?: string;
|
|
8
8
|
workflowSpan?: string;
|
|
9
9
|
};
|
|
10
|
-
type
|
|
10
|
+
type ActivityWorkflowDataType = {
|
|
11
11
|
activityName: string;
|
|
12
12
|
arguments: any[];
|
|
13
13
|
workflowId: string;
|
|
@@ -35,6 +35,11 @@ type WorkerConfig = {
|
|
|
35
35
|
namespace?: string;
|
|
36
36
|
taskQueue: string;
|
|
37
37
|
workflow: Function;
|
|
38
|
+
options?: WorkerOptions;
|
|
39
|
+
};
|
|
40
|
+
type WorkerOptions = {
|
|
41
|
+
maxSystemRetries?: number;
|
|
42
|
+
backoffExponent?: number;
|
|
38
43
|
};
|
|
39
44
|
type ContextType = {
|
|
40
45
|
workflowId: string;
|
|
@@ -54,4 +59,4 @@ type ActivityConfig = {
|
|
|
54
59
|
maximumInterval: string;
|
|
55
60
|
};
|
|
56
61
|
};
|
|
57
|
-
export { ActivityConfig,
|
|
62
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, };
|
package/build/types/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
|
|
|
3
3
|
export { AsyncSignal } from './async';
|
|
4
4
|
export { CacheMode } from './cache';
|
|
5
5
|
export { CollationFaultType, CollationStage } from './collator';
|
|
6
|
+
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
6
7
|
export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
|
|
7
8
|
export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
|
|
8
9
|
export { ILogger } from './logger';
|
package/modules/errors.ts
CHANGED
|
@@ -12,6 +12,35 @@ class SetStateError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
class DurableTimeoutError extends Error {
|
|
16
|
+
code: number;
|
|
17
|
+
constructor(message: string) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = 596;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
class DurableMaxedError extends Error {
|
|
23
|
+
code: number;
|
|
24
|
+
constructor(message: string) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = 597;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class DurableFatalError extends Error {
|
|
30
|
+
code: number;
|
|
31
|
+
constructor(message: string) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.code = 598;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
class DurableRetryError extends Error {
|
|
37
|
+
code: number;
|
|
38
|
+
constructor(message: string) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.code = 599;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
class MapDataError extends Error {
|
|
16
45
|
constructor() {
|
|
17
46
|
super("Error occurred while mapping data");
|
|
@@ -52,4 +81,16 @@ class CollationError extends Error {
|
|
|
52
81
|
}
|
|
53
82
|
}
|
|
54
83
|
|
|
55
|
-
export {
|
|
84
|
+
export {
|
|
85
|
+
CollationError,
|
|
86
|
+
DurableTimeoutError,
|
|
87
|
+
DurableMaxedError,
|
|
88
|
+
DurableFatalError,
|
|
89
|
+
DurableRetryError,
|
|
90
|
+
DuplicateJobError,
|
|
91
|
+
GetStateError,
|
|
92
|
+
SetStateError,
|
|
93
|
+
MapDataError,
|
|
94
|
+
RegisterTimeoutError,
|
|
95
|
+
ExecActivityError
|
|
96
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "Durable Workflows",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
44
44
|
"test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
45
45
|
"test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
46
|
+
"test:durable:fatal": "NODE_ENV=test jest ./tests/durable/fatal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
46
47
|
"test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
47
48
|
"test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
|
|
48
49
|
},
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
getValueByPath,
|
|
5
5
|
restoreHierarchy } from '../../modules/utils';
|
|
6
6
|
import { CollatorService } from '../collator';
|
|
7
|
-
import { DimensionService } from '../dimension';
|
|
8
7
|
import { EngineService } from '../engine';
|
|
9
8
|
import { ILogger } from '../logger';
|
|
10
9
|
import { MapperService } from '../mapper';
|
|
@@ -84,6 +83,7 @@ class Activity {
|
|
|
84
83
|
if (this.doesHook()) {
|
|
85
84
|
//sleep and wait to awaken upon a signal
|
|
86
85
|
await this.registerHook(multi);
|
|
86
|
+
this.mapOutputData();
|
|
87
87
|
this.mapJobData();
|
|
88
88
|
await this.setState(multi);
|
|
89
89
|
await CollatorService.authorizeReentry(this, multi);
|
|
@@ -94,6 +94,7 @@ class Activity {
|
|
|
94
94
|
} else {
|
|
95
95
|
//end the activity and transition to its children
|
|
96
96
|
this.adjacencyList = await this.filterAdjacent();
|
|
97
|
+
this.mapOutputData();
|
|
97
98
|
this.mapJobData();
|
|
98
99
|
await this.setState(multi);
|
|
99
100
|
await CollatorService.notarizeEarlyCompletion(this, multi);
|
|
@@ -322,6 +323,17 @@ class Activity {
|
|
|
322
323
|
}
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
mapOutputData(): void {
|
|
327
|
+
//activity YAML may include output map data that produces/extends activity output data.
|
|
328
|
+
if(this.config.output?.maps) {
|
|
329
|
+
const mapper = new MapperService(this.config.output.maps, this.context);
|
|
330
|
+
const actOutData = mapper.mapRules();
|
|
331
|
+
const activityId = this.metadata.aid;
|
|
332
|
+
const data = { ...this.context[activityId].output, ...actOutData };
|
|
333
|
+
this.context[activityId].output.data = data;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
325
337
|
async registerTimeout(): Promise<void> {
|
|
326
338
|
//set timeout in support of hook and/or duplex
|
|
327
339
|
}
|
|
@@ -526,7 +538,7 @@ class Activity {
|
|
|
526
538
|
|
|
527
539
|
resolveAdjacentDad(): string {
|
|
528
540
|
//concat self and child dimension (all children (leg 1) begin life at 0)
|
|
529
|
-
return `${this.resolveDad()}${
|
|
541
|
+
return `${this.resolveDad()}${CollatorService.getDimensionalSeed(0)}`;
|
|
530
542
|
};
|
|
531
543
|
|
|
532
544
|
async filterAdjacent(): Promise<StreamData[]> {
|
|
@@ -81,13 +81,18 @@ class Cycle extends Activity {
|
|
|
81
81
|
* pattern allows for retries without violating the DAG.
|
|
82
82
|
*/
|
|
83
83
|
async cycleAncestorActivity(multi: RedisMulti): Promise<string> {
|
|
84
|
+
//Cycle activity L1 is a standin for the target ancestor L1.
|
|
85
|
+
//Input data mapping (mapInputData) allows for the
|
|
86
|
+
//next dimensonal thread to execute with different
|
|
87
|
+
//input data than the current dimensional thread
|
|
88
|
+
this.mapInputData();
|
|
84
89
|
const streamData: StreamData = {
|
|
85
90
|
metadata: {
|
|
86
91
|
dad: CollatorService.resolveReentryDimension(this),
|
|
87
92
|
jid: this.context.metadata.jid,
|
|
88
93
|
aid: this.config.ancestor,
|
|
89
94
|
},
|
|
90
|
-
data:
|
|
95
|
+
data: this.context.data
|
|
91
96
|
};
|
|
92
97
|
return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
|
|
93
98
|
}
|
|
@@ -3,7 +3,6 @@ import { DuplicateJobError } from '../../modules/errors';
|
|
|
3
3
|
import { formatISODate, getTimeSeries } from '../../modules/utils';
|
|
4
4
|
import { Activity } from './activity';
|
|
5
5
|
import { CollatorService } from '../collator';
|
|
6
|
-
import { DimensionService } from '../dimension';
|
|
7
6
|
import { EngineService } from '../engine';
|
|
8
7
|
import { Pipe } from '../pipe';
|
|
9
8
|
import { ReporterService } from '../reporter';
|
|
@@ -99,7 +98,7 @@ class Trigger extends Activity {
|
|
|
99
98
|
|
|
100
99
|
const utc = formatISODate(new Date());
|
|
101
100
|
const { id, version } = await this.engine.getVID();
|
|
102
|
-
this.initDimensionalAddress(
|
|
101
|
+
this.initDimensionalAddress(CollatorService.getDimensionalSeed());
|
|
103
102
|
const activityMetadata = {
|
|
104
103
|
...this.metadata,
|
|
105
104
|
jid: jobId,
|
|
@@ -119,7 +118,7 @@ class Trigger extends Activity {
|
|
|
119
118
|
trc: this.context.metadata.trc,
|
|
120
119
|
spn: this.context.metadata.spn,
|
|
121
120
|
jid: jobId,
|
|
122
|
-
dad:
|
|
121
|
+
dad: CollatorService.getDimensionalSeed(), //top-level job implicitly uses `,0`
|
|
123
122
|
key: jobKey,
|
|
124
123
|
jc: utc,
|
|
125
124
|
ju: utc,
|
|
@@ -85,7 +85,7 @@ class CollatorService {
|
|
|
85
85
|
static async notarizeCompletion(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
86
86
|
//1) ALWAYS actualize leg2 dimension (+1)
|
|
87
87
|
//2) IF the activity is used in a cycle, don't close leg 2!
|
|
88
|
-
const decrement = activity.config.cycle ? 0 :
|
|
88
|
+
const decrement = activity.config.cycle ? 0 : 1_000_000_000_000;
|
|
89
89
|
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), multi);
|
|
90
90
|
};
|
|
91
91
|
|
|
@@ -234,6 +234,17 @@ class CollatorService {
|
|
|
234
234
|
static isActivityComplete(status: number): boolean {
|
|
235
235
|
return (status - 0) <= 0;
|
|
236
236
|
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* All activities exist on a dimensional plane. Zero
|
|
240
|
+
* is the default. A value of
|
|
241
|
+
* `AxY,0,0,0,0,1,0,0` would reflect that
|
|
242
|
+
* an ancestor activity was dimensionalized beyond
|
|
243
|
+
* the default.
|
|
244
|
+
*/
|
|
245
|
+
static getDimensionalSeed(index = 0): string {
|
|
246
|
+
return `,${index}`;
|
|
247
|
+
}
|
|
237
248
|
}
|
|
238
249
|
|
|
239
250
|
export { CollatorService };
|
|
@@ -1,4 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 1) `maxSystemRetries` | can be 0 to 3 and represents milliseconds;
|
|
3
|
+
* if there is an error, the workflow will retry up to `maxSystemRetries` times
|
|
4
|
+
* delaying by 10, 100, and 1000ms; this is a system level retry
|
|
5
|
+
* and is not configurable. It exists to handle intermittent network
|
|
6
|
+
* errors. (NOTE: each retry spawns a new transition stream)
|
|
7
|
+
*
|
|
8
|
+
* 2) `backoffExponent` | can be any number and represents `seconds` when applied;
|
|
9
|
+
* retries will happen indefinitely and adhere to the
|
|
10
|
+
* exponential backoff algorithm by multiplying by `backoffExponent`.
|
|
11
|
+
* For example, if `backoffExponent` is 10, the workflow will retry
|
|
12
|
+
* in 10s, 100s, 1000s, 10000s, etc.
|
|
13
|
+
*
|
|
14
|
+
* EXAMPLE | Using `maxSystemRetries = 3` and `backoffExponent = 10`, errant
|
|
15
|
+
* workflows will be retried on the following schedule (8 times in 27 hours):
|
|
16
|
+
* => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
|
|
17
|
+
*/
|
|
18
|
+
const getWorkflowYAML = (topic: string, version = '1', maxSystemRetries = 2, backoffExponent = 10) => {
|
|
2
19
|
return `app:
|
|
3
20
|
id: ${topic}
|
|
4
21
|
version: '${version}'
|
|
@@ -30,10 +47,20 @@ const getWorkflowYAML = (topic: string, version = '1') => {
|
|
|
30
47
|
a1:
|
|
31
48
|
type: activity
|
|
32
49
|
cycle: true
|
|
33
|
-
|
|
50
|
+
output:
|
|
51
|
+
schema:
|
|
52
|
+
type: object
|
|
53
|
+
properties:
|
|
54
|
+
duration:
|
|
55
|
+
type: number
|
|
56
|
+
maps:
|
|
57
|
+
duration: ${backoffExponent}
|
|
58
|
+
|
|
34
59
|
w1:
|
|
35
60
|
type: worker
|
|
36
61
|
topic: ${topic}
|
|
62
|
+
retry:
|
|
63
|
+
'599': [${maxSystemRetries}]
|
|
37
64
|
input:
|
|
38
65
|
schema:
|
|
39
66
|
type: object
|
|
@@ -55,18 +82,33 @@ const getWorkflowYAML = (topic: string, version = '1') => {
|
|
|
55
82
|
maps:
|
|
56
83
|
response: '{$self.output.data.response}'
|
|
57
84
|
|
|
85
|
+
a599:
|
|
86
|
+
title: Sleep before trying again
|
|
87
|
+
type: activity
|
|
88
|
+
sleep: "{a1.output.data.duration}"
|
|
89
|
+
|
|
58
90
|
c1:
|
|
91
|
+
title: Goto Activity a1
|
|
59
92
|
type: cycle
|
|
60
93
|
ancestor: a1
|
|
94
|
+
input:
|
|
95
|
+
maps:
|
|
96
|
+
duration:
|
|
97
|
+
'@pipe':
|
|
98
|
+
- ['{a1.output.data.duration}', ${backoffExponent}]
|
|
99
|
+
- ['{@math.multiply}']
|
|
100
|
+
|
|
61
101
|
transitions:
|
|
62
102
|
t1:
|
|
63
103
|
- to: a1
|
|
64
104
|
a1:
|
|
65
105
|
- to: w1
|
|
66
106
|
w1:
|
|
67
|
-
- to:
|
|
107
|
+
- to: a599
|
|
68
108
|
conditions:
|
|
69
|
-
code:
|
|
109
|
+
code: 599
|
|
110
|
+
a599:
|
|
111
|
+
- to: c1
|
|
70
112
|
`;
|
|
71
113
|
}
|
|
72
114
|
|
|
@@ -16,22 +16,37 @@ export class WorkflowHandleService {
|
|
|
16
16
|
let status = await this.hotMesh.getStatus(this.workflowId);
|
|
17
17
|
const topic = `${this.workflowTopic}.${this.workflowId}`;
|
|
18
18
|
|
|
19
|
-
if (status == 0) {
|
|
20
|
-
return (await this.hotMesh.getState(this.workflowTopic, this.workflowId)).data?.response;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
19
|
return new Promise((resolve, reject) => {
|
|
24
20
|
let isResolved = false;
|
|
25
21
|
//common fulfill/unsubscribe
|
|
26
|
-
const complete = async (response?: any) => {
|
|
22
|
+
const complete = async (response?: any, err?: string) => {
|
|
27
23
|
if (isResolved) return;
|
|
28
24
|
isResolved = true;
|
|
29
25
|
this.hotMesh.unsub(topic);
|
|
30
|
-
|
|
26
|
+
if (err) {
|
|
27
|
+
return reject(JSON.parse(err));
|
|
28
|
+
} else if (!response) {
|
|
29
|
+
const state = await this.hotMesh.getState(this.workflowTopic, this.workflowId);
|
|
30
|
+
if (!state.data && state.metadata.err) {
|
|
31
|
+
return reject(JSON.parse(state.metadata.err));
|
|
32
|
+
}
|
|
33
|
+
response = state.data?.response;
|
|
34
|
+
}
|
|
35
|
+
resolve(response);
|
|
31
36
|
};
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
//check for done
|
|
38
|
+
if (status == 0) {
|
|
39
|
+
return complete();
|
|
40
|
+
}
|
|
41
|
+
//subscribe to topic
|
|
42
|
+
this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
|
|
43
|
+
if (!state.data && state.metadata.err) {
|
|
44
|
+
await complete(null, state.metadata.err);
|
|
45
|
+
} else {
|
|
46
|
+
await complete(state.data?.response);
|
|
47
|
+
}
|
|
34
48
|
});
|
|
49
|
+
//resolve for race condition
|
|
35
50
|
setTimeout(async () => {
|
|
36
51
|
status = await this.hotMesh.getStatus(this.workflowId);
|
|
37
52
|
if (status == 0) {
|
|
@@ -2,8 +2,18 @@ import { asyncLocalStorage } from './asyncLocalStorage';
|
|
|
2
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
3
|
import { RedisClass, RedisOptions } from '../../types/redis';
|
|
4
4
|
import { StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
|
|
5
|
-
import {
|
|
5
|
+
import { ActivityWorkflowDataType,
|
|
6
|
+
Connection,
|
|
7
|
+
Registry,
|
|
8
|
+
WorkerConfig,
|
|
9
|
+
WorkerOptions,
|
|
10
|
+
WorkflowDataType } from "../../types/durable";
|
|
6
11
|
import { getWorkflowYAML, getActivityYAML } from './factory';
|
|
12
|
+
import {
|
|
13
|
+
DurableFatalError,
|
|
14
|
+
DurableMaxedError,
|
|
15
|
+
DurableRetryError,
|
|
16
|
+
DurableTimeoutError } from '../../modules/errors';
|
|
7
17
|
|
|
8
18
|
/*
|
|
9
19
|
Here is an example of how the methods in this file are used:
|
|
@@ -45,7 +55,7 @@ export class WorkerService {
|
|
|
45
55
|
workflowRunner: HotMesh;
|
|
46
56
|
activityRunner: HotMesh;
|
|
47
57
|
|
|
48
|
-
static getHotMesh = async (worflowTopic: string) => {
|
|
58
|
+
static getHotMesh = async (worflowTopic: string, options?: WorkerOptions) => {
|
|
49
59
|
if (WorkerService.instances.has(worflowTopic)) {
|
|
50
60
|
return await WorkerService.instances.get(worflowTopic);
|
|
51
61
|
}
|
|
@@ -54,17 +64,17 @@ export class WorkerService {
|
|
|
54
64
|
engine: { redis: { ...WorkerService.connection } }
|
|
55
65
|
});
|
|
56
66
|
WorkerService.instances.set(worflowTopic, hotMesh);
|
|
57
|
-
await WorkerService.activateWorkflow(await hotMesh, worflowTopic, getWorkflowYAML);
|
|
67
|
+
await WorkerService.activateWorkflow(await hotMesh, worflowTopic, getWorkflowYAML, options);
|
|
58
68
|
return hotMesh;
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
static async activateWorkflow(hotMesh: HotMesh, topic: string,
|
|
71
|
+
static async activateWorkflow(hotMesh: HotMesh, topic: string, dagFactory: Function, options: WorkerOptions = {}) {
|
|
62
72
|
const version = '1';
|
|
63
73
|
const app = await hotMesh.engine.store.getApp(topic);
|
|
64
74
|
const appVersion = app?.version;
|
|
65
75
|
if(!appVersion) {
|
|
66
76
|
try {
|
|
67
|
-
await hotMesh.deploy(
|
|
77
|
+
await hotMesh.deploy(dagFactory(topic, version, options.maxSystemRetries, options.backoffExponent));
|
|
68
78
|
await hotMesh.activate(version);
|
|
69
79
|
} catch (err) {
|
|
70
80
|
hotMesh.engine.logger.error('durable-worker-deploy-activate-err', err);
|
|
@@ -117,7 +127,7 @@ export class WorkerService {
|
|
|
117
127
|
worker.activityRunner = await worker.initActivityWorkflow(config, activityTopic);
|
|
118
128
|
await WorkerService.activateWorkflow(worker.activityRunner, activityTopic, getActivityYAML);
|
|
119
129
|
worker.workflowRunner = await worker.initWorkerWorkflow(config, workflowTopic, workflowFunction);
|
|
120
|
-
await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML);
|
|
130
|
+
await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML, config.options);
|
|
121
131
|
return worker;
|
|
122
132
|
}
|
|
123
133
|
|
|
@@ -160,7 +170,7 @@ export class WorkerService {
|
|
|
160
170
|
return async (data: StreamData): Promise<StreamDataResponse> => {
|
|
161
171
|
try {
|
|
162
172
|
//always run the activity function when instructed; return the response
|
|
163
|
-
const activityInput = data.data as unknown as
|
|
173
|
+
const activityInput = data.data as unknown as ActivityWorkflowDataType;
|
|
164
174
|
const activityName = activityInput.activityName;
|
|
165
175
|
const activityFunction = WorkerService.activityRegistry[activityName];
|
|
166
176
|
const pojoResponse = await activityFunction.apply(this, activityInput.arguments);
|
|
@@ -172,12 +182,16 @@ export class WorkerService {
|
|
|
172
182
|
};
|
|
173
183
|
} catch (err) {
|
|
174
184
|
this.activityRunner.engine.logger.error('durable-worker-activity-err', err);
|
|
185
|
+
if (!(err instanceof DurableTimeoutError) &&
|
|
186
|
+
!(err instanceof DurableMaxedError) &&
|
|
187
|
+
!(err instanceof DurableFatalError)) {
|
|
188
|
+
err = new DurableRetryError(err.message);
|
|
189
|
+
}
|
|
175
190
|
return {
|
|
176
191
|
status: StreamStatus.ERROR,
|
|
177
|
-
code:
|
|
178
|
-
message: err.message,
|
|
192
|
+
code: err.code,
|
|
179
193
|
metadata: { ...data.metadata },
|
|
180
|
-
data: {
|
|
194
|
+
data: { message: err.message }
|
|
181
195
|
} as StreamDataResponse;
|
|
182
196
|
}
|
|
183
197
|
}
|
|
@@ -257,11 +271,12 @@ export class WorkerService {
|
|
|
257
271
|
data: { response: workflowResponse }
|
|
258
272
|
};
|
|
259
273
|
} catch (err) {
|
|
274
|
+
// 59* - Durable*Error
|
|
260
275
|
return {
|
|
261
276
|
status: StreamStatus.ERROR,
|
|
262
|
-
code:
|
|
277
|
+
code: err.code || new DurableRetryError(err.message).code,
|
|
263
278
|
metadata: { ...data.metadata },
|
|
264
|
-
data: {
|
|
279
|
+
data: { message: err.message, type: err.name }
|
|
265
280
|
} as StreamDataResponse;
|
|
266
281
|
}
|
|
267
282
|
}
|
|
@@ -62,7 +62,7 @@ export class WorkflowService {
|
|
|
62
62
|
const client = new Client({
|
|
63
63
|
connection: await Connection.connect(WorkerService.connection),
|
|
64
64
|
});
|
|
65
|
-
//todo:
|
|
65
|
+
//todo: allow cross/app callback (pj:'@DURABLE@hello-world@<pjid>'/pa: <paid>/pd: <pdad>)
|
|
66
66
|
const handle = await client.workflow.start({
|
|
67
67
|
...options,
|
|
68
68
|
workflowId: `${workflowId}${options.workflowId}`, //concat
|
|
@@ -1,4 +1,78 @@
|
|
|
1
1
|
class MathHandler {
|
|
2
|
+
add(...operands: (number | number[])[]): number {
|
|
3
|
+
// @ts-ignore
|
|
4
|
+
return operands.reduce((a: number, b: number | number[]) => {
|
|
5
|
+
if (Array.isArray(b)) {
|
|
6
|
+
return a + this.add(...b);
|
|
7
|
+
} else {
|
|
8
|
+
return a + b;
|
|
9
|
+
}
|
|
10
|
+
}, 0);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
subtract(...operands: (number | number[])[]): number {
|
|
14
|
+
if (operands.length === 0) {
|
|
15
|
+
throw new Error('At least one operand is required.');
|
|
16
|
+
}
|
|
17
|
+
let flatOperands: number[] = [];
|
|
18
|
+
operands.forEach((op: number | number[]) => {
|
|
19
|
+
if (Array.isArray(op)) {
|
|
20
|
+
flatOperands = [...flatOperands, ...op];
|
|
21
|
+
} else {
|
|
22
|
+
flatOperands.push(op);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (flatOperands.length === 0) {
|
|
26
|
+
throw new Error('At least one operand is required after flattening.');
|
|
27
|
+
}
|
|
28
|
+
const result = flatOperands.reduce((a: number, b: number, i: number) => {
|
|
29
|
+
return i === 0 ? a : a - b;
|
|
30
|
+
});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
multiply(...operands: (number | number[])[]): number {
|
|
35
|
+
if (operands.length === 0) {
|
|
36
|
+
throw new Error('At least one operand is required.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// @ts-ignore
|
|
40
|
+
return operands.reduce((a: number, b: number | number[]) => {
|
|
41
|
+
if (Array.isArray(b)) {
|
|
42
|
+
return a * this.multiply(...b);
|
|
43
|
+
} else {
|
|
44
|
+
return a * b;
|
|
45
|
+
}
|
|
46
|
+
}, 1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
divide(...operands: (number | number[])[]): number {
|
|
50
|
+
if (operands.length === 0) {
|
|
51
|
+
throw new Error('At least one operand is required.');
|
|
52
|
+
}
|
|
53
|
+
let flatOperands: number[] = [];
|
|
54
|
+
operands.forEach((op: number | number[]) => {
|
|
55
|
+
if (Array.isArray(op)) {
|
|
56
|
+
flatOperands = [...flatOperands, ...op];
|
|
57
|
+
} else {
|
|
58
|
+
flatOperands.push(op);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
if (flatOperands.length === 0) {
|
|
62
|
+
throw new Error('At least one operand is required after flattening.');
|
|
63
|
+
}
|
|
64
|
+
const result = flatOperands.reduce((a: number, b: number, i: number) => {
|
|
65
|
+
if (b === 0) {
|
|
66
|
+
return NaN;
|
|
67
|
+
}
|
|
68
|
+
return i === 0 ? a : a / b;
|
|
69
|
+
});
|
|
70
|
+
if (isNaN(result)) {
|
|
71
|
+
return NaN;
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
|
|
2
76
|
abs(x: number): number {
|
|
3
77
|
return Math.abs(x);
|
|
4
78
|
}
|