@hotmeshio/hotmesh 0.0.35 → 0.0.37
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 +12 -12
- package/build/modules/enums.d.ts +1 -0
- package/build/modules/enums.js +3 -1
- package/build/modules/errors.d.ts +9 -1
- package/build/modules/errors.js +12 -1
- package/build/modules/key.d.ts +20 -19
- package/build/modules/key.js +20 -20
- package/build/package.json +1 -1
- package/build/services/activities/activity.d.ts +10 -0
- package/build/services/activities/activity.js +28 -3
- package/build/services/activities/await.js +10 -9
- package/build/services/activities/cycle.js +10 -9
- package/build/services/activities/hook.d.ts +7 -1
- package/build/services/activities/hook.js +61 -44
- package/build/services/activities/interrupt.js +10 -9
- package/build/services/activities/signal.js +7 -7
- package/build/services/activities/trigger.js +4 -2
- package/build/services/activities/worker.js +9 -8
- package/build/services/durable/meshos.js +2 -2
- package/build/services/durable/worker.js +2 -2
- package/build/services/durable/workflow.js +17 -17
- package/build/services/engine/index.d.ts +5 -7
- package/build/services/engine/index.js +53 -47
- package/build/services/hotmesh/index.js +3 -3
- package/build/services/{signaler/stream.d.ts → router/index.d.ts} +3 -3
- package/build/services/{signaler/stream.js → router/index.js} +6 -6
- package/build/services/serializer/index.js +1 -1
- package/build/services/store/clients/ioredis.js +1 -0
- package/build/services/store/index.d.ts +9 -4
- package/build/services/store/index.js +21 -10
- package/build/services/task/index.d.ts +13 -4
- package/build/services/task/index.js +115 -17
- package/build/services/telemetry/index.js +6 -6
- package/build/services/worker/index.d.ts +3 -3
- package/build/services/worker/index.js +8 -8
- package/build/types/job.d.ts +2 -0
- package/build/types/stream.d.ts +1 -0
- package/modules/enums.ts +4 -1
- package/modules/errors.ts +18 -0
- package/modules/key.ts +21 -20
- package/package.json +1 -1
- package/services/activities/activity.ts +44 -4
- package/services/activities/await.ts +14 -10
- package/services/activities/cycle.ts +14 -10
- package/services/activities/hook.ts +70 -47
- package/services/activities/interrupt.ts +13 -10
- package/services/activities/signal.ts +11 -8
- package/services/activities/trigger.ts +5 -1
- package/services/activities/worker.ts +13 -9
- package/services/durable/meshos.ts +1 -1
- package/services/durable/worker.ts +1 -1
- package/services/durable/workflow.ts +1 -1
- package/services/engine/index.ts +82 -44
- package/services/hotmesh/index.ts +3 -3
- package/services/{signaler/stream.ts → router/index.ts} +5 -5
- package/services/serializer/index.ts +1 -1
- package/services/store/clients/ioredis.ts +1 -0
- package/services/store/index.ts +23 -12
- package/services/task/index.ts +120 -21
- package/services/telemetry/index.ts +6 -6
- package/services/worker/index.ts +7 -7
- package/types/job.ts +2 -0
- package/types/stream.ts +6 -5
- package/build/services/signaler/store.d.ts +0 -15
- package/build/services/signaler/store.js +0 -68
- package/services/signaler/store.ts +0 -76
- /package/build/{services/durable/asyncLocalStorage.d.ts → modules/storage.d.ts} +0 -0
- /package/build/{services/durable/asyncLocalStorage.js → modules/storage.js} +0 -0
- /package/{services/durable/asyncLocalStorage.ts → modules/storage.ts} +0 -0
package/services/engine/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
STATUS_CODE_SUCCESS,
|
|
5
5
|
STATUS_CODE_PENDING,
|
|
6
6
|
STATUS_CODE_TIMEOUT,
|
|
7
|
-
DURABLE_EXPIRE_SECONDS} from '../../modules/enums';
|
|
7
|
+
DURABLE_EXPIRE_SECONDS } from '../../modules/enums';
|
|
8
8
|
import {
|
|
9
9
|
formatISODate,
|
|
10
10
|
getSubscriptionTopic,
|
|
@@ -23,9 +23,8 @@ import { Trigger } from '../activities/trigger';
|
|
|
23
23
|
import { CompilerService } from '../compiler';
|
|
24
24
|
import { ILogger } from '../logger';
|
|
25
25
|
import { ReporterService } from '../reporter';
|
|
26
|
+
import { Router } from '../router';
|
|
26
27
|
import { SerializerService } from '../serializer';
|
|
27
|
-
import { StoreSignaler } from '../signaler/store';
|
|
28
|
-
import { StreamSignaler } from '../signaler/stream';
|
|
29
28
|
import { StoreService } from '../store';
|
|
30
29
|
import { RedisStoreService as RedisStore } from '../store/clients/redis';
|
|
31
30
|
import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
|
|
@@ -84,12 +83,11 @@ class EngineService {
|
|
|
84
83
|
apps: HotMeshApps | null;
|
|
85
84
|
appId: string;
|
|
86
85
|
guid: string;
|
|
86
|
+
router: Router | null;
|
|
87
87
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
88
88
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
89
89
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
90
|
-
|
|
91
|
-
streamSignaler: StreamSignaler | null;
|
|
92
|
-
task: TaskService | null;
|
|
90
|
+
taskService: TaskService | null;
|
|
93
91
|
logger: ILogger;
|
|
94
92
|
cacheMode: CacheMode = 'cache';
|
|
95
93
|
untilVersion: string | null = null;
|
|
@@ -110,9 +108,9 @@ class EngineService {
|
|
|
110
108
|
await instance.initStoreChannel(config.engine.store);
|
|
111
109
|
await instance.initSubChannel(config.engine.sub);
|
|
112
110
|
await instance.initStreamChannel(config.engine.stream);
|
|
113
|
-
instance.
|
|
111
|
+
instance.router = instance.initRouter(config);
|
|
114
112
|
|
|
115
|
-
instance.
|
|
113
|
+
instance.router.consumeMessages(
|
|
116
114
|
instance.stream.mintKey(
|
|
117
115
|
KeyType.STREAMS,
|
|
118
116
|
{ appId: instance.appId },
|
|
@@ -122,12 +120,8 @@ class EngineService {
|
|
|
122
120
|
instance.processStreamMessage.bind(instance)
|
|
123
121
|
);
|
|
124
122
|
|
|
125
|
-
//the storeSignaler service is used by the engine to create `webhooks`
|
|
126
|
-
//todo: unify/move to the task service (it manages all `signal` types)
|
|
127
|
-
instance.storeSignaler = new StoreSignaler(instance.store, logger);
|
|
128
|
-
|
|
129
123
|
//the task service is used by the engine to process `webhooks` and `timehooks`
|
|
130
|
-
instance.
|
|
124
|
+
instance.taskService = new TaskService(instance.store, logger);
|
|
131
125
|
|
|
132
126
|
return instance;
|
|
133
127
|
}
|
|
@@ -181,8 +175,8 @@ class EngineService {
|
|
|
181
175
|
);
|
|
182
176
|
}
|
|
183
177
|
|
|
184
|
-
|
|
185
|
-
return new
|
|
178
|
+
initRouter(config: HotMeshConfig): Router {
|
|
179
|
+
return new Router(
|
|
186
180
|
{
|
|
187
181
|
namespace: this.namespace,
|
|
188
182
|
appId: this.appId,
|
|
@@ -231,21 +225,20 @@ class EngineService {
|
|
|
231
225
|
}
|
|
232
226
|
|
|
233
227
|
async processWebHooks() {
|
|
234
|
-
this.
|
|
228
|
+
this.taskService.processWebHooks((this.hook).bind(this));
|
|
235
229
|
}
|
|
236
230
|
|
|
237
231
|
async processTimeHooks() {
|
|
238
|
-
this.
|
|
232
|
+
this.taskService.processTimeHooks((this.hookTime).bind(this));
|
|
239
233
|
}
|
|
240
234
|
|
|
241
235
|
async throttle(delayInMillis: number) {
|
|
242
|
-
this.
|
|
236
|
+
this.router.setThrottle(delayInMillis);
|
|
243
237
|
}
|
|
244
238
|
|
|
245
239
|
// ************* METADATA/MODEL METHODS *************
|
|
246
240
|
async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Await|Cycle|Hook|Signal|Trigger|Worker|Interrupt> {
|
|
247
241
|
const [activityId, schema] = await this.getSchema(topic);
|
|
248
|
-
polyfill
|
|
249
242
|
const ActivityHandler = Activities[polyfill.resolveActivityType(schema.type)];
|
|
250
243
|
if (ActivityHandler) {
|
|
251
244
|
const utc = formatISODate(new Date());
|
|
@@ -329,6 +322,7 @@ class EngineService {
|
|
|
329
322
|
async processStreamMessage(streamData: StreamDataResponse): Promise<void> {
|
|
330
323
|
this.logger.debug('engine-process-stream-message', {
|
|
331
324
|
jid: streamData.metadata.jid,
|
|
325
|
+
gid: streamData.metadata.gid,
|
|
332
326
|
dad: streamData.metadata.dad,
|
|
333
327
|
aid: streamData.metadata.aid,
|
|
334
328
|
status: streamData.status || StreamStatus.SUCCESS,
|
|
@@ -338,41 +332,83 @@ class EngineService {
|
|
|
338
332
|
const context: PartialJobState = {
|
|
339
333
|
metadata: {
|
|
340
334
|
jid: streamData.metadata.jid,
|
|
335
|
+
gid: streamData.metadata.gid,
|
|
341
336
|
dad: streamData.metadata.dad,
|
|
342
337
|
aid: streamData.metadata.aid,
|
|
343
338
|
},
|
|
344
339
|
data: streamData.data,
|
|
345
340
|
};
|
|
346
|
-
if (streamData.type === StreamDataType.TIMEHOOK
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
341
|
+
if (streamData.type === StreamDataType.TIMEHOOK) {
|
|
342
|
+
//TIMEHOOK AWAKEN
|
|
343
|
+
const activityHandler = await this.initActivity(
|
|
344
|
+
`.${streamData.metadata.aid}`,
|
|
345
|
+
context.data,
|
|
346
|
+
context as JobState,
|
|
347
|
+
) as Hook;
|
|
348
|
+
await activityHandler.processTimeHookEvent(streamData.metadata.jid);
|
|
349
|
+
} else if (streamData.type === StreamDataType.WEBHOOK) {
|
|
350
|
+
//WEBHOOK AWAKEN (SIGNAL IN)
|
|
351
|
+
const activityHandler = await this.initActivity(
|
|
352
|
+
`.${streamData.metadata.aid}`,
|
|
353
|
+
context.data,
|
|
354
|
+
context as JobState,
|
|
355
|
+
) as Hook;
|
|
356
|
+
await activityHandler.processWebHookEvent(
|
|
357
|
+
streamData.status,
|
|
358
|
+
streamData.code
|
|
359
|
+
);
|
|
360
|
+
} else if (streamData.type === StreamDataType.TRANSITION) {
|
|
361
|
+
//TRANSITION (ADJACENT ACTIVITY)
|
|
362
|
+
const activityHandler = await this.initActivity(
|
|
363
|
+
`.${streamData.metadata.aid}`,
|
|
364
|
+
context.data,
|
|
365
|
+
context as JobState,
|
|
366
|
+
) as Hook; //todo: `as Activity` (type is more generic)
|
|
367
|
+
await activityHandler.process();
|
|
356
368
|
} else if (streamData.type === StreamDataType.AWAIT) {
|
|
369
|
+
//TRIGGER JOB
|
|
357
370
|
context.metadata = {
|
|
358
371
|
...context.metadata,
|
|
359
372
|
pj: streamData.metadata.jid,
|
|
373
|
+
pg: streamData.metadata.gid,
|
|
360
374
|
pd: streamData.metadata.dad,
|
|
361
375
|
pa: streamData.metadata.aid,
|
|
362
376
|
trc: streamData.metadata.trc,
|
|
363
377
|
spn: streamData.metadata.spn,
|
|
364
378
|
};
|
|
365
|
-
const activityHandler = await this.initActivity(
|
|
379
|
+
const activityHandler = await this.initActivity(
|
|
380
|
+
streamData.metadata.topic,
|
|
381
|
+
streamData.data,
|
|
382
|
+
context as JobState
|
|
383
|
+
) as Trigger;
|
|
366
384
|
await activityHandler.process();
|
|
367
385
|
} else if (streamData.type === StreamDataType.RESULT) {
|
|
368
|
-
|
|
369
|
-
await
|
|
386
|
+
//AWAIT RESULT
|
|
387
|
+
const activityHandler = await this.initActivity(
|
|
388
|
+
`.${context.metadata.aid}`,
|
|
389
|
+
streamData.data,
|
|
390
|
+
context as JobState,
|
|
391
|
+
) as Await;
|
|
392
|
+
await activityHandler.processEvent(
|
|
393
|
+
streamData.status,
|
|
394
|
+
streamData.code,
|
|
395
|
+
);
|
|
370
396
|
} else {
|
|
371
|
-
|
|
372
|
-
await
|
|
397
|
+
//WORKER RESULT
|
|
398
|
+
const activityHandler = await this.initActivity(
|
|
399
|
+
`.${streamData.metadata.aid}`,
|
|
400
|
+
streamData.data,
|
|
401
|
+
context as JobState,
|
|
402
|
+
) as Worker;
|
|
403
|
+
await activityHandler.processEvent(
|
|
404
|
+
streamData.status,
|
|
405
|
+
streamData.code,
|
|
406
|
+
'output'
|
|
407
|
+
);
|
|
373
408
|
}
|
|
374
409
|
this.logger.debug('engine-process-stream-message-end', {
|
|
375
410
|
jid: streamData.metadata.jid,
|
|
411
|
+
gid: streamData.metadata.gid,
|
|
376
412
|
aid: streamData.metadata.aid
|
|
377
413
|
});
|
|
378
414
|
}
|
|
@@ -387,6 +423,7 @@ class EngineService {
|
|
|
387
423
|
metadata: {
|
|
388
424
|
guid: guid(),
|
|
389
425
|
jid: context.metadata.pj,
|
|
426
|
+
gid: context.metadata.pg,
|
|
390
427
|
dad: context.metadata.pd,
|
|
391
428
|
aid: context.metadata.pa,
|
|
392
429
|
trc: context.metadata.trc,
|
|
@@ -406,7 +443,7 @@ class EngineService {
|
|
|
406
443
|
streamData.status = StreamStatus.SUCCESS;
|
|
407
444
|
streamData.code = STATUS_CODE_SUCCESS;
|
|
408
445
|
}
|
|
409
|
-
return (await this.
|
|
446
|
+
return (await this.router?.publishMessage(null, streamData)) as string;
|
|
410
447
|
}
|
|
411
448
|
}
|
|
412
449
|
hasParentJob(context: JobState): boolean {
|
|
@@ -437,7 +474,7 @@ class EngineService {
|
|
|
437
474
|
|
|
438
475
|
// ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
|
|
439
476
|
async hook(topic: string, data: JobData, status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<string> {
|
|
440
|
-
const hookRule = await this.
|
|
477
|
+
const hookRule = await this.taskService.getHookRule(topic);
|
|
441
478
|
const [aid] = await this.getSchema(`.${hookRule.to}`);
|
|
442
479
|
const streamData: StreamData = {
|
|
443
480
|
type: StreamDataType.WEBHOOK,
|
|
@@ -450,9 +487,9 @@ class EngineService {
|
|
|
450
487
|
},
|
|
451
488
|
data,
|
|
452
489
|
};
|
|
453
|
-
return await this.
|
|
490
|
+
return await this.router.publishMessage(null, streamData) as string;
|
|
454
491
|
}
|
|
455
|
-
async hookTime(jobId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
|
|
492
|
+
async hookTime(jobId: string, gId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
|
|
456
493
|
if (type === 'interrupt') {
|
|
457
494
|
return await this.interrupt(
|
|
458
495
|
activityId, //note: 'activityId' is the actually job topic
|
|
@@ -470,16 +507,17 @@ class EngineService {
|
|
|
470
507
|
metadata: {
|
|
471
508
|
guid: guid(),
|
|
472
509
|
jid: jobId,
|
|
473
|
-
|
|
510
|
+
gid: gId,
|
|
474
511
|
dad,
|
|
512
|
+
aid,
|
|
475
513
|
},
|
|
476
514
|
data: { timestamp: Date.now() },
|
|
477
515
|
};
|
|
478
|
-
await this.
|
|
516
|
+
await this.router.publishMessage(null, streamData);
|
|
479
517
|
}
|
|
480
518
|
async hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets: string[] = []): Promise<string[]> {
|
|
481
519
|
const config = await this.getVID();
|
|
482
|
-
const hookRule = await this.
|
|
520
|
+
const hookRule = await this.taskService.getHookRule(hookTopic);
|
|
483
521
|
if (hookRule) {
|
|
484
522
|
const subscriptionTopic = await getSubscriptionTopic(hookRule.to, this.store, config)
|
|
485
523
|
const resolvedQuery = await this.resolveQuery(subscriptionTopic, keyResolver);
|
|
@@ -597,7 +635,7 @@ class EngineService {
|
|
|
597
635
|
}
|
|
598
636
|
}
|
|
599
637
|
async add(streamData: StreamData|StreamDataResponse): Promise<string> {
|
|
600
|
-
return await this.
|
|
638
|
+
return await this.router.publishMessage(null, streamData) as string;
|
|
601
639
|
}
|
|
602
640
|
|
|
603
641
|
registerJobCallback(jobId: string, jobCallback: JobMessageCallback) {
|
|
@@ -632,7 +670,7 @@ class EngineService {
|
|
|
632
670
|
this.pubPermSubs(context, jobOutput, options.emit);
|
|
633
671
|
}
|
|
634
672
|
if (!options.emit) {
|
|
635
|
-
this.
|
|
673
|
+
this.taskService.registerJobForCleanup(
|
|
636
674
|
context.metadata.jid,
|
|
637
675
|
this.resolveExpires(context, options),
|
|
638
676
|
options,
|
|
@@ -647,7 +685,7 @@ class EngineService {
|
|
|
647
685
|
* it will be expired immediately.
|
|
648
686
|
*/
|
|
649
687
|
resolveExpires(context: JobState, options: JobCompletionOptions): number {
|
|
650
|
-
return
|
|
688
|
+
return options.expire ?? context.metadata.expire ?? DURABLE_EXPIRE_SECONDS;
|
|
651
689
|
}
|
|
652
690
|
|
|
653
691
|
|
|
@@ -4,8 +4,8 @@ import { RedisConnection } from '../connector/clients/redis';
|
|
|
4
4
|
import { RedisConnection as IORedisConnection } from '../connector/clients/ioredis';
|
|
5
5
|
import { EngineService } from '../engine';
|
|
6
6
|
import { LoggerService, ILogger } from '../logger';
|
|
7
|
-
import { StreamSignaler } from '../signaler/stream';
|
|
8
7
|
import { QuorumService } from '../quorum';
|
|
8
|
+
import { Router } from '../router';
|
|
9
9
|
import { WorkerService } from '../worker';
|
|
10
10
|
import {
|
|
11
11
|
JobState,
|
|
@@ -193,14 +193,14 @@ class HotMeshService {
|
|
|
193
193
|
static async stop() {
|
|
194
194
|
if (!this.disconnecting) {
|
|
195
195
|
this.disconnecting = true;
|
|
196
|
-
await
|
|
196
|
+
await Router.stopConsuming();
|
|
197
197
|
await RedisConnection.disconnectAll();
|
|
198
198
|
await IORedisConnection.disconnectAll();
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
stop() {
|
|
203
|
-
this.engine?.
|
|
203
|
+
this.engine?.taskService.cancelCleanup();
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
async compress(terms: string[]): Promise<boolean> {
|
|
@@ -27,8 +27,8 @@ import {
|
|
|
27
27
|
StreamStatus
|
|
28
28
|
} from '../../types/stream';
|
|
29
29
|
|
|
30
|
-
class
|
|
31
|
-
static
|
|
30
|
+
class Router {
|
|
31
|
+
static instances: Set<Router> = new Set();
|
|
32
32
|
appId: string;
|
|
33
33
|
guid: string;
|
|
34
34
|
role: StreamRole;
|
|
@@ -70,7 +70,7 @@ class StreamSignaler {
|
|
|
70
70
|
|
|
71
71
|
async consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse|void>): Promise<void> {
|
|
72
72
|
this.logger.info(`stream-consumer-starting`, { group, consumer, stream });
|
|
73
|
-
|
|
73
|
+
Router.instances.add(this);
|
|
74
74
|
this.shouldConsume = true;
|
|
75
75
|
await this.createGroup(stream, group);
|
|
76
76
|
let lastCheckedPendingMessagesAt = Date.now();
|
|
@@ -254,7 +254,7 @@ class StreamSignaler {
|
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
static async stopConsuming() {
|
|
257
|
-
for (const instance of [...
|
|
257
|
+
for (const instance of [...Router.instances]) {
|
|
258
258
|
instance.stopConsuming();
|
|
259
259
|
}
|
|
260
260
|
await sleepFor(BLOCK_TIME_MS);
|
|
@@ -356,4 +356,4 @@ class StreamSignaler {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
export {
|
|
359
|
+
export { Router };
|
|
@@ -19,7 +19,7 @@ export const MDATA_SYMBOLS = {
|
|
|
19
19
|
KEYS: ['au', 'err', 'l2s']
|
|
20
20
|
},
|
|
21
21
|
JOB: {
|
|
22
|
-
KEYS: ['ngn', 'tpc', 'pj', 'pd', 'pa', 'key', 'app', 'vrs', 'jid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
22
|
+
KEYS: ['ngn', 'tpc', 'pj', 'pg', 'pd', 'pa', 'key', 'app', 'vrs', 'jid', 'gid', 'aid', 'ts', 'jc', 'ju', 'js', 'err', 'trc']
|
|
23
23
|
},
|
|
24
24
|
JOB_UPDATE: {
|
|
25
25
|
KEYS: ['ju', 'err']
|
package/services/store/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ 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 { STATUS_CODE_INTERRUPT } from '../../modules/enums';
|
|
32
|
+
import { SCOUT_INTERVAL_SECONDS, STATUS_CODE_INTERRUPT } from '../../modules/enums';
|
|
33
33
|
import { GetStateError } from '../../modules/errors';
|
|
34
34
|
|
|
35
35
|
interface AbstractRedisClient {
|
|
@@ -168,10 +168,19 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
168
168
|
this.cache.invalidate();
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
/**
|
|
172
|
+
* At any given time only a single engine will
|
|
173
|
+
* check for and process work items in the
|
|
174
|
+
* time and signal task queues.
|
|
175
|
+
*/
|
|
176
|
+
async reserveScoutRole(scoutType: 'time' | 'signal', delay = SCOUT_INTERVAL_SECONDS): Promise<boolean> {
|
|
177
|
+
const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
178
|
+
const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
|
|
179
|
+
if (this.isSuccessful(success)) {
|
|
180
|
+
await this.redisClient[this.commands.expire](key, delay - 1);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
return false;
|
|
175
184
|
}
|
|
176
185
|
|
|
177
186
|
async getSettings(bCreate = false): Promise<HotMeshSettings> {
|
|
@@ -380,11 +389,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
380
389
|
* list (added via RPUSH) are LPOPed. If origin was expired, then
|
|
381
390
|
* LPOPed items from the list are likewise expired;
|
|
382
391
|
*/
|
|
383
|
-
async setDependency(originJobId: string, topic: string, jobId: string, multi? : U): Promise<any> {
|
|
392
|
+
async setDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
384
393
|
const privateMulti = multi || this.getMulti();
|
|
385
394
|
const depParams = { appId: this.appId, jobId: originJobId };
|
|
386
395
|
const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
|
|
387
|
-
privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
|
|
396
|
+
privateMulti[this.commands.rpush](depKey, `expire::${topic}::${gId}::${jobId}`);
|
|
388
397
|
if (!multi) {
|
|
389
398
|
return await privateMulti.exec();
|
|
390
399
|
}
|
|
@@ -784,9 +793,9 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
784
793
|
* for the given sleep group. Sleep groups are
|
|
785
794
|
* organized into 'n'-second blocks (LISTS))
|
|
786
795
|
*/
|
|
787
|
-
async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
|
|
796
|
+
async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
|
|
788
797
|
const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
789
|
-
const timeEvent = `${type}::${activityId}::${jobId}
|
|
798
|
+
const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
|
|
790
799
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
791
800
|
if (multi || len === 1) {
|
|
792
801
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
@@ -794,7 +803,8 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
794
803
|
}
|
|
795
804
|
}
|
|
796
805
|
|
|
797
|
-
async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] |
|
|
806
|
+
async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | boolean> {
|
|
807
|
+
const existing = Boolean(listKey);
|
|
798
808
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
799
809
|
listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
|
|
800
810
|
if (listKey) {
|
|
@@ -802,11 +812,12 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
802
812
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
803
813
|
if (timeEvent) {
|
|
804
814
|
//there are 3 time-related event triggers: sleep, expire, interrupt
|
|
805
|
-
const [_type, activityId, ...jobId] = timeEvent.split('::');
|
|
806
|
-
return [listKey, jobId.join('::'), activityId, pType];
|
|
815
|
+
const [_type, activityId, gId, ...jobId] = timeEvent.split('::');
|
|
816
|
+
return [listKey, jobId.join('::'), gId, activityId, pType];
|
|
807
817
|
}
|
|
808
818
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
809
819
|
}
|
|
820
|
+
return existing;
|
|
810
821
|
}
|
|
811
822
|
|
|
812
823
|
/**
|
package/services/task/index.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EXPIRE_DURATION,
|
|
3
|
-
FIDELITY_SECONDS
|
|
3
|
+
FIDELITY_SECONDS,
|
|
4
|
+
SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
|
|
4
5
|
import { XSleepFor, sleepFor } from '../../modules/utils';
|
|
5
6
|
import { ILogger } from '../logger';
|
|
6
7
|
import { StoreService } from '../store';
|
|
7
|
-
import { HookInterface } from '../../types/hook';
|
|
8
|
-
import { JobCompletionOptions } from '../../types/job';
|
|
8
|
+
import { HookInterface, HookRule, HookSignal } from '../../types/hook';
|
|
9
|
+
import { JobCompletionOptions, JobState } from '../../types/job';
|
|
9
10
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
11
|
+
import { Pipe } from '../pipe';
|
|
10
12
|
|
|
11
13
|
class TaskService {
|
|
12
14
|
store: StoreService<RedisClient, RedisMulti>;
|
|
13
15
|
logger: ILogger;
|
|
14
16
|
cleanupTimeout: NodeJS.Timeout | null = null;
|
|
17
|
+
isScout: boolean = false;
|
|
15
18
|
|
|
16
19
|
constructor(
|
|
17
20
|
store: StoreService<RedisClient, RedisMulti>,
|
|
@@ -50,28 +53,59 @@ class TaskService {
|
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
|
|
56
|
+
async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
|
|
54
57
|
const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
|
|
55
|
-
await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
|
|
58
|
+
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Should this engine instance play the role of 'scout' for the quorum.
|
|
63
|
+
*/
|
|
64
|
+
async shouldScout() {
|
|
65
|
+
const wasScout = this.isScout;
|
|
66
|
+
const isScout = wasScout || (this.isScout = await this.store.reserveScoutRole('time'));
|
|
67
|
+
if (isScout) {
|
|
68
|
+
if (!wasScout) {
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
this.isScout = false;
|
|
71
|
+
}, SCOUT_INTERVAL_SECONDS * 1_000);
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
|
|
79
|
+
if (await this.shouldScout()) {
|
|
80
|
+
try {
|
|
81
|
+
const timeJob = await this.store.getNextTimeJob(listKey);
|
|
82
|
+
if (Array.isArray(timeJob)) {
|
|
83
|
+
//a queue had a job; try again immediately
|
|
84
|
+
const [listKey, jobId, gId, activityId, type] = timeJob;
|
|
85
|
+
await timeEventCallback(jobId, gId, activityId, type);
|
|
86
|
+
await sleepFor(0);
|
|
87
|
+
this.processTimeHooks(timeEventCallback, listKey);
|
|
88
|
+
} else if (timeJob) {
|
|
89
|
+
//a queue was just emptied; try again immediately
|
|
90
|
+
await sleepFor(0);
|
|
91
|
+
this.processTimeHooks(timeEventCallback);
|
|
92
|
+
} else {
|
|
93
|
+
//all queues are empty; sleep before checking
|
|
94
|
+
let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
|
|
95
|
+
this.cleanupTimeout = sleep.timerId;
|
|
96
|
+
await sleep.promise;
|
|
97
|
+
this.processTimeHooks(timeEventCallback);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
//todo: retry connect to redis
|
|
101
|
+
this.logger.error('task-process-timehooks-error', err);
|
|
71
102
|
}
|
|
72
|
-
}
|
|
73
|
-
//
|
|
74
|
-
|
|
103
|
+
} else {
|
|
104
|
+
//didn't get the scout role; try again in 'one-ish' minutes
|
|
105
|
+
let sleep = XSleepFor(SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
|
|
106
|
+
this.cleanupTimeout = sleep.timerId;
|
|
107
|
+
await sleep.promise;
|
|
108
|
+
this.processTimeHooks(timeEventCallback);
|
|
75
109
|
}
|
|
76
110
|
}
|
|
77
111
|
|
|
@@ -81,6 +115,71 @@ class TaskService {
|
|
|
81
115
|
this.cleanupTimeout = undefined;
|
|
82
116
|
}
|
|
83
117
|
}
|
|
118
|
+
|
|
119
|
+
async getHookRule(topic: string): Promise<HookRule | undefined> {
|
|
120
|
+
const rules = await this.store.getHookRules();
|
|
121
|
+
return rules?.[topic]?.[0] as HookRule;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string> {
|
|
125
|
+
const hookRule = await this.getHookRule(topic);
|
|
126
|
+
if (hookRule) {
|
|
127
|
+
const mapExpression = hookRule.conditions.match[0].expected;
|
|
128
|
+
const resolved = Pipe.resolve(mapExpression, context);
|
|
129
|
+
const jobId = context.metadata.jid;
|
|
130
|
+
const gId = context.metadata.gid;
|
|
131
|
+
const activityId = hookRule.to;
|
|
132
|
+
const hook: HookSignal = {
|
|
133
|
+
topic,
|
|
134
|
+
resolved,
|
|
135
|
+
jobId: `${activityId}::${dad}::${gId}::${jobId}`,
|
|
136
|
+
}
|
|
137
|
+
await this.store.setHookSignal(hook, multi);
|
|
138
|
+
return jobId;
|
|
139
|
+
} else {
|
|
140
|
+
throw new Error('signaler.registerWebHook:error: hook rule not found');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string, string] | undefined> {
|
|
145
|
+
const hookRule = await this.getHookRule(topic);
|
|
146
|
+
if (hookRule) {
|
|
147
|
+
//NOTE: both formats are supported by the mapping engine:
|
|
148
|
+
// `$self.hook.data` OR `$hook.data`
|
|
149
|
+
const context = { $self: { hook: { data }}, $hook: { data }};
|
|
150
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
151
|
+
const resolved = Pipe.resolve(mapExpression, context);
|
|
152
|
+
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
153
|
+
if (!hookSignalId) {
|
|
154
|
+
//messages can be double-processed; not an issue; return undefined
|
|
155
|
+
//users can also provide a bogus topic; not an issue; return undefined
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
//`aid` is part of composit key, but the hook `topic` is its public interface;
|
|
159
|
+
// this means that a new version of the graph can be deployed and the
|
|
160
|
+
// topic can be re-mapped to a different activity id. Outside callers
|
|
161
|
+
// can adhere to the unchanged contract (calling the same topic),
|
|
162
|
+
// while the internal system can be updated in real time as necessary.
|
|
163
|
+
const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
|
|
164
|
+
return [jid.join('::'), hookRule.to, dad, gid];
|
|
165
|
+
} else {
|
|
166
|
+
throw new Error('signal-not-found');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number> {
|
|
171
|
+
const hookRule = await this.getHookRule(topic);
|
|
172
|
+
if (hookRule) {
|
|
173
|
+
//NOTE: both formats are supported by the mapping engine:
|
|
174
|
+
// `$self.hook.data` OR `$hook.data`
|
|
175
|
+
const context = { $self: { hook: { data }}, $hook: { data }};
|
|
176
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
177
|
+
const resolved = Pipe.resolve(mapExpression, context);
|
|
178
|
+
return await this.store.deleteHookSignal(topic, resolved);
|
|
179
|
+
} else {
|
|
180
|
+
throw new Error('signaler.process:error: hook rule not found');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
84
183
|
}
|
|
85
184
|
|
|
86
185
|
export { TaskService };
|
|
@@ -126,20 +126,20 @@ class TelemetryService {
|
|
|
126
126
|
return result;
|
|
127
127
|
}, {})
|
|
128
128
|
};
|
|
129
|
-
this.span
|
|
129
|
+
this.span?.setAttributes(namespacedAtts as StringScalarType);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
setActivityAttributes(attributes: StringScalarType): void {
|
|
134
|
-
this.span
|
|
134
|
+
this.span?.setAttributes(attributes);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
setStreamAttributes(attributes: StringScalarType): void {
|
|
138
|
-
this.span
|
|
138
|
+
this.span?.setAttributes(attributes);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
setJobAttributes(attributes: StringScalarType): void {
|
|
142
|
-
this.jobSpan
|
|
142
|
+
this.jobSpan?.setAttributes(attributes);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
endJobSpan(): void {
|
|
@@ -216,11 +216,11 @@ class TelemetryService {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
setActivityError(message: string) {
|
|
219
|
-
this.span
|
|
219
|
+
this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
setStreamError(message: string) {
|
|
223
|
-
this.span
|
|
223
|
+
this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|