@hotmeshio/hotmesh 0.0.42 → 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/utils.js +1 -1
- package/build/package.json +1 -1
- package/build/services/durable/client.d.ts +2 -1
- package/build/services/durable/client.js +17 -3
- package/build/services/durable/meshos.js +3 -0
- 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 +2 -1
- package/build/services/store/index.js +8 -6
- package/build/types/hotmesh.d.ts +1 -1
- package/modules/enums.ts +4 -0
- package/modules/utils.ts +1 -1
- package/package.json +1 -1
- package/services/durable/client.ts +19 -4
- package/services/durable/meshos.ts +3 -0
- package/services/quorum/index.ts +35 -12
- package/services/store/clients/redis.ts +1 -0
- package/services/store/index.ts +10 -7
- package/types/hotmesh.ts +1 -1
package/build/modules/enums.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export declare const HMSH_CODE_DURABLE_MAXED = 597;
|
|
|
15
15
|
export declare const HMSH_CODE_DURABLE_FATAL = 598;
|
|
16
16
|
export declare const HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
17
17
|
export declare const HMSH_STATUS_UNKNOWN = "unknown";
|
|
18
|
+
export declare const HMSH_QUORUM_DELAY_MS = 250;
|
|
19
|
+
export declare const HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
18
20
|
export declare const HMSH_OTT_WAIT_TIME: number;
|
|
19
21
|
export declare const HMSH_EXPIRE_JOB_SECONDS: number;
|
|
20
22
|
export declare const HMSH_MAX_RETRIES: number;
|
package/build/modules/enums.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
|
|
3
|
+
exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_LOGLEVEL = void 0;
|
|
4
4
|
// HOTMESH SYSTEM
|
|
5
5
|
exports.HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL || 'info';
|
|
6
6
|
// STATUS CODES AND MESSAGES
|
|
@@ -19,6 +19,9 @@ exports.HMSH_CODE_DURABLE_MAXED = 597;
|
|
|
19
19
|
exports.HMSH_CODE_DURABLE_FATAL = 598;
|
|
20
20
|
exports.HMSH_CODE_DURABLE_RETRYABLE = 599;
|
|
21
21
|
exports.HMSH_STATUS_UNKNOWN = 'unknown';
|
|
22
|
+
// QUORUM
|
|
23
|
+
exports.HMSH_QUORUM_DELAY_MS = 250;
|
|
24
|
+
exports.HMSH_ACTIVATION_MAX_RETRY = 3;
|
|
22
25
|
// ENGINE
|
|
23
26
|
exports.HMSH_OTT_WAIT_TIME = parseInt(process.env.HMSH_OTT_WAIT_TIME, 10) || 1000;
|
|
24
27
|
exports.HMSH_EXPIRE_JOB_SECONDS = parseInt(process.env.HMSH_EXPIRE_JOB_SECONDS, 10) || 1;
|
package/build/modules/utils.js
CHANGED
package/build/package.json
CHANGED
|
@@ -3,8 +3,8 @@ import { HotMeshService as HotMesh } from '../hotmesh';
|
|
|
3
3
|
import { ClientConfig, Connection, HookOptions, WorkflowOptions, WorkflowSearchOptions } from '../../types/durable';
|
|
4
4
|
export declare class ClientService {
|
|
5
5
|
connection: Connection;
|
|
6
|
-
topics: string[];
|
|
7
6
|
options: WorkflowOptions;
|
|
7
|
+
static topics: string[];
|
|
8
8
|
static instances: Map<string, HotMesh | Promise<HotMesh>>;
|
|
9
9
|
constructor(config: ClientConfig);
|
|
10
10
|
getHotMeshClient: (workflowTopic: string, namespace?: string) => Promise<HotMesh>;
|
|
@@ -37,6 +37,7 @@ export declare class ClientService {
|
|
|
37
37
|
getHandle: (taskQueue: string, workflowName: string, workflowId: string, namespace?: string) => Promise<WorkflowHandleService>;
|
|
38
38
|
search: (taskQueue: string, workflowName: string, namespace: null | string, index: string, ...query: string[]) => Promise<string[]>;
|
|
39
39
|
};
|
|
40
|
+
verifyWorkflowActive(hotMesh: HotMesh, appId?: string, count?: number): Promise<boolean>;
|
|
40
41
|
activateWorkflow(hotMesh: HotMesh, appId?: string, version?: string): Promise<void>;
|
|
41
42
|
static shutdown(): Promise<void>;
|
|
42
43
|
}
|
|
@@ -8,16 +8,17 @@ const key_1 = require("../../modules/key");
|
|
|
8
8
|
const search_1 = require("./search");
|
|
9
9
|
const types_1 = require("../../types");
|
|
10
10
|
const enums_1 = require("../../modules/enums");
|
|
11
|
+
const utils_1 = require("../../modules/utils");
|
|
11
12
|
class ClientService {
|
|
12
13
|
constructor(config) {
|
|
13
|
-
this.topics = [];
|
|
14
14
|
this.getHotMeshClient = async (workflowTopic, namespace) => {
|
|
15
15
|
//use the cached instance
|
|
16
16
|
const instanceId = 'SINGLETON';
|
|
17
17
|
if (ClientService.instances.has(instanceId)) {
|
|
18
18
|
const hotMeshClient = await ClientService.instances.get(instanceId);
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
await this.verifyWorkflowActive(hotMeshClient, namespace ?? factory_1.APP_ID);
|
|
20
|
+
if (!ClientService.topics.includes(workflowTopic)) {
|
|
21
|
+
ClientService.topics.push(workflowTopic);
|
|
21
22
|
await this.createStream(hotMeshClient, workflowTopic, namespace);
|
|
22
23
|
}
|
|
23
24
|
return hotMeshClient;
|
|
@@ -174,6 +175,18 @@ class ClientService {
|
|
|
174
175
|
};
|
|
175
176
|
this.connection = config.connection;
|
|
176
177
|
}
|
|
178
|
+
async verifyWorkflowActive(hotMesh, appId = factory_1.APP_ID, count = 0) {
|
|
179
|
+
const app = await hotMesh.engine.store.getApp(appId);
|
|
180
|
+
const appVersion = app?.version;
|
|
181
|
+
if (isNaN(appVersion)) {
|
|
182
|
+
if (count > 10) {
|
|
183
|
+
throw new Error('Workflow failed to activate');
|
|
184
|
+
}
|
|
185
|
+
await (0, utils_1.sleepFor)(enums_1.HMSH_QUORUM_DELAY_MS * 2);
|
|
186
|
+
return await this.verifyWorkflowActive(hotMesh, appId, count + 1);
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
177
190
|
async activateWorkflow(hotMesh, appId = factory_1.APP_ID, version = factory_1.APP_VERSION) {
|
|
178
191
|
const app = await hotMesh.engine.store.getApp(appId);
|
|
179
192
|
const appVersion = app?.version;
|
|
@@ -203,5 +216,6 @@ class ClientService {
|
|
|
203
216
|
}
|
|
204
217
|
}
|
|
205
218
|
}
|
|
219
|
+
ClientService.topics = [];
|
|
206
220
|
ClientService.instances = new Map();
|
|
207
221
|
exports.ClientService = ClientService;
|
|
@@ -194,6 +194,9 @@ class MeshOSService {
|
|
|
194
194
|
return await this.find(options.options ?? {}, ...args);
|
|
195
195
|
}
|
|
196
196
|
static generateSearchQuery(query) {
|
|
197
|
+
if (!Array.isArray(query) || query.length === 0) {
|
|
198
|
+
return '*';
|
|
199
|
+
}
|
|
197
200
|
const my = new this();
|
|
198
201
|
let queryString = query.map(q => {
|
|
199
202
|
const { field, is, value, type } = q;
|
|
@@ -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]>;
|
|
@@ -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();
|
package/build/types/hotmesh.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/utils.ts
CHANGED
package/package.json
CHANGED
|
@@ -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;
|
|
@@ -295,6 +295,9 @@ export class MeshOSService {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
static generateSearchQuery(query: FindWhereQuery[]) {
|
|
298
|
+
if (!Array.isArray(query) || query.length === 0) {
|
|
299
|
+
return '*';
|
|
300
|
+
}
|
|
298
301
|
const my = new this();
|
|
299
302
|
let queryString = query.map(q => {
|
|
300
303
|
const { field, is, value, type } = q;
|
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();
|
|
@@ -152,7 +159,7 @@ class QuorumService {
|
|
|
152
159
|
);
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
async requestQuorum(delay =
|
|
162
|
+
async requestQuorum(delay = HMSH_QUORUM_DELAY_MS, details = false): Promise<number> {
|
|
156
163
|
const quorum = this.quorum;
|
|
157
164
|
this.quorum = 0;
|
|
158
165
|
this.profiles.length = 0;
|
|
@@ -188,7 +195,7 @@ class QuorumService {
|
|
|
188
195
|
|
|
189
196
|
|
|
190
197
|
// ************* COMPILER METHODS *************
|
|
191
|
-
async rollCall(delay =
|
|
198
|
+
async rollCall(delay = HMSH_QUORUM_DELAY_MS): Promise<QuorumProfile[]> {
|
|
192
199
|
await this.requestQuorum(delay, true);
|
|
193
200
|
const targetStreams = [];
|
|
194
201
|
const multi = this.store.getMulti();
|
|
@@ -209,10 +216,20 @@ class QuorumService {
|
|
|
209
216
|
});
|
|
210
217
|
return this.profiles;
|
|
211
218
|
}
|
|
212
|
-
|
|
219
|
+
/**
|
|
220
|
+
* request a quorum; if successful activate the app version
|
|
221
|
+
*/
|
|
222
|
+
async activate(version: string, delay = HMSH_QUORUM_DELAY_MS, count = 0): Promise<boolean> {
|
|
213
223
|
version = version.toString();
|
|
224
|
+
const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
|
|
225
|
+
if (!canActivate) {
|
|
226
|
+
//another engine is already activating the app version
|
|
227
|
+
this.logger.debug('quorum-activation-awaiting', { version });
|
|
228
|
+
await sleepFor(delay * 6);
|
|
229
|
+
const app = await this.store.getApp(this.appId, true);
|
|
230
|
+
return app?.active == true && app?.version === version;
|
|
231
|
+
}
|
|
214
232
|
const config = await this.engine.getVID();
|
|
215
|
-
//request a quorum to activate the version
|
|
216
233
|
await this.requestQuorum(delay);
|
|
217
234
|
const q1 = await this.requestQuorum(delay);
|
|
218
235
|
const q2 = await this.requestQuorum(delay);
|
|
@@ -225,6 +242,7 @@ class QuorumService {
|
|
|
225
242
|
this.appId
|
|
226
243
|
);
|
|
227
244
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
245
|
+
await this.store.releaseScoutRole('activate');
|
|
228
246
|
//confirm we received the activation message
|
|
229
247
|
if (this.engine.untilVersion === version) {
|
|
230
248
|
this.logger.info('quorum-activation-succeeded', { version });
|
|
@@ -236,7 +254,12 @@ class QuorumService {
|
|
|
236
254
|
throw new Error(`UntilVersion Not Received. Version ${version} not activated`);
|
|
237
255
|
}
|
|
238
256
|
} else {
|
|
239
|
-
this.logger.
|
|
257
|
+
this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
|
|
258
|
+
this.store.releaseScoutRole('activate');
|
|
259
|
+
if (count < HMSH_ACTIVATION_MAX_RETRY) {
|
|
260
|
+
//increase the delay (give the quorum time to respond) and try again
|
|
261
|
+
return await this.activate(version, delay * 2, count + 1);
|
|
262
|
+
}
|
|
240
263
|
throw new Error(`Quorum not reached. Version ${version} not activated.`);
|
|
241
264
|
}
|
|
242
265
|
}
|
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> {
|
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;
|