@hotmeshio/hotmesh 0.0.37 → 0.0.38

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/README.md CHANGED
@@ -15,7 +15,7 @@ npm install @hotmeshio/hotmesh
15
15
  ## Understanding HotMesh
16
16
  HotMesh inverts the relationship to Redis: those functions that once used Redis as a cache, are instead *cached and governed* by Redis. Consider the following. It's a typical microservices network, with a tangled mess of services and functions. There's important business logic in there (functions *A*, *B* and *C* are critical!), but they're hard to find and access.
17
17
 
18
- <img src="https://github.com/hotmeshio/sdk-typescript/tree/main/docs/img/operational_data_layer.png" alt="A Tangled Microservices Network with 3 functions buried within" style="max-width:100%;width:600px;">
18
+ <img src="./docs/img/operational_data_layer.png" alt="A Tangled Microservices Network with 3 valuable functions buried within" style="max-width:100%;width:600px;">
19
19
 
20
20
  HotMesh creates an *ad hoc*, Redis-backed network of functions and organizes them into a unified service mesh. *Any service with access to Redis can join in the network, bypassing the legacy clutter.*
21
21
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -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 } from '../../types/quorum';
7
+ import { JobMessageCallback, QuorumProfile } 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 } from '../../types/serializer';
@@ -31,10 +31,10 @@ declare class HotMeshService {
31
31
  punsub(wild: string): Promise<void>;
32
32
  pubsub(topic: string, data?: JobData, context?: JobState | null, timeout?: number): Promise<JobOutput>;
33
33
  add(streamData: StreamData | StreamDataResponse): Promise<string>;
34
+ rollCall(delay?: number): Promise<QuorumProfile[]>;
34
35
  plan(path: string): Promise<HotMeshManifest>;
35
36
  deploy(pathOrYAML: string): Promise<HotMeshManifest>;
36
37
  activate(version: string, delay?: number): Promise<boolean>;
37
- inventory(version: string, delay?: number): Promise<number>;
38
38
  getStats(topic: string, query: JobStatsInput): Promise<StatsResponse>;
39
39
  getStatus(jobId: string): Promise<JobStatus>;
40
40
  getState(topic: string, jobId: string): Promise<JobOutput>;
@@ -90,6 +90,9 @@ class HotMeshService {
90
90
  return await this.engine.add(streamData);
91
91
  }
92
92
  // ************* COMPILER METHODS *************
93
+ async rollCall(delay) {
94
+ return await this.quorum?.rollCall(delay);
95
+ }
93
96
  async plan(path) {
94
97
  return await this.engine?.plan(path);
95
98
  }
@@ -100,10 +103,6 @@ class HotMeshService {
100
103
  //activation is a quorum operation
101
104
  return await this.quorum?.activate(version, delay);
102
105
  }
103
- async inventory(version, delay) {
104
- //get count of all peers
105
- return await this.quorum?.inventory(delay);
106
- }
107
106
  // ************* REPORTER METHODS *************
108
107
  async getStats(topic, query) {
109
108
  return await this.engine?.getStats(topic, query);
@@ -3,15 +3,15 @@ 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, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
7
- import { HotMeshApps, HotMeshConfig } from '../../types/hotmesh';
6
+ import { QuorumMessageCallback, QuorumProfile, SubscriptionCallback, ThrottleMessage } from '../../types/quorum';
7
+ import { HotMeshConfig } from '../../types/hotmesh';
8
8
  import { RedisClient, RedisMulti } from '../../types/redis';
9
9
  declare class QuorumService {
10
10
  namespace: string;
11
- apps: HotMeshApps | null;
12
11
  appId: string;
13
12
  guid: string;
14
13
  engine: EngineService;
14
+ profiles: QuorumProfile[];
15
15
  store: StoreService<RedisClient, RedisMulti> | null;
16
16
  subscribe: SubService<RedisClient, RedisMulti> | null;
17
17
  logger: ILogger;
@@ -24,12 +24,12 @@ declare class QuorumService {
24
24
  initStoreChannel(store: RedisClient): Promise<void>;
25
25
  initSubChannel(sub: RedisClient): Promise<void>;
26
26
  subscriptionHandler(): SubscriptionCallback;
27
- sayPong(appId: string, guid: string, originator: string): Promise<void>;
28
- requestQuorum(delay?: number): Promise<number>;
27
+ sayPong(appId: string, guid: string, originator: string, details?: boolean): Promise<void>;
28
+ requestQuorum(delay?: number, details?: boolean): Promise<number>;
29
29
  pub(quorumMessage: ThrottleMessage): Promise<boolean>;
30
30
  sub(callback: QuorumMessageCallback): Promise<void>;
31
31
  unsub(callback: QuorumMessageCallback): Promise<void>;
32
- inventory(delay?: number): Promise<number>;
32
+ rollCall(delay?: number): Promise<QuorumProfile[]>;
33
33
  activate(version: string, delay?: number): Promise<boolean>;
34
34
  }
35
35
  export { QuorumService };
@@ -12,6 +12,7 @@ const redis_2 = require("../sub/clients/redis");
12
12
  const QUORUM_DELAY = 250;
13
13
  class QuorumService {
14
14
  constructor() {
15
+ this.profiles = [];
15
16
  this.cacheMode = 'cache';
16
17
  this.untilVersion = null;
17
18
  this.quorum = null;
@@ -68,10 +69,13 @@ class QuorumService {
68
69
  self.engine.setCacheMode(message.cache_mode, message.until_version);
69
70
  }
70
71
  else if (message.type === 'ping') {
71
- this.sayPong(self.appId, self.guid, message.originator);
72
+ self.sayPong(self.appId, self.guid, message.originator, message.details);
72
73
  }
73
74
  else if (message.type === 'pong' && self.guid === message.originator) {
74
75
  self.quorum = self.quorum + 1;
76
+ if (message.profile) {
77
+ self.profiles.push(message.profile);
78
+ }
75
79
  }
76
80
  else if (message.type === 'throttle') {
77
81
  self.engine.throttle(message.throttle);
@@ -91,13 +95,31 @@ class QuorumService {
91
95
  }
92
96
  };
93
97
  }
94
- async sayPong(appId, guid, originator) {
95
- this.store.publish(key_1.KeyType.QUORUM, { type: 'pong', guid, originator }, appId);
98
+ async sayPong(appId, guid, originator, details = false) {
99
+ let profile;
100
+ if (details) {
101
+ profile = {
102
+ engine_id: this.guid,
103
+ namespace: this.namespace,
104
+ app_id: this.appId,
105
+ stream: this.engine.stream.mintKey(key_1.KeyType.STREAMS, { appId: this.appId })
106
+ };
107
+ }
108
+ this.store.publish(key_1.KeyType.QUORUM, {
109
+ type: 'pong',
110
+ guid, originator,
111
+ profile,
112
+ }, appId);
96
113
  }
97
- async requestQuorum(delay = QUORUM_DELAY) {
114
+ async requestQuorum(delay = QUORUM_DELAY, details = false) {
98
115
  const quorum = this.quorum;
99
116
  this.quorum = 0;
100
- await this.store.publish(key_1.KeyType.QUORUM, { type: 'ping', originator: this.guid }, this.appId);
117
+ this.profiles.length = 0;
118
+ await this.store.publish(key_1.KeyType.QUORUM, {
119
+ type: 'ping',
120
+ originator: this.guid,
121
+ details,
122
+ }, this.appId);
101
123
  await (0, utils_1.sleepFor)(delay);
102
124
  return quorum;
103
125
  }
@@ -117,12 +139,26 @@ class QuorumService {
117
139
  this.callbacks = this.callbacks.filter(cb => cb !== callback);
118
140
  }
119
141
  // ************* COMPILER METHODS *************
120
- async inventory(delay = QUORUM_DELAY) {
121
- await this.requestQuorum(delay);
122
- const q1 = await this.requestQuorum(delay);
123
- const q2 = await this.requestQuorum(delay);
124
- const q3 = await this.requestQuorum(delay);
125
- return Math.round((q1 + q2 + q3) / 3);
142
+ async rollCall(delay = QUORUM_DELAY) {
143
+ await this.requestQuorum(delay, true);
144
+ const targetStreams = [];
145
+ const multi = this.store.getMulti();
146
+ this.profiles.forEach((profile) => {
147
+ if (!targetStreams.includes(profile.stream)) {
148
+ targetStreams.push(profile.stream);
149
+ this.store.xlen(profile.stream, multi);
150
+ }
151
+ });
152
+ const stream_depths = await multi.exec();
153
+ this.profiles.forEach(async (profile) => {
154
+ const index = targetStreams.indexOf(profile.stream);
155
+ if (index != -1) {
156
+ profile.stream_depth = Array.isArray(stream_depths[index]) ?
157
+ stream_depths[index][1] :
158
+ stream_depths[index];
159
+ }
160
+ });
161
+ return this.profiles;
126
162
  }
127
163
  async activate(version, delay = QUORUM_DELAY) {
128
164
  version = version.toString();
@@ -24,5 +24,6 @@ declare class IORedisStoreService extends StoreService<RedisClientType, RedisMul
24
24
  xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
25
25
  xack(key: string, group: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
26
26
  xdel(key: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
27
+ xlen(key: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
27
28
  }
28
29
  export { IORedisStoreService };
@@ -105,5 +105,14 @@ class IORedisStoreService extends index_1.StoreService {
105
105
  throw error;
106
106
  }
107
107
  }
108
+ async xlen(key, multi) {
109
+ try {
110
+ return await (multi || this.redisClient).xlen(key);
111
+ }
112
+ catch (error) {
113
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
114
+ throw error;
115
+ }
116
+ }
108
117
  }
109
118
  exports.IORedisStoreService = IORedisStoreService;
@@ -26,5 +26,6 @@ declare class RedisStoreService extends StoreService<RedisClientType, RedisMulti
26
26
  xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
27
27
  xack(key: string, group: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
28
28
  xdel(key: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
29
+ xlen(key: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
29
30
  }
30
31
  export { RedisStoreService };
@@ -30,6 +30,7 @@ class RedisStoreService extends index_1.StoreService {
30
30
  rpush: 'RPUSH',
31
31
  xack: 'XACK',
32
32
  xdel: 'XDEL',
33
+ xlen: 'XLEN',
33
34
  };
34
35
  }
35
36
  getMulti() {
@@ -141,5 +142,20 @@ class RedisStoreService extends index_1.StoreService {
141
142
  throw error;
142
143
  }
143
144
  }
145
+ async xlen(key, multi) {
146
+ try {
147
+ if (multi) {
148
+ multi.XLEN(key);
149
+ return multi;
150
+ }
151
+ else {
152
+ return await this.redisClient.XLEN(key);
153
+ }
154
+ }
155
+ catch (error) {
156
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
157
+ throw error;
158
+ }
159
+ }
144
160
  }
145
161
  exports.RedisStoreService = RedisStoreService;
@@ -31,6 +31,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
31
31
  abstract xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
32
32
  abstract xack(key: string, group: string, id: string, multi?: U): Promise<number | U>;
33
33
  abstract xdel(key: string, id: string, multi?: U): Promise<number | U>;
34
+ abstract xlen(key: string, multi?: U): Promise<number | U>;
34
35
  constructor(redisClient: T);
35
36
  init(namespace: string, appId: string, logger: ILogger): Promise<HotMeshApps>;
36
37
  isSuccessful(result: any): boolean;
@@ -19,5 +19,6 @@ declare class IORedisStreamService extends StreamService<RedisClientType, RedisM
19
19
  xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
20
20
  xack(key: string, group: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
21
21
  xdel(key: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
+ xlen(key: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
23
  }
23
24
  export { IORedisStreamService };
@@ -25,18 +25,18 @@ class IORedisStreamService extends index_1.StreamService {
25
25
  try {
26
26
  return (await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK';
27
27
  }
28
- catch (err) {
28
+ catch (error) {
29
29
  this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
30
- throw err;
30
+ throw error;
31
31
  }
32
32
  }
33
33
  else {
34
34
  try {
35
35
  return (await this.redisClient.xgroup(command, key, groupName, id)) === 'OK';
36
36
  }
37
- catch (err) {
37
+ catch (error) {
38
38
  this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
39
- throw err;
39
+ throw error;
40
40
  }
41
41
  }
42
42
  }
@@ -44,9 +44,9 @@ class IORedisStreamService extends index_1.StreamService {
44
44
  try {
45
45
  return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
46
46
  }
47
- catch (err) {
48
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
49
- throw err;
47
+ catch (error) {
48
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
49
+ throw error;
50
50
  }
51
51
  }
52
52
  async xreadgroup(command, groupName, consumerName, blockOption, blockTime, streamsOption, streamName, id) {
@@ -56,9 +56,9 @@ class IORedisStreamService extends index_1.StreamService {
56
56
  // @ts-ignore
57
57
  blockOption, blockTime, streamsOption, streamName, id);
58
58
  }
59
- catch (err) {
60
- this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, err);
61
- throw err;
59
+ catch (error) {
60
+ this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, { error });
61
+ throw error;
62
62
  }
63
63
  }
64
64
  async xpending(key, group, start, end, count, consumer) {
@@ -75,40 +75,49 @@ class IORedisStreamService extends index_1.StreamService {
75
75
  try {
76
76
  return await this.redisClient.call('XPENDING', ...args);
77
77
  }
78
- catch (err) {
79
- this.logger.error('err, args', err, args);
78
+ catch (error) {
79
+ this.logger.error('err, args', { error }, args);
80
80
  }
81
81
  }
82
- catch (err) {
83
- this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, err);
84
- throw err;
82
+ catch (error) {
83
+ this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
84
+ throw error;
85
85
  }
86
86
  }
87
87
  async xclaim(key, group, consumer, minIdleTime, id, ...args) {
88
88
  try {
89
89
  return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args);
90
90
  }
91
- catch (err) {
92
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
93
- throw err;
91
+ catch (error) {
92
+ this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
93
+ throw error;
94
94
  }
95
95
  }
96
96
  async xack(key, group, id, multi) {
97
97
  try {
98
98
  return await (multi || this.redisClient).xack(key, group, id);
99
99
  }
100
- catch (err) {
101
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
102
- throw err;
100
+ catch (error) {
101
+ this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
102
+ throw error;
103
103
  }
104
104
  }
105
105
  async xdel(key, id, multi) {
106
106
  try {
107
107
  return await (multi || this.redisClient).xdel(key, id);
108
108
  }
109
- catch (err) {
110
- this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, err);
111
- throw err;
109
+ catch (error) {
110
+ this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
111
+ throw error;
112
+ }
113
+ }
114
+ async xlen(key, multi) {
115
+ try {
116
+ return await (multi || this.redisClient).xlen(key);
117
+ }
118
+ catch (error) {
119
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
120
+ throw error;
112
121
  }
113
122
  }
114
123
  }
@@ -19,5 +19,6 @@ declare class RedisStreamService extends StreamService<RedisClientType, RedisMul
19
19
  xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
20
20
  xack(key: string, group: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
21
21
  xdel(key: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
+ xlen(key: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
23
  }
23
24
  export { RedisStreamService };
@@ -115,5 +115,20 @@ class RedisStreamService extends index_1.StreamService {
115
115
  throw err;
116
116
  }
117
117
  }
118
+ async xlen(key, multi) {
119
+ try {
120
+ if (multi) {
121
+ multi.XLEN(key);
122
+ return multi;
123
+ }
124
+ else {
125
+ return await this.redisClient.XLEN(key);
126
+ }
127
+ }
128
+ catch (error) {
129
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
130
+ throw error;
131
+ }
132
+ }
118
133
  }
119
134
  exports.RedisStreamService = RedisStreamService;
@@ -17,5 +17,6 @@ declare abstract class StreamService<T, U> {
17
17
  abstract xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
18
18
  abstract xack(key: string, group: string, id: string, multi?: U): Promise<number | U>;
19
19
  abstract xdel(key: string, id: string, multi?: U): Promise<number | U>;
20
+ abstract xlen(key: string, multi?: U): Promise<number | U>;
20
21
  }
21
22
  export { StreamService };
@@ -25,6 +25,7 @@ declare class WorkerService {
25
25
  initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
26
26
  initRouter(worker: HotMeshWorker, logger: ILogger): Router;
27
27
  subscriptionHandler(): SubscriptionCallback;
28
+ sayPong(appId: string, guid: string, originator: string, details?: boolean): Promise<void>;
28
29
  throttle(delayInMillis: number): Promise<void>;
29
30
  }
30
31
  export { WorkerService };
@@ -96,8 +96,32 @@ class WorkerService {
96
96
  if (message.type === 'throttle') {
97
97
  self.throttle(message.throttle);
98
98
  }
99
+ else if (message.type === 'ping') {
100
+ self.sayPong(self.appId, self.guid, message.originator, message.details);
101
+ }
99
102
  };
100
103
  }
104
+ async sayPong(appId, guid, originator, details = false) {
105
+ let profile;
106
+ if (details) {
107
+ const params = {
108
+ appId: this.appId,
109
+ topic: this.topic,
110
+ };
111
+ profile = {
112
+ engine_id: this.guid,
113
+ namespace: this.namespace,
114
+ app_id: this.appId,
115
+ worker_topic: this.topic,
116
+ stream: this.stream.mintKey(key_1.KeyType.STREAMS, params),
117
+ };
118
+ }
119
+ this.store.publish(key_1.KeyType.QUORUM, {
120
+ type: 'pong',
121
+ guid, originator,
122
+ profile,
123
+ }, appId);
124
+ }
101
125
  async throttle(delayInMillis) {
102
126
  this.router.setThrottle(delayInMillis);
103
127
  }
@@ -1,7 +1,16 @@
1
1
  import { JobOutput } from "./job";
2
+ export interface QuorumProfile {
3
+ namespace: string;
4
+ app_id: string;
5
+ engine_id: string;
6
+ worker_topic?: string;
7
+ stream?: string;
8
+ stream_depth?: number;
9
+ }
2
10
  export interface PingMessage {
3
11
  type: 'ping';
4
12
  originator: string;
13
+ details?: boolean;
5
14
  }
6
15
  export interface WorkMessage {
7
16
  type: 'work';
@@ -13,8 +22,9 @@ export interface CronMessage {
13
22
  }
14
23
  export interface PongMessage {
15
24
  type: 'pong';
16
- originator: string;
17
25
  guid: string;
26
+ originator: string;
27
+ profile?: QuorumProfile;
18
28
  }
19
29
  export interface ActivateMessage {
20
30
  type: 'activate';
@@ -4,6 +4,7 @@ interface RedisMultiType {
4
4
  XADD(key: string, id: string, fields: any): this;
5
5
  XACK(key: string, group: string, id: string): this;
6
6
  XDEL(key: string, id: string): this;
7
+ XLEN(key: string): this;
7
8
  HDEL(key: string, itemId: string): this;
8
9
  HGET(key: string, itemId: string): this;
9
10
  HGETALL(key: string): this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  HotMeshConfig,
18
18
  HotMeshManifest } from '../../types/hotmesh';
19
- import { JobMessageCallback } from '../../types/quorum';
19
+ import { JobMessageCallback, QuorumProfile } from '../../types/quorum';
20
20
  import {
21
21
  JobStatsInput,
22
22
  GetStatsOptions,
@@ -137,6 +137,9 @@ class HotMeshService {
137
137
  }
138
138
 
139
139
  // ************* COMPILER METHODS *************
140
+ async rollCall(delay?: number): Promise<QuorumProfile[]> {
141
+ return await this.quorum?.rollCall(delay);
142
+ }
140
143
  async plan(path: string): Promise<HotMeshManifest> {
141
144
  return await this.engine?.plan(path);
142
145
  }
@@ -147,10 +150,6 @@ class HotMeshService {
147
150
  //activation is a quorum operation
148
151
  return await this.quorum?.activate(version, delay);
149
152
  }
150
- async inventory(version: string, delay?: number): Promise<number> {
151
- //get count of all peers
152
- return await this.quorum?.inventory(delay);
153
- }
154
153
 
155
154
  // ************* REPORTER METHODS *************
156
155
  async getStats(topic: string, query: JobStatsInput): Promise<StatsResponse> {
@@ -14,6 +14,7 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
14
14
  import {
15
15
  QuorumMessage,
16
16
  QuorumMessageCallback,
17
+ QuorumProfile,
17
18
  SubscriptionCallback,
18
19
  ThrottleMessage
19
20
  } from '../../types/quorum';
@@ -26,10 +27,10 @@ const QUORUM_DELAY = 250;
26
27
 
27
28
  class QuorumService {
28
29
  namespace: string;
29
- apps: HotMeshApps | null;
30
30
  appId: string;
31
31
  guid: string;
32
32
  engine: EngineService;
33
+ profiles: QuorumProfile[] = [];
33
34
  store: StoreService<RedisClient, RedisMulti> | null;
34
35
  subscribe: SubService<RedisClient, RedisMulti> | null;
35
36
  logger: ILogger;
@@ -108,9 +109,12 @@ class QuorumService {
108
109
  if (message.type === 'activate') {
109
110
  self.engine.setCacheMode(message.cache_mode, message.until_version);
110
111
  } else if (message.type === 'ping') {
111
- this.sayPong(self.appId, self.guid, message.originator);
112
+ self.sayPong(self.appId, self.guid, message.originator, message.details);
112
113
  } else if (message.type === 'pong' && self.guid === message.originator) {
113
114
  self.quorum = self.quorum + 1;
115
+ if (message.profile) {
116
+ self.profiles.push(message.profile);
117
+ }
114
118
  } else if (message.type === 'throttle') {
115
119
  self.engine.throttle(message.throttle);
116
120
  } else if (message.type === 'work') {
@@ -127,20 +131,38 @@ class QuorumService {
127
131
  };
128
132
  }
129
133
 
130
- async sayPong(appId: string, guid: string, originator: string) {
134
+ async sayPong(appId: string, guid: string, originator: string, details = false) {
135
+ let profile: QuorumProfile;
136
+ if (details) {
137
+ profile = {
138
+ engine_id: this.guid,
139
+ namespace: this.namespace,
140
+ app_id: this.appId,
141
+ stream: this.engine.stream.mintKey(KeyType.STREAMS, { appId: this.appId })
142
+ };
143
+ }
131
144
  this.store.publish(
132
145
  KeyType.QUORUM,
133
- { type: 'pong', guid, originator },
146
+ {
147
+ type: 'pong',
148
+ guid, originator,
149
+ profile,
150
+ },
134
151
  appId,
135
152
  );
136
153
  }
137
154
 
138
- async requestQuorum(delay = QUORUM_DELAY): Promise<number> {
155
+ async requestQuorum(delay = QUORUM_DELAY, details = false): Promise<number> {
139
156
  const quorum = this.quorum;
140
157
  this.quorum = 0;
158
+ this.profiles.length = 0;
141
159
  await this.store.publish(
142
160
  KeyType.QUORUM,
143
- { type: 'ping', originator: this.guid },
161
+ {
162
+ type: 'ping',
163
+ originator: this.guid,
164
+ details,
165
+ },
144
166
  this.appId,
145
167
  );
146
168
  await sleepFor(delay);
@@ -166,12 +188,26 @@ class QuorumService {
166
188
 
167
189
 
168
190
  // ************* COMPILER METHODS *************
169
- async inventory(delay = QUORUM_DELAY): Promise<number> {
170
- await this.requestQuorum(delay);
171
- const q1 = await this.requestQuorum(delay);
172
- const q2 = await this.requestQuorum(delay);
173
- const q3 = await this.requestQuorum(delay);
174
- return Math.round((q1 + q2 + q3) / 3);
191
+ async rollCall(delay = QUORUM_DELAY): Promise<QuorumProfile[]> {
192
+ await this.requestQuorum(delay, true);
193
+ const targetStreams = [];
194
+ const multi = this.store.getMulti();
195
+ this.profiles.forEach((profile: QuorumProfile) => {
196
+ if (!targetStreams.includes(profile.stream)) {
197
+ targetStreams.push(profile.stream);
198
+ this.store.xlen(profile.stream, multi);
199
+ }
200
+ });
201
+ const stream_depths = await multi.exec() as number[];
202
+ this.profiles.forEach(async (profile: QuorumProfile) => {
203
+ const index = targetStreams.indexOf(profile.stream);
204
+ if (index != -1) {
205
+ profile.stream_depth = Array.isArray(stream_depths[index]) ?
206
+ stream_depths[index][1] :
207
+ stream_depths[index];
208
+ }
209
+ });
210
+ return this.profiles;
175
211
  }
176
212
  async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
177
213
  version = version.toString();
@@ -131,6 +131,15 @@ class IORedisStoreService extends StoreService<RedisClientType, RedisMultiType>
131
131
  throw error;
132
132
  }
133
133
  }
134
+
135
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
136
+ try {
137
+ return await (multi || this.redisClient).xlen(key);
138
+ } catch (error) {
139
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
140
+ throw error;
141
+ }
142
+ }
134
143
  }
135
144
 
136
145
  export { IORedisStoreService };
@@ -42,6 +42,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
42
42
  rpush: 'RPUSH',
43
43
  xack: 'XACK',
44
44
  xdel: 'XDEL',
45
+ xlen: 'XLEN',
45
46
  };
46
47
  }
47
48
 
@@ -167,6 +168,21 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
167
168
  throw error;
168
169
  }
169
170
  }
171
+
172
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
173
+ try {
174
+ if (multi) {
175
+ multi.XLEN(key);
176
+ return multi;
177
+ } else {
178
+ return await this.redisClient.XLEN(key);
179
+ }
180
+ } catch (error) {
181
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
182
+ throw error;
183
+ }
184
+ }
185
+
170
186
  }
171
187
 
172
188
  export { RedisStoreService };
@@ -118,6 +118,10 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
118
118
  id: string,
119
119
  multi?: U
120
120
  ): Promise<number|U>;
121
+ abstract xlen(
122
+ key: string,
123
+ multi?: U
124
+ ): Promise<number|U>;
121
125
 
122
126
  constructor(redisClient: T) {
123
127
  this.redisClient = redisClient;
@@ -33,16 +33,16 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
33
33
  if (mkStream === 'MKSTREAM') {
34
34
  try {
35
35
  return (await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK';
36
- } catch (err) {
36
+ } catch (error) {
37
37
  this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
38
- throw err;
38
+ throw error;
39
39
  }
40
40
  } else {
41
41
  try {
42
42
  return (await this.redisClient.xgroup(command, key, groupName, id)) === 'OK';
43
- } catch (err) {
43
+ } catch (error) {
44
44
  this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
45
- throw err;
45
+ throw error;
46
46
  }
47
47
  }
48
48
  }
@@ -50,9 +50,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
50
50
  async xadd(key: string, id: string, messageId: string, messageValue: string, multi?: RedisMultiType): Promise<string | RedisMultiType> {
51
51
  try {
52
52
  return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
53
- } catch (err) {
54
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
55
- throw err;
53
+ } catch (error) {
54
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
55
+ throw error;
56
56
  }
57
57
  }
58
58
 
@@ -79,9 +79,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
79
79
  streamName,
80
80
  id
81
81
  );
82
- } catch (err) {
83
- this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, err);
84
- throw err;
82
+ } catch (error) {
83
+ this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, { error });
84
+ throw error;
85
85
  }
86
86
  }
87
87
 
@@ -101,12 +101,12 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
101
101
  if (consumer) args.push(consumer);
102
102
  try {
103
103
  return await this.redisClient.call('XPENDING', ...args) as [string, string, number, number][];
104
- } catch (err) {
105
- this.logger.error('err, args', err, args);
104
+ } catch (error) {
105
+ this.logger.error('err, args', { error }, args);
106
106
  }
107
- } catch (err) {
108
- this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, err);
109
- throw err;
107
+ } catch (error) {
108
+ this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
109
+ throw error;
110
110
  }
111
111
  }
112
112
 
@@ -120,27 +120,36 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
120
120
  ): Promise<ReclaimedMessageType> {
121
121
  try {
122
122
  return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args) as unknown as ReclaimedMessageType;
123
- } catch (err) {
124
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
125
- throw err;
123
+ } catch (error) {
124
+ this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
125
+ throw error;
126
126
  }
127
127
  }
128
128
 
129
129
  async xack(key: string, group: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
130
130
  try {
131
131
  return await (multi || this.redisClient).xack(key, group, id);
132
- } catch (err) {
133
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
134
- throw err;
132
+ } catch (error) {
133
+ this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
134
+ throw error;
135
135
  }
136
136
  }
137
137
 
138
138
  async xdel(key: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
139
139
  try {
140
140
  return await (multi || this.redisClient).xdel(key, id);
141
- } catch (err) {
142
- this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, err);
143
- throw err;
141
+ } catch (error) {
142
+ this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
148
+ try {
149
+ return await (multi || this.redisClient).xlen(key);
150
+ } catch (error) {
151
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
152
+ throw error;
144
153
  }
145
154
  }
146
155
  }
@@ -139,6 +139,20 @@ class RedisStreamService extends StreamService<RedisClientType, RedisMultiType>
139
139
  throw err;
140
140
  }
141
141
  }
142
+
143
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
144
+ try {
145
+ if (multi) {
146
+ multi.XLEN(key);
147
+ return multi;
148
+ } else {
149
+ return await this.redisClient.XLEN(key);
150
+ }
151
+ } catch (error) {
152
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
153
+ throw error;
154
+ }
155
+ }
142
156
  }
143
157
 
144
158
  export { RedisStreamService };
@@ -52,6 +52,7 @@ abstract class StreamService<T, U> {
52
52
  ...args: string[]): Promise<ReclaimedMessageType>;
53
53
  abstract xack(key: string, group: string, id: string, multi?: U): Promise<number|U>;
54
54
  abstract xdel(key: string, id: string, multi?: U): Promise<number|U>;
55
+ abstract xlen(key: string, multi?: U): Promise<number|U>;
55
56
  }
56
57
 
57
58
  export { StreamService };
@@ -14,6 +14,7 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
14
14
  import { HotMeshConfig, HotMeshWorker } from "../../types/hotmesh";
15
15
  import {
16
16
  QuorumMessage,
17
+ QuorumProfile,
17
18
  SubscriptionCallback } from "../../types/quorum";
18
19
  import { RedisClient, RedisMulti } from "../../types/redis";
19
20
  import { RedisClientType } from '../../types/redisclient';
@@ -153,10 +154,39 @@ class WorkerService {
153
154
  self.logger.debug('worker-event-received', { topic, type: message.type });
154
155
  if (message.type === 'throttle') {
155
156
  self.throttle(message.throttle);
157
+ } else if(message.type === 'ping') {
158
+ self.sayPong(self.appId, self.guid, message.originator, message.details);
156
159
  }
157
160
  };
158
161
  }
159
162
 
163
+ async sayPong(appId: string, guid: string, originator: string, details = false) {
164
+ let profile: QuorumProfile;
165
+ if (details) {
166
+ const params = {
167
+ appId: this.appId,
168
+ topic: this.topic,
169
+ };
170
+
171
+ profile = {
172
+ engine_id: this.guid,
173
+ namespace: this.namespace,
174
+ app_id: this.appId,
175
+ worker_topic: this.topic,
176
+ stream: this.stream.mintKey(KeyType.STREAMS, params),
177
+ };
178
+ }
179
+ this.store.publish(
180
+ KeyType.QUORUM,
181
+ {
182
+ type: 'pong',
183
+ guid, originator,
184
+ profile,
185
+ },
186
+ appId,
187
+ );
188
+ }
189
+
160
190
  async throttle(delayInMillis: number) {
161
191
  this.router.setThrottle(delayInMillis);
162
192
  }
package/types/quorum.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import { JobOutput } from "./job";
2
2
 
3
- //used for coordination like version activation
3
+ //used for coordination (like version activation)
4
+
5
+ export interface QuorumProfile {
6
+ namespace: string;
7
+ app_id: string;
8
+ engine_id: string;
9
+ worker_topic?: string;
10
+ stream?: string;
11
+ stream_depth?: number;
12
+ }
13
+
4
14
  export interface PingMessage {
5
15
  type: 'ping';
6
16
  originator: string; //guid
17
+ details?: boolean; //if true, all endpoints will include their profile
7
18
  }
8
19
 
9
20
  export interface WorkMessage {
@@ -16,11 +27,11 @@ export interface CronMessage {
16
27
  originator: string; //guid
17
28
  }
18
29
 
19
- //used for coordination like version activation
20
30
  export interface PongMessage {
21
31
  type: 'pong';
22
- originator: string; //clone of originator guid passed in ping
23
- guid: string;
32
+ guid: string; //call initiator
33
+ originator: string; //clone of originator guid passed in ping
34
+ profile?: QuorumProfile; //contains details about the engine/worker
24
35
  }
25
36
 
26
37
  export interface ActivateMessage {
@@ -6,6 +6,7 @@ interface RedisMultiType {
6
6
  XADD(key: string, id: string, fields: any): this;
7
7
  XACK(key: string, group: string, id: string): this;
8
8
  XDEL(key: string, id: string): this;
9
+ XLEN(key: string): this;
9
10
  HDEL(key: string, itemId: string): this;
10
11
  HGET(key: string, itemId: string): this;
11
12
  HGETALL(key: string): this;