Skip to content
quarry Artifact [41e5a0a469]
Login

Artifact [41e5a0a469]

Artifact 41e5a0a46987788f857b623f507874f5c29bc6f55788965814d314fdd62a2617:


// Pure decision logic for the uptime probe; no I/O so it tests without a network.

export const DOWN_THRESHOLD = 2;
export const BROKEN_THRESHOLD = 2;
export const STARTING_GRACE_MS = 3 * 60 * 1000;

export function initialState() {
  return {
    downStreak: 0,
    downSince: null,
    brokenStreak: 0,
    brokenSince: null,
    incident: null, // null | "down" | "broken"
    version: null,
    node: null,
    versionChangedAt: null,
    lastStatus: null,
    lastCheckAt: null,
  };
}

export function isHealthy(health) {
  return !!health && health.status === 200 && health.ready === true;
}

// The timeline is only worth fetching once the version endpoint is healthy.
export function needsTimeline(health) {
  return isHealthy(health);
}

// A 503 right after a version change is the new process still starting.
export function timelineBroken(timelineStatus, versionChangedAt, now) {
  if (timelineStatus === null || timelineStatus === undefined) return false;
  if (timelineStatus < 500) return false;
  if (timelineStatus === 503 && versionChangedAt !== null && now - versionChangedAt < STARTING_GRACE_MS) {
    return false;
  }
  return true;
}

// obs: {health: {status, ready, version, node, error}, timelineStatus: number|null}
// Returns {state, actions}; actions are {kind: "down"|"broken"|"recovered", since, outageMs?}.
export function step(prev, obs, now) {
  const s = { ...prev };
  const actions = [];
  const h = obs.health || {};
  s.lastCheckAt = now;
  s.lastStatus = h.status ?? null;

  if (!isHealthy(h)) {
    s.downStreak = prev.downStreak + 1;
    s.brokenStreak = 0;
    if (s.downStreak === 1) s.downSince = now;
    if (s.downStreak >= DOWN_THRESHOLD && prev.incident !== "down") {
      s.incident = "down";
      actions.push({ kind: "down", since: s.downSince });
    }
    return { state: s, actions };
  }

  if (h.version !== prev.version) {
    s.versionChangedAt = prev.version === null ? null : now;
    s.version = h.version ?? null;
  }
  s.node = h.node ?? prev.node;

  const wasDown = prev.incident === "down";
  const downSince = prev.downSince;
  s.downStreak = 0;
  s.downSince = null;

  if (timelineBroken(obs.timelineStatus, s.versionChangedAt, now)) {
    s.brokenStreak = prev.brokenStreak + 1;
    if (s.brokenStreak === 1) s.brokenSince = now;
    if (wasDown) {
      // the host answers again but the forge is still broken: the outage is not over
      s.incident = "broken";
      s.brokenSince = now;
      actions.push({ kind: "recovered", since: downSince, outageMs: now - downSince, from: "down" });
      s.brokenStreak = 1;
    } else if (s.brokenStreak >= BROKEN_THRESHOLD && prev.incident !== "broken") {
      s.incident = "broken";
      actions.push({ kind: "broken", since: s.brokenSince });
    }
    return { state: s, actions };
  }

  s.brokenStreak = 0;
  if (wasDown) {
    actions.push({ kind: "recovered", since: downSince, outageMs: now - downSince, from: "down" });
  } else if (prev.incident === "broken") {
    actions.push({ kind: "recovered", since: prev.brokenSince, outageMs: now - prev.brokenSince, from: "broken" });
  }
  if (wasDown || prev.incident === "broken") s.incident = null;
  s.brokenSince = null;
  return { state: s, actions };
}

export function formatDuration(ms) {
  const total = Math.max(0, Math.round(ms / 1000));
  const h = Math.floor(total / 3600);
  const m = Math.floor((total % 3600) / 60);
  const sec = total % 60;
  if (h > 0) return `${h}h ${m}m`;
  if (m > 0) return `${m}m ${sec}s`;
  return `${sec}s`;
}

export function composeMessage(action, state, forgeUrl) {
  const since = new Date(action.since).toISOString();
  let subject;
  if (action.kind === "down") subject = `quarry forge DOWN since ${since}`;
  else if (action.kind === "broken") subject = `quarry forge up but BROKEN since ${since}`;
  else subject = `quarry forge recovered after ${formatDuration(action.outageMs)} (since ${since})`;
  const body = [
    subject,
    "",
    `last status: ${state.lastStatus === null ? "no response" : state.lastStatus}`,
    `version: ${state.version ?? "unknown"}`,
    `node: ${state.node ?? "unknown"}`,
    `forge: ${forgeUrl}`,
    "",
  ].join("\r\n");
  return { subject, body };
}