From 4da47b3dfa11882107a764b3c63f59cfc8abc7e0 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Fri, 25 Sep 2026 06:07:53 -0700 Subject: [PATCH] v1 plan: split task 06 into admin (06) and main wiring (07); smoke becomes 08; note the finding Co-Authored-By: Claude Fable 5.1 --- .../v1/{06-admin-main.md => 06-admin.md} | 60 +++++++---------- docs/plans/v1/07-main.md | 67 +++++++++++++++++++ ...{07-smoke-readme.md => 08-smoke-readme.md} | 6 +- docs/plans/v1/README.md | 15 +++-- 4 files changed, 106 insertions(+), 42 deletions(-) rename docs/plans/v1/{06-admin-main.md => 06-admin.md} (69%) create mode 100644 docs/plans/v1/07-main.md rename docs/plans/v1/{07-smoke-readme.md => 08-smoke-readme.md} (93%) diff --git a/docs/plans/v1/06-admin-main.md b/docs/plans/v1/06-admin.md similarity index 69% rename from docs/plans/v1/06-admin-main.md rename to docs/plans/v1/06-admin.md index 337463d..43692df 100644 --- a/docs/plans/v1/06-admin-main.md +++ b/docs/plans/v1/06-admin.md @@ -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) -**Commit subject:** `Admin: leases, pin, release, drain, usage, metrics; wire the store into crossbar` +**Commit subject:** `Admin: leases, pin, release, drain, usage, metrics` ## Goal 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 -the accounting questions, `/metrics` exposes them to Prometheus. `cmd/crossbar` opens the store, -builds the lease table and limiter, runs idle expiry and pruning, and shuts down cleanly. -`PLAN.md` §7, §7a. +the accounting questions, `/metrics` exposes them to Prometheus. `PLAN.md` §7, §7a. The +`cmd/crossbar` wiring is the next task (07), not this one. + +**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 -- Copy (**replaces** v0's): `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` +- Already copied (verify with `cmp`, never edit): `internal/admin/admin_test.go`, `example.toml` +- 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 @@ -94,40 +101,23 @@ Endpoints (all JSON unless said; errors `{"error":"…"}`; wrong method → 405 ``` 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 -- [ ] **1. Copy (replace).** `git switch v1`; copy `admin_test.go` and `example.toml` from `docs/plans/v1/_files/`. -- [ ] **2. See it fail** (compile). **3. Write the code** (admin, the store's `StatusCounts`, main). `gofmt -w .` -- [ ] **4. See it pass.** `go test -race -count=1 ./...`. -- [ ] **5. Build and run for three seconds.** +- [ ] **1. Check the tree.** `git switch v1`; `git status --short` shows the modified store, lease, + admin and example files listed above. `cmp internal/admin/admin_test.go docs/plans/v1/_files/internal/admin/admin_test.go` + and `cmp example.toml docs/plans/v1/_files/example.toml` print nothing. If they do not, copy the given files again. +- [ ] **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 -make build -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 add internal/admin internal/store internal/lease example.toml docs/implementer-log.md git commit ``` ## 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/`. diff --git a/docs/plans/v1/07-main.md b/docs/plans/v1/07-main.md new file mode 100644 index 0000000..4331e02 --- /dev/null +++ b/docs/plans/v1/07-main.md @@ -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. diff --git a/docs/plans/v1/07-smoke-readme.md b/docs/plans/v1/08-smoke-readme.md similarity index 93% rename from docs/plans/v1/07-smoke-readme.md rename to docs/plans/v1/08-smoke-readme.md index 5b03179..c1334c4 100644 --- a/docs/plans/v1/07-smoke-readme.md +++ b/docs/plans/v1/08-smoke-readme.md @@ -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) **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 -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 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}` 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. -- [ ] **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 git add tools/smoke.sh cmd/fakeupstream/main.go README.md docs/implementer-log.md diff --git a/docs/plans/v1/README.md b/docs/plans/v1/README.md index 7b0973c..6168b63 100644 --- a/docs/plans/v1/README.md +++ b/docs/plans/v1/README.md @@ -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` | | 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) | -| 06 | `06-admin-main.md` | admin v1 (pin/release/drain/usage/metrics), `cmd/crossbar` wiring | `admin_test.go` (replaces), start/stop check | -| 07 | `07-smoke-readme.md` | `tools/smoke.sh` v1 run, README update | `make smoke` | +| 06 | `06-admin.md` | admin v1 (pin/release/drain/usage/metrics) | `admin_test.go` (replaces) | +| 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 @@ -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 ``` -## 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 ..v1 --stat -- PLAN.md AGENTS.md docs/plans` empty. 3. `make gate`, `make smoke`. 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 `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. +- 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.