@hatchet-dev/typescript-sdk 1.34.0 → 1.34.1

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.
@@ -28,5 +28,6 @@ export declare class ActionListener {
28
28
  getListenStrategy(): Promise<ListenStrategy>;
29
29
  incrementRetries(): Promise<void>;
30
30
  getListenClient(): Promise<AsyncIterable<AssignedAction>>;
31
+ stopStream(): void;
31
32
  unregister(): Promise<import("../../protoc/dispatcher").WorkerUnsubscribeResponse>;
32
33
  }
@@ -225,14 +225,19 @@ class ActionListener {
225
225
  }
226
226
  });
227
227
  }
228
+ // Leaves the heartbeat running: tasks that are already in flight still need
229
+ // the engine to see this worker as alive until they finish.
230
+ stopStream() {
231
+ this.done = true;
232
+ // Abort the gRPC stream to immediately cancel the generator
233
+ if (this.abortController) {
234
+ this.abortController.abort('Worker stopping');
235
+ }
236
+ }
228
237
  unregister() {
229
238
  return __awaiter(this, void 0, void 0, function* () {
230
- this.done = true;
239
+ this.stopStream();
231
240
  this.heartbeat.stop();
232
- // Abort the gRPC stream to immediately cancel the generator
233
- if (this.abortController) {
234
- this.abortController.abort('Worker stopping');
235
- }
236
241
  try {
237
242
  return yield this.client.unsubscribe({
238
243
  workerId: this.workerId,
@@ -18,6 +18,9 @@ export declare class DurableListenerClient {
18
18
  private _pendingCallbacks;
19
19
  private _orderedCompletions;
20
20
  private _pendingEvictionAcks;
21
+ private _newlyWaitingKeys;
22
+ private _waitingSince;
23
+ private _workerStatusSendScheduled;
21
24
  private _receiveAbort;
22
25
  private _statusInterval;
23
26
  private _startLock;
@@ -34,13 +37,20 @@ export declare class DurableListenerClient {
34
37
  private _requestIterator;
35
38
  private _enqueueRequest;
36
39
  private _startStatusPolling;
37
- private _pollWorkerStatus;
40
+ private _registerWaitingKey;
41
+ private _scheduleWorkerStatusSend;
42
+ private _enqueueWorkerStatusForNewlyWaitingKeys;
43
+ private _enqueueWorkerStatusForLongPendingWaiters;
44
+ private _enqueueFullWorkerStatus;
45
+ private _enqueueWorkerStatusFor;
38
46
  private _failPendingAcks;
39
47
  private _failAllPending;
40
48
  private _handleResponse;
41
49
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: RunChildrenEvent): Promise<DurableTaskEventRunAck>;
42
50
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: WaitForEvent): Promise<DurableTaskEventWaitForAck>;
43
51
  sendEvent(durableTaskExternalId: string, invocationCount: number, event: MemoEvent): Promise<DurableTaskEventMemoAck>;
52
+ private _sendRequestAndAwaitAck;
53
+ private _sendRunChildrenEvent;
44
54
  private _drainsInProgress;
45
55
  private _drainRerunRequests;
46
56
  private _drainOrderedCompletions;
@@ -94,7 +94,10 @@ class TTLMap {
94
94
  }
95
95
  const DEFAULT_RECONNECT_INTERVAL = 3000;
96
96
  const EVICTION_ACK_TIMEOUT_MS = 30000;
97
- const WORKER_STATUS_POLL_INTERVAL_MS = 1000;
97
+ const WORKER_STATUS_RESEND_INTERVAL_MS = 5000;
98
+ const WORKER_STATUS_RESEND_MIN_PENDING_MS = 2000;
99
+ const WORKER_STATUS_MAX_ENTRIES_PER_REQUEST = 10000;
100
+ const RUN_CHILDREN_MAX_CHILDREN_PER_REQUEST = 100;
98
101
  function eventLogEntryResultFromProto(proto) {
99
102
  var _a, _b, _c, _d;
100
103
  let payload;
@@ -109,6 +112,15 @@ function eventLogEntryResultFromProto(proto) {
109
112
  errorMessage: proto.errorMessage,
110
113
  };
111
114
  }
115
+ function awaitedCompletedEntryFromCallbackKey(key) {
116
+ const [durableTaskExternalId, invocationCount, branchId, nodeId] = key.split(':');
117
+ return {
118
+ durableTaskExternalId,
119
+ invocationCount: parseInt(invocationCount, 10),
120
+ branchId: parseInt(branchId, 10),
121
+ nodeId: parseInt(nodeId, 10),
122
+ };
123
+ }
112
124
  function ackKey(taskExtId, invocationCount) {
113
125
  return `${taskExtId}:${invocationCount}`;
114
126
  }
@@ -142,6 +154,9 @@ class DurableListenerClient {
142
154
  // without consuming everything; the server stall-evicts long before.
143
155
  this._orderedCompletions = new TTLMap(300000);
144
156
  this._pendingEvictionAcks = new Map();
157
+ this._newlyWaitingKeys = new Set();
158
+ this._waitingSince = new Map();
159
+ this._workerStatusSendScheduled = false;
145
160
  this._consecutiveFailures = 0;
146
161
  this._drainsInProgress = new Set();
147
162
  this._drainRerunRequests = new Set();
@@ -205,7 +220,7 @@ class DurableListenerClient {
205
220
  this._enqueueRequest({
206
221
  registerWorker: { workerId: this._workerId },
207
222
  });
208
- this._pollWorkerStatus();
223
+ this._enqueueFullWorkerStatus();
209
224
  void this._streamLoop();
210
225
  this.logger.info('durable event listener connected');
211
226
  });
@@ -300,28 +315,57 @@ class DurableListenerClient {
300
315
  clearInterval(this._statusInterval);
301
316
  }
302
317
  this._statusInterval = setInterval(() => {
303
- this._pollWorkerStatus();
304
- }, WORKER_STATUS_POLL_INTERVAL_MS);
318
+ this._enqueueWorkerStatusForLongPendingWaiters();
319
+ }, WORKER_STATUS_RESEND_INTERVAL_MS);
305
320
  }
306
- _pollWorkerStatus() {
307
- if (!this._workerId || this._pendingCallbacks.size === 0)
321
+ _registerWaitingKey(key) {
322
+ this._waitingSince.set(key, Date.now());
323
+ this._newlyWaitingKeys.add(key);
324
+ }
325
+ // Waiters registered in the same turn of the event loop share one request,
326
+ // instead of each sending its own.
327
+ _scheduleWorkerStatusSend() {
328
+ if (this._workerStatusSendScheduled)
308
329
  return;
309
- const waitingEntries = [];
310
- for (const key of this._pendingCallbacks.keys()) {
311
- const parts = key.split(':');
312
- waitingEntries.push({
313
- durableTaskExternalId: parts[0],
314
- invocationCount: parseInt(parts[1], 10),
315
- branchId: parseInt(parts[2], 10),
316
- nodeId: parseInt(parts[3], 10),
330
+ this._workerStatusSendScheduled = true;
331
+ setImmediate(() => this._enqueueWorkerStatusForNewlyWaitingKeys());
332
+ }
333
+ _enqueueWorkerStatusForNewlyWaitingKeys() {
334
+ this._workerStatusSendScheduled = false;
335
+ const keys = [...this._newlyWaitingKeys].filter((key) => this._pendingCallbacks.has(key));
336
+ this._newlyWaitingKeys.clear();
337
+ this._enqueueWorkerStatusFor(keys);
338
+ }
339
+ _enqueueWorkerStatusForLongPendingWaiters() {
340
+ const registeredBefore = Date.now() - WORKER_STATUS_RESEND_MIN_PENDING_MS;
341
+ const keys = [];
342
+ for (const [key, waitingSince] of this._waitingSince) {
343
+ if (!this._pendingCallbacks.has(key)) {
344
+ this._waitingSince.delete(key);
345
+ }
346
+ else if (waitingSince <= registeredBefore) {
347
+ keys.push(key);
348
+ }
349
+ }
350
+ this._enqueueWorkerStatusFor(keys);
351
+ }
352
+ _enqueueFullWorkerStatus() {
353
+ this._newlyWaitingKeys.clear();
354
+ this._enqueueWorkerStatusFor([...this._pendingCallbacks.keys()]);
355
+ }
356
+ _enqueueWorkerStatusFor(keys, maxEntriesPerRequest = WORKER_STATUS_MAX_ENTRIES_PER_REQUEST) {
357
+ if (!this._workerId)
358
+ return;
359
+ for (let start = 0; start < keys.length; start += maxEntriesPerRequest) {
360
+ this._enqueueRequest({
361
+ workerStatus: {
362
+ workerId: this._workerId,
363
+ waitingEntries: keys
364
+ .slice(start, start + maxEntriesPerRequest)
365
+ .map(awaitedCompletedEntryFromCallbackKey),
366
+ },
317
367
  });
318
368
  }
319
- this._enqueueRequest({
320
- workerStatus: {
321
- workerId: this._workerId,
322
- waitingEntries,
323
- },
324
- });
325
369
  }
326
370
  _failPendingAcks(exc) {
327
371
  for (const d of this._pendingEventAcks.values()) {
@@ -406,13 +450,15 @@ class DurableListenerClient {
406
450
  const orderKey = completionOrderKey((_s = ref === null || ref === void 0 ? void 0 : ref.durableTaskExternalId) !== null && _s !== void 0 ? _s : '', (_t = ref === null || ref === void 0 ? void 0 : ref.invocationCount) !== null && _t !== void 0 ? _t : 0);
407
451
  const queue = (_u = this._orderedCompletions.get(orderKey)) !== null && _u !== void 0 ? _u : {
408
452
  pending: [],
453
+ pendingKeys: new Set(),
409
454
  delivered: new Set(),
410
455
  };
411
456
  if (!queue.delivered.has(key)) {
412
457
  queue.delivered.add(key);
458
+ queue.pendingKeys.add(key);
413
459
  queue.pending.push({ key, result });
414
460
  }
415
- else if (queue.pending.every((entry) => entry.key !== key)) {
461
+ else if (!queue.pendingKeys.has(key)) {
416
462
  // Re-delivery of a completion that already drained (reconnect,
417
463
  // worker-status re-send, or a repeated wait on a node deduped by
418
464
  // child key). Its satisfied order was released before anything
@@ -478,20 +524,10 @@ class DurableListenerClient {
478
524
  }
479
525
  sendEvent(durableTaskExternalId, invocationCount, event) {
480
526
  return __awaiter(this, void 0, void 0, function* () {
481
- const key = ackKey(durableTaskExternalId, invocationCount);
482
- const d = deferred();
483
- this._pendingEventAcks.set(key, d);
484
527
  let request;
485
528
  switch (event.kind) {
486
- case 'runChildren': {
487
- const triggerRunsReq = {
488
- invocationCount,
489
- durableTaskExternalId,
490
- triggerOpts: event.triggerOpts,
491
- };
492
- request = { triggerRuns: triggerRunsReq };
493
- break;
494
- }
529
+ case 'runChildren':
530
+ return this._sendRunChildrenEvent(durableTaskExternalId, invocationCount, event);
495
531
  case 'waitFor': {
496
532
  const waitForReq = {
497
533
  invocationCount,
@@ -517,8 +553,33 @@ class DurableListenerClient {
517
553
  throw new Error(`Unknown durable task send event: ${_}`);
518
554
  }
519
555
  }
520
- this._enqueueRequest(request);
521
- return d.promise;
556
+ return this._sendRequestAndAwaitAck(durableTaskExternalId, invocationCount, request);
557
+ });
558
+ }
559
+ _sendRequestAndAwaitAck(durableTaskExternalId, invocationCount, request) {
560
+ const d = deferred();
561
+ this._pendingEventAcks.set(ackKey(durableTaskExternalId, invocationCount), d);
562
+ this._enqueueRequest(request);
563
+ return d.promise;
564
+ }
565
+ _sendRunChildrenEvent(durableTaskExternalId, invocationCount, event) {
566
+ return __awaiter(this, void 0, void 0, function* () {
567
+ const runEntries = [];
568
+ for (let start = 0; start < event.triggerOpts.length; start += RUN_CHILDREN_MAX_CHILDREN_PER_REQUEST) {
569
+ const triggerRunsReq = {
570
+ invocationCount,
571
+ durableTaskExternalId,
572
+ triggerOpts: event.triggerOpts.slice(start, start + RUN_CHILDREN_MAX_CHILDREN_PER_REQUEST),
573
+ };
574
+ const ack = yield this._sendRequestAndAwaitAck(durableTaskExternalId, invocationCount, {
575
+ triggerRuns: triggerRunsReq,
576
+ });
577
+ if (ack.ackType !== 'run') {
578
+ throw new Error(`Expected a run ack for a trigger runs request, got ${ack.ackType}`);
579
+ }
580
+ runEntries.push(...ack.runEntries);
581
+ }
582
+ return { ackType: 'run', invocationCount, durableTaskExternalId, runEntries };
522
583
  });
523
584
  }
524
585
  // Hands queued completions to their waiters strictly in delivery order.
@@ -560,6 +621,7 @@ class DurableListenerClient {
560
621
  if (!waiter)
561
622
  return;
562
623
  queue.pending.shift();
624
+ queue.pendingKeys.delete(head.key);
563
625
  this._pendingCallbacks.delete(head.key);
564
626
  waiter.resolve(head.result);
565
627
  released = true;
@@ -577,6 +639,7 @@ class DurableListenerClient {
577
639
  const d = deferred();
578
640
  d.promise.catch(() => { });
579
641
  this._pendingCallbacks.set(key, d);
642
+ this._registerWaitingKey(key);
580
643
  this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
581
644
  }
582
645
  waitForCallback(durableTaskExternalId, invocationCount, branchId, nodeId, opts) {
@@ -594,9 +657,10 @@ class DurableListenerClient {
594
657
  // not crash the process as an unhandled rejection.
595
658
  d.promise.catch(() => { });
596
659
  this._pendingCallbacks.set(key, d);
660
+ this._registerWaitingKey(key);
597
661
  this._drainOrderedCompletions(completionOrderKey(durableTaskExternalId, invocationCount));
598
662
  if (this._pendingCallbacks.has(key)) {
599
- this._pollWorkerStatus();
663
+ this._scheduleWorkerStatusSend();
600
664
  }
601
665
  }
602
666
  if (!signal) {
@@ -656,6 +720,10 @@ class DurableListenerClient {
656
720
  sendEvictInvocation(durableTaskExternalId, invocationCount, reason) {
657
721
  return __awaiter(this, void 0, void 0, function* () {
658
722
  const key = evictionKey(durableTaskExternalId, invocationCount);
723
+ const pending = this._pendingEvictionAcks.get(key);
724
+ if (pending) {
725
+ return pending.promise;
726
+ }
659
727
  const d = deferred();
660
728
  this._pendingEvictionAcks.set(key, d);
661
729
  const req = {
@@ -664,15 +732,17 @@ class DurableListenerClient {
664
732
  reason,
665
733
  };
666
734
  this._enqueueRequest({ evictInvocation: req });
667
- const timeout = (0, sleep_1.default)(EVICTION_ACK_TIMEOUT_MS).then(() => {
668
- throw new Error(`Eviction ack timed out after ${EVICTION_ACK_TIMEOUT_MS}ms for task ${durableTaskExternalId} invocation ${invocationCount}`);
669
- });
735
+ const timeout = setTimeout(() => {
736
+ d.reject(new Error(`Eviction ack timed out after ${EVICTION_ACK_TIMEOUT_MS}ms for task ${durableTaskExternalId} invocation ${invocationCount}`));
737
+ }, EVICTION_ACK_TIMEOUT_MS);
670
738
  try {
671
- yield Promise.race([d.promise, timeout]);
739
+ yield d.promise;
672
740
  }
673
- catch (err) {
674
- this._pendingEvictionAcks.delete(key);
675
- throw err;
741
+ finally {
742
+ clearTimeout(timeout);
743
+ if (this._pendingEvictionAcks.get(key) === d) {
744
+ this._pendingEvictionAcks.delete(key);
745
+ }
676
746
  }
677
747
  });
678
748
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hatchet-dev/typescript-sdk",
3
- "version": "1.34.0",
3
+ "version": "1.34.1",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },
@@ -45,6 +45,7 @@ export declare class InternalWorker {
45
45
  enableHealthServer: boolean;
46
46
  private healthServer;
47
47
  private status;
48
+ private shutdownPromise;
48
49
  constructor(client: HatchetClient, options: {
49
50
  name: string;
50
51
  handleKill?: boolean;
@@ -96,6 +97,7 @@ export declare class InternalWorker {
96
97
  handleCancelStepRun(action: Action): Promise<void>;
97
98
  stop(): Promise<void>;
98
99
  exitGracefully(handleKill: boolean): Promise<void>;
100
+ private shutdown;
99
101
  /**
100
102
  * Creates an action listener by registering the worker with the dispatcher.
101
103
  * Override in subclasses to change registration behavior (e.g. legacy engines).
@@ -628,6 +628,18 @@ class InternalWorker {
628
628
  exitGracefully(handleKill) {
629
629
  return __awaiter(this, void 0, void 0, function* () {
630
630
  var _a;
631
+ // Signals and an explicit stop must await the same eviction and listener teardown.
632
+ (_a = this.shutdownPromise) !== null && _a !== void 0 ? _a : (this.shutdownPromise = this.shutdown());
633
+ yield this.shutdownPromise;
634
+ if (handleKill) {
635
+ this.logger.info('Exiting hatchet worker...');
636
+ process.exit(0);
637
+ }
638
+ });
639
+ }
640
+ shutdown() {
641
+ return __awaiter(this, void 0, void 0, function* () {
642
+ var _a, _b;
631
643
  this.killing = true;
632
644
  this.setStatus(health_server_1.workerStatus.UNHEALTHY);
633
645
  this.logger.info('Starting to exit...');
@@ -658,16 +670,19 @@ class InternalWorker {
658
670
  catch (e) {
659
671
  this.logger.error(`Could not stop durable listener: ${e.message}`);
660
672
  }
673
+ (_a = this.listener) === null || _a === void 0 ? void 0 : _a.stopStream();
674
+ this.logger.info('Gracefully exiting hatchet worker, running tasks will attempt to finish...');
675
+ // attempt to wait for futures to finish
676
+ yield Promise.all(Object.values(this.futures).map(({ promise }) => promise));
677
+ this.logger.info('Successfully finished pending tasks.');
678
+ // Unregistering stops the heartbeat, so it has to wait until the running tasks are done:
679
+ // otherwise the engine sees a dead worker and reassigns tasks that are about to complete.
661
680
  try {
662
- yield ((_a = this.listener) === null || _a === void 0 ? void 0 : _a.unregister());
681
+ yield ((_b = this.listener) === null || _b === void 0 ? void 0 : _b.unregister());
663
682
  }
664
683
  catch (e) {
665
684
  this.logger.error(`Could not unregister listener: ${e.message}`);
666
685
  }
667
- this.logger.info('Gracefully exiting hatchet worker, running tasks will attempt to finish...');
668
- // attempt to wait for futures to finish
669
- yield Promise.all(Object.values(this.futures).map(({ promise }) => promise));
670
- this.logger.info('Successfully finished pending tasks.');
671
686
  if (this.healthServer) {
672
687
  try {
673
688
  yield this.healthServer.stop();
@@ -676,10 +691,6 @@ class InternalWorker {
676
691
  this.logger.error(`Could not stop health server: ${e.message}`);
677
692
  }
678
693
  }
679
- if (handleKill) {
680
- this.logger.info('Exiting hatchet worker...');
681
- process.exit(0);
682
- }
683
694
  });
684
695
  }
685
696
  /**
@@ -700,10 +711,18 @@ class InternalWorker {
700
711
  start() {
701
712
  return __awaiter(this, void 0, void 0, function* () {
702
713
  var _a, e_1, _b, _c;
714
+ if (this.killing) {
715
+ return;
716
+ }
703
717
  this.setStatus(health_server_1.workerStatus.STARTING);
704
718
  if (this.healthServer) {
705
719
  try {
706
720
  yield this.healthServer.start();
721
+ if (this.killing) {
722
+ yield this.shutdownPromise;
723
+ yield this.healthServer.stop();
724
+ return;
725
+ }
707
726
  }
708
727
  catch (e) {
709
728
  this.logger.error(`Could not start health server: ${e.message}`);
@@ -713,11 +732,17 @@ class InternalWorker {
713
732
  }
714
733
  // ensure all workflows are registered
715
734
  yield Promise.all(this.registeredWorkflowPromises);
716
- if (Object.keys(this.action_registry).length === 0) {
735
+ if (this.killing || Object.keys(this.action_registry).length === 0) {
717
736
  return;
718
737
  }
719
738
  try {
720
- this.listener = yield this.createListener();
739
+ const listener = yield this.createListener();
740
+ if (this.killing) {
741
+ // Shutdown cannot clean up a listener that registration has not returned yet.
742
+ yield listener.unregister();
743
+ return;
744
+ }
745
+ this.listener = listener;
721
746
  this.workerId = this.listener.workerId;
722
747
  this.setStatus(health_server_1.workerStatus.HEALTHY);
723
748
  const generator = this.listener.actions();
package/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const HATCHET_VERSION = "1.34.0";
1
+ export declare const HATCHET_VERSION = "1.34.1";
package/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HATCHET_VERSION = void 0;
4
- exports.HATCHET_VERSION = '1.34.0';
4
+ exports.HATCHET_VERSION = '1.34.1';