g1t/apps/status/src/probe.ts

146 lines5,421 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1/**
2 * Running a part's check: each request it makes, all at once, with a short
3 * timeout. The answer's body is never read.
4 */
5import type { Check, Step } from "./components.ts";
6
7/** A check's raw outcome. */
8export type ProbeResult = {
9 ok: boolean;
10 /** How long it took, in milliseconds: the slowest of its requests. */
11 ms: number;
12 /** Why it failed, in a few words: "timed out", "HTTP 502". */
13 error?: string;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily14 /** Why it worked but not well, when that is not just slowness. */
15 degraded?: string;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas16};
17
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily18/** How one git store namespace answered lately: repos `store_health`. */
19export type StoreHealthRow = {
20 store: string;
21 calls: number;
22 errors: number;
23 rate_limited: number;
24 rejected: number;
25 ms_total: number;
26};
27
28export type StorageReport = { minutes: number; stores: StoreHealthRow[] };
29
30/** At least this many failed calls, and this share of them, before git storage is down. */
31const STORAGE_MIN_ERRORS = 5;
32const STORAGE_DOWN_SHARE = 0.25;
33
34/**
35 * What the git store's recent answers mean: down when a quarter or more of
36 * its calls failed (at least five), or calls were refused after repeated
37 * failures; degraded when it rate limited g1t; otherwise as fast as its
38 * mean call. Quiet is up.
39 */
40export function judgeStorage(report: StorageReport): ProbeResult {
41 const sum = (key: keyof Omit<StoreHealthRow, "store">) =>
42 report.stores.reduce((total, row) => total + (Number(row[key]) || 0), 0);
43 const calls = sum("calls");
44 const errors = sum("errors");
45 const limited = sum("rate_limited");
46 const rejected = sum("rejected");
47 const ms = calls > 0 ? sum("ms_total") / calls : 0;
48 if (rejected > 0) return { ok: false, ms, error: `calls refused after repeated failures (${rejected})` };
49 if (errors >= STORAGE_MIN_ERRORS && errors / Math.max(calls, 1) >= STORAGE_DOWN_SHARE) {
50 return { ok: false, ms, error: `${Math.round((100 * errors) / calls)}% of calls failed` };
51 }
52 if (limited > 0) return { ok: true, ms, degraded: `Rate limited ${limited} times in ${report.minutes} minutes` };
53 return { ok: true, ms };
54}
55
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas56/** No request waits longer than this. */
57export const TIMEOUT_MS = 5000;
58
59export const USER_AGENT = "g1t-status (+https://status.g1t.sh)";
60
61type Fetch = (url: string, init: RequestInit) => Promise<Response>;
62
63class Timeout extends Error {}
64
65/** Runs `work`, timing it, and failing it after `timeoutMs`. */
66export async function timed(
67 work: (signal: AbortSignal) => Promise<true | string>,
68 timeoutMs = TIMEOUT_MS,
69 now: () => number = Date.now,
70): Promise<ProbeResult> {
71 const started = now();
72 const controller = new AbortController();
73 let timer: ReturnType<typeof setTimeout> | undefined;
74 const deadline = new Promise<never>((_, reject) => {
75 timer = setTimeout(() => {
76 controller.abort();
77 reject(new Timeout());
78 }, timeoutMs);
79 });
80 try {
81 const outcome = await Promise.race([work(controller.signal), deadline]);
82 const ms = now() - started;
83 return outcome === true ? { ok: true, ms } : { ok: false, ms, error: outcome };
84 } catch (error) {
85 const ms = now() - started;
86 return { ok: false, ms, error: error instanceof Timeout ? "timed out" : "could not connect" };
87 } finally {
88 clearTimeout(timer);
89 }
90}
91
92/** One request, answered with the status that means it works. */
93export function step(fetcher: Fetch, { url, headers = {}, expect }: Step, timeoutMs = TIMEOUT_MS): Promise<ProbeResult> {
94 return timed(async (signal) => {
95 const response = await fetcher(url, {
96 signal,
97 redirect: "manual",
98 headers: { "user-agent": USER_AGENT, "cache-control": "no-cache", ...headers },
99 });
100 await response.body?.cancel().catch(() => undefined);
101 const good = expect == null ? response.ok : response.status === expect;
102 return good || `HTTP ${response.status}`;
103 }, timeoutMs);
104}
105
106/** A check's requests together: it works when every one does, and takes as long as the slowest. */
107export function combine(results: ProbeResult[]): ProbeResult {
108 const ms = Math.max(0, ...results.map((r) => r.ms));
109 const failed = results.find((r) => !r.ok);
110 return failed ? { ok: false, ms, error: failed.error ?? "no answer" } : { ok: true, ms };
111}
112
113/** What runs a check. `billing` is null when there is no binding to it. */
114export type Probers = {
115 fetch: Fetch;
116 billing: (() => Promise<unknown>) | null;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily117 /** Git storage's recent health, through the repos service; null when not bound. */
118 storage?: (() => Promise<StorageReport>) | null;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas119 timeoutMs?: number;
120};
121
122/** Runs one part's check. Null for a part with no check. */
123export async function runCheck(check: Check, probers: Probers): Promise<ProbeResult | null> {
124 const timeoutMs = probers.timeoutMs ?? TIMEOUT_MS;
125 switch (check.kind) {
126 case "http":
127 return combine(await Promise.all(check.steps.map((s) => step(probers.fetch, s, timeoutMs))));
128 case "billing": {
129 const billing = probers.billing;
130 if (!billing) return null;
131 return timed(async () => ((await billing()) ? true : "no price book"), timeoutMs);
132 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily133 case "storage": {
134 const storage = probers.storage;
135 if (!storage) return null;
136 let report: StorageReport | null = null;
137 const asked = await timed(async () => {
138 report = await storage();
139 return true;
140 }, timeoutMs);
141 return report ? judgeStorage(report) : asked;
142 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas143 case "none":
144 return null;
145 }
146}