Remove a peer's live routes when its session goes down (closes #56)
check / check (push) Canceled after 0s
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:
@@ -108,6 +108,143 @@ func (h updateHandler) HandleMessage(*ristypes.RISMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
// stateDownLine is the STATE message RIS Live sends when the collector's BGP
|
||||
// session with the peer stateDownPeer goes down.
|
||||
const (
|
||||
stateDownPeer = "192.0.2.1"
|
||||
stateDownLine = `{"type":"ris_message","data":{"type":"STATE","peer":"` + stateDownPeer + `",` +
|
||||
`"peer_asn":"64496","timestamp":1700000000,"state":"down"}}` + "\n"
|
||||
)
|
||||
|
||||
// stateHandler wants STATE messages only. Tests read what it is handed from
|
||||
// its queue, so HandleMessage does nothing.
|
||||
type stateHandler struct{}
|
||||
|
||||
func (stateHandler) WantsMessage(messageType string) bool {
|
||||
return messageType == ristypes.MessageTypeState
|
||||
}
|
||||
func (stateHandler) QueueCapacity() int { return 10 }
|
||||
func (stateHandler) HandleMessage(*ristypes.RISMessage) {}
|
||||
|
||||
// TestStreamHandsStateMessagesToHandlersThatWantThem has the server send one
|
||||
// STATE message whose state is down. The streamer must queue it, with its
|
||||
// peer and state, for a handler that wants STATE messages.
|
||||
func TestStreamHandsStateMessagesToHandlersThatWantThem(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, stateDownLine)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
s := New(logger.New(), metrics.New())
|
||||
s.url = srv.URL
|
||||
s.RegisterHandler(stateHandler{})
|
||||
|
||||
if err := s.stream(context.Background()); !errors.Is(err, errStreamEnded) {
|
||||
t.Fatalf("stream returned %v, want %v", err, errStreamEnded)
|
||||
}
|
||||
|
||||
select {
|
||||
case msg := <-s.handlers[0].queue:
|
||||
if msg.Type != ristypes.MessageTypeState || msg.Peer != stateDownPeer || msg.State != ristypes.StateDown {
|
||||
t.Errorf("queued message has type %q, peer %q, state %q; want %q, %q, %q",
|
||||
msg.Type, msg.Peer, msg.State, ristypes.MessageTypeState, stateDownPeer, ristypes.StateDown)
|
||||
}
|
||||
default:
|
||||
t.Fatal("the STATE message was not queued for the handler that wants it")
|
||||
}
|
||||
}
|
||||
|
||||
// streamWithFullStateQueue runs stream, in a goroutine, against a streamer
|
||||
// reading from url whose stateHandler's queue is full of STATE messages with
|
||||
// no peer. It returns the queue and a channel that gets what stream returns.
|
||||
func streamWithFullStateQueue(url string, idleTimeout time.Duration) (
|
||||
queue chan *ristypes.RISMessage, streamErr chan error) {
|
||||
s := New(logger.New(), metrics.New())
|
||||
s.url = url
|
||||
s.idleTimeout = idleTimeout
|
||||
s.RegisterHandler(stateHandler{})
|
||||
queue = s.handlers[0].queue
|
||||
for len(queue) < cap(queue) {
|
||||
queue <- &ristypes.RISMessage{Type: ristypes.MessageTypeState}
|
||||
}
|
||||
|
||||
streamErr = make(chan error, 1)
|
||||
go func() { streamErr <- s.stream(context.Background()) }()
|
||||
|
||||
return queue, streamErr
|
||||
}
|
||||
|
||||
// TestStreamWaitsForRoomForAStateMessage has the server send one STATE message
|
||||
// whose state is down while the queue of the handler that wants it is full. A
|
||||
// full queue is where the random drop always drops, and where a message that
|
||||
// cannot wait is dropped. The streamer must not drop it: it waits until the
|
||||
// handler takes a message off its queue, then queues it.
|
||||
func TestStreamWaitsForRoomForAStateMessage(t *testing.T) {
|
||||
const (
|
||||
stillWaitingAfter = 100 * time.Millisecond
|
||||
maxWait = 5 * time.Second
|
||||
)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, stateDownLine)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
queue, streamErr := streamWithFullStateQueue(srv.URL, streamIdleTimeout)
|
||||
|
||||
select {
|
||||
case err := <-streamErr:
|
||||
t.Fatalf("stream returned %v while the queue was full: it dropped the STATE message", err)
|
||||
case <-time.After(stillWaitingAfter):
|
||||
}
|
||||
|
||||
<-queue // the handler takes a message, which makes room
|
||||
select {
|
||||
case err := <-streamErr:
|
||||
if !errors.Is(err, errStreamEnded) {
|
||||
t.Fatalf("stream returned %v, want %v", err, errStreamEnded)
|
||||
}
|
||||
case <-time.After(maxWait):
|
||||
t.Fatalf("stream did not queue the STATE message within %s of there being room", maxWait)
|
||||
}
|
||||
|
||||
var last *ristypes.RISMessage
|
||||
for len(queue) > 0 {
|
||||
last = <-queue
|
||||
}
|
||||
if last.Peer != stateDownPeer || last.State != ristypes.StateDown {
|
||||
t.Errorf("last queued message has peer %q, state %q; want %q, %q",
|
||||
last.Peer, last.State, stateDownPeer, ristypes.StateDown)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamStopsWaitingForRoomAtTheIdleTimeout has the server send one STATE
|
||||
// message and then hold the response open, while the queue of the handler
|
||||
// that wants it stays full. The wait for room must not stall the read loop
|
||||
// past its idle timeout: the stream ends as when no line arrives.
|
||||
func TestStreamStopsWaitingForRoomAtTheIdleTimeout(t *testing.T) {
|
||||
const (
|
||||
idleTimeout = 50 * time.Millisecond
|
||||
maxWait = 5 * time.Second
|
||||
)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = io.WriteString(w, stateDownLine)
|
||||
w.(http.Flusher).Flush()
|
||||
<-r.Context().Done()
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
_, streamErr := streamWithFullStateQueue(srv.URL, idleTimeout)
|
||||
|
||||
select {
|
||||
case err := <-streamErr:
|
||||
if err == nil || errors.Is(err, errStreamEnded) {
|
||||
t.Errorf("stream returned %v, want the error of a stream dropped as idle", err)
|
||||
}
|
||||
case <-time.After(maxWait):
|
||||
t.Fatalf("stream still waiting for room %s after the idle timeout of %s", maxWait, idleTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStopBeforeMessageReachesQueues stops the streamer after the read loop
|
||||
// has checked for cancellation but before it hands the message to the handler
|
||||
// queues. That is the gap a stop from another goroutine can land in, and it
|
||||
|
||||
Reference in New Issue
Block a user