Remove a peer's live routes when its session goes down (closes #56)
check / check (push) Waiting to run

RIS sends no withdrawals for the routes of a peer whose BGP session goes
down, so routewatch kept them.

The streamer now hands STATE messages to the prefix handler. They are never
dropped for backpressure: one that finds the queue full is held and queued
first once there is room, and the handler's other messages are dropped
meanwhile, so none overtakes it. Nothing waits. On a down, the prefix
handler drops the peer's pending updates and its next flush deletes all the
peer's live routes before the batch's other writes, keeping the in-memory
counts exact. The live route tables are unique on peer and prefix, which
also finds a peer's routes.

Model: opus-5-5
This commit was merged in pull request #88.
This commit is contained in:
2026-10-19 11:40:31 +02:00
parent a0ddc0d254
commit 825afa2ebe
12 changed files with 787 additions and 51 deletions
+60 -22
View File
@@ -78,7 +78,8 @@ type MessageHandler interface {
// QueueCapacity returns the desired queue capacity for this handler.
// Handlers that process quickly can have larger queues to buffer bursts.
// When the queue fills up, messages will be dropped according to the
// backpressure algorithm.
// backpressure algorithm, except STATE messages, which are held until
// there is room.
QueueCapacity() int
}
@@ -102,6 +103,9 @@ type handlerInfo struct {
handler MessageHandler
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.
heldState []*ristypes.RISMessage
}
// Streamer manages a connection to the RIPE RIS Live streaming API for receiving
@@ -629,7 +633,7 @@ func (s *Streamer) stream(ctx context.Context) error {
// Process based on message type
switch msg.Type {
case "UPDATE":
case ristypes.MessageTypeUpdate:
// Process BGP UPDATE messages
// Will be dispatched to handlers
case "RIS_PEER_STATE":
@@ -668,9 +672,9 @@ func (s *Streamer) stream(ctx context.Context) error {
)
continue
case "STATE":
// Peer state changes - silently ignore
continue
case ristypes.MessageTypeState:
// The BGP session with the peer changed state, for example went
// down. Dispatched to the handlers that want it.
default:
s.logger.Warn("Unknown message type, skipping",
"type", msg.Type,
@@ -693,30 +697,20 @@ func (s *Streamer) stream(ctx context.Context) error {
continue
}
// Check if we should drop due to backpressure
if s.shouldDropForBackpressure(info) {
if !s.queueMessage(info, &msg) {
atomic.AddUint64(&info.metrics.droppedCount, 1)
atomic.AddUint64(&s.totalDropped, 1)
continue
}
// Try to queue the message
select {
case info.queue <- &msg:
// Message queued successfully
// Update high water mark if needed
queueLen := len(info.queue)
info.metrics.mu.Lock()
if queueLen > info.metrics.queueHighWaterMark {
info.metrics.queueHighWaterMark = queueLen
}
info.metrics.mu.Unlock()
default:
// Queue is full, drop the message
atomic.AddUint64(&info.metrics.droppedCount, 1)
atomic.AddUint64(&s.totalDropped, 1)
// Update high water mark if needed
queueLen := len(info.queue)
info.metrics.mu.Lock()
if queueLen > info.metrics.queueHighWaterMark {
info.metrics.queueHighWaterMark = queueLen
}
info.metrics.mu.Unlock()
}
s.mu.RUnlock()
}
@@ -736,6 +730,50 @@ func (s *Streamer) stream(ctx context.Context) error {
return errStreamEnded
}
// 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.
//
// 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.
//
// 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.
func (s *Streamer) queueMessage(info *handlerInfo, msg *ristypes.RISMessage) bool {
for len(info.heldState) > 0 && trySend(info.queue, info.heldState[0]) {
info.heldState = info.heldState[1:]
}
if msg.Type == ristypes.MessageTypeState {
if len(info.heldState) > 0 || !trySend(info.queue, msg) {
info.heldState = append(info.heldState, msg)
}
return true
}
if len(info.heldState) > 0 || s.shouldDropForBackpressure(info) {
return false
}
return trySend(info.queue, msg)
}
// trySend puts msg on queue if there is room and reports whether it did.
func trySend(queue chan *ristypes.RISMessage, msg *ristypes.RISMessage) bool {
select {
case queue <- msg:
return true
default:
return false
}
}
// shouldDropForBackpressure determines if a message should be dropped based on queue utilization
func (s *Streamer) shouldDropForBackpressure(info *handlerInfo) bool {
// Calculate queue utilization