@hotmeshio/hotmesh 0.0.51 → 0.0.53
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 +13 -9
- package/build/index.d.ts +1 -2
- package/build/index.js +1 -3
- package/build/modules/enums.d.ts +8 -3
- package/build/modules/enums.js +16 -8
- package/build/modules/errors.d.ts +98 -18
- package/build/modules/errors.js +90 -33
- package/build/package.json +7 -2
- package/build/services/activities/activity.d.ts +8 -0
- package/build/services/activities/activity.js +65 -16
- package/build/services/activities/await.js +6 -6
- package/build/services/activities/cycle.d.ts +2 -2
- package/build/services/activities/cycle.js +5 -5
- package/build/services/activities/hook.js +4 -4
- package/build/services/activities/interrupt.d.ts +3 -3
- package/build/services/activities/interrupt.js +15 -6
- package/build/services/activities/signal.d.ts +2 -2
- package/build/services/activities/signal.js +4 -4
- package/build/services/activities/trigger.js +12 -3
- package/build/services/activities/worker.js +6 -6
- package/build/services/compiler/deployer.js +33 -5
- package/build/services/compiler/validator.d.ts +2 -0
- package/build/services/compiler/validator.js +5 -1
- package/build/services/durable/client.d.ts +7 -1
- package/build/services/durable/client.js +56 -30
- package/build/services/durable/exporter.d.ts +7 -72
- package/build/services/durable/exporter.js +105 -295
- package/build/services/durable/handle.d.ts +11 -6
- package/build/services/durable/handle.js +59 -46
- package/build/services/durable/index.d.ts +0 -2
- package/build/services/durable/index.js +0 -2
- package/build/services/durable/schemas/factory.d.ts +33 -0
- package/build/services/durable/schemas/factory.js +2356 -0
- package/build/services/durable/search.js +8 -8
- package/build/services/durable/worker.js +117 -25
- package/build/services/durable/workflow.d.ts +46 -43
- package/build/services/durable/workflow.js +273 -277
- package/build/services/engine/index.js +3 -0
- package/build/services/exporter/index.d.ts +2 -4
- package/build/services/exporter/index.js +4 -5
- package/build/services/mapper/index.d.ts +6 -2
- package/build/services/mapper/index.js +6 -2
- package/build/services/pipe/functions/array.d.ts +2 -10
- package/build/services/pipe/functions/array.js +30 -28
- package/build/services/pipe/functions/conditional.d.ts +1 -0
- package/build/services/pipe/functions/conditional.js +3 -0
- package/build/services/pipe/functions/date.d.ts +1 -0
- package/build/services/pipe/functions/date.js +4 -0
- package/build/services/pipe/functions/index.d.ts +2 -0
- package/build/services/pipe/functions/index.js +2 -0
- package/build/services/pipe/functions/logical.d.ts +5 -0
- package/build/services/pipe/functions/logical.js +12 -0
- package/build/services/pipe/functions/object.d.ts +3 -0
- package/build/services/pipe/functions/object.js +25 -7
- package/build/services/pipe/index.d.ts +20 -3
- package/build/services/pipe/index.js +82 -16
- package/build/services/router/index.js +14 -3
- package/build/services/serializer/index.d.ts +3 -2
- package/build/services/serializer/index.js +11 -4
- package/build/services/store/clients/ioredis.js +6 -6
- package/build/services/store/clients/redis.js +7 -7
- package/build/services/store/index.d.ts +2 -0
- package/build/services/store/index.js +4 -1
- package/build/services/stream/clients/ioredis.js +8 -8
- package/build/services/stream/clients/redis.js +1 -1
- package/build/types/activity.d.ts +60 -5
- package/build/types/durable.d.ts +168 -33
- package/build/types/exporter.d.ts +26 -4
- package/build/types/index.d.ts +2 -2
- package/build/types/job.d.ts +69 -5
- package/build/types/pipe.d.ts +81 -3
- package/build/types/stream.d.ts +61 -1
- package/build/types/stream.js +4 -0
- package/index.ts +1 -2
- package/modules/enums.ts +16 -8
- package/modules/errors.ts +174 -32
- package/package.json +7 -2
- package/services/activities/activity.ts +67 -18
- package/services/activities/await.ts +6 -6
- package/services/activities/cycle.ts +7 -6
- package/services/activities/hook.ts +4 -4
- package/services/activities/interrupt.ts +19 -9
- package/services/activities/signal.ts +6 -5
- package/services/activities/trigger.ts +16 -4
- package/services/activities/worker.ts +7 -7
- package/services/compiler/deployer.ts +33 -6
- package/services/compiler/validator.ts +7 -3
- package/services/durable/client.ts +47 -14
- package/services/durable/exporter.ts +110 -318
- package/services/durable/handle.ts +63 -50
- package/services/durable/index.ts +0 -2
- package/services/durable/schemas/factory.ts +2358 -0
- package/services/durable/search.ts +8 -8
- package/services/durable/worker.ts +128 -29
- package/services/durable/workflow.ts +304 -288
- package/services/engine/index.ts +4 -0
- package/services/exporter/index.ts +10 -12
- package/services/mapper/index.ts +6 -2
- package/services/pipe/functions/array.ts +24 -37
- package/services/pipe/functions/conditional.ts +4 -0
- package/services/pipe/functions/date.ts +6 -0
- package/services/pipe/functions/index.ts +7 -5
- package/services/pipe/functions/logical.ts +11 -0
- package/services/pipe/functions/object.ts +26 -7
- package/services/pipe/index.ts +99 -21
- package/services/quorum/index.ts +1 -3
- package/services/router/index.ts +14 -3
- package/services/serializer/index.ts +12 -5
- package/services/store/clients/ioredis.ts +6 -6
- package/services/store/clients/redis.ts +7 -7
- package/services/store/index.ts +4 -1
- package/services/stream/clients/ioredis.ts +8 -8
- package/services/stream/clients/redis.ts +1 -1
- package/types/activity.ts +87 -15
- package/types/durable.ts +246 -73
- package/types/exporter.ts +31 -5
- package/types/index.ts +6 -7
- package/types/job.ts +130 -36
- package/types/pipe.ts +84 -3
- package/types/stream.ts +82 -23
- package/build/services/durable/factory.d.ts +0 -17
- package/build/services/durable/factory.js +0 -817
- package/build/services/durable/meshos.d.ts +0 -127
- package/build/services/durable/meshos.js +0 -380
- package/services/durable/factory.ts +0 -818
- package/services/durable/meshos.ts +0 -441
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ms from 'ms';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
APP_ID,
|
|
5
|
+
APP_VERSION,
|
|
6
|
+
getWorkflowYAML } from './schemas/factory';
|
|
7
|
+
import {
|
|
8
|
+
HMSH_LOGLEVEL,
|
|
9
|
+
HMSH_EXPIRE_JOB_SECONDS,
|
|
10
|
+
HMSH_QUORUM_DELAY_MS,
|
|
11
|
+
HMSH_DURABLE_EXP_BACKOFF,
|
|
12
|
+
HMSH_DURABLE_MAX_ATTEMPTS,
|
|
13
|
+
HMSH_DURABLE_MAX_INTERVAL } from '../../modules/enums';
|
|
14
|
+
import { sleepFor } from '../../modules/utils';
|
|
2
15
|
import { WorkflowHandleService } from './handle';
|
|
3
16
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
4
17
|
import {
|
|
@@ -11,8 +24,6 @@ import { JobState } from '../../types/job';
|
|
|
11
24
|
import { KeyService, KeyType } from '../../modules/key';
|
|
12
25
|
import { Search } from './search';
|
|
13
26
|
import { StreamStatus } from '../../types';
|
|
14
|
-
import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
|
|
15
|
-
import { sleepFor } from '../../modules/utils';
|
|
16
27
|
|
|
17
28
|
export class ClientService {
|
|
18
29
|
|
|
@@ -32,7 +43,7 @@ export class ClientService {
|
|
|
32
43
|
await this.verifyWorkflowActive(hotMeshClient, targetNS);
|
|
33
44
|
if (!ClientService.topics.includes(workflowTopic)) {
|
|
34
45
|
ClientService.topics.push(workflowTopic);
|
|
35
|
-
await
|
|
46
|
+
await ClientService.createStream(hotMeshClient, workflowTopic, namespace);
|
|
36
47
|
}
|
|
37
48
|
return hotMeshClient;
|
|
38
49
|
}
|
|
@@ -49,7 +60,7 @@ export class ClientService {
|
|
|
49
60
|
}
|
|
50
61
|
});
|
|
51
62
|
ClientService.instances.set(targetNS, hotMeshClient);
|
|
52
|
-
await
|
|
63
|
+
await ClientService.createStream(await hotMeshClient, workflowTopic, namespace);
|
|
53
64
|
await this.activateWorkflow(await hotMeshClient, targetNS);
|
|
54
65
|
return hotMeshClient;
|
|
55
66
|
}
|
|
@@ -60,7 +71,7 @@ export class ClientService {
|
|
|
60
71
|
* has not yet been initialized, so this call ensures that the channel
|
|
61
72
|
* exists and is ready to serve as a container for events.
|
|
62
73
|
*/
|
|
63
|
-
createStream = async(hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => {
|
|
74
|
+
static createStream = async(hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => {
|
|
64
75
|
const store = hotMeshClient.engine.store;
|
|
65
76
|
const params = { appId: namespace ?? APP_ID, topic: workflowTopic };
|
|
66
77
|
const streamKey = store.mintKey(KeyType.STREAMS, params);
|
|
@@ -71,6 +82,23 @@ export class ClientService {
|
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
/**
|
|
86
|
+
* It is possible for a client to invoke a workflow without first
|
|
87
|
+
* creating the stream. This method will verify that the stream
|
|
88
|
+
* exists and if not, create it.
|
|
89
|
+
*/
|
|
90
|
+
static verifyStream = async(workflowTopic: string, namespace?: string) => {
|
|
91
|
+
const targetNS = namespace ?? APP_ID;
|
|
92
|
+
if (ClientService.instances.has(targetNS)) {
|
|
93
|
+
const hotMeshClient = await ClientService.instances.get(targetNS);
|
|
94
|
+
if (!ClientService.topics.includes(workflowTopic)) {
|
|
95
|
+
ClientService.topics.push(workflowTopic);
|
|
96
|
+
await ClientService.createStream(hotMeshClient, workflowTopic, namespace);
|
|
97
|
+
}
|
|
98
|
+
return hotMeshClient;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
/**
|
|
75
103
|
* For those deployments with a redis stack backend (with the FT module),
|
|
76
104
|
* this method will configure the search index for the workflow.
|
|
@@ -95,8 +123,8 @@ export class ClientService {
|
|
|
95
123
|
const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
|
|
96
124
|
const prefixes = search.prefix.map((prefix) => `${hotMeshPrefix}${prefix}`);
|
|
97
125
|
await store.exec('FT.CREATE', `${search.index}`, 'ON', 'HASH', 'PREFIX', prefixes.length, ...prefixes, 'SCHEMA', ...schema);
|
|
98
|
-
} catch (
|
|
99
|
-
hotMeshClient.engine.logger.info('durable-client-search-err', {
|
|
126
|
+
} catch (error) {
|
|
127
|
+
hotMeshClient.engine.logger.info('durable-client-search-err', { ...error });
|
|
100
128
|
}
|
|
101
129
|
}
|
|
102
130
|
}
|
|
@@ -127,8 +155,11 @@ export class ClientService {
|
|
|
127
155
|
parentWorkflowId: options.parentWorkflowId,
|
|
128
156
|
workflowId: options.workflowId || HotMesh.guid(),
|
|
129
157
|
workflowTopic: workflowTopic,
|
|
130
|
-
backoffCoefficient: options.config?.backoffCoefficient ||
|
|
158
|
+
backoffCoefficient: options.config?.backoffCoefficient || HMSH_DURABLE_EXP_BACKOFF,
|
|
159
|
+
maximumAttempts: options.config?.maximumAttempts || HMSH_DURABLE_MAX_ATTEMPTS,
|
|
160
|
+
maximumInterval: ms(options.config?.maximumInterval || HMSH_DURABLE_MAX_INTERVAL) / 1000,
|
|
131
161
|
}
|
|
162
|
+
|
|
132
163
|
const context = { metadata: { trc, spn }, data: {}};
|
|
133
164
|
const jobId = await hotMeshClient.pub(
|
|
134
165
|
`${options.namespace ?? APP_ID}.execute`,
|
|
@@ -165,7 +196,9 @@ export class ClientService {
|
|
|
165
196
|
arguments: [...options.args],
|
|
166
197
|
id: options.workflowId,
|
|
167
198
|
workflowTopic,
|
|
168
|
-
backoffCoefficient: options.config?.backoffCoefficient ||
|
|
199
|
+
backoffCoefficient: options.config?.backoffCoefficient || HMSH_DURABLE_EXP_BACKOFF,
|
|
200
|
+
maximumAttempts: options.config?.maximumAttempts || HMSH_DURABLE_MAX_ATTEMPTS,
|
|
201
|
+
maximumInterval: ms(options.config?.maximumInterval || HMSH_DURABLE_MAX_INTERVAL) / 1000,
|
|
169
202
|
}
|
|
170
203
|
//seed search data if presentthe hook before entering
|
|
171
204
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
|
|
@@ -190,9 +223,9 @@ export class ClientService {
|
|
|
190
223
|
const hotMeshClient = await this.getHotMeshClient(workflowTopic, namespace);
|
|
191
224
|
try {
|
|
192
225
|
return await this.search(hotMeshClient, index, query);
|
|
193
|
-
} catch (
|
|
194
|
-
hotMeshClient.engine.logger.error('durable-client-search-err', {
|
|
195
|
-
throw
|
|
226
|
+
} catch (error) {
|
|
227
|
+
hotMeshClient.engine.logger.error('durable-client-search-err', { ...error });
|
|
228
|
+
throw error;
|
|
196
229
|
}
|
|
197
230
|
}
|
|
198
231
|
}
|
|
@@ -218,7 +251,7 @@ export class ClientService {
|
|
|
218
251
|
await hotMesh.deploy(getWorkflowYAML(appId, version));
|
|
219
252
|
await hotMesh.activate(version);
|
|
220
253
|
} catch (error) {
|
|
221
|
-
hotMesh.engine.logger.error('durable-client-deploy-activate-err', { error });
|
|
254
|
+
hotMesh.engine.logger.error('durable-client-deploy-activate-err', { ...error });
|
|
222
255
|
throw error;
|
|
223
256
|
}
|
|
224
257
|
} else if(app && !app.active) {
|
|
@@ -1,184 +1,43 @@
|
|
|
1
|
+
import { VALSEP } from '../../modules/key';
|
|
2
|
+
import { restoreHierarchy } from '../../modules/utils';
|
|
1
3
|
import { ILogger } from '../logger';
|
|
4
|
+
import { SerializerService } from '../serializer';
|
|
2
5
|
import { StoreService } from '../store';
|
|
3
|
-
import { StringStringType, Symbols } from "../../types";
|
|
4
|
-
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
5
6
|
import {
|
|
6
|
-
ActivityAction,
|
|
7
|
-
DependencyExport,
|
|
8
7
|
ExportItem,
|
|
9
8
|
ExportOptions,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
import { restoreHierarchy } from '../../modules/utils';
|
|
16
|
-
import { VALSEP } from '../../modules/key';
|
|
9
|
+
DurableJobExport,
|
|
10
|
+
IdemType,
|
|
11
|
+
TimelineEntry } from '../../types/exporter';
|
|
12
|
+
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
13
|
+
import { StringStringType, Symbols } from "../../types/serializer";
|
|
17
14
|
|
|
18
|
-
/**
|
|
19
|
-
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
20
|
-
* Splits, Inflates, and Sorts the job data for use in durable contexts
|
|
21
|
-
*/
|
|
22
15
|
class ExporterService {
|
|
23
16
|
appId: string;
|
|
24
17
|
logger: ILogger;
|
|
25
|
-
serializer: SerializerService
|
|
26
18
|
store: StoreService<RedisClient, RedisMulti>;
|
|
27
19
|
symbols: Promise<Symbols> | Symbols;
|
|
28
20
|
|
|
29
|
-
/**
|
|
30
|
-
* Friendly names for the activity ids
|
|
31
|
-
*/
|
|
32
|
-
activitySymbols: Symbols = {
|
|
33
|
-
t1: 'trigger',
|
|
34
|
-
a1: 'pivot',
|
|
35
|
-
w1: 'worker',
|
|
36
|
-
a592: 'sleeper',
|
|
37
|
-
a594: 'awaiter',
|
|
38
|
-
a599: 'retryer',
|
|
39
|
-
c592: 'sleep_cycler',
|
|
40
|
-
c594: 'await_cycler',
|
|
41
|
-
c599: 'retry_cycler',
|
|
42
|
-
s5: 'scrubber',
|
|
43
|
-
sig: 'hook',
|
|
44
|
-
siga1: 'hook_pivot',
|
|
45
|
-
sigw1: 'hook_worker',
|
|
46
|
-
siga592: 'hook_sleeper',
|
|
47
|
-
siga594: 'hook_awaiter',
|
|
48
|
-
siga599: 'hook_retryer',
|
|
49
|
-
sigc592: 'hook_sleep_cycler',
|
|
50
|
-
sigc594: 'hook_await_cycler',
|
|
51
|
-
sigc599: 'hook_retry_cycler',
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
//adjacent transitions
|
|
55
|
-
transitions = {
|
|
56
|
-
trigger: ['pivot', 'hook'],
|
|
57
|
-
pivot: ['worker'],
|
|
58
|
-
worker: ['sleeper', 'awaiter', 'retryer', 'scrubber'],
|
|
59
|
-
sleeper: ['sleep_cycler'],
|
|
60
|
-
awaiter: ['await_cycler'],
|
|
61
|
-
retryer: ['retry_cycler'],
|
|
62
|
-
hook: ['hook_pivot'],
|
|
63
|
-
hook_pivot: ['hook_worker'],
|
|
64
|
-
hook_worker: ['hook_sleeper', 'hook_awaiter', 'hook_retryer'],
|
|
65
|
-
hook_sleeper: ['hook_sleep_cycler'],
|
|
66
|
-
hook_awaiter: ['hook_await_cycler'],
|
|
67
|
-
hook_retryer: ['hook_retry_cycler'],
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
//goto transitions
|
|
71
|
-
cycles = {
|
|
72
|
-
sleep_cycler: ['pivot'],
|
|
73
|
-
await_cycler: ['pivot'],
|
|
74
|
-
retry_cycler: ['pivot'],
|
|
75
|
-
hook_sleep_cycler: ['hook_pivot'],
|
|
76
|
-
hook_await_cycler: ['hook_pivot'],
|
|
77
|
-
hook_retry_cycler: ['hook_pivot'],
|
|
78
|
-
}
|
|
79
|
-
|
|
80
21
|
constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
|
|
81
22
|
this.appId = appId;
|
|
82
23
|
this.logger = logger;
|
|
83
24
|
this.store = store;
|
|
84
|
-
this.serializer = new SerializerService();
|
|
85
25
|
}
|
|
86
26
|
|
|
87
27
|
/**
|
|
88
|
-
* Convert the job hash
|
|
89
|
-
*
|
|
90
|
-
* in terms relevant to narrative storytelling.
|
|
28
|
+
* Convert the job hash from its compiles format into a DurableJobExport object with
|
|
29
|
+
* facets that describe the workflow in terms relevant to narrative storytelling.
|
|
91
30
|
*/
|
|
92
31
|
async export(jobId: string, options: ExportOptions = {}): Promise<DurableJobExport> {
|
|
93
32
|
if (!this.symbols) {
|
|
94
33
|
this.symbols = this.store.getAllSymbols();
|
|
95
34
|
this.symbols = await this.symbols;
|
|
96
35
|
}
|
|
97
|
-
const depData = await this.store.getDependencies(jobId);
|
|
98
36
|
const jobData = await this.store.getRaw(jobId);
|
|
99
|
-
const jobExport = this.inflate(jobData
|
|
37
|
+
const jobExport = this.inflate(jobData/*, depData*/);
|
|
100
38
|
return jobExport;
|
|
101
39
|
}
|
|
102
40
|
|
|
103
|
-
/**
|
|
104
|
-
* Interleave actions into the replay timeline to create
|
|
105
|
-
* a time-ordered timeline of the entire interaction, beginning
|
|
106
|
-
* with the entry trigger and concluding with the scrubber
|
|
107
|
-
* activity. Using the returned timeline, it is possible to
|
|
108
|
-
* create an animated narrative of the job, highlighting
|
|
109
|
-
* activities in the graph according to the timeline's
|
|
110
|
-
* activity-created (/ac) and activity-updated (/au) entries.
|
|
111
|
-
*/
|
|
112
|
-
createTimeline(replay: ExportItem[], actions: JobActionExport): JobTimeline[] {
|
|
113
|
-
const timeline: JobTimeline[] = [];
|
|
114
|
-
replay.forEach((item) => {
|
|
115
|
-
const dimensions = item[0];
|
|
116
|
-
const parts = dimensions.split('/');
|
|
117
|
-
const activityName = item[1].split('/')[0];
|
|
118
|
-
const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
|
|
119
|
-
const timestamp = item[2];
|
|
120
|
-
let event: JobTimeline = {
|
|
121
|
-
activity: activityName,
|
|
122
|
-
duplex: duplex as 'entry' | 'exit',
|
|
123
|
-
dimension: dimensions,
|
|
124
|
-
timestamp,
|
|
125
|
-
created: timestamp,
|
|
126
|
-
updated: timestamp,
|
|
127
|
-
};
|
|
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
|
-
}
|
|
139
|
-
|
|
140
|
-
if (this.isMainEntry(item[1])) {
|
|
141
|
-
event.actions = [] as ActivityAction[];
|
|
142
|
-
this.interleaveActions(actions.main, event.actions);
|
|
143
|
-
} else if (this.isHookEntry(item[1])) {
|
|
144
|
-
const hookDimension = `/${parts[1]}/${parts[2]}`;
|
|
145
|
-
const hookActions = actions.hooks[hookDimension];
|
|
146
|
-
event.actions = [] as ActivityAction[];
|
|
147
|
-
this.interleaveActions(hookActions, event.actions);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
return timeline;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Interleave actions into the 'worker' and 'hook_worker'
|
|
155
|
-
* activities (between their /ac and /au entries)
|
|
156
|
-
*/
|
|
157
|
-
interleaveActions(target: JobAction, actions: ActivityAction[]) {
|
|
158
|
-
if (target) {
|
|
159
|
-
for (let i = target.cursor + 1; i < target.items.length; i++) {
|
|
160
|
-
const [_, actionType, jobOrIndex] = target.items[i];
|
|
161
|
-
actions.push({ action: actionType, target: jobOrIndex });
|
|
162
|
-
target.cursor = i;
|
|
163
|
-
if (this.isPausingAction(actionType)) {
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
isPausingAction(actionType: string): boolean {
|
|
171
|
-
return actionType === 'sleep' || actionType === 'waitForSignal';
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
isMainEntry(key: string): boolean {
|
|
175
|
-
return key.startsWith('worker/') && key.endsWith('/ac');
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
isHookEntry(key: string): boolean {
|
|
179
|
-
return key.startsWith('hook_worker/') && key.endsWith('/ac');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
41
|
/**
|
|
183
42
|
* Inflates the key from Redis, 3-character symbol
|
|
184
43
|
* into a human-readable JSON path, reflecting the
|
|
@@ -188,9 +47,6 @@ class ExporterService {
|
|
|
188
47
|
if (key in this.symbols) {
|
|
189
48
|
const path = this.symbols[key];
|
|
190
49
|
const parts = path.split('/');
|
|
191
|
-
if (parts[0] in this.activitySymbols) {
|
|
192
|
-
parts[0] = this.activitySymbols[parts[0]];
|
|
193
|
-
}
|
|
194
50
|
return parts.join('/');
|
|
195
51
|
}
|
|
196
52
|
return key;
|
|
@@ -203,89 +59,19 @@ class ExporterService {
|
|
|
203
59
|
* @param data - the dependency data from Redis
|
|
204
60
|
* @returns - the organized dependency data
|
|
205
61
|
*/
|
|
206
|
-
inflateDependencyData(data: string[]
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const [action, topic, gid, _pd, ...jid] = dependency.split(VALSEP);
|
|
211
|
-
const jobId = jid.join(VALSEP);
|
|
212
|
-
const match = jobId.match(hookReg);
|
|
213
|
-
let prefix: string;
|
|
214
|
-
let type: 'hook' | 'flow' | 'other';
|
|
215
|
-
let dimensionKey: string = '';
|
|
216
|
-
if (match) {
|
|
217
|
-
//hook-originating dependency
|
|
218
|
-
const [_, dimension, counter] = match;
|
|
219
|
-
dimensionKey = dimension.split(',').join('/');
|
|
220
|
-
prefix = `${dimensionKey}[${counter}]`;
|
|
221
|
-
type = 'hook';
|
|
222
|
-
} else {
|
|
223
|
-
const match = jobId.match(flowReg);
|
|
224
|
-
if (match) {
|
|
225
|
-
//main workflow-originating dependency
|
|
226
|
-
const [_, counter] = match;
|
|
227
|
-
prefix = `[${counter}]`;
|
|
228
|
-
type = 'flow';
|
|
229
|
-
} else {
|
|
230
|
-
//'other' types like signal cleanup
|
|
231
|
-
prefix = '/';
|
|
232
|
-
type = 'other';
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
this.seedActions(
|
|
236
|
-
type,
|
|
237
|
-
action,
|
|
238
|
-
topic,
|
|
239
|
-
dependency,
|
|
240
|
-
prefix,
|
|
241
|
-
dimensionKey,
|
|
242
|
-
actions,
|
|
243
|
-
jobId,
|
|
244
|
-
);
|
|
62
|
+
inflateDependencyData(data: string[]): Record<string, any>[] {
|
|
63
|
+
return data.map((dependency, index: number): Record<string, any> => {
|
|
64
|
+
const [action, topic, gid, dimension, ...jid] = dependency.split(VALSEP);
|
|
65
|
+
const job_id = jid.join(VALSEP);
|
|
245
66
|
return {
|
|
246
|
-
|
|
67
|
+
index,
|
|
68
|
+
action,
|
|
247
69
|
topic,
|
|
248
70
|
gid,
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* Adds historical actions (proxyActivity, executeChild)
|
|
256
|
-
* using the `dependency list` to determine
|
|
257
|
-
* after-the-fact what happened within the 'black-box'
|
|
258
|
-
* worker function. This is necessary to interleave the
|
|
259
|
-
* actions into the replay timeline, given that it isn't
|
|
260
|
-
* really possible to know the inner-workings of the user's
|
|
261
|
-
* function
|
|
262
|
-
*
|
|
263
|
-
*/
|
|
264
|
-
seedActions(type: 'flow'|'hook'|'other', action: string, topic: string, dep: string, prefix: string, dimensionKey: string, actions: JobActionExport, jobId: string) {
|
|
265
|
-
if (type !== 'other' && action === 'expire-child') {
|
|
266
|
-
let depType: string;
|
|
267
|
-
if (topic == `${this.appId}.activity.execute`) {
|
|
268
|
-
depType = 'proxyActivity';
|
|
269
|
-
} else if (topic == `${this.appId}.execute`) {
|
|
270
|
-
depType = 'executeChild';
|
|
271
|
-
} else if (topic == `${this.appId}.wfsc.execute`) {
|
|
272
|
-
depType = 'waitForSignal';
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (depType) {
|
|
276
|
-
if (type === 'flow') {
|
|
277
|
-
actions.main.items.push([prefix, depType, jobId]);
|
|
278
|
-
} else if (type === 'hook') {
|
|
279
|
-
if (!actions.hooks[dimensionKey]) {
|
|
280
|
-
actions.hooks[dimensionKey] = {
|
|
281
|
-
cursor: -1,
|
|
282
|
-
items: [],
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
actions.hooks[dimensionKey].items.push([prefix, depType, jobId]);
|
|
286
|
-
}
|
|
71
|
+
dimension,
|
|
72
|
+
job_id,
|
|
287
73
|
}
|
|
288
|
-
}
|
|
74
|
+
});
|
|
289
75
|
}
|
|
290
76
|
|
|
291
77
|
/**
|
|
@@ -294,17 +80,12 @@ class ExporterService {
|
|
|
294
80
|
* @param dependencyList - the list of dependencies for the job
|
|
295
81
|
* @returns - the inflated job data
|
|
296
82
|
*/
|
|
297
|
-
inflate(jobHash: StringStringType
|
|
298
|
-
|
|
299
|
-
const actions: JobActionExport = {
|
|
300
|
-
hooks: {},
|
|
301
|
-
main: { cursor: -1, items: [] },
|
|
302
|
-
};
|
|
303
|
-
const dependencies = this.inflateDependencyData(dependencyList, actions);
|
|
83
|
+
inflate(jobHash: StringStringType): DurableJobExport {
|
|
84
|
+
const idempotents: IdemType[] = [];
|
|
304
85
|
const state: StringStringType = {};
|
|
305
86
|
const data: StringStringType = {};
|
|
306
87
|
const other: ExportItem[] = [];
|
|
307
|
-
const replay:
|
|
88
|
+
const replay: Record<string, TimelineEntry> = {};
|
|
308
89
|
const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
|
|
309
90
|
|
|
310
91
|
Object.entries(jobHash).forEach(([key, value]) => {
|
|
@@ -314,104 +95,115 @@ class ExporterService {
|
|
|
314
95
|
this.inflateProcess(match, value, replay);
|
|
315
96
|
} else if (key.length === 3) {
|
|
316
97
|
//job state
|
|
317
|
-
state[this.inflateKey(key)] =
|
|
98
|
+
state[this.inflateKey(key)] = SerializerService.fromString(value);
|
|
318
99
|
} else if (key.startsWith('_')) {
|
|
319
100
|
//job data
|
|
320
101
|
data[key.substring(1)] = value;
|
|
321
102
|
} else if (key.startsWith('-')) {
|
|
322
103
|
//actions with side effect (replayable)
|
|
323
|
-
|
|
104
|
+
idempotents.push({
|
|
105
|
+
key,
|
|
106
|
+
value: SerializerService.fromString(value),
|
|
107
|
+
parts: extractParts(key),
|
|
108
|
+
});
|
|
324
109
|
} else {
|
|
325
110
|
//collator guids, etc
|
|
326
111
|
other.push([null, key, value]);
|
|
327
112
|
}
|
|
328
113
|
});
|
|
329
114
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
115
|
+
const sortEntriesByCreated = (obj: { [key: string]: TimelineEntry }): TimelineEntry[] => {
|
|
116
|
+
const entriesArray: TimelineEntry[] = Object.values(obj);
|
|
117
|
+
entriesArray.sort((a, b) => {
|
|
118
|
+
return (a.created || a.updated).localeCompare(b.created || b.updated);
|
|
119
|
+
});
|
|
120
|
+
return entriesArray;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* idem list has a complicated sort order based on indexes and dimensions
|
|
125
|
+
*/
|
|
126
|
+
const sortParts = (parts: IdemType[]): IdemType[]=> {
|
|
127
|
+
return parts.sort((a, b) => {
|
|
128
|
+
const { dimension: aDim, index: aIdx, secondary: aSec } = a.parts;
|
|
129
|
+
const { dimension: bDim, index: bIdx, secondary: bSec } = b.parts;
|
|
130
|
+
|
|
131
|
+
if (aDim === undefined && bDim !== undefined) return -1;
|
|
132
|
+
if (aDim !== undefined && bDim === undefined) return 1;
|
|
133
|
+
if (aDim !== undefined && bDim !== undefined) {
|
|
134
|
+
if (aDim < bDim) return -1;
|
|
135
|
+
if (aDim > bDim) return 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (aIdx < bIdx) return -1;
|
|
139
|
+
if (aIdx > bIdx) return 1;
|
|
140
|
+
|
|
141
|
+
if (aSec === undefined && bSec !== undefined) return -1;
|
|
142
|
+
if (aSec !== undefined && bSec === undefined) return 1;
|
|
143
|
+
if (aSec !== undefined && bSec !== undefined) {
|
|
144
|
+
if (aSec < bSec) return -1;
|
|
145
|
+
if (aSec > bSec) return 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return 0;
|
|
149
|
+
});
|
|
150
|
+
};
|
|
335
151
|
|
|
152
|
+
function extractParts(key: string): {index: number, dimension?: string, secondary?: number} {
|
|
153
|
+
function extractDimension(label: string): string {
|
|
154
|
+
const parts = label.split(',');
|
|
155
|
+
if (parts.length > 1) {
|
|
156
|
+
parts.shift();
|
|
157
|
+
return parts.join(',');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const parts = key.split('-');
|
|
162
|
+
if (parts.length === 4) {
|
|
163
|
+
//-proxy-5- -search-1-1-
|
|
164
|
+
return {
|
|
165
|
+
index: parseInt(parts[2], 10),
|
|
166
|
+
dimension: extractDimension(parts[1]),
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
//-search,0,0-1-1- -proxy,0,0-1-
|
|
170
|
+
return {
|
|
171
|
+
index: parseInt(parts[2], 10),
|
|
172
|
+
secondary: parseInt(parts[3], 10),
|
|
173
|
+
dimension: extractDimension(parts[1]),
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
336
178
|
return {
|
|
337
179
|
data: restoreHierarchy(data),
|
|
338
|
-
|
|
180
|
+
idempotents: sortParts(idempotents),
|
|
339
181
|
state: Object.entries(restoreHierarchy(state))[0][1],
|
|
340
182
|
status: jobHash[':'],
|
|
341
|
-
|
|
342
|
-
transitions: { ...this.transitions },
|
|
343
|
-
cycles: { ...this.cycles },
|
|
183
|
+
replay: sortEntriesByCreated(replay),
|
|
344
184
|
};
|
|
345
185
|
}
|
|
346
186
|
|
|
347
|
-
inflateProcess(match: RegExpMatchArray, value: string, replay:
|
|
348
|
-
const [_, letters,
|
|
187
|
+
inflateProcess(match: RegExpMatchArray, value: string, replay: Record<string, Record<string, any>>) {
|
|
188
|
+
const [_, letters, dimensions] = match;
|
|
349
189
|
const path = this.inflateKey(letters);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
let [_, dimensionalType, counter, subcounter] = key.split('-');
|
|
364
|
-
if (subcounter) {
|
|
365
|
-
counter = `${counter}.${subcounter}`;
|
|
366
|
-
}
|
|
367
|
-
const [type, ...dimensions] = dimensionalType.split(',');
|
|
368
|
-
let dimensionKey = '';
|
|
369
|
-
let isHook = false;
|
|
370
|
-
if (dimensions.length > 0) {
|
|
371
|
-
dimensionKey = `/${dimensions.join('/')}`;
|
|
372
|
-
isHook = true;
|
|
373
|
-
}
|
|
374
|
-
let targetList: ExportItem[];
|
|
375
|
-
if (isHook) {
|
|
376
|
-
if (!actions.hooks[dimensionKey]) {
|
|
377
|
-
actions.hooks[dimensionKey] = {
|
|
378
|
-
cursor: -1,
|
|
379
|
-
items: [],
|
|
190
|
+
const parts = path.split('/');
|
|
191
|
+
const activity = parts[0];
|
|
192
|
+
const isCreate = path.endsWith('/output/metadata/ac');
|
|
193
|
+
const isUpdate = path.endsWith('/output/metadata/au');
|
|
194
|
+
if (isCreate || isUpdate) {
|
|
195
|
+
const targetName = `${activity},${dimensions}`;
|
|
196
|
+
let target = replay[targetName];
|
|
197
|
+
if (!target) {
|
|
198
|
+
replay[targetName] = {
|
|
199
|
+
activity,
|
|
200
|
+
dimensions,
|
|
201
|
+
created: isCreate ? value : null,
|
|
202
|
+
updated: isUpdate ? value : null,
|
|
380
203
|
};
|
|
204
|
+
} else {
|
|
205
|
+
target[isCreate ? 'created' : 'updated'] = value;
|
|
381
206
|
}
|
|
382
|
-
targetList = actions.hooks[dimensionKey].items;
|
|
383
|
-
} else {
|
|
384
|
-
targetList = actions.main.items;
|
|
385
|
-
}
|
|
386
|
-
targetList.push([
|
|
387
|
-
`${dimensionKey}[${counter}]`,
|
|
388
|
-
type,
|
|
389
|
-
value,
|
|
390
|
-
]);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
reverseSort(aKey: ExportItem, bKey: ExportItem) {
|
|
394
|
-
if (aKey[0] > bKey[0]) {
|
|
395
|
-
return 1;
|
|
396
|
-
} else if (aKey[0] < bKey[0]) {
|
|
397
|
-
return -1;
|
|
398
|
-
} else {
|
|
399
|
-
if (aKey[1] > bKey[1]) {
|
|
400
|
-
return 1;
|
|
401
|
-
} else if (aKey[1] < bKey[1]) {
|
|
402
|
-
return -1;
|
|
403
|
-
}
|
|
404
|
-
return 0;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
dateSort(aKey: ExportItem, bKey: ExportItem) {
|
|
409
|
-
if (aKey[2] > bKey[2]) {
|
|
410
|
-
return 1;
|
|
411
|
-
} else if (aKey[2] < bKey[2]) {
|
|
412
|
-
return -1;
|
|
413
|
-
} else {
|
|
414
|
-
return 0;
|
|
415
207
|
}
|
|
416
208
|
}
|
|
417
209
|
}
|