@hotmeshio/hotmesh 0.0.42 → 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/modules/enums.d.ts +2 -0
- package/build/modules/enums.js +4 -1
- package/build/modules/utils.js +1 -1
- package/build/package.json +1 -1
- package/build/services/activities/trigger.js +7 -1
- package/build/services/durable/client.d.ts +2 -1
- package/build/services/durable/client.js +17 -3
- 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/meshos.js +3 -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.d.ts +5 -2
- package/build/services/quorum/index.js +33 -15
- package/build/services/router/index.d.ts +3 -0
- package/build/services/router/index.js +3 -0
- package/build/services/store/clients/redis.js +1 -0
- package/build/services/store/index.d.ts +7 -3
- package/build/services/store/index.js +62 -12
- 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/hotmesh.d.ts +1 -1
- 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/modules/enums.ts +4 -0
- package/modules/utils.ts +1 -1
- package/package.json +1 -1
- package/services/activities/trigger.ts +14 -0
- package/services/durable/client.ts +19 -4
- package/services/durable/exporter.ts +408 -0
- package/services/durable/factory.ts +6 -63
- package/services/durable/handle.ts +12 -0
- package/services/durable/meshos.ts +3 -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 +37 -13
- package/services/router/index.ts +3 -0
- package/services/store/clients/redis.ts +1 -0
- package/services/store/index.ts +66 -14
- 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/hotmesh.ts +1 -1
- package/types/index.ts +13 -1
- package/types/quorum.ts +1 -0
- package/types/task.ts +1 -1
package/services/engine/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { Signal } from '../activities/signal';
|
|
|
21
21
|
import { Worker } from '../activities/worker';
|
|
22
22
|
import { Trigger } from '../activities/trigger';
|
|
23
23
|
import { CompilerService } from '../compiler';
|
|
24
|
+
import { ExporterService } from '../exporter';
|
|
24
25
|
import { ILogger } from '../logger';
|
|
25
26
|
import { ReporterService } from '../reporter';
|
|
26
27
|
import { Router } from '../router';
|
|
@@ -78,12 +79,14 @@ import {
|
|
|
78
79
|
StreamRole,
|
|
79
80
|
StreamStatus } from '../../types/stream';
|
|
80
81
|
import { WorkListTaskType } from '../../types/task';
|
|
82
|
+
import { JobExport } from '../../types/exporter';
|
|
81
83
|
|
|
82
84
|
class EngineService {
|
|
83
85
|
namespace: string;
|
|
84
86
|
apps: HotMeshApps | null;
|
|
85
87
|
appId: string;
|
|
86
88
|
guid: string;
|
|
89
|
+
exporter: ExporterService | null;
|
|
87
90
|
router: Router | null;
|
|
88
91
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
89
92
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
@@ -109,8 +112,8 @@ class EngineService {
|
|
|
109
112
|
await instance.initStoreChannel(config.engine.store);
|
|
110
113
|
await instance.initSubChannel(config.engine.sub);
|
|
111
114
|
await instance.initStreamChannel(config.engine.stream);
|
|
112
|
-
instance.router = instance.initRouter(config);
|
|
113
115
|
|
|
116
|
+
instance.router = instance.initRouter(config);
|
|
114
117
|
instance.router.consumeMessages(
|
|
115
118
|
instance.stream.mintKey(
|
|
116
119
|
KeyType.STREAMS,
|
|
@@ -121,9 +124,15 @@ class EngineService {
|
|
|
121
124
|
instance.processStreamMessage.bind(instance)
|
|
122
125
|
);
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
+
instance.taskService = new TaskService(
|
|
128
|
+
instance.store,
|
|
129
|
+
logger
|
|
130
|
+
);
|
|
131
|
+
instance.exporter = new ExporterService(
|
|
132
|
+
instance.appId,
|
|
133
|
+
instance.store,
|
|
134
|
+
logger,
|
|
135
|
+
);
|
|
127
136
|
return instance;
|
|
128
137
|
}
|
|
129
138
|
}
|
|
@@ -690,9 +699,15 @@ class EngineService {
|
|
|
690
699
|
|
|
691
700
|
|
|
692
701
|
// ****** GET JOB STATE/COLLATION STATUS BY ID *********
|
|
702
|
+
async export(jobId: string): Promise<JobExport> {
|
|
703
|
+
return await this.exporter.export(jobId);
|
|
704
|
+
}
|
|
705
|
+
async getRaw(jobId: string): Promise<StringStringType> {
|
|
706
|
+
return await this.store.getRaw(jobId);
|
|
707
|
+
}
|
|
693
708
|
async getStatus(jobId: string): Promise<JobStatus> {
|
|
694
709
|
const { id: appId } = await this.getVID();
|
|
695
|
-
return this.store.getStatus(jobId, appId);
|
|
710
|
+
return await this.store.getStatus(jobId, appId);
|
|
696
711
|
}
|
|
697
712
|
//todo: add 'options' parameter;
|
|
698
713
|
// (e.g, if {dimensions:true}, use hscan to deliver
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ILogger } from '../logger';
|
|
2
|
+
import { StoreService } from '../store';
|
|
3
|
+
import {
|
|
4
|
+
StringAnyType,
|
|
5
|
+
StringStringType,
|
|
6
|
+
Symbols } from "../../types/serializer";
|
|
7
|
+
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
8
|
+
import {
|
|
9
|
+
DependencyExport,
|
|
10
|
+
ExportOptions,
|
|
11
|
+
JobActionExport,
|
|
12
|
+
JobExport } from '../../types/exporter';
|
|
13
|
+
import { SerializerService } from '../serializer';
|
|
14
|
+
import { restoreHierarchy } from '../../modules/utils';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Downloads job data from Redis (hscan, hmget, hgetall)
|
|
18
|
+
* Expands process data and includes dependency list
|
|
19
|
+
*/
|
|
20
|
+
class ExporterService {
|
|
21
|
+
appId: string;
|
|
22
|
+
logger: ILogger;
|
|
23
|
+
serializer: SerializerService
|
|
24
|
+
store: StoreService<RedisClient, RedisMulti>;
|
|
25
|
+
symbols: Promise<Symbols> | Symbols;
|
|
26
|
+
|
|
27
|
+
constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
|
|
28
|
+
this.appId = appId;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
this.store = store;
|
|
31
|
+
this.serializer = new SerializerService();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert the job hash and dependency list into a JobExport object.
|
|
36
|
+
* This object contains various facets that describe the interaction
|
|
37
|
+
* in terms relevant to narrative storytelling.
|
|
38
|
+
*/
|
|
39
|
+
async export(jobId: string, options: ExportOptions = {}): Promise<JobExport> {
|
|
40
|
+
if (!this.symbols) {
|
|
41
|
+
this.symbols = this.store.getAllSymbols();
|
|
42
|
+
this.symbols = await this.symbols;
|
|
43
|
+
}
|
|
44
|
+
const depData = await this.store.getDependencies(jobId);
|
|
45
|
+
const jobData = await this.store.getRaw(jobId);
|
|
46
|
+
const jobExport = this.inflate(jobData, depData);
|
|
47
|
+
return jobExport;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Inflates the key from Redis, 3-character symbol
|
|
52
|
+
* into a human-readable JSON path, reflecting the
|
|
53
|
+
* tree-like structure of the unidimensional Hash
|
|
54
|
+
*/
|
|
55
|
+
inflateKey(key: string): string {
|
|
56
|
+
return (key in this.symbols) ? this.symbols[key] : key;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Inflates the job data from Redis into a JobExport object
|
|
61
|
+
* @param jobHash - the job data from Redis
|
|
62
|
+
* @param dependencyList - the list of dependencies for the job
|
|
63
|
+
* @returns - the inflated job data
|
|
64
|
+
*/
|
|
65
|
+
inflate(jobHash: StringStringType, dependencyList: string[]): JobExport {
|
|
66
|
+
//the list of actions taken in the workflow and hook functions
|
|
67
|
+
const actions: JobActionExport = {
|
|
68
|
+
hooks: {},
|
|
69
|
+
main: {
|
|
70
|
+
cursor: -1,
|
|
71
|
+
items: []
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const process: StringAnyType = {};
|
|
75
|
+
const dependencies = this.inflateDependencyData(dependencyList, actions);
|
|
76
|
+
const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
|
|
77
|
+
|
|
78
|
+
Object.entries(jobHash).forEach(([key, value]) => {
|
|
79
|
+
const match = key.match(regex);
|
|
80
|
+
if (match) {
|
|
81
|
+
//activity process state
|
|
82
|
+
const [_, letters, numbers] = match;
|
|
83
|
+
const path = this.inflateKey(letters);
|
|
84
|
+
const dimensions = `${numbers.replace(/,/g, '/')}`;
|
|
85
|
+
const resolved = this.serializer.fromString(value);
|
|
86
|
+
process[`${dimensions}/${path}`] = resolved;
|
|
87
|
+
} else if (key.length === 3) {
|
|
88
|
+
//job state
|
|
89
|
+
process[this.inflateKey(key)] = this.serializer.fromString(value);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
dependencies,
|
|
95
|
+
process: restoreHierarchy(process),
|
|
96
|
+
status: jobHash[':'],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Inflates the dependency data from Redis into a JobExport object by
|
|
102
|
+
* organizing the dimensional isolate in sch a way asto interleave
|
|
103
|
+
* into a story
|
|
104
|
+
* @param data - the dependency data from Redis
|
|
105
|
+
* @returns - the organized dependency data
|
|
106
|
+
*/
|
|
107
|
+
inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
|
|
108
|
+
const hookReg = /([0-9,]+)-(\d+)$/;
|
|
109
|
+
const flowReg = /-(\d+)$/;
|
|
110
|
+
return data.map((dependency, index: number): DependencyExport => {
|
|
111
|
+
const [action, topic, gid, ...jid] = dependency.split('::');
|
|
112
|
+
const jobId = jid.join('::');
|
|
113
|
+
const match = jobId.match(hookReg);
|
|
114
|
+
let prefix: string;
|
|
115
|
+
let type: 'hook' | 'flow' | 'other';
|
|
116
|
+
let dimensionKey: string = '';
|
|
117
|
+
|
|
118
|
+
if (match) {
|
|
119
|
+
//hook-originating dependency
|
|
120
|
+
const [_, dimension, counter] = match;
|
|
121
|
+
dimensionKey = dimension.split(',').join('/');
|
|
122
|
+
prefix = `${dimensionKey}[${counter}]`;
|
|
123
|
+
type = 'hook';
|
|
124
|
+
} else {
|
|
125
|
+
const match = jobId.match(flowReg);
|
|
126
|
+
if (match) {
|
|
127
|
+
//main workflow-originating dependency
|
|
128
|
+
const [_, counter] = match;
|
|
129
|
+
prefix = `[${counter}]`;
|
|
130
|
+
type = 'flow';
|
|
131
|
+
} else {
|
|
132
|
+
//'other' types like signal cleanup
|
|
133
|
+
prefix = '/';
|
|
134
|
+
type = 'other';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
type: action,
|
|
139
|
+
topic,
|
|
140
|
+
gid,
|
|
141
|
+
jid: jobId,
|
|
142
|
+
} as unknown as DependencyExport;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { ExporterService };
|
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
StatsResponse } from '../../types/stats';
|
|
25
25
|
import { ConnectorService } from '../connector';
|
|
26
26
|
import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
|
|
27
|
-
import { StringAnyType } from '../../types/serializer';
|
|
27
|
+
import { StringAnyType, StringStringType } from '../../types/serializer';
|
|
28
|
+
import { JobExport } from '../../types/exporter';
|
|
28
29
|
|
|
29
30
|
class HotMeshService {
|
|
30
31
|
namespace: string;
|
|
@@ -152,6 +153,12 @@ class HotMeshService {
|
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
// ************* REPORTER METHODS *************
|
|
156
|
+
async export(jobId: string): Promise<JobExport> {
|
|
157
|
+
return await this.engine?.export(jobId);
|
|
158
|
+
}
|
|
159
|
+
async getRaw(jobId: string): Promise<StringStringType> {
|
|
160
|
+
return await this.engine?.getRaw(jobId);
|
|
161
|
+
}
|
|
155
162
|
async getStats(topic: string, query: JobStatsInput): Promise<StatsResponse> {
|
|
156
163
|
return await this.engine?.getStats(topic, query);
|
|
157
164
|
}
|
package/services/quorum/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { HMSH_ACTIVATION_MAX_RETRY, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
|
|
2
2
|
import { identifyRedisType, sleepFor } from '../../modules/utils';
|
|
3
3
|
import { CompilerService } from '../compiler';
|
|
4
4
|
import { EngineService } from '../engine';
|
|
@@ -10,6 +10,7 @@ import { SubService } from '../sub';
|
|
|
10
10
|
import { IORedisSubService as IORedisSub } from '../sub/clients/ioredis';
|
|
11
11
|
import { RedisSubService as RedisSub } from '../sub/clients/redis';
|
|
12
12
|
import { CacheMode } from '../../types/cache';
|
|
13
|
+
import { HotMeshConfig, KeyType } from '../../types/hotmesh';
|
|
13
14
|
import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
|
|
14
15
|
import {
|
|
15
16
|
QuorumMessage,
|
|
@@ -18,13 +19,9 @@ import {
|
|
|
18
19
|
SubscriptionCallback,
|
|
19
20
|
ThrottleMessage
|
|
20
21
|
} from '../../types/quorum';
|
|
21
|
-
import { HotMeshApps, HotMeshConfig } from '../../types/hotmesh';
|
|
22
22
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
23
23
|
import { RedisClientType } from '../../types/redisclient';
|
|
24
24
|
|
|
25
|
-
//wait time to see if quorum is reached
|
|
26
|
-
const QUORUM_DELAY = 250;
|
|
27
|
-
|
|
28
25
|
class QuorumService {
|
|
29
26
|
namespace: string;
|
|
30
27
|
appId: string;
|
|
@@ -59,8 +56,18 @@ class QuorumService {
|
|
|
59
56
|
//note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
|
|
60
57
|
await instance.initStoreChannel(config.engine.store);
|
|
61
58
|
await instance.initSubChannel(config.engine.sub);
|
|
62
|
-
|
|
63
|
-
await instance.subscribe.subscribe(
|
|
59
|
+
//general quorum subscription
|
|
60
|
+
await instance.subscribe.subscribe(
|
|
61
|
+
KeyType.QUORUM,
|
|
62
|
+
instance.subscriptionHandler(),
|
|
63
|
+
appId
|
|
64
|
+
);
|
|
65
|
+
//app-specific quorum subscription (used for pubsub one-time request/response)
|
|
66
|
+
await instance.subscribe.subscribe(
|
|
67
|
+
KeyType.QUORUM,
|
|
68
|
+
instance.subscriptionHandler(),
|
|
69
|
+
appId, instance.guid
|
|
70
|
+
);
|
|
64
71
|
|
|
65
72
|
instance.engine.processWebHooks();
|
|
66
73
|
instance.engine.processTimeHooks();
|
|
@@ -138,7 +145,8 @@ class QuorumService {
|
|
|
138
145
|
engine_id: this.guid,
|
|
139
146
|
namespace: this.namespace,
|
|
140
147
|
app_id: this.appId,
|
|
141
|
-
stream: this.engine.stream.mintKey(KeyType.STREAMS, { appId: this.appId })
|
|
148
|
+
stream: this.engine.stream.mintKey(KeyType.STREAMS, { appId: this.appId }),
|
|
149
|
+
counts: this.engine.router.counts,
|
|
142
150
|
};
|
|
143
151
|
}
|
|
144
152
|
this.store.publish(
|
|
@@ -152,7 +160,7 @@ class QuorumService {
|
|
|
152
160
|
);
|
|
153
161
|
}
|
|
154
162
|
|
|
155
|
-
async requestQuorum(delay =
|
|
163
|
+
async requestQuorum(delay = HMSH_QUORUM_DELAY_MS, details = false): Promise<number> {
|
|
156
164
|
const quorum = this.quorum;
|
|
157
165
|
this.quorum = 0;
|
|
158
166
|
this.profiles.length = 0;
|
|
@@ -188,7 +196,7 @@ class QuorumService {
|
|
|
188
196
|
|
|
189
197
|
|
|
190
198
|
// ************* COMPILER METHODS *************
|
|
191
|
-
async rollCall(delay =
|
|
199
|
+
async rollCall(delay = HMSH_QUORUM_DELAY_MS): Promise<QuorumProfile[]> {
|
|
192
200
|
await this.requestQuorum(delay, true);
|
|
193
201
|
const targetStreams = [];
|
|
194
202
|
const multi = this.store.getMulti();
|
|
@@ -209,10 +217,20 @@ class QuorumService {
|
|
|
209
217
|
});
|
|
210
218
|
return this.profiles;
|
|
211
219
|
}
|
|
212
|
-
|
|
220
|
+
/**
|
|
221
|
+
* request a quorum; if successful activate the app version
|
|
222
|
+
*/
|
|
223
|
+
async activate(version: string, delay = HMSH_QUORUM_DELAY_MS, count = 0): Promise<boolean> {
|
|
213
224
|
version = version.toString();
|
|
225
|
+
const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
|
|
226
|
+
if (!canActivate) {
|
|
227
|
+
//another engine is already activating the app version
|
|
228
|
+
this.logger.debug('quorum-activation-awaiting', { version });
|
|
229
|
+
await sleepFor(delay * 6);
|
|
230
|
+
const app = await this.store.getApp(this.appId, true);
|
|
231
|
+
return app?.active == true && app?.version === version;
|
|
232
|
+
}
|
|
214
233
|
const config = await this.engine.getVID();
|
|
215
|
-
//request a quorum to activate the version
|
|
216
234
|
await this.requestQuorum(delay);
|
|
217
235
|
const q1 = await this.requestQuorum(delay);
|
|
218
236
|
const q2 = await this.requestQuorum(delay);
|
|
@@ -225,6 +243,7 @@ class QuorumService {
|
|
|
225
243
|
this.appId
|
|
226
244
|
);
|
|
227
245
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
246
|
+
await this.store.releaseScoutRole('activate');
|
|
228
247
|
//confirm we received the activation message
|
|
229
248
|
if (this.engine.untilVersion === version) {
|
|
230
249
|
this.logger.info('quorum-activation-succeeded', { version });
|
|
@@ -236,7 +255,12 @@ class QuorumService {
|
|
|
236
255
|
throw new Error(`UntilVersion Not Received. Version ${version} not activated`);
|
|
237
256
|
}
|
|
238
257
|
} else {
|
|
239
|
-
this.logger.
|
|
258
|
+
this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
|
|
259
|
+
this.store.releaseScoutRole('activate');
|
|
260
|
+
if (count < HMSH_ACTIVATION_MAX_RETRY) {
|
|
261
|
+
//increase the delay (give the quorum time to respond) and try again
|
|
262
|
+
return await this.activate(version, delay * 2, count + 1);
|
|
263
|
+
}
|
|
240
264
|
throw new Error(`Quorum not reached. Version ${version} not activated.`);
|
|
241
265
|
}
|
|
242
266
|
}
|
package/services/router/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ class Router {
|
|
|
40
40
|
logger: ILogger;
|
|
41
41
|
throttle = 0;
|
|
42
42
|
errorCount = 0;
|
|
43
|
+
counts: { [key: string]: number } = {};
|
|
43
44
|
currentTimerId: NodeJS.Timeout | null = null;
|
|
44
45
|
shouldConsume: boolean;
|
|
45
46
|
|
|
@@ -183,6 +184,8 @@ class Router {
|
|
|
183
184
|
} else {
|
|
184
185
|
output.metadata.guid = guid();
|
|
185
186
|
}
|
|
187
|
+
const code = output.code || 200;
|
|
188
|
+
this.counts[code] = (this.counts[code] || 0) + 1;
|
|
186
189
|
output.type = StreamDataType.RESPONSE;
|
|
187
190
|
return await this.publishMessage(null, output as StreamDataResponse) as string;
|
|
188
191
|
}
|
package/services/store/index.ts
CHANGED
|
@@ -45,6 +45,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
45
45
|
appId: string
|
|
46
46
|
logger: ILogger;
|
|
47
47
|
commands: Record<string, string> = {
|
|
48
|
+
set: 'set',
|
|
48
49
|
setnx: 'setnx',
|
|
49
50
|
del: 'del',
|
|
50
51
|
expire: 'expire',
|
|
@@ -178,14 +179,16 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
178
179
|
* check for and process work items in the
|
|
179
180
|
* time and signal task queues.
|
|
180
181
|
*/
|
|
181
|
-
async reserveScoutRole(scoutType: 'time' | 'signal', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
|
|
182
|
+
async reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
|
|
182
183
|
const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
183
|
-
const success = await this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
const success = await this.exec('SET', key, `${scoutType}:${formatISODate(new Date())}`, 'NX', 'EX', `${delay - 1}`);
|
|
185
|
+
return this.isSuccessful(success);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean> {
|
|
189
|
+
const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
190
|
+
const success = await this.exec('DEL', key);
|
|
191
|
+
return this.isSuccessful(success);
|
|
189
192
|
}
|
|
190
193
|
|
|
191
194
|
async getSettings(bCreate = false): Promise<HotMeshSettings> {
|
|
@@ -238,6 +241,36 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
async getAllSymbols(): Promise<Symbols> {
|
|
245
|
+
//get hash with all reserved symbol ranges
|
|
246
|
+
const rangeKey = this.mintKey(KeyType.SYMKEYS, { appId: this.appId });
|
|
247
|
+
const ranges = await this.redisClient[this.commands.hgetall](rangeKey);
|
|
248
|
+
const rangeKeys = Object.keys(ranges).sort();
|
|
249
|
+
delete rangeKeys[':cursor'];
|
|
250
|
+
const multi = this.getMulti();
|
|
251
|
+
for (const rangeKey of rangeKeys) {
|
|
252
|
+
const symbolKey = this.mintKey(KeyType.SYMKEYS, { activityId: rangeKey, appId: this.appId });
|
|
253
|
+
multi[this.commands.hgetall](symbolKey);
|
|
254
|
+
}
|
|
255
|
+
const results = await multi.exec() as Array<[null, Symbols]> | Array<Symbols>;
|
|
256
|
+
|
|
257
|
+
const symbolSets: Symbols = {};
|
|
258
|
+
results.forEach((result: [null, Symbols] | Symbols, index: number) => {
|
|
259
|
+
if (result) {
|
|
260
|
+
let vals: Symbols;
|
|
261
|
+
if (Array.isArray(result) && result.length === 2) {
|
|
262
|
+
vals = result[1];
|
|
263
|
+
} else {
|
|
264
|
+
vals = result as Symbols;
|
|
265
|
+
}
|
|
266
|
+
for (const [key, value] of Object.entries(vals)) {
|
|
267
|
+
symbolSets[value as string] = key.startsWith(rangeKeys[index]) ? key : `${rangeKeys[index]}/${key}`;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
return symbolSets;
|
|
272
|
+
}
|
|
273
|
+
|
|
241
274
|
async getSymbols(activityId: string): Promise<Symbols> {
|
|
242
275
|
let symbols: Symbols = this.cache.getSymbols(this.appId, activityId);
|
|
243
276
|
if (symbols) {
|
|
@@ -393,7 +426,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
393
426
|
* when `originJobId` is interrupted/expired, the items in the
|
|
394
427
|
* list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
|
|
395
428
|
*/
|
|
396
|
-
async registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
429
|
+
async registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
397
430
|
const privateMulti = multi || this.getMulti();
|
|
398
431
|
const dependencyParams = {
|
|
399
432
|
appId: this.appId,
|
|
@@ -403,8 +436,8 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
403
436
|
KeyType.JOB_DEPENDENTS,
|
|
404
437
|
dependencyParams,
|
|
405
438
|
);
|
|
406
|
-
//
|
|
407
|
-
const expireTask =
|
|
439
|
+
//items listed as job dependencies have different relationships
|
|
440
|
+
const expireTask = `${depType}::${topic}::${gId}::${jobId}`;
|
|
408
441
|
privateMulti[this.commands.rpush](depKey, expireTask);
|
|
409
442
|
if (!multi) {
|
|
410
443
|
return await privateMulti.exec();
|
|
@@ -586,6 +619,15 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
586
619
|
}
|
|
587
620
|
}
|
|
588
621
|
|
|
622
|
+
async getRaw(jobId: string): Promise<StringStringType> {
|
|
623
|
+
const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
624
|
+
const job = await this.redisClient[this.commands.hgetall](jobKey);
|
|
625
|
+
if (!job) {
|
|
626
|
+
throw new GetStateError(jobId);
|
|
627
|
+
}
|
|
628
|
+
return job;
|
|
629
|
+
}
|
|
630
|
+
|
|
589
631
|
/**
|
|
590
632
|
* collate is a generic method for incrementing a value in a hash
|
|
591
633
|
* in order to track their progress during processing.
|
|
@@ -836,6 +878,12 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
836
878
|
await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
|
|
837
879
|
}
|
|
838
880
|
|
|
881
|
+
async getDependencies(jobId: string): Promise<string[]> {
|
|
882
|
+
const depParams = { appId: this.appId, jobId };
|
|
883
|
+
const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
|
|
884
|
+
return this.redisClient[this.commands.lrange](depKey, 0, -1);
|
|
885
|
+
}
|
|
886
|
+
|
|
839
887
|
/**
|
|
840
888
|
* registers a hook activity to be awakened (uses ZSET to
|
|
841
889
|
* store the 'sleep group' and LIST to store the events
|
|
@@ -859,11 +907,15 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
859
907
|
let [pType, pKey] = this.resolveTaskKeyContext(listKey);
|
|
860
908
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
861
909
|
if (timeEvent) {
|
|
862
|
-
//there are
|
|
863
|
-
//1) sleep (awaken), 2) expire, 3) interrupt, 4) delist
|
|
864
|
-
|
|
910
|
+
//there are task types
|
|
911
|
+
//1) sleep (awaken), 2) expire (OR expire-child), 3) interrupt, 4) delist, 5) child (just an index helper; no work to do)
|
|
912
|
+
let [type, activityId, gId, ...jobId] = timeEvent.split('::');
|
|
865
913
|
if (type === 'delist') {
|
|
866
914
|
pType = 'delist';
|
|
915
|
+
} else if (type === 'child') {
|
|
916
|
+
pType = 'child';
|
|
917
|
+
} else if (type === 'expire-child') {
|
|
918
|
+
type = 'expire'; //use the same logic as 'expire'
|
|
867
919
|
}
|
|
868
920
|
return [listKey, jobId.join('::'), gId, activityId, pType];
|
|
869
921
|
}
|
|
@@ -881,7 +933,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
881
933
|
* generic LIST (lists typically contain target job ids)
|
|
882
934
|
* @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
|
|
883
935
|
*/
|
|
884
|
-
resolveTaskKeyContext(listKey: string): [
|
|
936
|
+
resolveTaskKeyContext(listKey: string): [WorkListTaskType, string] {
|
|
885
937
|
if (listKey.startsWith('::INTERRUPT')) {
|
|
886
938
|
return ['interrupt', listKey.split('::')[2]];
|
|
887
939
|
} else if (listKey.startsWith('::EXPIRE')) {
|
package/services/task/index.ts
CHANGED
|
@@ -117,7 +117,10 @@ class TaskService {
|
|
|
117
117
|
|
|
118
118
|
if (Array.isArray(workListTask)) {
|
|
119
119
|
const [listKey, target, gId, activityId, type] = workListTask;
|
|
120
|
-
if (type === '
|
|
120
|
+
if (type === 'child') {
|
|
121
|
+
//continue; this child is listed here for convenience, but
|
|
122
|
+
// will be expired by an origin ancestor and is listed there
|
|
123
|
+
} else if (type === 'delist') {
|
|
121
124
|
//delist the signalKey (target)
|
|
122
125
|
const key = this.store.mintKey(KeyType.SIGNALS, { appId: this.store.appId });
|
|
123
126
|
await this.store.redisClient[this.store.commands.hdel](key, target);
|
package/services/worker/index.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { KeyType } from "../../modules/key";
|
|
2
|
+
import { identifyRedisType } from "../../modules/utils";
|
|
3
|
+
import { ConnectorService } from "../connector";
|
|
2
4
|
import { ILogger } from "../logger";
|
|
3
5
|
import { Router } from "../router";
|
|
4
6
|
import { StoreService } from '../store';
|
|
5
|
-
import { RedisStoreService as RedisStore } from '../store/clients/redis';
|
|
6
7
|
import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
|
|
8
|
+
import { RedisStoreService as RedisStore } from '../store/clients/redis';
|
|
7
9
|
import { StreamService } from '../stream';
|
|
8
|
-
import { RedisStreamService as RedisStream } from '../stream/clients/redis';
|
|
9
10
|
import { IORedisStreamService as IORedisStream } from '../stream/clients/ioredis';
|
|
11
|
+
import { RedisStreamService as RedisStream } from '../stream/clients/redis';
|
|
10
12
|
import { SubService } from '../sub';
|
|
11
13
|
import { IORedisSubService as IORedisSub } from '../sub/clients/ioredis';
|
|
12
14
|
import { RedisSubService as RedisSub } from '../sub/clients/redis';
|
|
13
|
-
import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
|
|
14
15
|
import { HotMeshConfig, HotMeshWorker } from "../../types/hotmesh";
|
|
16
|
+
import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
|
|
15
17
|
import {
|
|
16
18
|
QuorumMessage,
|
|
17
19
|
QuorumProfile,
|
|
@@ -19,8 +21,6 @@ import {
|
|
|
19
21
|
import { RedisClient, RedisMulti } from "../../types/redis";
|
|
20
22
|
import { RedisClientType } from '../../types/redisclient';
|
|
21
23
|
import { StreamRole } from "../../types/stream";
|
|
22
|
-
import { identifyRedisType } from "../../modules/utils";
|
|
23
|
-
import { ConnectorService } from "../connector";
|
|
24
24
|
|
|
25
25
|
class WorkerService {
|
|
26
26
|
namespace: string;
|
|
@@ -174,6 +174,7 @@ class WorkerService {
|
|
|
174
174
|
app_id: this.appId,
|
|
175
175
|
worker_topic: this.topic,
|
|
176
176
|
stream: this.stream.mintKey(KeyType.STREAMS, params),
|
|
177
|
+
counts: this.router.counts,
|
|
177
178
|
};
|
|
178
179
|
}
|
|
179
180
|
this.store.publish(
|
package/types/activity.ts
CHANGED
|
@@ -38,13 +38,18 @@ interface Measure {
|
|
|
38
38
|
|
|
39
39
|
interface TriggerActivityStats {
|
|
40
40
|
/**
|
|
41
|
-
* parent job; including this allows the parent's
|
|
41
|
+
* dependent parent job id; including this allows the parent's
|
|
42
42
|
* expiration/interruption events to cascade; set
|
|
43
43
|
* `expire` in the YAML for the dependent graph
|
|
44
44
|
* to 0 and provide the parent for dependent,
|
|
45
45
|
* cascading interruption and cleanup
|
|
46
46
|
*/
|
|
47
47
|
parent?: string;
|
|
48
|
+
/**
|
|
49
|
+
* adjacent parent job id; this is the actual adjacent
|
|
50
|
+
* parent in the graph, but it is not used for cascading expiration
|
|
51
|
+
*/
|
|
52
|
+
adjacent?: string;
|
|
48
53
|
id?: { [key: string]: unknown } | string;
|
|
49
54
|
key?: { [key: string]: unknown } | string;
|
|
50
55
|
/**
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { StringAnyType, StringStringType } from "./serializer";
|
|
2
|
+
|
|
3
|
+
export type ExportItem = [(string | null), string, any];
|
|
4
|
+
|
|
5
|
+
export interface ExportOptions {};
|
|
6
|
+
|
|
7
|
+
export type JobAction = {
|
|
8
|
+
cursor: number;
|
|
9
|
+
items: ExportItem[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface JobActionExport {
|
|
13
|
+
hooks: {
|
|
14
|
+
[key: string]: JobAction;
|
|
15
|
+
};
|
|
16
|
+
main: JobAction;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface ActivityAction {
|
|
20
|
+
action: string;
|
|
21
|
+
target: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface JobTimeline {
|
|
25
|
+
activity: string; //activity name
|
|
26
|
+
dimension: string; //dimensional isolate path
|
|
27
|
+
duplex: 'entry' | 'exit'; //activity entry or exit
|
|
28
|
+
timestamp: string; //actually a number but too many digits for JS
|
|
29
|
+
actions?: ActivityAction[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DependencyExport {
|
|
33
|
+
type: string;
|
|
34
|
+
topic: string;
|
|
35
|
+
gid: string;
|
|
36
|
+
jid: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ExportTransitions {
|
|
40
|
+
[key: string]: string[];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface ExportCycles {
|
|
44
|
+
[key: string]: string[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface DurableJobExport {
|
|
48
|
+
data: StringAnyType;
|
|
49
|
+
dependencies: DependencyExport[];
|
|
50
|
+
state: StringAnyType;
|
|
51
|
+
status: string;
|
|
52
|
+
timeline: JobTimeline[];
|
|
53
|
+
transitions: ExportTransitions;
|
|
54
|
+
cycles: ExportCycles;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export interface JobExport {
|
|
58
|
+
dependencies: DependencyExport[];
|
|
59
|
+
process: StringAnyType;
|
|
60
|
+
status: string;
|
|
61
|
+
};
|
package/types/hotmesh.ts
CHANGED
|
@@ -44,7 +44,7 @@ type KeyStoreParams = {
|
|
|
44
44
|
facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
|
|
45
45
|
topic?: string; //topic name (e.g., "foo" or "" for top-level)
|
|
46
46
|
timeValue?: number; //time value (rounded to minute) (for delete range)
|
|
47
|
-
scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
|
|
47
|
+
scoutType?: 'signal' | 'time' | 'activate'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
type HotMesh = typeof HotMeshService;
|