@hotmeshio/hotmesh 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/build/modules/enums.d.ts +29 -23
- package/build/modules/enums.js +38 -29
- package/build/modules/errors.d.ts +1 -1
- package/build/modules/errors.js +9 -7
- package/build/modules/key.d.ts +1 -34
- package/build/modules/key.js +24 -47
- package/build/package.json +1 -1
- package/build/services/activities/activity.js +1 -1
- package/build/services/activities/hook.js +4 -9
- package/build/services/activities/trigger.d.ts +3 -2
- package/build/services/activities/trigger.js +10 -6
- package/build/services/durable/client.d.ts +9 -1
- package/build/services/durable/client.js +30 -14
- package/build/services/durable/handle.js +2 -2
- package/build/services/durable/worker.js +4 -3
- package/build/services/engine/index.d.ts +2 -1
- package/build/services/engine/index.js +6 -6
- package/build/services/hotmesh/index.d.ts +2 -2
- package/build/services/hotmesh/index.js +3 -4
- package/build/services/quorum/index.d.ts +6 -6
- package/build/services/quorum/index.js +47 -11
- package/build/services/router/index.js +16 -14
- package/build/services/store/clients/ioredis.d.ts +1 -0
- package/build/services/store/clients/ioredis.js +9 -0
- package/build/services/store/clients/redis.d.ts +1 -0
- package/build/services/store/clients/redis.js +16 -0
- package/build/services/store/index.d.ts +15 -9
- package/build/services/store/index.js +46 -23
- package/build/services/stream/clients/ioredis.d.ts +1 -0
- package/build/services/stream/clients/ioredis.js +33 -24
- package/build/services/stream/clients/redis.d.ts +1 -0
- package/build/services/stream/clients/redis.js +15 -0
- package/build/services/stream/index.d.ts +1 -0
- package/build/services/task/index.d.ts +10 -3
- package/build/services/task/index.js +35 -17
- package/build/services/worker/index.d.ts +1 -0
- package/build/services/worker/index.js +24 -0
- package/build/types/durable.d.ts +3 -2
- package/build/types/hotmesh.d.ts +43 -2
- package/build/types/hotmesh.js +28 -0
- package/build/types/index.d.ts +3 -2
- package/build/types/index.js +3 -1
- package/build/types/logger.d.ts +1 -0
- package/build/types/logger.js +1 -0
- package/build/types/quorum.d.ts +11 -1
- package/build/types/redisclient.d.ts +1 -0
- package/build/types/task.d.ts +1 -0
- package/build/types/task.js +2 -0
- package/modules/enums.ts +49 -35
- package/modules/errors.ts +17 -8
- package/modules/key.ts +3 -40
- package/package.json +1 -1
- package/services/activities/activity.ts +2 -2
- package/services/activities/hook.ts +18 -9
- package/services/activities/trigger.ts +10 -6
- package/services/durable/client.ts +31 -15
- package/services/durable/handle.ts +3 -3
- package/services/durable/worker.ts +4 -3
- package/services/engine/index.ts +13 -12
- package/services/hotmesh/index.ts +4 -5
- package/services/quorum/index.ts +48 -12
- package/services/router/index.ts +26 -24
- package/services/store/clients/ioredis.ts +9 -0
- package/services/store/clients/redis.ts +16 -0
- package/services/store/index.ts +63 -25
- package/services/stream/clients/ioredis.ts +33 -24
- package/services/stream/clients/redis.ts +14 -0
- package/services/stream/index.ts +1 -0
- package/services/task/index.ts +66 -24
- package/services/worker/index.ts +30 -0
- package/types/durable.ts +6 -5
- package/types/hotmesh.ts +47 -2
- package/types/index.ts +8 -1
- package/types/logger.ts +3 -1
- package/types/quorum.ts +15 -4
- package/types/redisclient.ts +1 -0
- package/types/task.ts +1 -0
package/modules/errors.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { ActivityDuplex } from "../types/activity";
|
|
2
2
|
import { CollationFaultType, CollationStage } from "../types/collator";
|
|
3
|
+
import {
|
|
4
|
+
HMSH_CODE_DURABLE_MAXED,
|
|
5
|
+
HMSH_CODE_DURABLE_TIMEOUT,
|
|
6
|
+
HMSH_CODE_DURABLE_FATAL,
|
|
7
|
+
HMSH_CODE_DURABLE_INCOMPLETE,
|
|
8
|
+
HMSH_CODE_NOTFOUND,
|
|
9
|
+
HMSH_CODE_DURABLE_RETRYABLE,
|
|
10
|
+
HMSH_CODE_DURABLE_SLEEPFOR,
|
|
11
|
+
HMSH_CODE_DURABLE_WAITFOR } from "./enums";
|
|
3
12
|
|
|
4
13
|
class GetStateError extends Error {
|
|
5
14
|
jobId: string;
|
|
6
|
-
code
|
|
15
|
+
code = HMSH_CODE_NOTFOUND;
|
|
7
16
|
constructor(jobId: string) {
|
|
8
17
|
super(`${jobId} Not Found`);
|
|
9
18
|
this.jobId = jobId;
|
|
@@ -21,7 +30,7 @@ class DurableIncompleteSignalError extends Error {
|
|
|
21
30
|
code: number;
|
|
22
31
|
constructor(message: string) {
|
|
23
32
|
super(message);
|
|
24
|
-
this.code =
|
|
33
|
+
this.code = HMSH_CODE_DURABLE_INCOMPLETE;
|
|
25
34
|
}
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -32,7 +41,7 @@ class DurableWaitForSignalError extends Error {
|
|
|
32
41
|
constructor(message: string, signals: {signal: string, index: number}[]) {
|
|
33
42
|
super(message);
|
|
34
43
|
this.signals = signals;
|
|
35
|
-
this.code =
|
|
44
|
+
this.code = HMSH_CODE_DURABLE_WAITFOR;
|
|
36
45
|
}
|
|
37
46
|
}
|
|
38
47
|
|
|
@@ -60,35 +69,35 @@ class DurableSleepForError extends Error {
|
|
|
60
69
|
this.duration = duration;
|
|
61
70
|
this.index = index;
|
|
62
71
|
this.dimension = dimension;
|
|
63
|
-
this.code =
|
|
72
|
+
this.code = HMSH_CODE_DURABLE_SLEEPFOR;
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
class DurableTimeoutError extends Error {
|
|
67
76
|
code: number;
|
|
68
77
|
constructor(message: string) {
|
|
69
78
|
super(message);
|
|
70
|
-
this.code =
|
|
79
|
+
this.code = HMSH_CODE_DURABLE_TIMEOUT;
|
|
71
80
|
}
|
|
72
81
|
}
|
|
73
82
|
class DurableMaxedError extends Error {
|
|
74
83
|
code: number;
|
|
75
84
|
constructor(message: string) {
|
|
76
85
|
super(message);
|
|
77
|
-
this.code =
|
|
86
|
+
this.code = HMSH_CODE_DURABLE_MAXED;
|
|
78
87
|
}
|
|
79
88
|
}
|
|
80
89
|
class DurableFatalError extends Error {
|
|
81
90
|
code: number;
|
|
82
91
|
constructor(message: string) {
|
|
83
92
|
super(message);
|
|
84
|
-
this.code =
|
|
93
|
+
this.code = HMSH_CODE_DURABLE_FATAL;
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
class DurableRetryError extends Error {
|
|
88
97
|
code: number;
|
|
89
98
|
constructor(message: string) {
|
|
90
99
|
super(message);
|
|
91
|
-
this.code =
|
|
100
|
+
this.code = HMSH_CODE_DURABLE_RETRYABLE;
|
|
92
101
|
}
|
|
93
102
|
}
|
|
94
103
|
|
package/modules/key.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { KeyStoreParams, KeyType } from '../types/hotmesh';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Keys
|
|
3
5
|
*
|
|
@@ -26,46 +28,7 @@
|
|
|
26
28
|
* hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
|
|
27
29
|
*/
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
const HMNS = "hmsh";
|
|
31
|
-
|
|
32
|
-
//these are the entity types that are stored in the key/value store
|
|
33
|
-
enum KeyType {
|
|
34
|
-
APP = 'APP',
|
|
35
|
-
ENGINE_ID = 'ENGINE',
|
|
36
|
-
HOOKS = 'HOOKS',
|
|
37
|
-
JOB_DEPENDENTS = 'JOB_DEPENDENTS',
|
|
38
|
-
JOB_STATE = 'JOB_STATE',
|
|
39
|
-
JOB_STATS_GENERAL = 'JOB_STATS_GENERAL',
|
|
40
|
-
JOB_STATS_MEDIAN = 'JOB_STATS_MEDIAN',
|
|
41
|
-
JOB_STATS_INDEX = 'JOB_STATS_INDEX',
|
|
42
|
-
HOTMESH = 'HOTMESH',
|
|
43
|
-
QUORUM = 'QUORUM',
|
|
44
|
-
SCHEMAS = 'SCHEMAS',
|
|
45
|
-
SIGNALS = 'SIGNALS',
|
|
46
|
-
STREAMS = 'STREAMS',
|
|
47
|
-
SUBSCRIPTIONS = 'SUBSCRIPTIONS',
|
|
48
|
-
SUBSCRIPTION_PATTERNS = 'SUBSCRIPTION_PATTERNS',
|
|
49
|
-
SYMKEYS = 'SYMKEYS',
|
|
50
|
-
SYMVALS = 'SYMVALS',
|
|
51
|
-
TIME_RANGE = 'TIME_RANGE',
|
|
52
|
-
WORK_ITEMS = 'WORK_ITEMS',
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
//when minting a key, the following parameters are used to create a unique key per entity
|
|
56
|
-
type KeyStoreParams = {
|
|
57
|
-
appId?: string; //app id is a uuid for a hotmesh app
|
|
58
|
-
engineId?: string; //unique auto-generated guid for an ephemeral engine instance
|
|
59
|
-
appVersion?: string; //(e.g. "1.0.0", "1", "1.0")
|
|
60
|
-
jobId?: string; //a customer-defined id for job; must be unique for the entire app
|
|
61
|
-
activityId?: string; //activity id is a uuid for a given hotmesh app
|
|
62
|
-
jobKey?: string; //a customer-defined label for a job that serves to categorize events
|
|
63
|
-
dateTime?: string; //UTC date time: YYYY-MM-DDTHH:MM (20203-04-12T00:00); serves as a time-series bucket for the job_key
|
|
64
|
-
facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
|
|
65
|
-
topic?: string; //topic name (e.g., "foo" or "" for top-level)
|
|
66
|
-
timeValue?: number; //time value (rounded to minute) (for delete range)
|
|
67
|
-
scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
|
|
68
|
-
};
|
|
31
|
+
const HMNS = "hmsh"; //default
|
|
69
32
|
|
|
70
33
|
class KeyService {
|
|
71
34
|
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HMSH_EXPIRE_DURATION } from '../../modules/enums';
|
|
2
2
|
import {
|
|
3
3
|
CollationError,
|
|
4
4
|
GenerationalError,
|
|
@@ -435,7 +435,7 @@ class Activity {
|
|
|
435
435
|
|
|
436
436
|
initPolicies(context: JobState) {
|
|
437
437
|
const expire = Pipe.resolve(
|
|
438
|
-
this.config.expire ??
|
|
438
|
+
this.config.expire ?? HMSH_EXPIRE_DURATION,
|
|
439
439
|
context
|
|
440
440
|
);
|
|
441
441
|
context.metadata.expire = expire;
|
|
@@ -131,16 +131,25 @@ class Hook extends Activity {
|
|
|
131
131
|
|
|
132
132
|
async registerHook(multi?: RedisMulti): Promise<string | void> {
|
|
133
133
|
if (this.config.hook?.topic) {
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
return await this.engine.taskService.registerWebHook(
|
|
135
|
+
this.config.hook.topic,
|
|
136
|
+
this.context,
|
|
137
|
+
this.resolveDad(),
|
|
138
|
+
multi
|
|
139
|
+
);
|
|
136
140
|
} else if (this.config.sleep) {
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
const duration = Pipe.resolve(
|
|
142
|
+
this.config.sleep,
|
|
143
|
+
this.context,
|
|
144
|
+
);
|
|
145
|
+
await this.engine.taskService.registerTimeHook(
|
|
146
|
+
this.context.metadata.jid,
|
|
147
|
+
this.context.metadata.gid,
|
|
148
|
+
`${this.metadata.aid}${this.metadata.dad || ''}`,
|
|
149
|
+
'sleep',
|
|
150
|
+
duration,
|
|
151
|
+
);
|
|
152
|
+
return this.context.metadata.jid;
|
|
144
153
|
}
|
|
145
154
|
}
|
|
146
155
|
|
|
@@ -47,7 +47,7 @@ class Trigger extends Activity {
|
|
|
47
47
|
const multi = this.store.getMulti();
|
|
48
48
|
await this.setState(multi);
|
|
49
49
|
await this.setStats(multi);
|
|
50
|
-
await this.
|
|
50
|
+
await this.registerJobDependency(multi);
|
|
51
51
|
await multi.exec();
|
|
52
52
|
|
|
53
53
|
telemetry.mapActivityAttributes();
|
|
@@ -180,13 +180,17 @@ class Trigger extends Activity {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Registers this job as a dependent of the parent job
|
|
183
|
+
* Registers this job as a dependent of the parent job; when the
|
|
184
|
+
* parent job is interrupted, this job will be interrupted
|
|
184
185
|
*/
|
|
185
|
-
async
|
|
186
|
-
const depKey = this.config.stats?.parent;
|
|
187
|
-
|
|
186
|
+
async registerJobDependency(multi?: RedisMulti): Promise<void> {
|
|
187
|
+
const depKey = this.config.stats?.parent ?? this.context.metadata.pj;
|
|
188
|
+
let resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
|
|
189
|
+
if (!resolvedDepKey) {
|
|
190
|
+
resolvedDepKey = this.context.metadata.pj;
|
|
191
|
+
}
|
|
188
192
|
if (resolvedDepKey) {
|
|
189
|
-
await this.store.
|
|
193
|
+
await this.store.registerJobDependency(
|
|
190
194
|
resolvedDepKey,
|
|
191
195
|
this.context.metadata.tpc,
|
|
192
196
|
this.context.metadata.jid,
|
|
@@ -11,11 +11,12 @@ import { JobState } from '../../types/job';
|
|
|
11
11
|
import { KeyService, KeyType } from '../../modules/key';
|
|
12
12
|
import { Search } from './search';
|
|
13
13
|
import { StreamStatus } from '../../types';
|
|
14
|
-
import {
|
|
14
|
+
import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
|
|
15
15
|
|
|
16
16
|
export class ClientService {
|
|
17
17
|
|
|
18
18
|
connection: Connection;
|
|
19
|
+
topics: string[] = [];
|
|
19
20
|
options: WorkflowOptions;
|
|
20
21
|
static instances = new Map<string, HotMesh | Promise<HotMesh>>();
|
|
21
22
|
|
|
@@ -23,14 +24,22 @@ export class ClientService {
|
|
|
23
24
|
this.connection = config.connection;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
getHotMeshClient = async (
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
getHotMeshClient = async (workflowTopic: string, namespace?: string) => {
|
|
28
|
+
//use the cached instance
|
|
29
|
+
const instanceId = 'SINGLETON';
|
|
30
|
+
if (ClientService.instances.has(instanceId)) {
|
|
31
|
+
const hotMeshClient = await ClientService.instances.get(instanceId);
|
|
32
|
+
if (!this.topics.includes(workflowTopic)) {
|
|
33
|
+
this.topics.push(workflowTopic);
|
|
34
|
+
await this.createStream(hotMeshClient, workflowTopic, namespace);
|
|
35
|
+
}
|
|
36
|
+
return hotMeshClient;
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
//create and cache an instance
|
|
32
40
|
const hotMeshClient = HotMesh.init({
|
|
33
41
|
appId: namespace ?? APP_ID,
|
|
42
|
+
logLevel: HMSH_LOGLEVEL,
|
|
34
43
|
engine: {
|
|
35
44
|
redis: {
|
|
36
45
|
class: this.connection.class,
|
|
@@ -38,19 +47,27 @@ export class ClientService {
|
|
|
38
47
|
}
|
|
39
48
|
}
|
|
40
49
|
});
|
|
41
|
-
ClientService.instances.set(
|
|
50
|
+
ClientService.instances.set(instanceId, hotMeshClient);
|
|
51
|
+
await this.createStream(await hotMeshClient, workflowTopic, namespace);
|
|
52
|
+
await this.activateWorkflow(await hotMeshClient, namespace ?? APP_ID);
|
|
53
|
+
return hotMeshClient;
|
|
54
|
+
}
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Creates a stream (Redis `XGROUP.CREATE`) where events can be published (XADD).
|
|
58
|
+
* It is possible that the worker that will read from this stream channel
|
|
59
|
+
* has not yet been initialized, so this call ensures that the channel
|
|
60
|
+
* exists and is ready to serve as a container for events.
|
|
61
|
+
*/
|
|
62
|
+
createStream = async(hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => {
|
|
63
|
+
const store = hotMeshClient.engine.store;
|
|
64
|
+
const params = { appId: namespace ?? APP_ID, topic: workflowTopic };
|
|
46
65
|
const streamKey = store.mintKey(KeyType.STREAMS, params);
|
|
47
66
|
try {
|
|
48
67
|
await store.xgroup('CREATE', streamKey, 'WORKER', '$', 'MKSTREAM');
|
|
49
68
|
} catch (err) {
|
|
50
69
|
//ignore if already exists
|
|
51
70
|
}
|
|
52
|
-
await this.activateWorkflow(await hotMeshClient, namespace ?? APP_ID);
|
|
53
|
-
return hotMeshClient;
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
/**
|
|
@@ -105,7 +122,7 @@ export class ClientService {
|
|
|
105
122
|
const payload = {
|
|
106
123
|
arguments: [...options.args],
|
|
107
124
|
originJobId: options.originJobId,
|
|
108
|
-
expire: options.expire ??
|
|
125
|
+
expire: options.expire ?? HMSH_EXPIRE_JOB_SECONDS,
|
|
109
126
|
parentWorkflowId: options.parentWorkflowId,
|
|
110
127
|
workflowId: options.workflowId || HotMesh.guid(),
|
|
111
128
|
workflowTopic: workflowTopic,
|
|
@@ -155,9 +172,8 @@ export class ClientService {
|
|
|
155
172
|
if (options.search?.data) {
|
|
156
173
|
const searchSessionId = `-search-${HotMesh.guid()}-0`;
|
|
157
174
|
const search = new Search(options.workflowId, hotMeshClient, searchSessionId);
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
175
|
+
const entries = Object.entries(options.search.data).flat();
|
|
176
|
+
await search.set(...entries);
|
|
161
177
|
}
|
|
162
178
|
return msgId;
|
|
163
179
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HMSH_CODE_INTERRUPT } from '../../modules/enums';
|
|
2
2
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
3
|
import { JobInterruptOptions, JobOutput } from '../../types/job';
|
|
4
4
|
import { StreamError } from '../../types/stream';
|
|
@@ -101,7 +101,7 @@ export class WorkflowHandleService {
|
|
|
101
101
|
const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
|
|
102
102
|
if (state.metadata.err) {
|
|
103
103
|
const error = JSON.parse(state.metadata.err) as StreamError;
|
|
104
|
-
if (error.code ===
|
|
104
|
+
if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
|
|
105
105
|
return reject({ ...error, job_id: this.workflowId });
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -117,7 +117,7 @@ export class WorkflowHandleService {
|
|
|
117
117
|
this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
|
|
118
118
|
if (state.metadata.err) {
|
|
119
119
|
const error = JSON.parse(state.metadata.err) as StreamError;
|
|
120
|
-
if (error.code ===
|
|
120
|
+
if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
|
|
121
121
|
return await complete(null, state.metadata.err);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
StreamData,
|
|
24
24
|
StreamDataResponse,
|
|
25
25
|
StreamStatus } from '../../types/stream';
|
|
26
|
+
import { HMSH_LOGLEVEL } from '../../modules/enums';
|
|
26
27
|
|
|
27
28
|
export class WorkerService {
|
|
28
29
|
static activityRegistry: Registry = {}; //user's activities
|
|
@@ -36,7 +37,7 @@ export class WorkerService {
|
|
|
36
37
|
return await WorkerService.instances.get(workflowTopic);
|
|
37
38
|
}
|
|
38
39
|
const hotMeshClient = HotMesh.init({
|
|
39
|
-
logLevel: options?.logLevel
|
|
40
|
+
logLevel: options?.logLevel ?? HMSH_LOGLEVEL,
|
|
40
41
|
appId: config.namespace ?? APP_ID,
|
|
41
42
|
engine: { redis: { ...WorkerService.connection } }
|
|
42
43
|
});
|
|
@@ -121,7 +122,7 @@ export class WorkerService {
|
|
|
121
122
|
options: config.connection.options as RedisOptions
|
|
122
123
|
};
|
|
123
124
|
const hotMeshWorker = await HotMesh.init({
|
|
124
|
-
logLevel: config.options?.logLevel
|
|
125
|
+
logLevel: config.options?.logLevel ?? HMSH_LOGLEVEL,
|
|
125
126
|
appId: config.namespace ?? APP_ID,
|
|
126
127
|
engine: { redis: redisConfig },
|
|
127
128
|
workers: [
|
|
@@ -172,7 +173,7 @@ export class WorkerService {
|
|
|
172
173
|
options: config.connection.options as RedisOptions
|
|
173
174
|
};
|
|
174
175
|
const hotMeshWorker = await HotMesh.init({
|
|
175
|
-
logLevel: config.options?.logLevel
|
|
176
|
+
logLevel: config.options?.logLevel ?? HMSH_LOGLEVEL,
|
|
176
177
|
appId: config.namespace ?? APP_ID,
|
|
177
178
|
engine: { redis: redisConfig },
|
|
178
179
|
workers: [{
|
package/services/engine/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { KeyType } from '../../modules/key';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
HMSH_OTT_WAIT_TIME,
|
|
4
|
+
HMSH_CODE_SUCCESS,
|
|
5
|
+
HMSH_CODE_PENDING,
|
|
6
|
+
HMSH_CODE_TIMEOUT,
|
|
7
|
+
HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
|
|
8
8
|
import {
|
|
9
9
|
formatISODate,
|
|
10
10
|
getSubscriptionTopic,
|
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
StreamError,
|
|
78
78
|
StreamRole,
|
|
79
79
|
StreamStatus } from '../../types/stream';
|
|
80
|
+
import { WorkListTaskType } from '../../types/task';
|
|
80
81
|
|
|
81
82
|
class EngineService {
|
|
82
83
|
namespace: string;
|
|
@@ -438,10 +439,10 @@ class EngineService {
|
|
|
438
439
|
streamData.code = error.code;
|
|
439
440
|
} else if (emit) {
|
|
440
441
|
streamData.status = StreamStatus.PENDING;
|
|
441
|
-
streamData.code =
|
|
442
|
+
streamData.code = HMSH_CODE_PENDING;
|
|
442
443
|
} else {
|
|
443
444
|
streamData.status = StreamStatus.SUCCESS;
|
|
444
|
-
streamData.code =
|
|
445
|
+
streamData.code = HMSH_CODE_SUCCESS;
|
|
445
446
|
}
|
|
446
447
|
return (await this.router?.publishMessage(null, streamData)) as string;
|
|
447
448
|
}
|
|
@@ -489,7 +490,7 @@ class EngineService {
|
|
|
489
490
|
};
|
|
490
491
|
return await this.router.publishMessage(null, streamData) as string;
|
|
491
492
|
}
|
|
492
|
-
async hookTime(jobId: string, gId: string, activityId: string, type?:
|
|
493
|
+
async hookTime(jobId: string, gId: string, activityId: string, type?: WorkListTaskType): Promise<string | void> {
|
|
493
494
|
if (type === 'interrupt') {
|
|
494
495
|
return await this.interrupt(
|
|
495
496
|
activityId, //note: 'activityId' is the actually job topic
|
|
@@ -499,7 +500,6 @@ class EngineService {
|
|
|
499
500
|
} else if (type === 'expire') {
|
|
500
501
|
return await this.store.expireJob(jobId, 1);
|
|
501
502
|
}
|
|
502
|
-
//'sleep': parse the activityId into parts
|
|
503
503
|
const [aid, ...dimensions] = activityId.split(',');
|
|
504
504
|
const dad = `,${dimensions.join(',')}`;
|
|
505
505
|
const streamData: StreamData = {
|
|
@@ -575,7 +575,7 @@ class EngineService {
|
|
|
575
575
|
return await this.subscribe.punsubscribe(KeyType.QUORUM, this.appId, wild);
|
|
576
576
|
}
|
|
577
577
|
//publish and await (returns the job and data (if ready)); throws error with jobid if not
|
|
578
|
-
async pubsub(topic: string, data: JobData, context?: JobState | null, timeout =
|
|
578
|
+
async pubsub(topic: string, data: JobData, context?: JobState | null, timeout = HMSH_OTT_WAIT_TIME): Promise<JobOutput> {
|
|
579
579
|
context = {
|
|
580
580
|
metadata: {
|
|
581
581
|
ngn: this.guid,
|
|
@@ -597,9 +597,10 @@ class EngineService {
|
|
|
597
597
|
}
|
|
598
598
|
});
|
|
599
599
|
setTimeout(() => {
|
|
600
|
+
//note: job is still active (the subscriber timed out)
|
|
600
601
|
this.delistJobCallback(jobId);
|
|
601
602
|
reject({
|
|
602
|
-
code:
|
|
603
|
+
code: HMSH_CODE_TIMEOUT,
|
|
603
604
|
message: 'timeout',
|
|
604
605
|
job_id: jobId
|
|
605
606
|
} as StreamError);
|
|
@@ -685,7 +686,7 @@ class EngineService {
|
|
|
685
686
|
* it will be expired immediately.
|
|
686
687
|
*/
|
|
687
688
|
resolveExpires(context: JobState, options: JobCompletionOptions): number {
|
|
688
|
-
return options.expire ?? context.metadata.expire ??
|
|
689
|
+
return options.expire ?? context.metadata.expire ?? HMSH_EXPIRE_JOB_SECONDS;
|
|
689
690
|
}
|
|
690
691
|
|
|
691
692
|
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
import {
|
|
17
17
|
HotMeshConfig,
|
|
18
18
|
HotMeshManifest } from '../../types/hotmesh';
|
|
19
|
-
import { JobMessageCallback } from '../../types/quorum';
|
|
19
|
+
import { JobMessageCallback, QuorumProfile } from '../../types/quorum';
|
|
20
20
|
import {
|
|
21
21
|
JobStatsInput,
|
|
22
22
|
GetStatsOptions,
|
|
@@ -137,6 +137,9 @@ class HotMeshService {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// ************* COMPILER METHODS *************
|
|
140
|
+
async rollCall(delay?: number): Promise<QuorumProfile[]> {
|
|
141
|
+
return await this.quorum?.rollCall(delay);
|
|
142
|
+
}
|
|
140
143
|
async plan(path: string): Promise<HotMeshManifest> {
|
|
141
144
|
return await this.engine?.plan(path);
|
|
142
145
|
}
|
|
@@ -147,10 +150,6 @@ class HotMeshService {
|
|
|
147
150
|
//activation is a quorum operation
|
|
148
151
|
return await this.quorum?.activate(version, delay);
|
|
149
152
|
}
|
|
150
|
-
async inventory(version: string, delay?: number): Promise<number> {
|
|
151
|
-
//get count of all peers
|
|
152
|
-
return await this.quorum?.inventory(delay);
|
|
153
|
-
}
|
|
154
153
|
|
|
155
154
|
// ************* REPORTER METHODS *************
|
|
156
155
|
async getStats(topic: string, query: JobStatsInput): Promise<StatsResponse> {
|
package/services/quorum/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
|
|
|
14
14
|
import {
|
|
15
15
|
QuorumMessage,
|
|
16
16
|
QuorumMessageCallback,
|
|
17
|
+
QuorumProfile,
|
|
17
18
|
SubscriptionCallback,
|
|
18
19
|
ThrottleMessage
|
|
19
20
|
} from '../../types/quorum';
|
|
@@ -26,10 +27,10 @@ const QUORUM_DELAY = 250;
|
|
|
26
27
|
|
|
27
28
|
class QuorumService {
|
|
28
29
|
namespace: string;
|
|
29
|
-
apps: HotMeshApps | null;
|
|
30
30
|
appId: string;
|
|
31
31
|
guid: string;
|
|
32
32
|
engine: EngineService;
|
|
33
|
+
profiles: QuorumProfile[] = [];
|
|
33
34
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
34
35
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
35
36
|
logger: ILogger;
|
|
@@ -108,9 +109,12 @@ class QuorumService {
|
|
|
108
109
|
if (message.type === 'activate') {
|
|
109
110
|
self.engine.setCacheMode(message.cache_mode, message.until_version);
|
|
110
111
|
} else if (message.type === 'ping') {
|
|
111
|
-
|
|
112
|
+
self.sayPong(self.appId, self.guid, message.originator, message.details);
|
|
112
113
|
} else if (message.type === 'pong' && self.guid === message.originator) {
|
|
113
114
|
self.quorum = self.quorum + 1;
|
|
115
|
+
if (message.profile) {
|
|
116
|
+
self.profiles.push(message.profile);
|
|
117
|
+
}
|
|
114
118
|
} else if (message.type === 'throttle') {
|
|
115
119
|
self.engine.throttle(message.throttle);
|
|
116
120
|
} else if (message.type === 'work') {
|
|
@@ -127,20 +131,38 @@ class QuorumService {
|
|
|
127
131
|
};
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
async sayPong(appId: string, guid: string, originator: string) {
|
|
134
|
+
async sayPong(appId: string, guid: string, originator: string, details = false) {
|
|
135
|
+
let profile: QuorumProfile;
|
|
136
|
+
if (details) {
|
|
137
|
+
profile = {
|
|
138
|
+
engine_id: this.guid,
|
|
139
|
+
namespace: this.namespace,
|
|
140
|
+
app_id: this.appId,
|
|
141
|
+
stream: this.engine.stream.mintKey(KeyType.STREAMS, { appId: this.appId })
|
|
142
|
+
};
|
|
143
|
+
}
|
|
131
144
|
this.store.publish(
|
|
132
145
|
KeyType.QUORUM,
|
|
133
|
-
{
|
|
146
|
+
{
|
|
147
|
+
type: 'pong',
|
|
148
|
+
guid, originator,
|
|
149
|
+
profile,
|
|
150
|
+
},
|
|
134
151
|
appId,
|
|
135
152
|
);
|
|
136
153
|
}
|
|
137
154
|
|
|
138
|
-
async requestQuorum(delay = QUORUM_DELAY): Promise<number> {
|
|
155
|
+
async requestQuorum(delay = QUORUM_DELAY, details = false): Promise<number> {
|
|
139
156
|
const quorum = this.quorum;
|
|
140
157
|
this.quorum = 0;
|
|
158
|
+
this.profiles.length = 0;
|
|
141
159
|
await this.store.publish(
|
|
142
160
|
KeyType.QUORUM,
|
|
143
|
-
{
|
|
161
|
+
{
|
|
162
|
+
type: 'ping',
|
|
163
|
+
originator: this.guid,
|
|
164
|
+
details,
|
|
165
|
+
},
|
|
144
166
|
this.appId,
|
|
145
167
|
);
|
|
146
168
|
await sleepFor(delay);
|
|
@@ -166,12 +188,26 @@ class QuorumService {
|
|
|
166
188
|
|
|
167
189
|
|
|
168
190
|
// ************* COMPILER METHODS *************
|
|
169
|
-
async
|
|
170
|
-
await this.requestQuorum(delay);
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
191
|
+
async rollCall(delay = QUORUM_DELAY): Promise<QuorumProfile[]> {
|
|
192
|
+
await this.requestQuorum(delay, true);
|
|
193
|
+
const targetStreams = [];
|
|
194
|
+
const multi = this.store.getMulti();
|
|
195
|
+
this.profiles.forEach((profile: QuorumProfile) => {
|
|
196
|
+
if (!targetStreams.includes(profile.stream)) {
|
|
197
|
+
targetStreams.push(profile.stream);
|
|
198
|
+
this.store.xlen(profile.stream, multi);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
const stream_depths = await multi.exec() as number[];
|
|
202
|
+
this.profiles.forEach(async (profile: QuorumProfile) => {
|
|
203
|
+
const index = targetStreams.indexOf(profile.stream);
|
|
204
|
+
if (index != -1) {
|
|
205
|
+
profile.stream_depth = Array.isArray(stream_depths[index]) ?
|
|
206
|
+
stream_depths[index][1] :
|
|
207
|
+
stream_depths[index];
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
return this.profiles;
|
|
175
211
|
}
|
|
176
212
|
async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
|
|
177
213
|
version = version.toString();
|