@hotmeshio/hotmesh 0.0.41 → 0.0.43
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/errors.d.ts +1 -8
- package/build/modules/errors.js +1 -12
- package/build/modules/utils.js +1 -1
- package/build/package.json +1 -1
- package/build/services/activities/activity.d.ts +8 -1
- package/build/services/activities/activity.js +17 -12
- package/build/services/collator/index.d.ts +20 -2
- package/build/services/collator/index.js +41 -7
- package/build/services/durable/client.d.ts +2 -1
- package/build/services/durable/client.js +17 -3
- package/build/services/durable/factory.d.ts +0 -1
- package/build/services/durable/factory.js +0 -138
- package/build/services/durable/meshos.js +3 -0
- package/build/services/durable/worker.js +0 -15
- package/build/services/durable/workflow.d.ts +0 -9
- package/build/services/durable/workflow.js +0 -29
- package/build/services/engine/index.d.ts +1 -1
- package/build/services/engine/index.js +5 -8
- package/build/services/quorum/index.d.ts +5 -2
- package/build/services/quorum/index.js +32 -15
- package/build/services/store/clients/redis.js +1 -0
- package/build/services/store/index.d.ts +13 -1
- package/build/services/store/index.js +22 -6
- package/build/types/hotmesh.d.ts +1 -1
- package/build/types/job.d.ts +1 -0
- package/modules/enums.ts +4 -0
- package/modules/errors.ts +0 -15
- package/modules/utils.ts +1 -1
- package/package.json +1 -1
- package/services/activities/activity.ts +30 -15
- package/services/collator/index.ts +41 -8
- package/services/durable/client.ts +19 -4
- package/services/durable/factory.ts +0 -138
- package/services/durable/meshos.ts +3 -0
- package/services/durable/worker.ts +0 -16
- package/services/durable/workflow.ts +0 -32
- package/services/engine/index.ts +5 -6
- package/services/quorum/index.ts +35 -12
- package/services/store/clients/redis.ts +1 -0
- package/services/store/index.ts +25 -7
- package/types/hotmesh.ts +1 -1
- package/types/job.ts +1 -0
|
@@ -226,6 +226,7 @@ class EngineService {
|
|
|
226
226
|
});
|
|
227
227
|
const context = {
|
|
228
228
|
metadata: {
|
|
229
|
+
guid: streamData.metadata.guid,
|
|
229
230
|
jid: streamData.metadata.jid,
|
|
230
231
|
gid: streamData.metadata.gid,
|
|
231
232
|
dad: streamData.metadata.dad,
|
|
@@ -353,15 +354,11 @@ class EngineService {
|
|
|
353
354
|
};
|
|
354
355
|
return await this.router.publishMessage(null, streamData);
|
|
355
356
|
}
|
|
356
|
-
async hookTime(jobId, gId,
|
|
357
|
-
if (type === 'interrupt') {
|
|
358
|
-
return await this.interrupt(
|
|
359
|
-
jobId, { suppress: true, expire: 1 });
|
|
357
|
+
async hookTime(jobId, gId, topicOrActivity, type) {
|
|
358
|
+
if (type === 'interrupt' || type === 'expire') {
|
|
359
|
+
return await this.interrupt(topicOrActivity, jobId, { suppress: true, expire: 1 });
|
|
360
360
|
}
|
|
361
|
-
|
|
362
|
-
return await this.store.expireJob(jobId, 1);
|
|
363
|
-
}
|
|
364
|
-
const [aid, ...dimensions] = activityId.split(',');
|
|
361
|
+
const [aid, ...dimensions] = topicOrActivity.split(',');
|
|
365
362
|
const dad = `,${dimensions.join(',')}`;
|
|
366
363
|
const streamData = {
|
|
367
364
|
type: stream_1.StreamDataType.TIMEHOOK,
|
|
@@ -3,8 +3,8 @@ import { ILogger } from '../logger';
|
|
|
3
3
|
import { StoreService } from '../store';
|
|
4
4
|
import { SubService } from '../sub';
|
|
5
5
|
import { CacheMode } from '../../types/cache';
|
|
6
|
-
import { QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
|
|
7
6
|
import { HotMeshConfig } from '../../types/hotmesh';
|
|
7
|
+
import { QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
|
|
8
8
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
9
9
|
declare class QuorumService {
|
|
10
10
|
namespace: string;
|
|
@@ -30,6 +30,9 @@ declare class QuorumService {
|
|
|
30
30
|
sub(callback: QuorumMessageCallback): Promise<void>;
|
|
31
31
|
unsub(callback: QuorumMessageCallback): Promise<void>;
|
|
32
32
|
rollCall(delay?: number): Promise<QuorumProfile[]>;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* request a quorum; if successful activate the app version
|
|
35
|
+
*/
|
|
36
|
+
activate(version: string, delay?: number, count?: number): Promise<boolean>;
|
|
34
37
|
}
|
|
35
38
|
export { QuorumService };
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QuorumService = void 0;
|
|
4
|
-
const
|
|
4
|
+
const enums_1 = require("../../modules/enums");
|
|
5
5
|
const utils_1 = require("../../modules/utils");
|
|
6
6
|
const compiler_1 = require("../compiler");
|
|
7
7
|
const redis_1 = require("../store/clients/redis");
|
|
8
8
|
const ioredis_1 = require("../store/clients/ioredis");
|
|
9
9
|
const ioredis_2 = require("../sub/clients/ioredis");
|
|
10
10
|
const redis_2 = require("../sub/clients/redis");
|
|
11
|
-
|
|
12
|
-
const QUORUM_DELAY = 250;
|
|
11
|
+
const hotmesh_1 = require("../../types/hotmesh");
|
|
13
12
|
class QuorumService {
|
|
14
13
|
constructor() {
|
|
15
14
|
this.profiles = [];
|
|
@@ -30,8 +29,10 @@ class QuorumService {
|
|
|
30
29
|
//note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
|
|
31
30
|
await instance.initStoreChannel(config.engine.store);
|
|
32
31
|
await instance.initSubChannel(config.engine.sub);
|
|
33
|
-
|
|
34
|
-
await instance.subscribe.subscribe(
|
|
32
|
+
//general quorum subscription
|
|
33
|
+
await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId);
|
|
34
|
+
//app-specific quorum subscription (used for pubsub one-time request/response)
|
|
35
|
+
await instance.subscribe.subscribe(hotmesh_1.KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid);
|
|
35
36
|
instance.engine.processWebHooks();
|
|
36
37
|
instance.engine.processTimeHooks();
|
|
37
38
|
return instance;
|
|
@@ -102,20 +103,20 @@ class QuorumService {
|
|
|
102
103
|
engine_id: this.guid,
|
|
103
104
|
namespace: this.namespace,
|
|
104
105
|
app_id: this.appId,
|
|
105
|
-
stream: this.engine.stream.mintKey(
|
|
106
|
+
stream: this.engine.stream.mintKey(hotmesh_1.KeyType.STREAMS, { appId: this.appId })
|
|
106
107
|
};
|
|
107
108
|
}
|
|
108
|
-
this.store.publish(
|
|
109
|
+
this.store.publish(hotmesh_1.KeyType.QUORUM, {
|
|
109
110
|
type: 'pong',
|
|
110
111
|
guid, originator,
|
|
111
112
|
profile,
|
|
112
113
|
}, appId);
|
|
113
114
|
}
|
|
114
|
-
async requestQuorum(delay =
|
|
115
|
+
async requestQuorum(delay = enums_1.HMSH_QUORUM_DELAY_MS, details = false) {
|
|
115
116
|
const quorum = this.quorum;
|
|
116
117
|
this.quorum = 0;
|
|
117
118
|
this.profiles.length = 0;
|
|
118
|
-
await this.store.publish(
|
|
119
|
+
await this.store.publish(hotmesh_1.KeyType.QUORUM, {
|
|
119
120
|
type: 'ping',
|
|
120
121
|
originator: this.guid,
|
|
121
122
|
details,
|
|
@@ -126,7 +127,7 @@ class QuorumService {
|
|
|
126
127
|
// ************* PUB/SUB METHODS *************
|
|
127
128
|
//publish a message to the quorum
|
|
128
129
|
async pub(quorumMessage) {
|
|
129
|
-
return await this.store.publish(
|
|
130
|
+
return await this.store.publish(hotmesh_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
|
|
130
131
|
}
|
|
131
132
|
//subscribe user to quorum messages
|
|
132
133
|
async sub(callback) {
|
|
@@ -139,7 +140,7 @@ class QuorumService {
|
|
|
139
140
|
this.callbacks = this.callbacks.filter(cb => cb !== callback);
|
|
140
141
|
}
|
|
141
142
|
// ************* COMPILER METHODS *************
|
|
142
|
-
async rollCall(delay =
|
|
143
|
+
async rollCall(delay = enums_1.HMSH_QUORUM_DELAY_MS) {
|
|
143
144
|
await this.requestQuorum(delay, true);
|
|
144
145
|
const targetStreams = [];
|
|
145
146
|
const multi = this.store.getMulti();
|
|
@@ -160,18 +161,29 @@ class QuorumService {
|
|
|
160
161
|
});
|
|
161
162
|
return this.profiles;
|
|
162
163
|
}
|
|
163
|
-
|
|
164
|
+
/**
|
|
165
|
+
* request a quorum; if successful activate the app version
|
|
166
|
+
*/
|
|
167
|
+
async activate(version, delay = enums_1.HMSH_QUORUM_DELAY_MS, count = 0) {
|
|
164
168
|
version = version.toString();
|
|
169
|
+
const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
|
|
170
|
+
if (!canActivate) {
|
|
171
|
+
//another engine is already activating the app version
|
|
172
|
+
this.logger.debug('quorum-activation-awaiting', { version });
|
|
173
|
+
await (0, utils_1.sleepFor)(delay * 6);
|
|
174
|
+
const app = await this.store.getApp(this.appId, true);
|
|
175
|
+
return app?.active == true && app?.version === version;
|
|
176
|
+
}
|
|
165
177
|
const config = await this.engine.getVID();
|
|
166
|
-
//request a quorum to activate the version
|
|
167
178
|
await this.requestQuorum(delay);
|
|
168
179
|
const q1 = await this.requestQuorum(delay);
|
|
169
180
|
const q2 = await this.requestQuorum(delay);
|
|
170
181
|
const q3 = await this.requestQuorum(delay);
|
|
171
182
|
if (q1 && q1 === q2 && q2 === q3) {
|
|
172
183
|
this.logger.info('quorum-rollcall-succeeded', { q1, q2, q3 });
|
|
173
|
-
this.store.publish(
|
|
184
|
+
this.store.publish(hotmesh_1.KeyType.QUORUM, { type: 'activate', cache_mode: 'nocache', until_version: version }, this.appId);
|
|
174
185
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
186
|
+
await this.store.releaseScoutRole('activate');
|
|
175
187
|
//confirm we received the activation message
|
|
176
188
|
if (this.engine.untilVersion === version) {
|
|
177
189
|
this.logger.info('quorum-activation-succeeded', { version });
|
|
@@ -185,7 +197,12 @@ class QuorumService {
|
|
|
185
197
|
}
|
|
186
198
|
}
|
|
187
199
|
else {
|
|
188
|
-
this.logger.
|
|
200
|
+
this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
|
|
201
|
+
this.store.releaseScoutRole('activate');
|
|
202
|
+
if (count < enums_1.HMSH_ACTIVATION_MAX_RETRY) {
|
|
203
|
+
//increase the delay (give the quorum time to respond) and try again
|
|
204
|
+
return await this.activate(version, delay * 2, count + 1);
|
|
205
|
+
}
|
|
189
206
|
throw new Error(`Quorum not reached. Version ${version} not activated.`);
|
|
190
207
|
}
|
|
191
208
|
}
|
|
@@ -46,7 +46,8 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
46
46
|
* check for and process work items in the
|
|
47
47
|
* time and signal task queues.
|
|
48
48
|
*/
|
|
49
|
-
reserveScoutRole(scoutType: 'time' | 'signal', delay?: number): Promise<boolean>;
|
|
49
|
+
reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay?: number): Promise<boolean>;
|
|
50
|
+
releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean>;
|
|
50
51
|
getSettings(bCreate?: boolean): Promise<HotMeshSettings>;
|
|
51
52
|
setSettings(manifest: HotMeshSettings): Promise<any>;
|
|
52
53
|
reserveSymbolRange(target: string, size: number, type: 'JOB' | 'ACTIVITY'): Promise<[number, number, Symbols]>;
|
|
@@ -86,7 +87,18 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
86
87
|
*/
|
|
87
88
|
getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
|
|
88
89
|
getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
|
|
90
|
+
/**
|
|
91
|
+
* collate is a generic method for incrementing a value in a hash
|
|
92
|
+
* in order to track their progress during processing.
|
|
93
|
+
*/
|
|
89
94
|
collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi?: U): Promise<number>;
|
|
95
|
+
/**
|
|
96
|
+
* synthentic collation affects those activities in the graph
|
|
97
|
+
* that represent the synthetic DAG that was materialized during compilation;
|
|
98
|
+
* Synthetic targeting ensures that re-entry due to failure can be distinguished from
|
|
99
|
+
* purposeful re-entry.
|
|
100
|
+
*/
|
|
101
|
+
collateSynthetic(jobId: string, guid: string, amount: number, multi?: U): Promise<number>;
|
|
90
102
|
setStateNX(jobId: string, appId: string): Promise<boolean>;
|
|
91
103
|
getSchema(activityId: string, appVersion: AppVID): Promise<ActivityType>;
|
|
92
104
|
getSchemas(appVersion: AppVID): Promise<Record<string, ActivityType>>;
|
|
@@ -33,6 +33,7 @@ const errors_1 = require("../../modules/errors");
|
|
|
33
33
|
class StoreService {
|
|
34
34
|
constructor(redisClient) {
|
|
35
35
|
this.commands = {
|
|
36
|
+
set: 'set',
|
|
36
37
|
setnx: 'setnx',
|
|
37
38
|
del: 'del',
|
|
38
39
|
expire: 'expire',
|
|
@@ -106,12 +107,13 @@ class StoreService {
|
|
|
106
107
|
*/
|
|
107
108
|
async reserveScoutRole(scoutType, delay = enums_1.HMSH_SCOUT_INTERVAL_SECONDS) {
|
|
108
109
|
const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
109
|
-
const success = await this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
110
|
+
const success = await this.exec('SET', key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`, 'NX', 'EX', `${delay - 1}`);
|
|
111
|
+
return this.isSuccessful(success);
|
|
112
|
+
}
|
|
113
|
+
async releaseScoutRole(scoutType) {
|
|
114
|
+
const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
115
|
+
const success = await this.exec('DEL', key);
|
|
116
|
+
return this.isSuccessful(success);
|
|
115
117
|
}
|
|
116
118
|
async getSettings(bCreate = false) {
|
|
117
119
|
let settings = this.cache?.getSettings();
|
|
@@ -485,6 +487,10 @@ class StoreService {
|
|
|
485
487
|
throw new errors_1.GetStateError(jobId);
|
|
486
488
|
}
|
|
487
489
|
}
|
|
490
|
+
/**
|
|
491
|
+
* collate is a generic method for incrementing a value in a hash
|
|
492
|
+
* in order to track their progress during processing.
|
|
493
|
+
*/
|
|
488
494
|
async collate(jobId, activityId, amount, dIds, multi) {
|
|
489
495
|
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
490
496
|
const collationKey = `${activityId}/output/metadata/as`; //activity state
|
|
@@ -497,6 +503,16 @@ class StoreService {
|
|
|
497
503
|
const targetId = Object.keys(hashData)[0];
|
|
498
504
|
return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, targetId, amount);
|
|
499
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* synthentic collation affects those activities in the graph
|
|
508
|
+
* that represent the synthetic DAG that was materialized during compilation;
|
|
509
|
+
* Synthetic targeting ensures that re-entry due to failure can be distinguished from
|
|
510
|
+
* purposeful re-entry.
|
|
511
|
+
*/
|
|
512
|
+
async collateSynthetic(jobId, guid, amount, multi) {
|
|
513
|
+
const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
|
|
514
|
+
return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, guid, amount);
|
|
515
|
+
}
|
|
500
516
|
async setStateNX(jobId, appId) {
|
|
501
517
|
const hashKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId, jobId });
|
|
502
518
|
const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', '1');
|
package/build/types/hotmesh.d.ts
CHANGED
package/build/types/job.d.ts
CHANGED
package/modules/enums.ts
CHANGED
|
@@ -22,6 +22,10 @@ export const HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
|
22
22
|
|
|
23
23
|
export const HMSH_STATUS_UNKNOWN = 'unknown';
|
|
24
24
|
|
|
25
|
+
// QUORUM
|
|
26
|
+
export const HMSH_QUORUM_DELAY_MS = 250;
|
|
27
|
+
export const HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
28
|
+
|
|
25
29
|
// ENGINE
|
|
26
30
|
export const HMSH_OTT_WAIT_TIME = parseInt(process.env.HMSH_OTT_WAIT_TIME, 10) || 1000;
|
|
27
31
|
export const HMSH_EXPIRE_JOB_SECONDS = parseInt(process.env.HMSH_EXPIRE_JOB_SECONDS, 10) || 1;
|
package/modules/errors.ts
CHANGED
|
@@ -45,20 +45,6 @@ class DurableWaitForSignalError extends Error {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/* @deprecated */
|
|
49
|
-
class DurableSleepError extends Error {
|
|
50
|
-
code: number;
|
|
51
|
-
duration: number; //seconds
|
|
52
|
-
index: number; //execution order in the workflow
|
|
53
|
-
dimension: string; //hook dimension (e.g., ',0,1,0') (uses empty string for `null`)
|
|
54
|
-
constructor(message: string, duration: number, index: number, dimension: string) {
|
|
55
|
-
super(message);
|
|
56
|
-
this.duration = duration;
|
|
57
|
-
this.index = index;
|
|
58
|
-
this.dimension = dimension;
|
|
59
|
-
this.code = 595;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
48
|
class DurableSleepForError extends Error {
|
|
63
49
|
code: number;
|
|
64
50
|
duration: number; //seconds
|
|
@@ -175,7 +161,6 @@ export {
|
|
|
175
161
|
DurableIncompleteSignalError,
|
|
176
162
|
DurableMaxedError,
|
|
177
163
|
DurableRetryError,
|
|
178
|
-
DurableSleepError,
|
|
179
164
|
DurableSleepForError,
|
|
180
165
|
DurableTimeoutError,
|
|
181
166
|
DurableWaitForSignalError,
|
package/modules/utils.ts
CHANGED
package/package.json
CHANGED
|
@@ -90,6 +90,22 @@ class Activity {
|
|
|
90
90
|
await CollatorService.notarizeEntry(this);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Upon entering leg 2 of a duplexed activty, verify
|
|
95
|
+
* all aspects of the re-entry including job and activty state
|
|
96
|
+
*/
|
|
97
|
+
async verifyReentry(): Promise<number> {
|
|
98
|
+
const guid = this.context.metadata.guid;
|
|
99
|
+
this.setLeg(2);
|
|
100
|
+
await this.getState();
|
|
101
|
+
CollatorService.assertJobActive(
|
|
102
|
+
this.context.metadata.js,
|
|
103
|
+
this.context.metadata.jid,
|
|
104
|
+
this.metadata.aid
|
|
105
|
+
);
|
|
106
|
+
return await CollatorService.notarizeReentry(this, guid);
|
|
107
|
+
}
|
|
108
|
+
|
|
93
109
|
//******** DUPLEX RE-ENTRY POINT ********//
|
|
94
110
|
async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output'): Promise<void> {
|
|
95
111
|
this.setLeg(2);
|
|
@@ -103,23 +119,20 @@ class Activity {
|
|
|
103
119
|
this.code = code;
|
|
104
120
|
this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
|
|
105
121
|
let telemetry: TelemetryService;
|
|
106
|
-
try {
|
|
107
|
-
await this.getState();
|
|
108
|
-
CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
|
|
109
|
-
const aState = await CollatorService.notarizeReentry(this);
|
|
110
|
-
this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
|
|
111
|
-
|
|
112
|
-
telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
113
|
-
let isComplete = CollatorService.isActivityComplete(this.context.metadata.js);
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.logger.debug('activity-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
123
|
+
try {
|
|
124
|
+
const collationKey = await this.verifyReentry();
|
|
120
125
|
|
|
126
|
+
this.adjacentIndex = CollatorService.getDimensionalIndex(collationKey);
|
|
127
|
+
telemetry = new TelemetryService(
|
|
128
|
+
this.engine.appId,
|
|
129
|
+
this.config,
|
|
130
|
+
this.metadata,
|
|
131
|
+
this.context,
|
|
132
|
+
);
|
|
121
133
|
telemetry.startActivitySpan(this.leg);
|
|
122
134
|
let multiResponse: MultiResponseFlags;
|
|
135
|
+
|
|
123
136
|
if (status === StreamStatus.PENDING) {
|
|
124
137
|
multiResponse = await this.processPending(telemetry, type);
|
|
125
138
|
} else if (status === StreamStatus.SUCCESS) {
|
|
@@ -384,7 +397,7 @@ class Activity {
|
|
|
384
397
|
TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
|
|
385
398
|
let { dad, jid } = this.context.metadata;
|
|
386
399
|
const dIds = CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad || '');
|
|
387
|
-
//`state` is a
|
|
400
|
+
//`state` is a unidimensional hash; context is a tree
|
|
388
401
|
const [state, status] = await this.store.getState(jid, consumes, dIds);
|
|
389
402
|
this.context = restoreHierarchy(state) as JobState;
|
|
390
403
|
this.assertGenerationalId(this.context.metadata.gid, gid);
|
|
@@ -395,7 +408,9 @@ class Activity {
|
|
|
395
408
|
|
|
396
409
|
/**
|
|
397
410
|
* if the job is created/deleted/created with the same key,
|
|
398
|
-
* the 'gid' ensures no stale messages
|
|
411
|
+
* the 'gid' ensures no stale messages (such as sleep delays)
|
|
412
|
+
* enter the workstream. Any message with a mismatched gid
|
|
413
|
+
* belongs to a prior job and can safely be ignored/dropped.
|
|
399
414
|
*/
|
|
400
415
|
assertGenerationalId(jobGID: string, msgGID?: string) {
|
|
401
416
|
if (msgGID !== jobGID) {
|
|
@@ -78,11 +78,28 @@ class CollatorService {
|
|
|
78
78
|
return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_001 - decrement, this.getDimensionalAddress(activity), multi);
|
|
79
79
|
};
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/**
|
|
82
|
+
* verifies both the concrete and synthetic keys for the activity; concrete keys
|
|
83
|
+
* exist in the original model and are effectively the 'real' keys. In reality,
|
|
84
|
+
* hook activities are atomized during compilation to create a synthetic DAG that
|
|
85
|
+
* is used to track the status of the graph in a distributed environment. The
|
|
86
|
+
* synthetic key represents different dimensional realities and is used to
|
|
87
|
+
* track re-entry overages (it distinguishes between the original and re-entry).
|
|
88
|
+
* The essential challenge is: is this a re-entry that is purposeful in
|
|
89
|
+
* order to induce cycles, or is the re-entry due to a failure in the system?
|
|
90
|
+
*/
|
|
91
|
+
static async notarizeReentry(activity: Activity, guid: string, multi?: RedisMulti): Promise<number> {
|
|
92
|
+
const jid = activity.context.metadata.jid;
|
|
93
|
+
const localMulti = multi || activity.store.getMulti();
|
|
82
94
|
//increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
await activity.store.collate(jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity, true), localMulti);
|
|
96
|
+
await activity.store.collateSynthetic(jid, guid, 1_000_000, localMulti);
|
|
97
|
+
const [_amountConcrete, _amountSynthetic] = await localMulti.exec();
|
|
98
|
+
const amountConcrete = Array.isArray(_amountConcrete) ? _amountConcrete[1] : _amountConcrete;
|
|
99
|
+
const amountSynthetic = Array.isArray(_amountSynthetic) ? _amountSynthetic[1] : _amountSynthetic;
|
|
100
|
+
this.verifyInteger(amountConcrete as number, 2, 'enter');
|
|
101
|
+
this.verifySyntheticInteger(amountSynthetic as number);
|
|
102
|
+
return amountConcrete as number;
|
|
86
103
|
};
|
|
87
104
|
|
|
88
105
|
static async notarizeContinuation(activity: Activity, multi?: RedisMulti): Promise<number> {
|
|
@@ -134,6 +151,26 @@ class CollatorService {
|
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
153
|
|
|
154
|
+
/**
|
|
155
|
+
* During compilation, the graphs are compiled into structures necessary
|
|
156
|
+
* for distributed processing; these are referred to as 'synthetic DAGs',
|
|
157
|
+
* because they are not part of the original graph, but are used to track
|
|
158
|
+
* the status of the graph in a distributed environment. This check ensures
|
|
159
|
+
* that the 'synthetic key' is not a duplicate. (which is different than
|
|
160
|
+
* saying the 'key' is not a duplicate)
|
|
161
|
+
*/
|
|
162
|
+
static verifySyntheticInteger(amount: number): void {
|
|
163
|
+
const samount = amount.toString();
|
|
164
|
+
const isCompletedValue = parseInt(samount[samount.length - 1], 10);
|
|
165
|
+
if (isCompletedValue > 0) {
|
|
166
|
+
//already done error (ack/delete clearly failed; this is a duplicate)
|
|
167
|
+
throw new CollationError(amount, 2, 'enter', CollationFaultType.INACTIVE);
|
|
168
|
+
} else if (amount >= 2_000_000) {
|
|
169
|
+
//duplicate synthetic key (todo: need to resolve/fix this!!)
|
|
170
|
+
throw new CollationError(amount, 2, 'enter', CollationFaultType.DUPLICATE);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
137
174
|
static verifyInteger(amount: number, leg: ActivityDuplex, stage: CollationStage): void {
|
|
138
175
|
let faultType: CollationFaultType | undefined;
|
|
139
176
|
if (leg === 1 && stage === 'enter') {
|
|
@@ -239,10 +276,6 @@ class CollatorService {
|
|
|
239
276
|
});
|
|
240
277
|
}
|
|
241
278
|
|
|
242
|
-
static isActivityComplete(status: number): boolean {
|
|
243
|
-
return (status - 0) <= 0;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
279
|
/**
|
|
247
280
|
* All activities exist on a dimensional plane. Zero
|
|
248
281
|
* is the default. A value of
|
|
@@ -11,13 +11,14 @@ import { JobState } from '../../types/job';
|
|
|
11
11
|
import { KeyService, KeyType } from '../../modules/key';
|
|
12
12
|
import { Search } from './search';
|
|
13
13
|
import { StreamStatus } from '../../types';
|
|
14
|
-
import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
|
|
14
|
+
import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
|
|
15
|
+
import { sleepFor } from '../../modules/utils';
|
|
15
16
|
|
|
16
17
|
export class ClientService {
|
|
17
18
|
|
|
18
19
|
connection: Connection;
|
|
19
|
-
topics: string[] = [];
|
|
20
20
|
options: WorkflowOptions;
|
|
21
|
+
static topics: string[] = [];
|
|
21
22
|
static instances = new Map<string, HotMesh | Promise<HotMesh>>();
|
|
22
23
|
|
|
23
24
|
constructor(config: ClientConfig) {
|
|
@@ -29,8 +30,9 @@ export class ClientService {
|
|
|
29
30
|
const instanceId = 'SINGLETON';
|
|
30
31
|
if (ClientService.instances.has(instanceId)) {
|
|
31
32
|
const hotMeshClient = await ClientService.instances.get(instanceId);
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
await this.verifyWorkflowActive(hotMeshClient, namespace ?? APP_ID);
|
|
34
|
+
if (!ClientService.topics.includes(workflowTopic)) {
|
|
35
|
+
ClientService.topics.push(workflowTopic);
|
|
34
36
|
await this.createStream(hotMeshClient, workflowTopic, namespace);
|
|
35
37
|
}
|
|
36
38
|
return hotMeshClient;
|
|
@@ -196,6 +198,19 @@ export class ClientService {
|
|
|
196
198
|
}
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
async verifyWorkflowActive(hotMesh: HotMesh, appId = APP_ID, count = 0): Promise<boolean> {
|
|
202
|
+
const app = await hotMesh.engine.store.getApp(appId);
|
|
203
|
+
const appVersion = app?.version as unknown as number;
|
|
204
|
+
if(isNaN(appVersion)) {
|
|
205
|
+
if (count > 10) {
|
|
206
|
+
throw new Error('Workflow failed to activate');
|
|
207
|
+
}
|
|
208
|
+
await sleepFor(HMSH_QUORUM_DELAY_MS * 2);
|
|
209
|
+
return await this.verifyWorkflowActive(hotMesh, appId, count + 1);
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
199
214
|
async activateWorkflow(hotMesh: HotMesh, appId = APP_ID, version = APP_VERSION): Promise<void> {
|
|
200
215
|
const app = await hotMesh.engine.store.getApp(appId);
|
|
201
216
|
const appVersion = app?.version as unknown as number;
|