@hotmeshio/hotmesh 0.0.42 → 0.0.44
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/build/modules/enums.d.ts +2 -0
- package/build/modules/enums.js +4 -1
- package/build/modules/utils.js +1 -1
- package/build/package.json +1 -1
- package/build/services/activities/trigger.js +7 -1
- package/build/services/durable/client.d.ts +2 -1
- package/build/services/durable/client.js +17 -3
- package/build/services/durable/exporter.d.ts +105 -0
- package/build/services/durable/exporter.js +374 -0
- package/build/services/durable/factory.js +6 -63
- package/build/services/durable/handle.d.ts +4 -0
- package/build/services/durable/handle.js +5 -0
- package/build/services/durable/meshos.js +3 -0
- package/build/services/durable/workflow.js +24 -21
- package/build/services/engine/index.d.ts +6 -1
- package/build/services/engine/index.js +9 -2
- package/build/services/exporter/index.d.ts +46 -0
- package/build/services/exporter/index.js +126 -0
- package/build/services/hotmesh/index.d.ts +4 -1
- package/build/services/hotmesh/index.js +6 -0
- package/build/services/quorum/index.d.ts +5 -2
- package/build/services/quorum/index.js +33 -15
- package/build/services/router/index.d.ts +3 -0
- package/build/services/router/index.js +3 -0
- package/build/services/store/clients/redis.js +1 -0
- package/build/services/store/index.d.ts +7 -3
- package/build/services/store/index.js +62 -12
- package/build/services/task/index.js +5 -1
- package/build/services/worker/index.js +5 -4
- package/build/types/activity.d.ts +6 -1
- package/build/types/exporter.d.ts +51 -0
- package/build/types/exporter.js +8 -0
- package/build/types/hotmesh.d.ts +1 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/quorum.d.ts +1 -0
- package/build/types/task.d.ts +1 -1
- package/modules/enums.ts +4 -0
- package/modules/utils.ts +1 -1
- package/package.json +1 -1
- package/services/activities/trigger.ts +14 -0
- package/services/durable/client.ts +19 -4
- package/services/durable/exporter.ts +408 -0
- package/services/durable/factory.ts +6 -63
- package/services/durable/handle.ts +12 -0
- package/services/durable/meshos.ts +3 -0
- package/services/durable/workflow.ts +24 -22
- package/services/engine/index.ts +20 -5
- package/services/exporter/index.ts +147 -0
- package/services/hotmesh/index.ts +8 -1
- package/services/quorum/index.ts +37 -13
- package/services/router/index.ts +3 -0
- package/services/store/clients/redis.ts +1 -0
- package/services/store/index.ts +66 -14
- package/services/task/index.ts +4 -1
- package/services/worker/index.ts +6 -5
- package/types/activity.ts +6 -1
- package/types/exporter.ts +61 -0
- package/types/hotmesh.ts +1 -1
- package/types/index.ts +13 -1
- package/types/quorum.ts +1 -0
- package/types/task.ts +1 -1
|
@@ -60,6 +60,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
60
60
|
id: '{$self.input.data.workflowId}'
|
|
61
61
|
key: '{$self.input.data.parentWorkflowId}'
|
|
62
62
|
parent: '{$self.input.data.originJobId}'
|
|
63
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
63
64
|
job:
|
|
64
65
|
maps:
|
|
65
66
|
done: false
|
|
@@ -263,10 +264,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
263
264
|
description: index will be appended later
|
|
264
265
|
maps:
|
|
265
266
|
signals: '{sigw1.output.data.signals}'
|
|
266
|
-
parentWorkflowId:
|
|
267
|
-
'@pipe':
|
|
268
|
-
- ['{$job.metadata.jid}', '-w']
|
|
269
|
-
- ['{@string.concat}']
|
|
267
|
+
parentWorkflowId: '{$job.metadata.jid}'
|
|
270
268
|
originJobId:
|
|
271
269
|
'@pipe':
|
|
272
270
|
- ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
|
|
@@ -356,10 +354,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
356
354
|
description: index will be appended later
|
|
357
355
|
maps:
|
|
358
356
|
signals: '{w1.output.data.signals}'
|
|
359
|
-
parentWorkflowId:
|
|
360
|
-
'@pipe':
|
|
361
|
-
- ['{$job.metadata.jid}', '-w']
|
|
362
|
-
- ['{@string.concat}']
|
|
357
|
+
parentWorkflowId: '{$job.metadata.jid}'
|
|
363
358
|
originJobId:
|
|
364
359
|
'@pipe':
|
|
365
360
|
- ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
|
|
@@ -526,6 +521,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
526
521
|
id: '{$self.input.data.workflowId}'
|
|
527
522
|
key: '{$self.input.data.parentWorkflowId}'
|
|
528
523
|
parent: '{$self.input.data.originJobId}'
|
|
524
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
529
525
|
|
|
530
526
|
w1a:
|
|
531
527
|
title: Activity Worker - Calls Activity Functions
|
|
@@ -566,61 +562,6 @@ const getWorkflowYAML = (app, version) => {
|
|
|
566
562
|
t1a:
|
|
567
563
|
- to: w1a
|
|
568
564
|
|
|
569
|
-
- subscribes: ${app}.sleep.execute
|
|
570
|
-
publishes: ${app}.sleep.executed
|
|
571
|
-
|
|
572
|
-
expire: 0
|
|
573
|
-
|
|
574
|
-
input:
|
|
575
|
-
schema:
|
|
576
|
-
type: object
|
|
577
|
-
properties:
|
|
578
|
-
parentWorkflowId:
|
|
579
|
-
type: string
|
|
580
|
-
originJobId:
|
|
581
|
-
type: string
|
|
582
|
-
workflowId:
|
|
583
|
-
type: string
|
|
584
|
-
duration:
|
|
585
|
-
type: number
|
|
586
|
-
description: in seconds
|
|
587
|
-
index:
|
|
588
|
-
type: number
|
|
589
|
-
output:
|
|
590
|
-
schema:
|
|
591
|
-
type: object
|
|
592
|
-
properties:
|
|
593
|
-
done:
|
|
594
|
-
type: boolean
|
|
595
|
-
duration:
|
|
596
|
-
type: number
|
|
597
|
-
index:
|
|
598
|
-
type: number
|
|
599
|
-
|
|
600
|
-
activities:
|
|
601
|
-
t1s:
|
|
602
|
-
title: Sleep Flow Trigger
|
|
603
|
-
type: trigger
|
|
604
|
-
stats:
|
|
605
|
-
id: '{$self.input.data.workflowId}'
|
|
606
|
-
key: '{$self.input.data.parentWorkflowId}'
|
|
607
|
-
parent: '{$self.input.data.originJobId}'
|
|
608
|
-
|
|
609
|
-
a1s:
|
|
610
|
-
title: Sleep for a duration
|
|
611
|
-
type: hook
|
|
612
|
-
sleep: '{t1s.output.data.duration}'
|
|
613
|
-
job:
|
|
614
|
-
maps:
|
|
615
|
-
done: true
|
|
616
|
-
duration: '{t1s.output.data.duration}'
|
|
617
|
-
index: '{t1s.output.data.index}'
|
|
618
|
-
workflowId: '{t1s.output.data.workflowId}'
|
|
619
|
-
|
|
620
|
-
transitions:
|
|
621
|
-
t1s:
|
|
622
|
-
- to: a1s
|
|
623
|
-
|
|
624
565
|
- subscribes: ${app}.wfsc.execute
|
|
625
566
|
publishes: ${app}.wfsc.executed
|
|
626
567
|
|
|
@@ -665,6 +606,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
665
606
|
stats:
|
|
666
607
|
id: '{$self.input.data.cycleWorkflowId}'
|
|
667
608
|
parent: '{$self.input.data.originJobId}'
|
|
609
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
668
610
|
|
|
669
611
|
a1wc:
|
|
670
612
|
title: Pivot - All Cycling Descendants Point Here
|
|
@@ -836,6 +778,7 @@ const getWorkflowYAML = (app, version) => {
|
|
|
836
778
|
id: '{$self.input.data.workflowId}'
|
|
837
779
|
key: '{$self.input.data.parentWorkflowId}'
|
|
838
780
|
parent: '{$self.input.data.originJobId}'
|
|
781
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
839
782
|
|
|
840
783
|
a1ww:
|
|
841
784
|
title: WFS - signal entry point
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { ExporterService } from './exporter';
|
|
1
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
+
import { DurableJobExport } from '../../types/exporter';
|
|
2
4
|
import { JobInterruptOptions } from '../../types/job';
|
|
3
5
|
export declare class WorkflowHandleService {
|
|
6
|
+
exporter: ExporterService;
|
|
4
7
|
hotMesh: HotMesh;
|
|
5
8
|
workflowTopic: string;
|
|
6
9
|
workflowId: string;
|
|
7
10
|
constructor(hotMesh: HotMesh, workflowTopic: string, workflowId: string);
|
|
11
|
+
export(): Promise<DurableJobExport>;
|
|
8
12
|
/**
|
|
9
13
|
* Sends a signal to the workflow. This is a way to send
|
|
10
14
|
* a message to a workflow that is paused due to having
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WorkflowHandleService = void 0;
|
|
4
4
|
const enums_1 = require("../../modules/enums");
|
|
5
|
+
const exporter_1 = require("./exporter");
|
|
5
6
|
class WorkflowHandleService {
|
|
6
7
|
constructor(hotMesh, workflowTopic, workflowId) {
|
|
7
8
|
this.workflowTopic = workflowTopic;
|
|
8
9
|
this.workflowId = workflowId;
|
|
9
10
|
this.hotMesh = hotMesh;
|
|
11
|
+
this.exporter = new exporter_1.ExporterService(this.hotMesh.appId, this.hotMesh.engine.store, this.hotMesh.engine.logger);
|
|
12
|
+
}
|
|
13
|
+
async export() {
|
|
14
|
+
return this.exporter.export(this.workflowId);
|
|
10
15
|
}
|
|
11
16
|
/**
|
|
12
17
|
* Sends a signal to the workflow. This is a way to send
|
|
@@ -194,6 +194,9 @@ class MeshOSService {
|
|
|
194
194
|
return await this.find(options.options ?? {}, ...args);
|
|
195
195
|
}
|
|
196
196
|
static generateSearchQuery(query) {
|
|
197
|
+
if (!Array.isArray(query) || query.length === 0) {
|
|
198
|
+
return '*';
|
|
199
|
+
}
|
|
197
200
|
const my = new this();
|
|
198
201
|
let queryString = query.map(q => {
|
|
199
202
|
const { field, is, value, type } = q;
|
|
@@ -37,7 +37,7 @@ class WorkflowService {
|
|
|
37
37
|
const entityOrEmptyString = options.entity ?? '';
|
|
38
38
|
//If the workflowId is not provided, it is generated from the entity and the workflow name
|
|
39
39
|
const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
|
|
40
|
-
const parentWorkflowId =
|
|
40
|
+
const parentWorkflowId = workflowId;
|
|
41
41
|
const client = new client_1.ClientService({
|
|
42
42
|
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
43
43
|
});
|
|
@@ -75,32 +75,35 @@ class WorkflowService {
|
|
|
75
75
|
const workflowSpan = store.get('workflowSpan');
|
|
76
76
|
const COUNTER = store.get('counter');
|
|
77
77
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
78
|
+
const sessionId = `-start${workflowDimension}-${execIndex}-`;
|
|
78
79
|
//NOTE: this is the hash prefix; necessary for the search index to locate the entity
|
|
79
80
|
const entityOrEmptyString = options.entity ?? '';
|
|
80
81
|
//If the workflowId is not provided, it is generated from the entity and the workflow name
|
|
81
|
-
const
|
|
82
|
-
const parentWorkflowId = `${workflowId}-f`;
|
|
82
|
+
const parentWorkflowId = workflowId;
|
|
83
83
|
const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
85
|
+
const keyParams = { appId: hotMeshClient.appId, jobId: workflowId };
|
|
86
|
+
const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
|
|
87
|
+
let childJobId = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId);
|
|
88
|
+
if (childJobId) {
|
|
88
89
|
return childJobId;
|
|
89
90
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
93
|
-
});
|
|
94
|
-
await client.workflow.start({
|
|
95
|
-
...options,
|
|
96
|
-
namespace,
|
|
97
|
-
workflowId: childJobId,
|
|
98
|
-
parentWorkflowId,
|
|
99
|
-
workflowTrace,
|
|
100
|
-
workflowSpan,
|
|
101
|
-
});
|
|
102
|
-
return childJobId;
|
|
91
|
+
else {
|
|
92
|
+
childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
|
|
103
93
|
}
|
|
94
|
+
const client = new client_1.ClientService({
|
|
95
|
+
connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
|
|
96
|
+
});
|
|
97
|
+
await client.workflow.start({
|
|
98
|
+
...options,
|
|
99
|
+
namespace,
|
|
100
|
+
workflowId: childJobId,
|
|
101
|
+
parentWorkflowId,
|
|
102
|
+
workflowTrace,
|
|
103
|
+
workflowSpan,
|
|
104
|
+
});
|
|
105
|
+
await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, childJobId);
|
|
106
|
+
return childJobId;
|
|
104
107
|
}
|
|
105
108
|
/**
|
|
106
109
|
* Wraps activities in a proxy that will durably run them
|
|
@@ -439,7 +442,7 @@ class WorkflowService {
|
|
|
439
442
|
arguments: Array.from(arguments),
|
|
440
443
|
//when the origin job is removed
|
|
441
444
|
originJobId: originJobId ?? workflowId,
|
|
442
|
-
parentWorkflowId:
|
|
445
|
+
parentWorkflowId: workflowId,
|
|
443
446
|
workflowId: activityJobId,
|
|
444
447
|
workflowTopic: activityTopic,
|
|
445
448
|
activityName,
|
|
@@ -5,6 +5,7 @@ import { Interrupt } from '../activities/interrupt';
|
|
|
5
5
|
import { Signal } from '../activities/signal';
|
|
6
6
|
import { Worker } from '../activities/worker';
|
|
7
7
|
import { Trigger } from '../activities/trigger';
|
|
8
|
+
import { ExporterService } from '../exporter';
|
|
8
9
|
import { ILogger } from '../logger';
|
|
9
10
|
import { Router } from '../router';
|
|
10
11
|
import { StoreService } from '../store';
|
|
@@ -18,15 +19,17 @@ import { JobState, JobData, JobMetadata, JobOutput, JobStatus, JobInterruptOptio
|
|
|
18
19
|
import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '../../types/hotmesh';
|
|
19
20
|
import { JobMessageCallback } from '../../types/quorum';
|
|
20
21
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
21
|
-
import { StringAnyType } from '../../types/serializer';
|
|
22
|
+
import { StringAnyType, StringStringType } from '../../types/serializer';
|
|
22
23
|
import { GetStatsOptions, IdsResponse, JobStatsInput, StatsResponse } from '../../types/stats';
|
|
23
24
|
import { StreamCode, StreamData, StreamDataResponse, StreamError, StreamStatus } from '../../types/stream';
|
|
24
25
|
import { WorkListTaskType } from '../../types/task';
|
|
26
|
+
import { JobExport } from '../../types/exporter';
|
|
25
27
|
declare class EngineService {
|
|
26
28
|
namespace: string;
|
|
27
29
|
apps: HotMeshApps | null;
|
|
28
30
|
appId: string;
|
|
29
31
|
guid: string;
|
|
32
|
+
exporter: ExporterService | null;
|
|
30
33
|
router: Router | null;
|
|
31
34
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
32
35
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
@@ -88,6 +91,8 @@ declare class EngineService {
|
|
|
88
91
|
* it will be expired immediately.
|
|
89
92
|
*/
|
|
90
93
|
resolveExpires(context: JobState, options: JobCompletionOptions): number;
|
|
94
|
+
export(jobId: string): Promise<JobExport>;
|
|
95
|
+
getRaw(jobId: string): Promise<StringStringType>;
|
|
91
96
|
getStatus(jobId: string): Promise<JobStatus>;
|
|
92
97
|
getState(topic: string, jobId: string): Promise<JobOutput>;
|
|
93
98
|
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
@@ -9,6 +9,7 @@ const enums_1 = require("../../modules/enums");
|
|
|
9
9
|
const utils_1 = require("../../modules/utils");
|
|
10
10
|
const activities_1 = __importDefault(require("../activities"));
|
|
11
11
|
const compiler_1 = require("../compiler");
|
|
12
|
+
const exporter_1 = require("../exporter");
|
|
12
13
|
const reporter_1 = require("../reporter");
|
|
13
14
|
const router_1 = require("../router");
|
|
14
15
|
const serializer_1 = require("../serializer");
|
|
@@ -41,8 +42,8 @@ class EngineService {
|
|
|
41
42
|
await instance.initStreamChannel(config.engine.stream);
|
|
42
43
|
instance.router = instance.initRouter(config);
|
|
43
44
|
instance.router.consumeMessages(instance.stream.mintKey(key_1.KeyType.STREAMS, { appId: instance.appId }), 'ENGINE', instance.guid, instance.processStreamMessage.bind(instance));
|
|
44
|
-
//the task service is used by the engine to process `webhooks` and `timehooks`
|
|
45
45
|
instance.taskService = new task_1.TaskService(instance.store, logger);
|
|
46
|
+
instance.exporter = new exporter_1.ExporterService(instance.appId, instance.store, logger);
|
|
46
47
|
return instance;
|
|
47
48
|
}
|
|
48
49
|
}
|
|
@@ -526,9 +527,15 @@ class EngineService {
|
|
|
526
527
|
return options.expire ?? context.metadata.expire ?? enums_1.HMSH_EXPIRE_JOB_SECONDS;
|
|
527
528
|
}
|
|
528
529
|
// ****** GET JOB STATE/COLLATION STATUS BY ID *********
|
|
530
|
+
async export(jobId) {
|
|
531
|
+
return await this.exporter.export(jobId);
|
|
532
|
+
}
|
|
533
|
+
async getRaw(jobId) {
|
|
534
|
+
return await this.store.getRaw(jobId);
|
|
535
|
+
}
|
|
529
536
|
async getStatus(jobId) {
|
|
530
537
|
const { id: appId } = await this.getVID();
|
|
531
|
-
return this.store.getStatus(jobId, appId);
|
|
538
|
+
return await this.store.getStatus(jobId, appId);
|
|
532
539
|
}
|
|
533
540
|
//todo: add 'options' parameter;
|
|
534
541
|
// (e.g, if {dimensions:true}, use hscan to deliver
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ILogger } from '../logger';
|
|
2
|
+
import { StoreService } from '../store';
|
|
3
|
+
import { StringStringType, Symbols } from "../../types/serializer";
|
|
4
|
+
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
5
|
+
import { DependencyExport, ExportOptions, JobActionExport, JobExport } from '../../types/exporter';
|
|
6
|
+
import { SerializerService } from '../serializer';
|
|
7
|
+
/**
|
|
8
|
+
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
9
|
+
* Expands process data and includes dependency list
|
|
10
|
+
*/
|
|
11
|
+
declare class ExporterService {
|
|
12
|
+
appId: string;
|
|
13
|
+
logger: ILogger;
|
|
14
|
+
serializer: SerializerService;
|
|
15
|
+
store: StoreService<RedisClient, RedisMulti>;
|
|
16
|
+
symbols: Promise<Symbols> | Symbols;
|
|
17
|
+
constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
18
|
+
/**
|
|
19
|
+
* Convert the job hash and dependency list into a JobExport object.
|
|
20
|
+
* This object contains various facets that describe the interaction
|
|
21
|
+
* in terms relevant to narrative storytelling.
|
|
22
|
+
*/
|
|
23
|
+
export(jobId: string, options?: ExportOptions): Promise<JobExport>;
|
|
24
|
+
/**
|
|
25
|
+
* Inflates the key from Redis, 3-character symbol
|
|
26
|
+
* into a human-readable JSON path, reflecting the
|
|
27
|
+
* tree-like structure of the unidimensional Hash
|
|
28
|
+
*/
|
|
29
|
+
inflateKey(key: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Inflates the job data from Redis into a JobExport object
|
|
32
|
+
* @param jobHash - the job data from Redis
|
|
33
|
+
* @param dependencyList - the list of dependencies for the job
|
|
34
|
+
* @returns - the inflated job data
|
|
35
|
+
*/
|
|
36
|
+
inflate(jobHash: StringStringType, dependencyList: string[]): JobExport;
|
|
37
|
+
/**
|
|
38
|
+
* Inflates the dependency data from Redis into a JobExport object by
|
|
39
|
+
* organizing the dimensional isolate in sch a way asto interleave
|
|
40
|
+
* into a story
|
|
41
|
+
* @param data - the dependency data from Redis
|
|
42
|
+
* @returns - the organized dependency data
|
|
43
|
+
*/
|
|
44
|
+
inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[];
|
|
45
|
+
}
|
|
46
|
+
export { ExporterService };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExporterService = void 0;
|
|
4
|
+
const serializer_1 = require("../serializer");
|
|
5
|
+
const utils_1 = require("../../modules/utils");
|
|
6
|
+
/**
|
|
7
|
+
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
8
|
+
* Expands process data and includes dependency list
|
|
9
|
+
*/
|
|
10
|
+
class ExporterService {
|
|
11
|
+
constructor(appId, store, logger) {
|
|
12
|
+
this.appId = appId;
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
this.store = store;
|
|
15
|
+
this.serializer = new serializer_1.SerializerService();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Convert the job hash and dependency list into a JobExport object.
|
|
19
|
+
* This object contains various facets that describe the interaction
|
|
20
|
+
* in terms relevant to narrative storytelling.
|
|
21
|
+
*/
|
|
22
|
+
async export(jobId, options = {}) {
|
|
23
|
+
if (!this.symbols) {
|
|
24
|
+
this.symbols = this.store.getAllSymbols();
|
|
25
|
+
this.symbols = await this.symbols;
|
|
26
|
+
}
|
|
27
|
+
const depData = await this.store.getDependencies(jobId);
|
|
28
|
+
const jobData = await this.store.getRaw(jobId);
|
|
29
|
+
const jobExport = this.inflate(jobData, depData);
|
|
30
|
+
return jobExport;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Inflates the key from Redis, 3-character symbol
|
|
34
|
+
* into a human-readable JSON path, reflecting the
|
|
35
|
+
* tree-like structure of the unidimensional Hash
|
|
36
|
+
*/
|
|
37
|
+
inflateKey(key) {
|
|
38
|
+
return (key in this.symbols) ? this.symbols[key] : key;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Inflates the job data from Redis into a JobExport object
|
|
42
|
+
* @param jobHash - the job data from Redis
|
|
43
|
+
* @param dependencyList - the list of dependencies for the job
|
|
44
|
+
* @returns - the inflated job data
|
|
45
|
+
*/
|
|
46
|
+
inflate(jobHash, dependencyList) {
|
|
47
|
+
//the list of actions taken in the workflow and hook functions
|
|
48
|
+
const actions = {
|
|
49
|
+
hooks: {},
|
|
50
|
+
main: {
|
|
51
|
+
cursor: -1,
|
|
52
|
+
items: []
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const process = {};
|
|
56
|
+
const dependencies = this.inflateDependencyData(dependencyList, actions);
|
|
57
|
+
const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
|
|
58
|
+
Object.entries(jobHash).forEach(([key, value]) => {
|
|
59
|
+
const match = key.match(regex);
|
|
60
|
+
if (match) {
|
|
61
|
+
//activity process state
|
|
62
|
+
const [_, letters, numbers] = match;
|
|
63
|
+
const path = this.inflateKey(letters);
|
|
64
|
+
const dimensions = `${numbers.replace(/,/g, '/')}`;
|
|
65
|
+
const resolved = this.serializer.fromString(value);
|
|
66
|
+
process[`${dimensions}/${path}`] = resolved;
|
|
67
|
+
}
|
|
68
|
+
else if (key.length === 3) {
|
|
69
|
+
//job state
|
|
70
|
+
process[this.inflateKey(key)] = this.serializer.fromString(value);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
dependencies,
|
|
75
|
+
process: (0, utils_1.restoreHierarchy)(process),
|
|
76
|
+
status: jobHash[':'],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Inflates the dependency data from Redis into a JobExport object by
|
|
81
|
+
* organizing the dimensional isolate in sch a way asto interleave
|
|
82
|
+
* into a story
|
|
83
|
+
* @param data - the dependency data from Redis
|
|
84
|
+
* @returns - the organized dependency data
|
|
85
|
+
*/
|
|
86
|
+
inflateDependencyData(data, actions) {
|
|
87
|
+
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
88
|
+
const flowReg = /-(\d+)$/;
|
|
89
|
+
return data.map((dependency, index) => {
|
|
90
|
+
const [action, topic, gid, ...jid] = dependency.split('::');
|
|
91
|
+
const jobId = jid.join('::');
|
|
92
|
+
const match = jobId.match(hookReg);
|
|
93
|
+
let prefix;
|
|
94
|
+
let type;
|
|
95
|
+
let dimensionKey = '';
|
|
96
|
+
if (match) {
|
|
97
|
+
//hook-originating dependency
|
|
98
|
+
const [_, dimension, counter] = match;
|
|
99
|
+
dimensionKey = dimension.split(',').join('/');
|
|
100
|
+
prefix = `${dimensionKey}[${counter}]`;
|
|
101
|
+
type = 'hook';
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const match = jobId.match(flowReg);
|
|
105
|
+
if (match) {
|
|
106
|
+
//main workflow-originating dependency
|
|
107
|
+
const [_, counter] = match;
|
|
108
|
+
prefix = `[${counter}]`;
|
|
109
|
+
type = 'flow';
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
//'other' types like signal cleanup
|
|
113
|
+
prefix = '/';
|
|
114
|
+
type = 'other';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
type: action,
|
|
119
|
+
topic,
|
|
120
|
+
gid,
|
|
121
|
+
jid: jobId,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
exports.ExporterService = ExporterService;
|
|
@@ -7,7 +7,8 @@ import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
|
|
|
7
7
|
import { JobMessageCallback, QuorumProfile } from '../../types/quorum';
|
|
8
8
|
import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
|
|
9
9
|
import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
|
|
10
|
-
import { StringAnyType } from '../../types/serializer';
|
|
10
|
+
import { StringAnyType, StringStringType } from '../../types/serializer';
|
|
11
|
+
import { JobExport } from '../../types/exporter';
|
|
11
12
|
declare class HotMeshService {
|
|
12
13
|
namespace: string;
|
|
13
14
|
appId: string;
|
|
@@ -35,6 +36,8 @@ declare class HotMeshService {
|
|
|
35
36
|
plan(path: string): Promise<HotMeshManifest>;
|
|
36
37
|
deploy(pathOrYAML: string): Promise<HotMeshManifest>;
|
|
37
38
|
activate(version: string, delay?: number): Promise<boolean>;
|
|
39
|
+
export(jobId: string): Promise<JobExport>;
|
|
40
|
+
getRaw(jobId: string): Promise<StringStringType>;
|
|
38
41
|
getStats(topic: string, query: JobStatsInput): Promise<StatsResponse>;
|
|
39
42
|
getStatus(jobId: string): Promise<JobStatus>;
|
|
40
43
|
getState(topic: string, jobId: string): Promise<JobOutput>;
|
|
@@ -104,6 +104,12 @@ class HotMeshService {
|
|
|
104
104
|
return await this.quorum?.activate(version, delay);
|
|
105
105
|
}
|
|
106
106
|
// ************* REPORTER METHODS *************
|
|
107
|
+
async export(jobId) {
|
|
108
|
+
return await this.engine?.export(jobId);
|
|
109
|
+
}
|
|
110
|
+
async getRaw(jobId) {
|
|
111
|
+
return await this.engine?.getRaw(jobId);
|
|
112
|
+
}
|
|
107
113
|
async getStats(topic, query) {
|
|
108
114
|
return await this.engine?.getStats(topic, query);
|
|
109
115
|
}
|
|
@@ -3,8 +3,8 @@ import { ILogger } from '../logger';
|
|
|
3
3
|
import { StoreService } from '../store';
|
|
4
4
|
import { SubService } from '../sub';
|
|
5
5
|
import { CacheMode } from '../../types/cache';
|
|
6
|
-
import { QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
|
|
7
6
|
import { HotMeshConfig } from '../../types/hotmesh';
|
|
7
|
+
import { QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
|
|
8
8
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
9
9
|
declare class QuorumService {
|
|
10
10
|
namespace: string;
|
|
@@ -30,6 +30,9 @@ declare class QuorumService {
|
|
|
30
30
|
sub(callback: QuorumMessageCallback): Promise<void>;
|
|
31
31
|
unsub(callback: QuorumMessageCallback): Promise<void>;
|
|
32
32
|
rollCall(delay?: number): Promise<QuorumProfile[]>;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* request a quorum; if successful activate the app version
|
|
35
|
+
*/
|
|
36
|
+
activate(version: string, delay?: number, count?: number): Promise<boolean>;
|
|
34
37
|
}
|
|
35
38
|
export { QuorumService };
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QuorumService = void 0;
|
|
4
|
-
const
|
|
4
|
+
const enums_1 = require("../../modules/enums");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
6
|
const compiler_1 = require("../compiler");
|
|
7
7
|
const redis_1 = require("../store/clients/redis");
|
|
8
8
|
const ioredis_1 = require("../store/clients/ioredis");
|
|
9
9
|
const ioredis_2 = require("../sub/clients/ioredis");
|
|
10
10
|
const redis_2 = require("../sub/clients/redis");
|
|
11
|
-
|
|
12
|
-
const QUORUM_DELAY = 250;
|
|
11
|
+
const hotmesh_1 = require("../../types/hotmesh");
|
|
13
12
|
class QuorumService {
|
|
14
13
|
constructor() {
|
|
15
14
|
this.profiles = [];
|
|
@@ -30,8 +29,10 @@ class QuorumService {
|
|
|
30
29
|
//note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
|
|
31
30
|
await instance.initStoreChannel(config.engine.store);
|
|
32
31
|
await instance.initSubChannel(config.engine.sub);
|
|
33
|
-
|
|
34
|
-
await instance.subscribe.subscribe(
|
|
32
|
+
//general quorum subscription
|
|
33
|
+
await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId);
|
|
34
|
+
//app-specific quorum subscription (used for pubsub one-time request/response)
|
|
35
|
+
await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid);
|
|
35
36
|
instance.engine.processWebHooks();
|
|
36
37
|
instance.engine.processTimeHooks();
|
|
37
38
|
return instance;
|
|
@@ -102,20 +103,21 @@ class QuorumService {
|
|
|
102
103
|
engine_id: this.guid,
|
|
103
104
|
namespace: this.namespace,
|
|
104
105
|
app_id: this.appId,
|
|
105
|
-
stream: this.engine.stream.mintKey(
|
|
106
|
+
stream: this.engine.stream.mintKey(hotmesh_1.KeyType.STREAMS, { appId: this.appId }),
|
|
107
|
+
counts: this.engine.router.counts,
|
|
106
108
|
};
|
|
107
109
|
}
|
|
108
|
-
this.store.publish(
|
|
110
|
+
this.store.publish(hotmesh_1.KeyType.QUORUM, {
|
|
109
111
|
type: 'pong',
|
|
110
112
|
guid, originator,
|
|
111
113
|
profile,
|
|
112
114
|
}, appId);
|
|
113
115
|
}
|
|
114
|
-
async requestQuorum(delay =
|
|
116
|
+
async requestQuorum(delay = enums_1.HMSH_QUORUM_DELAY_MS, details = false) {
|
|
115
117
|
const quorum = this.quorum;
|
|
116
118
|
this.quorum = 0;
|
|
117
119
|
this.profiles.length = 0;
|
|
118
|
-
await this.store.publish(
|
|
120
|
+
await this.store.publish(hotmesh_1.KeyType.QUORUM, {
|
|
119
121
|
type: 'ping',
|
|
120
122
|
originator: this.guid,
|
|
121
123
|
details,
|
|
@@ -126,7 +128,7 @@ class QuorumService {
|
|
|
126
128
|
// ************* PUB/SUB METHODS *************
|
|
127
129
|
//publish a message to the quorum
|
|
128
130
|
async pub(quorumMessage) {
|
|
129
|
-
return await this.store.publish(
|
|
131
|
+
return await this.store.publish(hotmesh_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
|
|
130
132
|
}
|
|
131
133
|
//subscribe user to quorum messages
|
|
132
134
|
async sub(callback) {
|
|
@@ -139,7 +141,7 @@ class QuorumService {
|
|
|
139
141
|
this.callbacks = this.callbacks.filter(cb => cb !== callback);
|
|
140
142
|
}
|
|
141
143
|
// ************* COMPILER METHODS *************
|
|
142
|
-
async rollCall(delay =
|
|
144
|
+
async rollCall(delay = enums_1.HMSH_QUORUM_DELAY_MS) {
|
|
143
145
|
await this.requestQuorum(delay, true);
|
|
144
146
|
const targetStreams = [];
|
|
145
147
|
const multi = this.store.getMulti();
|
|
@@ -160,18 +162,29 @@ class QuorumService {
|
|
|
160
162
|
});
|
|
161
163
|
return this.profiles;
|
|
162
164
|
}
|
|
163
|
-
|
|
165
|
+
/**
|
|
166
|
+
* request a quorum; if successful activate the app version
|
|
167
|
+
*/
|
|
168
|
+
async activate(version, delay = enums_1.HMSH_QUORUM_DELAY_MS, count = 0) {
|
|
164
169
|
version = version.toString();
|
|
170
|
+
const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
|
|
171
|
+
if (!canActivate) {
|
|
172
|
+
//another engine is already activating the app version
|
|
173
|
+
this.logger.debug('quorum-activation-awaiting', { version });
|
|
174
|
+
await (0, utils_1.sleepFor)(delay * 6);
|
|
175
|
+
const app = await this.store.getApp(this.appId, true);
|
|
176
|
+
return app?.active == true && app?.version === version;
|
|
177
|
+
}
|
|
165
178
|
const config = await this.engine.getVID();
|
|
166
|
-
//request a quorum to activate the version
|
|
167
179
|
await this.requestQuorum(delay);
|
|
168
180
|
const q1 = await this.requestQuorum(delay);
|
|
169
181
|
const q2 = await this.requestQuorum(delay);
|
|
170
182
|
const q3 = await this.requestQuorum(delay);
|
|
171
183
|
if (q1 && q1 === q2 && q2 === q3) {
|
|
172
184
|
this.logger.info('quorum-rollcall-succeeded', { q1, q2, q3 });
|
|
173
|
-
this.store.publish(
|
|
185
|
+
this.store.publish(hotmesh_1.KeyType.QUORUM, { type: 'activate', cache_mode: 'nocache', until_version: version }, this.appId);
|
|
174
186
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
187
|
+
await this.store.releaseScoutRole('activate');
|
|
175
188
|
//confirm we received the activation message
|
|
176
189
|
if (this.engine.untilVersion === version) {
|
|
177
190
|
this.logger.info('quorum-activation-succeeded', { version });
|
|
@@ -185,7 +198,12 @@ class QuorumService {
|
|
|
185
198
|
}
|
|
186
199
|
}
|
|
187
200
|
else {
|
|
188
|
-
this.logger.
|
|
201
|
+
this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
|
|
202
|
+
this.store.releaseScoutRole('activate');
|
|
203
|
+
if (count < enums_1.HMSH_ACTIVATION_MAX_RETRY) {
|
|
204
|
+
//increase the delay (give the quorum time to respond) and try again
|
|
205
|
+
return await this.activate(version, delay * 2, count + 1);
|
|
206
|
+
}
|
|
189
207
|
throw new Error(`Quorum not reached. Version ${version} not activated.`);
|
|
190
208
|
}
|
|
191
209
|
}
|
|
@@ -17,6 +17,9 @@ declare class Router {
|
|
|
17
17
|
logger: ILogger;
|
|
18
18
|
throttle: number;
|
|
19
19
|
errorCount: number;
|
|
20
|
+
counts: {
|
|
21
|
+
[key: string]: number;
|
|
22
|
+
};
|
|
20
23
|
currentTimerId: NodeJS.Timeout | null;
|
|
21
24
|
shouldConsume: boolean;
|
|
22
25
|
constructor(config: StreamConfig, stream: StreamService<RedisClient, RedisMulti>, store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|