@hotmeshio/hotmesh 0.0.48 → 0.0.50
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 +1 -1
- package/build/modules/enums.d.ts +1 -0
- package/build/modules/enums.js +2 -1
- package/build/modules/key.d.ts +5 -1
- package/build/modules/key.js +10 -2
- package/build/package.json +2 -1
- package/build/services/activities/await.js +6 -0
- package/build/services/activities/hook.js +1 -1
- package/build/services/activities/trigger.d.ts +1 -0
- package/build/services/activities/trigger.js +23 -2
- package/build/services/durable/exporter.js +19 -5
- package/build/services/durable/meshos.js +11 -6
- package/build/services/durable/search.d.ts +20 -1
- package/build/services/durable/search.js +73 -25
- package/build/services/durable/worker.js +10 -0
- package/build/services/durable/workflow.d.ts +1 -0
- package/build/services/durable/workflow.js +17 -1
- package/build/services/engine/index.d.ts +1 -1
- package/build/services/engine/index.js +12 -3
- package/build/services/exporter/index.js +3 -2
- package/build/services/hotmesh/index.js +4 -0
- package/build/services/quorum/index.d.ts +11 -2
- package/build/services/quorum/index.js +33 -0
- package/build/services/router/index.d.ts +15 -0
- package/build/services/router/index.js +55 -7
- package/build/services/serializer/index.js +1 -1
- package/build/services/store/clients/redis.js +2 -0
- package/build/services/store/index.d.ts +6 -4
- package/build/services/store/index.js +86 -21
- package/build/services/task/index.d.ts +2 -1
- package/build/services/task/index.js +30 -13
- package/build/services/worker/index.d.ts +13 -2
- package/build/services/worker/index.js +44 -3
- package/build/types/activity.d.ts +1 -0
- package/build/types/durable.d.ts +9 -0
- package/build/types/exporter.d.ts +2 -0
- package/build/types/job.d.ts +1 -0
- package/build/types/quorum.d.ts +22 -8
- package/build/types/stream.d.ts +1 -0
- package/modules/enums.ts +1 -0
- package/modules/key.ts +7 -2
- package/package.json +2 -1
- package/services/activities/await.ts +6 -0
- package/services/activities/hook.ts +1 -0
- package/services/activities/trigger.ts +25 -1
- package/services/durable/exporter.ts +18 -7
- package/services/durable/meshos.ts +10 -6
- package/services/durable/search.ts +73 -26
- package/services/durable/worker.ts +13 -1
- package/services/durable/workflow.ts +18 -0
- package/services/engine/index.ts +13 -5
- package/services/exporter/index.ts +3 -2
- package/services/hotmesh/index.ts +4 -0
- package/services/quorum/index.ts +38 -2
- package/services/router/index.ts +59 -9
- package/services/serializer/index.ts +1 -1
- package/services/store/clients/redis.ts +2 -0
- package/services/store/index.ts +108 -22
- package/services/task/index.ts +31 -11
- package/services/worker/index.ts +49 -5
- package/types/activity.ts +1 -0
- package/types/durable.ts +11 -0
- package/types/exporter.ts +2 -0
- package/types/job.ts +1 -0
- package/types/quorum.ts +28 -13
- package/types/stream.ts +1 -0
package/services/task/index.ts
CHANGED
|
@@ -11,12 +11,14 @@ import { KeyType } from '../../types/hotmesh';
|
|
|
11
11
|
import { JobCompletionOptions, JobState } from '../../types/job';
|
|
12
12
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
13
13
|
import { WorkListTaskType } from '../../types/task';
|
|
14
|
+
import { VALSEP, WEBSEP } from '../../modules/key';
|
|
14
15
|
|
|
15
16
|
class TaskService {
|
|
16
17
|
store: StoreService<RedisClient, RedisMulti>;
|
|
17
18
|
logger: ILogger;
|
|
18
19
|
cleanupTimeout: NodeJS.Timeout | null = null;
|
|
19
20
|
isScout: boolean = false;
|
|
21
|
+
errorCount = 0;
|
|
20
22
|
|
|
21
23
|
constructor(
|
|
22
24
|
store: StoreService<RedisClient, RedisMulti>,
|
|
@@ -29,8 +31,8 @@ class TaskService {
|
|
|
29
31
|
async processWebHooks(hookEventCallback: HookInterface): Promise<void> {
|
|
30
32
|
const workItemKey = await this.store.getActiveTaskQueue();
|
|
31
33
|
if (workItemKey) {
|
|
32
|
-
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(
|
|
33
|
-
const data = JSON.parse(sdata.join(
|
|
34
|
+
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(WEBSEP);
|
|
35
|
+
const data = JSON.parse(sdata.join(WEBSEP));
|
|
34
36
|
const destinationKey = `${sourceKey}:processed`;
|
|
35
37
|
const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
|
|
36
38
|
if (jobId) {
|
|
@@ -72,6 +74,7 @@ class TaskService {
|
|
|
72
74
|
activityId: string,
|
|
73
75
|
type: WorkListTaskType,
|
|
74
76
|
inSeconds = HMSH_FIDELITY_SECONDS,
|
|
77
|
+
dad: string,
|
|
75
78
|
multi?: RedisMulti,
|
|
76
79
|
): Promise<void> {
|
|
77
80
|
const fromNow = Date.now() + (inSeconds * 1000);
|
|
@@ -83,6 +86,7 @@ class TaskService {
|
|
|
83
86
|
activityId,
|
|
84
87
|
type,
|
|
85
88
|
awakenTimeSlot,
|
|
89
|
+
dad,
|
|
86
90
|
multi,
|
|
87
91
|
);
|
|
88
92
|
}
|
|
@@ -129,21 +133,29 @@ class TaskService {
|
|
|
129
133
|
await timeEventCallback(target, gId, activityId, type);
|
|
130
134
|
}
|
|
131
135
|
await sleepFor(0);
|
|
136
|
+
this.errorCount = 0;
|
|
132
137
|
this.processTimeHooks(timeEventCallback, listKey);
|
|
133
138
|
} else if (workListTask) {
|
|
134
139
|
//a worklist was just emptied; try again immediately
|
|
135
140
|
await sleepFor(0);
|
|
141
|
+
this.errorCount = 0;
|
|
136
142
|
this.processTimeHooks(timeEventCallback);
|
|
137
143
|
} else {
|
|
138
144
|
//no worklists exist; sleep before checking
|
|
139
145
|
let sleep = XSleepFor(HMSH_FIDELITY_SECONDS * 1000);
|
|
140
146
|
this.cleanupTimeout = sleep.timerId;
|
|
141
147
|
await sleep.promise;
|
|
148
|
+
this.errorCount = 0;
|
|
142
149
|
this.processTimeHooks(timeEventCallback);
|
|
143
150
|
}
|
|
144
151
|
} catch (err) {
|
|
145
|
-
//
|
|
146
|
-
|
|
152
|
+
//most common reasons: deleted job not found; container stopping; test stopping
|
|
153
|
+
//less common: redis/cluster down; retry with fallback (5s max main reassignment)
|
|
154
|
+
this.logger.warn('task-process-timehooks-error', err);
|
|
155
|
+
await sleepFor(1_000 * this.errorCount++);
|
|
156
|
+
if (this.errorCount < 5) {
|
|
157
|
+
this.processTimeHooks(timeEventCallback);
|
|
158
|
+
}
|
|
147
159
|
}
|
|
148
160
|
} else {
|
|
149
161
|
//didn't get the scout role; try again in 'one-ish' minutes
|
|
@@ -174,10 +186,18 @@ class TaskService {
|
|
|
174
186
|
const jobId = context.metadata.jid;
|
|
175
187
|
const gId = context.metadata.gid;
|
|
176
188
|
const activityId = hookRule.to;
|
|
189
|
+
//composite keys are used to fully describe the task target
|
|
190
|
+
const compositeJobKey = [
|
|
191
|
+
activityId,
|
|
192
|
+
dad,
|
|
193
|
+
gId,
|
|
194
|
+
jobId
|
|
195
|
+
].join(WEBSEP);
|
|
196
|
+
|
|
177
197
|
const hook: HookSignal = {
|
|
178
198
|
topic,
|
|
179
199
|
resolved,
|
|
180
|
-
jobId:
|
|
200
|
+
jobId: compositeJobKey,
|
|
181
201
|
}
|
|
182
202
|
await this.store.setHookSignal(hook, multi);
|
|
183
203
|
return jobId;
|
|
@@ -196,17 +216,17 @@ class TaskService {
|
|
|
196
216
|
const resolved = Pipe.resolve(mapExpression, context);
|
|
197
217
|
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
198
218
|
if (!hookSignalId) {
|
|
199
|
-
//messages can be double-processed; not an issue; return undefined
|
|
200
|
-
//users can also provide a bogus topic; not an issue; return undefined
|
|
219
|
+
//messages can be double-processed; not an issue; return `undefined`
|
|
220
|
+
//users can also provide a bogus topic; not an issue; return `undefined`
|
|
201
221
|
return undefined;
|
|
202
222
|
}
|
|
203
|
-
//`aid` is part of
|
|
223
|
+
//`aid` is part of composite key, but the hook `topic` is its public interface;
|
|
204
224
|
// this means that a new version of the graph can be deployed and the
|
|
205
225
|
// topic can be re-mapped to a different activity id. Outside callers
|
|
206
226
|
// can adhere to the unchanged contract (calling the same topic),
|
|
207
|
-
// while the internal system can be updated in real
|
|
208
|
-
const [_aid, dad, gid, ...jid] = hookSignalId.split(
|
|
209
|
-
return [jid.join(
|
|
227
|
+
// while the internal system can be updated in real-time as necessary.
|
|
228
|
+
const [_aid, dad, gid, ...jid] = hookSignalId.split(WEBSEP);
|
|
229
|
+
return [jid.join(WEBSEP), hookRule.to, dad, gid];
|
|
210
230
|
} else {
|
|
211
231
|
throw new Error('signal-not-found');
|
|
212
232
|
}
|
package/services/worker/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KeyType } from "../../modules/key";
|
|
2
|
-
import { formatISODate, getSystemHealth, identifyRedisType } from "../../modules/utils";
|
|
2
|
+
import { XSleepFor, formatISODate, getSystemHealth, identifyRedisType, sleepFor } from "../../modules/utils";
|
|
3
3
|
import { ConnectorService } from "../connector";
|
|
4
4
|
import { ILogger } from "../logger";
|
|
5
5
|
import { Router } from "../router";
|
|
@@ -17,10 +17,12 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
|
|
|
17
17
|
import {
|
|
18
18
|
QuorumMessage,
|
|
19
19
|
QuorumProfile,
|
|
20
|
+
RollCallMessage,
|
|
20
21
|
SubscriptionCallback } from "../../types/quorum";
|
|
21
22
|
import { RedisClient, RedisMulti } from "../../types/redis";
|
|
22
23
|
import { RedisClientType } from '../../types/redisclient';
|
|
23
|
-
import { StreamRole } from "../../types/stream";
|
|
24
|
+
import { StreamData, StreamRole, StreamDataResponse } from "../../types/stream";
|
|
25
|
+
import { HMSH_QUORUM_ROLLCALL_CYCLES } from "../../modules/enums";
|
|
24
26
|
|
|
25
27
|
class WorkerService {
|
|
26
28
|
namespace: string;
|
|
@@ -28,6 +30,7 @@ class WorkerService {
|
|
|
28
30
|
guid: string;
|
|
29
31
|
topic: string;
|
|
30
32
|
config: HotMeshConfig;
|
|
33
|
+
callback: (streamData: StreamData) => Promise<StreamDataResponse|void>;
|
|
31
34
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
32
35
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
33
36
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
@@ -35,6 +38,7 @@ class WorkerService {
|
|
|
35
38
|
logger: ILogger;
|
|
36
39
|
reporting = false;
|
|
37
40
|
inited: string;
|
|
41
|
+
rollCallInterval: NodeJS.Timeout;
|
|
38
42
|
|
|
39
43
|
static async init(
|
|
40
44
|
namespace: string,
|
|
@@ -58,6 +62,7 @@ class WorkerService {
|
|
|
58
62
|
service.namespace = namespace;
|
|
59
63
|
service.appId = appId;
|
|
60
64
|
service.guid = guid;
|
|
65
|
+
service.callback = worker.callback;
|
|
61
66
|
service.topic = worker.topic;
|
|
62
67
|
service.config = config;
|
|
63
68
|
service.logger = logger;
|
|
@@ -155,14 +160,51 @@ class WorkerService {
|
|
|
155
160
|
return async (topic: string, message: QuorumMessage) => {
|
|
156
161
|
self.logger.debug('worker-event-received', { topic, type: message.type });
|
|
157
162
|
if (message.type === 'throttle') {
|
|
158
|
-
|
|
163
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
164
|
+
self.throttle(message.throttle);
|
|
165
|
+
}
|
|
159
166
|
} else if(message.type === 'ping') {
|
|
160
167
|
self.sayPong(self.appId, self.guid, message.originator, message.details);
|
|
168
|
+
} else if(message.type === 'rollcall') {
|
|
169
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
170
|
+
self.doRollCall(message);
|
|
171
|
+
}
|
|
161
172
|
}
|
|
162
173
|
};
|
|
163
174
|
}
|
|
164
175
|
|
|
165
|
-
|
|
176
|
+
/**
|
|
177
|
+
* A quorum-wide command to broadcaset system details.
|
|
178
|
+
*
|
|
179
|
+
*/
|
|
180
|
+
async doRollCall(message: RollCallMessage) {
|
|
181
|
+
let iteration = 0;
|
|
182
|
+
let max = !isNaN(message.max) ? message.max : HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
183
|
+
if (this.rollCallInterval) clearTimeout(this.rollCallInterval);
|
|
184
|
+
const base = (message.interval / 2);
|
|
185
|
+
const amount = base + Math.ceil(Math.random() * base);
|
|
186
|
+
do {
|
|
187
|
+
await sleepFor(Math.ceil(Math.random() * 1000));
|
|
188
|
+
await this.sayPong(this.appId, this.guid, null, true, message.signature);
|
|
189
|
+
if (!message.interval) return;
|
|
190
|
+
const { promise, timerId } = XSleepFor(amount * 1000);
|
|
191
|
+
this.rollCallInterval = timerId;
|
|
192
|
+
await promise;
|
|
193
|
+
} while (this.rollCallInterval && iteration++ < max - 1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
cancelRollCall() {
|
|
197
|
+
if (this.rollCallInterval) {
|
|
198
|
+
clearTimeout(this.rollCallInterval);
|
|
199
|
+
delete this.rollCallInterval;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
stop() {
|
|
204
|
+
this.cancelRollCall();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async sayPong(appId: string, guid: string, originator?: string, details = false, signature = false) {
|
|
166
208
|
let profile: QuorumProfile;
|
|
167
209
|
if (details) {
|
|
168
210
|
const params = {
|
|
@@ -183,13 +225,15 @@ class WorkerService {
|
|
|
183
225
|
reclaimDelay: this.router.reclaimDelay,
|
|
184
226
|
reclaimCount: this.router.reclaimCount,
|
|
185
227
|
system: await getSystemHealth(),
|
|
228
|
+
signature: signature ? this.callback.toString() : undefined,
|
|
186
229
|
};
|
|
187
230
|
}
|
|
188
231
|
this.store.publish(
|
|
189
232
|
KeyType.QUORUM,
|
|
190
233
|
{
|
|
191
234
|
type: 'pong',
|
|
192
|
-
guid,
|
|
235
|
+
guid,
|
|
236
|
+
originator,
|
|
193
237
|
profile,
|
|
194
238
|
},
|
|
195
239
|
appId,
|
package/types/activity.ts
CHANGED
package/types/durable.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { LogLevel } from './logger';
|
|
2
2
|
import { RedisClass, RedisOptions } from './redis';
|
|
3
|
+
import { StringStringType } from './serializer';
|
|
3
4
|
|
|
4
5
|
type WorkflowConfig = {
|
|
5
6
|
backoffCoefficient?: number; //default 10
|
|
@@ -15,6 +16,16 @@ type WorkflowContext = {
|
|
|
15
16
|
*/
|
|
16
17
|
counter: number;
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* number as string for the replay cursor
|
|
21
|
+
*/
|
|
22
|
+
cursor: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* the replay hash of name/value pairs representing prior executions
|
|
26
|
+
*/
|
|
27
|
+
replay: StringStringType;
|
|
28
|
+
|
|
18
29
|
/**
|
|
19
30
|
* the HotMesh App namespace. `durable` is the default.
|
|
20
31
|
*/
|
package/types/exporter.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface JobTimeline {
|
|
|
26
26
|
dimension: string; //dimensional isolate path
|
|
27
27
|
duplex: 'entry' | 'exit'; //activity entry or exit
|
|
28
28
|
timestamp: string; //actually a number but too many digits for JS
|
|
29
|
+
created?: string; //actually a number but too many digits for JS
|
|
30
|
+
updated?: string; //actually a number but too many digits for JS
|
|
29
31
|
actions?: ActivityAction[];
|
|
30
32
|
}
|
|
31
33
|
|
package/types/job.ts
CHANGED
|
@@ -17,6 +17,7 @@ type JobMetadata = {
|
|
|
17
17
|
pg?: string; //parent_generational_id (system assigned at trigger inception); pg is the parent job's gid (just in case user created/deleted/created a job with same jid)
|
|
18
18
|
pd?: string; //parent_dimensional_address
|
|
19
19
|
pa?: string; //parent_activity_id
|
|
20
|
+
px?: boolean; //sever the dependency chain if true (startChild/vs/executeChild)
|
|
20
21
|
ngn?: string; //engine guid (one time subscriptions)
|
|
21
22
|
app: string; //app_id
|
|
22
23
|
vrs: string; //app version
|
package/types/quorum.ts
CHANGED
|
@@ -50,50 +50,65 @@ export interface QuorumProfile {
|
|
|
50
50
|
reclaimDelay?: number;
|
|
51
51
|
reclaimCount?: number;
|
|
52
52
|
system?: SystemHealth;
|
|
53
|
+
signature?: string; //stringified function
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
interface QuorumMessageBase {
|
|
57
|
+
guid?: string;
|
|
58
|
+
topic?: string;
|
|
59
|
+
type?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Messages extending QuorumMessageBase
|
|
63
|
+
export interface PingMessage extends QuorumMessageBase {
|
|
57
64
|
type: 'ping';
|
|
58
65
|
originator: string; //guid
|
|
59
66
|
details?: boolean; //if true, all endpoints will include their profile
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
export interface WorkMessage {
|
|
69
|
+
export interface WorkMessage extends QuorumMessageBase {
|
|
63
70
|
type: 'work';
|
|
64
71
|
originator: string; //guid
|
|
65
72
|
}
|
|
66
73
|
|
|
67
|
-
export interface CronMessage {
|
|
74
|
+
export interface CronMessage extends QuorumMessageBase {
|
|
68
75
|
type: 'cron';
|
|
69
76
|
originator: string; //guid
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
export interface PongMessage {
|
|
79
|
+
export interface PongMessage extends QuorumMessageBase {
|
|
73
80
|
type: 'pong';
|
|
74
81
|
guid: string; //call initiator
|
|
75
82
|
originator: string; //clone of originator guid passed in ping
|
|
76
83
|
profile?: QuorumProfile; //contains details about the engine/worker
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
export interface ActivateMessage {
|
|
86
|
+
export interface ActivateMessage extends QuorumMessageBase {
|
|
80
87
|
type: 'activate';
|
|
81
88
|
cache_mode: 'nocache' | 'cache';
|
|
82
89
|
until_version: string;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
export interface JobMessage {
|
|
92
|
+
export interface JobMessage extends QuorumMessageBase {
|
|
86
93
|
type: 'job';
|
|
87
94
|
topic: string; //this comes from the 'publishes' field in the YAML
|
|
88
95
|
job: JobOutput
|
|
89
96
|
}
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
export interface ThrottleMessage {
|
|
98
|
+
export interface ThrottleMessage extends QuorumMessageBase {
|
|
93
99
|
type: 'throttle';
|
|
94
|
-
guid?: string; //target
|
|
95
|
-
topic?: string; //target
|
|
96
|
-
throttle: number; //0-n
|
|
100
|
+
guid?: string; //target engine AND workers with this guid
|
|
101
|
+
topic?: string; //target worker(s) matching this topic (pass null to only target the engine, pass undefined to target engine and workers)
|
|
102
|
+
throttle: number; //0-n; millis
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface RollCallMessage extends QuorumMessageBase {
|
|
106
|
+
type: 'rollcall';
|
|
107
|
+
guid?: string; //target the engine quorum
|
|
108
|
+
topic?: string | null; //target a worker if string; suppress if `null`;
|
|
109
|
+
interval: number; //every 'n' seconds
|
|
110
|
+
max?: number; //max broadcasts
|
|
111
|
+
signature?: boolean; //include bound worker function in broadcast
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
export interface JobMessageCallback {
|
|
@@ -114,4 +129,4 @@ export interface QuorumMessageCallback {
|
|
|
114
129
|
* These messages serve to coordinate the cache invalidation and switch-over
|
|
115
130
|
* to the new version without any downtime and a coordinating parent server.
|
|
116
131
|
*/
|
|
117
|
-
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
|
|
132
|
+
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | RollCallMessage | CronMessage;
|
package/types/stream.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface StreamData {
|
|
|
42
42
|
trc?: string; //trace id
|
|
43
43
|
spn?: string; //span id
|
|
44
44
|
try?: number; //current try count
|
|
45
|
+
await?: boolean; //(waitfor) if explicitly false, do not await; sever the connection
|
|
45
46
|
};
|
|
46
47
|
type?: StreamDataType;
|
|
47
48
|
data: Record<string, unknown>;
|