Problem
Split out of #381 (item 2). On preforking web servers, each Puma worker lazily builds its own Pgbus::Web::Streamer::Instance — and with it one dedicated direct LISTEN connection (lib/pgbus/web/streamer/listener.rb, created per worker on the first SSE subscriber via Pgbus::Web::Streamer.current). A host with N Puma workers pins N direct connections; on transaction-pool PgBouncer platforms those come out of the scarce direct slice of max_connections.
Goal: one streams LISTEN connection per host — the listener lives in the Puma master, workers are woken over IPC.
Why this is not a trivial port of the supervisor-side change (#381 item 1)
The job-side consolidation only has to deliver a payload-free wake byte for a mostly-static queue set. The streamer Listener's contract is much richer, and each piece has to survive the master→worker process boundary:
- Synchronous
ensure_listening ack (listener.rb:134-138): Dispatcher#handle_connect blocks until LISTEN is actually active before issuing read_after, otherwise a broadcast committed in the gap is lost (neither in the read_after result nor delivered as a wake). Cross-process, this becomes a worker→master request/ack round trip with a bounded timeout, per new stream subscription.
- Per-subscriber dynamic channel sets: the LISTEN set follows SSE subscriber registration/GC per worker (
ensure_listening / lazy remove_listening). The master must maintain a refcounted union across workers and GC channels only when no worker holds a subscriber.
- Ephemeral wakes carry payloads (
listener.rb:253-278): WakeMessage.payload is the only copy of the broadcast HTML — it cannot be dropped and must be framed and fanned out to the right workers over the pipe/socket, not just a wake byte. Durable wakes (payload nil) are droppable under backpressure; ephemeral are not — the IPC channel needs that distinction.
- Lazy instantiation: nothing starts at worker boot today (
lib/puma/plugin/pgbus_streams.rb only registers teardown hooks); a master-owned listener needs a before_fork-time start plus per-worker inherited channels, and a story for single-mode / non-preforking servers (keep the current per-process listener behind a scope config, mirroring worker_notify_scope).
Sketch
Acceptance
- Preforking web server with N workers: exactly 1 streams LISTEN connection per host.
- No lost broadcasts across the subscribe/read_after gap (the ack contract holds cross-process).
- Ephemeral wakes are never dropped by the IPC layer; durable-wake backpressure semantics preserved (
dispatch_queue_limit).
- Single-mode Puma / non-preforking servers keep working with the per-process listener.
Depends on
Problem
Split out of #381 (item 2). On preforking web servers, each Puma worker lazily builds its own
Pgbus::Web::Streamer::Instance— and with it one dedicated direct LISTEN connection (lib/pgbus/web/streamer/listener.rb, created per worker on the first SSE subscriber viaPgbus::Web::Streamer.current). A host with N Puma workers pins N direct connections; on transaction-pool PgBouncer platforms those come out of the scarce direct slice ofmax_connections.Goal: one streams LISTEN connection per host — the listener lives in the Puma master, workers are woken over IPC.
Why this is not a trivial port of the supervisor-side change (#381 item 1)
The job-side consolidation only has to deliver a payload-free wake byte for a mostly-static queue set. The streamer Listener's contract is much richer, and each piece has to survive the master→worker process boundary:
ensure_listeningack (listener.rb:134-138):Dispatcher#handle_connectblocks until LISTEN is actually active before issuingread_after, otherwise a broadcast committed in the gap is lost (neither in the read_after result nor delivered as a wake). Cross-process, this becomes a worker→master request/ack round trip with a bounded timeout, per new stream subscription.ensure_listening/ lazyremove_listening). The master must maintain a refcounted union across workers and GC channels only when no worker holds a subscriber.listener.rb:253-278):WakeMessage.payloadis the only copy of the broadcast HTML — it cannot be dropped and must be framed and fanned out to the right workers over the pipe/socket, not just a wake byte. Durable wakes (payload nil) are droppable under backpressure; ephemeral are not — the IPC channel needs that distinction.lib/puma/plugin/pgbus_streams.rbonly registers teardown hooks); a master-owned listener needs abefore_fork-time start plus per-worker inherited channels, and a story for single-mode / non-preforking servers (keep the current per-process listener behind a scope config, mirroringworker_notify_scope).Sketch
streams_listen_scope = :master | :processconfig (default TBD after Connection efficiency: host-level shared LISTEN (supervisor-owned NotifyListener, one direct connection per host) + explicit multi-queue read priority contract #381 item 1 burn-in).before_fork): workers sendsubscribe(queue)/unsubscribe(queue)control frames and receivewake(queue, payload?)frames; master hub owns the single Listener and a refcounted channel union.Acceptance
dispatch_queue_limit).Depends on