Merge origin/master into v1: task 06 split into admin + main

This commit is contained in:
2026-09-25 06:08:28 -07:00
4 changed files with 106 additions and 42 deletions
@@ -1,20 +1,27 @@
# v1 task 06: admin v1 and the wiring # v1 task 06: the admin handler (v1)
**Branch:** `v1` (run `git switch v1`; `git status --short` must be empty, otherwise stop) **Branch:** `v1` (run `git switch v1`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Admin: leases, pin, release, drain, usage, metrics; wire the store into crossbar` **Commit subject:** `Admin: leases, pin, release, drain, usage, metrics`
## Goal ## Goal
Operators see and steer the system: the hosts view gains slots and drain state, the routes view Operators see and steer the system: the hosts view gains slots and drain state, the routes view
shows leases and pins, `POST` endpoints pin/release a route and drain a host, `/usage` answers shows leases and pins, `POST` endpoints pin/release a route and drain a host, `/usage` answers
the accounting questions, `/metrics` exposes them to Prometheus. `cmd/crossbar` opens the store, the accounting questions, `/metrics` exposes them to Prometheus. `PLAN.md` §7, §7a. The
builds the lease table and limiter, runs idle expiry and pruning, and shuts down cleanly. `cmd/crossbar` wiring is the next task (07), not this one.
`PLAN.md` §7, §7a.
**State of the tree when this task starts:** an earlier session already added
`store.StatusCounts` (`internal/store`) and `lease.Table.Candidates` (`internal/lease`), copied
`example.toml` and `admin_test.go`, and left a broken draft of `internal/admin/admin.go`
(duplicate `Handler` declarations). Those files are uncommitted in the working tree. Keep the
store and lease additions (they pass their tests); treat `admin.go` as scratch you may rewrite
from a blank file. This task commits all of them.
## Files ## Files
- Copy (**replaces** v0's): `internal/admin/admin_test.go`, `example.toml` - Already copied (verify with `cmp`, never edit): `internal/admin/admin_test.go`, `example.toml`
- Modify: `internal/admin/admin.go` (split if over 400 lines), `cmd/crossbar/main.go`, `docs/implementer-log.md` - Modify: `internal/admin/admin.go` (split if over 400 lines), `docs/implementer-log.md`
- Already modified, commit as they are after their tests pass: `internal/store/store.go`, `internal/store/schema.go`, `internal/lease/lease.go`
## Interfaces ## Interfaces
@@ -94,40 +101,23 @@ Endpoints (all JSON unless said; errors `{"error":"…"}`; wrong method → 405
``` ```
Label values escaped (`"` and `\`), lines sorted, no trailing spaces. Label values escaped (`"` and `\`), lines sorted, no trailing spaces.
`cmd/crossbar/main.go`:
- After `config.Load`: `store.Open(cfg.DB)` (error → exit 1 `crossbar: …`); `defer st.Close()`.
- `hosts := proxy.HostView(table, cfg)`; `lim := limiter.New()` configured for every host/model
from config with `cfg.QueueMax`; `leases, err := lease.New(st, hosts, proxy.Chooser(cfg, table, lim), cfg.LeaseIdle.Duration)`.
- `for name, rt := range cfg.Routes { leases.Candidates(name, rt.Hosts) }`.
- `proxy.New(cfg, table, leases, lim, st, log)`; `admin.Handler(cfg, table, leases, lim, st, hosts)`.
- Background loops until ctx is done: every minute `leases.ExpireIdle(time.Now())`; every hour
`st.Prune(time.Now(), cfg.Retention.Duration)`; after every poll round the health table's
observations are recorded with `st.RecordHostHealth` — do this from a goroutine that every
`poll_interval` reads `table.All()` and writes one row per host.
- Shutdown as v0, then `st.Close()`.
## Steps ## Steps
- [ ] **1. Copy (replace).** `git switch v1`; copy `admin_test.go` and `example.toml` from `docs/plans/v1/_files/`. - [ ] **1. Check the tree.** `git switch v1`; `git status --short` shows the modified store, lease,
- [ ] **2. See it fail** (compile). **3. Write the code** (admin, the store's `StatusCounts`, main). `gofmt -w .` admin and example files listed above. `cmp internal/admin/admin_test.go docs/plans/v1/_files/internal/admin/admin_test.go`
- [ ] **4. See it pass.** `go test -race -count=1 ./...`. and `cmp example.toml docs/plans/v1/_files/example.toml` print nothing. If they do not, copy the given files again.
- [ ] **5. Build and run for three seconds.** - [ ] **2. Confirm the inherited pieces pass.** `go test -race -count=1 ./internal/store/ ./internal/lease/`. Expected: both `ok`.
- [ ] **3. Write `internal/admin/admin.go`** (delete the draft first if it is easier). `gofmt -w internal/admin/`.
- [ ] **4. See the test pass.** `go test -race -count=1 ./internal/admin/`. Expected: `ok`.
- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. (`cmd/crossbar` still passes
nils to `proxy.New`; that is task 07's job and does not fail the gate.)
- [ ] **6. Log and commit.** Row `v1/06-admin`. Deviations: say that the store and lease additions came from the earlier session.
```sh ```sh
make build git add internal/admin internal/store internal/lease example.toml docs/implementer-log.md
timeout --preserve-status --signal=TERM 3 bin/crossbar -config example.toml; echo "exit=$?"
ls -la crossbar.db* && rm -f crossbar.db crossbar.db-wal crossbar.db-shm
```
Expected: `listening`, `shutting down`, `exit=0`; the SQLite file was created (then removed).
- [ ] **6. Run the gate.** `make gate`. **7. Log and commit.** Row `v1/06-admin-main`.
```sh
git add internal/admin internal/store internal/lease cmd/crossbar example.toml docs/implementer-log.md
git commit git commit
``` ```
## Done when ## Done when
- All tests pass with `-race`; the three-second run exits 0 and created the db; `make gate` prints `gate: ok`; both copied files byte-identical. - `go test -race -count=1 ./...` passes; `make gate` prints `gate: ok`; `admin_test.go` and `example.toml` are byte-identical to `_files/`.
+67
View File
@@ -0,0 +1,67 @@
# v1 task 07: wire the store, leases and limiter into `crossbar`
**Branch:** `v1` (run `git switch v1`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Wire the store, lease table and limiter into crossbar`
## Goal
`cmd/crossbar` opens the SQLite store, builds the lease table and the limiter from the config,
registers every route's hosts, serves the v1 proxy and admin, runs idle expiry and pruning in
the background, records poller observations, and shuts down cleanly. `PLAN.md` §5–§7a.
## Files
- Modify: `cmd/crossbar/main.go`, `docs/implementer-log.md`
## Rules
`cmd/crossbar/main.go` (keep the v0 shape: `-config`, slog to stderr, signal context, graceful shutdown):
1. After `config.Load`: `st, err := store.Open(cfg.DB)`; error → `crossbar: …` on stderr, exit 1. Close it on the way out.
2. `table := health.New(bases, cfg.PollInterval.Duration, nil)`; `go table.Run(ctx)`.
3. `hosts := proxy.HostView(table, cfg)`; `lim := limiter.New()` and, for every host and model in
`cfg.Hosts`, `lim.Configure(host, model, m.Parallel, cfg.QueueMax)`.
4. `leases, err := lease.New(st, hosts, proxy.Chooser(cfg, table, lim), cfg.LeaseIdle.Duration)`;
error → exit 1. Then `for name, rt := range cfg.Routes { leases.Candidates(name, rt.Hosts) }`.
5. `mux.Handle("/_crossbar/", admin.Handler(cfg, table, leases, lim, st, hosts))`;
`mux.Handle("/", proxy.New(cfg, table, leases, lim, st, log))`.
6. Background goroutines until `ctx` is done: every minute `leases.ExpireIdle(time.Now())`; every
hour `st.Prune(time.Now(), cfg.Retention.Duration)` (log the count); every `poll_interval`
read `table.All()` and `st.RecordHostHealth` one row per host. Log errors, never exit on them.
7. Shutdown as v0 (`srv.Shutdown` with a 10 s timeout), then `st.Close()`. Exit 0.
## Steps
- [ ] **1.** `git switch v1`; `git status --short` empty.
- [ ] **2. Write `main.go`.** `gofmt -w cmd/`.
- [ ] **3. Build and run for three seconds.**
```sh
make build
timeout --preserve-status --signal=TERM 3 bin/crossbar -config example.toml; echo "exit=$?"
ls crossbar.db* && rm -f crossbar.db crossbar.db-wal crossbar.db-shm
```
Expected: `listening`, `shutting down`, `exit=0`; the SQLite file existed (then removed). Then:
```sh
bin/crossbar -config /nonexistent.toml; echo "exit=$?"
```
Expected: `crossbar: config: open /nonexistent.toml: no such file or directory`, `exit=1`.
- [ ] **4. Run the gate.** `make gate`. Expected last line: `gate: ok`.
- [ ] **5. Log and commit.** Row `v1/07-main`.
```sh
git add cmd/crossbar docs/implementer-log.md
git commit
```
## Done when
- The three-second run exits 0 and created the db; the missing-config run exits 1; `make gate` prints `gate: ok`.
## Stop and report if
- `bin/crossbar` does not exit 0 on SIGTERM within the timeout.
@@ -1,4 +1,4 @@
# v1 task 07: the smoke run and the README # v1 task 08: the smoke run and the README
**Branch:** `v1` (run `git switch v1`; `git status --short` must be empty, otherwise stop) **Branch:** `v1` (run `git switch v1`; `git status --short` must be empty, otherwise stop)
**Commit subject:** `Smoke run for v1; README for leases, admin and accounting` **Commit subject:** `Smoke run for v1; README for leases, admin and accounting`
@@ -26,7 +26,7 @@ make smoke
``` ```
Expected last line: `smoke: ok (stream spread N ms)`, N ≥ 600. The script prints which numbered Expected last line: `smoke: ok (stream spread N ms)`, N ≥ 600. The script prints which numbered
check failed and crossbar's log. A failure is a defect in tasks 01–06 **or in the script**: fix check failed and crossbar's log. A failure is a defect in tasks 01–07 **or in the script**: fix
code only when a rule from an earlier task was broken; if the script's expectation contradicts a code only when a rule from an earlier task was broken; if the script's expectation contradicts a
task rule, stop and report which. task rule, stop and report which.
@@ -37,7 +37,7 @@ task rule, stop and report which.
each (`GET hosts`, `GET routes`, `POST routes/{route}` pin and release, `POST hosts/{host}` each (`GET hosts`, `GET routes`, `POST routes/{route}` pin and release, `POST hosts/{host}`
drain, `GET usage` JSON and text, `GET metrics`), taken from the smoke run; `## What v1 does not drain, `GET usage` JSON and text, `GET metrics`), taken from the smoke run; `## What v1 does not
do`: context-size guard, wake-on-LAN, Tailscale identity, `/slots` — see `PLAN.md` v2. do`: context-size guard, wake-on-LAN, Tailscale identity, `/slots` — see `PLAN.md` v2.
- [ ] **3. Run the gate.** `make gate`. **4. Log and commit.** Row `v1/07-smoke-readme`, with the smoke line in Notes. - [ ] **3. Run the gate.** `make gate`. **4. Log and commit.** Row `v1/08-smoke-readme`, with the smoke line in Notes.
```sh ```sh
git add tools/smoke.sh cmd/fakeupstream/main.go README.md docs/implementer-log.md git add tools/smoke.sh cmd/fakeupstream/main.go README.md docs/implementer-log.md
+11 -4
View File
@@ -38,8 +38,9 @@ v1.59.0 (pure Go; `go.sum` given). No other module.
| 03 | `03-limiter-choose.md` | `internal/limiter`, `internal/choose` | `limiter_test.go`, `choose_test.go` | | 03 | `03-limiter-choose.md` | `internal/limiter`, `internal/choose` | `limiter_test.go`, `choose_test.go` |
| 04 | `04-lease.md` | `internal/lease`: the sticky table | `lease_test.go` | | 04 | `04-lease.md` | `internal/lease`: the sticky table | `lease_test.go` |
| 05 | `05-proxy.md` | proxy v1: leases, queue, SSE tee, accounting, header route | `proxy_test.go` (replaces v0's) | | 05 | `05-proxy.md` | proxy v1: leases, queue, SSE tee, accounting, header route | `proxy_test.go` (replaces v0's) |
| 06 | `06-admin-main.md` | admin v1 (pin/release/drain/usage/metrics), `cmd/crossbar` wiring | `admin_test.go` (replaces), start/stop check | | 06 | `06-admin.md` | admin v1 (pin/release/drain/usage/metrics) | `admin_test.go` (replaces) |
| 07 | `07-smoke-readme.md` | `tools/smoke.sh` v1 run, README update | `make smoke` | | 07 | `07-main.md` | `cmd/crossbar` wiring, background loops | start/stop check |
| 08 | `08-smoke-readme.md` | `tools/smoke.sh` v1 run, README update | `make smoke` |
## For the owner: running a task ## For the owner: running a task
@@ -47,9 +48,9 @@ v1.59.0 (pure Go; `go.sum` given). No other module.
tools/run-plan.sh docs/plans/v1 # from a clean checkout on master tools/run-plan.sh docs/plans/v1 # from a clean checkout on master
``` ```
## For the reviewer: after task 07 ## For the reviewer: after task 08
1. `git log --oneline master..v1`: seven commits with the trailer. 1. `git log --oneline master..v1`: eight task commits with the trailer (plus owner merges).
2. Copied files byte-identical to `_files/`; `git diff <merge-base>..v1 --stat -- PLAN.md AGENTS.md docs/plans` empty. 2. Copied files byte-identical to `_files/`; `git diff <merge-base>..v1 --stat -- PLAN.md AGENTS.md docs/plans` empty.
3. `make gate`, `make smoke`. 3. `make gate`, `make smoke`.
4. Read every source file against its task. Probe outside the tests: a lease whose host is 4. Read every source file against its task. Probe outside the tests: a lease whose host is
@@ -108,3 +109,9 @@ tools/run-plan.sh docs/plans/v1 # from a clean checkout on master
of stopping. Test-file fault (mine): the rig and fake-upstream scaffolding moved into of stopping. Test-file fault (mine): the rig and fake-upstream scaffolding moved into
`helpers_test.go` (211 + 271 lines). Ornith's own `proxy.go` was also at 411 lines; splitting it `helpers_test.go` (211 + 271 lines). Ornith's own `proxy.go` was also at 411 lines; splitting it
is part of the task as written. is part of the task as written.
- 2026-09-25, task 06, first session: 50 minutes, admin package never compiled (duplicate
`Handler`, re-reading the same files in a loop) while the store and lease additions it made
were correct. Too large a task for one session — the size lesson from boxmaker, mine to apply.
Split into `06-admin.md` (handler only; inherits the store/lease additions from the tree) and
`07-main.md` (wiring); the smoke task became 08. Session stopped by the owner; the broken
`admin.go` draft was left in the tree for the next session to replace.