| 1 | /** |
| 2 | * How long the site waits on a service before it gives up. Nothing on |
| 3 | * Workers ends a subrequest that stops answering: the page waits as long |
| 4 | * as the visitor does, so a service stuck on one call holds the page on |
| 5 | * its loading bar for good. With a deadline it fails instead, as an error |
| 6 | * the page can draw and the next click can get past. |
| 7 | */ |
| 8 | |
| 9 | /** A read: no page should wait longer than this on one service call. */ |
| 10 | export const READ_DEADLINE_MS = 15_000; |
| 11 | |
| 12 | /** A write: some do real work (a merge, a commit), so they get longer. */ |
| 13 | export const WRITE_DEADLINE_MS = 60_000; |
| 14 | |
| 15 | /** Reads that do real work inline the first time, or walk history. */ |
| 16 | export const SLOW_READ_DEADLINE_MS = 60_000; |
| 17 | |
| 18 | /** Jobs a person starts and waits on: an import, a model's draft, a computer waking. */ |
| 19 | export const LONG_DEADLINE_MS = 600_000; |
| 20 | |
| 21 | /** |
| 22 | * Reads that can take longer than `READ_DEADLINE_MS` and still be right, |
| 23 | * by service binding and method. Cut off, their work is lost and starts |
| 24 | * again on the next visit, so they get the time to finish. |
| 25 | */ |
| 26 | const SLOW_READS = new Set([ |
| 27 | // A repository's first look runs its dependency scan inline (services/security/src/deps.rs). |
| 28 | "security overview", |
| 29 | "security dependency_graph", |
| 30 | "security sbom", |
| 31 | // Walks up to 400 commits, reading trees and blobs one after another (services/repos/src/blame.rs). |
| 32 | "repos blame", |
| 33 | // Up to 2,500 store reads per call (services/repos/src/last_commits.rs). |
| 34 | "repos last_commits", |
| 35 | ]); |
| 36 | |
| 37 | /** |
| 38 | * Writes that run for minutes and still finish: copying a repository in, |
| 39 | * syncing a mirror, a model writing a draft, a computer's home moved in or |
| 40 | * out of storage, a scan over many repositories. |
| 41 | */ |
| 42 | const LONG_CALLS = new Set([ |
| 43 | "repos create", |
| 44 | "integrations github_import", |
| 45 | "integrations mirror_add", |
| 46 | "integrations mirror_sync", |
| 47 | "integrations mirror_move_in", |
| 48 | "integrations mirror_hand_back", |
| 49 | "agents draft", |
| 50 | "agents try_draft", |
| 51 | "agents redraft", |
| 52 | "agents draft_skill", |
| 53 | "agents sync_skill_mirror", |
| 54 | "agents computer_wake", |
| 55 | "agents computer_sleep", |
| 56 | "security dry_run_pattern", |
| 57 | "security rescan", |
| 58 | "security check_updates", |
| 59 | "artifacts add_repo_space", |
| 60 | ]); |
| 61 | |
| 62 | /** How long a call to `service`'s `method` may take; `writes` as lib/perf.ts `mayWrite` says. */ |
| 63 | export function deadlineFor(service: string, method: string, writes: boolean): number { |
| 64 | const call = `${service} ${method}`; |
| 65 | if (LONG_CALLS.has(call)) return LONG_DEADLINE_MS; |
| 66 | if (SLOW_READS.has(call)) return SLOW_READ_DEADLINE_MS; |
| 67 | return writes ? WRITE_DEADLINE_MS : READ_DEADLINE_MS; |
| 68 | } |
| 69 | |
| 70 | /** What a call past its deadline throws. */ |
| 71 | export class DeadlineExceeded extends Error { |
| 72 | constructor(what: string, ms: number) { |
| 73 | super(`${what} did not answer within ${ms}ms`); |
| 74 | this.name = "DeadlineExceeded"; |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | /** |
| 79 | * `run`'s answer, or a `DeadlineExceeded` after `ms`. `run` is handed a |
| 80 | * signal that aborts at the deadline, so the call it makes is cancelled |
| 81 | * too; `outer`, when given, aborts it sooner. Whatever `run` reads must be |
| 82 | * read inside it: the deadline ends when it returns. |
| 83 | */ |
| 84 | export async function withDeadline<T>( |
| 85 | what: string, |
| 86 | ms: number, |
| 87 | run: (signal: AbortSignal) => Promise<T>, |
| 88 | outer?: AbortSignal | null, |
| 89 | ): Promise<T> { |
| 90 | const controller = new AbortController(); |
| 91 | let timer: ReturnType<typeof setTimeout> | undefined; |
| 92 | const expired = new Promise<never>((_, reject) => { |
| 93 | timer = setTimeout(() => { |
| 94 | const error = new DeadlineExceeded(what, ms); |
| 95 | controller.abort(error); |
| 96 | reject(error); |
| 97 | }, ms); |
| 98 | }); |
| 99 | const stop = () => controller.abort(outer?.reason); |
| 100 | if (outer?.aborted) stop(); |
| 101 | else outer?.addEventListener("abort", stop, { once: true }); |
| 102 | try { |
| 103 | // Raced as well as aborted: a binding that ignores the signal still lets go. |
| 104 | return await Promise.race([run(controller.signal), expired]); |
| 105 | } finally { |
| 106 | clearTimeout(timer); |
| 107 | outer?.removeEventListener("abort", stop); |
| 108 | } |
| 109 | } |