Repository navigation
feat(gate): admit at once while the host has headroom - #189
Conversation
Quest-Claim: 264968c8b3e93113
Each admission from the last minute counts as one more unit of load and one command's memory budget. Commands start at once while load and memory still fit counting those, one at a time SETTLE_MS after the previous admission while only the raw measures fit, and otherwise wait. Admissions live in a lock-free ledger of numbered versions published by hard-linking a complete draft, so concurrent callers never exceed the rule, a burst always elects a winner, and a crashed caller blocks no one. Every caller of one user on one host shares ~/.cache/quest/admissions/<hostname>, whatever its environment or login session. A terminal Ctrl-C now reaches the child of `quest gate` and `quest worktree run` once: SIGINT is not forwarded while Quest is in the terminal's foreground process group; SIGHUP and SIGTERM always are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Implementation pushed in f13f8c6. (Written by Claude Opus 5.5) |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f13f8c629f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A caller stalled for SETTLE_MS or more between sampling the host and publishing now decides again, and every admission records the time it was published rather than the time it was sampled. Bun's os.userInfo() prefers $HOME, so the ledger directory followed the caller's environment. The account's home now comes from a child process without $HOME, and an account missing from the user database uses /tmp/quest-<uid> instead of falling back to $HOME. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07b0ead14e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Another local user can create /tmp/quest-<uid> first. Create it with mode 0700 and refuse it unless it is a real directory, owned by the UID, that no one else can write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merge summary for this round:
The maintainer's earlier decisions stand. New implementation choices are listed under Decisions 3 and 4 in the description. (Written by Claude Opus 5.5) |
Finishes
quest/m0/gate-headroom.md, planned in kixelated/quest#180 after the measurements in kixelated/quest#177.Changes
src/cli/gate.ts,admit()). Each admission from the last minute counts as one more unit of load and one command's memory budget:SETTLE_MS(5 s) after the previous admission. Waiters sleep until that moment, not to a window boundary.Host.claim(window)is replaced byledger()/publish(base, admissions). The ledger is a directory of numbered versions, each the JSON list of the last minute's admissions. A caller decides against the newest version, then publishes version N + 1 by hard-linking a complete draft to that name. Only one caller can create a given name, so a burst elects exactly one winner per version; the losers re-read and decide again. No lock is held, so a caller that crashes at any point blocks no one, on every platform, and there is no PID check to fool. A caller that stalls and re-creates a pruned version number sees that the newest version doesn't hold its admission, withdraws, and retries.process.hrtime()restarts in each Bun process (41 ms in a fresh one), andos.uptime()has whole-second resolution. A backward step dates future admissions to now and publishes that, so they keep counting and spacing admissions but expire a minute after the step. A forward step can expire admissions at most a minute early. Both stay within the one-lifetime bound.SETTLE_MSor more after sampling the clock, load, and memory, it decides again, so a descheduled caller can't record a stale admission that lets the next one skip its spacing.claimDirectory()is always~/.cache/quest/admissions/<hostname>. The home directory comes from the user database, not$HOMEorXDG_RUNTIME_DIR, so it no longer switches with the environment or with logind sessions. Bun'sos.userInfo()reads$HOMEfirst, soaccountHome()runs the lookup in a child process started withoutHOME. That child setsBUN_BE_BUN=1so the compiled binary works too, and costs about 10 ms per admission. An account missing from the user database, as in arbitrary-UID containers, uses/tmp/quest-<uid>/admissions/<hostname>. Quest creates/tmp/quest-<uid>with mode 0700 and refuses it unless only that UID can write it. The directory is a new name, so this release never reads the old window files.src/cli/interrupt.ts, shared byquest gateandquest worktree run). SIGINT is not forwarded while Quest is in its terminal's foreground process group, because the terminal already delivered it to the child. Linux reads that from/proc/self/stat; other Unixes useps -o tpgid=,pgid=. SIGHUP and SIGTERM are always forwarded. On Windows,quest gatewaits for the child instead of forwarding.quest gate --help, and theadmit()doc comment describe the new behaviour.scripts/gate-bench.sh overheadtimes admission in-process against a fresh ledger per run, since the directory no longer followsXDG_RUNTIME_DIR.quest/m0/worktree.md.Hazards covered by tests (
test/gate.test.ts)SETTLE_MS − 1.performance.now()per process, andCLOCK_MONOTONICat boot: neither is used. The multi-process test checks spacing across processes.XDG_RUNTIME_DIR, or started with anotherHOME, get the same directory. Each runs as its own process, because Bun reads the environment once at startup. An account missing from the user database gets a directory keyed by its UID.SETTLE_MSafter sampling the host decides again and records the time it published, so the next caller still waitsSETTLE_MS.script) counts the SIGINTs the gated child receives. It sees 1; without the fix it sees 2.Benchmarks
Host: 24 CPUs, 61 GiB RAM. Not idle: other agents' builds and tests kept the 1-minute load at about 19 to 42 throughout (24 is the limit), so these numbers are noisy, and the idle-burst case could not be measured on an idle host.
test/seed.test.ts, the same test as in test(gate): measure admission under Worker suite contention #177. As the plan expected, headroom is measured across the host, so the old gate's protection does not apply to ataskset-pinned burst.overhead 10): median 1 ms in-process against a fresh ledger. The CLI startup is about 105 ms bypassed, slower than test(gate): measure admission under Worker suite contention #177's 32 ms because of the host load. One run waited 250 s for the load to fall.Validation
just checkpasses. It covers TypeScript, prettier, quest validation, actionlint, shellcheck, shfmt, nixfmt, and the Worker check.just testpasses: 398 + 306 tests, the hook suites, the lifecycle of both builds, and the Worker tests.quest check --base origin/mainpasses.interrupts=2. Ignoring a failed publish makes the multi-process test admit 12 instead of 4. Recording the sampled time makes the stall test recordat: 0. Reading the home with an in-processuserInfo()makes the directory followHOME.ps -o tpgid=,pgid=path) and Windows (linkSync, file-removal tolerance). Windows CI runsbun run testonce the PR is ready for review.Follow-ups
scripts/gate-bench.sh gated 8andungated 8on an idle host to measure the idle burst directly. The expectation is close to test(gate): measure admission under Worker suite contention #177's 19 s ungated makespan. Accepted as a follow-up in Decision 1 below.cloudflare/test/seed.test.tstimed out again under pinned 8×. It was folded intoquest/m0/kix/inbox-accuracy.md, which kixelated/quest#182 finished. If it still times out under load, it is a/quest-plancandidate.os.hostname()can change with the network when no hostname is set withscutil, which would move the ledger directory. A low-priority/quest-plancandidate, if it shows up in practice.Decisions
Settled by the maintainer:
scripts/gate-bench.shis a suggested follow-up.~/.cache/quest/admissions/<hostname>stays. The ledger uses the wall clock, with clamping, because Bun offers no host-wide monotonic clock; a forward step can expire up to a minute of admissions early. The directory is a new name, so the old window files are never read. The Linux-only/proc/uptimeclock was declined to avoid a per-platform branch.Chosen while addressing review, open to change:
HOME. It costs about 10 ms for each non-nestedquest gate. The alternatives were callinggetpwuidthroughbun:ffi, whosepasswdlayout differs between Linux and macOS, or parsing/etc/passwd, which misses NSS and macOS directory-service accounts./tmp/quest-<uid>. The path depends on no environment variable. If another local user creates the name first, Quest refuses it with an error rather than trust their ledger, so they can block that account's gated commands but cannot make them overrun the host.(Written by Claude Opus 5.5)
🤖 Generated with Claude Code