@hotmeshio/hotmesh 0.0.48 → 0.0.50
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 +1 -1
- package/build/modules/enums.d.ts +1 -0
- package/build/modules/enums.js +2 -1
- package/build/modules/key.d.ts +5 -1
- package/build/modules/key.js +10 -2
- package/build/package.json +2 -1
- package/build/services/activities/await.js +6 -0
- package/build/services/activities/hook.js +1 -1
- package/build/services/activities/trigger.d.ts +1 -0
- package/build/services/activities/trigger.js +23 -2
- package/build/services/durable/exporter.js +19 -5
- package/build/services/durable/meshos.js +11 -6
- package/build/services/durable/search.d.ts +20 -1
- package/build/services/durable/search.js +73 -25
- package/build/services/durable/worker.js +10 -0
- package/build/services/durable/workflow.d.ts +1 -0
- package/build/services/durable/workflow.js +17 -1
- package/build/services/engine/index.d.ts +1 -1
- package/build/services/engine/index.js +12 -3
- package/build/services/exporter/index.js +3 -2
- package/build/services/hotmesh/index.js +4 -0
- package/build/services/quorum/index.d.ts +11 -2
- package/build/services/quorum/index.js +33 -0
- package/build/services/router/index.d.ts +15 -0
- package/build/services/router/index.js +55 -7
- package/build/services/serializer/index.js +1 -1
- package/build/services/store/clients/redis.js +2 -0
- package/build/services/store/index.d.ts +6 -4
- package/build/services/store/index.js +86 -21
- package/build/services/task/index.d.ts +2 -1
- package/build/services/task/index.js +30 -13
- package/build/services/worker/index.d.ts +13 -2
- package/build/services/worker/index.js +44 -3
- package/build/types/activity.d.ts +1 -0
- package/build/types/durable.d.ts +9 -0
- package/build/types/exporter.d.ts +2 -0
- package/build/types/job.d.ts +1 -0
- package/build/types/quorum.d.ts +22 -8
- package/build/types/stream.d.ts +1 -0
- package/modules/enums.ts +1 -0
- package/modules/key.ts +7 -2
- package/package.json +2 -1
- package/services/activities/await.ts +6 -0
- package/services/activities/hook.ts +1 -0
- package/services/activities/trigger.ts +25 -1
- package/services/durable/exporter.ts +18 -7
- package/services/durable/meshos.ts +10 -6
- package/services/durable/search.ts +73 -26
- package/services/durable/worker.ts +13 -1
- package/services/durable/workflow.ts +18 -0
- package/services/engine/index.ts +13 -5
- package/services/exporter/index.ts +3 -2
- package/services/hotmesh/index.ts +4 -0
- package/services/quorum/index.ts +38 -2
- package/services/router/index.ts +59 -9
- package/services/serializer/index.ts +1 -1
- package/services/store/clients/redis.ts +2 -0
- package/services/store/index.ts +108 -22
- package/services/task/index.ts +31 -11
- package/services/worker/index.ts +49 -5
- package/types/activity.ts +1 -0
- package/types/durable.ts +11 -0
- package/types/exporter.ts +2 -0
- package/types/job.ts +1 -0
- package/types/quorum.ts +28 -13
- package/types/stream.ts +1 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# HotMesh
|
|
2
2
|

|
|
3
3
|
|
|
4
|
-
HotMesh
|
|
4
|
+
HotMesh transforms Redis into a distributed orchestration engine.
|
|
5
5
|
|
|
6
6
|
*Write functions in your own preferred style, and let Redis govern their execution, reliably and durably.*
|
|
7
7
|
|
package/build/modules/enums.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare const HMSH_CODE_DURABLE_MAXED = 597;
|
|
|
15
15
|
export declare const HMSH_CODE_DURABLE_FATAL = 598;
|
|
16
16
|
export declare const HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
17
17
|
export declare const HMSH_STATUS_UNKNOWN = "unknown";
|
|
18
|
+
export declare const HMSH_QUORUM_ROLLCALL_CYCLES = 12;
|
|
18
19
|
export declare const HMSH_QUORUM_DELAY_MS = 250;
|
|
19
20
|
export declare const HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
20
21
|
export declare const HMSH_OTT_WAIT_TIME: number;
|
package/build/modules/enums.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
|
|
3
|
+
exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
|
|
4
4
|
// HOTMESH SYSTEM
|
|
5
5
|
exports.HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL || 'info';
|
|
6
6
|
// STATUS CODES AND MESSAGES
|
|
@@ -20,6 +20,7 @@ exports.HMSH_CODE_DURABLE_FATAL = 598;
|
|
|
20
20
|
exports.HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
21
21
|
exports.HMSH_STATUS_UNKNOWN = 'unknown';
|
|
22
22
|
// QUORUM
|
|
23
|
+
exports.HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
|
|
23
24
|
exports.HMSH_QUORUM_DELAY_MS = 250;
|
|
24
25
|
exports.HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
25
26
|
// ENGINE
|
package/build/modules/key.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
|
|
|
27
27
|
* hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
|
|
28
28
|
*/
|
|
29
29
|
declare const HMNS = "hmsh";
|
|
30
|
+
declare const KEYSEP = ":";
|
|
31
|
+
declare const VALSEP = "::";
|
|
32
|
+
declare const WEBSEP = "::";
|
|
33
|
+
declare const TYPSEP = "::";
|
|
30
34
|
declare class KeyService {
|
|
31
35
|
/**
|
|
32
36
|
* returns a key that can be used to access a value in the key/value store
|
|
@@ -41,4 +45,4 @@ declare class KeyService {
|
|
|
41
45
|
*/
|
|
42
46
|
static mintKey(namespace: string, keyType: KeyType, params: KeyStoreParams): string;
|
|
43
47
|
}
|
|
44
|
-
export { KeyService, KeyType, KeyStoreParams, HMNS };
|
|
48
|
+
export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
|
package/build/modules/key.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HMNS = exports.KeyType = exports.KeyService = void 0;
|
|
3
|
+
exports.VALSEP = exports.WEBSEP = exports.TYPSEP = exports.KEYSEP = exports.HMNS = exports.KeyType = exports.KeyService = void 0;
|
|
4
4
|
const hotmesh_1 = require("../types/hotmesh");
|
|
5
5
|
Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () { return hotmesh_1.KeyType; } });
|
|
6
6
|
/**
|
|
@@ -30,8 +30,16 @@ Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () {
|
|
|
30
30
|
* hmsh:<appid>:sym:keys:<activityid|$subscribes> -> {hash} list of symbols based upon schema enums (initially) and adaptively optimized (later) during runtime; if '$subscribes' is used as the activityid, it is a top-level `job` symbol set (for job keys)
|
|
31
31
|
* hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
|
|
32
32
|
*/
|
|
33
|
-
const HMNS = "hmsh";
|
|
33
|
+
const HMNS = "hmsh";
|
|
34
34
|
exports.HMNS = HMNS;
|
|
35
|
+
const KEYSEP = ':'; //default delimiter for keys
|
|
36
|
+
exports.KEYSEP = KEYSEP;
|
|
37
|
+
const VALSEP = '::'; //default delimiter for vals
|
|
38
|
+
exports.VALSEP = VALSEP;
|
|
39
|
+
const WEBSEP = '::'; //default delimiter for webhook vals
|
|
40
|
+
exports.WEBSEP = WEBSEP;
|
|
41
|
+
const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
|
|
42
|
+
exports.TYPSEP = TYPSEP;
|
|
35
43
|
class KeyService {
|
|
36
44
|
/**
|
|
37
45
|
* returns a key that can be used to access a value in the key/value store
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
29
29
|
"test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
30
30
|
"test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
|
+
"test:await": "NODE_ENV=test jest ./tests/functional/awaiter/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
32
|
"test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
32
33
|
"test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
33
34
|
"test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -78,6 +78,12 @@ class Await extends activity_1.Activity {
|
|
|
78
78
|
type: stream_1.StreamDataType.AWAIT,
|
|
79
79
|
data: this.context.data
|
|
80
80
|
};
|
|
81
|
+
if (this.config.await !== true) {
|
|
82
|
+
const doAwait = pipe_1.Pipe.resolve(this.config.await, this.context);
|
|
83
|
+
if (doAwait === false) {
|
|
84
|
+
streamData.metadata.await = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
81
87
|
if (this.config.retry) {
|
|
82
88
|
streamData.policies = {
|
|
83
89
|
retry: this.config.retry
|
|
@@ -107,7 +107,7 @@ class Hook extends activity_1.Activity {
|
|
|
107
107
|
}
|
|
108
108
|
else if (this.config.sleep) {
|
|
109
109
|
const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
|
|
110
|
-
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration);
|
|
110
|
+
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '');
|
|
111
111
|
return this.context.metadata.jid;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -8,6 +8,7 @@ declare class Trigger extends Activity {
|
|
|
8
8
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
9
9
|
process(): Promise<string>;
|
|
10
10
|
setStatus(amount: number): Promise<void>;
|
|
11
|
+
execAdjacentParent(): Promise<void>;
|
|
11
12
|
createInputContext(): Partial<JobState>;
|
|
12
13
|
getState(): Promise<void>;
|
|
13
14
|
bindJobMetadataPaths(): string[];
|
|
@@ -31,6 +31,9 @@ class Trigger extends activity_1.Activity {
|
|
|
31
31
|
await this.setStats(multi);
|
|
32
32
|
await this.registerJobDependency(multi);
|
|
33
33
|
await multi.exec();
|
|
34
|
+
//if the parent (spawner) chose not to await,
|
|
35
|
+
// emit the job_id as the data payload { job_id }
|
|
36
|
+
this.execAdjacentParent();
|
|
34
37
|
telemetry.mapActivityAttributes();
|
|
35
38
|
const jobStatus = Number(this.context.metadata.js);
|
|
36
39
|
telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
|
|
@@ -61,6 +64,11 @@ class Trigger extends activity_1.Activity {
|
|
|
61
64
|
async setStatus(amount) {
|
|
62
65
|
this.context.metadata.js = amount;
|
|
63
66
|
}
|
|
67
|
+
async execAdjacentParent() {
|
|
68
|
+
if (this.context.metadata.px) {
|
|
69
|
+
await this.engine.execAdjacentParent(this.context, { metadata: this.context.metadata, data: { job_id: this.context.metadata.jid } });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
64
72
|
createInputContext() {
|
|
65
73
|
const input = {
|
|
66
74
|
[this.metadata.aid]: {
|
|
@@ -95,6 +103,7 @@ class Trigger extends activity_1.Activity {
|
|
|
95
103
|
pg: this.context.metadata.pg,
|
|
96
104
|
pd: this.context.metadata.pd,
|
|
97
105
|
pa: this.context.metadata.pa,
|
|
106
|
+
px: this.context.metadata.px,
|
|
98
107
|
app: id,
|
|
99
108
|
vrs: version,
|
|
100
109
|
tpc: this.config.subscribes,
|
|
@@ -165,10 +174,22 @@ class Trigger extends activity_1.Activity {
|
|
|
165
174
|
}
|
|
166
175
|
if (resolvedDepKey) {
|
|
167
176
|
const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
|
|
168
|
-
|
|
177
|
+
let type;
|
|
178
|
+
if (isParentOrigin) {
|
|
179
|
+
if (this.context.metadata.px) {
|
|
180
|
+
type = 'child';
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
type = 'expire-child';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
type = 'expire';
|
|
188
|
+
}
|
|
189
|
+
await this.store.registerJobDependency(type, resolvedDepKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
|
|
169
190
|
}
|
|
170
191
|
if (resolvedAdjKey && resolvedAdjKey !== resolvedDepKey) {
|
|
171
|
-
await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, multi);
|
|
192
|
+
await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
|
|
172
193
|
}
|
|
173
194
|
}
|
|
174
195
|
async setStats(multi) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ExporterService = void 0;
|
|
4
4
|
const serializer_1 = require("../serializer");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
|
+
const key_1 = require("../../modules/key");
|
|
6
7
|
/**
|
|
7
8
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
8
9
|
* Splits, Inflates, and Sorts the job data for use in durable contexts
|
|
@@ -94,13 +95,27 @@ class ExporterService {
|
|
|
94
95
|
const activityName = item[1].split('/')[0];
|
|
95
96
|
const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
|
|
96
97
|
const timestamp = item[2];
|
|
97
|
-
|
|
98
|
+
let event = {
|
|
98
99
|
activity: activityName,
|
|
99
100
|
duplex: duplex,
|
|
100
101
|
dimension: dimensions,
|
|
101
102
|
timestamp,
|
|
103
|
+
created: timestamp,
|
|
104
|
+
updated: timestamp,
|
|
102
105
|
};
|
|
103
|
-
timeline.
|
|
106
|
+
const prior = timeline[timeline.length - 1];
|
|
107
|
+
if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
|
|
108
|
+
if (event.duplex === 'exit') {
|
|
109
|
+
prior.updated = event.timestamp;
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
prior.created = event.timestamp;
|
|
113
|
+
}
|
|
114
|
+
event = prior;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
timeline.push(event);
|
|
118
|
+
}
|
|
104
119
|
if (this.isMainEntry(item[1])) {
|
|
105
120
|
event.actions = [];
|
|
106
121
|
this.interleaveActions(actions.main, event.actions);
|
|
@@ -163,12 +178,11 @@ class ExporterService {
|
|
|
163
178
|
* @returns - the organized dependency data
|
|
164
179
|
*/
|
|
165
180
|
inflateDependencyData(data, actions) {
|
|
166
|
-
//console.log('dependency data>', data);
|
|
167
181
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
168
182
|
const flowReg = /-(\d+)$/;
|
|
169
183
|
return data.map((dependency, index) => {
|
|
170
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
171
|
-
const jobId = jid.join(
|
|
184
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
|
|
185
|
+
const jobId = jid.join(key_1.VALSEP);
|
|
172
186
|
const match = jobId.match(hookReg);
|
|
173
187
|
let prefix;
|
|
174
188
|
let type;
|
|
@@ -187,13 +187,18 @@ class MeshOSService {
|
|
|
187
187
|
}
|
|
188
188
|
else {
|
|
189
189
|
//limit which hash fields to return
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
args.push('RETURN');
|
|
191
|
+
args.push(((options.return?.length ?? 0) + 1).toString());
|
|
192
|
+
args.push('$');
|
|
193
|
+
options.return?.forEach(returnField => {
|
|
194
|
+
if (returnField.startsWith('"')) {
|
|
195
|
+
//allow literal values to be requested
|
|
196
|
+
args.push(returnField.slice(1, -1));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
194
199
|
args.push(`_${returnField}`);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
197
202
|
//paginate
|
|
198
203
|
if (options.limit) {
|
|
199
204
|
args.push('LIMIT', options.limit.start.toString(), options.limit.size.toString());
|
|
@@ -30,10 +30,29 @@ export declare class Search {
|
|
|
30
30
|
* calling any method that produces side effects (changes the value)
|
|
31
31
|
*/
|
|
32
32
|
getSearchSessionGuid(): string;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Sets the fields listed in args. Returns the
|
|
35
|
+
* count of new fields that were set (does not
|
|
36
|
+
* count fields that were updated)
|
|
37
|
+
*/
|
|
38
|
+
set(...args: string[]): Promise<number>;
|
|
34
39
|
get(key: string): Promise<string>;
|
|
35
40
|
mget(...args: string[]): Promise<string[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Deletes the fields listed in args. Returns the
|
|
43
|
+
* count of fields that were deleted.
|
|
44
|
+
*/
|
|
36
45
|
del(...args: string[]): Promise<number | void>;
|
|
46
|
+
/**
|
|
47
|
+
* Increments the value of a field by the given amount. Returns the
|
|
48
|
+
* new value of the field after the increment. Can be
|
|
49
|
+
* used to decrement the value of a field by specifying a negative.
|
|
50
|
+
*/
|
|
37
51
|
incr(key: string, val: number): Promise<number>;
|
|
52
|
+
/**
|
|
53
|
+
* Multiplies the value of a field by the given amount. Returns the
|
|
54
|
+
* new value of the field after the multiplication. NOTE:
|
|
55
|
+
* this is exponential multiplication.
|
|
56
|
+
*/
|
|
38
57
|
mult(key: string, val: number): Promise<number>;
|
|
39
58
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Search = void 0;
|
|
4
4
|
const key_1 = require("../../modules/key");
|
|
5
|
+
const storage_1 = require("../../modules/storage");
|
|
5
6
|
class Search {
|
|
6
7
|
constructor(workflowId, hotMeshClient, searchSessionId) {
|
|
7
8
|
this.searchSessionIndex = 0;
|
|
@@ -57,9 +58,15 @@ class Search {
|
|
|
57
58
|
* @returns {Promise<string[]>} - the list of search indexes
|
|
58
59
|
*/
|
|
59
60
|
static async listSearchIndexes(hotMeshClient) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
try {
|
|
62
|
+
const store = hotMeshClient.engine.store;
|
|
63
|
+
const searchIndexes = await store.exec('FT._LIST');
|
|
64
|
+
return searchIndexes;
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
hotMeshClient.engine.logger.info('durable-client-search-list-err', { err });
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
63
70
|
}
|
|
64
71
|
/**
|
|
65
72
|
* increments the index to return a unique search session guid when
|
|
@@ -69,18 +76,28 @@ class Search {
|
|
|
69
76
|
//return the search session as it would exist in the search session index
|
|
70
77
|
return `${this.searchSessionId}-${this.searchSessionIndex++}-`;
|
|
71
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* Sets the fields listed in args. Returns the
|
|
81
|
+
* count of new fields that were set (does not
|
|
82
|
+
* count fields that were updated)
|
|
83
|
+
*/
|
|
72
84
|
async set(...args) {
|
|
73
85
|
const ssGuid = this.getSearchSessionGuid();
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const key = this.safeKey(args[i]);
|
|
79
|
-
const value = args[i + 1].toString();
|
|
80
|
-
safeArgs.push(key, value);
|
|
81
|
-
}
|
|
82
|
-
await this.store.exec('HSET', this.jobId, ...safeArgs);
|
|
86
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
87
|
+
const replay = store?.get('replay') ?? {};
|
|
88
|
+
if (ssGuid in replay) {
|
|
89
|
+
return Number(replay[ssGuid]);
|
|
83
90
|
}
|
|
91
|
+
const safeArgs = [];
|
|
92
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
93
|
+
const key = this.safeKey(args[i]);
|
|
94
|
+
const value = args[i + 1].toString();
|
|
95
|
+
safeArgs.push(key, value);
|
|
96
|
+
}
|
|
97
|
+
const fieldCount = await this.store.exec('HSET', this.jobId, ...safeArgs);
|
|
98
|
+
//no need to wait; set this interim value in the replay
|
|
99
|
+
this.store.exec('HSET', this.jobId, ssGuid, fieldCount.toString());
|
|
100
|
+
return Number(fieldCount);
|
|
84
101
|
}
|
|
85
102
|
async get(key) {
|
|
86
103
|
try {
|
|
@@ -104,32 +121,63 @@ class Search {
|
|
|
104
121
|
return [];
|
|
105
122
|
}
|
|
106
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Deletes the fields listed in args. Returns the
|
|
126
|
+
* count of fields that were deleted.
|
|
127
|
+
*/
|
|
107
128
|
async del(...args) {
|
|
108
129
|
const ssGuid = this.getSearchSessionGuid();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
130
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
131
|
+
const replay = store?.get('replay') ?? {};
|
|
132
|
+
if (ssGuid in replay) {
|
|
133
|
+
return Number(replay[ssGuid]);
|
|
134
|
+
}
|
|
135
|
+
const safeArgs = [];
|
|
136
|
+
for (let i = 0; i < args.length; i++) {
|
|
137
|
+
safeArgs.push(this.safeKey(args[i]));
|
|
117
138
|
}
|
|
139
|
+
const response = await this.store.exec('HDEL', this.jobId, ...safeArgs);
|
|
140
|
+
const formattedResponse = isNaN(response) ? 0 : Number(response);
|
|
141
|
+
//no need to wait; set this interim value in the replay
|
|
142
|
+
this.store.exec('HSET', this.jobId, ssGuid, formattedResponse.toString());
|
|
143
|
+
return formattedResponse;
|
|
118
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Increments the value of a field by the given amount. Returns the
|
|
147
|
+
* new value of the field after the increment. Can be
|
|
148
|
+
* used to decrement the value of a field by specifying a negative.
|
|
149
|
+
*/
|
|
119
150
|
async incr(key, val) {
|
|
120
151
|
const ssGuid = this.getSearchSessionGuid();
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
153
|
+
const replay = store?.get('replay') ?? {};
|
|
154
|
+
if (ssGuid in replay) {
|
|
155
|
+
return Number(replay[ssGuid]);
|
|
124
156
|
}
|
|
157
|
+
const num = await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), val.toString());
|
|
158
|
+
//no need to wait; set this interim value in the replay
|
|
159
|
+
this.store.exec('HSET', this.jobId, ssGuid, num.toString());
|
|
160
|
+
return Number(num);
|
|
125
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Multiplies the value of a field by the given amount. Returns the
|
|
164
|
+
* new value of the field after the multiplication. NOTE:
|
|
165
|
+
* this is exponential multiplication.
|
|
166
|
+
*/
|
|
126
167
|
async mult(key, val) {
|
|
127
168
|
const ssGuid = this.getSearchSessionGuid();
|
|
169
|
+
const store = storage_1.asyncLocalStorage.getStore();
|
|
170
|
+
const replay = store?.get('replay') ?? {};
|
|
171
|
+
if (ssGuid in replay) {
|
|
172
|
+
return Math.exp(Number(replay[ssGuid]));
|
|
173
|
+
}
|
|
128
174
|
const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1'));
|
|
129
175
|
if (ssGuidValue === 1) {
|
|
130
176
|
const log = Math.log(val);
|
|
131
|
-
const logTotal =
|
|
132
|
-
|
|
177
|
+
const logTotal = await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), log.toString());
|
|
178
|
+
//no need to wait; set this interim value in the replay
|
|
179
|
+
this.store.exec('HSET', this.jobId, ssGuid, logTotal.toString());
|
|
180
|
+
return Math.exp(Number(logTotal));
|
|
133
181
|
}
|
|
134
182
|
}
|
|
135
183
|
}
|
|
@@ -164,16 +164,26 @@ class WorkerService {
|
|
|
164
164
|
// garbage collect (expire) this job when originJobId is expired
|
|
165
165
|
context.set('originJobId', workflowInput.originJobId);
|
|
166
166
|
}
|
|
167
|
+
let replayQuery = '';
|
|
167
168
|
if (workflowInput.workflowDimension) {
|
|
168
169
|
//every hook function runs in an isolated dimension controlled
|
|
169
170
|
//by the index assigned when the signal was received; even if the
|
|
170
171
|
//hook function re-runs, its scope will always remain constant
|
|
171
172
|
context.set('workflowDimension', workflowInput.workflowDimension);
|
|
173
|
+
replayQuery = `-*${workflowInput.workflowDimension}-*`;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
//last letter of words like 'hook', 'sleep', 'wait', 'signal', 'search', 'start'
|
|
177
|
+
replayQuery = '-*[ehklpt]-*';
|
|
172
178
|
}
|
|
173
179
|
context.set('workflowTopic', workflowTopic);
|
|
174
180
|
context.set('workflowName', workflowTopic.split('-').pop());
|
|
175
181
|
context.set('workflowTrace', data.metadata.trc);
|
|
176
182
|
context.set('workflowSpan', data.metadata.spn);
|
|
183
|
+
const store = this.workflowRunner.engine.store;
|
|
184
|
+
const [cursor, replay] = await store.findJobFields(workflowInput.workflowId, replayQuery, 50000, 5000);
|
|
185
|
+
context.set('replay', replay);
|
|
186
|
+
context.set('cursor', cursor); // if != 0, more remain
|
|
177
187
|
const workflowResponse = await storage_1.asyncLocalStorage.run(context, async () => {
|
|
178
188
|
return await workflowFunction.apply(this, workflowInput.arguments);
|
|
179
189
|
});
|
|
@@ -76,6 +76,10 @@ class WorkflowService {
|
|
|
76
76
|
const COUNTER = store.get('counter');
|
|
77
77
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
78
78
|
const sessionId = `-start${workflowDimension}-${execIndex}-`;
|
|
79
|
+
const replay = store.get('replay');
|
|
80
|
+
if (sessionId in replay) {
|
|
81
|
+
return replay[sessionId];
|
|
82
|
+
}
|
|
79
83
|
//NOTE: this is the hash prefix; necessary for the search index to locate the entity
|
|
80
84
|
const entityOrEmptyString = options.entity ?? '';
|
|
81
85
|
//If the workflowId is not provided, it is generated from the entity and the workflow name
|
|
@@ -172,6 +176,8 @@ class WorkflowService {
|
|
|
172
176
|
static getContext() {
|
|
173
177
|
const store = storage_1.asyncLocalStorage.getStore();
|
|
174
178
|
const workflowId = store.get('workflowId');
|
|
179
|
+
const replay = store.get('replay');
|
|
180
|
+
const cursor = store.get('cursor');
|
|
175
181
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
176
182
|
const workflowTopic = store.get('workflowTopic');
|
|
177
183
|
const namespace = store.get('namespace');
|
|
@@ -180,7 +186,9 @@ class WorkflowService {
|
|
|
180
186
|
const COUNTER = store.get('counter');
|
|
181
187
|
return {
|
|
182
188
|
counter: COUNTER.counter,
|
|
189
|
+
cursor,
|
|
183
190
|
namespace,
|
|
191
|
+
replay,
|
|
184
192
|
workflowId,
|
|
185
193
|
workflowDimension,
|
|
186
194
|
workflowTopic,
|
|
@@ -201,6 +209,10 @@ class WorkflowService {
|
|
|
201
209
|
const COUNTER = store.get('counter');
|
|
202
210
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
203
211
|
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
212
|
+
const replay = store.get('replay');
|
|
213
|
+
if (sessionId in replay) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
204
216
|
const keyParams = {
|
|
205
217
|
appId: hotMeshClient.appId,
|
|
206
218
|
jobId: workflowId
|
|
@@ -274,6 +286,7 @@ class WorkflowService {
|
|
|
274
286
|
workflowTopic: store.get('workflowTopic'),
|
|
275
287
|
workflowDimension: store.get('workflowDimension') ?? '',
|
|
276
288
|
counter: store.get('counter'),
|
|
289
|
+
replay: store.get('replay'),
|
|
277
290
|
};
|
|
278
291
|
}
|
|
279
292
|
/**
|
|
@@ -284,9 +297,12 @@ class WorkflowService {
|
|
|
284
297
|
* @template T - the result type
|
|
285
298
|
*/
|
|
286
299
|
static async once(fn, ...args) {
|
|
287
|
-
const { workflowId, namespace, workflowTopic, workflowDimension, counter: COUNTER, } = WorkflowService.getLocalState();
|
|
300
|
+
const { workflowId, namespace, workflowTopic, workflowDimension, counter: COUNTER, replay, } = WorkflowService.getLocalState();
|
|
288
301
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
289
302
|
const sessionId = `-once${workflowDimension}-${execIndex}-`;
|
|
303
|
+
if (sessionId in replay) {
|
|
304
|
+
return JSON.parse(replay[sessionId]);
|
|
305
|
+
}
|
|
290
306
|
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
291
307
|
const keyParams = {
|
|
292
308
|
appId: hotMeshClient.appId,
|
|
@@ -65,7 +65,7 @@ declare class EngineService {
|
|
|
65
65
|
resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
|
|
66
66
|
processStreamMessage(streamData: StreamDataResponse): Promise<void>;
|
|
67
67
|
execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<string>;
|
|
68
|
-
hasParentJob(context: JobState): boolean;
|
|
68
|
+
hasParentJob(context: JobState, checkSevered?: boolean): boolean;
|
|
69
69
|
resolveError(metadata: JobMetadata): StreamError | undefined;
|
|
70
70
|
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
|
|
71
71
|
scrub(jobId: string): Promise<void>;
|
|
@@ -259,6 +259,7 @@ class EngineService {
|
|
|
259
259
|
pg: streamData.metadata.gid,
|
|
260
260
|
pd: streamData.metadata.dad,
|
|
261
261
|
pa: streamData.metadata.aid,
|
|
262
|
+
px: streamData.metadata.await === false,
|
|
262
263
|
trc: streamData.metadata.trc,
|
|
263
264
|
spn: streamData.metadata.spn,
|
|
264
265
|
};
|
|
@@ -316,7 +317,10 @@ class EngineService {
|
|
|
316
317
|
return (await this.router?.publishMessage(null, streamData));
|
|
317
318
|
}
|
|
318
319
|
}
|
|
319
|
-
hasParentJob(context) {
|
|
320
|
+
hasParentJob(context, checkSevered = false) {
|
|
321
|
+
if (checkSevered) {
|
|
322
|
+
return Boolean(context.metadata.pj && context.metadata.pa && !context.metadata.px);
|
|
323
|
+
}
|
|
320
324
|
return Boolean(context.metadata.pj && context.metadata.pa);
|
|
321
325
|
}
|
|
322
326
|
resolveError(metadata) {
|
|
@@ -385,7 +389,12 @@ class EngineService {
|
|
|
385
389
|
const workItems = await reporter.getWorkItems(resolvedQuery, queryFacets);
|
|
386
390
|
if (workItems.length) {
|
|
387
391
|
const taskService = new task_1.TaskService(this.store, this.logger);
|
|
388
|
-
await taskService.enqueueWorkItems(workItems.map(workItem =>
|
|
392
|
+
await taskService.enqueueWorkItems(workItems.map(workItem => [
|
|
393
|
+
hookTopic,
|
|
394
|
+
workItem,
|
|
395
|
+
keyResolver.scrub || false,
|
|
396
|
+
JSON.stringify(data)
|
|
397
|
+
].join(key_1.VALSEP)));
|
|
389
398
|
this.store.publish(key_1.KeyType.QUORUM, { type: 'work', originator: this.guid }, this.appId);
|
|
390
399
|
}
|
|
391
400
|
return workItems;
|
|
@@ -504,7 +513,7 @@ class EngineService {
|
|
|
504
513
|
// ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
|
|
505
514
|
async runJobCompletionTasks(context, options = {}) {
|
|
506
515
|
//'emit' indicates the job is still active
|
|
507
|
-
const isAwait = this.hasParentJob(context);
|
|
516
|
+
const isAwait = this.hasParentJob(context, true);
|
|
508
517
|
const isOneTimeSub = this.hasOneTimeSubscription(context);
|
|
509
518
|
const topic = await this.getPublishesTopic(context);
|
|
510
519
|
let msgId;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ExporterService = void 0;
|
|
4
4
|
const serializer_1 = require("../serializer");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
|
+
const key_1 = require("../../modules/key");
|
|
6
7
|
/**
|
|
7
8
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
8
9
|
* Expands process data and includes dependency list
|
|
@@ -87,8 +88,8 @@ class ExporterService {
|
|
|
87
88
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
88
89
|
const flowReg = /-(\d+)$/;
|
|
89
90
|
return data.map((dependency, index) => {
|
|
90
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
91
|
-
const jobId = jid.join(
|
|
91
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
|
|
92
|
+
const jobId = jid.join(key_1.VALSEP);
|
|
92
93
|
const match = jobId.match(hookReg);
|
|
93
94
|
let prefix;
|
|
94
95
|
let type;
|
|
@@ -167,6 +167,10 @@ class HotMeshService {
|
|
|
167
167
|
}
|
|
168
168
|
stop() {
|
|
169
169
|
this.engine?.taskService.cancelCleanup();
|
|
170
|
+
this.quorum?.stop();
|
|
171
|
+
this.workers?.forEach((worker) => {
|
|
172
|
+
worker.stop();
|
|
173
|
+
});
|
|
170
174
|
}
|
|
171
175
|
async compress(terms) {
|
|
172
176
|
return await this.engine?.compress(terms);
|