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 <noreply@anthropic.com>
This commit is contained in:
@@ -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/`.
|
||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user