@hotmeshio/hotmesh 0.0.33 → 0.0.35

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 (94) hide show
  1. package/README.md +30 -18
  2. package/build/modules/enums.d.ts +22 -0
  3. package/build/modules/enums.js +29 -0
  4. package/build/modules/errors.d.ts +10 -2
  5. package/build/modules/errors.js +14 -3
  6. package/build/modules/key.d.ts +16 -15
  7. package/build/modules/key.js +18 -15
  8. package/build/modules/utils.d.ts +1 -0
  9. package/build/modules/utils.js +6 -1
  10. package/build/package.json +4 -1
  11. package/build/services/activities/activity.d.ts +5 -0
  12. package/build/services/activities/activity.js +27 -6
  13. package/build/services/activities/await.js +11 -3
  14. package/build/services/activities/cycle.js +10 -2
  15. package/build/services/activities/hook.js +8 -2
  16. package/build/services/activities/index.d.ts +2 -2
  17. package/build/services/activities/index.js +2 -2
  18. package/build/services/activities/interrupt.d.ts +16 -0
  19. package/build/services/activities/interrupt.js +129 -0
  20. package/build/services/activities/signal.js +9 -2
  21. package/build/services/activities/trigger.d.ts +4 -0
  22. package/build/services/activities/trigger.js +14 -4
  23. package/build/services/activities/worker.js +10 -2
  24. package/build/services/collator/index.d.ts +4 -0
  25. package/build/services/collator/index.js +8 -0
  26. package/build/services/compiler/deployer.js +1 -3
  27. package/build/services/connector/index.js +2 -3
  28. package/build/services/durable/client.js +9 -6
  29. package/build/services/durable/factory.js +65 -284
  30. package/build/services/durable/handle.d.ts +37 -0
  31. package/build/services/durable/handle.js +52 -9
  32. package/build/services/durable/index.d.ts +5 -0
  33. package/build/services/durable/index.js +10 -0
  34. package/build/services/durable/meshos.js +3 -6
  35. package/build/services/durable/worker.js +11 -5
  36. package/build/services/durable/workflow.d.ts +24 -0
  37. package/build/services/durable/workflow.js +56 -1
  38. package/build/services/engine/index.d.ts +14 -6
  39. package/build/services/engine/index.js +52 -27
  40. package/build/services/hotmesh/index.d.ts +6 -2
  41. package/build/services/hotmesh/index.js +23 -5
  42. package/build/services/quorum/index.d.ts +1 -0
  43. package/build/services/quorum/index.js +10 -0
  44. package/build/services/signaler/stream.js +25 -29
  45. package/build/services/store/index.d.ts +40 -4
  46. package/build/services/store/index.js +114 -9
  47. package/build/services/task/index.d.ts +5 -4
  48. package/build/services/task/index.js +12 -14
  49. package/build/types/activity.d.ts +35 -5
  50. package/build/types/durable.d.ts +4 -0
  51. package/build/types/index.d.ts +1 -1
  52. package/build/types/job.d.ts +18 -1
  53. package/build/types/quorum.d.ts +11 -7
  54. package/build/types/stream.d.ts +4 -1
  55. package/build/types/stream.js +2 -0
  56. package/modules/enums.ts +32 -0
  57. package/modules/errors.ts +24 -9
  58. package/modules/key.ts +4 -1
  59. package/modules/utils.ts +5 -0
  60. package/package.json +4 -1
  61. package/services/activities/activity.ts +34 -8
  62. package/services/activities/await.ts +11 -4
  63. package/services/activities/cycle.ts +10 -3
  64. package/services/activities/hook.ts +8 -3
  65. package/services/activities/index.ts +2 -2
  66. package/services/activities/interrupt.ts +159 -0
  67. package/services/activities/signal.ts +9 -3
  68. package/services/activities/trigger.ts +21 -5
  69. package/services/activities/worker.ts +10 -3
  70. package/services/collator/index.ts +10 -1
  71. package/services/compiler/deployer.ts +1 -3
  72. package/services/connector/index.ts +3 -5
  73. package/services/durable/client.ts +10 -7
  74. package/services/durable/factory.ts +65 -284
  75. package/services/durable/handle.ts +55 -9
  76. package/services/durable/index.ts +11 -0
  77. package/services/durable/meshos.ts +3 -7
  78. package/services/durable/worker.ts +11 -5
  79. package/services/durable/workflow.ts +66 -2
  80. package/services/engine/index.ts +74 -26
  81. package/services/hotmesh/index.ts +28 -6
  82. package/services/quorum/index.ts +9 -0
  83. package/services/signaler/stream.ts +28 -25
  84. package/services/store/index.ts +119 -11
  85. package/services/task/index.ts +18 -18
  86. package/types/activity.ts +38 -8
  87. package/types/durable.ts +8 -4
  88. package/types/index.ts +1 -1
  89. package/types/job.ts +30 -1
  90. package/types/quorum.ts +13 -8
  91. package/types/stream.ts +3 -0
  92. package/build/services/activities/iterate.d.ts +0 -9
  93. package/build/services/activities/iterate.js +0 -13
  94. package/services/activities/iterate.ts +0 -26
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HotMeshService = void 0;
4
- const nanoid_1 = require("nanoid");
5
4
  const key_1 = require("../../modules/key");
5
+ const utils_1 = require("../../modules/utils");
6
+ const redis_1 = require("../connector/clients/redis");
7
+ const ioredis_1 = require("../connector/clients/ioredis");
6
8
  const engine_1 = require("../engine");
7
9
  const logger_1 = require("../logger");
8
10
  const stream_1 = require("../signaler/stream");
@@ -39,7 +41,7 @@ class HotMeshService {
39
41
  }
40
42
  static async init(config) {
41
43
  const instance = new HotMeshService();
42
- instance.guid = (0, nanoid_1.nanoid)();
44
+ instance.guid = (0, utils_1.guid)();
43
45
  instance.verifyAndSetNamespace(config.namespace);
44
46
  instance.verifyAndSetAppId(config.appId);
45
47
  instance.logger = new logger_1.LoggerService(config.appId, instance.guid, config.name || '', config.logLevel);
@@ -49,7 +51,7 @@ class HotMeshService {
49
51
  return instance;
50
52
  }
51
53
  static guid() {
52
- return (0, nanoid_1.nanoid)();
54
+ return (0, utils_1.guid)();
53
55
  }
54
56
  async initEngine(config, logger) {
55
57
  if (config.engine) {
@@ -98,6 +100,10 @@ class HotMeshService {
98
100
  //activation is a quorum operation
99
101
  return await this.quorum?.activate(version, delay);
100
102
  }
103
+ async inventory(version, delay) {
104
+ //get count of all peers
105
+ return await this.quorum?.inventory(delay);
106
+ }
101
107
  // ************* REPORTER METHODS *************
102
108
  async getStats(topic, query) {
103
109
  return await this.engine?.getStats(topic, query);
@@ -117,6 +123,10 @@ class HotMeshService {
117
123
  async resolveQuery(topic, query) {
118
124
  return await this.engine?.resolveQuery(topic, query);
119
125
  }
126
+ // ****************** `INTERRUPT` ACTIVE JOBS *****************
127
+ async interrupt(topic, jobId, options = {}) {
128
+ return await this.engine?.interrupt(topic, jobId, options);
129
+ }
120
130
  // ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
121
131
  async scrub(jobId) {
122
132
  await this.engine?.scrub(jobId);
@@ -128,8 +138,15 @@ class HotMeshService {
128
138
  async hookAll(hookTopic, data, query, queryFacets = []) {
129
139
  return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
130
140
  }
131
- async stop() {
132
- await stream_1.StreamSignaler.stopConsuming();
141
+ static async stop() {
142
+ if (!this.disconnecting) {
143
+ this.disconnecting = true;
144
+ await stream_1.StreamSignaler.stopConsuming();
145
+ await redis_1.RedisConnection.disconnectAll();
146
+ await ioredis_1.RedisConnection.disconnectAll();
147
+ }
148
+ }
149
+ stop() {
133
150
  this.engine?.task.cancelCleanup();
134
151
  }
135
152
  async compress(terms) {
@@ -137,3 +154,4 @@ class HotMeshService {
137
154
  }
138
155
  }
139
156
  exports.HotMeshService = HotMeshService;
157
+ HotMeshService.disconnecting = false;
@@ -29,6 +29,7 @@ declare class QuorumService {
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
33
  activate(version: string, delay?: number): Promise<boolean>;
33
34
  }
34
35
  export { QuorumService };
@@ -82,6 +82,9 @@ class QuorumService {
82
82
  else if (message.type === 'job') {
83
83
  self.engine.routeToSubscribers(message.topic, message.job);
84
84
  }
85
+ else if (message.type === 'cron') {
86
+ self.engine.processTimeHooks();
87
+ }
85
88
  //if there are any callbacks, call them
86
89
  if (self.callbacks.length > 0) {
87
90
  self.callbacks.forEach(cb => cb(topic, message));
@@ -114,6 +117,13 @@ class QuorumService {
114
117
  this.callbacks = this.callbacks.filter(cb => cb !== callback);
115
118
  }
116
119
  // ************* 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);
126
+ }
117
127
  async activate(version, delay = QUORUM_DELAY) {
118
128
  version = version.toString();
119
129
  const config = await this.engine.getVID();
@@ -1,22 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StreamSignaler = void 0;
4
+ const enums_1 = require("../../modules/enums");
4
5
  const key_1 = require("../../modules/key");
5
6
  const utils_1 = require("../../modules/utils");
6
7
  const telemetry_1 = require("../telemetry");
7
8
  const stream_1 = require("../../types/stream");
8
- const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
9
- const MAX_TIMEOUT_MS = 60000;
10
- const GRADUATED_INTERVAL_MS = 5000;
11
- const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
12
- const TEST_BLOCK_DURATION = 1000; //Set to `1000` so tests can interrupt quickly
13
- const BLOCK_TIME_MS = process.env.NODE_ENV === 'test' ? TEST_BLOCK_DURATION : BLOCK_DURATION;
14
- const SYSTEM_STATUS_CODE = 999;
15
- const UNKNOWN_STATUS_CODE = 500;
16
- const UNKNOWN_STATUS_MESSAGE = 'unknown';
17
- const XCLAIM_DELAY_MS = 1000 * 60; //max time a message can be unacked before it is claimed by another
18
- const XCLAIM_COUNT = 3; //max number of times a message can be claimed by another before it is dead-lettered
19
- const XPENDING_COUNT = 10;
20
9
  class StreamSignaler {
21
10
  constructor(config, stream, store, logger) {
22
11
  this.throttle = 0;
@@ -28,8 +17,8 @@ class StreamSignaler {
28
17
  this.topic = config.topic;
29
18
  this.stream = stream;
30
19
  this.store = store;
31
- this.reclaimDelay = config.reclaimDelay || XCLAIM_DELAY_MS;
32
- this.reclaimCount = config.reclaimCount || XCLAIM_COUNT;
20
+ this.reclaimDelay = config.reclaimDelay || enums_1.XCLAIM_DELAY_MS;
21
+ this.reclaimCount = config.reclaimCount || enums_1.XCLAIM_COUNT;
33
22
  this.logger = logger;
34
23
  }
35
24
  async createGroup(stream, group) {
@@ -59,7 +48,7 @@ class StreamSignaler {
59
48
  return;
60
49
  }
61
50
  try {
62
- const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', BLOCK_TIME_MS, 'STREAMS', stream, '>');
51
+ const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', enums_1.BLOCK_TIME_MS, 'STREAMS', stream, '>');
63
52
  if (this.isStreamMessage(result)) {
64
53
  const [[, messages]] = result;
65
54
  for (const [id, message] of messages) {
@@ -81,7 +70,7 @@ class StreamSignaler {
81
70
  if (this.shouldConsume && process.env.NODE_ENV !== 'test') {
82
71
  this.logger.error(`stream-consume-message-error`, { err, stream, group, consumer });
83
72
  this.errorCount++;
84
- const timeout = Math.min(GRADUATED_INTERVAL_MS * (2 ** this.errorCount), MAX_TIMEOUT_MS);
73
+ const timeout = Math.min(enums_1.GRADUATED_INTERVAL_MS * (2 ** this.errorCount), enums_1.MAX_TIMEOUT_MS);
85
74
  setTimeout(consume.bind(this), timeout);
86
75
  }
87
76
  }
@@ -101,7 +90,7 @@ class StreamSignaler {
101
90
  telemetry.startStreamSpan(input, this.role);
102
91
  output = await this.execStreamLeg(input, stream, id, callback.bind(this));
103
92
  if (output?.status === stream_1.StreamStatus.ERROR) {
104
- telemetry.setStreamError(`Function Status Code ${output.code || UNKNOWN_STATUS_CODE}`);
93
+ telemetry.setStreamError(`Function Status Code ${output.code || enums_1.STATUS_CODE_UNKNOWN}`);
105
94
  }
106
95
  this.errorCount = 0;
107
96
  }
@@ -140,6 +129,7 @@ class StreamSignaler {
140
129
  await (0, utils_1.sleepFor)(timeout);
141
130
  return await this.publishMessage(input.metadata.topic, {
142
131
  data: input.data,
132
+ //note: retain guid (this is a retry attempt)
143
133
  metadata: { ...input.metadata, try: (input.metadata.try || 0) + 1 },
144
134
  policies: input.policies,
145
135
  });
@@ -148,6 +138,12 @@ class StreamSignaler {
148
138
  output = this.structureError(input, output);
149
139
  }
150
140
  }
141
+ else if (typeof output.metadata !== 'object') {
142
+ output.metadata = { ...input.metadata, guid: (0, utils_1.guid)() };
143
+ }
144
+ else {
145
+ output.metadata.guid = (0, utils_1.guid)();
146
+ }
151
147
  output.type = stream_1.StreamDataType.RESPONSE;
152
148
  return await this.publishMessage(null, output);
153
149
  }
@@ -157,7 +153,7 @@ class StreamSignaler {
157
153
  const errorCode = output.code.toString();
158
154
  const policy = policies?.[errorCode];
159
155
  const maxRetries = policy?.[0];
160
- const tryCount = Math.min(input.metadata.try || 0, MAX_RETRIES);
156
+ const tryCount = Math.min(input.metadata.try || 0, enums_1.MAX_RETRIES);
161
157
  //only possible values for maxRetries are 1, 2, 3
162
158
  //only possible values for tryCount are 0, 1, 2
163
159
  if (maxRetries > tryCount) {
@@ -172,7 +168,7 @@ class StreamSignaler {
172
168
  error.message = err.message;
173
169
  }
174
170
  else {
175
- error.message = UNKNOWN_STATUS_MESSAGE;
171
+ error.message = enums_1.STATUS_MESSAGE_UNKNOWN;
176
172
  }
177
173
  if (typeof err.stack === 'string') {
178
174
  error.stack = err.stack;
@@ -182,17 +178,17 @@ class StreamSignaler {
182
178
  }
183
179
  return {
184
180
  status: 'error',
185
- code: UNKNOWN_STATUS_CODE,
186
- metadata: { ...input.metadata },
181
+ code: enums_1.STATUS_CODE_UNKNOWN,
182
+ metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
187
183
  data: error
188
184
  };
189
185
  }
190
186
  structureUnacknowledgedError(input) {
191
187
  const message = 'stream message max delivery count exceeded';
192
- const code = SYSTEM_STATUS_CODE;
188
+ const code = enums_1.STATUS_CODE_UNACKED;
193
189
  const data = { message, code };
194
190
  const output = {
195
- metadata: { ...input.metadata },
191
+ metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
196
192
  status: stream_1.StreamStatus.ERROR,
197
193
  code,
198
194
  data,
@@ -202,9 +198,9 @@ class StreamSignaler {
202
198
  return output;
203
199
  }
204
200
  structureError(input, output) {
205
- const message = output.data?.message ? output.data?.message.toString() : UNKNOWN_STATUS_MESSAGE;
201
+ const message = output.data?.message ? output.data?.message.toString() : enums_1.STATUS_MESSAGE_UNKNOWN;
206
202
  const statusCode = output.code || output.data?.code;
207
- const code = isNaN(statusCode) ? UNKNOWN_STATUS_CODE : parseInt(statusCode.toString());
203
+ const code = isNaN(statusCode) ? enums_1.STATUS_CODE_UNKNOWN : parseInt(statusCode.toString());
208
204
  const data = { message, code };
209
205
  if (typeof output.data?.error === 'object') {
210
206
  data.error = { ...output.data.error };
@@ -212,7 +208,7 @@ class StreamSignaler {
212
208
  return {
213
209
  status: stream_1.StreamStatus.ERROR,
214
210
  code,
215
- metadata: { ...input.metadata },
211
+ metadata: { ...input.metadata, guid: (0, utils_1.guid)() },
216
212
  data
217
213
  };
218
214
  }
@@ -220,12 +216,12 @@ class StreamSignaler {
220
216
  for (const instance of [...StreamSignaler.signalers]) {
221
217
  instance.stopConsuming();
222
218
  }
219
+ await (0, utils_1.sleepFor)(enums_1.BLOCK_TIME_MS);
223
220
  }
224
221
  async stopConsuming() {
225
222
  this.shouldConsume = false;
226
223
  this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
227
224
  this.cancelThrottle();
228
- //await sleepFor(BLOCK_TIME_MS);
229
225
  }
230
226
  cancelThrottle() {
231
227
  if (this.currentTimerId !== undefined) {
@@ -240,7 +236,7 @@ class StreamSignaler {
240
236
  this.throttle = delayInMillis;
241
237
  this.logger.info(`stream-throttle-reset`, { delay: this.throttle, topic: this.topic });
242
238
  }
243
- async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = XPENDING_COUNT) {
239
+ async claimUnacknowledged(stream, group, consumer, idleTimeMs = this.reclaimDelay, limit = enums_1.XPENDING_COUNT) {
244
240
  let pendingMessages = [];
245
241
  const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit); //[[ '1688768134881-0', 'testConsumer1', 1017, 1 ]]
246
242
  for (const pendingMessageInfo of pendingMessagesInfo) {
@@ -269,7 +265,7 @@ class StreamSignaler {
269
265
  // ii) corrupt hardware/network/transport/etc
270
266
  // 3b) system error: Redis unable to accept `xadd` request
271
267
  // 4c) system error: Redis unable to accept `xdel`/`xack` request
272
- this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: SYSTEM_STATUS_CODE, count });
268
+ this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: enums_1.STATUS_CODE_UNACKED, count });
273
269
  const streamData = reclaimedMessage[0]?.[1]?.[1];
274
270
  //fatal risk point 1 of 3): json is corrupt
275
271
  const [err, input] = this.parseStreamData(streamData);
@@ -10,6 +10,7 @@ import { SymbolSets, StringStringType, StringAnyType, Symbols } from '../../type
10
10
  import { IdsData, JobStatsRange, StatsType } from '../../types/stats';
11
11
  import { Transitions } from '../../types/transition';
12
12
  import { ReclaimedMessageType } from '../../types/stream';
13
+ import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
13
14
  interface AbstractRedisClient {
14
15
  exec(): any;
15
16
  }
@@ -54,6 +55,13 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
54
55
  setApp(id: string, version: string): Promise<HotMeshApp>;
55
56
  activateAppVersion(id: string, version: string): Promise<boolean>;
56
57
  registerAppVersion(appId: string, version: string): Promise<any>;
58
+ /**
59
+ * Registers jobId with the originJobId that spawned it. In the future,
60
+ * when originJobId is interrupted or expired, the items in the
61
+ * list (added via RPUSH) are LPOPed. If origin was expired, then
62
+ * LPOPed items from the list are likewise expired;
63
+ */
64
+ setDependency(originJobId: string, topic: string, jobId: string, multi?: U): Promise<any>;
57
65
  setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, multi?: U): Promise<any>;
58
66
  hGetAllResult(result: any): any;
59
67
  getJobStats(jobKeys: string[]): Promise<JobStatsRange>;
@@ -62,8 +70,8 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
62
70
  getStatus(jobId: string, appId: string): Promise<number>;
63
71
  setState({ ...state }: StringAnyType, status: number | null, jobId: string, symbolNames: string[], dIds: StringStringType, multi?: U): Promise<string>;
64
72
  /**
65
- * returns custom search fields and values. The fields param
66
- * should not prefix items with an underscore.
73
+ * Returns custom search fields and values.
74
+ * NOTE: The `fields` param should NOT prefix items with an underscore.
67
75
  */
68
76
  getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
69
77
  getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
@@ -87,8 +95,36 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
87
95
  deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub?: boolean): Promise<void>;
88
96
  processTaskQueue(sourceKey: string, destinationKey: string): Promise<any>;
89
97
  expireJob(jobId: string, inSeconds: number): Promise<void>;
90
- registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'cron', deletionTime: number, multi?: U): Promise<void>;
91
- getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string] | void>;
98
+ /**
99
+ * register the descendants of an expired origin flow to be
100
+ * expired at a future date; options indicate whether this
101
+ * is a standard `expire` or an `interrupt`
102
+ */
103
+ registerExpireJob(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void>;
104
+ /**
105
+ * registers a hook activity to be awakened (uses ZSET to
106
+ * store the 'sleep group' and LIST to store the events
107
+ * for the given sleep group. Sleep groups are
108
+ * organized into 'n'-second blocks (LISTS))
109
+ */
110
+ registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt', deletionTime: number, multi?: U): Promise<void>;
111
+ getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt'] | void>;
112
+ /**
113
+ * when processing time jobs, the target LIST ID returned
114
+ * from the ZSET query can be prefixed to denote what to
115
+ * do with the work list. (not everything is known in advance,
116
+ * so the ZSET key defines HOW to approach the work in the
117
+ * generic LIST (lists typically contain target job ids)
118
+ * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
119
+ */
120
+ resolveKeyContext(listKey: string): [('sleep' | 'expire' | 'interrupt'), string];
121
+ /**
122
+ * Interrupts a job and sets sets a job error (410), if 'throw'!=false.
123
+ * This method is called by the engine and not by an activity and is
124
+ * followed by a call to execute job completion/cleanup tasks
125
+ * associated with a job completion event.
126
+ */
127
+ interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<void>;
92
128
  scrub(jobId: string): Promise<void>;
93
129
  }
94
130
  export { StoreService };
@@ -28,6 +28,8 @@ const key_1 = require("../../modules/key");
28
28
  const serializer_1 = require("../serializer");
29
29
  const cache_1 = require("./cache");
30
30
  const utils_1 = require("../../modules/utils");
31
+ const enums_1 = require("../../modules/enums");
32
+ const errors_1 = require("../../modules/errors");
31
33
  class StoreService {
32
34
  constructor(redisClient) {
33
35
  this.commands = {
@@ -296,6 +298,21 @@ class StoreService {
296
298
  };
297
299
  return await this.redisClient[this.commands.hset](key, payload);
298
300
  }
301
+ /**
302
+ * Registers jobId with the originJobId that spawned it. In the future,
303
+ * when originJobId is interrupted or expired, the items in the
304
+ * list (added via RPUSH) are LPOPed. If origin was expired, then
305
+ * LPOPed items from the list are likewise expired;
306
+ */
307
+ async setDependency(originJobId, topic, jobId, multi) {
308
+ const privateMulti = multi || this.getMulti();
309
+ const depParams = { appId: this.appId, jobId: originJobId };
310
+ const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
311
+ privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
312
+ if (!multi) {
313
+ return await privateMulti.exec();
314
+ }
315
+ }
299
316
  async setStats(jobKey, jobId, dateTime, stats, appVersion, multi) {
300
317
  const params = { appId: appVersion.id, jobId, jobKey, dateTime };
301
318
  const privateMulti = multi || this.getMulti();
@@ -395,8 +412,8 @@ class StoreService {
395
412
  return jobId;
396
413
  }
397
414
  /**
398
- * returns custom search fields and values. The fields param
399
- * should not prefix items with an underscore.
415
+ * Returns custom search fields and values.
416
+ * NOTE: The `fields` param should NOT prefix items with an underscore.
400
417
  */
401
418
  async getQueryState(jobId, fields) {
402
419
  const key = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
@@ -437,7 +454,7 @@ class StoreService {
437
454
  return [state, status];
438
455
  }
439
456
  else {
440
- throw new Error(`Job ${jobId} not found`);
457
+ throw new errors_1.GetStateError(jobId);
441
458
  }
442
459
  }
443
460
  async collate(jobId, activityId, amount, dIds, multi) {
@@ -646,6 +663,25 @@ class StoreService {
646
663
  await this.redisClient[this.commands.expire](jobKey, inSeconds);
647
664
  }
648
665
  }
666
+ /**
667
+ * register the descendants of an expired origin flow to be
668
+ * expired at a future date; options indicate whether this
669
+ * is a standard `expire` or an `interrupt`
670
+ */
671
+ async registerExpireJob(jobId, deletionTime, options) {
672
+ const depParams = { appId: this.appId, jobId };
673
+ const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
674
+ const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
675
+ const depKeyContext = `::${context}::${depKey}`;
676
+ const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
677
+ await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
678
+ }
679
+ /**
680
+ * registers a hook activity to be awakened (uses ZSET to
681
+ * store the 'sleep group' and LIST to store the events
682
+ * for the given sleep group. Sleep groups are
683
+ * organized into 'n'-second blocks (LISTS))
684
+ */
649
685
  async registerTimeHook(jobId, activityId, type, deletionTime, multi) {
650
686
  const listKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
651
687
  const timeEvent = `${type}::${activityId}::${jobId}`;
@@ -657,18 +693,87 @@ class StoreService {
657
693
  }
658
694
  async getNextTimeJob(listKey) {
659
695
  const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
660
- const now = Date.now();
661
- listKey = listKey || await this.zRangeByScore(zsetKey, 0, now);
696
+ listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
662
697
  if (listKey) {
663
- const timeEvent = await this.redisClient[this.commands.lpop](listKey);
698
+ const [pType, pKey] = this.resolveKeyContext(listKey);
699
+ const timeEvent = await this.redisClient[this.commands.lpop](pKey);
664
700
  if (timeEvent) {
665
- //placeholder: there are 3 time-related event triggers: sleep, expire, cron
666
- const [type, activityId, ...jobId] = timeEvent.split('::');
667
- return [listKey, jobId.join('::'), activityId];
701
+ //there are 3 time-related event triggers: sleep, expire, interrupt
702
+ const [_type, activityId, ...jobId] = timeEvent.split('::');
703
+ return [listKey, jobId.join('::'), activityId, pType];
668
704
  }
669
705
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
670
706
  }
671
707
  }
708
+ /**
709
+ * when processing time jobs, the target LIST ID returned
710
+ * from the ZSET query can be prefixed to denote what to
711
+ * do with the work list. (not everything is known in advance,
712
+ * so the ZSET key defines HOW to approach the work in the
713
+ * generic LIST (lists typically contain target job ids)
714
+ * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
715
+ */
716
+ resolveKeyContext(listKey) {
717
+ if (listKey.startsWith('::INTERRUPT')) {
718
+ return ['interrupt', listKey.split('::')[2]];
719
+ }
720
+ else if (listKey.startsWith('::EXPIRE')) {
721
+ return ['expire', listKey.split('::')[2]];
722
+ }
723
+ else {
724
+ return ['sleep', listKey];
725
+ }
726
+ }
727
+ /**
728
+ * Interrupts a job and sets sets a job error (410), if 'throw'!=false.
729
+ * This method is called by the engine and not by an activity and is
730
+ * followed by a call to execute job completion/cleanup tasks
731
+ * associated with a job completion event.
732
+ */
733
+ async interrupt(topic, jobId, options = {}) {
734
+ try {
735
+ //verify job exists
736
+ const status = await this.getStatus(jobId, this.appId);
737
+ if (status <= 0) {
738
+ //verify still active; job already completed
739
+ throw new Error(`Job ${jobId} already completed`);
740
+ }
741
+ //decrement job status (:) by 1bil
742
+ const amount = -1000000000;
743
+ const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
744
+ const result = await this.redisClient[this.commands.hincrbyfloat](jobKey, ':', amount);
745
+ if (result <= amount) {
746
+ //verify active state; job already interrupted
747
+ throw new Error(`Job ${jobId} already completed`);
748
+ }
749
+ //persist the error unless specifically told not to
750
+ if (options.throw !== false) {
751
+ const errKey = `metadata/err`; //job errors are stored at the path `metadata/err`
752
+ const symbolNames = [`$${topic}`]; //the symbol for `metadata/err` is in redis and stored using the job topic
753
+ const symKeys = await this.getSymbolKeys(symbolNames);
754
+ const symVals = await this.getSymbolValues();
755
+ this.serializer.resetSymbols(symKeys, symVals, {});
756
+ //persists the standard 410 error (job is `gone`)
757
+ const err = JSON.stringify({
758
+ code: enums_1.STATUS_CODE_INTERRUPT,
759
+ message: options.reason ?? `job [${jobId}] interrupted`,
760
+ job_id: jobId
761
+ });
762
+ const payload = { [errKey]: amount.toString() };
763
+ const hashData = this.serializer.package(payload, symbolNames);
764
+ const errSymbol = Object.keys(hashData)[0];
765
+ await this.redisClient[this.commands.hset](jobKey, errSymbol, err);
766
+ }
767
+ }
768
+ catch (e) {
769
+ if (!options.suppress) {
770
+ throw e;
771
+ }
772
+ else {
773
+ this.logger.debug('suppressed-interrupt', { message: e.message });
774
+ }
775
+ }
776
+ }
672
777
  async scrub(jobId) {
673
778
  const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
674
779
  await this.redisClient[this.commands.del](jobKey);
@@ -1,8 +1,9 @@
1
1
  /// <reference types="node" />
2
2
  import { ILogger } from '../logger';
3
3
  import { StoreService } from '../store';
4
- import { RedisClient, RedisMulti } from '../../types/redis';
5
4
  import { HookInterface } from '../../types/hook';
5
+ import { JobCompletionOptions } from '../../types/job';
6
+ import { RedisClient, RedisMulti } from '../../types/redis';
6
7
  declare class TaskService {
7
8
  store: StoreService<RedisClient, RedisMulti>;
8
9
  logger: ILogger;
@@ -10,9 +11,9 @@ declare class TaskService {
10
11
  constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
11
12
  processWebHooks(hookEventCallback: HookInterface): Promise<void>;
12
13
  enqueueWorkItems(keys: string[]): Promise<void>;
13
- registerJobForCleanup(jobId: string, inSeconds?: number): Promise<void>;
14
- registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'cron', inSeconds?: number, multi?: RedisMulti): Promise<void>;
15
- processTimeHooks(timeEventCallback: (jobId: string, activityId: string) => Promise<void>, listKey?: string): Promise<void>;
14
+ registerJobForCleanup(jobId: string, inSeconds: number, options: JobCompletionOptions): Promise<void>;
15
+ registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt', inSeconds?: number, multi?: RedisMulti): Promise<void>;
16
+ processTimeHooks(timeEventCallback: (jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt') => Promise<void>, listKey?: string): Promise<void>;
16
17
  cancelCleanup(): void;
17
18
  }
18
19
  export { TaskService };
@@ -1,11 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskService = void 0;
4
+ const enums_1 = require("../../modules/enums");
4
5
  const utils_1 = require("../../modules/utils");
5
- //system timer granularity limit (task queues organize)
6
- const FIDELITY_SECONDS = 15; //note: this can be reduced using 'watch' or scout role
7
- //default resolution/fidelity when expiring
8
- const EXPIRATION_FIDELITY_SECONDS = 60;
9
6
  class TaskService {
10
7
  constructor(store, logger) {
11
8
  this.cleanupTimeout = null;
@@ -32,27 +29,28 @@ class TaskService {
32
29
  async enqueueWorkItems(keys) {
33
30
  await this.store.addTaskQueues(keys);
34
31
  }
35
- async registerJobForCleanup(jobId, inSeconds = EXPIRATION_FIDELITY_SECONDS) {
36
- if (inSeconds > -1) {
32
+ async registerJobForCleanup(jobId, inSeconds = enums_1.EXPIRE_DURATION, options) {
33
+ if (inSeconds > 0) {
37
34
  await this.store.expireJob(jobId, inSeconds);
35
+ const expireTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (enums_1.FIDELITY_SECONDS * 1000)) * (enums_1.FIDELITY_SECONDS * 1000); //n second awaken groups
36
+ await this.store.registerExpireJob(jobId, expireTimeSlot, options);
38
37
  }
39
38
  }
40
- async registerTimeHook(jobId, activityId, type, inSeconds = FIDELITY_SECONDS, multi) {
41
- const awakenTimeSlot = Math.floor((Date.now() + inSeconds * 1000) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
39
+ async registerTimeHook(jobId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
40
+ const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (enums_1.FIDELITY_SECONDS * 1000)) * (enums_1.FIDELITY_SECONDS * 1000); //n second awaken groups
42
41
  await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
43
42
  }
44
- //todo: need 'scout' role in quorum to check for this and then alert the quorum to get to work
45
43
  async processTimeHooks(timeEventCallback, listKey) {
46
44
  try {
47
- const job = await this.store.getNextTimeJob(listKey);
48
- if (job) {
49
- const [listKey, jobId, activityId] = job;
50
- await timeEventCallback(jobId, activityId);
45
+ const timeJob = await this.store.getNextTimeJob(listKey);
46
+ if (timeJob) {
47
+ const [listKey, jobId, activityId, type] = timeJob;
48
+ await timeEventCallback(jobId, activityId, type);
51
49
  await (0, utils_1.sleepFor)(0);
52
50
  this.processTimeHooks(timeEventCallback, listKey);
53
51
  }
54
52
  else {
55
- let sleep = (0, utils_1.XSleepFor)(FIDELITY_SECONDS * 1000);
53
+ let sleep = (0, utils_1.XSleepFor)(enums_1.FIDELITY_SECONDS * 1000);
56
54
  this.cleanupTimeout = sleep.timerId;
57
55
  await sleep.promise;
58
56
  this.processTimeHooks(timeEventCallback);