@hotmeshio/hotmesh 0.0.49 → 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/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/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/serializer/index.js +1 -1
- package/build/services/store/index.d.ts +5 -5
- package/build/services/store/index.js +43 -22
- 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/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/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/serializer/index.ts +1 -1
- package/services/store/index.ts +51 -24
- package/services/task/index.ts +31 -11
- package/services/worker/index.ts +49 -5
- package/types/activity.ts +1 -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
|
@@ -5,18 +5,20 @@ const enums_1 = require("../../modules/enums");
|
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
6
|
const pipe_1 = require("../pipe");
|
|
7
7
|
const hotmesh_1 = require("../../types/hotmesh");
|
|
8
|
+
const key_1 = require("../../modules/key");
|
|
8
9
|
class TaskService {
|
|
9
10
|
constructor(store, logger) {
|
|
10
11
|
this.cleanupTimeout = null;
|
|
11
12
|
this.isScout = false;
|
|
13
|
+
this.errorCount = 0;
|
|
12
14
|
this.logger = logger;
|
|
13
15
|
this.store = store;
|
|
14
16
|
}
|
|
15
17
|
async processWebHooks(hookEventCallback) {
|
|
16
18
|
const workItemKey = await this.store.getActiveTaskQueue();
|
|
17
19
|
if (workItemKey) {
|
|
18
|
-
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(
|
|
19
|
-
const data = JSON.parse(sdata.join(
|
|
20
|
+
const [topic, sourceKey, scrub, ...sdata] = workItemKey.split(key_1.WEBSEP);
|
|
21
|
+
const data = JSON.parse(sdata.join(key_1.WEBSEP));
|
|
20
22
|
const destinationKey = `${sourceKey}:processed`;
|
|
21
23
|
const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
|
|
22
24
|
if (jobId) {
|
|
@@ -41,11 +43,11 @@ class TaskService {
|
|
|
41
43
|
await this.store.registerDependenciesForCleanup(jobId, timeSlot, options);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
44
|
-
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, multi) {
|
|
46
|
+
async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.HMSH_FIDELITY_SECONDS, dad, multi) {
|
|
45
47
|
const fromNow = Date.now() + (inSeconds * 1000);
|
|
46
48
|
const fidelityMS = enums_1.HMSH_FIDELITY_SECONDS * 1000;
|
|
47
49
|
const awakenTimeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
|
|
48
|
-
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
|
|
50
|
+
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, dad, multi);
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* Should this engine instance play the role of 'scout' on behalf
|
|
@@ -89,11 +91,13 @@ class TaskService {
|
|
|
89
91
|
await timeEventCallback(target, gId, activityId, type);
|
|
90
92
|
}
|
|
91
93
|
await (0, utils_1.sleepFor)(0);
|
|
94
|
+
this.errorCount = 0;
|
|
92
95
|
this.processTimeHooks(timeEventCallback, listKey);
|
|
93
96
|
}
|
|
94
97
|
else if (workListTask) {
|
|
95
98
|
//a worklist was just emptied; try again immediately
|
|
96
99
|
await (0, utils_1.sleepFor)(0);
|
|
100
|
+
this.errorCount = 0;
|
|
97
101
|
this.processTimeHooks(timeEventCallback);
|
|
98
102
|
}
|
|
99
103
|
else {
|
|
@@ -101,12 +105,18 @@ class TaskService {
|
|
|
101
105
|
let sleep = (0, utils_1.XSleepFor)(enums_1.HMSH_FIDELITY_SECONDS * 1000);
|
|
102
106
|
this.cleanupTimeout = sleep.timerId;
|
|
103
107
|
await sleep.promise;
|
|
108
|
+
this.errorCount = 0;
|
|
104
109
|
this.processTimeHooks(timeEventCallback);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
107
112
|
catch (err) {
|
|
108
|
-
//
|
|
109
|
-
|
|
113
|
+
//most common reasons: deleted job not found; container stopping; test stopping
|
|
114
|
+
//less common: redis/cluster down; retry with fallback (5s max main reassignment)
|
|
115
|
+
this.logger.warn('task-process-timehooks-error', err);
|
|
116
|
+
await (0, utils_1.sleepFor)(1000 * this.errorCount++);
|
|
117
|
+
if (this.errorCount < 5) {
|
|
118
|
+
this.processTimeHooks(timeEventCallback);
|
|
119
|
+
}
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
122
|
else {
|
|
@@ -135,10 +145,17 @@ class TaskService {
|
|
|
135
145
|
const jobId = context.metadata.jid;
|
|
136
146
|
const gId = context.metadata.gid;
|
|
137
147
|
const activityId = hookRule.to;
|
|
148
|
+
//composite keys are used to fully describe the task target
|
|
149
|
+
const compositeJobKey = [
|
|
150
|
+
activityId,
|
|
151
|
+
dad,
|
|
152
|
+
gId,
|
|
153
|
+
jobId
|
|
154
|
+
].join(key_1.WEBSEP);
|
|
138
155
|
const hook = {
|
|
139
156
|
topic,
|
|
140
157
|
resolved,
|
|
141
|
-
jobId:
|
|
158
|
+
jobId: compositeJobKey,
|
|
142
159
|
};
|
|
143
160
|
await this.store.setHookSignal(hook, multi);
|
|
144
161
|
return jobId;
|
|
@@ -157,17 +174,17 @@ class TaskService {
|
|
|
157
174
|
const resolved = pipe_1.Pipe.resolve(mapExpression, context);
|
|
158
175
|
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
159
176
|
if (!hookSignalId) {
|
|
160
|
-
//messages can be double-processed; not an issue; return undefined
|
|
161
|
-
//users can also provide a bogus topic; not an issue; return undefined
|
|
177
|
+
//messages can be double-processed; not an issue; return `undefined`
|
|
178
|
+
//users can also provide a bogus topic; not an issue; return `undefined`
|
|
162
179
|
return undefined;
|
|
163
180
|
}
|
|
164
|
-
//`aid` is part of
|
|
181
|
+
//`aid` is part of composite key, but the hook `topic` is its public interface;
|
|
165
182
|
// this means that a new version of the graph can be deployed and the
|
|
166
183
|
// topic can be re-mapped to a different activity id. Outside callers
|
|
167
184
|
// can adhere to the unchanged contract (calling the same topic),
|
|
168
|
-
// while the internal system can be updated in real
|
|
169
|
-
const [_aid, dad, gid, ...jid] = hookSignalId.split(
|
|
170
|
-
return [jid.join(
|
|
185
|
+
// while the internal system can be updated in real-time as necessary.
|
|
186
|
+
const [_aid, dad, gid, ...jid] = hookSignalId.split(key_1.WEBSEP);
|
|
187
|
+
return [jid.join(key_1.WEBSEP), hookRule.to, dad, gid];
|
|
171
188
|
}
|
|
172
189
|
else {
|
|
173
190
|
throw new Error('signal-not-found');
|
|
@@ -1,17 +1,20 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
1
2
|
import { ILogger } from "../logger";
|
|
2
3
|
import { Router } from "../router";
|
|
3
4
|
import { StoreService } from '../store';
|
|
4
5
|
import { StreamService } from '../stream';
|
|
5
6
|
import { SubService } from '../sub';
|
|
6
7
|
import { HotMeshConfig, HotMeshWorker } from "../../types/hotmesh";
|
|
7
|
-
import { SubscriptionCallback } from "../../types/quorum";
|
|
8
|
+
import { RollCallMessage, SubscriptionCallback } from "../../types/quorum";
|
|
8
9
|
import { RedisClient, RedisMulti } from "../../types/redis";
|
|
10
|
+
import { StreamData, StreamDataResponse } from "../../types/stream";
|
|
9
11
|
declare class WorkerService {
|
|
10
12
|
namespace: string;
|
|
11
13
|
appId: string;
|
|
12
14
|
guid: string;
|
|
13
15
|
topic: string;
|
|
14
16
|
config: HotMeshConfig;
|
|
17
|
+
callback: (streamData: StreamData) => Promise<StreamDataResponse | void>;
|
|
15
18
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
16
19
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
17
20
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
@@ -19,6 +22,7 @@ declare class WorkerService {
|
|
|
19
22
|
logger: ILogger;
|
|
20
23
|
reporting: boolean;
|
|
21
24
|
inited: string;
|
|
25
|
+
rollCallInterval: NodeJS.Timeout;
|
|
22
26
|
static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<WorkerService[]>;
|
|
23
27
|
verifyWorkerFields(worker: HotMeshWorker): void;
|
|
24
28
|
initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
|
|
@@ -26,7 +30,14 @@ declare class WorkerService {
|
|
|
26
30
|
initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
|
|
27
31
|
initRouter(worker: HotMeshWorker, logger: ILogger): Router;
|
|
28
32
|
subscriptionHandler(): SubscriptionCallback;
|
|
29
|
-
|
|
33
|
+
/**
|
|
34
|
+
* A quorum-wide command to broadcaset system details.
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
doRollCall(message: RollCallMessage): Promise<void>;
|
|
38
|
+
cancelRollCall(): void;
|
|
39
|
+
stop(): void;
|
|
40
|
+
sayPong(appId: string, guid: string, originator?: string, details?: boolean, signature?: boolean): Promise<void>;
|
|
30
41
|
throttle(delayInMillis: number): Promise<void>;
|
|
31
42
|
}
|
|
32
43
|
export { WorkerService };
|
|
@@ -12,6 +12,7 @@ const redis_2 = require("../stream/clients/redis");
|
|
|
12
12
|
const ioredis_3 = require("../sub/clients/ioredis");
|
|
13
13
|
const redis_3 = require("../sub/clients/redis");
|
|
14
14
|
const stream_1 = require("../../types/stream");
|
|
15
|
+
const enums_1 = require("../../modules/enums");
|
|
15
16
|
class WorkerService {
|
|
16
17
|
constructor() {
|
|
17
18
|
this.reporting = false;
|
|
@@ -26,6 +27,7 @@ class WorkerService {
|
|
|
26
27
|
service.namespace = namespace;
|
|
27
28
|
service.appId = appId;
|
|
28
29
|
service.guid = guid;
|
|
30
|
+
service.callback = worker.callback;
|
|
29
31
|
service.topic = worker.topic;
|
|
30
32
|
service.config = config;
|
|
31
33
|
service.logger = logger;
|
|
@@ -95,14 +97,51 @@ class WorkerService {
|
|
|
95
97
|
return async (topic, message) => {
|
|
96
98
|
self.logger.debug('worker-event-received', { topic, type: message.type });
|
|
97
99
|
if (message.type === 'throttle') {
|
|
98
|
-
|
|
100
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
101
|
+
self.throttle(message.throttle);
|
|
102
|
+
}
|
|
99
103
|
}
|
|
100
104
|
else if (message.type === 'ping') {
|
|
101
105
|
self.sayPong(self.appId, self.guid, message.originator, message.details);
|
|
102
106
|
}
|
|
107
|
+
else if (message.type === 'rollcall') {
|
|
108
|
+
if (message.topic !== null) { //undefined allows passthrough
|
|
109
|
+
self.doRollCall(message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
103
112
|
};
|
|
104
113
|
}
|
|
105
|
-
|
|
114
|
+
/**
|
|
115
|
+
* A quorum-wide command to broadcaset system details.
|
|
116
|
+
*
|
|
117
|
+
*/
|
|
118
|
+
async doRollCall(message) {
|
|
119
|
+
let iteration = 0;
|
|
120
|
+
let max = !isNaN(message.max) ? message.max : enums_1.HMSH_QUORUM_ROLLCALL_CYCLES;
|
|
121
|
+
if (this.rollCallInterval)
|
|
122
|
+
clearTimeout(this.rollCallInterval);
|
|
123
|
+
const base = (message.interval / 2);
|
|
124
|
+
const amount = base + Math.ceil(Math.random() * base);
|
|
125
|
+
do {
|
|
126
|
+
await (0, utils_1.sleepFor)(Math.ceil(Math.random() * 1000));
|
|
127
|
+
await this.sayPong(this.appId, this.guid, null, true, message.signature);
|
|
128
|
+
if (!message.interval)
|
|
129
|
+
return;
|
|
130
|
+
const { promise, timerId } = (0, utils_1.XSleepFor)(amount * 1000);
|
|
131
|
+
this.rollCallInterval = timerId;
|
|
132
|
+
await promise;
|
|
133
|
+
} while (this.rollCallInterval && iteration++ < max - 1);
|
|
134
|
+
}
|
|
135
|
+
cancelRollCall() {
|
|
136
|
+
if (this.rollCallInterval) {
|
|
137
|
+
clearTimeout(this.rollCallInterval);
|
|
138
|
+
delete this.rollCallInterval;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
stop() {
|
|
142
|
+
this.cancelRollCall();
|
|
143
|
+
}
|
|
144
|
+
async sayPong(appId, guid, originator, details = false, signature = false) {
|
|
106
145
|
let profile;
|
|
107
146
|
if (details) {
|
|
108
147
|
const params = {
|
|
@@ -122,11 +161,13 @@ class WorkerService {
|
|
|
122
161
|
reclaimDelay: this.router.reclaimDelay,
|
|
123
162
|
reclaimCount: this.router.reclaimCount,
|
|
124
163
|
system: await (0, utils_1.getSystemHealth)(),
|
|
164
|
+
signature: signature ? this.callback.toString() : undefined,
|
|
125
165
|
};
|
|
126
166
|
}
|
|
127
167
|
this.store.publish(key_1.KeyType.QUORUM, {
|
|
128
168
|
type: 'pong',
|
|
129
|
-
guid,
|
|
169
|
+
guid,
|
|
170
|
+
originator,
|
|
130
171
|
profile,
|
|
131
172
|
}, appId);
|
|
132
173
|
}
|
package/build/types/job.d.ts
CHANGED
package/build/types/quorum.d.ts
CHANGED
|
@@ -45,42 +45,56 @@ export interface QuorumProfile {
|
|
|
45
45
|
reclaimDelay?: number;
|
|
46
46
|
reclaimCount?: number;
|
|
47
47
|
system?: SystemHealth;
|
|
48
|
+
signature?: string;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
+
interface QuorumMessageBase {
|
|
51
|
+
guid?: string;
|
|
52
|
+
topic?: string;
|
|
53
|
+
type?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface PingMessage extends QuorumMessageBase {
|
|
50
56
|
type: 'ping';
|
|
51
57
|
originator: string;
|
|
52
58
|
details?: boolean;
|
|
53
59
|
}
|
|
54
|
-
export interface WorkMessage {
|
|
60
|
+
export interface WorkMessage extends QuorumMessageBase {
|
|
55
61
|
type: 'work';
|
|
56
62
|
originator: string;
|
|
57
63
|
}
|
|
58
|
-
export interface CronMessage {
|
|
64
|
+
export interface CronMessage extends QuorumMessageBase {
|
|
59
65
|
type: 'cron';
|
|
60
66
|
originator: string;
|
|
61
67
|
}
|
|
62
|
-
export interface PongMessage {
|
|
68
|
+
export interface PongMessage extends QuorumMessageBase {
|
|
63
69
|
type: 'pong';
|
|
64
70
|
guid: string;
|
|
65
71
|
originator: string;
|
|
66
72
|
profile?: QuorumProfile;
|
|
67
73
|
}
|
|
68
|
-
export interface ActivateMessage {
|
|
74
|
+
export interface ActivateMessage extends QuorumMessageBase {
|
|
69
75
|
type: 'activate';
|
|
70
76
|
cache_mode: 'nocache' | 'cache';
|
|
71
77
|
until_version: string;
|
|
72
78
|
}
|
|
73
|
-
export interface JobMessage {
|
|
79
|
+
export interface JobMessage extends QuorumMessageBase {
|
|
74
80
|
type: 'job';
|
|
75
81
|
topic: string;
|
|
76
82
|
job: JobOutput;
|
|
77
83
|
}
|
|
78
|
-
export interface ThrottleMessage {
|
|
84
|
+
export interface ThrottleMessage extends QuorumMessageBase {
|
|
79
85
|
type: 'throttle';
|
|
80
86
|
guid?: string;
|
|
81
87
|
topic?: string;
|
|
82
88
|
throttle: number;
|
|
83
89
|
}
|
|
90
|
+
export interface RollCallMessage extends QuorumMessageBase {
|
|
91
|
+
type: 'rollcall';
|
|
92
|
+
guid?: string;
|
|
93
|
+
topic?: string | null;
|
|
94
|
+
interval: number;
|
|
95
|
+
max?: number;
|
|
96
|
+
signature?: boolean;
|
|
97
|
+
}
|
|
84
98
|
export interface JobMessageCallback {
|
|
85
99
|
(topic: string, message: JobOutput): void;
|
|
86
100
|
}
|
|
@@ -96,5 +110,5 @@ export interface QuorumMessageCallback {
|
|
|
96
110
|
* These messages serve to coordinate the cache invalidation and switch-over
|
|
97
111
|
* to the new version without any downtime and a coordinating parent server.
|
|
98
112
|
*/
|
|
99
|
-
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
|
|
113
|
+
export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | RollCallMessage | CronMessage;
|
|
100
114
|
export {};
|
package/build/types/stream.d.ts
CHANGED
package/modules/enums.ts
CHANGED
|
@@ -23,6 +23,7 @@ export const HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
|
23
23
|
export const HMSH_STATUS_UNKNOWN = 'unknown';
|
|
24
24
|
|
|
25
25
|
// QUORUM
|
|
26
|
+
export const HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
|
|
26
27
|
export const HMSH_QUORUM_DELAY_MS = 250;
|
|
27
28
|
export const HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
28
29
|
|
package/modules/key.ts
CHANGED
|
@@ -28,7 +28,12 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
|
|
|
28
28
|
* hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
const HMNS = "hmsh";
|
|
31
|
+
const HMNS = "hmsh";
|
|
32
|
+
|
|
33
|
+
const KEYSEP = ':'; //default delimiter for keys
|
|
34
|
+
const VALSEP = '::'; //default delimiter for vals
|
|
35
|
+
const WEBSEP = '::'; //default delimiter for webhook vals
|
|
36
|
+
const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
|
|
32
37
|
|
|
33
38
|
class KeyService {
|
|
34
39
|
|
|
@@ -93,4 +98,4 @@ class KeyService {
|
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
export { KeyService, KeyType, KeyStoreParams, HMNS };
|
|
101
|
+
export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.50",
|
|
4
4
|
"description": "Unbreakable Workflows",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
29
29
|
"test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
|
|
30
30
|
"test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
|
+
"test:await": "NODE_ENV=test jest ./tests/functional/awaiter/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
31
32
|
"test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
32
33
|
"test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
33
34
|
"test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
|
|
@@ -95,6 +95,12 @@ class Await extends Activity {
|
|
|
95
95
|
type: StreamDataType.AWAIT,
|
|
96
96
|
data: this.context.data
|
|
97
97
|
};
|
|
98
|
+
if (this.config.await !== true) {
|
|
99
|
+
const doAwait = Pipe.resolve(this.config.await, this.context);
|
|
100
|
+
if (doAwait === false) {
|
|
101
|
+
streamData.metadata.await = false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
98
104
|
if (this.config.retry) {
|
|
99
105
|
streamData.policies = {
|
|
100
106
|
retry: this.config.retry
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { JobState } from '../../types/job';
|
|
16
16
|
import { RedisMulti } from '../../types/redis';
|
|
17
17
|
import { StringScalarType } from '../../types/serializer';
|
|
18
|
+
import { WorkListTaskType } from '../../types/task';
|
|
18
19
|
|
|
19
20
|
class Trigger extends Activity {
|
|
20
21
|
config: TriggerActivity;
|
|
@@ -50,6 +51,10 @@ class Trigger extends Activity {
|
|
|
50
51
|
await this.registerJobDependency(multi);
|
|
51
52
|
await multi.exec();
|
|
52
53
|
|
|
54
|
+
//if the parent (spawner) chose not to await,
|
|
55
|
+
// emit the job_id as the data payload { job_id }
|
|
56
|
+
this.execAdjacentParent();
|
|
57
|
+
|
|
53
58
|
telemetry.mapActivityAttributes();
|
|
54
59
|
const jobStatus = Number(this.context.metadata.js);
|
|
55
60
|
telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
|
|
@@ -79,6 +84,12 @@ class Trigger extends Activity {
|
|
|
79
84
|
this.context.metadata.js = amount;
|
|
80
85
|
}
|
|
81
86
|
|
|
87
|
+
async execAdjacentParent() {
|
|
88
|
+
if (this.context.metadata.px) {
|
|
89
|
+
await this.engine.execAdjacentParent(this.context, {metadata: this.context.metadata, data: { job_id: this.context.metadata.jid }});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
82
93
|
createInputContext(): Partial<JobState> {
|
|
83
94
|
const input = {
|
|
84
95
|
[this.metadata.aid]: {
|
|
@@ -115,6 +126,7 @@ class Trigger extends Activity {
|
|
|
115
126
|
pg: this.context.metadata.pg,
|
|
116
127
|
pd: this.context.metadata.pd,
|
|
117
128
|
pa: this.context.metadata.pa,
|
|
129
|
+
px: this.context.metadata.px,
|
|
118
130
|
app: id,
|
|
119
131
|
vrs: version,
|
|
120
132
|
tpc: this.config.subscribes,
|
|
@@ -193,12 +205,23 @@ class Trigger extends Activity {
|
|
|
193
205
|
}
|
|
194
206
|
if (resolvedDepKey) {
|
|
195
207
|
const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
|
|
208
|
+
let type: WorkListTaskType;
|
|
209
|
+
if (isParentOrigin) {
|
|
210
|
+
if (this.context.metadata.px) {
|
|
211
|
+
type = 'child'
|
|
212
|
+
} else {
|
|
213
|
+
type = 'expire-child'
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
type = 'expire';
|
|
217
|
+
}
|
|
196
218
|
await this.store.registerJobDependency(
|
|
197
|
-
|
|
219
|
+
type,
|
|
198
220
|
resolvedDepKey,
|
|
199
221
|
this.context.metadata.tpc,
|
|
200
222
|
this.context.metadata.jid,
|
|
201
223
|
this.context.metadata.gid,
|
|
224
|
+
this.context.metadata.pd,
|
|
202
225
|
multi,
|
|
203
226
|
);
|
|
204
227
|
}
|
|
@@ -209,6 +232,7 @@ class Trigger extends Activity {
|
|
|
209
232
|
this.context.metadata.tpc,
|
|
210
233
|
this.context.metadata.jid,
|
|
211
234
|
this.context.metadata.gid,
|
|
235
|
+
this.context.metadata.pd,
|
|
212
236
|
multi,
|
|
213
237
|
);
|
|
214
238
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
JobTimeline } from '../../types/exporter';
|
|
14
14
|
import { SerializerService } from '../serializer';
|
|
15
15
|
import { restoreHierarchy } from '../../modules/utils';
|
|
16
|
+
import { VALSEP } from '../../modules/key';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
@@ -116,19 +117,31 @@ class ExporterService {
|
|
|
116
117
|
const activityName = item[1].split('/')[0];
|
|
117
118
|
const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
|
|
118
119
|
const timestamp = item[2];
|
|
119
|
-
|
|
120
|
+
let event: JobTimeline = {
|
|
120
121
|
activity: activityName,
|
|
121
122
|
duplex: duplex as 'entry' | 'exit',
|
|
122
123
|
dimension: dimensions,
|
|
123
124
|
timestamp,
|
|
125
|
+
created: timestamp,
|
|
126
|
+
updated: timestamp,
|
|
124
127
|
};
|
|
125
|
-
timeline.
|
|
128
|
+
const prior = timeline[timeline.length - 1];
|
|
129
|
+
if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
|
|
130
|
+
if (event.duplex === 'exit') {
|
|
131
|
+
prior.updated = event.timestamp;
|
|
132
|
+
} else {
|
|
133
|
+
prior.created = event.timestamp;
|
|
134
|
+
}
|
|
135
|
+
event = prior;
|
|
136
|
+
} else {
|
|
137
|
+
timeline.push(event);
|
|
138
|
+
}
|
|
126
139
|
|
|
127
140
|
if (this.isMainEntry(item[1])) {
|
|
128
141
|
event.actions = [] as ActivityAction[];
|
|
129
142
|
this.interleaveActions(actions.main, event.actions);
|
|
130
143
|
} else if (this.isHookEntry(item[1])) {
|
|
131
|
-
const hookDimension =
|
|
144
|
+
const hookDimension = `/${parts[1]}/${parts[2]}`;
|
|
132
145
|
const hookActions = actions.hooks[hookDimension];
|
|
133
146
|
event.actions = [] as ActivityAction[];
|
|
134
147
|
this.interleaveActions(hookActions, event.actions);
|
|
@@ -191,17 +204,15 @@ class ExporterService {
|
|
|
191
204
|
* @returns - the organized dependency data
|
|
192
205
|
*/
|
|
193
206
|
inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
|
|
194
|
-
//console.log('dependency data>', data);
|
|
195
207
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
196
208
|
const flowReg = /-(\d+)$/;
|
|
197
209
|
return data.map((dependency, index: number): DependencyExport => {
|
|
198
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
199
|
-
const jobId = jid.join(
|
|
210
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
|
|
211
|
+
const jobId = jid.join(VALSEP);
|
|
200
212
|
const match = jobId.match(hookReg);
|
|
201
213
|
let prefix: string;
|
|
202
214
|
let type: 'hook' | 'flow' | 'other';
|
|
203
215
|
let dimensionKey: string = '';
|
|
204
|
-
|
|
205
216
|
if (match) {
|
|
206
217
|
//hook-originating dependency
|
|
207
218
|
const [_, dimension, counter] = match;
|
package/services/engine/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { KeyType } from '../../modules/key';
|
|
1
|
+
import { KeyType, VALSEP } from '../../modules/key';
|
|
2
2
|
import {
|
|
3
3
|
HMSH_OTT_WAIT_TIME,
|
|
4
4
|
HMSH_CODE_SUCCESS,
|
|
@@ -386,9 +386,10 @@ class EngineService {
|
|
|
386
386
|
pg: streamData.metadata.gid,
|
|
387
387
|
pd: streamData.metadata.dad,
|
|
388
388
|
pa: streamData.metadata.aid,
|
|
389
|
+
px: streamData.metadata.await === false, //sever the parent connection (px)
|
|
389
390
|
trc: streamData.metadata.trc,
|
|
390
391
|
spn: streamData.metadata.spn,
|
|
391
|
-
|
|
392
|
+
};
|
|
392
393
|
const activityHandler = await this.initActivity(
|
|
393
394
|
streamData.metadata.topic,
|
|
394
395
|
streamData.data,
|
|
@@ -459,7 +460,10 @@ class EngineService {
|
|
|
459
460
|
return (await this.router?.publishMessage(null, streamData)) as string;
|
|
460
461
|
}
|
|
461
462
|
}
|
|
462
|
-
hasParentJob(context: JobState): boolean {
|
|
463
|
+
hasParentJob(context: JobState, checkSevered = false): boolean {
|
|
464
|
+
if (checkSevered) {
|
|
465
|
+
return Boolean(context.metadata.pj && context.metadata.pa && !context.metadata.px);
|
|
466
|
+
}
|
|
463
467
|
return Boolean(context.metadata.pj && context.metadata.pa);
|
|
464
468
|
}
|
|
465
469
|
resolveError(metadata: JobMetadata): StreamError | undefined {
|
|
@@ -537,7 +541,11 @@ class EngineService {
|
|
|
537
541
|
const taskService = new TaskService(this.store, this.logger);
|
|
538
542
|
await taskService.enqueueWorkItems(
|
|
539
543
|
workItems.map(
|
|
540
|
-
workItem =>
|
|
544
|
+
workItem => [
|
|
545
|
+
hookTopic,
|
|
546
|
+
workItem,
|
|
547
|
+
keyResolver.scrub || false,
|
|
548
|
+
JSON.stringify(data)].join(VALSEP)
|
|
541
549
|
));
|
|
542
550
|
this.store.publish(
|
|
543
551
|
KeyType.QUORUM,
|
|
@@ -663,7 +671,7 @@ class EngineService {
|
|
|
663
671
|
// ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
|
|
664
672
|
async runJobCompletionTasks(context: JobState, options: JobCompletionOptions = {}): Promise<string | void> {
|
|
665
673
|
//'emit' indicates the job is still active
|
|
666
|
-
const isAwait = this.hasParentJob(context);
|
|
674
|
+
const isAwait = this.hasParentJob(context, true);
|
|
667
675
|
const isOneTimeSub = this.hasOneTimeSubscription(context);
|
|
668
676
|
const topic = await this.getPublishesTopic(context);
|
|
669
677
|
let msgId: string;
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
JobExport } from '../../types/exporter';
|
|
13
13
|
import { SerializerService } from '../serializer';
|
|
14
14
|
import { restoreHierarchy } from '../../modules/utils';
|
|
15
|
+
import { VALSEP } from '../../modules/key';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
@@ -108,8 +109,8 @@ class ExporterService {
|
|
|
108
109
|
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
109
110
|
const flowReg = /-(\d+)$/;
|
|
110
111
|
return data.map((dependency, index: number): DependencyExport => {
|
|
111
|
-
const [action, topic, gid, ...jid] = dependency.split(
|
|
112
|
-
const jobId = jid.join(
|
|
112
|
+
const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
|
|
113
|
+
const jobId = jid.join(VALSEP);
|
|
113
114
|
const match = jobId.match(hookReg);
|
|
114
115
|
let prefix: string;
|
|
115
116
|
let type: 'hook' | 'flow' | 'other';
|
|
@@ -222,6 +222,10 @@ class HotMeshService {
|
|
|
222
222
|
|
|
223
223
|
stop() {
|
|
224
224
|
this.engine?.taskService.cancelCleanup();
|
|
225
|
+
this.quorum?.stop();
|
|
226
|
+
this.workers?.forEach((worker: WorkerService) => {
|
|
227
|
+
worker.stop();
|
|
228
|
+
});
|
|
225
229
|
}
|
|
226
230
|
|
|
227
231
|
async compress(terms: string[]): Promise<boolean> {
|