@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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { JobInterruptOptions, JobOutput, JobState } from '../../types/job';
|
|
22
22
|
import { StreamStatus } from '../../types/stream';
|
|
23
23
|
import { deterministicRandom } from '../../modules/utils';
|
|
24
|
+
import { StringStringType } from '../../types';
|
|
24
25
|
|
|
25
26
|
export class WorkflowService {
|
|
26
27
|
|
|
@@ -92,6 +93,10 @@ export class WorkflowService {
|
|
|
92
93
|
const COUNTER = store.get('counter');
|
|
93
94
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
94
95
|
const sessionId = `-start${workflowDimension}-${execIndex}-`;
|
|
96
|
+
const replay = store.get('replay') as StringStringType;
|
|
97
|
+
if (sessionId in replay) {
|
|
98
|
+
return replay[sessionId];
|
|
99
|
+
}
|
|
95
100
|
//NOTE: this is the hash prefix; necessary for the search index to locate the entity
|
|
96
101
|
const entityOrEmptyString = options.entity ?? '';
|
|
97
102
|
//If the workflowId is not provided, it is generated from the entity and the workflow name
|
|
@@ -193,6 +198,8 @@ export class WorkflowService {
|
|
|
193
198
|
static getContext(): WorkflowContext {
|
|
194
199
|
const store = asyncLocalStorage.getStore();
|
|
195
200
|
const workflowId = store.get('workflowId');
|
|
201
|
+
const replay = store.get('replay');
|
|
202
|
+
const cursor = store.get('cursor');
|
|
196
203
|
const workflowDimension = store.get('workflowDimension') ?? '';
|
|
197
204
|
const workflowTopic = store.get('workflowTopic');
|
|
198
205
|
const namespace = store.get('namespace');
|
|
@@ -201,7 +208,9 @@ export class WorkflowService {
|
|
|
201
208
|
const COUNTER = store.get('counter');
|
|
202
209
|
return {
|
|
203
210
|
counter: COUNTER.counter,
|
|
211
|
+
cursor,
|
|
204
212
|
namespace,
|
|
213
|
+
replay,
|
|
205
214
|
workflowId,
|
|
206
215
|
workflowDimension,
|
|
207
216
|
workflowTopic,
|
|
@@ -223,6 +232,10 @@ export class WorkflowService {
|
|
|
223
232
|
const COUNTER = store.get('counter');
|
|
224
233
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
225
234
|
const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
|
|
235
|
+
const replay = store.get('replay') as StringStringType;
|
|
236
|
+
if (sessionId in replay) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
226
239
|
const keyParams = {
|
|
227
240
|
appId: hotMeshClient.appId,
|
|
228
241
|
jobId: workflowId
|
|
@@ -300,6 +313,7 @@ export class WorkflowService {
|
|
|
300
313
|
workflowTopic: store.get('workflowTopic'),
|
|
301
314
|
workflowDimension: store.get('workflowDimension') ?? '',
|
|
302
315
|
counter: store.get('counter'),
|
|
316
|
+
replay: store.get('replay'),
|
|
303
317
|
}
|
|
304
318
|
}
|
|
305
319
|
|
|
@@ -317,9 +331,13 @@ export class WorkflowService {
|
|
|
317
331
|
workflowTopic,
|
|
318
332
|
workflowDimension,
|
|
319
333
|
counter: COUNTER,
|
|
334
|
+
replay,
|
|
320
335
|
} = WorkflowService.getLocalState();
|
|
321
336
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
322
337
|
const sessionId = `-once${workflowDimension}-${execIndex}-`;
|
|
338
|
+
if (sessionId in replay) {
|
|
339
|
+
return JSON.parse(replay[sessionId]);
|
|
340
|
+
}
|
|
323
341
|
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
324
342
|
const keyParams = {
|
|
325
343
|
appId: hotMeshClient.appId,
|
package/services/engine/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyType } from '../../modules/key';
|
|
1
|
+
import { KeyType, VALSEP } from '../../modules/key';
|
|
2
2
|
import {
|
|
3
3
|
HMSH_OTT_WAIT_TIME,
|
|
4
4
|
HMSH_CODE_SUCCESS,
|
|
@@ -386,9 +386,10 @@ class EngineService {
|
|
|
386
386
|
pg: streamData.metadata.gid,
|
|
387
387
|
pd: streamData.metadata.dad,
|
|
388
388
|
pa: streamData.metadata.aid,
|
|
389
|
+
px: streamData.metadata.await === false, //sever the parent connection (px)
|
|
389
390
|
trc: streamData.metadata.trc,
|
|
390
391
|
spn: streamData.metadata.spn,
|
|
391
|
-
|
|
392
|
+
};
|
|
392
393
|
const activityHandler = await this.initActivity(
|
|
393
394
|
streamData.metadata.topic,
|
|
394
395
|
streamData.data,
|
|
@@ -459,7 +460,10 @@ class EngineService {
|
|
|
459
460
|
return (await this.router?.publishMessage(null, streamData)) as string;
|
|
460
461
|
}
|
|
461
462
|
}
|
|
462
|
-
hasParentJob(context: JobState): boolean {
|
|
463
|
+
hasParentJob(context: JobState, checkSevered = false): boolean {
|
|
464
|
+
if (checkSevered) {
|
|
465
|
+
return Boolean(context.metadata.pj && context.metadata.pa && !context.metadata.px);
|
|
466
|
+
}
|
|
463
467
|
return Boolean(context.metadata.pj && context.metadata.pa);
|
|
464
468
|
}
|
|
465
469
|
resolveError(metadata: JobMetadata): StreamError | undefined {
|
|
@@ -537,7 +541,11 @@ class EngineService {
|
|
|
537
541
|
const taskService = new TaskService(this.store, this.logger);
|
|
538
542
|
await taskService.enqueueWorkItems(
|
|
539
543
|
workItems.map(
|
|
540
|
-
workItem =>
|
|
544
|
+
workItem => [
|
|
545
|
+
hookTopic,
|
|
546
|
+
workItem,
|
|
547
|
+
keyResolver.scrub || false,
|
|
548
|
+
JSON.stringify(data)].join(VALSEP)
|
|
541
549
|
));
|
|
542
550
|
this.store.publish(
|
|
543
551
|
KeyType.QUORUM,
|
|
@@ -663,7 +671,7 @@ class EngineService {
|
|
|
663
671
|
// ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
|
|
664
672
|
async runJobCompletionTasks(context: JobState, options: JobCompletionOptions = {}): Promise<string | void> {
|
|
665
673
|
//'emit' indicates the job is still active
|
|
666
|
-
const isAwait = this.hasParentJob(context);
|
|
674
|
+
const isAwait = this.hasParentJob(context, true);
|
|
667
675
|
const isOneTimeSub = this.hasOneTimeSubscription(context);
|
|
668
676
|
const topic = await this.getPublishesTopic(context);
|
|
669
677
|
let msgId: string;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
JobExport } from '../../types/exporter';
|
|
13
13
|
import { SerializerService } from '../serializer';
|
|
14
14
|
import { restoreHierarchy } from '../../modules/utils';
|
|
15
|
+
import { VALSEP } from '../../modules/key';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
@@ -108,8 +109,8 @@ class ExporterService {
|
|
|
108
109
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
109
110
|
const flowReg = /-(\d+)$/;
|
|
110
111
|
return data.map((dependency, index: number): DependencyExport => {
|
|
111
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
112
|
-
const jobId = jid.join(
|
|
112
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
|
|
113
|
+
const jobId = jid.join(VALSEP);
|
|
113
114
|
const match = jobId.match(hookReg);
|
|
114
115
|
let prefix: string;
|
|
115
116
|
let type: 'hook' | 'flow' | 'other';
|
|
@@ -222,6 +222,10 @@ class HotMeshService {
|
|
|
222
222
|
|
|
223
223
|
stop() {
|
|
224
224
|
this.engine?.taskService.cancelCleanup();
|
|
225
|
+
this.quorum?.stop();
|
|
226
|
+
this.workers?.forEach((worker: WorkerService) => {
|
|
227
|
+
worker.stop();
|
|
228
|
+
});
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
async compress(terms: string[]): Promise<boolean> {
|
package/services/quorum/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
HMSH_ACTIVATION_MAX_RETRY,
|
|
3
|
-
HMSH_QUORUM_DELAY_MS
|
|
3
|
+
HMSH_QUORUM_DELAY_MS,
|
|
4
|
+
HMSH_QUORUM_ROLLCALL_CYCLES} from '../../modules/enums';
|
|
4
5
|
import {
|
|
6
|
+
XSleepFor,
|
|
5
7
|
formatISODate,
|
|
6
8
|
getSystemHealth,
|
|
7
9
|
identifyRedisType,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
QuorumMessage,
|
|
23
25
|
QuorumMessageCallback,
|
|
24
26
|
QuorumProfile,
|
|
27
|
+
RollCallMessage,
|
|
25
28
|
SubscriptionCallback,
|
|
26
29
|
ThrottleMessage
|
|
27
30
|
} from '../../types/quorum';
|
|
@@ -41,6 +44,7 @@ class QuorumService {
|
|
|
41
44
|
untilVersion: string | null = null;
|
|
42
45
|
quorum: number | null = null;
|
|
43
46
|
callbacks: QuorumMessageCallback[] = [];
|
|
47
|
+
rollCallInterval: NodeJS.Timeout;
|
|
44
48
|
|
|
45
49
|
static async init(
|
|
46
50
|
namespace: string,
|
|
@@ -136,6 +140,8 @@ class QuorumService {
|
|
|
136
140
|
self.engine.routeToSubscribers(message.topic, message.job)
|
|
137
141
|
} else if (message.type === 'cron') {
|
|
138
142
|
self.engine.processTimeHooks();
|
|
143
|
+
} else if (message.type === 'rollcall') {
|
|
144
|
+
self.doRollCall(message);
|
|
139
145
|
}
|
|
140
146
|
//if there are any callbacks, call them
|
|
141
147
|
if (self.callbacks.length > 0) {
|
|
@@ -193,10 +199,40 @@ class QuorumService {
|
|
|
193
199
|
return quorum;
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
/**
|
|
203
|
+
* A quorum-wide command to broadcaset system details.
|
|
204
|
+
*
|
|
205
|
+
*/
|
|
206
|
+
async doRollCall(message: RollCallMessage) {
|
|
207
|
+
let iteration = 0;
|
|
208
|
+
let max = !isNaN(message.max) ? message.max : HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
209
|
+
if (this.rollCallInterval) clearTimeout(this.rollCallInterval);
|
|
210
|
+
const base = (message.interval / 2);
|
|
211
|
+
const amount = base + Math.ceil(Math.random() * base);
|
|
212
|
+
do {
|
|
213
|
+
await sleepFor(Math.ceil(Math.random() * 1000));
|
|
214
|
+
await this.sayPong(this.appId, this.guid, null, true);
|
|
215
|
+
if (!message.interval) return;
|
|
216
|
+
const { promise, timerId } = XSleepFor(amount * 1000);
|
|
217
|
+
this.rollCallInterval = timerId;
|
|
218
|
+
await promise;
|
|
219
|
+
} while (this.rollCallInterval && iteration++ < max - 1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
cancelRollCall() {
|
|
223
|
+
if (this.rollCallInterval) {
|
|
224
|
+
clearTimeout(this.rollCallInterval);
|
|
225
|
+
delete this.rollCallInterval;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
stop() {
|
|
230
|
+
this.cancelRollCall();
|
|
231
|
+
}
|
|
196
232
|
|
|
197
233
|
// ************* PUB/SUB METHODS *************
|
|
198
234
|
//publish a message to the quorum
|
|
199
|
-
async pub(quorumMessage:
|
|
235
|
+
async pub(quorumMessage: QuorumMessage) {
|
|
200
236
|
return await this.store.publish(KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
|
|
201
237
|
}
|
|
202
238
|
//subscribe user to quorum messages
|
package/services/router/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
HMSH_XCLAIM_DELAY_MS,
|
|
11
11
|
HMSH_XPENDING_COUNT } from '../../modules/enums';
|
|
12
12
|
import { KeyType } from '../../modules/key';
|
|
13
|
-
import {
|
|
13
|
+
import { guid, sleepFor } from '../../modules/utils';
|
|
14
14
|
import { ILogger } from '../logger';
|
|
15
15
|
import { StoreService } from '../store';
|
|
16
16
|
import { StreamService } from '../stream';
|
|
@@ -43,6 +43,10 @@ class Router {
|
|
|
43
43
|
counts: { [key: string]: number } = {};
|
|
44
44
|
currentTimerId: NodeJS.Timeout | null = null;
|
|
45
45
|
shouldConsume: boolean;
|
|
46
|
+
sleepPromiseResolve: (() => void) | null = null;
|
|
47
|
+
innerPromiseResolve: (() => void) | null = null;
|
|
48
|
+
isSleeping: boolean = false;
|
|
49
|
+
sleepTimout: NodeJS.Timeout | null = null;
|
|
46
50
|
|
|
47
51
|
constructor(config: StreamConfig, stream: StreamService<RedisClient, RedisMulti>, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
|
|
48
52
|
this.appId = config.appId;
|
|
@@ -54,6 +58,14 @@ class Router {
|
|
|
54
58
|
this.reclaimDelay = config.reclaimDelay || HMSH_XCLAIM_DELAY_MS;
|
|
55
59
|
this.reclaimCount = config.reclaimCount || HMSH_XCLAIM_COUNT;
|
|
56
60
|
this.logger = logger;
|
|
61
|
+
this.resetThrottleState();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private resetThrottleState() {
|
|
65
|
+
this.sleepPromiseResolve = null;
|
|
66
|
+
this.innerPromiseResolve = null;
|
|
67
|
+
this.isSleeping = false;
|
|
68
|
+
this.sleepTimout = null;
|
|
57
69
|
}
|
|
58
70
|
|
|
59
71
|
async createGroup(stream: string, group: string) {
|
|
@@ -72,6 +84,36 @@ class Router {
|
|
|
72
84
|
return await this.store.xadd(stream, '*', 'message', JSON.stringify(streamData), multi);
|
|
73
85
|
}
|
|
74
86
|
|
|
87
|
+
/**
|
|
88
|
+
* An adjustable throttle that will interrupt a sleeping
|
|
89
|
+
* router if the throttle is reduced and the sleep time
|
|
90
|
+
* has elapsed. If the throttle is increased, or if
|
|
91
|
+
* the sleep time has not elapsed, the router will continue
|
|
92
|
+
* to sleep until the new termination point. This
|
|
93
|
+
* allows for dynamic, elastic throttling with smooth
|
|
94
|
+
* acceleration and deceleration.
|
|
95
|
+
*/
|
|
96
|
+
public async customSleep(): Promise<void> {
|
|
97
|
+
if (this.throttle === 0) return;
|
|
98
|
+
if (this.isSleeping) return;
|
|
99
|
+
this.isSleeping = true;
|
|
100
|
+
let startTime = Date.now(); //anchor the origin
|
|
101
|
+
|
|
102
|
+
await new Promise<void>(async (outerResolve) => {
|
|
103
|
+
this.sleepPromiseResolve = outerResolve;
|
|
104
|
+
let elapsedTime = Date.now() - startTime;
|
|
105
|
+
while (elapsedTime < this.throttle) {
|
|
106
|
+
await new Promise<void>((innerResolve) => {
|
|
107
|
+
this.innerPromiseResolve = innerResolve;
|
|
108
|
+
this.sleepTimout = setTimeout(innerResolve, this.throttle - elapsedTime);
|
|
109
|
+
});
|
|
110
|
+
elapsedTime = Date.now() - startTime;
|
|
111
|
+
}
|
|
112
|
+
this.resetThrottleState();
|
|
113
|
+
outerResolve();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
75
117
|
async consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse|void>): Promise<void> {
|
|
76
118
|
this.logger.info(`stream-consumer-starting`, { group, consumer, stream });
|
|
77
119
|
Router.instances.add(this);
|
|
@@ -80,9 +122,7 @@ class Router {
|
|
|
80
122
|
let lastCheckedPendingMessagesAt = Date.now();
|
|
81
123
|
|
|
82
124
|
async function consume() {
|
|
83
|
-
|
|
84
|
-
this.currentTimerId = sleep.timerId;
|
|
85
|
-
await sleep.promise;
|
|
125
|
+
await this.customSleep();
|
|
86
126
|
if (!this.shouldConsume) {
|
|
87
127
|
this.logger.info(`stream-consumer-stopping`, { group, consumer, stream });
|
|
88
128
|
return;
|
|
@@ -273,18 +313,28 @@ class Router {
|
|
|
273
313
|
}
|
|
274
314
|
|
|
275
315
|
cancelThrottle() {
|
|
276
|
-
if (this.
|
|
277
|
-
clearTimeout(this.
|
|
278
|
-
this.currentTimerId = undefined;
|
|
316
|
+
if (this.sleepTimout) {
|
|
317
|
+
clearTimeout(this.sleepTimout);
|
|
279
318
|
}
|
|
319
|
+
this.resetThrottleState();
|
|
280
320
|
}
|
|
281
321
|
|
|
282
|
-
setThrottle(delayInMillis: number) {
|
|
322
|
+
public setThrottle(delayInMillis: number): void {
|
|
283
323
|
if (!Number.isInteger(delayInMillis) || delayInMillis < 0) {
|
|
284
324
|
throw new Error('Throttle must be a non-negative integer');
|
|
285
325
|
}
|
|
326
|
+
const wasDecreased = delayInMillis < this.throttle;
|
|
286
327
|
this.throttle = delayInMillis;
|
|
287
|
-
|
|
328
|
+
|
|
329
|
+
// If the throttle was decreased, and we're in the middle of a sleep cycle, adjust immediately
|
|
330
|
+
if (wasDecreased) {
|
|
331
|
+
if (this.sleepTimout) {
|
|
332
|
+
clearTimeout(this.sleepTimout);
|
|
333
|
+
}
|
|
334
|
+
if (this.innerPromiseResolve) {
|
|
335
|
+
this.innerPromiseResolve();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
288
338
|
}
|
|
289
339
|
|
|
290
340
|
async claimUnacknowledged(stream: string, group: string, consumer: string, idleTimeMs = this.reclaimDelay, limit = HMSH_XPENDING_COUNT): Promise<[string, [string, string]][]> {
|
|
@@ -19,7 +19,7 @@ export const MDATA_SYMBOLS = {
|
|
|
19
19
|
KEYS: ['au', 'err', 'l2s']
|
|
20
20
|
},
|
|
21
21
|
JOB: {
|
|
22
|
-
KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
22
|
+
KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'px', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
23
23
|
},
|
|
24
24
|
JOB_UPDATE: {
|
|
25
25
|
KEYS: ['ju', 'err']
|
|
@@ -22,6 +22,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
|
|
|
22
22
|
setnx: 'SETNX',
|
|
23
23
|
del: 'DEL',
|
|
24
24
|
expire: 'EXPIRE',
|
|
25
|
+
hscan: 'HSCAN',
|
|
25
26
|
hset: 'HSET',
|
|
26
27
|
hsetnx: 'HSETNX',
|
|
27
28
|
hincrby: 'HINCRBY',
|
|
@@ -41,6 +42,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
|
|
|
41
42
|
lpop: 'LPOP',
|
|
42
43
|
rename: 'RENAME',
|
|
43
44
|
rpush: 'RPUSH',
|
|
45
|
+
scan: 'SCAN',
|
|
44
46
|
xack: 'XACK',
|
|
45
47
|
xdel: 'XDEL',
|
|
46
48
|
xlen: 'XLEN',
|
package/services/store/index.ts
CHANGED
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
KeyService,
|
|
3
3
|
KeyStoreParams,
|
|
4
4
|
KeyType,
|
|
5
|
-
HMNS
|
|
5
|
+
HMNS,
|
|
6
|
+
VALSEP,
|
|
7
|
+
TYPSEP} from '../../modules/key';
|
|
6
8
|
import { ILogger } from '../logger';
|
|
7
9
|
import { MDATA_SYMBOLS, SerializerService as Serializer } from '../serializer';
|
|
8
10
|
import { Cache } from './cache';
|
|
@@ -50,6 +52,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
50
52
|
del: 'del',
|
|
51
53
|
expire: 'expire',
|
|
52
54
|
hset: 'hset',
|
|
55
|
+
hscan: 'hscan',
|
|
53
56
|
hsetnx: 'hsetnx',
|
|
54
57
|
hincrby: 'hincrby',
|
|
55
58
|
hdel: 'hdel',
|
|
@@ -68,6 +71,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
68
71
|
lrange: 'lrange',
|
|
69
72
|
rename: 'rename',
|
|
70
73
|
rpush: 'rpush',
|
|
74
|
+
scan: 'scan',
|
|
71
75
|
xack: 'xack',
|
|
72
76
|
xdel: 'xdel',
|
|
73
77
|
};
|
|
@@ -426,7 +430,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
426
430
|
* when `originJobId` is interrupted/expired, the items in the
|
|
427
431
|
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
428
432
|
*/
|
|
429
|
-
async registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
433
|
+
async registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, pd = '', multi? : U): Promise<any> {
|
|
430
434
|
const privateMulti = multi || this.getMulti();
|
|
431
435
|
const dependencyParams = {
|
|
432
436
|
appId: this.appId,
|
|
@@ -436,8 +440,13 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
436
440
|
KeyType.JOB_DEPENDENTS,
|
|
437
441
|
dependencyParams,
|
|
438
442
|
);
|
|
439
|
-
|
|
440
|
-
|
|
443
|
+
const expireTask = [
|
|
444
|
+
depType,
|
|
445
|
+
topic,
|
|
446
|
+
gId,
|
|
447
|
+
pd,
|
|
448
|
+
jobId,
|
|
449
|
+
].join(VALSEP);
|
|
441
450
|
privateMulti[this.commands.rpush](depKey, expireTask);
|
|
442
451
|
if (!multi) {
|
|
443
452
|
return await privateMulti.exec();
|
|
@@ -448,15 +457,20 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
448
457
|
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
449
458
|
* is interrupted/expired.
|
|
450
459
|
*/
|
|
451
|
-
async registerSignalDependency(jobId: string, signalKey: string, multi? : U): Promise<any> {
|
|
460
|
+
async registerSignalDependency(jobId: string, signalKey: string, dad: string, multi? : U): Promise<any> {
|
|
452
461
|
const privateMulti = multi || this.getMulti();
|
|
453
462
|
const dependencyParams = { appId: this.appId, jobId };
|
|
454
463
|
const dependencyKey = this.mintKey(
|
|
455
464
|
KeyType.JOB_DEPENDENTS,
|
|
456
465
|
dependencyParams,
|
|
457
466
|
);
|
|
458
|
-
//tasks
|
|
459
|
-
const delistTask =
|
|
467
|
+
//persiste dependency tasks as multi-segment composite keys
|
|
468
|
+
const delistTask = [
|
|
469
|
+
'delist',
|
|
470
|
+
'signal',
|
|
471
|
+
jobId,
|
|
472
|
+
dad,
|
|
473
|
+
signalKey].join(VALSEP);
|
|
460
474
|
privateMulti[this.commands.rpush](
|
|
461
475
|
dependencyKey,
|
|
462
476
|
delistTask,
|
|
@@ -797,11 +811,14 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
797
811
|
|
|
798
812
|
async setHookSignal(hook: HookSignal, multi?: U): Promise<any> {
|
|
799
813
|
const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
|
|
800
|
-
|
|
814
|
+
//destructure the hook key
|
|
815
|
+
const { topic, resolved, jobId} = hook;
|
|
801
816
|
const signalKey = `${topic}:${resolved}`;
|
|
802
817
|
const payload = { [signalKey]: jobId };
|
|
803
818
|
await (multi || this.redisClient)[this.commands.hset](key, payload);
|
|
804
|
-
|
|
819
|
+
//jobId needs even more destructuring
|
|
820
|
+
const [_aid, dad, _gid, jid] = jobId.split(VALSEP);
|
|
821
|
+
return await this.registerSignalDependency(jid, signalKey, dad, multi);
|
|
805
822
|
}
|
|
806
823
|
|
|
807
824
|
async getHookSignal(topic: string, resolved: string): Promise<string | undefined> {
|
|
@@ -873,7 +890,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
873
890
|
const depParams = { appId: this.appId, jobId };
|
|
874
891
|
const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
|
|
875
892
|
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
876
|
-
const depKeyContext =
|
|
893
|
+
const depKeyContext = `${TYPSEP}${context}${TYPSEP}${depKey}`;
|
|
877
894
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
878
895
|
await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
|
|
879
896
|
}
|
|
@@ -890,9 +907,15 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
890
907
|
* for the given sleep group. Sleep groups are
|
|
891
908
|
* organized into 'n'-second blocks (LISTS))
|
|
892
909
|
*/
|
|
893
|
-
async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, multi?: U): Promise<void> {
|
|
910
|
+
async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, dad: string, multi?: U): Promise<void> {
|
|
894
911
|
const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
895
|
-
|
|
912
|
+
//construct the composite key (the key has enough info to signal the hook)
|
|
913
|
+
const timeEvent = [
|
|
914
|
+
type,
|
|
915
|
+
activityId,
|
|
916
|
+
gId,
|
|
917
|
+
dad,
|
|
918
|
+
jobId].join(VALSEP);
|
|
896
919
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
897
920
|
if (multi || len === 1) {
|
|
898
921
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
@@ -907,17 +930,23 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
907
930
|
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
908
931
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
909
932
|
if (timeEvent) {
|
|
910
|
-
//
|
|
911
|
-
|
|
912
|
-
|
|
933
|
+
//deconstruct composite key
|
|
934
|
+
let [
|
|
935
|
+
type,
|
|
936
|
+
activityId,
|
|
937
|
+
gId,
|
|
938
|
+
_pd,
|
|
939
|
+
...jobId] = timeEvent.split(VALSEP);
|
|
940
|
+
const jid = jobId.join(VALSEP);
|
|
941
|
+
|
|
913
942
|
if (type === 'delist') {
|
|
914
943
|
pType = 'delist';
|
|
915
944
|
} else if (type === 'child') {
|
|
916
945
|
pType = 'child';
|
|
917
946
|
} else if (type === 'expire-child') {
|
|
918
|
-
type = 'expire';
|
|
947
|
+
type = 'expire';
|
|
919
948
|
}
|
|
920
|
-
return [listKey,
|
|
949
|
+
return [listKey, jid, gId, activityId, pType];
|
|
921
950
|
}
|
|
922
951
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
923
952
|
return true;
|
|
@@ -931,13 +960,13 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
931
960
|
* do with the work list. (not everything is known in advance,
|
|
932
961
|
* so the ZSET key defines HOW to approach the work in the
|
|
933
962
|
* generic LIST (lists typically contain target job ids)
|
|
934
|
-
* @param {string} listKey -
|
|
963
|
+
* @param {string} listKey - composite key
|
|
935
964
|
*/
|
|
936
965
|
resolveTaskKeyContext(listKey: string): [WorkListTaskType, string] {
|
|
937
|
-
if (listKey.startsWith(
|
|
938
|
-
return ['interrupt', listKey.split(
|
|
939
|
-
} else if (listKey.startsWith(
|
|
940
|
-
return ['expire', listKey.split(
|
|
966
|
+
if (listKey.startsWith(`${TYPSEP}INTERRUPT`)) {
|
|
967
|
+
return ['interrupt', listKey.split(TYPSEP)[2]];
|
|
968
|
+
} else if (listKey.startsWith(`${TYPSEP}EXPIRE`)) {
|
|
969
|
+
return ['expire', listKey.split(TYPSEP)[2]];
|
|
941
970
|
} else {
|
|
942
971
|
return ['sleep', listKey];
|
|
943
972
|
}
|
|
@@ -998,6 +1027,63 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
998
1027
|
const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
999
1028
|
await this.redisClient[this.commands.del](jobKey);
|
|
1000
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
async findJobs(queryString: string = '*', limit: number = 1000, batchSize: number = 1000): Promise<[string, string[]]> {
|
|
1032
|
+
const matchKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId: queryString });
|
|
1033
|
+
let cursor = '0';
|
|
1034
|
+
let keys: string[];
|
|
1035
|
+
const matchingKeys: string[] = [];
|
|
1036
|
+
do {
|
|
1037
|
+
const output = await this.exec(
|
|
1038
|
+
'SCAN',
|
|
1039
|
+
cursor,
|
|
1040
|
+
'MATCH',
|
|
1041
|
+
matchKey,
|
|
1042
|
+
'COUNT',
|
|
1043
|
+
batchSize.toString(),
|
|
1044
|
+
) as unknown as [string, string[]];
|
|
1045
|
+
if (Array.isArray(output)) {
|
|
1046
|
+
[cursor, keys] = output;
|
|
1047
|
+
for (let key of [...keys]) {
|
|
1048
|
+
matchingKeys.push(key);
|
|
1049
|
+
}
|
|
1050
|
+
if (matchingKeys.length >= limit) {
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
} else {
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
} while (cursor !== '0');
|
|
1057
|
+
return [cursor, matchingKeys];
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
async findJobFields(jobId: string, fieldMatchPattern: string = '*', limit: number = 1000, batchSize: number = 1000, cursor = '0'): Promise<[string, StringStringType]> {
|
|
1061
|
+
let fields: string[] = [];
|
|
1062
|
+
const matchingFields: StringStringType = {};
|
|
1063
|
+
const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
1064
|
+
let len = 0;
|
|
1065
|
+
do {
|
|
1066
|
+
const output = await this.exec(
|
|
1067
|
+
'HSCAN',
|
|
1068
|
+
jobKey,
|
|
1069
|
+
cursor,
|
|
1070
|
+
'MATCH',
|
|
1071
|
+
fieldMatchPattern,
|
|
1072
|
+
'COUNT',
|
|
1073
|
+
batchSize.toString(),
|
|
1074
|
+
) as unknown as [string, string[]];
|
|
1075
|
+
if (Array.isArray(output)) {
|
|
1076
|
+
[cursor, fields] = output;
|
|
1077
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
1078
|
+
len++;
|
|
1079
|
+
matchingFields[fields[i]] = fields[i + 1];
|
|
1080
|
+
}
|
|
1081
|
+
} else {
|
|
1082
|
+
break;
|
|
1083
|
+
}
|
|
1084
|
+
} while (cursor !== '0' && len < limit);
|
|
1085
|
+
return [cursor, matchingFields];
|
|
1086
|
+
}
|
|
1001
1087
|
}
|
|
1002
1088
|
|
|
1003
1089
|
export { StoreService };
|