@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
|
@@ -12,6 +12,7 @@ const redis_2 = require("../stream/clients/redis");
|
|
|
12
12
|
const ioredis_3 = require("../sub/clients/ioredis");
|
|
13
13
|
const redis_3 = require("../sub/clients/redis");
|
|
14
14
|
const stream_1 = require("../../types/stream");
|
|
15
|
+
const enums_1 = require("../../modules/enums");
|
|
15
16
|
class WorkerService {
|
|
16
17
|
constructor() {
|
|
17
18
|
this.reporting = false;
|
|
@@ -26,6 +27,7 @@ class WorkerService {
|
|
|
26
27
|
service.namespace = namespace;
|
|
27
28
|
service.appId = appId;
|
|
28
29
|
service.guid = guid;
|
|
30
|
+
service.callback = worker.callback;
|
|
29
31
|
service.topic = worker.topic;
|
|
30
32
|
service.config = config;
|
|
31
33
|
service.logger = logger;
|
|
@@ -95,14 +97,51 @@ class WorkerService {
|
|
|
95
97
|
return async (topic, message) => {
|
|
96
98
|
self.logger.debug('worker-event-received', { topic, type: message.type });
|
|
97
99
|
if (message.type === 'throttle') {
|
|
98
|
-
|
|
100
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
101
|
+
self.throttle(message.throttle);
|
|
102
|
+
}
|
|
99
103
|
}
|
|
100
104
|
else if (message.type === 'ping') {
|
|
101
105
|
self.sayPong(self.appId, self.guid, message.originator, message.details);
|
|
102
106
|
}
|
|
107
|
+
else if (message.type === 'rollcall') {
|
|
108
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
109
|
+
self.doRollCall(message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
103
112
|
};
|
|
104
113
|
}
|
|
105
|
-
|
|
114
|
+
/**
|
|
115
|
+
* A quorum-wide command to broadcaset system details.
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
118
|
+
async doRollCall(message) {
|
|
119
|
+
let iteration = 0;
|
|
120
|
+
let max = !isNaN(message.max) ? message.max : enums_1.HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
121
|
+
if (this.rollCallInterval)
|
|
122
|
+
clearTimeout(this.rollCallInterval);
|
|
123
|
+
const base = (message.interval / 2);
|
|
124
|
+
const amount = base + Math.ceil(Math.random() * base);
|
|
125
|
+
do {
|
|
126
|
+
await (0, utils_1.sleepFor)(Math.ceil(Math.random() * 1000));
|
|
127
|
+
await this.sayPong(this.appId, this.guid, null, true, message.signature);
|
|
128
|
+
if (!message.interval)
|
|
129
|
+
return;
|
|
130
|
+
const { promise, timerId } = (0, utils_1.XSleepFor)(amount * 1000);
|
|
131
|
+
this.rollCallInterval = timerId;
|
|
132
|
+
await promise;
|
|
133
|
+
} while (this.rollCallInterval && iteration++ < max - 1);
|
|
134
|
+
}
|
|
135
|
+
cancelRollCall() {
|
|
136
|
+
if (this.rollCallInterval) {
|
|
137
|
+
clearTimeout(this.rollCallInterval);
|
|
138
|
+
delete this.rollCallInterval;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
stop() {
|
|
142
|
+
this.cancelRollCall();
|
|
143
|
+
}
|
|
144
|
+
async sayPong(appId, guid, originator, details = false, signature = false) {
|
|
106
145
|
let profile;
|
|
107
146
|
if (details) {
|
|
108
147
|
const params = {
|
|
@@ -122,11 +161,13 @@ class WorkerService {
|
|
|
122
161
|
reclaimDelay: this.router.reclaimDelay,
|
|
123
162
|
reclaimCount: this.router.reclaimCount,
|
|
124
163
|
system: await (0, utils_1.getSystemHealth)(),
|
|
164
|
+
signature: signature ? this.callback.toString() : undefined,
|
|
125
165
|
};
|
|
126
166
|
}
|
|
127
167
|
this.store.publish(key_1.KeyType.QUORUM, {
|
|
128
168
|
type: 'pong',
|
|
129
|
-
guid,
|
|
169
|
+
guid,
|
|
170
|
+
originator,
|
|
130
171
|
profile,
|
|
131
172
|
}, appId);
|
|
132
173
|
}
|
package/build/types/durable.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LogLevel } from './logger';
|
|
2
2
|
import { RedisClass, RedisOptions } from './redis';
|
|
3
|
+
import { StringStringType } from './serializer';
|
|
3
4
|
type WorkflowConfig = {
|
|
4
5
|
backoffCoefficient?: number;
|
|
5
6
|
maximumAttempts?: number;
|
|
@@ -11,6 +12,14 @@ type WorkflowContext = {
|
|
|
11
12
|
* the reentrant semaphore, incremented in real-time as idempotent statements are re-traversed upon reentry. Indicates the current semaphore count.
|
|
12
13
|
*/
|
|
13
14
|
counter: number;
|
|
15
|
+
/**
|
|
16
|
+
* number as string for the replay cursor
|
|
17
|
+
*/
|
|
18
|
+
cursor: string;
|
|
19
|
+
/**
|
|
20
|
+
* the replay hash of name/value pairs representing prior executions
|
|
21
|
+
*/
|
|
22
|
+
replay: StringStringType;
|
|
14
23
|
/**
|
|
15
24
|
* the HotMesh App namespace. `durable` is the default.
|
|
16
25
|
*/
|
package/build/types/job.d.ts
CHANGED
package/build/types/quorum.d.ts
CHANGED
|
@@ -45,42 +45,56 @@ export interface QuorumProfile {
|
|
|
45
45
|
reclaimDelay?: number;
|
|
46
46
|
reclaimCount?: number;
|
|
47
47
|
system?: SystemHealth;
|
|
48
|
+
signature?: string;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
interface QuorumMessageBase {
|
|
51
|
+
guid?: string;
|
|
52
|
+
topic?: string;
|
|
53
|
+
type?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface PingMessage extends QuorumMessageBase {
|
|
50
56
|
type: 'ping';
|
|
51
57
|
originator: string;
|
|
52
58
|
details?: boolean;
|
|
53
59
|
}
|
|
54
|
-
export interface WorkMessage {
|
|
60
|
+
export interface WorkMessage extends QuorumMessageBase {
|
|
55
61
|
type: 'work';
|
|
56
62
|
originator: string;
|
|
57
63
|
}
|
|
58
|
-
export interface CronMessage {
|
|
64
|
+
export interface CronMessage extends QuorumMessageBase {
|
|
59
65
|
type: 'cron';
|
|
60
66
|
originator: string;
|
|
61
67
|
}
|
|
62
|
-
export interface PongMessage {
|
|
68
|
+
export interface PongMessage extends QuorumMessageBase {
|
|
63
69
|
type: 'pong';
|
|
64
70
|
guid: string;
|
|
65
71
|
originator: string;
|
|
66
72
|
profile?: QuorumProfile;
|
|
67
73
|
}
|
|
68
|
-
export interface ActivateMessage {
|
|
74
|
+
export interface ActivateMessage extends QuorumMessageBase {
|
|
69
75
|
type: 'activate';
|
|
70
76
|
cache_mode: 'nocache' | 'cache';
|
|
71
77
|
until_version: string;
|
|
72
78
|
}
|
|
73
|
-
export interface JobMessage {
|
|
79
|
+
export interface JobMessage extends QuorumMessageBase {
|
|
74
80
|
type: 'job';
|
|
75
81
|
topic: string;
|
|
76
82
|
job: JobOutput;
|
|
77
83
|
}
|
|
78
|
-
export interface ThrottleMessage {
|
|
84
|
+
export interface ThrottleMessage extends QuorumMessageBase {
|
|
79
85
|
type: 'throttle';
|
|
80
86
|
guid?: string;
|
|
81
87
|
topic?: string;
|
|
82
88
|
throttle: number;
|
|
83
89
|
}
|
|
90
|
+
export interface RollCallMessage extends QuorumMessageBase {
|
|
91
|
+
type: 'rollcall';
|
|
92
|
+
guid?: string;
|
|
93
|
+
topic?: string | null;
|
|
94
|
+
interval: number;
|
|
95
|
+
max?: number;
|
|
96
|
+
signature?: boolean;
|
|
97
|
+
}
|
|
84
98
|
export interface JobMessageCallback {
|
|
85
99
|
(topic: string, message: JobOutput): void;
|
|
86
100
|
}
|
|
@@ -96,5 +110,5 @@ export interface QuorumMessageCallback {
|
|
|
96
110
|
* These messages serve to coordinate the cache invalidation and switch-over
|
|
97
111
|
* to the new version without any downtime and a coordinating parent server.
|
|
98
112
|
*/
|
|
99
|
-
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
|
|
113
|
+
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | RollCallMessage | CronMessage;
|
|
100
114
|
export {};
|
package/build/types/stream.d.ts
CHANGED
package/modules/enums.ts
CHANGED
|
@@ -23,6 +23,7 @@ export const HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
|
23
23
|
export const HMSH_STATUS_UNKNOWN = 'unknown';
|
|
24
24
|
|
|
25
25
|
// QUORUM
|
|
26
|
+
export const HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
|
|
26
27
|
export const HMSH_QUORUM_DELAY_MS = 250;
|
|
27
28
|
export const HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
28
29
|
|
package/modules/key.ts
CHANGED
|
@@ -28,7 +28,12 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
|
|
|
28
28
|
* hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
const HMNS = "hmsh";
|
|
31
|
+
const HMNS = "hmsh";
|
|
32
|
+
|
|
33
|
+
const KEYSEP = ':'; //default delimiter for keys
|
|
34
|
+
const VALSEP = '::'; //default delimiter for vals
|
|
35
|
+
const WEBSEP = '::'; //default delimiter for webhook vals
|
|
36
|
+
const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
|
|
32
37
|
|
|
33
38
|
class KeyService {
|
|
34
39
|
|
|
@@ -93,4 +98,4 @@ class KeyService {
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
export { KeyService, KeyType, KeyStoreParams, HMNS };
|
|
101
|
+
export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
|
package/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",
|
|
@@ -95,6 +95,12 @@ class Await extends Activity {
|
|
|
95
95
|
type: StreamDataType.AWAIT,
|
|
96
96
|
data: this.context.data
|
|
97
97
|
};
|
|
98
|
+
if (this.config.await !== true) {
|
|
99
|
+
const doAwait = Pipe.resolve(this.config.await, this.context);
|
|
100
|
+
if (doAwait === false) {
|
|
101
|
+
streamData.metadata.await = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
98
104
|
if (this.config.retry) {
|
|
99
105
|
streamData.policies = {
|
|
100
106
|
retry: this.config.retry
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { JobState } from '../../types/job';
|
|
16
16
|
import { RedisMulti } from '../../types/redis';
|
|
17
17
|
import { StringScalarType } from '../../types/serializer';
|
|
18
|
+
import { WorkListTaskType } from '../../types/task';
|
|
18
19
|
|
|
19
20
|
class Trigger extends Activity {
|
|
20
21
|
config: TriggerActivity;
|
|
@@ -50,6 +51,10 @@ class Trigger extends Activity {
|
|
|
50
51
|
await this.registerJobDependency(multi);
|
|
51
52
|
await multi.exec();
|
|
52
53
|
|
|
54
|
+
//if the parent (spawner) chose not to await,
|
|
55
|
+
// emit the job_id as the data payload { job_id }
|
|
56
|
+
this.execAdjacentParent();
|
|
57
|
+
|
|
53
58
|
telemetry.mapActivityAttributes();
|
|
54
59
|
const jobStatus = Number(this.context.metadata.js);
|
|
55
60
|
telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
|
|
@@ -79,6 +84,12 @@ class Trigger extends Activity {
|
|
|
79
84
|
this.context.metadata.js = amount;
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
async execAdjacentParent() {
|
|
88
|
+
if (this.context.metadata.px) {
|
|
89
|
+
await this.engine.execAdjacentParent(this.context, {metadata: this.context.metadata, data: { job_id: this.context.metadata.jid }});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
createInputContext(): Partial<JobState> {
|
|
83
94
|
const input = {
|
|
84
95
|
[this.metadata.aid]: {
|
|
@@ -115,6 +126,7 @@ class Trigger extends Activity {
|
|
|
115
126
|
pg: this.context.metadata.pg,
|
|
116
127
|
pd: this.context.metadata.pd,
|
|
117
128
|
pa: this.context.metadata.pa,
|
|
129
|
+
px: this.context.metadata.px,
|
|
118
130
|
app: id,
|
|
119
131
|
vrs: version,
|
|
120
132
|
tpc: this.config.subscribes,
|
|
@@ -193,12 +205,23 @@ class Trigger extends Activity {
|
|
|
193
205
|
}
|
|
194
206
|
if (resolvedDepKey) {
|
|
195
207
|
const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
|
|
208
|
+
let type: WorkListTaskType;
|
|
209
|
+
if (isParentOrigin) {
|
|
210
|
+
if (this.context.metadata.px) {
|
|
211
|
+
type = 'child'
|
|
212
|
+
} else {
|
|
213
|
+
type = 'expire-child'
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
type = 'expire';
|
|
217
|
+
}
|
|
196
218
|
await this.store.registerJobDependency(
|
|
197
|
-
|
|
219
|
+
type,
|
|
198
220
|
resolvedDepKey,
|
|
199
221
|
this.context.metadata.tpc,
|
|
200
222
|
this.context.metadata.jid,
|
|
201
223
|
this.context.metadata.gid,
|
|
224
|
+
this.context.metadata.pd,
|
|
202
225
|
multi,
|
|
203
226
|
);
|
|
204
227
|
}
|
|
@@ -209,6 +232,7 @@ class Trigger extends Activity {
|
|
|
209
232
|
this.context.metadata.tpc,
|
|
210
233
|
this.context.metadata.jid,
|
|
211
234
|
this.context.metadata.gid,
|
|
235
|
+
this.context.metadata.pd,
|
|
212
236
|
multi,
|
|
213
237
|
);
|
|
214
238
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
JobTimeline } from '../../types/exporter';
|
|
14
14
|
import { SerializerService } from '../serializer';
|
|
15
15
|
import { restoreHierarchy } from '../../modules/utils';
|
|
16
|
+
import { VALSEP } from '../../modules/key';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
@@ -116,19 +117,31 @@ class ExporterService {
|
|
|
116
117
|
const activityName = item[1].split('/')[0];
|
|
117
118
|
const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
|
|
118
119
|
const timestamp = item[2];
|
|
119
|
-
|
|
120
|
+
let event: JobTimeline = {
|
|
120
121
|
activity: activityName,
|
|
121
122
|
duplex: duplex as 'entry' | 'exit',
|
|
122
123
|
dimension: dimensions,
|
|
123
124
|
timestamp,
|
|
125
|
+
created: timestamp,
|
|
126
|
+
updated: timestamp,
|
|
124
127
|
};
|
|
125
|
-
timeline.
|
|
128
|
+
const prior = timeline[timeline.length - 1];
|
|
129
|
+
if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
|
|
130
|
+
if (event.duplex === 'exit') {
|
|
131
|
+
prior.updated = event.timestamp;
|
|
132
|
+
} else {
|
|
133
|
+
prior.created = event.timestamp;
|
|
134
|
+
}
|
|
135
|
+
event = prior;
|
|
136
|
+
} else {
|
|
137
|
+
timeline.push(event);
|
|
138
|
+
}
|
|
126
139
|
|
|
127
140
|
if (this.isMainEntry(item[1])) {
|
|
128
141
|
event.actions = [] as ActivityAction[];
|
|
129
142
|
this.interleaveActions(actions.main, event.actions);
|
|
130
143
|
} else if (this.isHookEntry(item[1])) {
|
|
131
|
-
const hookDimension =
|
|
144
|
+
const hookDimension = `/${parts[1]}/${parts[2]}`;
|
|
132
145
|
const hookActions = actions.hooks[hookDimension];
|
|
133
146
|
event.actions = [] as ActivityAction[];
|
|
134
147
|
this.interleaveActions(hookActions, event.actions);
|
|
@@ -191,17 +204,15 @@ class ExporterService {
|
|
|
191
204
|
* @returns - the organized dependency data
|
|
192
205
|
*/
|
|
193
206
|
inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
|
|
194
|
-
//console.log('dependency data>', data);
|
|
195
207
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
196
208
|
const flowReg = /-(\d+)$/;
|
|
197
209
|
return data.map((dependency, index: number): DependencyExport => {
|
|
198
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
199
|
-
const jobId = jid.join(
|
|
210
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
|
|
211
|
+
const jobId = jid.join(VALSEP);
|
|
200
212
|
const match = jobId.match(hookReg);
|
|
201
213
|
let prefix: string;
|
|
202
214
|
let type: 'hook' | 'flow' | 'other';
|
|
203
215
|
let dimensionKey: string = '';
|
|
204
|
-
|
|
205
216
|
if (match) {
|
|
206
217
|
//hook-originating dependency
|
|
207
218
|
const [_, dimension, counter] = match;
|
|
@@ -288,13 +288,17 @@ export class MeshOSService {
|
|
|
288
288
|
args.push('LIMIT', '0', '0');
|
|
289
289
|
} else {
|
|
290
290
|
//limit which hash fields to return
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
291
|
+
args.push('RETURN');
|
|
292
|
+
args.push(((options.return?.length ?? 0) + 1).toString());
|
|
293
|
+
args.push('$');
|
|
294
|
+
options.return?.forEach(returnField => {
|
|
295
|
+
if (returnField.startsWith('"')) {
|
|
296
|
+
//allow literal values to be requested
|
|
297
|
+
args.push(returnField.slice(1, -1));
|
|
298
|
+
} else {
|
|
295
299
|
args.push(`_${returnField}`);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
298
302
|
//paginate
|
|
299
303
|
if (options.limit) {
|
|
300
304
|
args.push('LIMIT', options.limit.start.toString(), options.limit.size.toString());
|
|
@@ -3,6 +3,7 @@ import { RedisClient, RedisMulti } from '../../types/redis';
|
|
|
3
3
|
import { StoreService } from '../store';
|
|
4
4
|
import { KeyService, KeyType } from '../../modules/key';
|
|
5
5
|
import { WorkflowSearchOptions } from '../../types/durable';
|
|
6
|
+
import { asyncLocalStorage } from '../../modules/storage';
|
|
6
7
|
|
|
7
8
|
export class Search {
|
|
8
9
|
jobId: string;
|
|
@@ -66,9 +67,14 @@ export class Search {
|
|
|
66
67
|
* @returns {Promise<string[]>} - the list of search indexes
|
|
67
68
|
*/
|
|
68
69
|
static async listSearchIndexes(hotMeshClient: HotMesh): Promise<string[]> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
try {
|
|
71
|
+
const store = hotMeshClient.engine.store;
|
|
72
|
+
const searchIndexes = await store.exec('FT._LIST');
|
|
73
|
+
return searchIndexes as string[];
|
|
74
|
+
} catch (err) {
|
|
75
|
+
hotMeshClient.engine.logger.info('durable-client-search-list-err', { err });
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
/**
|
|
@@ -80,18 +86,28 @@ export class Search {
|
|
|
80
86
|
return `${this.searchSessionId}-${this.searchSessionIndex++}-`;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Sets the fields listed in args. Returns the
|
|
91
|
+
* count of new fields that were set (does not
|
|
92
|
+
* count fields that were updated)
|
|
93
|
+
*/
|
|
94
|
+
async set(...args: string[]): Promise<number> {
|
|
84
95
|
const ssGuid = this.getSearchSessionGuid();
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const key = this.safeKey(args[i]);
|
|
90
|
-
const value = args[i+1].toString();
|
|
91
|
-
safeArgs.push(key, value);
|
|
92
|
-
}
|
|
93
|
-
await this.store.exec('HSET', this.jobId, ...safeArgs);
|
|
96
|
+
const store = asyncLocalStorage.getStore();
|
|
97
|
+
const replay = store?.get('replay') ?? {};
|
|
98
|
+
if (ssGuid in replay) {
|
|
99
|
+
return Number(replay[ssGuid]);
|
|
94
100
|
}
|
|
101
|
+
const safeArgs: string[] = [];
|
|
102
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
103
|
+
const key = this.safeKey(args[i]);
|
|
104
|
+
const value = args[i+1].toString();
|
|
105
|
+
safeArgs.push(key, value);
|
|
106
|
+
}
|
|
107
|
+
const fieldCount = await this.store.exec('HSET', this.jobId, ...safeArgs);
|
|
108
|
+
//no need to wait; set this interim value in the replay
|
|
109
|
+
this.store.exec('HSET', this.jobId, ssGuid, fieldCount.toString());
|
|
110
|
+
return Number(fieldCount);
|
|
95
111
|
}
|
|
96
112
|
|
|
97
113
|
async get(key: string): Promise<string> {
|
|
@@ -116,34 +132,65 @@ export class Search {
|
|
|
116
132
|
}
|
|
117
133
|
}
|
|
118
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Deletes the fields listed in args. Returns the
|
|
137
|
+
* count of fields that were deleted.
|
|
138
|
+
*/
|
|
119
139
|
async del(...args: string[]): Promise<number | void> {
|
|
120
140
|
const ssGuid = this.getSearchSessionGuid();
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
141
|
+
const store = asyncLocalStorage.getStore();
|
|
142
|
+
const replay = store?.get('replay') ?? {};
|
|
143
|
+
if (ssGuid in replay) {
|
|
144
|
+
return Number(replay[ssGuid]);
|
|
145
|
+
}
|
|
146
|
+
const safeArgs: string[] = [];
|
|
147
|
+
for (let i = 0; i < args.length; i++) {
|
|
148
|
+
safeArgs.push(this.safeKey(args[i]));
|
|
129
149
|
}
|
|
150
|
+
const response = await this.store.exec('HDEL', this.jobId, ...safeArgs);
|
|
151
|
+
const formattedResponse = isNaN(response as unknown as number) ? 0 : Number(response);
|
|
152
|
+
//no need to wait; set this interim value in the replay
|
|
153
|
+
this.store.exec('HSET', this.jobId, ssGuid, formattedResponse.toString());
|
|
154
|
+
return formattedResponse;
|
|
130
155
|
}
|
|
131
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Increments the value of a field by the given amount. Returns the
|
|
159
|
+
* new value of the field after the increment. Can be
|
|
160
|
+
* used to decrement the value of a field by specifying a negative.
|
|
161
|
+
*/
|
|
132
162
|
async incr(key: string, val: number): Promise<number> {
|
|
133
163
|
const ssGuid = this.getSearchSessionGuid();
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
const store = asyncLocalStorage.getStore();
|
|
165
|
+
const replay = store?.get('replay') ?? {};
|
|
166
|
+
if (ssGuid in replay) {
|
|
167
|
+
return Number(replay[ssGuid]);
|
|
137
168
|
}
|
|
169
|
+
const num = await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), val.toString()) as string;
|
|
170
|
+
//no need to wait; set this interim value in the replay
|
|
171
|
+
this.store.exec('HSET', this.jobId, ssGuid, num.toString());
|
|
172
|
+
return Number(num);
|
|
138
173
|
}
|
|
139
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Multiplies the value of a field by the given amount. Returns the
|
|
177
|
+
* new value of the field after the multiplication. NOTE:
|
|
178
|
+
* this is exponential multiplication.
|
|
179
|
+
*/
|
|
140
180
|
async mult(key: string, val: number): Promise<number> {
|
|
141
181
|
const ssGuid = this.getSearchSessionGuid();
|
|
182
|
+
const store = asyncLocalStorage.getStore();
|
|
183
|
+
const replay = store?.get('replay') ?? {};
|
|
184
|
+
if (ssGuid in replay) {
|
|
185
|
+
return Math.exp(Number(replay[ssGuid]));
|
|
186
|
+
}
|
|
142
187
|
const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1') as string);
|
|
143
188
|
if (ssGuidValue === 1) {
|
|
144
189
|
const log = Math.log(val);
|
|
145
|
-
const logTotal =
|
|
146
|
-
|
|
190
|
+
const logTotal = await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), log.toString()) as string;
|
|
191
|
+
//no need to wait; set this interim value in the replay
|
|
192
|
+
this.store.exec('HSET', this.jobId, ssGuid, logTotal.toString());
|
|
193
|
+
return Math.exp(Number(logTotal));
|
|
147
194
|
}
|
|
148
195
|
}
|
|
149
196
|
}
|
|
@@ -211,16 +211,29 @@ export class WorkerService {
|
|
|
211
211
|
// garbage collect (expire) this job when originJobId is expired
|
|
212
212
|
context.set('originJobId', workflowInput.originJobId);
|
|
213
213
|
}
|
|
214
|
+
let replayQuery = '';
|
|
214
215
|
if (workflowInput.workflowDimension) {
|
|
215
216
|
//every hook function runs in an isolated dimension controlled
|
|
216
217
|
//by the index assigned when the signal was received; even if the
|
|
217
218
|
//hook function re-runs, its scope will always remain constant
|
|
218
219
|
context.set('workflowDimension', workflowInput.workflowDimension);
|
|
220
|
+
replayQuery = `-*${workflowInput.workflowDimension}-*`;
|
|
221
|
+
} else {
|
|
222
|
+
//last letter of words like 'hook', 'sleep', 'wait', 'signal', 'search', 'start'
|
|
223
|
+
replayQuery = '-*[ehklpt]-*';
|
|
219
224
|
}
|
|
220
225
|
context.set('workflowTopic', workflowTopic);
|
|
221
226
|
context.set('workflowName', workflowTopic.split('-').pop());
|
|
222
227
|
context.set('workflowTrace', data.metadata.trc);
|
|
223
228
|
context.set('workflowSpan', data.metadata.spn);
|
|
229
|
+
const store = this.workflowRunner.engine.store;
|
|
230
|
+
const [cursor, replay] = await store.findJobFields(
|
|
231
|
+
workflowInput.workflowId,
|
|
232
|
+
replayQuery,
|
|
233
|
+
50_000,
|
|
234
|
+
5_000,);
|
|
235
|
+
context.set('replay', replay);
|
|
236
|
+
context.set('cursor', cursor); // if != 0, more remain
|
|
224
237
|
const workflowResponse = await asyncLocalStorage.run(context, async () => {
|
|
225
238
|
return await workflowFunction.apply(this, workflowInput.arguments);
|
|
226
239
|
});
|
|
@@ -232,7 +245,6 @@ export class WorkerService {
|
|
|
232
245
|
data: { response: workflowResponse, done: true }
|
|
233
246
|
};
|
|
234
247
|
} catch (err) {
|
|
235
|
-
|
|
236
248
|
//not an error...just a trigger to sleep
|
|
237
249
|
if (err instanceof DurableSleepForError) {
|
|
238
250
|
return {
|