Skip to content

feat(archon,horismos): live QUIC dual-endpoint drain + HTTP listener rebind on config reload (#529 step 5) - #585

Merged
forkwright merged 1 commit into
mainfrom
audit/529-step5
Jul 7, 2026
Merged

feat(archon,horismos): live QUIC dual-endpoint drain + HTTP listener rebind on config reload (#529 step 5)#585
forkwright merged 1 commit into
mainfrom
audit/529-step5

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Step 5 of the #529 reactive-config migration — the QUIC and HTTP listen endpoints rebind live on a config reload, with the operator-chosen dual-endpoint + unbounded-drain posture (no dropped connections, no downtime window).

New config field paroche.renderer_quic_port: u16 (serde default 4433, replacing the hardcoded DEFAULT_QUIC_PORT). Validation rejects < 1024 and == paroche.port.

QUIC supervisor (render/server.rs) — the cancellation-token scoping is the crux:

  • accept_ct = process_shutdown.child_token() is per-generation; cancelling it stops that generation's admissions only.
  • Each connection task runs on its own process_shutdown.child_token()not a child of accept_ct — so retiring a generation never cancels its live sessions; only process shutdown does.
  • On a (listen_addr, renderer_quic_port) change: make-before-break — bind the new endpoint first, then retire() the old (cancel accept_ct, spawn an unbounded wait_idle().await drain with a 30s open_connections heartbeat). A retiring generation is never close()d — endpoint.close() appears at exactly two process-shutdown-only sites. On bind failure, a bounded break-before-make fallback (5×200ms) with rollback-bind and loud logging keeps the server running. resolve_listen_addr parse failure keeps the old generation. The Arc<RendererRegistry> and admission LiveGate are shared across generations, so draining sessions stay listed and functional.

HTTP supervisor (serve.rs) mirrors this: each generation serves under axum::serve(...).with_graceful_shutdown(sel) where sel fires on the process signal or a per-generation rebind token; a rebind stands up the new listener, then graceful-drains the old with no bound. CLI --listen/--port pinning still holds (overrides re-pin every publish, so the supervisor never observes a pinned change).

start_renderer_server/the HTTP path now take the ConfigHandle and derive their Section + watcher internally — one place, no watcher-vs-boot-snapshot ordering race.

Tests (real ConfigManager/Section): a held renderer session survives a QUIC port rebind and keeps reporting status into the shared registry while the old port stops answering and the new port serves, and the old socket frees after the held client disconnects (proving wait_idle completion); an in-flight slow HTTP request completes on the old listener while new requests are served on the new one; the bind-conflict fallback + rollback path; and the renderer_quic_port validation rules.

Gate: kanon gate --full green — fmt, check, clippy (-D warnings), nextest 272 (archon+horismos) / full workspace, deny, kanon lint (0/0). Gate-Passed stamped.

Refs #529

…rebind on config reload (#529)

Gate-Passed: kanon 0.1.5 +stages:fmt,check,clippy,nextest,lint sha:69d679041a23d0c5fa8caf209ec38e734c08479d
@forkwright
forkwright merged commit e3bc94e into main Jul 7, 2026
12 checks passed
@forkwright
forkwright deleted the audit/529-step5 branch July 7, 2026 01:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant