Skip to content
83 linesCodeBlameRaw
1/**
2 * When a job's page asks for more of its log (components/actions.tsx,
3 * `useJobLog`): about every second while a step is running, every few
4 * seconds while the job waits between steps or for a machine, and, when
5 * an answer fails (a 429, a 5xx, the network), again after a wait that
6 * doubles up to half a minute, or as long as `Retry-After` says. Nothing
7 * stops it for good but the job finishing or being gone.
8 */
9
10/** While a step is running. */
11export const RUNNING_POLL_MS = 1000;
12/** While the job is live but no step is running: queued, starting, between steps. */
13export const IDLE_POLL_MS = 3000;
14/** The first wait after a failure, doubled after each one that follows. */
15export const FIRST_RETRY_MS = 2000;
16/** The longest wait between tries after failures. */
17export const MAX_RETRY_MS = 30_000;
18/** A finished job's last fetch is tried this many times before it gives up. */
19export const FINISHED_TRIES = 5;
20
21export type PollAnswer =
22 /** An answer: whether the job is done, and whether the page was full (more is waiting). */
23 | { ok: true; done: boolean; full: boolean }
24 /** No answer: its status, if there was one, and its `Retry-After` in milliseconds. */
25 | { ok: false; status?: number; retryAfterMs?: number };
26
27export type PollState = {
28 /** Failures in a row so far. */
29 failures: number;
30 /** Whether the page shows the job as still going. */
31 live: boolean;
32 /** Whether one of its steps is running now. */
33 running: boolean;
34};
35
36/** How long to wait before the next fetch, or null to stop; and the failures in a row after this answer. */
37export function nextPoll(answer: PollAnswer, state: PollState): { delayMs: number | null; failures: number } {
38 if (answer.ok) {
39 if (answer.full) return { delayMs: 0, failures: 0 };
40 if (answer.done || !state.live) return { delayMs: null, failures: 0 };
41 return { delayMs: state.running ? RUNNING_POLL_MS : IDLE_POLL_MS, failures: 0 };
42 }
43 const failures = state.failures + 1;
44 // The job is not there (deleted, or the repository is gone): nothing to wait for.
45 if (answer.status === 404) return { delayMs: null, failures };
46 if (!state.live && failures >= FINISHED_TRIES) return { delayMs: null, failures };
47 const backoff = Math.min(MAX_RETRY_MS, FIRST_RETRY_MS * 2 ** (failures - 1));
48 const asked = answer.retryAfterMs !== undefined ? Math.min(answer.retryAfterMs, 2 * MAX_RETRY_MS) : 0;
49 return { delayMs: Math.max(backoff, asked), failures };
50}
51
52/** How long a running step may print nothing before its log says how long it has been quiet. */
53export const QUIET_AFTER_MS = 10_000;
54
55/** `45s`, `2m 10s`, `1h 5m`, as the run's page counts time. */
56export function elapsed(ms: number): string {
57 const seconds = Math.max(0, Math.round(ms / 1000));
58 if (seconds < 60) return `${seconds}s`;
59 const minutes = Math.floor(seconds / 60);
60 if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
61 return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
62}
63
64/**
65 * What a running step's log says under its last line: that nothing has
66 * come yet, and for how long, or, once it has gone quiet for a while
67 * (a build that prints nothing as it links, a download), for how long it
68 * has printed nothing. Null while output is flowing. Times in ms.
69 */
70export function quietWords(started: number | null, lastOutput: number | null, now: number): string | null {
71 if (lastOutput === null) return started === null ? "Running · no output yet" : `Running · no output yet, ${elapsed(now - started)}`;
72 if (now - lastOutput < QUIET_AFTER_MS) return null;
73 return `Running · no output for ${elapsed(now - lastOutput)}`;
74}
75
76/** `Retry-After` in milliseconds: seconds, or an HTTP date. */
77export function retryAfterMs(header: string | null, now = Date.now()): number | undefined {
78 if (!header) return undefined;
79 const seconds = Number(header.trim());
80 if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1000;
81 const at = Date.parse(header);
82 return Number.isFinite(at) ? Math.max(0, at - now) : undefined;
83}