@hotmeshio/hotmesh 0.0.42 → 0.0.44

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.
Files changed (61) hide show
  1. package/build/modules/enums.d.ts +2 -0
  2. package/build/modules/enums.js +4 -1
  3. package/build/modules/utils.js +1 -1
  4. package/build/package.json +1 -1
  5. package/build/services/activities/trigger.js +7 -1
  6. package/build/services/durable/client.d.ts +2 -1
  7. package/build/services/durable/client.js +17 -3
  8. package/build/services/durable/exporter.d.ts +105 -0
  9. package/build/services/durable/exporter.js +374 -0
  10. package/build/services/durable/factory.js +6 -63
  11. package/build/services/durable/handle.d.ts +4 -0
  12. package/build/services/durable/handle.js +5 -0
  13. package/build/services/durable/meshos.js +3 -0
  14. package/build/services/durable/workflow.js +24 -21
  15. package/build/services/engine/index.d.ts +6 -1
  16. package/build/services/engine/index.js +9 -2
  17. package/build/services/exporter/index.d.ts +46 -0
  18. package/build/services/exporter/index.js +126 -0
  19. package/build/services/hotmesh/index.d.ts +4 -1
  20. package/build/services/hotmesh/index.js +6 -0
  21. package/build/services/quorum/index.d.ts +5 -2
  22. package/build/services/quorum/index.js +33 -15
  23. package/build/services/router/index.d.ts +3 -0
  24. package/build/services/router/index.js +3 -0
  25. package/build/services/store/clients/redis.js +1 -0
  26. package/build/services/store/index.d.ts +7 -3
  27. package/build/services/store/index.js +62 -12
  28. package/build/services/task/index.js +5 -1
  29. package/build/services/worker/index.js +5 -4
  30. package/build/types/activity.d.ts +6 -1
  31. package/build/types/exporter.d.ts +51 -0
  32. package/build/types/exporter.js +8 -0
  33. package/build/types/hotmesh.d.ts +1 -1
  34. package/build/types/index.d.ts +1 -0
  35. package/build/types/quorum.d.ts +1 -0
  36. package/build/types/task.d.ts +1 -1
  37. package/modules/enums.ts +4 -0
  38. package/modules/utils.ts +1 -1
  39. package/package.json +1 -1
  40. package/services/activities/trigger.ts +14 -0
  41. package/services/durable/client.ts +19 -4
  42. package/services/durable/exporter.ts +408 -0
  43. package/services/durable/factory.ts +6 -63
  44. package/services/durable/handle.ts +12 -0
  45. package/services/durable/meshos.ts +3 -0
  46. package/services/durable/workflow.ts +24 -22
  47. package/services/engine/index.ts +20 -5
  48. package/services/exporter/index.ts +147 -0
  49. package/services/hotmesh/index.ts +8 -1
  50. package/services/quorum/index.ts +37 -13
  51. package/services/router/index.ts +3 -0
  52. package/services/store/clients/redis.ts +1 -0
  53. package/services/store/index.ts +66 -14
  54. package/services/task/index.ts +4 -1
  55. package/services/worker/index.ts +6 -5
  56. package/types/activity.ts +6 -1
  57. package/types/exporter.ts +61 -0
  58. package/types/hotmesh.ts +1 -1
  59. package/types/index.ts +13 -1
  60. package/types/quorum.ts +1 -0
  61. package/types/task.ts +1 -1
@@ -21,6 +21,7 @@ import { Signal } from '../activities/signal';
21
21
  import { Worker } from '../activities/worker';
22
22
  import { Trigger } from '../activities/trigger';
23
23
  import { CompilerService } from '../compiler';
24
+ import { ExporterService } from '../exporter';
24
25
  import { ILogger } from '../logger';
25
26
  import { ReporterService } from '../reporter';
26
27
  import { Router } from '../router';
@@ -78,12 +79,14 @@ import {
78
79
  StreamRole,
79
80
  StreamStatus } from '../../types/stream';
80
81
  import { WorkListTaskType } from '../../types/task';
82
+ import { JobExport } from '../../types/exporter';
81
83
 
82
84
  class EngineService {
83
85
  namespace: string;
84
86
  apps: HotMeshApps | null;
85
87
  appId: string;
86
88
  guid: string;
89
+ exporter: ExporterService | null;
87
90
  router: Router | null;
88
91
  store: StoreService<RedisClient, RedisMulti> | null;
89
92
  stream: StreamService<RedisClient, RedisMulti> | null;
@@ -109,8 +112,8 @@ class EngineService {
109
112
  await instance.initStoreChannel(config.engine.store);
110
113
  await instance.initSubChannel(config.engine.sub);
111
114
  await instance.initStreamChannel(config.engine.stream);
112
- instance.router = instance.initRouter(config);
113
115
 
116
+ instance.router = instance.initRouter(config);
114
117
  instance.router.consumeMessages(
115
118
  instance.stream.mintKey(
116
119
  KeyType.STREAMS,
@@ -121,9 +124,15 @@ class EngineService {
121
124
  instance.processStreamMessage.bind(instance)
122
125
  );
123
126
 
124
- //the task service is used by the engine to process `webhooks` and `timehooks`
125
- instance.taskService = new TaskService(instance.store, logger);
126
-
127
+ instance.taskService = new TaskService(
128
+ instance.store,
129
+ logger
130
+ );
131
+ instance.exporter = new ExporterService(
132
+ instance.appId,
133
+ instance.store,
134
+ logger,
135
+ );
127
136
  return instance;
128
137
  }
129
138
  }
@@ -690,9 +699,15 @@ class EngineService {
690
699
 
691
700
 
692
701
  // ****** GET JOB STATE/COLLATION STATUS BY ID *********
702
+ async export(jobId: string): Promise<JobExport> {
703
+ return await this.exporter.export(jobId);
704
+ }
705
+ async getRaw(jobId: string): Promise<StringStringType> {
706
+ return await this.store.getRaw(jobId);
707
+ }
693
708
  async getStatus(jobId: string): Promise<JobStatus> {
694
709
  const { id: appId } = await this.getVID();
695
- return this.store.getStatus(jobId, appId);
710
+ return await this.store.getStatus(jobId, appId);
696
711
  }
697
712
  //todo: add 'options' parameter;
698
713
  // (e.g, if {dimensions:true}, use hscan to deliver
@@ -0,0 +1,147 @@
1
+ import { ILogger } from '../logger';
2
+ import { StoreService } from '../store';
3
+ import {
4
+ StringAnyType,
5
+ StringStringType,
6
+ Symbols } from "../../types/serializer";
7
+ import { RedisClient, RedisMulti } from '../../types/redis';
8
+ import {
9
+ DependencyExport,
10
+ ExportOptions,
11
+ JobActionExport,
12
+ JobExport } from '../../types/exporter';
13
+ import { SerializerService } from '../serializer';
14
+ import { restoreHierarchy } from '../../modules/utils';
15
+
16
+ /**
17
+ * Downloads job data from Redis (hscan, hmget, hgetall)
18
+ * Expands process data and includes dependency list
19
+ */
20
+ class ExporterService {
21
+ appId: string;
22
+ logger: ILogger;
23
+ serializer: SerializerService
24
+ store: StoreService<RedisClient, RedisMulti>;
25
+ symbols: Promise<Symbols> | Symbols;
26
+
27
+ constructor(appId: string, store: StoreService<RedisClient, RedisMulti>, logger: ILogger) {
28
+ this.appId = appId;
29
+ this.logger = logger;
30
+ this.store = store;
31
+ this.serializer = new SerializerService();
32
+ }
33
+
34
+ /**
35
+ * Convert the job hash and dependency list into a JobExport object.
36
+ * This object contains various facets that describe the interaction
37
+ * in terms relevant to narrative storytelling.
38
+ */
39
+ async export(jobId: string, options: ExportOptions = {}): Promise<JobExport> {
40
+ if (!this.symbols) {
41
+ this.symbols = this.store.getAllSymbols();
42
+ this.symbols = await this.symbols;
43
+ }
44
+ const depData = await this.store.getDependencies(jobId);
45
+ const jobData = await this.store.getRaw(jobId);
46
+ const jobExport = this.inflate(jobData, depData);
47
+ return jobExport;
48
+ }
49
+
50
+ /**
51
+ * Inflates the key from Redis, 3-character symbol
52
+ * into a human-readable JSON path, reflecting the
53
+ * tree-like structure of the unidimensional Hash
54
+ */
55
+ inflateKey(key: string): string {
56
+ return (key in this.symbols) ? this.symbols[key] : key;
57
+ }
58
+
59
+ /**
60
+ * Inflates the job data from Redis into a JobExport object
61
+ * @param jobHash - the job data from Redis
62
+ * @param dependencyList - the list of dependencies for the job
63
+ * @returns - the inflated job data
64
+ */
65
+ inflate(jobHash: StringStringType, dependencyList: string[]): JobExport {
66
+ //the list of actions taken in the workflow and hook functions
67
+ const actions: JobActionExport = {
68
+ hooks: {},
69
+ main: {
70
+ cursor: -1,
71
+ items: []
72
+ }
73
+ };
74
+ const process: StringAnyType = {};
75
+ const dependencies = this.inflateDependencyData(dependencyList, actions);
76
+ const regex = /^([a-zA-Z]{3}),(\d+(?:,\d+)*)/;
77
+
78
+ Object.entries(jobHash).forEach(([key, value]) => {
79
+ const match = key.match(regex);
80
+ if (match) {
81
+ //activity process state
82
+ const [_, letters, numbers] = match;
83
+ const path = this.inflateKey(letters);
84
+ const dimensions = `${numbers.replace(/,/g, '/')}`;
85
+ const resolved = this.serializer.fromString(value);
86
+ process[`${dimensions}/${path}`] = resolved;
87
+ } else if (key.length === 3) {
88
+ //job state
89
+ process[this.inflateKey(key)] = this.serializer.fromString(value);
90
+ }
91
+ });
92
+
93
+ return {
94
+ dependencies,
95
+ process: restoreHierarchy(process),
96
+ status: jobHash[':'],
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Inflates the dependency data from Redis into a JobExport object by
102
+ * organizing the dimensional isolate in sch a way asto interleave
103
+ * into a story
104
+ * @param data - the dependency data from Redis
105
+ * @returns - the organized dependency data
106
+ */
107
+ inflateDependencyData(data: string[], actions: JobActionExport): DependencyExport[] {
108
+ const hookReg = /([0-9,]+)-(\d+)$/;
109
+ const flowReg = /-(\d+)$/;
110
+ return data.map((dependency, index: number): DependencyExport => {
111
+ const [action, topic, gid, ...jid] = dependency.split('::');
112
+ const jobId = jid.join('::');
113
+ const match = jobId.match(hookReg);
114
+ let prefix: string;
115
+ let type: 'hook' | 'flow' | 'other';
116
+ let dimensionKey: string = '';
117
+
118
+ if (match) {
119
+ //hook-originating dependency
120
+ const [_, dimension, counter] = match;
121
+ dimensionKey = dimension.split(',').join('/');
122
+ prefix = `${dimensionKey}[${counter}]`;
123
+ type = 'hook';
124
+ } else {
125
+ const match = jobId.match(flowReg);
126
+ if (match) {
127
+ //main workflow-originating dependency
128
+ const [_, counter] = match;
129
+ prefix = `[${counter}]`;
130
+ type = 'flow';
131
+ } else {
132
+ //'other' types like signal cleanup
133
+ prefix = '/';
134
+ type = 'other';
135
+ }
136
+ }
137
+ return {
138
+ type: action,
139
+ topic,
140
+ gid,
141
+ jid: jobId,
142
+ } as unknown as DependencyExport;
143
+ });
144
+ }
145
+ }
146
+
147
+ export { ExporterService };
@@ -24,7 +24,8 @@ import {
24
24
  StatsResponse } from '../../types/stats';
25
25
  import { ConnectorService } from '../connector';
26
26
  import { StreamCode, StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
27
- import { StringAnyType } from '../../types/serializer';
27
+ import { StringAnyType, StringStringType } from '../../types/serializer';
28
+ import { JobExport } from '../../types/exporter';
28
29
 
29
30
  class HotMeshService {
30
31
  namespace: string;
@@ -152,6 +153,12 @@ class HotMeshService {
152
153
  }
153
154
 
154
155
  // ************* REPORTER METHODS *************
156
+ async export(jobId: string): Promise<JobExport> {
157
+ return await this.engine?.export(jobId);
158
+ }
159
+ async getRaw(jobId: string): Promise<StringStringType> {
160
+ return await this.engine?.getRaw(jobId);
161
+ }
155
162
  async getStats(topic: string, query: JobStatsInput): Promise<StatsResponse> {
156
163
  return await this.engine?.getStats(topic, query);
157
164
  }
@@ -1,4 +1,4 @@
1
- import { KeyType } from '../../modules/key';
1
+ import { HMSH_ACTIVATION_MAX_RETRY, HMSH_QUORUM_DELAY_MS } from '../../modules/enums';
2
2
  import { identifyRedisType, sleepFor } from '../../modules/utils';
3
3
  import { CompilerService } from '../compiler';
4
4
  import { EngineService } from '../engine';
@@ -10,6 +10,7 @@ import { SubService } from '../sub';
10
10
  import { IORedisSubService as IORedisSub } from '../sub/clients/ioredis';
11
11
  import { RedisSubService as RedisSub } from '../sub/clients/redis';
12
12
  import { CacheMode } from '../../types/cache';
13
+ import { HotMeshConfig, KeyType } from '../../types/hotmesh';
13
14
  import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
14
15
  import {
15
16
  QuorumMessage,
@@ -18,13 +19,9 @@ import {
18
19
  SubscriptionCallback,
19
20
  ThrottleMessage
20
21
  } from '../../types/quorum';
21
- import { HotMeshApps, HotMeshConfig } from '../../types/hotmesh';
22
22
  import { RedisClient, RedisMulti } from '../../types/redis';
23
23
  import { RedisClientType } from '../../types/redisclient';
24
24
 
25
- //wait time to see if quorum is reached
26
- const QUORUM_DELAY = 250;
27
-
28
25
  class QuorumService {
29
26
  namespace: string;
30
27
  appId: string;
@@ -59,8 +56,18 @@ class QuorumService {
59
56
  //note: `quorum` shares/re-uses the engine's `store`/`sub` Redis clients
60
57
  await instance.initStoreChannel(config.engine.store);
61
58
  await instance.initSubChannel(config.engine.sub);
62
- await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId); //general quorum subscription
63
- await instance.subscribe.subscribe(KeyType.QUORUM, instance.subscriptionHandler(), appId, instance.guid); //app-specific quorum subscription (used for pubsub one-time request/response)
59
+ //general quorum subscription
60
+ await instance.subscribe.subscribe(
61
+ KeyType.QUORUM,
62
+ instance.subscriptionHandler(),
63
+ appId
64
+ );
65
+ //app-specific quorum subscription (used for pubsub one-time request/response)
66
+ await instance.subscribe.subscribe(
67
+ KeyType.QUORUM,
68
+ instance.subscriptionHandler(),
69
+ appId, instance.guid
70
+ );
64
71
 
65
72
  instance.engine.processWebHooks();
66
73
  instance.engine.processTimeHooks();
@@ -138,7 +145,8 @@ class QuorumService {
138
145
  engine_id: this.guid,
139
146
  namespace: this.namespace,
140
147
  app_id: this.appId,
141
- stream: this.engine.stream.mintKey(KeyType.STREAMS, { appId: this.appId })
148
+ stream: this.engine.stream.mintKey(KeyType.STREAMS, { appId: this.appId }),
149
+ counts: this.engine.router.counts,
142
150
  };
143
151
  }
144
152
  this.store.publish(
@@ -152,7 +160,7 @@ class QuorumService {
152
160
  );
153
161
  }
154
162
 
155
- async requestQuorum(delay = QUORUM_DELAY, details = false): Promise<number> {
163
+ async requestQuorum(delay = HMSH_QUORUM_DELAY_MS, details = false): Promise<number> {
156
164
  const quorum = this.quorum;
157
165
  this.quorum = 0;
158
166
  this.profiles.length = 0;
@@ -188,7 +196,7 @@ class QuorumService {
188
196
 
189
197
 
190
198
  // ************* COMPILER METHODS *************
191
- async rollCall(delay = QUORUM_DELAY): Promise<QuorumProfile[]> {
199
+ async rollCall(delay = HMSH_QUORUM_DELAY_MS): Promise<QuorumProfile[]> {
192
200
  await this.requestQuorum(delay, true);
193
201
  const targetStreams = [];
194
202
  const multi = this.store.getMulti();
@@ -209,10 +217,20 @@ class QuorumService {
209
217
  });
210
218
  return this.profiles;
211
219
  }
212
- async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
220
+ /**
221
+ * request a quorum; if successful activate the app version
222
+ */
223
+ async activate(version: string, delay = HMSH_QUORUM_DELAY_MS, count = 0): Promise<boolean> {
213
224
  version = version.toString();
225
+ const canActivate = await this.store.reserveScoutRole('activate', Math.ceil(delay * 6 / 1000) + 1);
226
+ if (!canActivate) {
227
+ //another engine is already activating the app version
228
+ this.logger.debug('quorum-activation-awaiting', { version });
229
+ await sleepFor(delay * 6);
230
+ const app = await this.store.getApp(this.appId, true);
231
+ return app?.active == true && app?.version === version;
232
+ }
214
233
  const config = await this.engine.getVID();
215
- //request a quorum to activate the version
216
234
  await this.requestQuorum(delay);
217
235
  const q1 = await this.requestQuorum(delay);
218
236
  const q2 = await this.requestQuorum(delay);
@@ -225,6 +243,7 @@ class QuorumService {
225
243
  this.appId
226
244
  );
227
245
  await new Promise(resolve => setTimeout(resolve, delay));
246
+ await this.store.releaseScoutRole('activate');
228
247
  //confirm we received the activation message
229
248
  if (this.engine.untilVersion === version) {
230
249
  this.logger.info('quorum-activation-succeeded', { version });
@@ -236,7 +255,12 @@ class QuorumService {
236
255
  throw new Error(`UntilVersion Not Received. Version ${version} not activated`);
237
256
  }
238
257
  } else {
239
- this.logger.info('quorum-rollcall-error', { q1, q2, q3 });
258
+ this.logger.warn('quorum-rollcall-error', { q1, q2, q3, count });
259
+ this.store.releaseScoutRole('activate');
260
+ if (count < HMSH_ACTIVATION_MAX_RETRY) {
261
+ //increase the delay (give the quorum time to respond) and try again
262
+ return await this.activate(version, delay * 2, count + 1);
263
+ }
240
264
  throw new Error(`Quorum not reached. Version ${version} not activated.`);
241
265
  }
242
266
  }
@@ -40,6 +40,7 @@ class Router {
40
40
  logger: ILogger;
41
41
  throttle = 0;
42
42
  errorCount = 0;
43
+ counts: { [key: string]: number } = {};
43
44
  currentTimerId: NodeJS.Timeout | null = null;
44
45
  shouldConsume: boolean;
45
46
 
@@ -183,6 +184,8 @@ class Router {
183
184
  } else {
184
185
  output.metadata.guid = guid();
185
186
  }
187
+ const code = output.code || 200;
188
+ this.counts[code] = (this.counts[code] || 0) + 1;
186
189
  output.type = StreamDataType.RESPONSE;
187
190
  return await this.publishMessage(null, output as StreamDataResponse) as string;
188
191
  }
@@ -18,6 +18,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
18
18
  constructor(redisClient: RedisClientType) {
19
19
  super(redisClient);
20
20
  this.commands = {
21
+ set: 'SET',
21
22
  setnx: 'SETNX',
22
23
  del: 'DEL',
23
24
  expire: 'EXPIRE',
@@ -45,6 +45,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
45
45
  appId: string
46
46
  logger: ILogger;
47
47
  commands: Record<string, string> = {
48
+ set: 'set',
48
49
  setnx: 'setnx',
49
50
  del: 'del',
50
51
  expire: 'expire',
@@ -178,14 +179,16 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
178
179
  * check for and process work items in the
179
180
  * time and signal task queues.
180
181
  */
181
- async reserveScoutRole(scoutType: 'time' | 'signal', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
182
+ async reserveScoutRole(scoutType: 'time' | 'signal' | 'activate', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
182
183
  const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
183
- const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
184
- if (this.isSuccessful(success)) {
185
- await this.redisClient[this.commands.expire](key, delay - 1);
186
- return true;
187
- }
188
- return false;
184
+ const success = await this.exec('SET', key, `${scoutType}:${formatISODate(new Date())}`, 'NX', 'EX', `${delay - 1}`);
185
+ return this.isSuccessful(success);
186
+ }
187
+
188
+ async releaseScoutRole(scoutType: 'time' | 'signal' | 'activate'): Promise<boolean> {
189
+ const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
190
+ const success = await this.exec('DEL', key);
191
+ return this.isSuccessful(success);
189
192
  }
190
193
 
191
194
  async getSettings(bCreate = false): Promise<HotMeshSettings> {
@@ -238,6 +241,36 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
238
241
  }
239
242
  }
240
243
 
244
+ async getAllSymbols(): Promise<Symbols> {
245
+ //get hash with all reserved symbol ranges
246
+ const rangeKey = this.mintKey(KeyType.SYMKEYS, { appId: this.appId });
247
+ const ranges = await this.redisClient[this.commands.hgetall](rangeKey);
248
+ const rangeKeys = Object.keys(ranges).sort();
249
+ delete rangeKeys[':cursor'];
250
+ const multi = this.getMulti();
251
+ for (const rangeKey of rangeKeys) {
252
+ const symbolKey = this.mintKey(KeyType.SYMKEYS, { activityId: rangeKey, appId: this.appId });
253
+ multi[this.commands.hgetall](symbolKey);
254
+ }
255
+ const results = await multi.exec() as Array<[null, Symbols]> | Array<Symbols>;
256
+
257
+ const symbolSets: Symbols = {};
258
+ results.forEach((result: [null, Symbols] | Symbols, index: number) => {
259
+ if (result) {
260
+ let vals: Symbols;
261
+ if (Array.isArray(result) && result.length === 2) {
262
+ vals = result[1];
263
+ } else {
264
+ vals = result as Symbols;
265
+ }
266
+ for (const [key, value] of Object.entries(vals)) {
267
+ symbolSets[value as string] = key.startsWith(rangeKeys[index]) ? key : `${rangeKeys[index]}/${key}`;
268
+ }
269
+ }
270
+ });
271
+ return symbolSets;
272
+ }
273
+
241
274
  async getSymbols(activityId: string): Promise<Symbols> {
242
275
  let symbols: Symbols = this.cache.getSymbols(this.appId, activityId);
243
276
  if (symbols) {
@@ -393,7 +426,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
393
426
  * when `originJobId` is interrupted/expired, the items in the
394
427
  * list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
395
428
  */
396
- async registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
429
+ async registerJobDependency(depType: WorkListTaskType, originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
397
430
  const privateMulti = multi || this.getMulti();
398
431
  const dependencyParams = {
399
432
  appId: this.appId,
@@ -403,8 +436,8 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
403
436
  KeyType.JOB_DEPENDENTS,
404
437
  dependencyParams,
405
438
  );
406
- //tasks have '4' segments
407
- const expireTask = `expire::${topic}::${gId}::${jobId}`;
439
+ //items listed as job dependencies have different relationships
440
+ const expireTask = `${depType}::${topic}::${gId}::${jobId}`;
408
441
  privateMulti[this.commands.rpush](depKey, expireTask);
409
442
  if (!multi) {
410
443
  return await privateMulti.exec();
@@ -586,6 +619,15 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
586
619
  }
587
620
  }
588
621
 
622
+ async getRaw(jobId: string): Promise<StringStringType> {
623
+ const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
624
+ const job = await this.redisClient[this.commands.hgetall](jobKey);
625
+ if (!job) {
626
+ throw new GetStateError(jobId);
627
+ }
628
+ return job;
629
+ }
630
+
589
631
  /**
590
632
  * collate is a generic method for incrementing a value in a hash
591
633
  * in order to track their progress during processing.
@@ -836,6 +878,12 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
836
878
  await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
837
879
  }
838
880
 
881
+ async getDependencies(jobId: string): Promise<string[]> {
882
+ const depParams = { appId: this.appId, jobId };
883
+ const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
884
+ return this.redisClient[this.commands.lrange](depKey, 0, -1);
885
+ }
886
+
839
887
  /**
840
888
  * registers a hook activity to be awakened (uses ZSET to
841
889
  * store the 'sleep group' and LIST to store the events
@@ -859,11 +907,15 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
859
907
  let [pType, pKey] = this.resolveTaskKeyContext(listKey);
860
908
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
861
909
  if (timeEvent) {
862
- //there are 4 time-related task
863
- //1) sleep (awaken), 2) expire, 3) interrupt, 4) delist
864
- const [type, activityId, gId, ...jobId] = timeEvent.split('::');
910
+ //there are task types
911
+ //1) sleep (awaken), 2) expire (OR expire-child), 3) interrupt, 4) delist, 5) child (just an index helper; no work to do)
912
+ let [type, activityId, gId, ...jobId] = timeEvent.split('::');
865
913
  if (type === 'delist') {
866
914
  pType = 'delist';
915
+ } else if (type === 'child') {
916
+ pType = 'child';
917
+ } else if (type === 'expire-child') {
918
+ type = 'expire'; //use the same logic as 'expire'
867
919
  }
868
920
  return [listKey, jobId.join('::'), gId, activityId, pType];
869
921
  }
@@ -881,7 +933,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
881
933
  * generic LIST (lists typically contain target job ids)
882
934
  * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
883
935
  */
884
- resolveTaskKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'|'delist'), string] {
936
+ resolveTaskKeyContext(listKey: string): [WorkListTaskType, string] {
885
937
  if (listKey.startsWith('::INTERRUPT')) {
886
938
  return ['interrupt', listKey.split('::')[2]];
887
939
  } else if (listKey.startsWith('::EXPIRE')) {
@@ -117,7 +117,10 @@ class TaskService {
117
117
 
118
118
  if (Array.isArray(workListTask)) {
119
119
  const [listKey, target, gId, activityId, type] = workListTask;
120
- if (type === 'delist') {
120
+ if (type === 'child') {
121
+ //continue; this child is listed here for convenience, but
122
+ // will be expired by an origin ancestor and is listed there
123
+ } else if (type === 'delist') {
121
124
  //delist the signalKey (target)
122
125
  const key = this.store.mintKey(KeyType.SIGNALS, { appId: this.store.appId });
123
126
  await this.store.redisClient[this.store.commands.hdel](key, target);
@@ -1,17 +1,19 @@
1
1
  import { KeyType } from "../../modules/key";
2
+ import { identifyRedisType } from "../../modules/utils";
3
+ import { ConnectorService } from "../connector";
2
4
  import { ILogger } from "../logger";
3
5
  import { Router } from "../router";
4
6
  import { StoreService } from '../store';
5
- import { RedisStoreService as RedisStore } from '../store/clients/redis';
6
7
  import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
8
+ import { RedisStoreService as RedisStore } from '../store/clients/redis';
7
9
  import { StreamService } from '../stream';
8
- import { RedisStreamService as RedisStream } from '../stream/clients/redis';
9
10
  import { IORedisStreamService as IORedisStream } from '../stream/clients/ioredis';
11
+ import { RedisStreamService as RedisStream } from '../stream/clients/redis';
10
12
  import { SubService } from '../sub';
11
13
  import { IORedisSubService as IORedisSub } from '../sub/clients/ioredis';
12
14
  import { RedisSubService as RedisSub } from '../sub/clients/redis';
13
- import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
14
15
  import { HotMeshConfig, HotMeshWorker } from "../../types/hotmesh";
16
+ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient';
15
17
  import {
16
18
  QuorumMessage,
17
19
  QuorumProfile,
@@ -19,8 +21,6 @@ import {
19
21
  import { RedisClient, RedisMulti } from "../../types/redis";
20
22
  import { RedisClientType } from '../../types/redisclient';
21
23
  import { StreamRole } from "../../types/stream";
22
- import { identifyRedisType } from "../../modules/utils";
23
- import { ConnectorService } from "../connector";
24
24
 
25
25
  class WorkerService {
26
26
  namespace: string;
@@ -174,6 +174,7 @@ class WorkerService {
174
174
  app_id: this.appId,
175
175
  worker_topic: this.topic,
176
176
  stream: this.stream.mintKey(KeyType.STREAMS, params),
177
+ counts: this.router.counts,
177
178
  };
178
179
  }
179
180
  this.store.publish(
package/types/activity.ts CHANGED
@@ -38,13 +38,18 @@ interface Measure {
38
38
 
39
39
  interface TriggerActivityStats {
40
40
  /**
41
- * parent job; including this allows the parent's
41
+ * dependent parent job id; including this allows the parent's
42
42
  * expiration/interruption events to cascade; set
43
43
  * `expire` in the YAML for the dependent graph
44
44
  * to 0 and provide the parent for dependent,
45
45
  * cascading interruption and cleanup
46
46
  */
47
47
  parent?: string;
48
+ /**
49
+ * adjacent parent job id; this is the actual adjacent
50
+ * parent in the graph, but it is not used for cascading expiration
51
+ */
52
+ adjacent?: string;
48
53
  id?: { [key: string]: unknown } | string;
49
54
  key?: { [key: string]: unknown } | string;
50
55
  /**
@@ -0,0 +1,61 @@
1
+ import { StringAnyType, StringStringType } from "./serializer";
2
+
3
+ export type ExportItem = [(string | null), string, any];
4
+
5
+ export interface ExportOptions {};
6
+
7
+ export type JobAction = {
8
+ cursor: number;
9
+ items: ExportItem[];
10
+ };
11
+
12
+ export interface JobActionExport {
13
+ hooks: {
14
+ [key: string]: JobAction;
15
+ };
16
+ main: JobAction;
17
+ };
18
+
19
+ export interface ActivityAction {
20
+ action: string;
21
+ target: string;
22
+ }
23
+
24
+ export interface JobTimeline {
25
+ activity: string; //activity name
26
+ dimension: string; //dimensional isolate path
27
+ duplex: 'entry' | 'exit'; //activity entry or exit
28
+ timestamp: string; //actually a number but too many digits for JS
29
+ actions?: ActivityAction[];
30
+ }
31
+
32
+ export interface DependencyExport {
33
+ type: string;
34
+ topic: string;
35
+ gid: string;
36
+ jid: string;
37
+ }
38
+
39
+ export interface ExportTransitions {
40
+ [key: string]: string[];
41
+ };
42
+
43
+ export interface ExportCycles {
44
+ [key: string]: string[];
45
+ };
46
+
47
+ export interface DurableJobExport {
48
+ data: StringAnyType;
49
+ dependencies: DependencyExport[];
50
+ state: StringAnyType;
51
+ status: string;
52
+ timeline: JobTimeline[];
53
+ transitions: ExportTransitions;
54
+ cycles: ExportCycles;
55
+ };
56
+
57
+ export interface JobExport {
58
+ dependencies: DependencyExport[];
59
+ process: StringAnyType;
60
+ status: string;
61
+ };
package/types/hotmesh.ts CHANGED
@@ -44,7 +44,7 @@ type KeyStoreParams = {
44
44
  facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
45
45
  topic?: string; //topic name (e.g., "foo" or "" for top-level)
46
46
  timeValue?: number; //time value (rounded to minute) (for delete range)
47
- scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
47
+ scoutType?: 'signal' | 'time' | 'activate'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
48
48
  };
49
49
 
50
50
  type HotMesh = typeof HotMeshService;