Drain the handler queues before stopping the handlers on shutdown (closes #69)
check / check (push) Canceled after 0s

Shutdown stopped the handlers before the streamer, so updates still in the
handler queues, up to 20,000 per queue, were not written, and a lost
withdrawal left its route in the table after a restart.

Shutdown now stops the streamer first. Streamer.Stop queues the STATE
messages it holds for a full queue, closes the queues and returns once
every worker has handed its handler everything on its queue; then each
handler writes what it holds before its Stop returns. PeeringHandler.Stop
now waits for its loop's final write instead of running a second one
beside it.

Model: opus-5-5
This commit was merged in pull request #95.
This commit is contained in:
2026-10-19 13:08:48 +02:00
parent 2712183f21
commit 28df9f8d49
6 changed files with 235 additions and 26 deletions
+35 -9
View File
@@ -120,7 +120,7 @@ type handlerInfo struct {
queue chan *ristypes.RISMessage
metrics handlerMetrics
// heldState holds, in feed order, the STATE messages that found queue
// full (see queueMessage). Only the read loop uses it.
// full (see queueMessage). Only the read loop uses it, and then Stop.
heldState []*ristypes.RISMessage
}
@@ -141,6 +141,7 @@ type Streamer struct {
mu sync.RWMutex
cancel context.CancelFunc
running bool
workers sync.WaitGroup // the handler workers; Stop waits for them
metrics *metrics.Tracker
totalDropped atomic.Uint64 // Total dropped messages across all handlers
random *rand.Rand // Random number generator for backpressure drops
@@ -194,10 +195,18 @@ func (s *Streamer) RegisterHandler(handler MessageHandler) {
// If we're already running, start a worker for this handler
if s.running {
s.workers.Add(1)
go s.runHandlerWorker(info)
}
}
// SetURL makes the streamer read from url instead of RIS Live. Call it before
// Start. Tests use it to read from a local server.
func (s *Streamer) SetURL(url string) {
s.url = url
}
// RegisterRawHandler sets a callback to receive raw JSON lines from the stream
// before they are parsed. Only one raw handler can be registered at a time;
// subsequent calls will replace the previous handler.
@@ -227,6 +236,8 @@ func (s *Streamer) Start(ctx context.Context) error {
// Start workers for each handler
for _, info := range s.handlers {
s.workers.Add(1)
go s.runHandlerWorker(info)
}
@@ -240,9 +251,10 @@ func (s *Streamer) Start(ctx context.Context) error {
return nil
}
// Stop halts the streaming connection and shuts down all handler workers.
// It cancels the streaming context, closes all handler queues, and updates
// the connection status in metrics. This method is safe to call multiple times.
// Stop ends the stream and stops the handler workers. It returns once each
// worker has handed its handler every message left on its queue and then the
// STATE messages held for it (see queueMessage). This method is safe to call
// multiple times.
func (s *Streamer) Stop() {
s.mu.Lock()
if s.cancel == nil {
@@ -252,16 +264,28 @@ func (s *Streamer) Stop() {
return
}
// Once ctx is cancelled the read loop queues and holds nothing more (see
// dispatchMessage).
s.cancel()
s.cancel = nil
// Close all handler queues to signal workers to stop
for _, info := range s.handlers {
// The held messages came after everything on the queue. Each waits
// for the worker, which does not take s.mu, to make room.
for _, msg := range info.heldState {
info.queue <- msg
}
info.heldState = nil
// The worker returns once it has emptied the closed queue.
close(info.queue)
}
s.running = false
s.mu.Unlock()
s.metrics.SetConnected(false)
s.workers.Wait()
}
// IsRunning reports whether the streamer is currently connected and processing
@@ -354,6 +378,8 @@ func (s *Streamer) GetDroppedMessages() uint64 {
// runHandlerWorker processes messages for a specific handler
func (s *Streamer) runHandlerWorker(info *handlerInfo) {
defer s.workers.Done()
for msg := range info.queue {
start := time.Now()
@@ -824,16 +850,16 @@ func (s *Streamer) dispatchMessage(
}
// queueMessage puts msg on the handler's queue and reports false if it dropped
// msg instead. It never waits for room, so a full queue holds up neither the
// read loop nor Stop.
// msg instead. It never waits for room, so a full queue does not hold up the
// read loop.
//
// A message is dropped while the queue is filling up (shouldDropForBackpressure)
// or full, except a STATE message: one whose state is down is the only notice
// that all of a peer's routes are gone. A STATE message skips the random drop,
// and one that finds the queue full is held in info.heldState. Held messages
// go into the queue, oldest first, before anything else for the handler, as
// soon as there is room; while any is still held, the handler's other messages
// are dropped, so none of them overtakes a held down.
// soon as there is room, or when Stop is called; while any is still held, the
// handler's other messages are dropped, so none of them overtakes a held down.
//
// The held list stays small: a STATE message is a few hundred bytes and the
// feed sends about ten a second, so a queue full for a minute holds about 600.