@hotmeshio/hotmesh 0.0.36 → 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 +11 -11
- package/build/modules/enums.d.ts +1 -0
- package/build/modules/enums.js +3 -1
- package/build/modules/errors.d.ts +9 -1
- package/build/modules/errors.js +12 -1
- package/build/modules/key.d.ts +20 -19
- package/build/modules/key.js +20 -20
- package/build/package.json +1 -1
- package/build/services/activities/activity.d.ts +10 -0
- package/build/services/activities/activity.js +28 -3
- package/build/services/activities/await.js +10 -9
- package/build/services/activities/cycle.js +10 -9
- package/build/services/activities/hook.d.ts +7 -1
- package/build/services/activities/hook.js +61 -44
- package/build/services/activities/interrupt.js +10 -9
- package/build/services/activities/signal.js +7 -7
- package/build/services/activities/trigger.js +4 -2
- package/build/services/activities/worker.js +9 -8
- package/build/services/durable/meshos.js +2 -2
- package/build/services/durable/worker.js +2 -2
- package/build/services/durable/workflow.js +17 -17
- package/build/services/engine/index.d.ts +5 -7
- package/build/services/engine/index.js +53 -47
- package/build/services/hotmesh/index.d.ts +2 -2
- package/build/services/hotmesh/index.js +6 -7
- package/build/services/quorum/index.d.ts +6 -6
- package/build/services/quorum/index.js +47 -11
- package/build/services/{signaler/stream.d.ts → router/index.d.ts} +3 -3
- package/build/services/{signaler/stream.js → router/index.js} +6 -6
- package/build/services/serializer/index.js +1 -1
- package/build/services/store/clients/ioredis.d.ts +1 -0
- package/build/services/store/clients/ioredis.js +9 -0
- package/build/services/store/clients/redis.d.ts +1 -0
- package/build/services/store/clients/redis.js +16 -0
- package/build/services/store/index.d.ts +10 -4
- package/build/services/store/index.js +21 -10
- package/build/services/stream/clients/ioredis.d.ts +1 -0
- package/build/services/stream/clients/ioredis.js +33 -24
- package/build/services/stream/clients/redis.d.ts +1 -0
- package/build/services/stream/clients/redis.js +15 -0
- package/build/services/stream/index.d.ts +1 -0
- package/build/services/task/index.d.ts +13 -4
- package/build/services/task/index.js +115 -17
- package/build/services/telemetry/index.js +6 -6
- package/build/services/worker/index.d.ts +4 -3
- package/build/services/worker/index.js +32 -8
- package/build/types/job.d.ts +2 -0
- package/build/types/quorum.d.ts +11 -1
- package/build/types/redisclient.d.ts +1 -0
- package/build/types/stream.d.ts +1 -0
- package/modules/enums.ts +3 -0
- package/modules/errors.ts +18 -0
- package/modules/key.ts +21 -20
- package/package.json +1 -1
- package/services/activities/activity.ts +44 -4
- package/services/activities/await.ts +14 -10
- package/services/activities/cycle.ts +14 -10
- package/services/activities/hook.ts +70 -47
- package/services/activities/interrupt.ts +13 -10
- package/services/activities/signal.ts +11 -8
- package/services/activities/trigger.ts +5 -1
- package/services/activities/worker.ts +13 -9
- package/services/durable/meshos.ts +1 -1
- package/services/durable/worker.ts +1 -1
- package/services/durable/workflow.ts +1 -1
- package/services/engine/index.ts +82 -44
- package/services/hotmesh/index.ts +7 -8
- package/services/quorum/index.ts +48 -12
- package/services/{signaler/stream.ts → router/index.ts} +5 -5
- package/services/serializer/index.ts +1 -1
- package/services/store/clients/ioredis.ts +9 -0
- package/services/store/clients/redis.ts +16 -0
- package/services/store/index.ts +27 -12
- package/services/stream/clients/ioredis.ts +33 -24
- package/services/stream/clients/redis.ts +14 -0
- package/services/stream/index.ts +1 -0
- package/services/task/index.ts +120 -21
- package/services/telemetry/index.ts +6 -6
- package/services/worker/index.ts +37 -7
- package/types/job.ts +2 -0
- package/types/quorum.ts +15 -4
- package/types/redisclient.ts +1 -0
- package/types/stream.ts +6 -5
- package/build/services/signaler/store.d.ts +0 -15
- package/build/services/signaler/store.js +0 -68
- package/services/signaler/store.ts +0 -76
- /package/build/{services/durable/asyncLocalStorage.d.ts → modules/storage.d.ts} +0 -0
- /package/build/{services/durable/asyncLocalStorage.js → modules/storage.js} +0 -0
- /package/{services/durable/asyncLocalStorage.ts → modules/storage.ts} +0 -0
package/services/store/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ import { Transitions } from '../../types/transition';
|
|
|
29
29
|
import { formatISODate, getSymKey } from '../../modules/utils';
|
|
30
30
|
import { ReclaimedMessageType } from '../../types/stream';
|
|
31
31
|
import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
|
|
32
|
-
import { STATUS_CODE_INTERRUPT } from '../../modules/enums';
|
|
32
|
+
import { SCOUT_INTERVAL_SECONDS, STATUS_CODE_INTERRUPT } from '../../modules/enums';
|
|
33
33
|
import { GetStateError } from '../../modules/errors';
|
|
34
34
|
|
|
35
35
|
interface AbstractRedisClient {
|
|
@@ -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;
|
|
@@ -168,10 +172,19 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
168
172
|
this.cache.invalidate();
|
|
169
173
|
}
|
|
170
174
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
/**
|
|
176
|
+
* At any given time only a single engine will
|
|
177
|
+
* check for and process work items in the
|
|
178
|
+
* time and signal task queues.
|
|
179
|
+
*/
|
|
180
|
+
async reserveScoutRole(scoutType: 'time' | 'signal', delay = SCOUT_INTERVAL_SECONDS): Promise<boolean> {
|
|
181
|
+
const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
|
|
182
|
+
const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
|
|
183
|
+
if (this.isSuccessful(success)) {
|
|
184
|
+
await this.redisClient[this.commands.expire](key, delay - 1);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
async getSettings(bCreate = false): Promise<HotMeshSettings> {
|
|
@@ -380,11 +393,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
380
393
|
* list (added via RPUSH) are LPOPed. If origin was expired, then
|
|
381
394
|
* LPOPed items from the list are likewise expired;
|
|
382
395
|
*/
|
|
383
|
-
async setDependency(originJobId: string, topic: string, jobId: string, multi? : U): Promise<any> {
|
|
396
|
+
async setDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
|
|
384
397
|
const privateMulti = multi || this.getMulti();
|
|
385
398
|
const depParams = { appId: this.appId, jobId: originJobId };
|
|
386
399
|
const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
|
|
387
|
-
privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
|
|
400
|
+
privateMulti[this.commands.rpush](depKey, `expire::${topic}::${gId}::${jobId}`);
|
|
388
401
|
if (!multi) {
|
|
389
402
|
return await privateMulti.exec();
|
|
390
403
|
}
|
|
@@ -784,9 +797,9 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
784
797
|
* for the given sleep group. Sleep groups are
|
|
785
798
|
* organized into 'n'-second blocks (LISTS))
|
|
786
799
|
*/
|
|
787
|
-
async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
|
|
800
|
+
async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
|
|
788
801
|
const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
|
|
789
|
-
const timeEvent = `${type}::${activityId}::${jobId}
|
|
802
|
+
const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
|
|
790
803
|
const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
|
|
791
804
|
if (multi || len === 1) {
|
|
792
805
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
@@ -794,7 +807,8 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
794
807
|
}
|
|
795
808
|
}
|
|
796
809
|
|
|
797
|
-
async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] |
|
|
810
|
+
async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | boolean> {
|
|
811
|
+
const existing = Boolean(listKey);
|
|
798
812
|
const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
|
|
799
813
|
listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
|
|
800
814
|
if (listKey) {
|
|
@@ -802,11 +816,12 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
|
|
|
802
816
|
const timeEvent = await this.redisClient[this.commands.lpop](pKey);
|
|
803
817
|
if (timeEvent) {
|
|
804
818
|
//there are 3 time-related event triggers: sleep, expire, interrupt
|
|
805
|
-
const [_type, activityId, ...jobId] = timeEvent.split('::');
|
|
806
|
-
return [listKey, jobId.join('::'), activityId, pType];
|
|
819
|
+
const [_type, activityId, gId, ...jobId] = timeEvent.split('::');
|
|
820
|
+
return [listKey, jobId.join('::'), gId, activityId, pType];
|
|
807
821
|
}
|
|
808
822
|
await this.redisClient[this.commands.zrem](zsetKey, listKey);
|
|
809
823
|
}
|
|
824
|
+
return existing;
|
|
810
825
|
}
|
|
811
826
|
|
|
812
827
|
/**
|
|
@@ -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 (
|
|
36
|
+
} catch (error) {
|
|
37
37
|
this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
|
|
38
|
-
throw
|
|
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 (
|
|
43
|
+
} catch (error) {
|
|
44
44
|
this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
|
|
45
|
-
throw
|
|
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 (
|
|
54
|
-
this.logger.error(`Error publishing 'xadd'; key: ${key}`,
|
|
55
|
-
throw
|
|
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 (
|
|
83
|
-
this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`,
|
|
84
|
-
throw
|
|
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 (
|
|
105
|
-
this.logger.error('err, args',
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.logger.error('err, args', { error }, args);
|
|
106
106
|
}
|
|
107
|
-
} catch (
|
|
108
|
-
this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`,
|
|
109
|
-
throw
|
|
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 (
|
|
124
|
-
this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`,
|
|
125
|
-
throw
|
|
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 (
|
|
133
|
-
this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`,
|
|
134
|
-
throw
|
|
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 (
|
|
142
|
-
this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`,
|
|
143
|
-
throw
|
|
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 };
|
package/services/stream/index.ts
CHANGED
|
@@ -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 };
|
package/services/task/index.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EXPIRE_DURATION,
|
|
3
|
-
FIDELITY_SECONDS
|
|
3
|
+
FIDELITY_SECONDS,
|
|
4
|
+
SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
|
|
4
5
|
import { XSleepFor, sleepFor } from '../../modules/utils';
|
|
5
6
|
import { ILogger } from '../logger';
|
|
6
7
|
import { StoreService } from '../store';
|
|
7
|
-
import { HookInterface } from '../../types/hook';
|
|
8
|
-
import { JobCompletionOptions } from '../../types/job';
|
|
8
|
+
import { HookInterface, HookRule, HookSignal } from '../../types/hook';
|
|
9
|
+
import { JobCompletionOptions, JobState } from '../../types/job';
|
|
9
10
|
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
11
|
+
import { Pipe } from '../pipe';
|
|
10
12
|
|
|
11
13
|
class TaskService {
|
|
12
14
|
store: StoreService<RedisClient, RedisMulti>;
|
|
13
15
|
logger: ILogger;
|
|
14
16
|
cleanupTimeout: NodeJS.Timeout | null = null;
|
|
17
|
+
isScout: boolean = false;
|
|
15
18
|
|
|
16
19
|
constructor(
|
|
17
20
|
store: StoreService<RedisClient, RedisMulti>,
|
|
@@ -50,28 +53,59 @@ class TaskService {
|
|
|
50
53
|
}
|
|
51
54
|
}
|
|
52
55
|
|
|
53
|
-
async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
|
|
56
|
+
async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
|
|
54
57
|
const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
|
|
55
|
-
await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
|
|
58
|
+
await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Should this engine instance play the role of 'scout' for the quorum.
|
|
63
|
+
*/
|
|
64
|
+
async shouldScout() {
|
|
65
|
+
const wasScout = this.isScout;
|
|
66
|
+
const isScout = wasScout || (this.isScout = await this.store.reserveScoutRole('time'));
|
|
67
|
+
if (isScout) {
|
|
68
|
+
if (!wasScout) {
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
this.isScout = false;
|
|
71
|
+
}, SCOUT_INTERVAL_SECONDS * 1_000);
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
|
|
79
|
+
if (await this.shouldScout()) {
|
|
80
|
+
try {
|
|
81
|
+
const timeJob = await this.store.getNextTimeJob(listKey);
|
|
82
|
+
if (Array.isArray(timeJob)) {
|
|
83
|
+
//a queue had a job; try again immediately
|
|
84
|
+
const [listKey, jobId, gId, activityId, type] = timeJob;
|
|
85
|
+
await timeEventCallback(jobId, gId, activityId, type);
|
|
86
|
+
await sleepFor(0);
|
|
87
|
+
this.processTimeHooks(timeEventCallback, listKey);
|
|
88
|
+
} else if (timeJob) {
|
|
89
|
+
//a queue was just emptied; try again immediately
|
|
90
|
+
await sleepFor(0);
|
|
91
|
+
this.processTimeHooks(timeEventCallback);
|
|
92
|
+
} else {
|
|
93
|
+
//all queues are empty; sleep before checking
|
|
94
|
+
let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
|
|
95
|
+
this.cleanupTimeout = sleep.timerId;
|
|
96
|
+
await sleep.promise;
|
|
97
|
+
this.processTimeHooks(timeEventCallback);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
//todo: retry connect to redis
|
|
101
|
+
this.logger.error('task-process-timehooks-error', err);
|
|
71
102
|
}
|
|
72
|
-
}
|
|
73
|
-
//
|
|
74
|
-
|
|
103
|
+
} else {
|
|
104
|
+
//didn't get the scout role; try again in 'one-ish' minutes
|
|
105
|
+
let sleep = XSleepFor(SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
|
|
106
|
+
this.cleanupTimeout = sleep.timerId;
|
|
107
|
+
await sleep.promise;
|
|
108
|
+
this.processTimeHooks(timeEventCallback);
|
|
75
109
|
}
|
|
76
110
|
}
|
|
77
111
|
|
|
@@ -81,6 +115,71 @@ class TaskService {
|
|
|
81
115
|
this.cleanupTimeout = undefined;
|
|
82
116
|
}
|
|
83
117
|
}
|
|
118
|
+
|
|
119
|
+
async getHookRule(topic: string): Promise<HookRule | undefined> {
|
|
120
|
+
const rules = await this.store.getHookRules();
|
|
121
|
+
return rules?.[topic]?.[0] as HookRule;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string> {
|
|
125
|
+
const hookRule = await this.getHookRule(topic);
|
|
126
|
+
if (hookRule) {
|
|
127
|
+
const mapExpression = hookRule.conditions.match[0].expected;
|
|
128
|
+
const resolved = Pipe.resolve(mapExpression, context);
|
|
129
|
+
const jobId = context.metadata.jid;
|
|
130
|
+
const gId = context.metadata.gid;
|
|
131
|
+
const activityId = hookRule.to;
|
|
132
|
+
const hook: HookSignal = {
|
|
133
|
+
topic,
|
|
134
|
+
resolved,
|
|
135
|
+
jobId: `${activityId}::${dad}::${gId}::${jobId}`,
|
|
136
|
+
}
|
|
137
|
+
await this.store.setHookSignal(hook, multi);
|
|
138
|
+
return jobId;
|
|
139
|
+
} else {
|
|
140
|
+
throw new Error('signaler.registerWebHook:error: hook rule not found');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string, string] | undefined> {
|
|
145
|
+
const hookRule = await this.getHookRule(topic);
|
|
146
|
+
if (hookRule) {
|
|
147
|
+
//NOTE: both formats are supported by the mapping engine:
|
|
148
|
+
// `$self.hook.data` OR `$hook.data`
|
|
149
|
+
const context = { $self: { hook: { data }}, $hook: { data }};
|
|
150
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
151
|
+
const resolved = Pipe.resolve(mapExpression, context);
|
|
152
|
+
const hookSignalId = await this.store.getHookSignal(topic, resolved);
|
|
153
|
+
if (!hookSignalId) {
|
|
154
|
+
//messages can be double-processed; not an issue; return undefined
|
|
155
|
+
//users can also provide a bogus topic; not an issue; return undefined
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
//`aid` is part of composit key, but the hook `topic` is its public interface;
|
|
159
|
+
// this means that a new version of the graph can be deployed and the
|
|
160
|
+
// topic can be re-mapped to a different activity id. Outside callers
|
|
161
|
+
// can adhere to the unchanged contract (calling the same topic),
|
|
162
|
+
// while the internal system can be updated in real time as necessary.
|
|
163
|
+
const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
|
|
164
|
+
return [jid.join('::'), hookRule.to, dad, gid];
|
|
165
|
+
} else {
|
|
166
|
+
throw new Error('signal-not-found');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number> {
|
|
171
|
+
const hookRule = await this.getHookRule(topic);
|
|
172
|
+
if (hookRule) {
|
|
173
|
+
//NOTE: both formats are supported by the mapping engine:
|
|
174
|
+
// `$self.hook.data` OR `$hook.data`
|
|
175
|
+
const context = { $self: { hook: { data }}, $hook: { data }};
|
|
176
|
+
const mapExpression = hookRule.conditions.match[0].actual;
|
|
177
|
+
const resolved = Pipe.resolve(mapExpression, context);
|
|
178
|
+
return await this.store.deleteHookSignal(topic, resolved);
|
|
179
|
+
} else {
|
|
180
|
+
throw new Error('signaler.process:error: hook rule not found');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
84
183
|
}
|
|
85
184
|
|
|
86
185
|
export { TaskService };
|
|
@@ -126,20 +126,20 @@ class TelemetryService {
|
|
|
126
126
|
return result;
|
|
127
127
|
}, {})
|
|
128
128
|
};
|
|
129
|
-
this.span
|
|
129
|
+
this.span?.setAttributes(namespacedAtts as StringScalarType);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
setActivityAttributes(attributes: StringScalarType): void {
|
|
134
|
-
this.span
|
|
134
|
+
this.span?.setAttributes(attributes);
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
setStreamAttributes(attributes: StringScalarType): void {
|
|
138
|
-
this.span
|
|
138
|
+
this.span?.setAttributes(attributes);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
setJobAttributes(attributes: StringScalarType): void {
|
|
142
|
-
this.jobSpan
|
|
142
|
+
this.jobSpan?.setAttributes(attributes);
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
endJobSpan(): void {
|
|
@@ -216,11 +216,11 @@ class TelemetryService {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
setActivityError(message: string) {
|
|
219
|
-
this.span
|
|
219
|
+
this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
setStreamError(message: string) {
|
|
223
|
-
this.span
|
|
223
|
+
this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|
package/services/worker/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KeyType } from "../../modules/key";
|
|
2
2
|
import { ILogger } from "../logger";
|
|
3
|
-
import {
|
|
3
|
+
import { Router } from "../router";
|
|
4
4
|
import { StoreService } from '../store';
|
|
5
5
|
import { RedisStoreService as RedisStore } from '../store/clients/redis';
|
|
6
6
|
import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
|
|
@@ -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';
|
|
@@ -30,7 +31,7 @@ class WorkerService {
|
|
|
30
31
|
store: StoreService<RedisClient, RedisMulti> | null;
|
|
31
32
|
stream: StreamService<RedisClient, RedisMulti> | null;
|
|
32
33
|
subscribe: SubService<RedisClient, RedisMulti> | null;
|
|
33
|
-
|
|
34
|
+
router: Router | null;
|
|
34
35
|
logger: ILogger;
|
|
35
36
|
reporting = false;
|
|
36
37
|
|
|
@@ -66,10 +67,10 @@ class WorkerService {
|
|
|
66
67
|
await service.subscribe.subscribe(KeyType.QUORUM, service.subscriptionHandler(), appId, service.topic);
|
|
67
68
|
await service.subscribe.subscribe(KeyType.QUORUM, service.subscriptionHandler(), appId, service.guid);
|
|
68
69
|
await service.initStreamChannel(service, worker.stream);
|
|
69
|
-
service.
|
|
70
|
+
service.router = service.initRouter(worker, logger);
|
|
70
71
|
|
|
71
72
|
const key = service.stream.mintKey(KeyType.STREAMS, { appId: service.appId, topic: worker.topic });
|
|
72
|
-
await service.
|
|
73
|
+
await service.router.consumeMessages(
|
|
73
74
|
key,
|
|
74
75
|
'WORKER',
|
|
75
76
|
service.guid,
|
|
@@ -130,8 +131,8 @@ class WorkerService {
|
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
return new
|
|
134
|
+
initRouter(worker: HotMeshWorker, logger: ILogger): Router {
|
|
135
|
+
return new Router(
|
|
135
136
|
{
|
|
136
137
|
namespace: this.namespace,
|
|
137
138
|
appId: this.appId,
|
|
@@ -153,12 +154,41 @@ 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
|
-
this.
|
|
191
|
+
this.router.setThrottle(delayInMillis);
|
|
162
192
|
}
|
|
163
193
|
}
|
|
164
194
|
|
package/types/job.ts
CHANGED
|
@@ -8,10 +8,12 @@ type ActivityData = {
|
|
|
8
8
|
|
|
9
9
|
type JobMetadata = {
|
|
10
10
|
key?: string; //job_key
|
|
11
|
+
gid: string; //system assigned guid; ensured created/deleted/created jobs are unique
|
|
11
12
|
jid: string; //job_id (jid+dad+aid) is composite key for activity
|
|
12
13
|
dad: string; //dimensional address for the activity (,0,0,1)
|
|
13
14
|
aid: string; //activity_id as in the YAML file
|
|
14
15
|
pj?: string; //parent_job_id (pj+pd+pa) is composite key for parent activity
|
|
16
|
+
pg?: string; //parent_generational_id (system assigned at trigger inception); pg is the parent job's gid (just in case user created/deleted/created a job with same jid)
|
|
15
17
|
pd?: string; //parent_dimensional_address
|
|
16
18
|
pa?: string; //parent_activity_id
|
|
17
19
|
ngn?: string; //engine guid (one time subscriptions)
|
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
|
-
|
|
23
|
-
|
|
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 {
|
package/types/redisclient.ts
CHANGED
|
@@ -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;
|
package/types/stream.ts
CHANGED
|
@@ -35,12 +35,13 @@ export interface StreamData {
|
|
|
35
35
|
metadata: {
|
|
36
36
|
guid: string; //every message is minted with a guid to distinguish retries from new messages
|
|
37
37
|
topic?: string;
|
|
38
|
-
jid?: string;
|
|
39
|
-
|
|
38
|
+
jid?: string; //is optional if type is WEBHOOK (system assigned or user assigned)
|
|
39
|
+
gid?: string; //is optional if type is WEBHOOK (system assigned job guid)
|
|
40
|
+
dad?: string; //dimensional address
|
|
40
41
|
aid: string;
|
|
41
|
-
trc?: string;
|
|
42
|
-
spn?: string;
|
|
43
|
-
try?: number;
|
|
42
|
+
trc?: string; //trace id
|
|
43
|
+
spn?: string; //span id
|
|
44
|
+
try?: number; //current try count
|
|
44
45
|
};
|
|
45
46
|
type?: StreamDataType;
|
|
46
47
|
data: Record<string, unknown>;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { ILogger } from '../logger';
|
|
2
|
-
import { StoreService } from '../store';
|
|
3
|
-
import { HookRule } from '../../types/hook';
|
|
4
|
-
import { JobState } from '../../types/job';
|
|
5
|
-
import { RedisClient, RedisMulti } from '../../types/redis';
|
|
6
|
-
declare class StoreSignaler {
|
|
7
|
-
store: StoreService<RedisClient, RedisMulti>;
|
|
8
|
-
logger: ILogger;
|
|
9
|
-
constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
|
|
10
|
-
getHookRule(topic: string): Promise<HookRule | undefined>;
|
|
11
|
-
registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string>;
|
|
12
|
-
processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string] | undefined>;
|
|
13
|
-
deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number>;
|
|
14
|
-
}
|
|
15
|
-
export { StoreSignaler };
|