@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.
@@ -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;
@@ -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;
@@ -7,7 +7,7 @@ async function sleepFor(ms) {
7
7
  }
8
8
  exports.sleepFor = sleepFor;
9
9
  function guid() {
10
- return (0, nanoid_1.nanoid)();
10
+ return (0, nanoid_1.nanoid)().replace(/[_-]/g, '0');
11
11
  }
12
12
  exports.guid = guid;
13
13
  function deterministicRandom(seed) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -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
- if (!this.topics.includes(workflowTopic)) {
20
- this.topics.push(workflowTopic);
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
- activate(version: string, delay?: number): Promise<boolean>;
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 key_1 = require("../../modules/key");
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
- //wait time to see if quorum is reached
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
- await instance.subscribe.subscribe(key_1.KeyType.QUORUM, instance.subscriptionHandler(), appId); //general quorum subscription
34
- await instance.subscribe.subscribe(key_1.KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid); //app-specific quorum subscription (used for pubsub one-time request/response)
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(key_1.KeyType.STREAMS, { appId: this.appId })
106
+ stream: this.engine.stream.mintKey(hotmesh_1.KeyType.STREAMS, { appId: this.appId })
106
107
  };
107
108
  }
108
- this.store.publish(key_1.KeyType.QUORUM, {
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 = QUORUM_DELAY, details = false) {
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(key_1.KeyType.QUORUM, {
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(key_1.KeyType.QUORUM, quorumMessage, this.appId, quorumMessage.topic || quorumMessage.guid);
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 = QUORUM_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
- async activate(version, delay = QUORUM_DELAY) {
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(key_1.KeyType.QUORUM, { type: 'activate', cache_mode: 'nocache', until_version: version }, this.appId);
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.info('quorum-rollcall-error', { q1, q2, q3 });
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
  }
@@ -6,6 +6,7 @@ class RedisStoreService extends index_1.StoreService {
6
6
  constructor(redisClient) {
7
7
  super(redisClient);
8
8
  this.commands = {
9
+ set: 'SET',
9
10
  setnx: 'SETNX',
10
11
  del: 'DEL',
11
12
  expire: 'EXPIRE',
@@ -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.redisClient[this.commands.setnx](key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`);
110
- if (this.isSuccessful(success)) {
111
- await this.redisClient[this.commands.expire](key, delay - 1);
112
- return true;
113
- }
114
- return false;
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();
@@ -42,7 +42,7 @@ type KeyStoreParams = {
42
42
  facet?: string;
43
43
  topic?: string;
44
44
  timeValue?: number;
45
- scoutType?: 'signal' | 'time';
45
+ scoutType?: 'signal' | 'time' | 'activate';
46
46
  };
47
47
  type HotMesh = typeof HotMeshService;
48
48
  type RedisConfig = {
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
@@ -10,7 +10,7 @@ export async function sleepFor(ms: number) {
10
10
  }
11
11
 
12
12
  export function guid(): string {
13
- return nanoid();
13
+ return nanoid().replace(/[_-]/g, '0');
14
14
  }
15
15
 
16
16
  export function deterministicRandom(seed: number): number {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -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
- if (!this.topics.includes(workflowTopic)) {
33
- this.topics.push(workflowTopic);
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;
@@ -1,4 +1,4 @@
1
- import { KeyType } from '../../modules/key';
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
- await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId); //general quorum subscription
63
- await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid); //app-specific quorum subscription (used for pubsub one-time request/response)
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 = QUORUM_DELAY, details = false): Promise<number> {
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 = QUORUM_DELAY): Promise<QuorumProfile[]> {
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
- async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
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.info('quorum-rollcall-error', { q1, q2, q3 });
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
  }
@@ -18,6 +18,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
18
18
  constructor(redisClient: RedisClientType) {
19
19
  super(redisClient);
20
20
  this.commands = {
21
+ set: 'SET',
21
22
  setnx: 'SETNX',
22
23
  del: 'DEL',
23
24
  expire: 'EXPIRE',
@@ -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.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
184
- if (this.isSuccessful(success)) {
185
- await this.redisClient[this.commands.expire](key, delay - 1);
186
- return true;
187
- }
188
- return false;
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;