File size: 3,366 Bytes
eb3f11e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
import { UPDATE_ACTIVATION_TIMEOUT_REASON } from "../../shared/update-outcome.js";
import {
  ABSOLUTE_DEADLINE_EXPIRED,
  awaitWithinDeadline,
  scheduleAbsoluteDeadline,
} from "../../utils/absolute-deadline.js";
import { UpdateCommandRecoveryPendingError } from "./update-command-recovery.js";

export class UpdateActivationTimeoutError extends UpdateCommandRecoveryPendingError {
  readonly reason = UPDATE_ACTIVATION_TIMEOUT_REASON;
  constructor(
    readonly root: string,
    readonly timeoutMs: number,
  ) {
    super(`Update activation exceeded its ${timeoutMs / 1000}-second budget.`);
    this.name = "UpdateActivationTimeoutError";
  }
}

/** The executor retains cancellation and settlement after its activation deadline. */
export function createUpdateActivationDeadline() {
  const controller = new AbortController();
  let cancelDeadline: (() => void) | undefined;
  let admission: { root: string; timeoutMs: number; deadlineAtMs: number } | undefined;
  let failure: UpdateActivationTimeoutError | undefined;
  let expire: (error: UpdateActivationTimeoutError) => void = () => {};
  const expired = new Promise<UpdateActivationTimeoutError>((resolve) => {
    expire = resolve;
  });
  const inspectDeadline = () => {
    if (!failure && admission && Date.now() >= admission.deadlineAtMs) {
      failure = new UpdateActivationTimeoutError(admission.root, admission.timeoutMs);
      controller.abort(failure);
      expire(failure);
    }
  };
  const assertCurrent = () => {
    inspectDeadline();
    controller.signal.throwIfAborted();
  };
  return {
    signal: controller.signal,
    assertCurrent,
    start(root: string, timeoutMs: number) {
      assertCurrent();
      if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
        throw new Error("Update activation requires a finite positive budget.");
      }
      if (admission) {
        return;
      }
      admission = { root, timeoutMs, deadlineAtMs: Date.now() + timeoutMs };
      cancelDeadline = scheduleAbsoluteDeadline(admission.deadlineAtMs, inspectDeadline);
    },
    async run<T>(operation: () => Promise<T>): Promise<T> {
      const work = Promise.resolve()
        .then(operation)
        .then(
          (value) => ({ value }),
          (error: unknown) => ({ error }),
        );
      try {
        const outcome = await Promise.race([work, expired]);
        inspectDeadline();
        const timeout = failure;
        if (timeout) {
          // Reuse the caller's phase budget for joining cancelled work. Expiry
          // never supplies the process-exit evidence required to release a lease.
          const joined = await awaitWithinDeadline(
            () => work,
            admission!.deadlineAtMs + timeout.timeoutMs,
          );
          timeout.message +=
            joined === ABSOLUTE_DEADLINE_EXPIRED
              ? " Cancellation did not settle within the same budget; outstanding writers retain update ownership."
              : " The update operation returned after cancellation; any unconfirmed ownership remains retained.";
          throw timeout;
        }
        if (outcome instanceof UpdateActivationTimeoutError) {
          throw outcome;
        }
        if ("error" in outcome) {
          throw outcome.error;
        }
        return outcome.value;
      } finally {
        cancelDeadline?.();
      }
    },
  };
}