@hotmeshio/hotmesh 0.0.38 → 0.0.40
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 +3 -3
- 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 +3 -3
- 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
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
|
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;
|
|
@@ -177,7 +178,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
177
178
|
* check for and process work items in the
|
|
178
179
|
* time and signal task queues.
|
|
179
180
|
*/
|
|
180
|
-
async reserveScoutRole(scoutType: 'time' | 'signal', delay =
|
|
181
|
+
async reserveScoutRole(scoutType: 'time' | 'signal', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
|
|
181
182
|
const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
182
183
|
const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
|
|
183
184
|
if (this.isSuccessful(success)) {
|
|
@@ -388,16 +389,45 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
388
389
|
}
|
|
389
390
|
|
|
390
391
|
/**
|
|
391
|
-
* Registers jobId with
|
|
392
|
-
* when originJobId is interrupted
|
|
393
|
-
* list (added via RPUSH)
|
|
394
|
-
* 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).
|
|
395
395
|
*/
|
|
396
|
-
async
|
|
396
|
+
async registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
397
397
|
const privateMulti = multi || this.getMulti();
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
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
|
+
);
|
|
401
431
|
if (!multi) {
|
|
402
432
|
return await privateMulti.exec();
|
|
403
433
|
}
|
|
@@ -710,11 +740,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
710
740
|
|
|
711
741
|
async setHookSignal(hook: HookSignal, multi?: U): Promise<any> {
|
|
712
742
|
const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
|
|
713
|
-
const { topic, resolved, jobId} = hook;
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
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);
|
|
718
748
|
}
|
|
719
749
|
|
|
720
750
|
async getHookSignal(topic: string, resolved: string): Promise<string | undefined> {
|
|
@@ -782,7 +812,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
782
812
|
* expired at a future date; options indicate whether this
|
|
783
813
|
* is a standard `expire` or an `interrupt`
|
|
784
814
|
*/
|
|
785
|
-
async
|
|
815
|
+
async registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
|
|
786
816
|
const depParams = { appId: this.appId, jobId };
|
|
787
817
|
const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
|
|
788
818
|
const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
|
|
@@ -797,7 +827,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
797
827
|
* for the given sleep group. Sleep groups are
|
|
798
828
|
* organized into 'n'-second blocks (LISTS))
|
|
799
829
|
*/
|
|
800
|
-
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> {
|
|
801
831
|
const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
802
832
|
const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
|
|
803
833
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
@@ -807,21 +837,25 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
807
837
|
}
|
|
808
838
|
}
|
|
809
839
|
|
|
810
|
-
async
|
|
811
|
-
const existing = Boolean(listKey);
|
|
840
|
+
async getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean> {
|
|
812
841
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
813
842
|
listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
|
|
814
843
|
if (listKey) {
|
|
815
|
-
|
|
844
|
+
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
816
845
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
817
846
|
if (timeEvent) {
|
|
818
|
-
//there are
|
|
819
|
-
|
|
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
|
+
}
|
|
820
853
|
return [listKey, jobId.join('::'), gId, activityId, pType];
|
|
821
854
|
}
|
|
822
855
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
856
|
+
return true;
|
|
823
857
|
}
|
|
824
|
-
return
|
|
858
|
+
return false;
|
|
825
859
|
}
|
|
826
860
|
|
|
827
861
|
/**
|
|
@@ -832,7 +866,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
832
866
|
* generic LIST (lists typically contain target job ids)
|
|
833
867
|
* @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
|
|
834
868
|
*/
|
|
835
|
-
|
|
869
|
+
resolveTaskKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'|'delist'), string] {
|
|
836
870
|
if (listKey.startsWith('::INTERRUPT')) {
|
|
837
871
|
return ['interrupt', listKey.split('::')[2]];
|
|
838
872
|
} else if (listKey.startsWith('::EXPIRE')) {
|
|
@@ -874,7 +908,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
874
908
|
|
|
875
909
|
//persists the standard 410 error (job is `gone`)
|
|
876
910
|
const err = JSON.stringify({
|
|
877
|
-
code:
|
|
911
|
+
code: HMSH_CODE_INTERRUPT,
|
|
878
912
|
message: options.reason ?? `job [${jobId}] interrupted`,
|
|
879
913
|
job_id: jobId
|
|
880
914
|
});
|
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);
|
package/types/durable.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LogLevel } from './logger';
|
|
1
2
|
import { RedisClass, RedisOptions } from './redis';
|
|
2
3
|
|
|
3
4
|
type WorkflowConfig = {
|
|
@@ -173,16 +174,16 @@ type MeshOSActivityOptions = {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
type MeshOSWorkerOptions = {
|
|
176
|
-
taskQueue?: string;
|
|
177
|
+
taskQueue?: string; //change the default task queue
|
|
177
178
|
allowList?: Array<MeshOSOptions | string>; //limit which `hook` and `workflow` workers start
|
|
178
|
-
logLevel?:
|
|
179
|
-
maxSystemRetries?: number;
|
|
179
|
+
logLevel?: LogLevel; //debug, info, warn, error
|
|
180
|
+
maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
|
|
180
181
|
backoffCoefficient?: number; //2-10ish
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
type WorkerOptions = {
|
|
184
|
-
logLevel?:
|
|
185
|
-
maxSystemRetries?: number;
|
|
185
|
+
logLevel?: LogLevel; //debug, info, warn, error
|
|
186
|
+
maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
|
|
186
187
|
backoffCoefficient?: number; //2-10ish
|
|
187
188
|
}
|
|
188
189
|
|
package/types/hotmesh.ts
CHANGED
|
@@ -3,6 +3,49 @@ 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
|
+
/**
|
|
9
|
+
* the full set of entity types that are stored in the key/value store
|
|
10
|
+
*/
|
|
11
|
+
enum KeyType {
|
|
12
|
+
APP = 'APP',
|
|
13
|
+
ENGINE_ID = 'ENGINE',
|
|
14
|
+
HOOKS = 'HOOKS',
|
|
15
|
+
JOB_DEPENDENTS = 'JOB_DEPENDENTS',
|
|
16
|
+
JOB_STATE = 'JOB_STATE',
|
|
17
|
+
JOB_STATS_GENERAL = 'JOB_STATS_GENERAL',
|
|
18
|
+
JOB_STATS_MEDIAN = 'JOB_STATS_MEDIAN',
|
|
19
|
+
JOB_STATS_INDEX = 'JOB_STATS_INDEX',
|
|
20
|
+
HOTMESH = 'HOTMESH',
|
|
21
|
+
QUORUM = 'QUORUM',
|
|
22
|
+
SCHEMAS = 'SCHEMAS',
|
|
23
|
+
SIGNALS = 'SIGNALS',
|
|
24
|
+
STREAMS = 'STREAMS',
|
|
25
|
+
SUBSCRIPTIONS = 'SUBSCRIPTIONS',
|
|
26
|
+
SUBSCRIPTION_PATTERNS = 'SUBSCRIPTION_PATTERNS',
|
|
27
|
+
SYMKEYS = 'SYMKEYS',
|
|
28
|
+
SYMVALS = 'SYMVALS',
|
|
29
|
+
TIME_RANGE = 'TIME_RANGE',
|
|
30
|
+
WORK_ITEMS = 'WORK_ITEMS',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* minting keys, requires one or more of the following parameters
|
|
35
|
+
*/
|
|
36
|
+
type KeyStoreParams = {
|
|
37
|
+
appId?: string; //app id is a uuid for a hotmesh app
|
|
38
|
+
engineId?: string; //unique auto-generated guid for an ephemeral engine instance
|
|
39
|
+
appVersion?: string; //(e.g. "1.0.0", "1", "1.0")
|
|
40
|
+
jobId?: string; //a customer-defined id for job; must be unique for the entire app
|
|
41
|
+
activityId?: string; //activity id is a uuid for a given hotmesh app
|
|
42
|
+
jobKey?: string; //a customer-defined label for a job that serves to categorize events
|
|
43
|
+
dateTime?: string; //UTC date time: YYYY-MM-DDTHH:MM (20203-04-12T00:00); serves as a time-series bucket for the job_key
|
|
44
|
+
facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
|
|
45
|
+
topic?: string; //topic name (e.g., "foo" or "" for top-level)
|
|
46
|
+
timeValue?: number; //time value (rounded to minute) (for delete range)
|
|
47
|
+
scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
|
|
48
|
+
};
|
|
6
49
|
|
|
7
50
|
type HotMesh = typeof HotMeshService;
|
|
8
51
|
|
|
@@ -39,7 +82,7 @@ type HotMeshConfig = {
|
|
|
39
82
|
namespace?: string;
|
|
40
83
|
name?: string;
|
|
41
84
|
logger?: ILogger;
|
|
42
|
-
logLevel?:
|
|
85
|
+
logLevel?: LogLevel;
|
|
43
86
|
engine?: HotMeshEngine;
|
|
44
87
|
workers?: HotMeshWorker[];
|
|
45
88
|
}
|
|
@@ -98,5 +141,7 @@ export {
|
|
|
98
141
|
HotMeshApps, //object array of all apps in the db
|
|
99
142
|
HotMeshConfig, //customer config
|
|
100
143
|
HotMeshManifest,
|
|
101
|
-
HotMeshGraph
|
|
144
|
+
HotMeshGraph,
|
|
145
|
+
KeyType,
|
|
146
|
+
KeyStoreParams,
|
|
102
147
|
};
|
package/types/index.ts
CHANGED
|
@@ -90,7 +90,9 @@ export {
|
|
|
90
90
|
HotMeshGraph,
|
|
91
91
|
HotMeshManifest,
|
|
92
92
|
HotMeshSettings,
|
|
93
|
-
HotMeshWorker
|
|
93
|
+
HotMeshWorker,
|
|
94
|
+
KeyStoreParams,
|
|
95
|
+
KeyType } from './hotmesh';
|
|
94
96
|
export {
|
|
95
97
|
ActivateMessage,
|
|
96
98
|
JobMessage,
|
|
@@ -98,6 +100,8 @@ export {
|
|
|
98
100
|
PingMessage,
|
|
99
101
|
PongMessage,
|
|
100
102
|
QuorumMessage,
|
|
103
|
+
QuorumMessageCallback,
|
|
104
|
+
QuorumProfile,
|
|
101
105
|
SubscriptionCallback,
|
|
102
106
|
ThrottleMessage,
|
|
103
107
|
WorkMessage } from './quorum';
|
|
@@ -161,7 +165,10 @@ export {
|
|
|
161
165
|
trace,
|
|
162
166
|
Tracer,
|
|
163
167
|
ValueType } from './telemetry';
|
|
168
|
+
export {
|
|
169
|
+
WorkListTaskType } from './task'
|
|
164
170
|
export {
|
|
165
171
|
TransitionMatch,
|
|
166
172
|
TransitionRule,
|
|
167
173
|
Transitions } from './transition';
|
|
174
|
+
|
package/types/logger.ts
CHANGED
package/types/task.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type WorkListTaskType = 'sleep' | 'expire' | 'interrupt' | 'delist';
|