feat(hub): Direction-2 immediate-sync wait channel (v0.58.0)
GET /api/v1/wait long-poll: the box holds an authed hanging GET; the hub completes it the instant any operator intent bumps that customer's in-memory generation, then the box fires its ordinary report and the ACK delivers everything through the unchanged machinery. 240s hold with a 25s heartbeat newline defeats the nginx 60s proxy_read_timeout with no ingress annotation; WriteTimeout lifted per-connection via ResponseController. - internal/intent: per-customer generation counter + waiter registry (Bump/Wait/Close), coalescing to latest, race-closer, in-memory by design. Red-proofs: counter-vs-queue + race-closer (run-fail-reverted). - api/wait.go: the endpoint (per-customer only; global key 400; A cannot see B). - web bumps after every intent write (fire-after-commit): config CRUD, claim resend, offsite re-issue/freeze, password regen, block/unblock, floors (global bumps all config-managed), controller log-tail + log-bundle. - main.go: one intent hub shared by web+api; Close() before server.Shutdown. Pairs with controller v0.140.0 (the long-poll client). Grounding: documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md.
This commit is contained in:
@@ -1,5 +1,45 @@
|
||||
# Felhom Hub — Changelog
|
||||
|
||||
## v0.58.0 — Direction-2 immediate-sync: the hub→box "sync now" wait channel (2026-07-16)
|
||||
|
||||
Implements option (b) of `documentation/audits/SPIKE-immediate-sync-transport-2026-07-16.md`: an
|
||||
operator action on the hub now reaches the box in **seconds** instead of on the next ~15-min report
|
||||
cycle. The box holds a hanging authenticated `GET /api/v1/wait` over the existing outbound ingress;
|
||||
the hub completes it the instant any operator intent lands for that customer. The box then fires its
|
||||
ordinary out-of-cycle report — the ACK delivers config/escrow/claim/floor through the UNCHANGED
|
||||
machinery. The 15-min cycle stays the reconciliation backbone; every wait failure degrades to it.
|
||||
Pairs with controller v0.140.0 (the long-poll client). Ground truth #1 holds: the box pulls even the
|
||||
wake-up; the hub never connects inbound and no state ever rides the wait response.
|
||||
|
||||
- **`internal/intent` — the in-memory operator-intent notifier.** A per-customer generation counter
|
||||
with a waiter registry: `Bump(customerID)` advances the generation and wakes every registered
|
||||
waiter (a burst coalesces into ONE completion carrying the LATEST generation — a counter, not a
|
||||
per-bump queue); `Wait(ctx, customerID, lastSeen, maxHold)` returns the instant the generation
|
||||
differs from `lastSeen`, on ctx-cancel, on `maxHold`, or on `Close`. A pre-register gen-check
|
||||
closes the bump-before-connect race (a bump is never lost). In-memory BY DESIGN — a hub restart
|
||||
resets generations; the box compares with `!=`, so a restart costs exactly one harmless full-state
|
||||
report, never a storm. No persistence, no store schema. Red-proofs: counter-vs-queue (return the
|
||||
as-of-register snapshot → `TestWait_CoalescesBurstToLatestGen` fails) and the race-closer (drop the
|
||||
pre-register check → `TestWait_RaceCloser_BumpBeforeWaitNotLost` fails); both run-fail-reverted.
|
||||
- **`GET /api/v1/wait` (api/wait.go).** Authed via `checkAuthCustomer`; per-customer only (a global
|
||||
operator key → 400; the customer is resolved from the key, no `customer_id` parameter is accepted,
|
||||
so A can never observe B). Holds up to **240 s**, writing a **25 s heartbeat newline** while it
|
||||
waits. nginx's `proxy_read_timeout` is measured BETWEEN upstream reads, so the heartbeat keeps the
|
||||
default 60 s from ever firing — **no ingress annotation / manifest timeout change is needed** (the
|
||||
transport spike measured that ceiling; §13 proves the heartbeat defeats it live). The response is
|
||||
contentless — a single `{"gen":N}` line. The connection's write deadline is lifted per-request via
|
||||
`http.NewResponseController().SetWriteDeadline` (the global `http.Server.WriteTimeout` of 60 s is
|
||||
deliberately untouched).
|
||||
- **Intent bumps (web).** Every operator-intent handler bumps the customer's generation AFTER its
|
||||
successful store write (fire-after-commit, never on an error path), via nil-safe `s.bumpIntent`:
|
||||
config create/update/delete, claim resend, offsite re-issue (UI + the re-enroll seam), offsite
|
||||
freeze/unfreeze, retrieval-password regen, block/unblock, per-customer floor, global floor (bumps
|
||||
every config-managed customer), controller log-tail request, and the CONTROLLER log-bundle request
|
||||
(the AGENT ring rides the heartbeat envelope — a separate plane, deliberately not bumped).
|
||||
- **Wiring + shutdown.** One `intent.New()` in `main.go`, injected into both the web server and the
|
||||
API handler; `intentHub.Close()` runs before `server.Shutdown` so held waits complete instantly
|
||||
instead of eating the 15 s grace window. nil-safe throughout (an unset hub → wait 503, bumps no-op).
|
||||
|
||||
## v0.57.0 — reinstall-of-existing-customer arc: claim continuity, offsite re-issue, escrow honesty (2026-07-16)
|
||||
|
||||
Closes the N100 physical-run findings F2/F3 and the correctness edge behind F4→2.3
|
||||
|
||||
Reference in New Issue
Block a user