Remove a peer's live routes when its session goes down (closes #56)
check / check (push) Canceled after 0s

RIS sends no withdrawals for the routes of a peer whose BGP session goes
down, so those routes stayed in the live tables for good.

The streamer now hands STATE messages to the handlers that want them and
never drops one for backpressure: it waits for room in a full queue until
the idle timer drops the connection. When the prefix handler gets one whose
state is down, it drops the peer's updates still in its batch and notes the
peer; the next flush deletes all of the peer's live routes, IPv4 and IPv6,
before writing the batch, so updates the peer sends after the down are
kept. The live route tables' unique key now leads with peer_ip, so it also
finds a peer's routes.

Model: opus-5-5
This commit is contained in:
2026-10-19 08:48:23 +00:00
committed by sneak
parent a0ddc0d254
commit 741bea4b9e
12 changed files with 684 additions and 46 deletions
+40 -22
View File
@@ -78,7 +78,7 @@ 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 wait for room.
QueueCapacity() int
}
@@ -629,7 +629,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 +668,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 +693,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(connCtx, 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 +726,34 @@ func (s *Streamer) stream(ctx context.Context) error {
return errStreamEnded
}
// queueMessage puts msg on the handler's queue and reports whether it did. 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, so a STATE message waits for room
// instead. The wait ends if the idle timer drops the connection by cancelling
// connCtx, so the read loop never waits longer than its idle timeout.
func (s *Streamer) queueMessage(connCtx context.Context, info *handlerInfo, msg *ristypes.RISMessage) bool {
if msg.Type == ristypes.MessageTypeState {
select {
case info.queue <- msg:
return true
case <-connCtx.Done():
return false
}
}
if s.shouldDropForBackpressure(info) {
return false
}
select {
case info.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