@hotmeshio/hotmesh 0.0.46 → 0.0.48

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.
@@ -4,6 +4,8 @@ import { AppSubscriptions, AppTransitions, AppVID } from "../types/app";
4
4
  import { RedisClient, RedisMulti } from "../types/redis";
5
5
  import { StringAnyType } from "../types/serializer";
6
6
  import { StreamCode, StreamStatus } from "../types/stream";
7
+ import { SystemHealth } from '../types/quorum';
8
+ export declare function getSystemHealth(): Promise<SystemHealth>;
7
9
  export declare function sleepFor(ms: number): Promise<unknown>;
8
10
  export declare function guid(): string;
9
11
  export declare function deterministicRandom(seed: number): number;
@@ -1,7 +1,46 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.guid = exports.sleepFor = void 0;
6
+ exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.guid = exports.sleepFor = exports.getSystemHealth = void 0;
7
+ const os_1 = __importDefault(require("os"));
8
+ const systeminformation_1 = __importDefault(require("systeminformation"));
4
9
  const nanoid_1 = require("nanoid");
10
+ async function safeExecute(operation, defaultValue) {
11
+ try {
12
+ return await operation;
13
+ }
14
+ catch (error) {
15
+ console.error(`Operation Error: ${error}`);
16
+ return defaultValue;
17
+ }
18
+ }
19
+ async function getSystemHealth() {
20
+ const totalMemory = os_1.default.totalmem();
21
+ const freeMemory = os_1.default.freemem();
22
+ const usedMemory = totalMemory - freeMemory;
23
+ const cpus = os_1.default.cpus();
24
+ // CPU load calculation remains unchanged
25
+ const cpuLoad = cpus.map((cpu, i) => {
26
+ const total = Object.values(cpu.times).reduce((acc, tv) => acc + tv, 0);
27
+ const idle = cpu.times.idle;
28
+ const usage = ((total - idle) / total) * 100;
29
+ return { [`CPU ${i} Usage`]: `${usage.toFixed(2)}%` };
30
+ });
31
+ // Wrap each systeminformation call with safeExecute
32
+ const networkStats = await safeExecute(systeminformation_1.default.networkStats(), []);
33
+ // Construct the system health object with error handling in mind
34
+ const systemHealth = {
35
+ TotalMemoryGB: `${(totalMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
36
+ FreeMemoryGB: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
37
+ UsedMemoryGB: `${(usedMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
38
+ CPULoad: cpuLoad,
39
+ NetworkStats: networkStats,
40
+ };
41
+ return systemHealth;
42
+ }
43
+ exports.getSystemHealth = getSystemHealth;
5
44
  async function sleepFor(ms) {
6
45
  return new Promise((resolve) => setTimeout(resolve, ms));
7
46
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -57,7 +57,14 @@
57
57
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
58
58
  "test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
59
59
  },
60
- "keywords": ["durable workflow", "hotmesh", "service mesh", "workflows", "operational data", "redis"],
60
+ "keywords": [
61
+ "durable workflow",
62
+ "hotmesh",
63
+ "service mesh",
64
+ "workflows",
65
+ "operational data",
66
+ "redis"
67
+ ],
61
68
  "author": "luke.birdeau@gmail.com",
62
69
  "license": "SEE LICENSE IN LICENSE",
63
70
  "dependencies": {
@@ -66,6 +73,7 @@
66
73
  "js-yaml": "^4.1.0",
67
74
  "ms": "^2.1.3",
68
75
  "nanoid": "^3.3.6",
76
+ "systeminformation": "^5.22.2",
69
77
  "winston": "^3.8.2"
70
78
  },
71
79
  "devDependencies": {
@@ -77,6 +77,10 @@ export declare class MeshOSService {
77
77
  * Creates an FT search index
78
78
  */
79
79
  static createIndex(): Promise<void>;
80
+ /**
81
+ * Lists FT search indexes
82
+ */
83
+ static listIndexes(): Promise<string[]>;
80
84
  /**
81
85
  * stop the workers
82
86
  * @returns {Promise<void>}
@@ -39,7 +39,15 @@ class MeshOSService {
39
39
  static async createIndex() {
40
40
  const my = new this();
41
41
  const hmClient = await MeshOSService.getHotMeshClient(my.redisClass, my.redisOptions, my.namespace, my.taskQueue);
42
- search_1.Search.configureSearchIndex(hmClient, my.search);
42
+ await search_1.Search.configureSearchIndex(hmClient, my.search);
43
+ }
44
+ /**
45
+ * Lists FT search indexes
46
+ */
47
+ static async listIndexes() {
48
+ const my = new this();
49
+ const hmClient = await MeshOSService.getHotMeshClient(my.redisClass, my.redisOptions, my.namespace, my.taskQueue);
50
+ return await search_1.Search.listSearchIndexes(hmClient);
43
51
  }
44
52
  /**
45
53
  * stop the workers
@@ -8,6 +8,7 @@ export declare class Search {
8
8
  searchSessionIndex: number;
9
9
  hotMeshClient: HotMesh;
10
10
  store: StoreService<RedisClient, RedisMulti> | null;
11
+ constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string);
11
12
  safeKey(key: string): string;
12
13
  /**
13
14
  * For those deployments with a redis stack backend (with the FT module),
@@ -17,7 +18,13 @@ export declare class Search {
17
18
  * in the HASH.
18
19
  */
19
20
  static configureSearchIndex(hotMeshClient: HotMesh, search?: WorkflowSearchOptions): Promise<void>;
20
- constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string);
21
+ /**
22
+ * For those deployments with a redis stack backend (with the FT module),
23
+ * this method will list all search indexes.
24
+ * @param {HotMesh} hotMeshClient - the hotmesh client
25
+ * @returns {Promise<string[]>} - the list of search indexes
26
+ */
27
+ static listSearchIndexes(hotMeshClient: HotMesh): Promise<string[]>;
21
28
  /**
22
29
  * increments the index to return a unique search session guid when
23
30
  * calling any method that produces side effects (changes the value)
@@ -3,8 +3,18 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Search = void 0;
4
4
  const key_1 = require("../../modules/key");
5
5
  class Search {
6
+ constructor(workflowId, hotMeshClient, searchSessionId) {
7
+ this.searchSessionIndex = 0;
8
+ const keyParams = {
9
+ appId: hotMeshClient.appId,
10
+ jobId: workflowId
11
+ };
12
+ this.jobId = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
13
+ this.searchSessionId = searchSessionId;
14
+ this.hotMeshClient = hotMeshClient;
15
+ this.store = hotMeshClient.engine.store;
16
+ }
6
17
  safeKey(key) {
7
- //note: protect the execution namespace with a prefix
8
18
  return `_${key}`;
9
19
  }
10
20
  /**
@@ -40,16 +50,16 @@ class Search {
40
50
  }
41
51
  }
42
52
  }
43
- constructor(workflowId, hotMeshClient, searchSessionId) {
44
- this.searchSessionIndex = 0;
45
- const keyParams = {
46
- appId: hotMeshClient.appId,
47
- jobId: workflowId
48
- };
49
- this.jobId = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
50
- this.searchSessionId = searchSessionId;
51
- this.hotMeshClient = hotMeshClient;
52
- this.store = hotMeshClient.engine.store;
53
+ /**
54
+ * For those deployments with a redis stack backend (with the FT module),
55
+ * this method will list all search indexes.
56
+ * @param {HotMesh} hotMeshClient - the hotmesh client
57
+ * @returns {Promise<string[]>} - the list of search indexes
58
+ */
59
+ static async listSearchIndexes(hotMeshClient) {
60
+ const store = hotMeshClient.engine.store;
61
+ const searchIndexes = await store.exec('FT._LIST');
62
+ return searchIndexes;
53
63
  }
54
64
  /**
55
65
  * increments the index to return a unique search session guid when
@@ -41,6 +41,7 @@ declare class EngineService {
41
41
  jobCallbacks: Record<string, JobMessageCallback>;
42
42
  reporting: boolean;
43
43
  jobId: number;
44
+ inited: string;
44
45
  static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<EngineService>;
45
46
  verifyEngineFields(config: HotMeshConfig): void;
46
47
  initStoreChannel(store: RedisClient): Promise<void>;
@@ -44,6 +44,7 @@ class EngineService {
44
44
  instance.router.consumeMessages(instance.stream.mintKey(key_1.KeyType.STREAMS, { appId: instance.appId }), 'ENGINE', instance.guid, instance.processStreamMessage.bind(instance));
45
45
  instance.taskService = new task_1.TaskService(instance.store, logger);
46
46
  instance.exporter = new exporter_1.ExporterService(instance.appId, instance.store, logger);
47
+ instance.inited = (0, utils_1.formatISODate)(new Date());
47
48
  return instance;
48
49
  }
49
50
  }
@@ -4,7 +4,7 @@ import { QuorumService } from '../quorum';
4
4
  import { WorkerService } from '../worker';
5
5
  import { JobState, JobData, JobOutput, JobStatus, JobInterruptOptions } from '../../types/job';
6
6
  import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
7
- import { JobMessageCallback, QuorumProfile } from '../../types/quorum';
7
+ import { JobMessageCallback, QuorumProfile, ThrottleOptions } from '../../types/quorum';
8
8
  import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
9
9
  import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
10
10
  import { StringAnyType, StringStringType } from '../../types/serializer';
@@ -33,6 +33,7 @@ declare class HotMeshService {
33
33
  pubsub(topic: string, data?: JobData, context?: JobState | null, timeout?: number): Promise<JobOutput>;
34
34
  add(streamData: StreamData | StreamDataResponse): Promise<string>;
35
35
  rollCall(delay?: number): Promise<QuorumProfile[]>;
36
+ throttle(options: ThrottleOptions): Promise<boolean>;
36
37
  plan(path: string): Promise<HotMeshManifest>;
37
38
  deploy(pathOrYAML: string): Promise<HotMeshManifest>;
38
39
  activate(version: string, delay?: number): Promise<boolean>;
@@ -89,10 +89,24 @@ class HotMeshService {
89
89
  async add(streamData) {
90
90
  return await this.engine.add(streamData);
91
91
  }
92
- // ************* COMPILER METHODS *************
92
+ // ************* QUORUM METHODS *************
93
93
  async rollCall(delay) {
94
94
  return await this.quorum?.rollCall(delay);
95
95
  }
96
+ async throttle(options) {
97
+ const throttleMessage = {
98
+ type: 'throttle',
99
+ throttle: options.throttle,
100
+ };
101
+ if (options.guid) {
102
+ throttleMessage.guid = options.guid;
103
+ }
104
+ else if (options.topic) {
105
+ throttleMessage.topic = options.topic;
106
+ }
107
+ return await this.quorum?.pub(throttleMessage);
108
+ }
109
+ // ************* COMPILER METHODS *************
96
110
  async plan(path) {
97
111
  return await this.engine?.plan(path);
98
112
  }
@@ -4,8 +4,8 @@ exports.QuorumService = void 0;
4
4
  const enums_1 = require("../../modules/enums");
5
5
  const utils_1 = require("../../modules/utils");
6
6
  const compiler_1 = require("../compiler");
7
- const redis_1 = require("../store/clients/redis");
8
7
  const ioredis_1 = require("../store/clients/ioredis");
8
+ const redis_1 = require("../store/clients/redis");
9
9
  const ioredis_2 = require("../sub/clients/ioredis");
10
10
  const redis_2 = require("../sub/clients/redis");
11
11
  const hotmesh_1 = require("../../types/hotmesh");
@@ -107,6 +107,11 @@ class QuorumService {
107
107
  stream,
108
108
  counts: this.engine.router.counts,
109
109
  timestamp: (0, utils_1.formatISODate)(new Date()),
110
+ inited: this.engine.inited,
111
+ throttle: this.engine.router.throttle,
112
+ reclaimDelay: this.engine.router.reclaimDelay,
113
+ reclaimCount: this.engine.router.reclaimCount,
114
+ system: await (0, utils_1.getSystemHealth)(),
110
115
  };
111
116
  }
112
117
  this.store.publish(hotmesh_1.KeyType.QUORUM, {
@@ -18,6 +18,7 @@ declare class WorkerService {
18
18
  router: Router | null;
19
19
  logger: ILogger;
20
20
  reporting: boolean;
21
+ inited: string;
21
22
  static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<WorkerService[]>;
22
23
  verifyWorkerFields(worker: HotMeshWorker): void;
23
24
  initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
@@ -38,6 +38,7 @@ class WorkerService {
38
38
  service.router = service.initRouter(worker, logger);
39
39
  const key = service.stream.mintKey(key_1.KeyType.STREAMS, { appId: service.appId, topic: worker.topic });
40
40
  await service.router.consumeMessages(key, 'WORKER', service.guid, worker.callback);
41
+ service.inited = (0, utils_1.formatISODate)(new Date());
41
42
  services.push(service);
42
43
  }
43
44
  }
@@ -116,6 +117,11 @@ class WorkerService {
116
117
  stream: this.stream.mintKey(key_1.KeyType.STREAMS, params),
117
118
  counts: this.router.counts,
118
119
  timestamp: (0, utils_1.formatISODate)(new Date()),
120
+ inited: this.inited,
121
+ throttle: this.router.throttle,
122
+ reclaimDelay: this.router.reclaimDelay,
123
+ reclaimCount: this.router.reclaimCount,
124
+ system: await (0, utils_1.getSystemHealth)(),
119
125
  };
120
126
  }
121
127
  this.store.publish(key_1.KeyType.QUORUM, {
@@ -12,7 +12,7 @@ export { JobData, JobsData, JobMetadata, JobOutput, JobState, JobStatus, Partial
12
12
  export { MappingStatements } from './map';
13
13
  export { Pipe, PipeItem, PipeItems } from './pipe';
14
14
  export { HotMesh, HotMeshApp, HotMeshApps, HotMeshConfig, HotMeshEngine, RedisConfig, HotMeshGraph, HotMeshManifest, HotMeshSettings, HotMeshWorker, KeyStoreParams, KeyType } from './hotmesh';
15
- export { ActivateMessage, JobMessage, JobMessageCallback, PingMessage, PongMessage, QuorumMessage, QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage, WorkMessage } from './quorum';
15
+ export { ActivateMessage, JobMessage, JobMessageCallback, PingMessage, PongMessage, QuorumMessage, QuorumMessageCallback, QuorumProfile, SubscriptionCallback, SystemHealth, ThrottleMessage, ThrottleOptions, WorkMessage } from './quorum';
16
16
  export { MultiResponseFlags, RedisClient, RedisMulti } from './redis';
17
17
  export { RedisClientType, RedisMultiType } from './redisclient';
18
18
  export { JSONSchema, StringAnyType, StringScalarType, StringStringType, SymbolMap, SymbolMaps, SymbolRanges, Symbols, SymbolSets } from './serializer';
@@ -1,4 +1,36 @@
1
1
  import { JobOutput } from "./job";
2
+ interface CPULoad {
3
+ [cpu: string]: string;
4
+ }
5
+ interface NetworkStat {
6
+ iface: string;
7
+ operstate: string;
8
+ rx_bytes: number;
9
+ rx_dropped: number;
10
+ rx_errors: number;
11
+ tx_bytes: number;
12
+ tx_dropped: number;
13
+ tx_errors: number;
14
+ rx_sec: number;
15
+ tx_sec: number;
16
+ ms: number;
17
+ }
18
+ /** reveals: memory, cpu, network */
19
+ export interface SystemHealth {
20
+ TotalMemoryGB: string;
21
+ FreeMemoryGB: string;
22
+ UsedMemoryGB: string;
23
+ CPULoad: CPULoad[];
24
+ NetworkStats: NetworkStat[];
25
+ }
26
+ export type ThrottleOptions = {
27
+ /** target an engine OR worker by GUID */
28
+ guid?: string;
29
+ /** target a worker quorum */
30
+ topic?: string;
31
+ /** in milliseconds; default is 0 */
32
+ throttle: number;
33
+ };
2
34
  export interface QuorumProfile {
3
35
  namespace: string;
4
36
  app_id: string;
@@ -7,7 +39,12 @@ export interface QuorumProfile {
7
39
  stream?: string;
8
40
  stream_depth?: number;
9
41
  counts?: Record<string, number>;
42
+ inited?: string;
10
43
  timestamp?: string;
44
+ throttle?: number;
45
+ reclaimDelay?: number;
46
+ reclaimCount?: number;
47
+ system?: SystemHealth;
11
48
  }
12
49
  export interface PingMessage {
13
50
  type: 'ping';
@@ -60,3 +97,4 @@ export interface QuorumMessageCallback {
60
97
  * to the new version without any downtime and a coordinating parent server.
61
98
  */
62
99
  export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
100
+ export {};
package/modules/utils.ts CHANGED
@@ -1,9 +1,50 @@
1
+ import os from 'os';
2
+ import si from 'systeminformation';
1
3
  import { nanoid } from "nanoid";
2
4
  import { StoreService } from "../services/store";
3
5
  import { AppSubscriptions, AppTransitions, AppVID } from "../types/app";
4
6
  import { RedisClient, RedisMulti } from "../types/redis";
5
7
  import { StringAnyType } from "../types/serializer";
6
8
  import { StreamCode, StreamStatus } from "../types/stream";
9
+ import { SystemHealth } from '../types/quorum';
10
+
11
+ async function safeExecute<T>(operation: Promise<T>, defaultValue: T): Promise<T> {
12
+ try {
13
+ return await operation;
14
+ } catch (error) {
15
+ console.error(`Operation Error: ${error}`);
16
+ return defaultValue;
17
+ }
18
+ }
19
+
20
+ export async function getSystemHealth(): Promise<SystemHealth> {
21
+ const totalMemory = os.totalmem();
22
+ const freeMemory = os.freemem();
23
+ const usedMemory = totalMemory - freeMemory;
24
+ const cpus = os.cpus();
25
+
26
+ // CPU load calculation remains unchanged
27
+ const cpuLoad = cpus.map((cpu, i) => {
28
+ const total = Object.values(cpu.times).reduce((acc, tv) => acc + tv, 0);
29
+ const idle = cpu.times.idle;
30
+ const usage = ((total - idle) / total) * 100;
31
+ return { [`CPU ${i} Usage`]: `${usage.toFixed(2)}%` };
32
+ });
33
+
34
+ // Wrap each systeminformation call with safeExecute
35
+ const networkStats = await safeExecute(si.networkStats(), []);
36
+
37
+ // Construct the system health object with error handling in mind
38
+ const systemHealth = {
39
+ TotalMemoryGB: `${(totalMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
40
+ FreeMemoryGB: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
41
+ UsedMemoryGB: `${(usedMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
42
+ CPULoad: cpuLoad,
43
+ NetworkStats: networkStats,
44
+ };
45
+
46
+ return systemHealth as SystemHealth;
47
+ }
7
48
 
8
49
  export async function sleepFor(ms: number) {
9
50
  return new Promise((resolve) => setTimeout(resolve, ms));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -57,7 +57,14 @@
57
57
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
58
58
  "test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
59
59
  },
60
- "keywords": ["durable workflow", "hotmesh", "service mesh", "workflows", "operational data", "redis"],
60
+ "keywords": [
61
+ "durable workflow",
62
+ "hotmesh",
63
+ "service mesh",
64
+ "workflows",
65
+ "operational data",
66
+ "redis"
67
+ ],
61
68
  "author": "luke.birdeau@gmail.com",
62
69
  "license": "SEE LICENSE IN LICENSE",
63
70
  "dependencies": {
@@ -66,6 +73,7 @@
66
73
  "js-yaml": "^4.1.0",
67
74
  "ms": "^2.1.3",
68
75
  "nanoid": "^3.3.6",
76
+ "systeminformation": "^5.22.2",
69
77
  "winston": "^3.8.2"
70
78
  },
71
79
  "devDependencies": {
@@ -125,7 +125,16 @@ export class MeshOSService {
125
125
  static async createIndex() {
126
126
  const my = new this();
127
127
  const hmClient = await MeshOSService.getHotMeshClient(my.redisClass, my.redisOptions, my.namespace, my.taskQueue);
128
- Search.configureSearchIndex(hmClient, my.search)
128
+ await Search.configureSearchIndex(hmClient, my.search);
129
+ }
130
+
131
+ /**
132
+ * Lists FT search indexes
133
+ */
134
+ static async listIndexes() {
135
+ const my = new this();
136
+ const hmClient = await MeshOSService.getHotMeshClient(my.redisClass, my.redisOptions, my.namespace, my.taskQueue);
137
+ return await Search.listSearchIndexes(hmClient)
129
138
  }
130
139
 
131
140
  /**
@@ -11,8 +11,18 @@ export class Search {
11
11
  hotMeshClient: HotMesh;
12
12
  store: StoreService<RedisClient, RedisMulti> | null;
13
13
 
14
+ constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string) {
15
+ const keyParams = {
16
+ appId: hotMeshClient.appId,
17
+ jobId: workflowId
18
+ }
19
+ this.jobId = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
20
+ this.searchSessionId = searchSessionId;
21
+ this.hotMeshClient = hotMeshClient;
22
+ this.store = hotMeshClient.engine.store as StoreService<RedisClient, RedisMulti>;
23
+ }
24
+
14
25
  safeKey(key:string): string {
15
- //note: protect the execution namespace with a prefix
16
26
  return `_${key}`;
17
27
  }
18
28
 
@@ -49,15 +59,16 @@ export class Search {
49
59
  }
50
60
  }
51
61
 
52
- constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string) {
53
- const keyParams = {
54
- appId: hotMeshClient.appId,
55
- jobId: workflowId
56
- }
57
- this.jobId = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
58
- this.searchSessionId = searchSessionId;
59
- this.hotMeshClient = hotMeshClient;
60
- this.store = hotMeshClient.engine.store as StoreService<RedisClient, RedisMulti>;
62
+ /**
63
+ * For those deployments with a redis stack backend (with the FT module),
64
+ * this method will list all search indexes.
65
+ * @param {HotMesh} hotMeshClient - the hotmesh client
66
+ * @returns {Promise<string[]>} - the list of search indexes
67
+ */
68
+ static async listSearchIndexes(hotMeshClient: HotMesh): Promise<string[]> {
69
+ const store = hotMeshClient.engine.store;
70
+ const searchIndexes = await store.exec('FT._LIST');
71
+ return searchIndexes as string[];
61
72
  }
62
73
 
63
74
  /**
@@ -93,7 +93,7 @@ export class WorkerService {
93
93
  const worker = new WorkerService();
94
94
  worker.activityRunner = await worker.initActivityWorker(config, activityTopic);
95
95
  worker.workflowRunner = await worker.initWorkflowWorker(config, workflowTopic, workflowFunction);
96
- Search.configureSearchIndex(worker.workflowRunner, config.search)
96
+ Search.configureSearchIndex(worker.workflowRunner, config.search);
97
97
  await WorkerService.activateWorkflow(worker.workflowRunner);
98
98
  return worker;
99
99
  }
@@ -98,6 +98,7 @@ class EngineService {
98
98
  jobCallbacks: Record<string, JobMessageCallback> = {};
99
99
  reporting = false;
100
100
  jobId = 1;
101
+ inited: string;
101
102
 
102
103
  static async init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<EngineService> {
103
104
  if (config.engine) {
@@ -133,6 +134,7 @@ class EngineService {
133
134
  instance.store,
134
135
  logger,
135
136
  );
137
+ instance.inited = formatISODate(new Date());
136
138
  return instance;
137
139
  }
138
140
  }
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  HotMeshConfig,
18
18
  HotMeshManifest } from '../../types/hotmesh';
19
- import { JobMessageCallback, QuorumProfile } from '../../types/quorum';
19
+ import { JobMessageCallback, QuorumProfile, ThrottleMessage, ThrottleOptions } from '../../types/quorum';
20
20
  import {
21
21
  JobStatsInput,
22
22
  GetStatsOptions,
@@ -137,10 +137,25 @@ class HotMeshService {
137
137
  return await this.engine.add(streamData) as string;
138
138
  }
139
139
 
140
- // ************* COMPILER METHODS *************
140
+
141
+ // ************* QUORUM METHODS *************
141
142
  async rollCall(delay?: number): Promise<QuorumProfile[]> {
142
143
  return await this.quorum?.rollCall(delay);
143
144
  }
145
+ async throttle(options: ThrottleOptions): Promise<boolean> {
146
+ const throttleMessage: ThrottleMessage = {
147
+ type: 'throttle',
148
+ throttle: options.throttle,
149
+ };
150
+ if (options.guid) {
151
+ throttleMessage.guid = options.guid;
152
+ } else if (options.topic) {
153
+ throttleMessage.topic = options.topic;
154
+ }
155
+ return await this.quorum?.pub(throttleMessage);
156
+ }
157
+
158
+ // ************* COMPILER METHODS *************
144
159
  async plan(path: string): Promise<HotMeshManifest> {
145
160
  return await this.engine?.plan(path);
146
161
  }
@@ -1,11 +1,17 @@
1
- import { HMSH_ACTIVATION_MAX_RETRY, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
2
- import { formatISODate, identifyRedisType, sleepFor } from '../../modules/utils';
1
+ import {
2
+ HMSH_ACTIVATION_MAX_RETRY,
3
+ HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
4
+ import {
5
+ formatISODate,
6
+ getSystemHealth,
7
+ identifyRedisType,
8
+ sleepFor } from '../../modules/utils';
3
9
  import { CompilerService } from '../compiler';
4
10
  import { EngineService } from '../engine';
5
11
  import { ILogger } from '../logger';
6
12
  import { StoreService } from '../store';
7
- import { RedisStoreService as RedisStore } from '../store/clients/redis';
8
13
  import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
14
+ import { RedisStoreService as RedisStore } from '../store/clients/redis';
9
15
  import { SubService } from '../sub';
10
16
  import { IORedisSubService as IORedisSub } from '../sub/clients/ioredis';
11
17
  import { RedisSubService as RedisSub } from '../sub/clients/redis';
@@ -152,6 +158,11 @@ class QuorumService {
152
158
  stream,
153
159
  counts: this.engine.router.counts,
154
160
  timestamp: formatISODate(new Date()),
161
+ inited: this.engine.inited,
162
+ throttle: this.engine.router.throttle,
163
+ reclaimDelay: this.engine.router.reclaimDelay,
164
+ reclaimCount: this.engine.router.reclaimCount,
165
+ system: await getSystemHealth(),
155
166
  };
156
167
  }
157
168
  this.store.publish(
@@ -1,5 +1,5 @@
1
1
  import { KeyType } from "../../modules/key";
2
- import { formatISODate, identifyRedisType } from "../../modules/utils";
2
+ import { formatISODate, getSystemHealth, identifyRedisType } from "../../modules/utils";
3
3
  import { ConnectorService } from "../connector";
4
4
  import { ILogger } from "../logger";
5
5
  import { Router } from "../router";
@@ -34,6 +34,7 @@ class WorkerService {
34
34
  router: Router | null;
35
35
  logger: ILogger;
36
36
  reporting = false;
37
+ inited: string;
37
38
 
38
39
  static async init(
39
40
  namespace: string,
@@ -76,6 +77,7 @@ class WorkerService {
76
77
  service.guid,
77
78
  worker.callback
78
79
  );
80
+ service.inited = formatISODate(new Date());
79
81
  services.push(service);
80
82
  }
81
83
  }
@@ -176,6 +178,11 @@ class WorkerService {
176
178
  stream: this.stream.mintKey(KeyType.STREAMS, params),
177
179
  counts: this.router.counts,
178
180
  timestamp: formatISODate(new Date()),
181
+ inited: this.inited,
182
+ throttle: this.router.throttle,
183
+ reclaimDelay: this.router.reclaimDelay,
184
+ reclaimCount: this.router.reclaimCount,
185
+ system: await getSystemHealth(),
179
186
  };
180
187
  }
181
188
  this.store.publish(
package/types/index.ts CHANGED
@@ -115,7 +115,9 @@ export {
115
115
  QuorumMessageCallback,
116
116
  QuorumProfile,
117
117
  SubscriptionCallback,
118
+ SystemHealth,
118
119
  ThrottleMessage,
120
+ ThrottleOptions,
119
121
  WorkMessage } from './quorum';
120
122
  export {
121
123
  MultiResponseFlags,
package/types/quorum.ts CHANGED
@@ -1,6 +1,40 @@
1
1
  import { JobOutput } from "./job";
2
2
 
3
- //used for coordination (like version activation)
3
+ interface CPULoad {
4
+ [cpu: string]: string;
5
+ }
6
+
7
+ interface NetworkStat {
8
+ iface: string;
9
+ operstate: string;
10
+ rx_bytes: number;
11
+ rx_dropped: number;
12
+ rx_errors: number;
13
+ tx_bytes: number;
14
+ tx_dropped: number;
15
+ tx_errors: number;
16
+ rx_sec: number;
17
+ tx_sec: number;
18
+ ms: number;
19
+ }
20
+
21
+ /** reveals: memory, cpu, network */
22
+ export interface SystemHealth {
23
+ TotalMemoryGB: string;
24
+ FreeMemoryGB: string;
25
+ UsedMemoryGB: string;
26
+ CPULoad: CPULoad[];
27
+ NetworkStats: NetworkStat[];
28
+ }
29
+
30
+ export type ThrottleOptions = {
31
+ /** target an engine OR worker by GUID */
32
+ guid?: string;
33
+ /** target a worker quorum */
34
+ topic?: string;
35
+ /** in milliseconds; default is 0 */
36
+ throttle: number;
37
+ };
4
38
 
5
39
  export interface QuorumProfile {
6
40
  namespace: string;
@@ -10,9 +44,15 @@ export interface QuorumProfile {
10
44
  stream?: string;
11
45
  stream_depth?: number;
12
46
  counts?: Record<string, number>;
47
+ inited?: string;
13
48
  timestamp?: string;
49
+ throttle?: number;
50
+ reclaimDelay?: number;
51
+ reclaimCount?: number;
52
+ system?: SystemHealth;
14
53
  }
15
54
 
55
+ //used for coordination (like version activation)
16
56
  export interface PingMessage {
17
57
  type: 'ping';
18
58
  originator: string; //guid