@hotmeshio/hotmesh 0.0.43 → 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/package.json +1 -1
- package/build/services/activities/trigger.js +7 -1
- 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/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.js +2 -1
- package/build/services/router/index.d.ts +3 -0
- package/build/services/router/index.js +3 -0
- package/build/services/store/index.d.ts +5 -2
- package/build/services/store/index.js +54 -6
- 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/index.d.ts +1 -0
- package/build/types/quorum.d.ts +1 -0
- package/build/types/task.d.ts +1 -1
- package/package.json +1 -1
- package/services/activities/trigger.ts +14 -0
- package/services/durable/exporter.ts +408 -0
- package/services/durable/factory.ts +6 -63
- package/services/durable/handle.ts +12 -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 +2 -1
- package/services/router/index.ts +3 -0
- package/services/store/index.ts +56 -7
- 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/index.ts +13 -1
- package/types/quorum.ts +1 -0
- package/types/task.ts +1 -1
package/build/types/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { AsyncSignal } from './async';
|
|
|
4
4
|
export { CacheMode } from './cache';
|
|
5
5
|
export { CollationFaultType, CollationStage } from './collator';
|
|
6
6
|
export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowContext, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
|
|
7
|
+
export { ActivityAction, DependencyExport, DurableJobExport, ExportCycles, ExportItem, ExportOptions, ExportTransitions, JobAction, JobExport, JobActionExport, JobTimeline } from './exporter';
|
|
7
8
|
export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
|
|
8
9
|
export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
|
|
9
10
|
export { ILogger } from './logger';
|
package/build/types/quorum.d.ts
CHANGED
package/build/types/task.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type WorkListTaskType = 'sleep' | 'expire' | 'interrupt' | 'delist';
|
|
1
|
+
export type WorkListTaskType = 'sleep' | 'expire' | 'expire-child' | 'interrupt' | 'delist' | 'child';
|
package/package.json
CHANGED
|
@@ -186,11 +186,15 @@ class Trigger extends Activity {
|
|
|
186
186
|
async registerJobDependency(multi?: RedisMulti): Promise<void> {
|
|
187
187
|
const depKey = this.config.stats?.parent ?? this.context.metadata.pj;
|
|
188
188
|
let resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
|
|
189
|
+
const adjKey = this.config.stats?.adjacent;
|
|
190
|
+
let resolvedAdjKey = depKey ? Pipe.resolve(adjKey, this.context) : '';
|
|
189
191
|
if (!resolvedDepKey) {
|
|
190
192
|
resolvedDepKey = this.context.metadata.pj;
|
|
191
193
|
}
|
|
192
194
|
if (resolvedDepKey) {
|
|
195
|
+
const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
|
|
193
196
|
await this.store.registerJobDependency(
|
|
197
|
+
isParentOrigin ? 'expire-child' : 'expire',
|
|
194
198
|
resolvedDepKey,
|
|
195
199
|
this.context.metadata.tpc,
|
|
196
200
|
this.context.metadata.jid,
|
|
@@ -198,6 +202,16 @@ class Trigger extends Activity {
|
|
|
198
202
|
multi,
|
|
199
203
|
);
|
|
200
204
|
}
|
|
205
|
+
if (resolvedAdjKey && resolvedAdjKey !== resolvedDepKey) {
|
|
206
|
+
await this.store.registerJobDependency(
|
|
207
|
+
'child',
|
|
208
|
+
resolvedAdjKey,
|
|
209
|
+
this.context.metadata.tpc,
|
|
210
|
+
this.context.metadata.jid,
|
|
211
|
+
this.context.metadata.gid,
|
|
212
|
+
multi,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
201
215
|
}
|
|
202
216
|
|
|
203
217
|
async setStats(multi?: RedisMulti): Promise<void> {
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { ILogger } from '../logger';
|
|
2
|
+
import { StoreService } from '../store';
|
|
3
|
+
import { StringStringType, Symbols } from "../../types";
|
|
4
|
+
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
5
|
+
import {
|
|
6
|
+
ActivityAction,
|
|
7
|
+
DependencyExport,
|
|
8
|
+
ExportItem,
|
|
9
|
+
ExportOptions,
|
|
10
|
+
JobAction,
|
|
11
|
+
JobActionExport,
|
|
12
|
+
DurableJobExport,
|
|
13
|
+
JobTimeline } from '../../types/exporter';
|
|
14
|
+
import { SerializerService } from '../serializer';
|
|
15
|
+
import { restoreHierarchy } from '../../modules/utils';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
19
|
+
* Splits, Inflates, and Sorts the job data for use in durable contexts
|
|
20
|
+
*/
|
|
21
|
+
class ExporterService {
|
|
22
|
+
appId: string;
|
|
23
|
+
logger: ILogger;
|
|
24
|
+
serializer: SerializerService
|
|
25
|
+
store: StoreService<RedisClient, RedisMulti>;
|
|
26
|
+
symbols: Promise<Symbols> | Symbols;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Friendly names for the activity ids
|
|
30
|
+
*/
|
|
31
|
+
activitySymbols: Symbols = {
|
|
32
|
+
t1: 'trigger',
|
|
33
|
+
a1: 'pivot',
|
|
34
|
+
w1: 'worker',
|
|
35
|
+
a592: 'sleeper',
|
|
36
|
+
a594: 'awaiter',
|
|
37
|
+
a599: 'retryer',
|
|
38
|
+
c592: 'sleep_cycler',
|
|
39
|
+
c594: 'await_cycler',
|
|
40
|
+
c599: 'retry_cycler',
|
|
41
|
+
s5: 'scrubber',
|
|
42
|
+
sig: 'hook',
|
|
43
|
+
siga1: 'hook_pivot',
|
|
44
|
+
sigw1: 'hook_worker',
|
|
45
|
+
siga592: 'hook_sleeper',
|
|
46
|
+
siga594: 'hook_awaiter',
|
|
47
|
+
siga599: 'hook_retryer',
|
|
48
|
+
sigc592: 'hook_sleep_cycler',
|
|
49
|
+
sigc594: 'hook_await_cycler',
|
|
50
|
+
sigc599: 'hook_retry_cycler',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
//adjacent transitions
|
|
54
|
+
transitions = {
|
|
55
|
+
trigger: ['pivot', 'hook'],
|
|
56
|
+
pivot: ['worker'],
|
|
57
|
+
worker: ['sleeper', 'awaiter', 'retryer', 'scrubber'],
|
|
58
|
+
sleeper: ['sleep_cycler'],
|
|
59
|
+
awaiter: ['await_cycler'],
|
|
60
|
+
retryer: ['retry_cycler'],
|
|
61
|
+
hook: ['hook_pivot'],
|
|
62
|
+
hook_pivot: ['hook_worker'],
|
|
63
|
+
hook_worker: ['hook_sleeper', 'hook_awaiter', 'hook_retryer'],
|
|
64
|
+
hook_sleeper: ['hook_sleep_cycler'],
|
|
65
|
+
hook_awaiter: ['hook_await_cycler'],
|
|
66
|
+
hook_retryer: ['hook_retry_cycler'],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
//goto transitions
|
|
70
|
+
cycles = {
|
|
71
|
+
sleep_cycler: ['pivot'],
|
|
72
|
+
await_cycler: ['pivot'],
|
|
73
|
+
retry_cycler: ['pivot'],
|
|
74
|
+
hook_sleep_cycler: ['hook_pivot'],
|
|
75
|
+
hook_await_cycler: ['hook_pivot'],
|
|
76
|
+
hook_retry_cycler: ['hook_pivot'],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
|
|
80
|
+
this.appId = appId;
|
|
81
|
+
this.logger = logger;
|
|
82
|
+
this.store = store;
|
|
83
|
+
this.serializer = new SerializerService();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Convert the job hash and dependency list into a DurableJobExport object.
|
|
88
|
+
* This object contains various facets that describe the interaction
|
|
89
|
+
* in terms relevant to narrative storytelling.
|
|
90
|
+
*/
|
|
91
|
+
async export(jobId: string, options: ExportOptions = {}): Promise<DurableJobExport> {
|
|
92
|
+
if (!this.symbols) {
|
|
93
|
+
this.symbols = this.store.getAllSymbols();
|
|
94
|
+
this.symbols = await this.symbols;
|
|
95
|
+
}
|
|
96
|
+
const depData = await this.store.getDependencies(jobId);
|
|
97
|
+
const jobData = await this.store.getRaw(jobId);
|
|
98
|
+
const jobExport = this.inflate(jobData, depData);
|
|
99
|
+
return jobExport;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Interleave actions into the replay timeline to create
|
|
104
|
+
* a time-ordered timeline of the entire interaction, beginning
|
|
105
|
+
* with the entry trigger and concluding with the scrubber
|
|
106
|
+
* activity. Using the returned timeline, it is possible to
|
|
107
|
+
* create an animated narrative of the job, highlighting
|
|
108
|
+
* activities in the graph according to the timeline's
|
|
109
|
+
* activity-created (/ac) and activity-updated (/au) entries.
|
|
110
|
+
*/
|
|
111
|
+
createTimeline(replay: ExportItem[], actions: JobActionExport): JobTimeline[] {
|
|
112
|
+
const timeline: JobTimeline[] = [];
|
|
113
|
+
replay.forEach((item) => {
|
|
114
|
+
const dimensions = item[0];
|
|
115
|
+
const parts = dimensions.split('/');
|
|
116
|
+
const activityName = item[1].split('/')[0];
|
|
117
|
+
const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
|
|
118
|
+
const timestamp = item[2];
|
|
119
|
+
const event: JobTimeline = {
|
|
120
|
+
activity: activityName,
|
|
121
|
+
duplex: duplex as 'entry' | 'exit',
|
|
122
|
+
dimension: dimensions,
|
|
123
|
+
timestamp,
|
|
124
|
+
};
|
|
125
|
+
timeline.push(event);
|
|
126
|
+
|
|
127
|
+
if (this.isMainEntry(item[1])) {
|
|
128
|
+
event.actions = [] as ActivityAction[];
|
|
129
|
+
this.interleaveActions(actions.main, event.actions);
|
|
130
|
+
} else if (this.isHookEntry(item[1])) {
|
|
131
|
+
const hookDimension = `/${parts[1]}/${parts[2]}`;
|
|
132
|
+
const hookActions = actions.hooks[hookDimension];
|
|
133
|
+
event.actions = [] as ActivityAction[];
|
|
134
|
+
this.interleaveActions(hookActions, event.actions);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return timeline;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Interleave actions into the 'worker' and 'hook_worker'
|
|
142
|
+
* activities (between their /ac and /au entries)
|
|
143
|
+
*/
|
|
144
|
+
interleaveActions(target: JobAction, actions: ActivityAction[]) {
|
|
145
|
+
if (target) {
|
|
146
|
+
for (let i = target.cursor + 1; i < target.items.length; i++) {
|
|
147
|
+
const [_, actionType, jobOrIndex] = target.items[i];
|
|
148
|
+
actions.push({ action: actionType, target: jobOrIndex });
|
|
149
|
+
target.cursor = i;
|
|
150
|
+
if (this.isPausingAction(actionType)) {
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
isPausingAction(actionType: string): boolean {
|
|
158
|
+
return actionType === 'sleep' || actionType === 'waitForSignal';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
isMainEntry(key: string): boolean {
|
|
162
|
+
return key.startsWith('worker/') && key.endsWith('/ac');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
isHookEntry(key: string): boolean {
|
|
166
|
+
return key.startsWith('hook_worker/') && key.endsWith('/ac');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Inflates the key from Redis, 3-character symbol
|
|
171
|
+
* into a human-readable JSON path, reflecting the
|
|
172
|
+
* tree-like structure of the unidimensional Hash
|
|
173
|
+
*/
|
|
174
|
+
inflateKey(key: string): string {
|
|
175
|
+
if (key in this.symbols) {
|
|
176
|
+
const path = this.symbols[key];
|
|
177
|
+
const parts = path.split('/');
|
|
178
|
+
if (parts[0] in this.activitySymbols) {
|
|
179
|
+
parts[0] = this.activitySymbols[parts[0]];
|
|
180
|
+
}
|
|
181
|
+
return parts.join('/');
|
|
182
|
+
}
|
|
183
|
+
return key;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Inflates the dependency data from Redis into a DurableJobExport object by
|
|
188
|
+
* organizing the dimensional isolate in sch a way asto interleave
|
|
189
|
+
* into a story
|
|
190
|
+
* @param data - the dependency data from Redis
|
|
191
|
+
* @returns - the organized dependency data
|
|
192
|
+
*/
|
|
193
|
+
inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
|
|
194
|
+
//console.log('dependency data>', data);
|
|
195
|
+
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
196
|
+
const flowReg = /-(\d+)$/;
|
|
197
|
+
return data.map((dependency, index: number): DependencyExport => {
|
|
198
|
+
const [action, topic, gid, ...jid] = dependency.split('::');
|
|
199
|
+
const jobId = jid.join('::');
|
|
200
|
+
const match = jobId.match(hookReg);
|
|
201
|
+
let prefix: string;
|
|
202
|
+
let type: 'hook' | 'flow' | 'other';
|
|
203
|
+
let dimensionKey: string = '';
|
|
204
|
+
|
|
205
|
+
if (match) {
|
|
206
|
+
//hook-originating dependency
|
|
207
|
+
const [_, dimension, counter] = match;
|
|
208
|
+
dimensionKey = dimension.split(',').join('/');
|
|
209
|
+
prefix = `${dimensionKey}[${counter}]`;
|
|
210
|
+
type = 'hook';
|
|
211
|
+
} else {
|
|
212
|
+
const match = jobId.match(flowReg);
|
|
213
|
+
if (match) {
|
|
214
|
+
//main workflow-originating dependency
|
|
215
|
+
const [_, counter] = match;
|
|
216
|
+
prefix = `[${counter}]`;
|
|
217
|
+
type = 'flow';
|
|
218
|
+
} else {
|
|
219
|
+
//'other' types like signal cleanup
|
|
220
|
+
prefix = '/';
|
|
221
|
+
type = 'other';
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
this.seedActions(
|
|
225
|
+
type,
|
|
226
|
+
action,
|
|
227
|
+
topic,
|
|
228
|
+
dependency,
|
|
229
|
+
prefix,
|
|
230
|
+
dimensionKey,
|
|
231
|
+
actions,
|
|
232
|
+
jobId,
|
|
233
|
+
);
|
|
234
|
+
return {
|
|
235
|
+
type: action,
|
|
236
|
+
topic,
|
|
237
|
+
gid,
|
|
238
|
+
jid: jobId,
|
|
239
|
+
} as unknown as DependencyExport;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Adds historical actions (proxyActivity, executeChild)
|
|
245
|
+
* using the `dependency list` to determine
|
|
246
|
+
* after-the-fact what happened within the 'black-box'
|
|
247
|
+
* worker function. This is necessary to interleave the
|
|
248
|
+
* actions into the replay timeline, given that it isn't
|
|
249
|
+
* really possible to know the inner-workings of the user's
|
|
250
|
+
* function
|
|
251
|
+
*
|
|
252
|
+
*/
|
|
253
|
+
seedActions(type: 'flow'|'hook'|'other', action: string, topic: string, dep: string, prefix: string, dimensionKey: string, actions: JobActionExport, jobId: string) {
|
|
254
|
+
if (type !== 'other' && action === 'expire-child') {
|
|
255
|
+
let depType: string;
|
|
256
|
+
if (topic == `${this.appId}.activity.execute`) {
|
|
257
|
+
depType = 'proxyActivity';
|
|
258
|
+
} else if (topic == `${this.appId}.execute`) {
|
|
259
|
+
depType = 'executeChild';
|
|
260
|
+
} else if (topic == `${this.appId}.wfsc.execute`) {
|
|
261
|
+
depType = 'waitForSignal';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (depType) {
|
|
265
|
+
if (type === 'flow') {
|
|
266
|
+
actions.main.items.push([prefix, depType, jobId]);
|
|
267
|
+
} else if (type === 'hook') {
|
|
268
|
+
if (!actions.hooks[dimensionKey]) {
|
|
269
|
+
actions.hooks[dimensionKey] = {
|
|
270
|
+
cursor: -1,
|
|
271
|
+
items: [],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
actions.hooks[dimensionKey].items.push([prefix, depType, jobId]);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Inflates the job data from Redis into a DurableJobExport object
|
|
282
|
+
* @param jobHash - the job data from Redis
|
|
283
|
+
* @param dependencyList - the list of dependencies for the job
|
|
284
|
+
* @returns - the inflated job data
|
|
285
|
+
*/
|
|
286
|
+
inflate(jobHash: StringStringType, dependencyList: string[]): DurableJobExport {
|
|
287
|
+
//the list of actions taken in the workflow and hook functions
|
|
288
|
+
const actions: JobActionExport = {
|
|
289
|
+
hooks: {},
|
|
290
|
+
main: { cursor: -1, items: [] },
|
|
291
|
+
};
|
|
292
|
+
const dependencies = this.inflateDependencyData(dependencyList, actions);
|
|
293
|
+
const state: StringStringType = {};
|
|
294
|
+
const data: StringStringType = {};
|
|
295
|
+
const other: ExportItem[] = [];
|
|
296
|
+
const replay: ExportItem[] = [];
|
|
297
|
+
const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
|
|
298
|
+
|
|
299
|
+
Object.entries(jobHash).forEach(([key, value]) => {
|
|
300
|
+
const match = key.match(regex);
|
|
301
|
+
if (match) {
|
|
302
|
+
//activity process state
|
|
303
|
+
this.inflateProcess(match, value, replay);
|
|
304
|
+
} else if (key.length === 3) {
|
|
305
|
+
//job state
|
|
306
|
+
state[this.inflateKey(key)] = this.serializer.fromString(value);
|
|
307
|
+
} else if (key.startsWith('_')) {
|
|
308
|
+
//job data
|
|
309
|
+
data[key.substring(1)] = value;
|
|
310
|
+
} else if (key.startsWith('-')) {
|
|
311
|
+
//actions with side effect (replayable)
|
|
312
|
+
this.inflateActions(key, value, actions);
|
|
313
|
+
} else {
|
|
314
|
+
//collator guids, etc
|
|
315
|
+
other.push([null, key, value]);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
replay.sort(this.dateSort)
|
|
320
|
+
actions.main.items.sort(this.reverseSort);
|
|
321
|
+
Object.entries(actions.hooks).forEach(([key, value]) => {
|
|
322
|
+
value.items.sort(this.reverseSort);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
data: restoreHierarchy(data),
|
|
327
|
+
dependencies,
|
|
328
|
+
state: Object.entries(restoreHierarchy(state))[0][1],
|
|
329
|
+
status: jobHash[':'],
|
|
330
|
+
timeline: this.createTimeline(replay, actions),
|
|
331
|
+
transitions: { ...this.transitions },
|
|
332
|
+
cycles: { ...this.cycles },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
inflateProcess(match: RegExpMatchArray, value: string, replay: ExportItem[]) {
|
|
337
|
+
const [_, letters, numbers] = match;
|
|
338
|
+
const path = this.inflateKey(letters);
|
|
339
|
+
if (path.endsWith('/output/metadata/ac') ||
|
|
340
|
+
path.endsWith('/output/metadata/au')) {
|
|
341
|
+
const dimensions = `/${numbers.replace(/,/g, '/')}`;
|
|
342
|
+
const resolved = this.serializer.fromString(value);
|
|
343
|
+
replay.push([
|
|
344
|
+
dimensions,
|
|
345
|
+
path,
|
|
346
|
+
resolved,
|
|
347
|
+
]);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
inflateActions(key: string, value: string, actions: JobActionExport) {
|
|
352
|
+
let [_, dimensionalType, counter, subcounter] = key.split('-');
|
|
353
|
+
if (subcounter) {
|
|
354
|
+
counter = `${counter}.${subcounter}`;
|
|
355
|
+
}
|
|
356
|
+
const [type, ...dimensions] = dimensionalType.split(',');
|
|
357
|
+
let dimensionKey = '';
|
|
358
|
+
let isHook = false;
|
|
359
|
+
if (dimensions.length > 0) {
|
|
360
|
+
dimensionKey = `/${dimensions.join('/')}`;
|
|
361
|
+
isHook = true;
|
|
362
|
+
}
|
|
363
|
+
let targetList: ExportItem[];
|
|
364
|
+
if (isHook) {
|
|
365
|
+
if (!actions.hooks[dimensionKey]) {
|
|
366
|
+
actions.hooks[dimensionKey] = {
|
|
367
|
+
cursor: -1,
|
|
368
|
+
items: [],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
targetList = actions.hooks[dimensionKey].items;
|
|
372
|
+
} else {
|
|
373
|
+
targetList = actions.main.items;
|
|
374
|
+
}
|
|
375
|
+
targetList.push([
|
|
376
|
+
`${dimensionKey}[${counter}]`,
|
|
377
|
+
type,
|
|
378
|
+
value,
|
|
379
|
+
]);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
reverseSort(aKey: ExportItem, bKey: ExportItem) {
|
|
383
|
+
if (aKey[0] > bKey[0]) {
|
|
384
|
+
return 1;
|
|
385
|
+
} else if (aKey[0] < bKey[0]) {
|
|
386
|
+
return -1;
|
|
387
|
+
} else {
|
|
388
|
+
if (aKey[1] > bKey[1]) {
|
|
389
|
+
return 1;
|
|
390
|
+
} else if (aKey[1] < bKey[1]) {
|
|
391
|
+
return -1;
|
|
392
|
+
}
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
dateSort(aKey: ExportItem, bKey: ExportItem) {
|
|
398
|
+
if (aKey[2] > bKey[2]) {
|
|
399
|
+
return 1;
|
|
400
|
+
} else if (aKey[2] < bKey[2]) {
|
|
401
|
+
return -1;
|
|
402
|
+
} else {
|
|
403
|
+
return 0;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export { ExporterService };
|
|
@@ -57,6 +57,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
57
57
|
id: '{$self.input.data.workflowId}'
|
|
58
58
|
key: '{$self.input.data.parentWorkflowId}'
|
|
59
59
|
parent: '{$self.input.data.originJobId}'
|
|
60
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
60
61
|
job:
|
|
61
62
|
maps:
|
|
62
63
|
done: false
|
|
@@ -260,10 +261,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
260
261
|
description: index will be appended later
|
|
261
262
|
maps:
|
|
262
263
|
signals: '{sigw1.output.data.signals}'
|
|
263
|
-
parentWorkflowId:
|
|
264
|
-
'@pipe':
|
|
265
|
-
- ['{$job.metadata.jid}', '-w']
|
|
266
|
-
- ['{@string.concat}']
|
|
264
|
+
parentWorkflowId: '{$job.metadata.jid}'
|
|
267
265
|
originJobId:
|
|
268
266
|
'@pipe':
|
|
269
267
|
- ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
|
|
@@ -353,10 +351,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
353
351
|
description: index will be appended later
|
|
354
352
|
maps:
|
|
355
353
|
signals: '{w1.output.data.signals}'
|
|
356
|
-
parentWorkflowId:
|
|
357
|
-
'@pipe':
|
|
358
|
-
- ['{$job.metadata.jid}', '-w']
|
|
359
|
-
- ['{@string.concat}']
|
|
354
|
+
parentWorkflowId: '{$job.metadata.jid}'
|
|
360
355
|
originJobId:
|
|
361
356
|
'@pipe':
|
|
362
357
|
- ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
|
|
@@ -523,6 +518,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
523
518
|
id: '{$self.input.data.workflowId}'
|
|
524
519
|
key: '{$self.input.data.parentWorkflowId}'
|
|
525
520
|
parent: '{$self.input.data.originJobId}'
|
|
521
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
526
522
|
|
|
527
523
|
w1a:
|
|
528
524
|
title: Activity Worker - Calls Activity Functions
|
|
@@ -563,61 +559,6 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
563
559
|
t1a:
|
|
564
560
|
- to: w1a
|
|
565
561
|
|
|
566
|
-
- subscribes: ${app}.sleep.execute
|
|
567
|
-
publishes: ${app}.sleep.executed
|
|
568
|
-
|
|
569
|
-
expire: 0
|
|
570
|
-
|
|
571
|
-
input:
|
|
572
|
-
schema:
|
|
573
|
-
type: object
|
|
574
|
-
properties:
|
|
575
|
-
parentWorkflowId:
|
|
576
|
-
type: string
|
|
577
|
-
originJobId:
|
|
578
|
-
type: string
|
|
579
|
-
workflowId:
|
|
580
|
-
type: string
|
|
581
|
-
duration:
|
|
582
|
-
type: number
|
|
583
|
-
description: in seconds
|
|
584
|
-
index:
|
|
585
|
-
type: number
|
|
586
|
-
output:
|
|
587
|
-
schema:
|
|
588
|
-
type: object
|
|
589
|
-
properties:
|
|
590
|
-
done:
|
|
591
|
-
type: boolean
|
|
592
|
-
duration:
|
|
593
|
-
type: number
|
|
594
|
-
index:
|
|
595
|
-
type: number
|
|
596
|
-
|
|
597
|
-
activities:
|
|
598
|
-
t1s:
|
|
599
|
-
title: Sleep Flow Trigger
|
|
600
|
-
type: trigger
|
|
601
|
-
stats:
|
|
602
|
-
id: '{$self.input.data.workflowId}'
|
|
603
|
-
key: '{$self.input.data.parentWorkflowId}'
|
|
604
|
-
parent: '{$self.input.data.originJobId}'
|
|
605
|
-
|
|
606
|
-
a1s:
|
|
607
|
-
title: Sleep for a duration
|
|
608
|
-
type: hook
|
|
609
|
-
sleep: '{t1s.output.data.duration}'
|
|
610
|
-
job:
|
|
611
|
-
maps:
|
|
612
|
-
done: true
|
|
613
|
-
duration: '{t1s.output.data.duration}'
|
|
614
|
-
index: '{t1s.output.data.index}'
|
|
615
|
-
workflowId: '{t1s.output.data.workflowId}'
|
|
616
|
-
|
|
617
|
-
transitions:
|
|
618
|
-
t1s:
|
|
619
|
-
- to: a1s
|
|
620
|
-
|
|
621
562
|
- subscribes: ${app}.wfsc.execute
|
|
622
563
|
publishes: ${app}.wfsc.executed
|
|
623
564
|
|
|
@@ -662,6 +603,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
662
603
|
stats:
|
|
663
604
|
id: '{$self.input.data.cycleWorkflowId}'
|
|
664
605
|
parent: '{$self.input.data.originJobId}'
|
|
606
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
665
607
|
|
|
666
608
|
a1wc:
|
|
667
609
|
title: Pivot - All Cycling Descendants Point Here
|
|
@@ -833,6 +775,7 @@ const getWorkflowYAML = (app: string, version: string) => {
|
|
|
833
775
|
id: '{$self.input.data.workflowId}'
|
|
834
776
|
key: '{$self.input.data.parentWorkflowId}'
|
|
835
777
|
parent: '{$self.input.data.originJobId}'
|
|
778
|
+
adjacent: '{$self.input.data.parentWorkflowId}'
|
|
836
779
|
|
|
837
780
|
a1ww:
|
|
838
781
|
title: WFS - signal entry point
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { HMSH_CODE_INTERRUPT } from '../../modules/enums';
|
|
2
|
+
import { ExporterService } from './exporter';
|
|
2
3
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
4
|
+
import { DurableJobExport } from '../../types/exporter';
|
|
3
5
|
import { JobInterruptOptions, JobOutput } from '../../types/job';
|
|
4
6
|
import { StreamError } from '../../types/stream';
|
|
5
7
|
|
|
6
8
|
export class WorkflowHandleService {
|
|
9
|
+
exporter: ExporterService
|
|
7
10
|
hotMesh: HotMesh;
|
|
8
11
|
workflowTopic: string;
|
|
9
12
|
workflowId: string;
|
|
@@ -12,6 +15,15 @@ export class WorkflowHandleService {
|
|
|
12
15
|
this.workflowTopic = workflowTopic;
|
|
13
16
|
this.workflowId = workflowId;
|
|
14
17
|
this.hotMesh = hotMesh;
|
|
18
|
+
this.exporter = new ExporterService(
|
|
19
|
+
this.hotMesh.appId,
|
|
20
|
+
this.hotMesh.engine.store,
|
|
21
|
+
this.hotMesh.engine.logger,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async export(): Promise<DurableJobExport> {
|
|
26
|
+
return this.exporter.export(this.workflowId);
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
/**
|
|
@@ -45,7 +45,7 @@ export class WorkflowService {
|
|
|
45
45
|
const entityOrEmptyString = options.entity ?? '';
|
|
46
46
|
//If the workflowId is not provided, it is generated from the entity and the workflow name
|
|
47
47
|
const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
|
|
48
|
-
const parentWorkflowId =
|
|
48
|
+
const parentWorkflowId = workflowId;
|
|
49
49
|
|
|
50
50
|
const client = new Client({
|
|
51
51
|
connection: await Connection.connect(WorkerService.connection),
|
|
@@ -91,33 +91,35 @@ export class WorkflowService {
|
|
|
91
91
|
const workflowSpan = store.get('workflowSpan');
|
|
92
92
|
const COUNTER = store.get('counter');
|
|
93
93
|
const execIndex = COUNTER.counter = COUNTER.counter + 1;
|
|
94
|
+
const sessionId = `-start${workflowDimension}-${execIndex}-`;
|
|
94
95
|
//NOTE: this is the hash prefix; necessary for the search index to locate the entity
|
|
95
96
|
const entityOrEmptyString = options.entity ?? '';
|
|
96
97
|
//If the workflowId is not provided, it is generated from the entity and the workflow name
|
|
97
|
-
const
|
|
98
|
-
const parentWorkflowId = `${workflowId}-f`;
|
|
98
|
+
const parentWorkflowId = workflowId;
|
|
99
99
|
const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} catch (error) {
|
|
107
|
-
const client = new Client({
|
|
108
|
-
connection: await Connection.connect(WorkerService.connection),
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
await client.workflow.start({
|
|
112
|
-
...options,
|
|
113
|
-
namespace,
|
|
114
|
-
workflowId: childJobId,
|
|
115
|
-
parentWorkflowId,
|
|
116
|
-
workflowTrace,
|
|
117
|
-
workflowSpan,
|
|
118
|
-
});
|
|
101
|
+
const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
|
|
102
|
+
const keyParams = { appId: hotMeshClient.appId, jobId: workflowId }
|
|
103
|
+
const workflowGuid = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
|
|
104
|
+
let childJobId = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId) as string;
|
|
105
|
+
if (childJobId) {
|
|
119
106
|
return childJobId;
|
|
107
|
+
} else {
|
|
108
|
+
childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
|
|
120
109
|
}
|
|
110
|
+
const client = new Client({
|
|
111
|
+
connection: await Connection.connect(WorkerService.connection),
|
|
112
|
+
});
|
|
113
|
+
await client.workflow.start({
|
|
114
|
+
...options,
|
|
115
|
+
namespace,
|
|
116
|
+
workflowId: childJobId,
|
|
117
|
+
parentWorkflowId,
|
|
118
|
+
workflowTrace,
|
|
119
|
+
workflowSpan,
|
|
120
|
+
});
|
|
121
|
+
await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, childJobId);
|
|
122
|
+
return childJobId;
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
/**
|
|
@@ -472,7 +474,7 @@ export class WorkflowService {
|
|
|
472
474
|
arguments: Array.from(arguments),
|
|
473
475
|
//when the origin job is removed
|
|
474
476
|
originJobId: originJobId ?? workflowId,
|
|
475
|
-
parentWorkflowId:
|
|
477
|
+
parentWorkflowId: workflowId,
|
|
476
478
|
workflowId: activityJobId,
|
|
477
479
|
workflowTopic: activityTopic,
|
|
478
480
|
activityName,
|