@hotmeshio/hotmesh 0.0.37 → 0.0.39
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 +14 -8
- package/build/modules/enums.d.ts +29 -23
- package/build/modules/enums.js +38 -29
- package/build/modules/errors.d.ts +1 -1
- package/build/modules/errors.js +9 -7
- package/build/modules/key.d.ts +1 -34
- package/build/modules/key.js +24 -47
- package/build/package.json +1 -1
- package/build/services/activities/activity.js +1 -1
- package/build/services/activities/hook.js +4 -9
- package/build/services/activities/trigger.d.ts +3 -2
- package/build/services/activities/trigger.js +10 -6
- package/build/services/durable/client.d.ts +9 -1
- package/build/services/durable/client.js +30 -14
- package/build/services/durable/handle.js +2 -2
- package/build/services/durable/worker.js +4 -3
- package/build/services/engine/index.d.ts +2 -1
- package/build/services/engine/index.js +6 -6
- package/build/services/hotmesh/index.d.ts +2 -2
- package/build/services/hotmesh/index.js +3 -4
- package/build/services/quorum/index.d.ts +6 -6
- package/build/services/quorum/index.js +47 -11
- package/build/services/router/index.js +16 -14
- package/build/services/store/clients/ioredis.d.ts +1 -0
- package/build/services/store/clients/ioredis.js +9 -0
- package/build/services/store/clients/redis.d.ts +1 -0
- package/build/services/store/clients/redis.js +16 -0
- package/build/services/store/index.d.ts +15 -9
- package/build/services/store/index.js +46 -23
- package/build/services/stream/clients/ioredis.d.ts +1 -0
- package/build/services/stream/clients/ioredis.js +33 -24
- package/build/services/stream/clients/redis.d.ts +1 -0
- package/build/services/stream/clients/redis.js +15 -0
- package/build/services/stream/index.d.ts +1 -0
- package/build/services/task/index.d.ts +10 -3
- package/build/services/task/index.js +35 -17
- package/build/services/worker/index.d.ts +1 -0
- package/build/services/worker/index.js +24 -0
- package/build/types/durable.d.ts +3 -2
- package/build/types/hotmesh.d.ts +43 -2
- package/build/types/hotmesh.js +28 -0
- package/build/types/index.d.ts +3 -2
- package/build/types/index.js +3 -1
- package/build/types/logger.d.ts +1 -0
- package/build/types/logger.js +1 -0
- package/build/types/quorum.d.ts +11 -1
- package/build/types/redisclient.d.ts +1 -0
- package/build/types/task.d.ts +1 -0
- package/build/types/task.js +2 -0
- package/modules/enums.ts +49 -35
- package/modules/errors.ts +17 -8
- package/modules/key.ts +3 -40
- package/package.json +1 -1
- package/services/activities/activity.ts +2 -2
- package/services/activities/hook.ts +18 -9
- package/services/activities/trigger.ts +10 -6
- package/services/durable/client.ts +31 -15
- package/services/durable/handle.ts +3 -3
- package/services/durable/worker.ts +4 -3
- package/services/engine/index.ts +13 -12
- package/services/hotmesh/index.ts +4 -5
- package/services/quorum/index.ts +48 -12
- package/services/router/index.ts +26 -24
- package/services/store/clients/ioredis.ts +9 -0
- package/services/store/clients/redis.ts +16 -0
- package/services/store/index.ts +63 -25
- package/services/stream/clients/ioredis.ts +33 -24
- package/services/stream/clients/redis.ts +14 -0
- package/services/stream/index.ts +1 -0
- package/services/task/index.ts +66 -24
- package/services/worker/index.ts +30 -0
- package/types/durable.ts +6 -5
- package/types/hotmesh.ts +47 -2
- package/types/index.ts +8 -1
- package/types/logger.ts +3 -1
- package/types/quorum.ts +15 -4
- package/types/redisclient.ts +1 -0
- package/types/task.ts +1 -0
package/services/router/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
HMSH_BLOCK_TIME_MS,
|
|
3
|
+
HMSH_MAX_RETRIES,
|
|
4
|
+
HMSH_MAX_TIMEOUT_MS,
|
|
5
|
+
HMSH_GRADUATED_INTERVAL_MS,
|
|
6
|
+
HMSH_CODE_UNACKED,
|
|
7
|
+
HMSH_CODE_UNKNOWN,
|
|
8
|
+
HMSH_STATUS_UNKNOWN,
|
|
9
|
+
HMSH_XCLAIM_COUNT,
|
|
10
|
+
HMSH_XCLAIM_DELAY_MS,
|
|
11
|
+
HMSH_XPENDING_COUNT } from '../../modules/enums';
|
|
12
12
|
import { KeyType } from '../../modules/key';
|
|
13
13
|
import { XSleepFor, guid, sleepFor } from '../../modules/utils';
|
|
14
14
|
import { ILogger } from '../logger';
|
|
@@ -50,8 +50,8 @@ class Router {
|
|
|
50
50
|
this.topic = config.topic;
|
|
51
51
|
this.stream = stream;
|
|
52
52
|
this.store = store;
|
|
53
|
-
this.reclaimDelay = config.reclaimDelay ||
|
|
54
|
-
this.reclaimCount = config.reclaimCount ||
|
|
53
|
+
this.reclaimDelay = config.reclaimDelay || HMSH_XCLAIM_DELAY_MS;
|
|
54
|
+
this.reclaimCount = config.reclaimCount || HMSH_XCLAIM_COUNT;
|
|
55
55
|
this.logger = logger;
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -85,7 +85,9 @@ class Router {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
try {
|
|
88
|
-
|
|
88
|
+
//randomizer that asymptotes at 150% of `HMSH_BLOCK_TIME_MS`
|
|
89
|
+
const streamDuration = HMSH_BLOCK_TIME_MS + Math.round((HMSH_BLOCK_TIME_MS * Math.random()));
|
|
90
|
+
const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', streamDuration, 'STREAMS', stream, '>');
|
|
89
91
|
if (this.isStreamMessage(result)) {
|
|
90
92
|
const [[, messages]] = result;
|
|
91
93
|
for (const [id, message] of messages) {
|
|
@@ -107,7 +109,7 @@ class Router {
|
|
|
107
109
|
if (this.shouldConsume && process.env.NODE_ENV !== 'test') {
|
|
108
110
|
this.logger.error(`stream-consume-message-error`, { err, stream, group, consumer });
|
|
109
111
|
this.errorCount++;
|
|
110
|
-
const timeout = Math.min(
|
|
112
|
+
const timeout = Math.min(HMSH_GRADUATED_INTERVAL_MS * (2 ** this.errorCount), HMSH_MAX_TIMEOUT_MS);
|
|
111
113
|
setTimeout(consume.bind(this), timeout);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
@@ -129,7 +131,7 @@ class Router {
|
|
|
129
131
|
telemetry.startStreamSpan(input, this.role);
|
|
130
132
|
output = await this.execStreamLeg(input, stream, id, callback.bind(this));
|
|
131
133
|
if (output?.status === StreamStatus.ERROR) {
|
|
132
|
-
telemetry.setStreamError(`Function Status Code ${ output.code ||
|
|
134
|
+
telemetry.setStreamError(`Function Status Code ${ output.code || HMSH_CODE_UNKNOWN }`);
|
|
133
135
|
}
|
|
134
136
|
this.errorCount = 0;
|
|
135
137
|
} catch (err) {
|
|
@@ -191,7 +193,7 @@ class Router {
|
|
|
191
193
|
const errorCode = output.code.toString();
|
|
192
194
|
const policy = policies?.[errorCode];
|
|
193
195
|
const maxRetries = policy?.[0];
|
|
194
|
-
const tryCount = Math.min(input.metadata.try || 0,
|
|
196
|
+
const tryCount = Math.min(input.metadata.try || 0, HMSH_MAX_RETRIES);
|
|
195
197
|
//only possible values for maxRetries are 1, 2, 3
|
|
196
198
|
//only possible values for tryCount are 0, 1, 2
|
|
197
199
|
if (maxRetries > tryCount) {
|
|
@@ -206,7 +208,7 @@ class Router {
|
|
|
206
208
|
if (typeof err.message === 'string') {
|
|
207
209
|
error.message = err.message;
|
|
208
210
|
} else {
|
|
209
|
-
error.message =
|
|
211
|
+
error.message = HMSH_STATUS_UNKNOWN;
|
|
210
212
|
}
|
|
211
213
|
if (typeof err.stack === 'string') {
|
|
212
214
|
error.stack = err.stack;
|
|
@@ -216,7 +218,7 @@ class Router {
|
|
|
216
218
|
}
|
|
217
219
|
return {
|
|
218
220
|
status: 'error',
|
|
219
|
-
code:
|
|
221
|
+
code: HMSH_CODE_UNKNOWN,
|
|
220
222
|
metadata: { ...input.metadata, guid: guid() },
|
|
221
223
|
data: error as StreamError
|
|
222
224
|
} as StreamDataResponse;
|
|
@@ -224,7 +226,7 @@ class Router {
|
|
|
224
226
|
|
|
225
227
|
structureUnacknowledgedError(input: StreamData) {
|
|
226
228
|
const message = 'stream message max delivery count exceeded';
|
|
227
|
-
const code =
|
|
229
|
+
const code = HMSH_CODE_UNACKED;
|
|
228
230
|
const data: StreamError = { message, code };
|
|
229
231
|
const output: StreamDataResponse = {
|
|
230
232
|
metadata: { ...input.metadata, guid: guid() },
|
|
@@ -238,9 +240,9 @@ class Router {
|
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
structureError(input: StreamData, output: StreamDataResponse): StreamDataResponse {
|
|
241
|
-
const message = output.data?.message ? output.data?.message.toString() :
|
|
243
|
+
const message = output.data?.message ? output.data?.message.toString() : HMSH_STATUS_UNKNOWN;
|
|
242
244
|
const statusCode = output.code || output.data?.code;
|
|
243
|
-
const code = isNaN(statusCode as number) ?
|
|
245
|
+
const code = isNaN(statusCode as number) ? HMSH_CODE_UNKNOWN : parseInt(statusCode.toString());
|
|
244
246
|
const data: StreamError = { message, code };
|
|
245
247
|
if (typeof output.data?.error === 'object') {
|
|
246
248
|
data.error = { ...output.data.error };
|
|
@@ -257,7 +259,7 @@ class Router {
|
|
|
257
259
|
for (const instance of [...Router.instances]) {
|
|
258
260
|
instance.stopConsuming();
|
|
259
261
|
}
|
|
260
|
-
await sleepFor(
|
|
262
|
+
await sleepFor(HMSH_BLOCK_TIME_MS * 2);
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
async stopConsuming() {
|
|
@@ -281,7 +283,7 @@ class Router {
|
|
|
281
283
|
this.logger.info(`stream-throttle-reset`, { delay: this.throttle, topic: this.topic });
|
|
282
284
|
}
|
|
283
285
|
|
|
284
|
-
async claimUnacknowledged(stream: string, group: string, consumer: string, idleTimeMs = this.reclaimDelay, limit =
|
|
286
|
+
async claimUnacknowledged(stream: string, group: string, consumer: string, idleTimeMs = this.reclaimDelay, limit = HMSH_XPENDING_COUNT): Promise<[string, [string, string]][]> {
|
|
285
287
|
let pendingMessages = [];
|
|
286
288
|
const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit); //[[ '1688768134881-0', 'testConsumer1', 1017, 1 ]]
|
|
287
289
|
for (const pendingMessageInfo of pendingMessagesInfo) {
|
|
@@ -310,7 +312,7 @@ class Router {
|
|
|
310
312
|
// ii) corrupt hardware/network/transport/etc
|
|
311
313
|
// 3b) system error: Redis unable to accept `xadd` request
|
|
312
314
|
// 4c) system error: Redis unable to accept `xdel`/`xack` request
|
|
313
|
-
this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code:
|
|
315
|
+
this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: HMSH_CODE_UNACKED, count });
|
|
314
316
|
const streamData = reclaimedMessage[0]?.[1]?.[1];
|
|
315
317
|
|
|
316
318
|
//fatal risk point 1 of 3): json is corrupt
|
|
@@ -131,6 +131,15 @@ class IORedisStoreService extends StoreService<RedisClientType, RedisMultiType>
|
|
|
131
131
|
throw error;
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
|
|
135
|
+
async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
|
|
136
|
+
try {
|
|
137
|
+
return await (multi || this.redisClient).xlen(key);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
this.logger.error(`Error getting stream depth: ${key}`, { error });
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
export { IORedisStoreService };
|
|
@@ -42,6 +42,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
|
|
|
42
42
|
rpush: 'RPUSH',
|
|
43
43
|
xack: 'XACK',
|
|
44
44
|
xdel: 'XDEL',
|
|
45
|
+
xlen: 'XLEN',
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
@@ -167,6 +168,21 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
|
|
|
167
168
|
throw error;
|
|
168
169
|
}
|
|
169
170
|
}
|
|
171
|
+
|
|
172
|
+
async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
|
|
173
|
+
try {
|
|
174
|
+
if (multi) {
|
|
175
|
+
multi.XLEN(key);
|
|
176
|
+
return multi;
|
|
177
|
+
} else {
|
|
178
|
+
return await this.redisClient.XLEN(key);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.logger.error(`Error getting stream depth: ${key}`, { error });
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
170
186
|
}
|
|
171
187
|
|
|
172
188
|
export { RedisStoreService };
|
package/services/store/index.ts
CHANGED
|
@@ -29,8 +29,9 @@ import { Transitions } from '../../types/transition';
|
|
|
29
29
|
import { formatISODate, getSymKey } from '../../modules/utils';
|
|
30
30
|
import { ReclaimedMessageType } from '../../types/stream';
|
|
31
31
|
import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
|
|
32
|
-
import {
|
|
32
|
+
import { HMSH_SCOUT_INTERVAL_SECONDS, HMSH_CODE_INTERRUPT } from '../../modules/enums';
|
|
33
33
|
import { GetStateError } from '../../modules/errors';
|
|
34
|
+
import { WorkListTaskType } from '../../types/task';
|
|
34
35
|
|
|
35
36
|
interface AbstractRedisClient {
|
|
36
37
|
exec(): any;
|
|
@@ -118,6 +119,10 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
118
119
|
id: string,
|
|
119
120
|
multi?: U
|
|
120
121
|
): Promise<number|U>;
|
|
122
|
+
abstract xlen(
|
|
123
|
+
key: string,
|
|
124
|
+
multi?: U
|
|
125
|
+
): Promise<number|U>;
|
|
121
126
|
|
|
122
127
|
constructor(redisClient: T) {
|
|
123
128
|
this.redisClient = redisClient;
|
|
@@ -173,7 +178,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
173
178
|
* check for and process work items in the
|
|
174
179
|
* time and signal task queues.
|
|
175
180
|
*/
|
|
176
|
-
async reserveScoutRole(scoutType: 'time' | 'signal', delay =
|
|
181
|
+
async reserveScoutRole(scoutType: 'time' | 'signal', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
|
|
177
182
|
const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
178
183
|
const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
|
|
179
184
|
if (this.isSuccessful(success)) {
|
|
@@ -384,16 +389,45 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
384
389
|
}
|
|
385
390
|
|
|
386
391
|
/**
|
|
387
|
-
* Registers jobId with
|
|
388
|
-
* when originJobId is interrupted
|
|
389
|
-
* list (added via RPUSH)
|
|
390
|
-
* LPOPed items from the list are likewise expired;
|
|
392
|
+
* Registers the job, `jobId`, with `originJobId`. In the future,
|
|
393
|
+
* when `originJobId` is interrupted/expired, the items in the
|
|
394
|
+
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
391
395
|
*/
|
|
392
|
-
async
|
|
396
|
+
async registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
393
397
|
const privateMulti = multi || this.getMulti();
|
|
394
|
-
const
|
|
395
|
-
|
|
396
|
-
|
|
398
|
+
const dependencyParams = {
|
|
399
|
+
appId: this.appId,
|
|
400
|
+
jobId: originJobId,
|
|
401
|
+
};
|
|
402
|
+
const depKey = this.mintKey(
|
|
403
|
+
KeyType.JOB_DEPENDENTS,
|
|
404
|
+
dependencyParams,
|
|
405
|
+
);
|
|
406
|
+
//tasks have '4' segments
|
|
407
|
+
const expireTask = `expire::${topic}::${gId}::${jobId}`;
|
|
408
|
+
privateMulti[this.commands.rpush](depKey, expireTask);
|
|
409
|
+
if (!multi) {
|
|
410
|
+
return await privateMulti.exec();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
416
|
+
* is interrupted/expired.
|
|
417
|
+
*/
|
|
418
|
+
async registerSignalDependency(jobId: string, signalKey: string, multi? : U): Promise<any> {
|
|
419
|
+
const privateMulti = multi || this.getMulti();
|
|
420
|
+
const dependencyParams = { appId: this.appId, jobId };
|
|
421
|
+
const dependencyKey = this.mintKey(
|
|
422
|
+
KeyType.JOB_DEPENDENTS,
|
|
423
|
+
dependencyParams,
|
|
424
|
+
);
|
|
425
|
+
//tasks have '4' segments
|
|
426
|
+
const delistTask = `delist::signal::${jobId}::${signalKey}`;
|
|
427
|
+
privateMulti[this.commands.rpush](
|
|
428
|
+
dependencyKey,
|
|
429
|
+
delistTask,
|
|
430
|
+
);
|
|
397
431
|
if (!multi) {
|
|
398
432
|
return await privateMulti.exec();
|
|
399
433
|
}
|
|
@@ -706,11 +740,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
706
740
|
|
|
707
741
|
async setHookSignal(hook: HookSignal, multi?: U): Promise<any> {
|
|
708
742
|
const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
|
|
709
|
-
const { topic, resolved, jobId} = hook;
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
return await
|
|
743
|
+
const { topic, resolved, jobId} = hook; //`${activityId}::${dad}::${gId}::${jobId}`
|
|
744
|
+
const signalKey = `${topic}:${resolved}`;
|
|
745
|
+
const payload = { [signalKey]: jobId };
|
|
746
|
+
await (multi || this.redisClient)[this.commands.hset](key, payload);
|
|
747
|
+
return await this.registerSignalDependency(jobId.split('::')[3], signalKey, multi);
|
|
714
748
|
}
|
|
715
749
|
|
|
716
750
|
async getHookSignal(topic: string, resolved: string): Promise<string | undefined> {
|
|
@@ -778,7 +812,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
778
812
|
* expired at a future date; options indicate whether this
|
|
779
813
|
* is a standard `expire` or an `interrupt`
|
|
780
814
|
*/
|
|
781
|
-
async
|
|
815
|
+
async registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
|
|
782
816
|
const depParams = { appId: this.appId, jobId };
|
|
783
817
|
const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
|
|
784
818
|
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
@@ -793,7 +827,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
793
827
|
* for the given sleep group. Sleep groups are
|
|
794
828
|
* organized into 'n'-second blocks (LISTS))
|
|
795
829
|
*/
|
|
796
|
-
async registerTimeHook(jobId: string, gId: string, activityId: string, type:
|
|
830
|
+
async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, multi?: U): Promise<void> {
|
|
797
831
|
const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
798
832
|
const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
|
|
799
833
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
@@ -803,21 +837,25 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
803
837
|
}
|
|
804
838
|
}
|
|
805
839
|
|
|
806
|
-
async
|
|
807
|
-
const existing = Boolean(listKey);
|
|
840
|
+
async getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean> {
|
|
808
841
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
809
842
|
listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
|
|
810
843
|
if (listKey) {
|
|
811
|
-
|
|
844
|
+
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
812
845
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
813
846
|
if (timeEvent) {
|
|
814
|
-
//there are
|
|
815
|
-
|
|
847
|
+
//there are 4 time-related task
|
|
848
|
+
//1) sleep (awaken), 2) expire, 3) interrupt, 4) delist
|
|
849
|
+
const [type, activityId, gId, ...jobId] = timeEvent.split('::');
|
|
850
|
+
if (type === 'delist') {
|
|
851
|
+
pType = 'delist';
|
|
852
|
+
}
|
|
816
853
|
return [listKey, jobId.join('::'), gId, activityId, pType];
|
|
817
854
|
}
|
|
818
855
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
856
|
+
return true;
|
|
819
857
|
}
|
|
820
|
-
return
|
|
858
|
+
return false;
|
|
821
859
|
}
|
|
822
860
|
|
|
823
861
|
/**
|
|
@@ -828,7 +866,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
828
866
|
* generic LIST (lists typically contain target job ids)
|
|
829
867
|
* @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
|
|
830
868
|
*/
|
|
831
|
-
|
|
869
|
+
resolveTaskKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'|'delist'), string] {
|
|
832
870
|
if (listKey.startsWith('::INTERRUPT')) {
|
|
833
871
|
return ['interrupt', listKey.split('::')[2]];
|
|
834
872
|
} else if (listKey.startsWith('::EXPIRE')) {
|
|
@@ -870,7 +908,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
870
908
|
|
|
871
909
|
//persists the standard 410 error (job is `gone`)
|
|
872
910
|
const err = JSON.stringify({
|
|
873
|
-
code:
|
|
911
|
+
code: HMSH_CODE_INTERRUPT,
|
|
874
912
|
message: options.reason ?? `job [${jobId}] interrupted`,
|
|
875
913
|
job_id: jobId
|
|
876
914
|
});
|
|
@@ -33,16 +33,16 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
|
|
|
33
33
|
if (mkStream === 'MKSTREAM') {
|
|
34
34
|
try {
|
|
35
35
|
return (await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK';
|
|
36
|
-
} catch (
|
|
36
|
+
} catch (error) {
|
|
37
37
|
this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
|
|
38
|
-
throw
|
|
38
|
+
throw error;
|
|
39
39
|
}
|
|
40
40
|
} else {
|
|
41
41
|
try {
|
|
42
42
|
return (await this.redisClient.xgroup(command, key, groupName, id)) === 'OK';
|
|
43
|
-
} catch (
|
|
43
|
+
} catch (error) {
|
|
44
44
|
this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
|
|
45
|
-
throw
|
|
45
|
+
throw error;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -50,9 +50,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
|
|
|
50
50
|
async xadd(key: string, id: string, messageId: string, messageValue: string, multi?: RedisMultiType): Promise<string | RedisMultiType> {
|
|
51
51
|
try {
|
|
52
52
|
return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
|
|
53
|
-
} catch (
|
|
54
|
-
this.logger.error(`Error publishing 'xadd'; key: ${key}`,
|
|
55
|
-
throw
|
|
53
|
+
} catch (error) {
|
|
54
|
+
this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
|
|
55
|
+
throw error;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -79,9 +79,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
|
|
|
79
79
|
streamName,
|
|
80
80
|
id
|
|
81
81
|
);
|
|
82
|
-
} catch (
|
|
83
|
-
this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`,
|
|
84
|
-
throw
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, { error });
|
|
84
|
+
throw error;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
@@ -101,12 +101,12 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
|
|
|
101
101
|
if (consumer) args.push(consumer);
|
|
102
102
|
try {
|
|
103
103
|
return await this.redisClient.call('XPENDING', ...args) as [string, string, number, number][];
|
|
104
|
-
} catch (
|
|
105
|
-
this.logger.error('err, args',
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.logger.error('err, args', { error }, args);
|
|
106
106
|
}
|
|
107
|
-
} catch (
|
|
108
|
-
this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`,
|
|
109
|
-
throw
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
|
|
109
|
+
throw error;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -120,27 +120,36 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
|
|
|
120
120
|
): Promise<ReclaimedMessageType> {
|
|
121
121
|
try {
|
|
122
122
|
return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args) as unknown as ReclaimedMessageType;
|
|
123
|
-
} catch (
|
|
124
|
-
this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`,
|
|
125
|
-
throw
|
|
123
|
+
} catch (error) {
|
|
124
|
+
this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
|
|
125
|
+
throw error;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
async xack(key: string, group: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
|
|
130
130
|
try {
|
|
131
131
|
return await (multi || this.redisClient).xack(key, group, id);
|
|
132
|
-
} catch (
|
|
133
|
-
this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`,
|
|
134
|
-
throw
|
|
132
|
+
} catch (error) {
|
|
133
|
+
this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
|
|
134
|
+
throw error;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
async xdel(key: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
|
|
139
139
|
try {
|
|
140
140
|
return await (multi || this.redisClient).xdel(key, id);
|
|
141
|
-
} catch (
|
|
142
|
-
this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`,
|
|
143
|
-
throw
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
|
|
148
|
+
try {
|
|
149
|
+
return await (multi || this.redisClient).xlen(key);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
this.logger.error(`Error getting stream depth: ${key}`, { error });
|
|
152
|
+
throw error;
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
}
|
|
@@ -139,6 +139,20 @@ class RedisStreamService extends StreamService<RedisClientType, RedisMultiType>
|
|
|
139
139
|
throw err;
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
+
|
|
143
|
+
async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
|
|
144
|
+
try {
|
|
145
|
+
if (multi) {
|
|
146
|
+
multi.XLEN(key);
|
|
147
|
+
return multi;
|
|
148
|
+
} else {
|
|
149
|
+
return await this.redisClient.XLEN(key);
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
this.logger.error(`Error getting stream depth: ${key}`, { error });
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
142
156
|
}
|
|
143
157
|
|
|
144
158
|
export { RedisStreamService };
|
package/services/stream/index.ts
CHANGED
|
@@ -52,6 +52,7 @@ abstract class StreamService<T, U> {
|
|
|
52
52
|
...args: string[]): Promise<ReclaimedMessageType>;
|
|
53
53
|
abstract xack(key: string, group: string, id: string, multi?: U): Promise<number|U>;
|
|
54
54
|
abstract xdel(key: string, id: string, multi?: U): Promise<number|U>;
|
|
55
|
+
abstract xlen(key: string, multi?: U): Promise<number|U>;
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
export { StreamService };
|
package/services/task/index.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
HMSH_EXPIRE_DURATION,
|
|
3
|
+
HMSH_FIDELITY_SECONDS,
|
|
4
|
+
HMSH_SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
|
|
5
5
|
import { XSleepFor, sleepFor } from '../../modules/utils';
|
|
6
6
|
import { ILogger } from '../logger';
|
|
7
|
+
import { Pipe } from '../pipe';
|
|
7
8
|
import { StoreService } from '../store';
|
|
8
9
|
import { HookInterface, HookRule, HookSignal } from '../../types/hook';
|
|
10
|
+
import { KeyType } from '../../types/hotmesh';
|
|
9
11
|
import { JobCompletionOptions, JobState } from '../../types/job';
|
|
10
12
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
11
|
-
import {
|
|
13
|
+
import { WorkListTaskType } from '../../types/task';
|
|
12
14
|
|
|
13
15
|
class TaskService {
|
|
14
16
|
store: StoreService<RedisClient, RedisMulti>;
|
|
@@ -35,7 +37,12 @@ class TaskService {
|
|
|
35
37
|
//todo: don't use 'id', make configurable using hook rule
|
|
36
38
|
await hookEventCallback(topic, { ...data, id: jobId });
|
|
37
39
|
} else {
|
|
38
|
-
await this.store.deleteProcessedTaskQueue(
|
|
40
|
+
await this.store.deleteProcessedTaskQueue(
|
|
41
|
+
workItemKey,
|
|
42
|
+
sourceKey,
|
|
43
|
+
destinationKey,
|
|
44
|
+
scrub === 'true'
|
|
45
|
+
);
|
|
39
46
|
}
|
|
40
47
|
setImmediate(() => this.processWebHooks(hookEventCallback));
|
|
41
48
|
}
|
|
@@ -45,21 +52,45 @@ class TaskService {
|
|
|
45
52
|
await this.store.addTaskQueues(keys);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
async registerJobForCleanup(jobId: string, inSeconds =
|
|
55
|
+
async registerJobForCleanup(jobId: string, inSeconds = HMSH_EXPIRE_DURATION, options: JobCompletionOptions): Promise<void> {
|
|
49
56
|
if (inSeconds > 0) {
|
|
50
57
|
await this.store.expireJob(jobId, inSeconds);
|
|
51
|
-
const
|
|
52
|
-
|
|
58
|
+
const fromNow = Date.now() + (inSeconds * 1000);
|
|
59
|
+
const fidelityMS = HMSH_FIDELITY_SECONDS * 1000;
|
|
60
|
+
const timeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
|
|
61
|
+
await this.store.registerDependenciesForCleanup(
|
|
62
|
+
jobId,
|
|
63
|
+
timeSlot,
|
|
64
|
+
options,
|
|
65
|
+
);
|
|
53
66
|
}
|
|
54
67
|
}
|
|
55
68
|
|
|
56
|
-
async registerTimeHook(
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
async registerTimeHook(
|
|
70
|
+
jobId: string,
|
|
71
|
+
gId: string,
|
|
72
|
+
activityId: string,
|
|
73
|
+
type: WorkListTaskType,
|
|
74
|
+
inSeconds = HMSH_FIDELITY_SECONDS,
|
|
75
|
+
multi?: RedisMulti,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const fromNow = Date.now() + (inSeconds * 1000);
|
|
78
|
+
const fidelityMS = HMSH_FIDELITY_SECONDS * 1000;
|
|
79
|
+
const awakenTimeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
|
|
80
|
+
await this.store.registerTimeHook(
|
|
81
|
+
jobId,
|
|
82
|
+
gId,
|
|
83
|
+
activityId,
|
|
84
|
+
type,
|
|
85
|
+
awakenTimeSlot,
|
|
86
|
+
multi,
|
|
87
|
+
);
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
/**
|
|
62
|
-
* Should this engine instance play the role of 'scout'
|
|
91
|
+
* Should this engine instance play the role of 'scout' on behalf
|
|
92
|
+
* of the entire quorum? The scout role is responsible for processing
|
|
93
|
+
* task lists on behalf of the collective.
|
|
63
94
|
*/
|
|
64
95
|
async shouldScout() {
|
|
65
96
|
const wasScout = this.isScout;
|
|
@@ -68,30 +99,41 @@ class TaskService {
|
|
|
68
99
|
if (!wasScout) {
|
|
69
100
|
setTimeout(() => {
|
|
70
101
|
this.isScout = false;
|
|
71
|
-
},
|
|
102
|
+
}, HMSH_SCOUT_INTERVAL_SECONDS * 1_000);
|
|
72
103
|
}
|
|
73
104
|
return true;
|
|
74
105
|
}
|
|
75
106
|
return false;
|
|
76
107
|
}
|
|
77
108
|
|
|
78
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Callback handler that takes an item from a work list and
|
|
111
|
+
* processes according to its type
|
|
112
|
+
*/
|
|
113
|
+
async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>, listKey?: string): Promise<void> {
|
|
79
114
|
if (await this.shouldScout()) {
|
|
80
115
|
try {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const [listKey,
|
|
85
|
-
|
|
116
|
+
const workListTask = await this.store.getNextTask(listKey);
|
|
117
|
+
|
|
118
|
+
if (Array.isArray(workListTask)) {
|
|
119
|
+
const [listKey, target, gId, activityId, type] = workListTask;
|
|
120
|
+
if (type === 'delist') {
|
|
121
|
+
//delist the signalKey (target)
|
|
122
|
+
const key = this.store.mintKey(KeyType.SIGNALS, { appId: this.store.appId });
|
|
123
|
+
await this.store.redisClient[this.store.commands.hdel](key, target);
|
|
124
|
+
} else {
|
|
125
|
+
//awaken/expire/interrupt
|
|
126
|
+
await timeEventCallback(target, gId, activityId, type);
|
|
127
|
+
}
|
|
86
128
|
await sleepFor(0);
|
|
87
129
|
this.processTimeHooks(timeEventCallback, listKey);
|
|
88
|
-
} else if (
|
|
89
|
-
//a
|
|
130
|
+
} else if (workListTask) {
|
|
131
|
+
//a worklist was just emptied; try again immediately
|
|
90
132
|
await sleepFor(0);
|
|
91
133
|
this.processTimeHooks(timeEventCallback);
|
|
92
134
|
} else {
|
|
93
|
-
//
|
|
94
|
-
let sleep = XSleepFor(
|
|
135
|
+
//no worklists exist; sleep before checking
|
|
136
|
+
let sleep = XSleepFor(HMSH_FIDELITY_SECONDS * 1000);
|
|
95
137
|
this.cleanupTimeout = sleep.timerId;
|
|
96
138
|
await sleep.promise;
|
|
97
139
|
this.processTimeHooks(timeEventCallback);
|
|
@@ -102,7 +144,7 @@ class TaskService {
|
|
|
102
144
|
}
|
|
103
145
|
} else {
|
|
104
146
|
//didn't get the scout role; try again in 'one-ish' minutes
|
|
105
|
-
let sleep = XSleepFor(
|
|
147
|
+
let sleep = XSleepFor(HMSH_SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
|
|
106
148
|
this.cleanupTimeout = sleep.timerId;
|
|
107
149
|
await sleep.promise;
|
|
108
150
|
this.processTimeHooks(timeEventCallback);
|