@hotmeshio/hotmesh 0.0.38 → 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 +13 -7
- 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/router/index.js +16 -14
- package/build/services/store/index.d.ts +14 -9
- package/build/services/store/index.js +46 -23
- package/build/services/task/index.d.ts +10 -3
- package/build/services/task/index.js +35 -17
- 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/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/router/index.ts +26 -24
- package/services/store/index.ts +59 -25
- package/services/task/index.ts +66 -24
- 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/task.ts +1 -0
|
@@ -8,6 +8,7 @@ const factory_1 = require("./factory");
|
|
|
8
8
|
const hotmesh_1 = require("../hotmesh");
|
|
9
9
|
const search_1 = require("./search");
|
|
10
10
|
const stream_1 = require("../../types/stream");
|
|
11
|
+
const enums_1 = require("../../modules/enums");
|
|
11
12
|
class WorkerService {
|
|
12
13
|
static async activateWorkflow(hotMesh) {
|
|
13
14
|
const app = await hotMesh.engine.store.getApp(hotMesh.engine.appId);
|
|
@@ -85,7 +86,7 @@ class WorkerService {
|
|
|
85
86
|
options: config.connection.options
|
|
86
87
|
};
|
|
87
88
|
const hotMeshWorker = await hotmesh_1.HotMeshService.init({
|
|
88
|
-
logLevel: config.options?.logLevel ??
|
|
89
|
+
logLevel: config.options?.logLevel ?? enums_1.HMSH_LOGLEVEL,
|
|
89
90
|
appId: config.namespace ?? factory_1.APP_ID,
|
|
90
91
|
engine: { redis: redisConfig },
|
|
91
92
|
workers: [
|
|
@@ -134,7 +135,7 @@ class WorkerService {
|
|
|
134
135
|
options: config.connection.options
|
|
135
136
|
};
|
|
136
137
|
const hotMeshWorker = await hotmesh_1.HotMeshService.init({
|
|
137
|
-
logLevel: config.options?.logLevel ??
|
|
138
|
+
logLevel: config.options?.logLevel ?? enums_1.HMSH_LOGLEVEL,
|
|
138
139
|
appId: config.namespace ?? factory_1.APP_ID,
|
|
139
140
|
engine: { redis: redisConfig },
|
|
140
141
|
workers: [{
|
|
@@ -260,7 +261,7 @@ WorkerService.getHotMesh = async (workflowTopic, config, options) => {
|
|
|
260
261
|
return await WorkerService.instances.get(workflowTopic);
|
|
261
262
|
}
|
|
262
263
|
const hotMeshClient = hotmesh_1.HotMeshService.init({
|
|
263
|
-
logLevel: options?.logLevel ??
|
|
264
|
+
logLevel: options?.logLevel ?? enums_1.HMSH_LOGLEVEL,
|
|
264
265
|
appId: config.namespace ?? factory_1.APP_ID,
|
|
265
266
|
engine: { redis: { ...WorkerService.connection } }
|
|
266
267
|
});
|
|
@@ -21,6 +21,7 @@ import { RedisClient, RedisMulti } from '../../types/redis';
|
|
|
21
21
|
import { StringAnyType } from '../../types/serializer';
|
|
22
22
|
import { GetStatsOptions, IdsResponse, JobStatsInput, StatsResponse } from '../../types/stats';
|
|
23
23
|
import { StreamCode, StreamData, StreamDataResponse, StreamError, StreamStatus } from '../../types/stream';
|
|
24
|
+
import { WorkListTaskType } from '../../types/task';
|
|
24
25
|
declare class EngineService {
|
|
25
26
|
namespace: string;
|
|
26
27
|
apps: HotMeshApps | null;
|
|
@@ -65,7 +66,7 @@ declare class EngineService {
|
|
|
65
66
|
interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
|
|
66
67
|
scrub(jobId: string): Promise<void>;
|
|
67
68
|
hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
|
|
68
|
-
hookTime(jobId: string, gId: string, activityId: string, type?:
|
|
69
|
+
hookTime(jobId: string, gId: string, activityId: string, type?: WorkListTaskType): Promise<string | void>;
|
|
69
70
|
hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
|
|
70
71
|
pub(topic: string, data: JobData, context?: JobState): Promise<string>;
|
|
71
72
|
sub(topic: string, callback: JobMessageCallback): Promise<void>;
|
|
@@ -304,11 +304,11 @@ class EngineService {
|
|
|
304
304
|
}
|
|
305
305
|
else if (emit) {
|
|
306
306
|
streamData.status = stream_1.StreamStatus.PENDING;
|
|
307
|
-
streamData.code = enums_1.
|
|
307
|
+
streamData.code = enums_1.HMSH_CODE_PENDING;
|
|
308
308
|
}
|
|
309
309
|
else {
|
|
310
310
|
streamData.status = stream_1.StreamStatus.SUCCESS;
|
|
311
|
-
streamData.code = enums_1.
|
|
311
|
+
streamData.code = enums_1.HMSH_CODE_SUCCESS;
|
|
312
312
|
}
|
|
313
313
|
return (await this.router?.publishMessage(null, streamData));
|
|
314
314
|
}
|
|
@@ -361,7 +361,6 @@ class EngineService {
|
|
|
361
361
|
else if (type === 'expire') {
|
|
362
362
|
return await this.store.expireJob(jobId, 1);
|
|
363
363
|
}
|
|
364
|
-
//'sleep': parse the activityId into parts
|
|
365
364
|
const [aid, ...dimensions] = activityId.split(',');
|
|
366
365
|
const dad = `,${dimensions.join(',')}`;
|
|
367
366
|
const streamData = {
|
|
@@ -430,7 +429,7 @@ class EngineService {
|
|
|
430
429
|
return await this.subscribe.punsubscribe(key_1.KeyType.QUORUM, this.appId, wild);
|
|
431
430
|
}
|
|
432
431
|
//publish and await (returns the job and data (if ready)); throws error with jobid if not
|
|
433
|
-
async pubsub(topic, data, context, timeout = enums_1.
|
|
432
|
+
async pubsub(topic, data, context, timeout = enums_1.HMSH_OTT_WAIT_TIME) {
|
|
434
433
|
context = {
|
|
435
434
|
metadata: {
|
|
436
435
|
ngn: this.guid,
|
|
@@ -453,9 +452,10 @@ class EngineService {
|
|
|
453
452
|
}
|
|
454
453
|
});
|
|
455
454
|
setTimeout(() => {
|
|
455
|
+
//note: job is still active (the subscriber timed out)
|
|
456
456
|
this.delistJobCallback(jobId);
|
|
457
457
|
reject({
|
|
458
|
-
code: enums_1.
|
|
458
|
+
code: enums_1.HMSH_CODE_TIMEOUT,
|
|
459
459
|
message: 'timeout',
|
|
460
460
|
job_id: jobId
|
|
461
461
|
});
|
|
@@ -526,7 +526,7 @@ class EngineService {
|
|
|
526
526
|
* it will be expired immediately.
|
|
527
527
|
*/
|
|
528
528
|
resolveExpires(context, options) {
|
|
529
|
-
return options.expire ?? context.metadata.expire ?? enums_1.
|
|
529
|
+
return options.expire ?? context.metadata.expire ?? enums_1.HMSH_EXPIRE_JOB_SECONDS;
|
|
530
530
|
}
|
|
531
531
|
// ****** GET JOB STATE/COLLATION STATUS BY ID *********
|
|
532
532
|
async getStatus(jobId) {
|
|
@@ -17,8 +17,8 @@ class Router {
|
|
|
17
17
|
this.topic = config.topic;
|
|
18
18
|
this.stream = stream;
|
|
19
19
|
this.store = store;
|
|
20
|
-
this.reclaimDelay = config.reclaimDelay || enums_1.
|
|
21
|
-
this.reclaimCount = config.reclaimCount || enums_1.
|
|
20
|
+
this.reclaimDelay = config.reclaimDelay || enums_1.HMSH_XCLAIM_DELAY_MS;
|
|
21
|
+
this.reclaimCount = config.reclaimCount || enums_1.HMSH_XCLAIM_COUNT;
|
|
22
22
|
this.logger = logger;
|
|
23
23
|
}
|
|
24
24
|
async createGroup(stream, group) {
|
|
@@ -48,7 +48,9 @@ class Router {
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
//randomizer that asymptotes at 150% of `HMSH_BLOCK_TIME_MS`
|
|
52
|
+
const streamDuration = enums_1.HMSH_BLOCK_TIME_MS + Math.round((enums_1.HMSH_BLOCK_TIME_MS * Math.random()));
|
|
53
|
+
const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', streamDuration, 'STREAMS', stream, '>');
|
|
52
54
|
if (this.isStreamMessage(result)) {
|
|
53
55
|
const [[, messages]] = result;
|
|
54
56
|
for (const [id, message] of messages) {
|
|
@@ -70,7 +72,7 @@ class Router {
|
|
|
70
72
|
if (this.shouldConsume && process.env.NODE_ENV !== 'test') {
|
|
71
73
|
this.logger.error(`stream-consume-message-error`, { err, stream, group, consumer });
|
|
72
74
|
this.errorCount++;
|
|
73
|
-
const timeout = Math.min(enums_1.
|
|
75
|
+
const timeout = Math.min(enums_1.HMSH_GRADUATED_INTERVAL_MS * (2 ** this.errorCount), enums_1.HMSH_MAX_TIMEOUT_MS);
|
|
74
76
|
setTimeout(consume.bind(this), timeout);
|
|
75
77
|
}
|
|
76
78
|
}
|
|
@@ -90,7 +92,7 @@ class Router {
|
|
|
90
92
|
telemetry.startStreamSpan(input, this.role);
|
|
91
93
|
output = await this.execStreamLeg(input, stream, id, callback.bind(this));
|
|
92
94
|
if (output?.status === stream_1.StreamStatus.ERROR) {
|
|
93
|
-
telemetry.setStreamError(`Function Status Code ${output.code || enums_1.
|
|
95
|
+
telemetry.setStreamError(`Function Status Code ${output.code || enums_1.HMSH_CODE_UNKNOWN}`);
|
|
94
96
|
}
|
|
95
97
|
this.errorCount = 0;
|
|
96
98
|
}
|
|
@@ -153,7 +155,7 @@ class Router {
|
|
|
153
155
|
const errorCode = output.code.toString();
|
|
154
156
|
const policy = policies?.[errorCode];
|
|
155
157
|
const maxRetries = policy?.[0];
|
|
156
|
-
const tryCount = Math.min(input.metadata.try || 0, enums_1.
|
|
158
|
+
const tryCount = Math.min(input.metadata.try || 0, enums_1.HMSH_MAX_RETRIES);
|
|
157
159
|
//only possible values for maxRetries are 1, 2, 3
|
|
158
160
|
//only possible values for tryCount are 0, 1, 2
|
|
159
161
|
if (maxRetries > tryCount) {
|
|
@@ -168,7 +170,7 @@ class Router {
|
|
|
168
170
|
error.message = err.message;
|
|
169
171
|
}
|
|
170
172
|
else {
|
|
171
|
-
error.message = enums_1.
|
|
173
|
+
error.message = enums_1.HMSH_STATUS_UNKNOWN;
|
|
172
174
|
}
|
|
173
175
|
if (typeof err.stack === 'string') {
|
|
174
176
|
error.stack = err.stack;
|
|
@@ -178,14 +180,14 @@ class Router {
|
|
|
178
180
|
}
|
|
179
181
|
return {
|
|
180
182
|
status: 'error',
|
|
181
|
-
code: enums_1.
|
|
183
|
+
code: enums_1.HMSH_CODE_UNKNOWN,
|
|
182
184
|
metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
|
|
183
185
|
data: error
|
|
184
186
|
};
|
|
185
187
|
}
|
|
186
188
|
structureUnacknowledgedError(input) {
|
|
187
189
|
const message = 'stream message max delivery count exceeded';
|
|
188
|
-
const code = enums_1.
|
|
190
|
+
const code = enums_1.HMSH_CODE_UNACKED;
|
|
189
191
|
const data = { message, code };
|
|
190
192
|
const output = {
|
|
191
193
|
metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
|
|
@@ -198,9 +200,9 @@ class Router {
|
|
|
198
200
|
return output;
|
|
199
201
|
}
|
|
200
202
|
structureError(input, output) {
|
|
201
|
-
const message = output.data?.message ? output.data?.message.toString() : enums_1.
|
|
203
|
+
const message = output.data?.message ? output.data?.message.toString() : enums_1.HMSH_STATUS_UNKNOWN;
|
|
202
204
|
const statusCode = output.code || output.data?.code;
|
|
203
|
-
const code = isNaN(statusCode) ? enums_1.
|
|
205
|
+
const code = isNaN(statusCode) ? enums_1.HMSH_CODE_UNKNOWN : parseInt(statusCode.toString());
|
|
204
206
|
const data = { message, code };
|
|
205
207
|
if (typeof output.data?.error === 'object') {
|
|
206
208
|
data.error = { ...output.data.error };
|
|
@@ -216,7 +218,7 @@ class Router {
|
|
|
216
218
|
for (const instance of [...Router.instances]) {
|
|
217
219
|
instance.stopConsuming();
|
|
218
220
|
}
|
|
219
|
-
await (0, utils_1.sleepFor)(enums_1.
|
|
221
|
+
await (0, utils_1.sleepFor)(enums_1.HMSH_BLOCK_TIME_MS * 2);
|
|
220
222
|
}
|
|
221
223
|
async stopConsuming() {
|
|
222
224
|
this.shouldConsume = false;
|
|
@@ -236,7 +238,7 @@ class Router {
|
|
|
236
238
|
this.throttle = delayInMillis;
|
|
237
239
|
this.logger.info(`stream-throttle-reset`, { delay: this.throttle, topic: this.topic });
|
|
238
240
|
}
|
|
239
|
-
async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = enums_1.
|
|
241
|
+
async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = enums_1.HMSH_XPENDING_COUNT) {
|
|
240
242
|
let pendingMessages = [];
|
|
241
243
|
const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit); //[[ '1688768134881-0', 'testConsumer1', 1017, 1 ]]
|
|
242
244
|
for (const pendingMessageInfo of pendingMessagesInfo) {
|
|
@@ -265,7 +267,7 @@ class Router {
|
|
|
265
267
|
// ii) corrupt hardware/network/transport/etc
|
|
266
268
|
// 3b) system error: Redis unable to accept `xadd` request
|
|
267
269
|
// 4c) system error: Redis unable to accept `xdel`/`xack` request
|
|
268
|
-
this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: enums_1.
|
|
270
|
+
this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: enums_1.HMSH_CODE_UNACKED, count });
|
|
269
271
|
const streamData = reclaimedMessage[0]?.[1]?.[1];
|
|
270
272
|
//fatal risk point 1 of 3): json is corrupt
|
|
271
273
|
const [err, input] = this.parseStreamData(streamData);
|
|
@@ -11,6 +11,7 @@ import { IdsData, JobStatsRange, StatsType } from '../../types/stats';
|
|
|
11
11
|
import { Transitions } from '../../types/transition';
|
|
12
12
|
import { ReclaimedMessageType } from '../../types/stream';
|
|
13
13
|
import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
|
|
14
|
+
import { WorkListTaskType } from '../../types/task';
|
|
14
15
|
interface AbstractRedisClient {
|
|
15
16
|
exec(): any;
|
|
16
17
|
}
|
|
@@ -62,12 +63,16 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
62
63
|
activateAppVersion(id: string, version: string): Promise<boolean>;
|
|
63
64
|
registerAppVersion(appId: string, version: string): Promise<any>;
|
|
64
65
|
/**
|
|
65
|
-
* Registers jobId with
|
|
66
|
-
* when originJobId is interrupted
|
|
67
|
-
* list (added via RPUSH)
|
|
68
|
-
* LPOPed items from the list are likewise expired;
|
|
66
|
+
* Registers the job, `jobId`, with `originJobId`. In the future,
|
|
67
|
+
* when `originJobId` is interrupted/expired, the items in the
|
|
68
|
+
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
69
69
|
*/
|
|
70
|
-
|
|
70
|
+
registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi?: U): Promise<any>;
|
|
71
|
+
/**
|
|
72
|
+
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
73
|
+
* is interrupted/expired.
|
|
74
|
+
*/
|
|
75
|
+
registerSignalDependency(jobId: string, signalKey: string, multi?: U): Promise<any>;
|
|
71
76
|
setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, multi?: U): Promise<any>;
|
|
72
77
|
hGetAllResult(result: any): any;
|
|
73
78
|
getJobStats(jobKeys: string[]): Promise<JobStatsRange>;
|
|
@@ -106,15 +111,15 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
106
111
|
* expired at a future date; options indicate whether this
|
|
107
112
|
* is a standard `expire` or an `interrupt`
|
|
108
113
|
*/
|
|
109
|
-
|
|
114
|
+
registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void>;
|
|
110
115
|
/**
|
|
111
116
|
* registers a hook activity to be awakened (uses ZSET to
|
|
112
117
|
* store the 'sleep group' and LIST to store the events
|
|
113
118
|
* for the given sleep group. Sleep groups are
|
|
114
119
|
* organized into 'n'-second blocks (LISTS))
|
|
115
120
|
*/
|
|
116
|
-
registerTimeHook(jobId: string, gId: string, activityId: string, type:
|
|
117
|
-
|
|
121
|
+
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, multi?: U): Promise<void>;
|
|
122
|
+
getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean>;
|
|
118
123
|
/**
|
|
119
124
|
* when processing time jobs, the target LIST ID returned
|
|
120
125
|
* from the ZSET query can be prefixed to denote what to
|
|
@@ -123,7 +128,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
123
128
|
* generic LIST (lists typically contain target job ids)
|
|
124
129
|
* @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
|
|
125
130
|
*/
|
|
126
|
-
|
|
131
|
+
resolveTaskKeyContext(listKey: string): [('sleep' | 'expire' | 'interrupt' | 'delist'), string];
|
|
127
132
|
/**
|
|
128
133
|
* Interrupts a job and sets sets a job error (410), if 'throw'!=false.
|
|
129
134
|
* This method is called by the engine and not by an activity and is
|
|
@@ -104,7 +104,7 @@ class StoreService {
|
|
|
104
104
|
* check for and process work items in the
|
|
105
105
|
* time and signal task queues.
|
|
106
106
|
*/
|
|
107
|
-
async reserveScoutRole(scoutType, delay = enums_1.
|
|
107
|
+
async reserveScoutRole(scoutType, delay = enums_1.HMSH_SCOUT_INTERVAL_SECONDS) {
|
|
108
108
|
const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
109
109
|
const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`);
|
|
110
110
|
if (this.isSuccessful(success)) {
|
|
@@ -308,16 +308,35 @@ class StoreService {
|
|
|
308
308
|
return await this.redisClient[this.commands.hset](key, payload);
|
|
309
309
|
}
|
|
310
310
|
/**
|
|
311
|
-
* Registers jobId with
|
|
312
|
-
* when originJobId is interrupted
|
|
313
|
-
* list (added via RPUSH)
|
|
314
|
-
* LPOPed items from the list are likewise expired;
|
|
311
|
+
* Registers the job, `jobId`, with `originJobId`. In the future,
|
|
312
|
+
* when `originJobId` is interrupted/expired, the items in the
|
|
313
|
+
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
315
314
|
*/
|
|
316
|
-
async
|
|
315
|
+
async registerJobDependency(originJobId, topic, jobId, gId, multi) {
|
|
317
316
|
const privateMulti = multi || this.getMulti();
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
|
|
317
|
+
const dependencyParams = {
|
|
318
|
+
appId: this.appId,
|
|
319
|
+
jobId: originJobId,
|
|
320
|
+
};
|
|
321
|
+
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
|
|
322
|
+
//tasks have '4' segments
|
|
323
|
+
const expireTask = `expire::${topic}::${gId}::${jobId}`;
|
|
324
|
+
privateMulti[this.commands.rpush](depKey, expireTask);
|
|
325
|
+
if (!multi) {
|
|
326
|
+
return await privateMulti.exec();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Ensures a `hook signal` is delisted when its parent activity/job
|
|
331
|
+
* is interrupted/expired.
|
|
332
|
+
*/
|
|
333
|
+
async registerSignalDependency(jobId, signalKey, multi) {
|
|
334
|
+
const privateMulti = multi || this.getMulti();
|
|
335
|
+
const dependencyParams = { appId: this.appId, jobId };
|
|
336
|
+
const dependencyKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, dependencyParams);
|
|
337
|
+
//tasks have '4' segments
|
|
338
|
+
const delistTask = `delist::signal::${jobId}::${signalKey}`;
|
|
339
|
+
privateMulti[this.commands.rpush](dependencyKey, delistTask);
|
|
321
340
|
if (!multi) {
|
|
322
341
|
return await privateMulti.exec();
|
|
323
342
|
}
|
|
@@ -612,11 +631,11 @@ class StoreService {
|
|
|
612
631
|
}
|
|
613
632
|
async setHookSignal(hook, multi) {
|
|
614
633
|
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
615
|
-
const { topic, resolved, jobId } = hook;
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
return await
|
|
634
|
+
const { topic, resolved, jobId } = hook; //`${activityId}::${dad}::${gId}::${jobId}`
|
|
635
|
+
const signalKey = `${topic}:${resolved}`;
|
|
636
|
+
const payload = { [signalKey]: jobId };
|
|
637
|
+
await (multi || this.redisClient)[this.commands.hset](key, payload);
|
|
638
|
+
return await this.registerSignalDependency(jobId.split('::')[3], signalKey, multi);
|
|
620
639
|
}
|
|
621
640
|
async getHookSignal(topic, resolved) {
|
|
622
641
|
const key = this.mintKey(key_1.KeyType.SIGNALS, { appId: this.appId });
|
|
@@ -677,7 +696,7 @@ class StoreService {
|
|
|
677
696
|
* expired at a future date; options indicate whether this
|
|
678
697
|
* is a standard `expire` or an `interrupt`
|
|
679
698
|
*/
|
|
680
|
-
async
|
|
699
|
+
async registerDependenciesForCleanup(jobId, deletionTime, options) {
|
|
681
700
|
const depParams = { appId: this.appId, jobId };
|
|
682
701
|
const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
|
|
683
702
|
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
@@ -700,21 +719,25 @@ class StoreService {
|
|
|
700
719
|
await this.zAdd(zsetKey, deletionTime.toString(), listKey, multi);
|
|
701
720
|
}
|
|
702
721
|
}
|
|
703
|
-
async
|
|
704
|
-
const existing = Boolean(listKey);
|
|
722
|
+
async getNextTask(listKey) {
|
|
705
723
|
const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
|
|
706
724
|
listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
|
|
707
725
|
if (listKey) {
|
|
708
|
-
|
|
726
|
+
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
709
727
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
710
728
|
if (timeEvent) {
|
|
711
|
-
//there are
|
|
712
|
-
|
|
729
|
+
//there are 4 time-related task
|
|
730
|
+
//1) sleep (awaken), 2) expire, 3) interrupt, 4) delist
|
|
731
|
+
const [type, activityId, gId, ...jobId] = timeEvent.split('::');
|
|
732
|
+
if (type === 'delist') {
|
|
733
|
+
pType = 'delist';
|
|
734
|
+
}
|
|
713
735
|
return [listKey, jobId.join('::'), gId, activityId, pType];
|
|
714
736
|
}
|
|
715
737
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
738
|
+
return true;
|
|
716
739
|
}
|
|
717
|
-
return
|
|
740
|
+
return false;
|
|
718
741
|
}
|
|
719
742
|
/**
|
|
720
743
|
* when processing time jobs, the target LIST ID returned
|
|
@@ -724,7 +747,7 @@ class StoreService {
|
|
|
724
747
|
* generic LIST (lists typically contain target job ids)
|
|
725
748
|
* @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
|
|
726
749
|
*/
|
|
727
|
-
|
|
750
|
+
resolveTaskKeyContext(listKey) {
|
|
728
751
|
if (listKey.startsWith('::INTERRUPT')) {
|
|
729
752
|
return ['interrupt', listKey.split('::')[2]];
|
|
730
753
|
}
|
|
@@ -766,7 +789,7 @@ class StoreService {
|
|
|
766
789
|
this.serializer.resetSymbols(symKeys, symVals, {});
|
|
767
790
|
//persists the standard 410 error (job is `gone`)
|
|
768
791
|
const err = JSON.stringify({
|
|
769
|
-
code: enums_1.
|
|
792
|
+
code: enums_1.HMSH_CODE_INTERRUPT,
|
|
770
793
|
message: options.reason ?? `job [${jobId}] interrupted`,
|
|
771
794
|
job_id: jobId
|
|
772
795
|
});
|
|
@@ -4,6 +4,7 @@ import { StoreService } from '../store';
|
|
|
4
4
|
import { HookInterface, HookRule } from '../../types/hook';
|
|
5
5
|
import { JobCompletionOptions, JobState } from '../../types/job';
|
|
6
6
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
7
|
+
import { WorkListTaskType } from '../../types/task';
|
|
7
8
|
declare class TaskService {
|
|
8
9
|
store: StoreService<RedisClient, RedisMulti>;
|
|
9
10
|
logger: ILogger;
|
|
@@ -13,12 +14,18 @@ declare class TaskService {
|
|
|
13
14
|
processWebHooks(hookEventCallback: HookInterface): Promise<void>;
|
|
14
15
|
enqueueWorkItems(keys: string[]): Promise<void>;
|
|
15
16
|
registerJobForCleanup(jobId: string, inSeconds: number, options: JobCompletionOptions): Promise<void>;
|
|
16
|
-
registerTimeHook(jobId: string, gId: string, activityId: string, type:
|
|
17
|
+
registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, inSeconds?: number, multi?: RedisMulti): Promise<void>;
|
|
17
18
|
/**
|
|
18
|
-
* Should this engine instance play the role of 'scout'
|
|
19
|
+
* Should this engine instance play the role of 'scout' on behalf
|
|
20
|
+
* of the entire quorum? The scout role is responsible for processing
|
|
21
|
+
* task lists on behalf of the collective.
|
|
19
22
|
*/
|
|
20
23
|
shouldScout(): Promise<boolean>;
|
|
21
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Callback handler that takes an item from a work list and
|
|
26
|
+
* processes according to its type
|
|
27
|
+
*/
|
|
28
|
+
processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>, listKey?: string): Promise<void>;
|
|
22
29
|
cancelCleanup(): void;
|
|
23
30
|
getHookRule(topic: string): Promise<HookRule | undefined>;
|
|
24
31
|
registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string>;
|
|
@@ -4,6 +4,7 @@ exports.TaskService = void 0;
|
|
|
4
4
|
const enums_1 = require("../../modules/enums");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
6
|
const pipe_1 = require("../pipe");
|
|
7
|
+
const hotmesh_1 = require("../../types/hotmesh");
|
|
7
8
|
class TaskService {
|
|
8
9
|
constructor(store, logger) {
|
|
9
10
|
this.cleanupTimeout = null;
|
|
@@ -31,19 +32,25 @@ class TaskService {
|
|
|
31
32
|
async enqueueWorkItems(keys) {
|
|
32
33
|
await this.store.addTaskQueues(keys);
|
|
33
34
|
}
|
|
34
|
-
async registerJobForCleanup(jobId, inSeconds = enums_1.
|
|
35
|
+
async registerJobForCleanup(jobId, inSeconds = enums_1.HMSH_EXPIRE_DURATION, options) {
|
|
35
36
|
if (inSeconds > 0) {
|
|
36
37
|
await this.store.expireJob(jobId, inSeconds);
|
|
37
|
-
const
|
|
38
|
-
|
|
38
|
+
const fromNow = Date.now() + (inSeconds * 1000);
|
|
39
|
+
const fidelityMS = enums_1.HMSH_FIDELITY_SECONDS * 1000;
|
|
40
|
+
const timeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
|
|
41
|
+
await this.store.registerDependenciesForCleanup(jobId, timeSlot, options);
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
|
-
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.
|
|
42
|
-
const
|
|
44
|
+
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, multi) {
|
|
45
|
+
const fromNow = Date.now() + (inSeconds * 1000);
|
|
46
|
+
const fidelityMS = enums_1.HMSH_FIDELITY_SECONDS * 1000;
|
|
47
|
+
const awakenTimeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
|
|
43
48
|
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
|
|
44
49
|
}
|
|
45
50
|
/**
|
|
46
|
-
* Should this engine instance play the role of 'scout'
|
|
51
|
+
* Should this engine instance play the role of 'scout' on behalf
|
|
52
|
+
* of the entire quorum? The scout role is responsible for processing
|
|
53
|
+
* task lists on behalf of the collective.
|
|
47
54
|
*/
|
|
48
55
|
async shouldScout() {
|
|
49
56
|
const wasScout = this.isScout;
|
|
@@ -52,31 +59,42 @@ class TaskService {
|
|
|
52
59
|
if (!wasScout) {
|
|
53
60
|
setTimeout(() => {
|
|
54
61
|
this.isScout = false;
|
|
55
|
-
}, enums_1.
|
|
62
|
+
}, enums_1.HMSH_SCOUT_INTERVAL_SECONDS * 1000);
|
|
56
63
|
}
|
|
57
64
|
return true;
|
|
58
65
|
}
|
|
59
66
|
return false;
|
|
60
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Callback handler that takes an item from a work list and
|
|
70
|
+
* processes according to its type
|
|
71
|
+
*/
|
|
61
72
|
async processTimeHooks(timeEventCallback, listKey) {
|
|
62
73
|
if (await this.shouldScout()) {
|
|
63
74
|
try {
|
|
64
|
-
const
|
|
65
|
-
if (Array.isArray(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
const workListTask = await this.store.getNextTask(listKey);
|
|
76
|
+
if (Array.isArray(workListTask)) {
|
|
77
|
+
const [listKey, target, gId, activityId, type] = workListTask;
|
|
78
|
+
if (type === 'delist') {
|
|
79
|
+
//delist the signalKey (target)
|
|
80
|
+
const key = this.store.mintKey(hotmesh_1.KeyType.SIGNALS, { appId: this.store.appId });
|
|
81
|
+
await this.store.redisClient[this.store.commands.hdel](key, target);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
//awaken/expire/interrupt
|
|
85
|
+
await timeEventCallback(target, gId, activityId, type);
|
|
86
|
+
}
|
|
69
87
|
await (0, utils_1.sleepFor)(0);
|
|
70
88
|
this.processTimeHooks(timeEventCallback, listKey);
|
|
71
89
|
}
|
|
72
|
-
else if (
|
|
73
|
-
//a
|
|
90
|
+
else if (workListTask) {
|
|
91
|
+
//a worklist was just emptied; try again immediately
|
|
74
92
|
await (0, utils_1.sleepFor)(0);
|
|
75
93
|
this.processTimeHooks(timeEventCallback);
|
|
76
94
|
}
|
|
77
95
|
else {
|
|
78
|
-
//
|
|
79
|
-
let sleep = (0, utils_1.XSleepFor)(enums_1.
|
|
96
|
+
//no worklists exist; sleep before checking
|
|
97
|
+
let sleep = (0, utils_1.XSleepFor)(enums_1.HMSH_FIDELITY_SECONDS * 1000);
|
|
80
98
|
this.cleanupTimeout = sleep.timerId;
|
|
81
99
|
await sleep.promise;
|
|
82
100
|
this.processTimeHooks(timeEventCallback);
|
|
@@ -89,7 +107,7 @@ class TaskService {
|
|
|
89
107
|
}
|
|
90
108
|
else {
|
|
91
109
|
//didn't get the scout role; try again in 'one-ish' minutes
|
|
92
|
-
let sleep = (0, utils_1.XSleepFor)(enums_1.
|
|
110
|
+
let sleep = (0, utils_1.XSleepFor)(enums_1.HMSH_SCOUT_INTERVAL_SECONDS * 1000 * 2 * Math.random());
|
|
93
111
|
this.cleanupTimeout = sleep.timerId;
|
|
94
112
|
await sleep.promise;
|
|
95
113
|
this.processTimeHooks(timeEventCallback);
|
package/build/types/durable.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LogLevel } from './logger';
|
|
1
2
|
import { RedisClass, RedisOptions } from './redis';
|
|
2
3
|
type WorkflowConfig = {
|
|
3
4
|
backoffCoefficient?: number;
|
|
@@ -151,12 +152,12 @@ type MeshOSActivityOptions = {
|
|
|
151
152
|
type MeshOSWorkerOptions = {
|
|
152
153
|
taskQueue?: string;
|
|
153
154
|
allowList?: Array<MeshOSOptions | string>;
|
|
154
|
-
logLevel?:
|
|
155
|
+
logLevel?: LogLevel;
|
|
155
156
|
maxSystemRetries?: number;
|
|
156
157
|
backoffCoefficient?: number;
|
|
157
158
|
};
|
|
158
159
|
type WorkerOptions = {
|
|
159
|
-
logLevel?:
|
|
160
|
+
logLevel?: LogLevel;
|
|
160
161
|
maxSystemRetries?: number;
|
|
161
162
|
backoffCoefficient?: number;
|
|
162
163
|
};
|
package/build/types/hotmesh.d.ts
CHANGED
|
@@ -3,6 +3,47 @@ import { HotMeshService } from '../services/hotmesh';
|
|
|
3
3
|
import { HookRules } from './hook';
|
|
4
4
|
import { RedisClass, RedisClient, RedisOptions } from './redis';
|
|
5
5
|
import { StreamData, StreamDataResponse } from './stream';
|
|
6
|
+
import { LogLevel } from './logger';
|
|
7
|
+
/**
|
|
8
|
+
* the full set of entity types that are stored in the key/value store
|
|
9
|
+
*/
|
|
10
|
+
declare enum KeyType {
|
|
11
|
+
APP = "APP",
|
|
12
|
+
ENGINE_ID = "ENGINE",
|
|
13
|
+
HOOKS = "HOOKS",
|
|
14
|
+
JOB_DEPENDENTS = "JOB_DEPENDENTS",
|
|
15
|
+
JOB_STATE = "JOB_STATE",
|
|
16
|
+
JOB_STATS_GENERAL = "JOB_STATS_GENERAL",
|
|
17
|
+
JOB_STATS_MEDIAN = "JOB_STATS_MEDIAN",
|
|
18
|
+
JOB_STATS_INDEX = "JOB_STATS_INDEX",
|
|
19
|
+
HOTMESH = "HOTMESH",
|
|
20
|
+
QUORUM = "QUORUM",
|
|
21
|
+
SCHEMAS = "SCHEMAS",
|
|
22
|
+
SIGNALS = "SIGNALS",
|
|
23
|
+
STREAMS = "STREAMS",
|
|
24
|
+
SUBSCRIPTIONS = "SUBSCRIPTIONS",
|
|
25
|
+
SUBSCRIPTION_PATTERNS = "SUBSCRIPTION_PATTERNS",
|
|
26
|
+
SYMKEYS = "SYMKEYS",
|
|
27
|
+
SYMVALS = "SYMVALS",
|
|
28
|
+
TIME_RANGE = "TIME_RANGE",
|
|
29
|
+
WORK_ITEMS = "WORK_ITEMS"
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* minting keys, requires one or more of the following parameters
|
|
33
|
+
*/
|
|
34
|
+
type KeyStoreParams = {
|
|
35
|
+
appId?: string;
|
|
36
|
+
engineId?: string;
|
|
37
|
+
appVersion?: string;
|
|
38
|
+
jobId?: string;
|
|
39
|
+
activityId?: string;
|
|
40
|
+
jobKey?: string;
|
|
41
|
+
dateTime?: string;
|
|
42
|
+
facet?: string;
|
|
43
|
+
topic?: string;
|
|
44
|
+
timeValue?: number;
|
|
45
|
+
scoutType?: 'signal' | 'time';
|
|
46
|
+
};
|
|
6
47
|
type HotMesh = typeof HotMeshService;
|
|
7
48
|
type RedisConfig = {
|
|
8
49
|
class: RedisClass;
|
|
@@ -34,7 +75,7 @@ type HotMeshConfig = {
|
|
|
34
75
|
namespace?: string;
|
|
35
76
|
name?: string;
|
|
36
77
|
logger?: ILogger;
|
|
37
|
-
logLevel?:
|
|
78
|
+
logLevel?: LogLevel;
|
|
38
79
|
engine?: HotMeshEngine;
|
|
39
80
|
workers?: HotMeshWorker[];
|
|
40
81
|
};
|
|
@@ -79,4 +120,4 @@ type HotMeshApps = {
|
|
|
79
120
|
export { HotMesh, HotMeshEngine, RedisConfig, HotMeshWorker, HotMeshSettings, HotMeshApp, //a single app in the db
|
|
80
121
|
HotMeshApps, //object array of all apps in the db
|
|
81
122
|
HotMeshConfig, //customer config
|
|
82
|
-
HotMeshManifest, HotMeshGraph };
|
|
123
|
+
HotMeshManifest, HotMeshGraph, KeyType, KeyStoreParams, };
|
package/build/types/hotmesh.js
CHANGED
|
@@ -1,2 +1,30 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyType = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* the full set of entity types that are stored in the key/value store
|
|
6
|
+
*/
|
|
7
|
+
var KeyType;
|
|
8
|
+
(function (KeyType) {
|
|
9
|
+
KeyType["APP"] = "APP";
|
|
10
|
+
KeyType["ENGINE_ID"] = "ENGINE";
|
|
11
|
+
KeyType["HOOKS"] = "HOOKS";
|
|
12
|
+
KeyType["JOB_DEPENDENTS"] = "JOB_DEPENDENTS";
|
|
13
|
+
KeyType["JOB_STATE"] = "JOB_STATE";
|
|
14
|
+
KeyType["JOB_STATS_GENERAL"] = "JOB_STATS_GENERAL";
|
|
15
|
+
KeyType["JOB_STATS_MEDIAN"] = "JOB_STATS_MEDIAN";
|
|
16
|
+
KeyType["JOB_STATS_INDEX"] = "JOB_STATS_INDEX";
|
|
17
|
+
KeyType["HOTMESH"] = "HOTMESH";
|
|
18
|
+
KeyType["QUORUM"] = "QUORUM";
|
|
19
|
+
KeyType["SCHEMAS"] = "SCHEMAS";
|
|
20
|
+
KeyType["SIGNALS"] = "SIGNALS";
|
|
21
|
+
KeyType["STREAMS"] = "STREAMS";
|
|
22
|
+
KeyType["SUBSCRIPTIONS"] = "SUBSCRIPTIONS";
|
|
23
|
+
KeyType["SUBSCRIPTION_PATTERNS"] = "SUBSCRIPTION_PATTERNS";
|
|
24
|
+
KeyType["SYMKEYS"] = "SYMKEYS";
|
|
25
|
+
KeyType["SYMVALS"] = "SYMVALS";
|
|
26
|
+
KeyType["TIME_RANGE"] = "TIME_RANGE";
|
|
27
|
+
KeyType["WORK_ITEMS"] = "WORK_ITEMS";
|
|
28
|
+
})(KeyType || (KeyType = {}));
|
|
29
|
+
exports.KeyType = KeyType;
|
|
30
|
+
;
|