@hotmeshio/hotmesh 0.0.12 → 0.0.14
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 +2 -2
- package/build/modules/errors.d.ts +22 -1
- package/build/modules/errors.js +28 -1
- package/build/modules/utils.d.ts +2 -1
- package/build/modules/utils.js +5 -1
- package/build/package.json +7 -2
- package/build/services/activities/activity.d.ts +2 -0
- package/build/services/activities/activity.js +16 -10
- package/build/services/activities/await.d.ts +2 -6
- package/build/services/activities/await.js +12 -75
- package/build/services/activities/cycle.js +2 -2
- package/build/services/activities/index.d.ts +2 -2
- package/build/services/activities/index.js +2 -2
- package/build/services/activities/signal.d.ts +16 -0
- package/build/services/activities/signal.js +94 -0
- package/build/services/activities/trigger.js +4 -3
- package/build/services/activities/worker.d.ts +2 -1
- package/build/services/activities/worker.js +11 -6
- package/build/services/compiler/deployer.js +3 -1
- package/build/services/durable/client.d.ts +3 -2
- package/build/services/durable/client.js +39 -21
- package/build/services/durable/factory.d.ts +22 -18
- package/build/services/durable/factory.js +722 -50
- package/build/services/durable/handle.d.ts +1 -0
- package/build/services/durable/handle.js +5 -1
- package/build/services/durable/worker.d.ts +3 -8
- package/build/services/durable/worker.js +75 -73
- package/build/services/durable/workflow.d.ts +5 -0
- package/build/services/durable/workflow.js +93 -24
- package/build/services/engine/index.d.ts +6 -6
- package/build/services/engine/index.js +25 -15
- package/build/services/hotmesh/index.d.ts +2 -1
- package/build/services/hotmesh/index.js +3 -1
- package/build/services/mapper/index.js +1 -1
- package/build/services/pipe/functions/array.d.ts +1 -0
- package/build/services/pipe/functions/array.js +3 -0
- package/build/services/reporter/index.js +9 -2
- package/build/services/signaler/store.js +8 -3
- package/build/services/signaler/stream.js +3 -3
- package/build/services/store/clients/ioredis.js +15 -15
- package/build/services/store/clients/redis.js +18 -18
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/index.js +11 -3
- package/build/services/task/index.js +3 -3
- package/build/types/activity.d.ts +15 -6
- package/build/types/durable.d.ts +15 -2
- package/build/types/index.d.ts +2 -2
- package/build/types/stats.d.ts +1 -0
- package/modules/errors.ts +35 -0
- package/modules/utils.ts +5 -1
- package/package.json +7 -2
- package/services/activities/activity.ts +19 -9
- package/services/activities/await.ts +14 -90
- package/services/activities/cycle.ts +2 -2
- package/services/activities/index.ts +2 -2
- package/services/activities/signal.ts +124 -0
- package/services/activities/trigger.ts +4 -3
- package/services/activities/worker.ts +13 -13
- package/services/compiler/deployer.ts +3 -1
- package/services/durable/client.ts +48 -23
- package/services/durable/factory.ts +723 -49
- package/services/durable/handle.ts +6 -1
- package/services/durable/worker.ts +92 -79
- package/services/durable/workflow.ts +95 -25
- package/services/engine/index.ts +33 -24
- package/services/hotmesh/index.ts +7 -4
- package/services/mapper/index.ts +1 -1
- package/services/pipe/functions/array.ts +4 -0
- package/services/reporter/index.ts +10 -2
- package/services/signaler/store.ts +8 -3
- package/services/signaler/stream.ts +3 -3
- package/services/store/clients/ioredis.ts +15 -15
- package/services/store/clients/redis.ts +18 -18
- package/services/store/index.ts +12 -3
- package/services/task/index.ts +3 -3
- package/types/activity.ts +16 -7
- package/types/durable.ts +18 -1
- package/types/index.ts +2 -1
- package/types/stats.ts +1 -0
- package/build/services/activities/emit.d.ts +0 -9
- package/build/services/activities/emit.js +0 -13
- package/services/activities/emit.ts +0 -25
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GetStateError } from '../../modules/errors';
|
|
2
2
|
import { Activity } from './activity';
|
|
3
|
+
import { CollatorService } from '../collator';
|
|
3
4
|
import { EngineService } from '../engine';
|
|
4
5
|
import {
|
|
5
6
|
ActivityData,
|
|
@@ -7,15 +8,10 @@ import {
|
|
|
7
8
|
AwaitActivity,
|
|
8
9
|
ActivityType } from '../../types/activity';
|
|
9
10
|
import { JobState } from '../../types/job';
|
|
10
|
-
import { MultiResponseFlags } from '../../types/redis';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
StreamCode,
|
|
14
|
-
StreamData,
|
|
15
|
-
StreamDataType,
|
|
16
|
-
StreamStatus } from '../../types/stream';
|
|
11
|
+
import { MultiResponseFlags, RedisMulti } from '../../types/redis';
|
|
12
|
+
import { StreamData, StreamDataType } from '../../types/stream';
|
|
17
13
|
import { TelemetryService } from '../telemetry';
|
|
18
|
-
import {
|
|
14
|
+
import { Pipe } from '../pipe';
|
|
19
15
|
|
|
20
16
|
class Await extends Activity {
|
|
21
17
|
config: AwaitActivity;
|
|
@@ -35,24 +31,26 @@ class Await extends Activity {
|
|
|
35
31
|
this.logger.debug('await-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
36
32
|
let telemetry: TelemetryService;
|
|
37
33
|
try {
|
|
34
|
+
//confirm entry is allowed and restore state
|
|
38
35
|
this.setLeg(1);
|
|
39
36
|
await CollatorService.notarizeEntry(this);
|
|
40
|
-
|
|
41
37
|
await this.getState();
|
|
42
38
|
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
43
39
|
telemetry.startActivitySpan(this.leg);
|
|
44
40
|
this.mapInputData();
|
|
45
41
|
|
|
42
|
+
//save state and authorize reentry
|
|
46
43
|
const multi = this.store.getMulti();
|
|
47
44
|
//todo: await this.registerTimeout();
|
|
45
|
+
const messageId = await this.execActivity(multi);
|
|
48
46
|
await CollatorService.authorizeReentry(this, multi);
|
|
49
47
|
await this.setState(multi);
|
|
50
48
|
await this.setStatus(0, multi);
|
|
51
49
|
const multiResponse = await multi.exec() as MultiResponseFlags;
|
|
52
50
|
|
|
51
|
+
//telemetry
|
|
53
52
|
telemetry.mapActivityAttributes();
|
|
54
53
|
const jobStatus = this.resolveStatus(multiResponse);
|
|
55
|
-
const messageId = await this.execActivity();
|
|
56
54
|
telemetry.setActivityAttributes({
|
|
57
55
|
'app.activity.mid': messageId,
|
|
58
56
|
'app.job.jss': jobStatus
|
|
@@ -61,9 +59,9 @@ class Await extends Activity {
|
|
|
61
59
|
} catch (error) {
|
|
62
60
|
telemetry.setActivityError(error.message);
|
|
63
61
|
if (error instanceof GetStateError) {
|
|
64
|
-
this.logger.error('await-get-state-error', error);
|
|
62
|
+
this.logger.error('await-get-state-error', { error });
|
|
65
63
|
} else {
|
|
66
|
-
this.logger.error('await-process-error', error);
|
|
64
|
+
this.logger.error('await-process-error', { error });
|
|
67
65
|
}
|
|
68
66
|
throw error;
|
|
69
67
|
} finally {
|
|
@@ -72,14 +70,14 @@ class Await extends Activity {
|
|
|
72
70
|
}
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
async execActivity(multi: RedisMulti): Promise<string> {
|
|
74
|
+
const topic = Pipe.resolve(this.config.subtype, this.context);
|
|
77
75
|
const streamData: StreamData = {
|
|
78
76
|
metadata: {
|
|
79
77
|
jid: this.context.metadata.jid,
|
|
80
78
|
dad: this.metadata.dad,
|
|
81
79
|
aid: this.metadata.aid,
|
|
82
|
-
topic
|
|
80
|
+
topic,
|
|
83
81
|
spn: this.context['$self'].output.metadata?.l1s,
|
|
84
82
|
trc: this.context.metadata.trc,
|
|
85
83
|
},
|
|
@@ -91,81 +89,7 @@ class Await extends Activity {
|
|
|
91
89
|
retry: this.config.retry
|
|
92
90
|
};
|
|
93
91
|
}
|
|
94
|
-
return (await this.engine.streamSignaler?.publishMessage(null, streamData)) as string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//******** `RESOLVE` ENTRY POINT (B) ********//
|
|
99
|
-
//this method is invoked when the job spawned by this job ends;
|
|
100
|
-
//`this.data` is the job data produced by the spawned job
|
|
101
|
-
async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200): Promise<void> {
|
|
102
|
-
this.setLeg(2);
|
|
103
|
-
const jid = this.context.metadata.jid;
|
|
104
|
-
const aid = this.metadata.aid;
|
|
105
|
-
if (!jid) {
|
|
106
|
-
throw new Error('await-process-event-error');
|
|
107
|
-
}
|
|
108
|
-
this.logger.debug('await-resolve-await', { jid, aid, status, code });
|
|
109
|
-
this.status = status;
|
|
110
|
-
this.code = code;
|
|
111
|
-
let telemetry: TelemetryService;
|
|
112
|
-
try {
|
|
113
|
-
await this.getState();
|
|
114
|
-
const aState = await CollatorService.notarizeReentry(this);
|
|
115
|
-
this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
|
|
116
|
-
|
|
117
|
-
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
118
|
-
telemetry.startActivitySpan(this.leg);
|
|
119
|
-
|
|
120
|
-
let multiResponse: MultiResponseFlags = [];
|
|
121
|
-
if (status === StreamStatus.SUCCESS) {
|
|
122
|
-
this.bindActivityData('output');
|
|
123
|
-
this.adjacencyList = await this.filterAdjacent();
|
|
124
|
-
multiResponse = await this.processSuccessResponse(this.adjacencyList);
|
|
125
|
-
} else {
|
|
126
|
-
this.bindActivityError(this.data);
|
|
127
|
-
this.adjacencyList = await this.filterAdjacent();
|
|
128
|
-
multiResponse = await this.processErrorResponse(this.adjacencyList);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
telemetry.mapActivityAttributes();
|
|
132
|
-
const jobStatus = this.resolveStatus(multiResponse);
|
|
133
|
-
const attrs: StringScalarType = { 'app.job.jss': jobStatus };
|
|
134
|
-
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
135
|
-
if (messageIds.length) {
|
|
136
|
-
attrs['app.activity.mids'] = messageIds.join(',')
|
|
137
|
-
}
|
|
138
|
-
telemetry.setActivityAttributes(attrs);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
this.logger.error('await-resolve-await-error', error);
|
|
141
|
-
telemetry.setActivityError(error.message);
|
|
142
|
-
throw error;
|
|
143
|
-
} finally {
|
|
144
|
-
telemetry.endActivitySpan();
|
|
145
|
-
this.logger.debug('await-resolve-await-end', { jid, aid, status, code });
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async processSuccessResponse(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
|
|
150
|
-
this.mapJobData();
|
|
151
|
-
const multi = this.store.getMulti();
|
|
152
|
-
await this.setState(multi);
|
|
153
|
-
await CollatorService.notarizeCompletion(this, multi);
|
|
154
|
-
|
|
155
|
-
await this.setStatus(adjacencyList.length - 1, multi);
|
|
156
|
-
return await multi.exec() as MultiResponseFlags;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async processErrorResponse(adjacencyList: StreamData[]): Promise<MultiResponseFlags> {
|
|
160
|
-
//todo: if adjacencyList.length == 0, then map to the job output
|
|
161
|
-
// this method would be added to Base activity class
|
|
162
|
-
//this.mapJobData();
|
|
163
|
-
const multi = this.store.getMulti();
|
|
164
|
-
await this.setState(multi);
|
|
165
|
-
await CollatorService.notarizeCompletion(this, multi);
|
|
166
|
-
|
|
167
|
-
await this.setStatus(adjacencyList.length - 1, multi);
|
|
168
|
-
return await multi.exec() as MultiResponseFlags;
|
|
92
|
+
return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
|
|
169
93
|
}
|
|
170
94
|
}
|
|
171
95
|
|
|
@@ -61,9 +61,9 @@ class Cycle extends Activity {
|
|
|
61
61
|
return this.context.metadata.aid;
|
|
62
62
|
} catch (error) {
|
|
63
63
|
if (error instanceof GetStateError) {
|
|
64
|
-
this.logger.error('cycle-get-state-error', error);
|
|
64
|
+
this.logger.error('cycle-get-state-error', { error });
|
|
65
65
|
} else {
|
|
66
|
-
this.logger.error('cycle-process-error', error);
|
|
66
|
+
this.logger.error('cycle-process-error', { error });
|
|
67
67
|
}
|
|
68
68
|
telemetry.setActivityError(error.message);
|
|
69
69
|
throw error;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Activity } from './activity';
|
|
2
2
|
import { Await } from './await';
|
|
3
3
|
import { Cycle } from './cycle';
|
|
4
|
-
import { Emit } from './emit';
|
|
5
4
|
import { Iterate } from './iterate';
|
|
5
|
+
import { Signal } from './signal';
|
|
6
6
|
import { Trigger } from './trigger';
|
|
7
7
|
import { Worker } from './worker';
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ export default {
|
|
|
11
11
|
await: Await,
|
|
12
12
|
cycle: Cycle,
|
|
13
13
|
iterate: Iterate,
|
|
14
|
-
|
|
14
|
+
signal: Signal,
|
|
15
15
|
trigger: Trigger,
|
|
16
16
|
worker: Worker,
|
|
17
17
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { GetStateError } from '../../modules/errors';
|
|
2
|
+
import { Activity, ActivityType } from './activity';
|
|
3
|
+
import { CollatorService } from '../collator';
|
|
4
|
+
import { EngineService } from '../engine';
|
|
5
|
+
import { MapperService } from '../mapper';
|
|
6
|
+
import { Pipe } from '../pipe';
|
|
7
|
+
import { TelemetryService } from '../telemetry';
|
|
8
|
+
import {
|
|
9
|
+
ActivityData,
|
|
10
|
+
ActivityMetadata,
|
|
11
|
+
SignalActivity } from '../../types/activity';
|
|
12
|
+
import { JobState } from '../../types/job';
|
|
13
|
+
import { MultiResponseFlags, RedisMulti } from '../../types/redis';
|
|
14
|
+
import { StringScalarType } from '../../types/serializer';
|
|
15
|
+
import { JobStatsInput } from '../../types/stats';
|
|
16
|
+
|
|
17
|
+
class Signal extends Activity {
|
|
18
|
+
config: SignalActivity;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
config: ActivityType,
|
|
22
|
+
data: ActivityData,
|
|
23
|
+
metadata: ActivityMetadata,
|
|
24
|
+
hook: ActivityData | null,
|
|
25
|
+
engine: EngineService,
|
|
26
|
+
context?: JobState) {
|
|
27
|
+
super(config, data, metadata, hook, engine, context);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
//******** LEG 1 ENTRY ********//
|
|
32
|
+
async process(): Promise<string> {
|
|
33
|
+
this.logger.debug('signal-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
34
|
+
let telemetry: TelemetryService;
|
|
35
|
+
try {
|
|
36
|
+
//verify entry is allowed
|
|
37
|
+
this.setLeg(1);
|
|
38
|
+
await CollatorService.notarizeEntry(this);
|
|
39
|
+
await this.getState();
|
|
40
|
+
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
41
|
+
telemetry.startActivitySpan(this.leg);
|
|
42
|
+
|
|
43
|
+
//save state and notarize early completion (signals only run leg1)
|
|
44
|
+
const multi = this.store.getMulti();
|
|
45
|
+
this.adjacencyList = await this.filterAdjacent();
|
|
46
|
+
this.mapOutputData();
|
|
47
|
+
this.mapJobData();
|
|
48
|
+
await this.setState(multi);
|
|
49
|
+
await CollatorService.notarizeEarlyCompletion(this, multi);
|
|
50
|
+
await this.setStatus(this.adjacencyList.length - 1, multi);
|
|
51
|
+
const multiResponse = await multi.exec() as MultiResponseFlags;
|
|
52
|
+
|
|
53
|
+
//signal to awaken all paused jobs that share the targeted job key
|
|
54
|
+
await this.hookAll();
|
|
55
|
+
|
|
56
|
+
//transition to adjacent activities
|
|
57
|
+
const jobStatus = this.resolveStatus(multiResponse);
|
|
58
|
+
const attrs: StringScalarType = { 'app.job.jss': jobStatus };
|
|
59
|
+
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
60
|
+
if (messageIds.length) {
|
|
61
|
+
attrs['app.activity.mids'] = messageIds.join(',')
|
|
62
|
+
}
|
|
63
|
+
telemetry.mapActivityAttributes();
|
|
64
|
+
telemetry.setActivityAttributes(attrs);
|
|
65
|
+
|
|
66
|
+
return this.context.metadata.aid;
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (error instanceof GetStateError) {
|
|
69
|
+
this.logger.error('signal-get-state-error', { error });
|
|
70
|
+
} else {
|
|
71
|
+
this.logger.error('signal-process-error', { error });
|
|
72
|
+
}
|
|
73
|
+
telemetry.setActivityError(error.message);
|
|
74
|
+
throw error;
|
|
75
|
+
} finally {
|
|
76
|
+
telemetry.endActivitySpan();
|
|
77
|
+
this.logger.debug('signal-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
mapSignalData(): Record<string, any> {
|
|
82
|
+
if(this.config.signal?.maps) {
|
|
83
|
+
const mapper = new MapperService(this.config.signal.maps, this.context);
|
|
84
|
+
return mapper.mapRules();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
mapResolverData(): Record<string, any> {
|
|
89
|
+
if(this.config.resolver?.maps) {
|
|
90
|
+
const mapper = new MapperService(this.config.resolver.maps, this.context);
|
|
91
|
+
return mapper.mapRules();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The signal activity will hook all paused jobs that share the same job key.
|
|
97
|
+
*/
|
|
98
|
+
async hookAll(): Promise<string[]> {
|
|
99
|
+
//prep 1) generate `input signal data` (essentially the webhook payload)
|
|
100
|
+
const signalInputData = this.mapSignalData();
|
|
101
|
+
|
|
102
|
+
//prep 2) generate data that resolves the job key (per the YAML config)
|
|
103
|
+
const keyResolverData = this.mapResolverData() as JobStatsInput;
|
|
104
|
+
if (this.config.scrub) {
|
|
105
|
+
//self-clean the indexes upon use if configured
|
|
106
|
+
keyResolverData.scrub = true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//prep 3) jobKeys can contain multiple indexes (per the YAML config)
|
|
110
|
+
const key_name = Pipe.resolve(this.config.key_name, this.context);
|
|
111
|
+
const key_value = Pipe.resolve(this.config.key_value, this.context);
|
|
112
|
+
const indexQueryFacets = [`${key_name}:${key_value}`];
|
|
113
|
+
|
|
114
|
+
//execute: `hookAll` will now resume all paused jobs that share the same job key
|
|
115
|
+
return await this.engine.hookAll(
|
|
116
|
+
this.config.topic,
|
|
117
|
+
signalInputData,
|
|
118
|
+
keyResolverData,
|
|
119
|
+
indexQueryFacets
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { Signal };
|
|
@@ -61,9 +61,9 @@ class Trigger extends Activity {
|
|
|
61
61
|
return this.context.metadata.jid;
|
|
62
62
|
} catch (error) {
|
|
63
63
|
if (error instanceof DuplicateJobError) {
|
|
64
|
-
this.logger.error('duplicate-job-error', error);
|
|
64
|
+
this.logger.error('duplicate-job-error', { error });
|
|
65
65
|
} else {
|
|
66
|
-
this.logger.error('trigger-process-error', error);
|
|
66
|
+
this.logger.error('trigger-process-error', { error });
|
|
67
67
|
}
|
|
68
68
|
telemetry.setActivityError(error.message);
|
|
69
69
|
throw error;
|
|
@@ -140,6 +140,7 @@ class Trigger extends Activity {
|
|
|
140
140
|
},
|
|
141
141
|
};
|
|
142
142
|
this.context['$self'] = this.context[this.metadata.aid];
|
|
143
|
+
this.context['$job'] = this.context; //NEVER call STRINGIFY! (circular)
|
|
143
144
|
}
|
|
144
145
|
|
|
145
146
|
bindJobMetadataPaths(): string[] {
|
|
@@ -151,7 +152,7 @@ class Trigger extends Activity {
|
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
resolveGranularity(): string {
|
|
154
|
-
return ReporterService.DEFAULT_GRANULARITY;
|
|
155
|
+
return this.config.stats?.granularity || ReporterService.DEFAULT_GRANULARITY;
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
getJobStatus(): number {
|
|
@@ -8,13 +8,10 @@ import {
|
|
|
8
8
|
ActivityType,
|
|
9
9
|
WorkerActivity } from '../../types/activity';
|
|
10
10
|
import { JobState } from '../../types/job';
|
|
11
|
-
import { MultiResponseFlags } from '../../types/redis';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
StreamCode,
|
|
15
|
-
StreamData,
|
|
16
|
-
StreamStatus } from '../../types/stream';
|
|
11
|
+
import { MultiResponseFlags, RedisMulti } from '../../types/redis';
|
|
12
|
+
import { StreamData} from '../../types/stream';
|
|
17
13
|
import { TelemetryService } from '../telemetry';
|
|
14
|
+
import { Pipe } from '../pipe';
|
|
18
15
|
|
|
19
16
|
class Worker extends Activity {
|
|
20
17
|
config: WorkerActivity;
|
|
@@ -34,24 +31,26 @@ class Worker extends Activity {
|
|
|
34
31
|
this.logger.debug('worker-process', { jid: this.context.metadata.jid, aid: this.metadata.aid });
|
|
35
32
|
let telemetry: TelemetryService;
|
|
36
33
|
try {
|
|
34
|
+
//confirm entry is allowed and restore state
|
|
37
35
|
this.setLeg(1);
|
|
38
36
|
await CollatorService.notarizeEntry(this);
|
|
39
|
-
|
|
40
37
|
await this.getState();
|
|
41
38
|
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
42
39
|
telemetry.startActivitySpan(this.leg);
|
|
43
40
|
this.mapInputData();
|
|
44
41
|
|
|
42
|
+
//save state and authorize reentry
|
|
45
43
|
const multi = this.store.getMulti();
|
|
46
44
|
//todo: await this.registerTimeout();
|
|
45
|
+
const messageId = await this.execActivity(multi);
|
|
47
46
|
await CollatorService.authorizeReentry(this, multi);
|
|
48
47
|
await this.setState(multi);
|
|
49
48
|
await this.setStatus(0, multi);
|
|
50
49
|
const multiResponse = await multi.exec() as MultiResponseFlags;
|
|
51
50
|
|
|
51
|
+
//telemetry
|
|
52
52
|
telemetry.mapActivityAttributes();
|
|
53
53
|
const jobStatus = this.resolveStatus(multiResponse);
|
|
54
|
-
const messageId = await this.execActivity();
|
|
55
54
|
telemetry.setActivityAttributes({
|
|
56
55
|
'app.activity.mid': messageId,
|
|
57
56
|
'app.job.jss': jobStatus
|
|
@@ -60,9 +59,9 @@ class Worker extends Activity {
|
|
|
60
59
|
return this.context.metadata.aid;
|
|
61
60
|
} catch (error) {
|
|
62
61
|
if (error instanceof GetStateError) {
|
|
63
|
-
this.logger.error('worker-get-state-error', error);
|
|
62
|
+
this.logger.error('worker-get-state-error', { error });
|
|
64
63
|
} else {
|
|
65
|
-
this.logger.error('worker-process-error', error);
|
|
64
|
+
this.logger.error('worker-process-error', { error });
|
|
66
65
|
}
|
|
67
66
|
telemetry.setActivityError(error.message);
|
|
68
67
|
throw error;
|
|
@@ -72,13 +71,14 @@ class Worker extends Activity {
|
|
|
72
71
|
}
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
async execActivity(): Promise<string> {
|
|
74
|
+
async execActivity(multi: RedisMulti): Promise<string> {
|
|
75
|
+
const topic = Pipe.resolve(this.config.subtype, this.context);
|
|
76
76
|
const streamData: StreamData = {
|
|
77
77
|
metadata: {
|
|
78
78
|
jid: this.context.metadata.jid,
|
|
79
79
|
dad: this.metadata.dad,
|
|
80
80
|
aid: this.metadata.aid,
|
|
81
|
-
topic
|
|
81
|
+
topic,
|
|
82
82
|
spn: this.context['$self'].output.metadata.l1s,
|
|
83
83
|
trc: this.context.metadata.trc,
|
|
84
84
|
},
|
|
@@ -89,7 +89,7 @@ class Worker extends Activity {
|
|
|
89
89
|
retry: this.config.retry
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
|
-
return (await this.engine.streamSignaler?.publishMessage(
|
|
92
|
+
return (await this.engine.streamSignaler?.publishMessage(topic, streamData, multi)) as string;
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -8,6 +8,7 @@ import { HookRule } from '../../types/hook';
|
|
|
8
8
|
import { HotMeshGraph, HotMeshManifest } from '../../types/hotmesh';
|
|
9
9
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
10
10
|
import { StringAnyType, Symbols } from '../../types/serializer';
|
|
11
|
+
import { Pipe } from '../pipe';
|
|
11
12
|
|
|
12
13
|
const DEFAULT_METADATA_RANGE_SIZE = 26; //metadata is 26 slots ([a-z] * 1)
|
|
13
14
|
const DEFAULT_DATA_RANGE_SIZE = 260; //data is 260 slots ([a-zA-Z] * 5)
|
|
@@ -425,7 +426,8 @@ class Deployer {
|
|
|
425
426
|
const activities = graph.activities;
|
|
426
427
|
for (const activityKey in activities) {
|
|
427
428
|
const activity = activities[activityKey];
|
|
428
|
-
if
|
|
429
|
+
//only precreate if the topic is concrete and not `mappable`
|
|
430
|
+
if (activity.type === 'worker' && Pipe.resolve(activity.subtype, {}) === activity.subtype) {
|
|
429
431
|
params.topic = activity.subtype;
|
|
430
432
|
const key = this.store.mintKey(KeyType.STREAMS, params);
|
|
431
433
|
//create one worker group per unique activity subtype (the topic)
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { nanoid } from 'nanoid';
|
|
2
|
+
import { APP_ID, APP_VERSION, DEFAULT_COEFFICIENT, SUBSCRIBES_TOPIC, getWorkflowYAML } from './factory';
|
|
1
3
|
import { WorkflowHandleService } from './handle';
|
|
2
4
|
import { HotMeshService as HotMesh } from '../hotmesh';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
+
import {
|
|
6
|
+
ClientConfig,
|
|
7
|
+
Connection,
|
|
8
|
+
WorkflowOptions } from '../../types/durable';
|
|
5
9
|
import { JobState } from '../../types/job';
|
|
10
|
+
import { KeyType } from '../../modules/key';
|
|
6
11
|
|
|
7
12
|
/*
|
|
8
13
|
Here is an example of how the methods in this file are used:
|
|
@@ -55,13 +60,14 @@ export class ClientService {
|
|
|
55
60
|
this.connection = config.connection;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
|
|
63
|
+
getHotMeshClient = async (worflowTopic: string) => {
|
|
64
|
+
//NOTE: every unique topic inits a new engine
|
|
59
65
|
if (ClientService.instances.has(worflowTopic)) {
|
|
60
66
|
return await ClientService.instances.get(worflowTopic);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
const
|
|
64
|
-
appId:
|
|
69
|
+
const hotMeshClient = HotMesh.init({
|
|
70
|
+
appId: APP_ID,
|
|
65
71
|
engine: {
|
|
66
72
|
redis: {
|
|
67
73
|
class: this.connection.class,
|
|
@@ -69,9 +75,19 @@ export class ClientService {
|
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
77
|
});
|
|
72
|
-
ClientService.instances.set(worflowTopic,
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
ClientService.instances.set(worflowTopic, hotMeshClient);
|
|
79
|
+
|
|
80
|
+
//since the YAML topic is dynamic, it MUST be manually created before use
|
|
81
|
+
const store = (await hotMeshClient).engine.store;
|
|
82
|
+
const params = { appId: APP_ID, topic: worflowTopic };
|
|
83
|
+
const streamKey = store.mintKey(KeyType.STREAMS, params);
|
|
84
|
+
try {
|
|
85
|
+
await store.xgroup('CREATE', streamKey, 'WORKER', '$', 'MKSTREAM');
|
|
86
|
+
} catch (err) {
|
|
87
|
+
//ignore if already exists
|
|
88
|
+
}
|
|
89
|
+
await this.activateWorkflow(await hotMeshClient);
|
|
90
|
+
return hotMeshClient;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
workflow = {
|
|
@@ -80,36 +96,45 @@ export class ClientService {
|
|
|
80
96
|
const workflowName = options.workflowName;
|
|
81
97
|
const trc = options.workflowTrace;
|
|
82
98
|
const spn = options.workflowSpan;
|
|
99
|
+
//topic is concat of taskQueue and workflowName
|
|
83
100
|
const workflowTopic = `${taskQueueName}-${workflowName}`;
|
|
84
|
-
const
|
|
101
|
+
const hotMeshClient = await this.getHotMeshClient(workflowTopic);
|
|
85
102
|
const payload = {
|
|
86
103
|
arguments: [...options.args],
|
|
87
|
-
workflowId: options.workflowId,
|
|
104
|
+
workflowId: options.workflowId || nanoid(),
|
|
105
|
+
workflowTopic: workflowTopic,
|
|
106
|
+
backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
|
|
88
107
|
}
|
|
89
108
|
const context = { metadata: { trc, spn }, data: {}};
|
|
90
|
-
const jobId = await
|
|
91
|
-
|
|
109
|
+
const jobId = await hotMeshClient.pub(
|
|
110
|
+
SUBSCRIBES_TOPIC,
|
|
111
|
+
payload,
|
|
112
|
+
context as JobState);
|
|
113
|
+
return new WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
|
|
92
114
|
},
|
|
93
|
-
};
|
|
94
115
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
116
|
+
signal: async (signalId: string, data: Record<any, any>): Promise<string> => {
|
|
117
|
+
return await (await this.getHotMeshClient('durable.wfs.signal')).hook('durable.wfs.signal', { id: signalId, data });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async activateWorkflow(hotMesh: HotMesh, appId = APP_ID, version = APP_VERSION): Promise<void> {
|
|
122
|
+
const app = await hotMesh.engine.store.getApp(appId);
|
|
98
123
|
const appVersion = app?.version as unknown as number;
|
|
99
124
|
if(isNaN(appVersion)) {
|
|
100
125
|
try {
|
|
101
|
-
await hotMesh.deploy(getWorkflowYAML(
|
|
126
|
+
await hotMesh.deploy(getWorkflowYAML(appId, version));
|
|
102
127
|
await hotMesh.activate(version);
|
|
103
|
-
} catch (
|
|
104
|
-
hotMesh.engine.logger.error('durable-client-deploy-activate-err',
|
|
105
|
-
throw
|
|
128
|
+
} catch (error) {
|
|
129
|
+
hotMesh.engine.logger.error('durable-client-deploy-activate-err', { error });
|
|
130
|
+
throw error;
|
|
106
131
|
}
|
|
107
132
|
} else if(app && !app.active) {
|
|
108
133
|
try {
|
|
109
134
|
await hotMesh.activate(version);
|
|
110
|
-
} catch (
|
|
111
|
-
hotMesh.engine.logger.error('durable-client-activate-err',
|
|
112
|
-
throw
|
|
135
|
+
} catch (error) {
|
|
136
|
+
hotMesh.engine.logger.error('durable-client-activate-err', { error});
|
|
137
|
+
throw error;
|
|
113
138
|
}
|
|
114
139
|
}
|
|
115
140
|
}
|