cairnq 0.6.0 → 0.7.0

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/src/worker.ts CHANGED
@@ -4,6 +4,7 @@ import { TaskContext } from "./context.js";
4
4
  import {
5
5
  asEnvelope,
6
6
  errorEnvelope,
7
+ EventLoopBlocked,
7
8
  exceptionEnvelope,
8
9
  type FailReason,
9
10
  LostLease,
@@ -923,6 +924,28 @@ export class Worker {
923
924
  let wake: (() => void) | null = null;
924
925
  // lease/3 gives two beats of slack; the floor only matters for sub-150ms leases.
925
926
  const interval = this.opts.heartbeatIntervalMs ?? Math.max(50, Math.floor(leaseMs / 3));
927
+
928
+ // When this heartbeat last ran. A loop cannot report its own absence — a
929
+ // handler that blocks for its whole attempt never lets the timer fire at all
930
+ // — so the check lives outside the loop and cancel() runs it too.
931
+ let lastBeatAt = Date.now();
932
+ /** Report a heartbeat that has not run for more than two intervals: a whole
933
+ * beat missed, which at lease/3 means the next such block loses the lease
934
+ * outright. Fires while the lease still holds — after it expires the only
935
+ * evidence is a task that ran twice, in two workers' logs, with no error in
936
+ * either. */
937
+ const checkBeat = (): void => {
938
+ const now = Date.now();
939
+ const lateMs = now - lastBeatAt - interval;
940
+ if (lateMs > interval) {
941
+ this.report(new EventLoopBlocked(lateMs, interval, leaseMs), {
942
+ phase: "execute",
943
+ taskId: ctxs[0].taskId,
944
+ });
945
+ }
946
+ lastBeatAt = now;
947
+ };
948
+
926
949
  const done = (async () => {
927
950
  while (active) {
928
951
  // Cancellable sleep: cancel() resolves this immediately and clears the
@@ -936,6 +959,7 @@ export class Worker {
936
959
  });
937
960
  wake = null;
938
961
  if (!active) break;
962
+ checkBeat();
939
963
  const live = ctxs.filter((c) => !c.settled && !c.lostLease);
940
964
  if (!live.length) break;
941
965
  try {
@@ -960,6 +984,10 @@ export class Worker {
960
984
  cancel: () => {
961
985
  active = false;
962
986
  if (wake) wake();
987
+ // The attempt is over, so this is the last chance to notice that the
988
+ // heartbeat never got one — the whole-attempt block, which is also the
989
+ // one where the lease is already gone.
990
+ checkBeat();
963
991
  },
964
992
  done,
965
993
  };