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
724 lines
20 KiB
Go
724 lines
20 KiB
Go
package streamer
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
|
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
|
)
|
|
|
|
func TestNewStreamer(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := logger.New()
|
|
metricsTracker := metrics.New()
|
|
s := New(logger, metricsTracker)
|
|
|
|
if s == nil {
|
|
t.Fatal("New() returned nil")
|
|
}
|
|
|
|
if s.logger != logger {
|
|
t.Error("logger not set correctly")
|
|
}
|
|
|
|
if s.client == nil {
|
|
t.Error("HTTP client not initialized")
|
|
}
|
|
|
|
if s.handlers == nil {
|
|
t.Error("handlers slice not initialized")
|
|
}
|
|
|
|
if s.metrics != metricsTracker {
|
|
t.Error("metrics tracker not set correctly")
|
|
}
|
|
|
|
if s.idleTimeout != streamIdleTimeout {
|
|
t.Errorf("idleTimeout = %s, want %s", s.idleTimeout, streamIdleTimeout)
|
|
}
|
|
|
|
if s.minBackoff != minBackoffDelay {
|
|
t.Errorf("minBackoff = %s, want %s", s.minBackoff, minBackoffDelay)
|
|
}
|
|
}
|
|
|
|
// TestStreamDoesNotLeakTickersAcrossReconnects drives many short-lived
|
|
// connections (each stream call is one reconnect cycle) and asserts the
|
|
// goroutine count returns to its starting value. Each connection starts two
|
|
// ticker goroutines; before the fix they lived until the streamer's lifetime
|
|
// context was cancelled, so every reconnect leaked two.
|
|
//
|
|
//nolint:paralleltest // counts goroutines, so no other test may run alongside
|
|
func TestStreamDoesNotLeakTickersAcrossReconnects(t *testing.T) {
|
|
// The handler returns immediately, so the response body is empty and each
|
|
// stream call ends at once, standing in for a dropped connection.
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(_ http.ResponseWriter, _ *http.Request) {}))
|
|
defer srv.Close()
|
|
|
|
s := New(logger.New(), metrics.New())
|
|
s.url = srv.URL
|
|
|
|
// One warm-up connection so any persistent HTTP transport goroutine exists
|
|
// before we take the baseline.
|
|
err := s.stream(context.Background())
|
|
if !errors.Is(err, errStreamEnded) {
|
|
t.Fatalf("warm-up stream returned %v, want %v", err, errStreamEnded)
|
|
}
|
|
|
|
s.client.CloseIdleConnections()
|
|
|
|
baseline := settledGoroutineCount()
|
|
|
|
const reconnects = 20
|
|
for range reconnects {
|
|
err := s.stream(context.Background())
|
|
if !errors.Is(err, errStreamEnded) {
|
|
t.Fatalf("stream returned %v, want %v", err, errStreamEnded)
|
|
}
|
|
}
|
|
|
|
s.client.CloseIdleConnections()
|
|
|
|
if !waitForGoroutines(baseline) {
|
|
t.Fatalf("goroutines did not return to baseline %d after %d reconnects, got %d",
|
|
baseline, reconnects, runtime.NumGoroutine())
|
|
}
|
|
}
|
|
|
|
// updateLine is one UPDATE message as RIS Live sends it.
|
|
const updateLine = `{"type":"ris_message","data":{"type":"UPDATE",` +
|
|
`"peer":"192.0.2.1","peer_asn":"64496","timestamp":1700000000}}` + "\n"
|
|
|
|
// updateHandler wants UPDATE messages. For each one it handles it sends on
|
|
// handled if there is room, and otherwise does nothing; a nil handled is
|
|
// never sent on.
|
|
type updateHandler struct {
|
|
handled chan struct{}
|
|
}
|
|
|
|
func (updateHandler) WantsMessage(messageType string) bool {
|
|
return messageType == "UPDATE"
|
|
}
|
|
|
|
func (updateHandler) QueueCapacity() int { return 10 }
|
|
|
|
func (h updateHandler) HandleMessage(*ristypes.RISMessage) {
|
|
select {
|
|
case h.handled <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
// flush sends what w has buffered to the client. The writers httptest servers
|
|
// hand to handlers can always do that.
|
|
func flush(w http.ResponseWriter) {
|
|
flusher, ok := w.(http.Flusher)
|
|
if ok {
|
|
flusher.Flush()
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
t.Parallel()
|
|
|
|
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{})
|
|
|
|
err := s.stream(context.Background())
|
|
if !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")
|
|
}
|
|
}
|
|
|
|
// TestHeldStateMessagesKeepTheirPlaceInTheQueue hands queueMessage two STATE
|
|
// messages and two updates while the queue is full, making room in between.
|
|
// Neither STATE message may be dropped: each must reach the queue after
|
|
// everything queued before it and before anything handed over after it. The
|
|
// update handed over while a STATE message is held is dropped.
|
|
func TestHeldStateMessagesKeepTheirPlaceInTheQueue(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := New(logger.New(), metrics.New())
|
|
s.RegisterHandler(stateHandler{})
|
|
|
|
info := s.handlers[0]
|
|
|
|
before := &ristypes.RISMessage{Type: ristypes.MessageTypeUpdate}
|
|
for len(info.queue) < cap(info.queue) {
|
|
info.queue <- before
|
|
}
|
|
|
|
firstDown := &ristypes.RISMessage{
|
|
Type: ristypes.MessageTypeState, State: ristypes.StateDown,
|
|
}
|
|
secondDown := &ristypes.RISMessage{
|
|
Type: ristypes.MessageTypeState, State: ristypes.StateDown,
|
|
}
|
|
updateWhileHeld := &ristypes.RISMessage{Type: ristypes.MessageTypeUpdate}
|
|
after := &ristypes.RISMessage{Type: ristypes.MessageTypeUpdate}
|
|
|
|
if !s.queueMessage(info, firstDown) {
|
|
t.Fatal("the first STATE message was dropped on a full queue")
|
|
}
|
|
|
|
<-info.queue // the handler makes room for one message
|
|
|
|
if !s.queueMessage(info, secondDown) {
|
|
t.Fatal("the second STATE message was dropped")
|
|
}
|
|
|
|
if s.queueMessage(info, updateWhileHeld) {
|
|
t.Fatal("an update was queued while a STATE message was held")
|
|
}
|
|
|
|
// The handler takes every message queued before the first STATE message.
|
|
for range cap(info.queue) - 1 {
|
|
if <-info.queue != before {
|
|
t.Fatal("a message overtook those queued before the first STATE message")
|
|
}
|
|
}
|
|
|
|
if !s.queueMessage(info, after) {
|
|
t.Fatal("an update was dropped on a queue with room")
|
|
}
|
|
|
|
if <-info.queue != firstDown {
|
|
t.Fatal("the first STATE message did not come next")
|
|
}
|
|
|
|
if <-info.queue != secondDown {
|
|
t.Fatal("the second STATE message did not come after the first")
|
|
}
|
|
|
|
if <-info.queue != after {
|
|
t.Fatal("the update handed over after the STATE messages did not come " +
|
|
"after them")
|
|
}
|
|
|
|
if len(info.queue) != 0 {
|
|
t.Errorf("%d messages left on the queue, want none", len(info.queue))
|
|
}
|
|
}
|
|
|
|
// takeAll takes the messages off queue, as a handler's worker would, and sends
|
|
// them on the returned channel once queue is closed.
|
|
func takeAll(queue chan *ristypes.RISMessage) chan []*ristypes.RISMessage {
|
|
taken := make(chan []*ristypes.RISMessage, 1)
|
|
|
|
go func() {
|
|
var msgs []*ristypes.RISMessage
|
|
|
|
for msg := range queue {
|
|
msgs = append(msgs, msg)
|
|
}
|
|
|
|
taken <- msgs
|
|
}()
|
|
|
|
return taken
|
|
}
|
|
|
|
// newStreamerWithFullStateQueue returns a streamer reading from url with one
|
|
// handler, a stateHandler whose queue is full.
|
|
func newStreamerWithFullStateQueue(url string) *Streamer {
|
|
s := New(logger.New(), metrics.New())
|
|
s.url = url
|
|
s.RegisterHandler(stateHandler{})
|
|
|
|
queue := s.handlers[0].queue
|
|
for len(queue) < cap(queue) {
|
|
queue <- &ristypes.RISMessage{Type: ristypes.MessageTypeState}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// TestOtherHandlersKeepReceivingWhileAStateMessageIsHeld has the server send a
|
|
// STATE message whose state is down, then an UPDATE, while the queue of the
|
|
// handler that wants STATE messages is full. The STATE message must be held
|
|
// for that handler, and the read loop must go on to queue the UPDATE for the
|
|
// handler that wants it.
|
|
func TestOtherHandlersKeepReceivingWhileAStateMessageIsHeld(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = io.WriteString(w, stateDownLine+updateLine)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := newStreamerWithFullStateQueue(srv.URL)
|
|
s.RegisterHandler(updateHandler{})
|
|
|
|
err := s.stream(context.Background())
|
|
if !errors.Is(err, errStreamEnded) {
|
|
t.Fatalf("stream returned %v, want %v", err, errStreamEnded)
|
|
}
|
|
|
|
held := s.handlers[0].heldState
|
|
if len(held) != 1 || held[0].Peer != stateDownPeer ||
|
|
held[0].State != ristypes.StateDown {
|
|
t.Errorf("held %d STATE messages, want the down from %s alone",
|
|
len(held), stateDownPeer)
|
|
}
|
|
|
|
got := len(s.handlers[1].queue)
|
|
if got != 1 {
|
|
t.Errorf("%d messages queued for the handler that wants UPDATE "+
|
|
"messages, want 1", got)
|
|
}
|
|
}
|
|
|
|
// TestStopQueuesTheHeldStateMessages has the server send two STATE messages
|
|
// and an UPDATE and hold the response open while the queue of the handler that
|
|
// wants STATE messages is full, so both are held. Stop must put both on the
|
|
// queue, after the messages already on it, before it closes the queue, and
|
|
// must end the stream.
|
|
func TestStopQueuesTheHeldStateMessages(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
held = 2
|
|
lines = held + 1
|
|
maxWait = 5 * time.Second
|
|
)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = io.WriteString(w, stateDownLine+stateDownLine+updateLine)
|
|
|
|
flush(w)
|
|
<-r.Context().Done()
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := newStreamerWithFullStateQueue(srv.URL)
|
|
// The raw handler sees each line before the read loop hands it to the
|
|
// queues, so once it has seen the UPDATE, both STATE messages are held.
|
|
linesSeen := make(chan struct{}, lines)
|
|
|
|
s.RegisterRawHandler(func(string) { linesSeen <- struct{}{} })
|
|
|
|
// As in TestStopBeforeMessageReachesQueues, setting cancel as Start does
|
|
// lets Stop cancel the stream run here.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
s.cancel = cancel
|
|
streamErr := make(chan error, 1)
|
|
|
|
go func() { streamErr <- s.stream(ctx) }()
|
|
|
|
for range lines {
|
|
select {
|
|
case <-linesSeen:
|
|
case <-time.After(maxWait):
|
|
t.Fatalf("the read loop did not read %d lines within %s",
|
|
lines, maxWait)
|
|
}
|
|
}
|
|
|
|
// No worker runs here: the test takes from the full queue in its place.
|
|
queue := s.handlers[0].queue
|
|
taken := takeAll(queue)
|
|
stopped := make(chan struct{})
|
|
|
|
go func() {
|
|
s.Stop()
|
|
close(stopped)
|
|
}()
|
|
|
|
select {
|
|
case <-stopped:
|
|
case <-time.After(maxWait):
|
|
t.Fatalf("Stop did not return within %s while a STATE message was held",
|
|
maxWait)
|
|
}
|
|
|
|
// Stop has closed the queue, so takeAll is done or about to be.
|
|
msgs := <-taken
|
|
if len(msgs) != cap(queue)+held {
|
|
t.Fatalf("%d messages came off the queue, want the %d on it and the "+
|
|
"%d held", len(msgs), cap(queue), held)
|
|
}
|
|
|
|
for _, msg := range msgs[cap(queue):] {
|
|
if msg.Peer != stateDownPeer || msg.State != ristypes.StateDown {
|
|
t.Errorf("message after those on the queue has peer %q, state %q; "+
|
|
"want the down from %s", msg.Peer, msg.State, stateDownPeer)
|
|
}
|
|
}
|
|
|
|
select {
|
|
case err := <-streamErr:
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Errorf("stream returned %v, want %v", err, context.Canceled)
|
|
}
|
|
case <-time.After(maxWait):
|
|
t.Fatalf("stream did not end within %s of Stop", maxWait)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// used to end in "send on closed channel". The raw handler runs in that gap on
|
|
// the read loop itself, so calling Stop from it hits the gap every time.
|
|
func TestStopBeforeMessageReachesQueues(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = io.WriteString(w, updateLine)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := New(logger.New(), metrics.New())
|
|
s.url = srv.URL
|
|
s.RegisterHandler(updateHandler{})
|
|
s.RegisterRawHandler(func(string) { s.Stop() })
|
|
|
|
// Start would run the stream in the background, where the test cannot
|
|
// wait for it. Setting cancel as Start does lets Stop cancel the stream
|
|
// run here instead.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
s.cancel = cancel
|
|
|
|
err := s.stream(ctx)
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("stream returned %v, want %v", err, context.Canceled)
|
|
}
|
|
|
|
// A second Stop must not close the queues again.
|
|
s.Stop()
|
|
}
|
|
|
|
// TestReconnectWhenStreamEnds has the server end the first response
|
|
// normally, as when RIS Live restarts. The streamer must treat that as a
|
|
// disconnect and connect again.
|
|
func TestReconnectWhenStreamEnds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
endResponse := func(http.ResponseWriter, *http.Request) {}
|
|
checkReconnect(t, endResponse, streamIdleTimeout)
|
|
}
|
|
|
|
// TestReconnectWhenStreamGoesIdle has the server hold the first response
|
|
// open without sending anything. The streamer must drop it once the idle
|
|
// timeout passes and connect again.
|
|
func TestReconnectWhenStreamGoesIdle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const idleTimeout = 50 * time.Millisecond
|
|
|
|
holdResponse := func(w http.ResponseWriter, r *http.Request) {
|
|
flush(w)
|
|
<-r.Context().Done()
|
|
}
|
|
checkReconnect(t, holdResponse, idleTimeout)
|
|
}
|
|
|
|
// TestReconnectWhenServerNeverAnswers has the server accept the first
|
|
// request and never answer it, not even with headers. The streamer must give
|
|
// up on it once the idle timeout passes and connect again.
|
|
func TestReconnectWhenServerNeverAnswers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const idleTimeout = 50 * time.Millisecond
|
|
|
|
neverAnswer := func(_ http.ResponseWriter, r *http.Request) {
|
|
<-r.Context().Done()
|
|
}
|
|
checkReconnect(t, neverAnswer, idleTimeout)
|
|
}
|
|
|
|
// TestStreamNotDroppedWhileLinesArrive has the server send lines well within
|
|
// the idle timeout for several times the timeout, then end the response. Each
|
|
// line must start the idle timeout over, so the stream lasts until the server
|
|
// ends it instead of being dropped as idle.
|
|
func TestStreamNotDroppedWhileLinesArrive(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
idleTimeout = 100 * time.Millisecond
|
|
lineInterval = idleTimeout / 10
|
|
sendFor = 5 * idleTimeout
|
|
)
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
deadline := time.Now().Add(sendFor)
|
|
for time.Now().Before(deadline) {
|
|
_, _ = io.WriteString(w, updateLine)
|
|
|
|
flush(w)
|
|
time.Sleep(lineInterval)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := New(logger.New(), metrics.New())
|
|
s.url = srv.URL
|
|
s.idleTimeout = idleTimeout
|
|
|
|
err := s.stream(context.Background())
|
|
if !errors.Is(err, errStreamEnded) {
|
|
t.Fatalf("stream returned %v, want %v", err, errStreamEnded)
|
|
}
|
|
}
|
|
|
|
// TestStreamReadsLongLine has the server send one UPDATE line of about
|
|
// 200 KiB, padded with raw hex the way RIS Live writes an extended BGP
|
|
// message, and then end the response. The line is longer than a
|
|
// bufio.Scanner reads by default. The streamer must hand the message to the
|
|
// handler's queue and read on until the server ends the stream, instead of
|
|
// dropping the connection at the long line.
|
|
func TestStreamReadsLongLine(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const rawHexLength = 200 * bytesPerKB
|
|
|
|
longLine := `{"type":"ris_message","data":{"type":"UPDATE","peer":"192.0.2.1",` +
|
|
`"peer_asn":"64496","timestamp":1700000000,"raw":"` +
|
|
strings.Repeat("0", rawHexLength) + `"}}` + "\n"
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = io.WriteString(w, longLine)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := New(logger.New(), metrics.New())
|
|
s.url = srv.URL
|
|
s.RegisterHandler(updateHandler{})
|
|
|
|
err := s.stream(context.Background())
|
|
if !errors.Is(err, errStreamEnded) {
|
|
t.Fatalf("stream returned %v, want %v", err, errStreamEnded)
|
|
}
|
|
|
|
queued := len(s.handlers[0].queue)
|
|
if queued != 1 {
|
|
t.Errorf("handler queue holds %d messages, want 1", queued)
|
|
}
|
|
}
|
|
|
|
// TestPeerCountCountsEveryPeerHeardFrom has the server send an UPDATE from
|
|
// stateDownPeer and a KEEPALIVE from another peer, with no OPEN from either,
|
|
// and then a message that ends stateDownPeer's session: a NOTIFICATION, or a
|
|
// STATE message whose state is down. The peer count must be 2 after the first
|
|
// two messages and 1 after the third.
|
|
func TestPeerCountCountsEveryPeerHeardFrom(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
otherPeer = "192.0.2.2"
|
|
peerUpdateLine = `{"type":"ris_message","data":{"type":"UPDATE",` +
|
|
`"peer":"` + stateDownPeer + `",` +
|
|
`"peer_asn":"64496","timestamp":1700000000}}` + "\n"
|
|
otherPeerKeepaliveLine = `{"type":"ris_message","data":{"type":"KEEPALIVE",` +
|
|
`"peer":"` + otherPeer + `",` +
|
|
`"peer_asn":"64497","timestamp":1700000000}}` + "\n"
|
|
peerNotificationLine = `{"type":"ris_message","data":{"type":"NOTIFICATION",` +
|
|
`"peer":"` + stateDownPeer + `",` +
|
|
`"peer_asn":"64496","timestamp":1700000000}}` + "\n"
|
|
bothPeers = 2
|
|
)
|
|
|
|
sessionEndLines := map[string]string{
|
|
"NOTIFICATION": peerNotificationLine,
|
|
"STATE down": stateDownLine,
|
|
}
|
|
|
|
for name, sessionEndLine := range sessionEndLines {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tracker := metrics.New()
|
|
s := New(logger.New(), tracker)
|
|
|
|
streamLines(t, s, peerUpdateLine+otherPeerKeepaliveLine)
|
|
|
|
got := tracker.GetBGPPeerCount()
|
|
if got != bothPeers {
|
|
t.Fatalf("peer count is %d after messages from two peers, want %d",
|
|
got, bothPeers)
|
|
}
|
|
|
|
streamLines(t, s, sessionEndLine)
|
|
|
|
got = tracker.GetBGPPeerCount()
|
|
if got != 1 {
|
|
t.Errorf("peer count is %d after one of two peers' sessions "+
|
|
"ended, want 1", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// streamLines has a server send lines as its whole response and runs one
|
|
// stream of s against it.
|
|
func streamLines(t *testing.T, s *Streamer, lines string) {
|
|
t.Helper()
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, _ *http.Request) {
|
|
_, _ = io.WriteString(w, lines)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s.url = srv.URL
|
|
|
|
err := s.stream(context.Background())
|
|
if !errors.Is(err, errStreamEnded) {
|
|
t.Fatalf("stream returned %v, want %v", err, errStreamEnded)
|
|
}
|
|
}
|
|
|
|
// checkReconnect runs a streamer against a fake RIS Live server. The first
|
|
// response is served by firstResponse; every later one carries one UPDATE
|
|
// line and then stays open. The streamer must come back for a second
|
|
// response, must not be marked connected when it does, and must handle the
|
|
// UPDATE in it.
|
|
func checkReconnect(
|
|
t *testing.T, firstResponse http.HandlerFunc, idleTimeout time.Duration,
|
|
) {
|
|
t.Helper()
|
|
|
|
const maxWait = 5 * time.Second
|
|
|
|
tracker := metrics.New()
|
|
connectedAtReconnect := make(chan bool, 1)
|
|
|
|
var requests atomic.Int32
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(
|
|
func(w http.ResponseWriter, r *http.Request) {
|
|
if requests.Add(1) == 1 {
|
|
firstResponse(w, r)
|
|
|
|
return
|
|
}
|
|
|
|
select {
|
|
case connectedAtReconnect <- tracker.IsConnected():
|
|
default:
|
|
}
|
|
|
|
_, _ = io.WriteString(w, updateLine)
|
|
|
|
flush(w)
|
|
<-r.Context().Done()
|
|
}))
|
|
defer srv.Close()
|
|
|
|
s := New(logger.New(), tracker)
|
|
s.url = srv.URL
|
|
s.idleTimeout = idleTimeout
|
|
s.minBackoff = time.Millisecond
|
|
handler := updateHandler{handled: make(chan struct{}, 1)}
|
|
s.RegisterHandler(handler)
|
|
|
|
err := s.Start(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("Start returned error: %v", err)
|
|
}
|
|
defer s.Stop()
|
|
|
|
select {
|
|
case <-handler.handled:
|
|
case <-time.After(maxWait):
|
|
t.Fatalf("no message from a second response handled within %s", maxWait)
|
|
}
|
|
|
|
if <-connectedAtReconnect {
|
|
t.Error("streamer was still marked connected when it came back " +
|
|
"for a second response")
|
|
}
|
|
}
|
|
|
|
// settledGoroutineCount lets transient goroutines finish, then reports the
|
|
// current count.
|
|
func settledGoroutineCount() int {
|
|
prev := runtime.NumGoroutine()
|
|
|
|
for range 20 {
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
cur := runtime.NumGoroutine()
|
|
if cur == prev {
|
|
return cur
|
|
}
|
|
|
|
prev = cur
|
|
}
|
|
|
|
return prev
|
|
}
|
|
|
|
// waitForGoroutines waits until the goroutine count drops to target or below.
|
|
func waitForGoroutines(target int) bool {
|
|
for range 100 {
|
|
if runtime.NumGoroutine() <= target {
|
|
return true
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
}
|
|
|
|
return false
|
|
}
|