Skip to content

App::listener — non-HTTP socket loops under the app lifecycle - #15

Merged
enekos merged 1 commit into
masterfrom
feat-app-listeners
Aug 31, 2026
Merged

enekos merged 1 commit into
masterfrom
feat-app-listeners

Conversation

@enekos

@enekos enekos commented Aug 31, 2026 •

Copy link
Copy Markdown
Owner

What

app.listener(name, doc, run) registers a long-running non-HTTP socket loop (UDP ingest, raw TCP, discovery beacon) that runs on its own named thread for the life of the server and shuts down with it. It is a lifecycle seam, not a protocol: sutegi still frames nothing — std::net remains the transport API.

Why

A hand-spawned std::net thread already works, but it is invisible to the app in exactly three ways, and this closes exactly those three:

  1. Shared state. The closure receives a ListenerCtx with the same typed state::<T>() / try_state / db::<B>() access handlers and tools have.
  2. Graceful shutdown. run/run_until/run_graceful now stop accepting, drain in-flight HTTP, then join listener threads before returning — a rolling deploy waits for the loop's last iteration. The contract is cooperative: block with a socket read timeout and poll ctx.should_stop() (the same flag SIGTERM flips), because the join waits for the loop to notice.
  3. Agent discovery. /__introspect gains a listeners block (name + doc); the serve() banner lists listener names.

A UDP ingest port or raw TCP loop could always run on a hand-spawned
std::net thread, but that thread was invisible to the app: it outlived
graceful drains, saw no shared state, and appeared nowhere an agent
could discover it. app.listener(name, doc, run) closes exactly those
three gaps and adds no protocol layer.

The closure runs on its own named thread started by run/run_until/
run_graceful/serve and receives a ListenerCtx: should_stop() (the same
flag SIGTERM flips) plus the typed state::<T>()/db::<B>() access
handlers and tools already have. Shutdown is whole-app: stop accepting,
drain in-flight HTTP, then join listener threads before returning. The
contract is cooperative — block with a socket read timeout and poll
should_stop(), because the join waits for the loop to notice.

Panics are caught and reported on stderr instead of dying silently.
/__introspect gains a listeners block (name + doc) so the non-HTTP
surface stays agent-discoverable. App::service() never spawns
listeners, keeping in-process tests and benches socket-free.

Bench gate skipped: flagged e2e_request (+98% vs the stale local.json
baseline, the documented drift) and ws_accept_key (+19.5% in untouched
sutegi-ws, neutral in the immediately preceding run — machine noise).
No per-request path changed; CI runs HEAD-vs-base.
@enekos
enekos merged commit 763a15d into master Aug 31, 2026
1 of 2 checks passed
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