check / check (push) Waiting to run
Shutdown stopped the handlers before the streamer, so the updates still on the handler queues reached handlers that no longer wrote them. It now stops the streamer first. Streamer.Stop puts the STATE messages it holds for a full queue on that queue, closes the queues and returns once every worker has handed its handler everything on its queue; then each handler writes what it holds. PeeringHandler.Stop now waits for its loop's final write instead of racing it. Streamer.SetURL lets the shutdown test read from a local server. 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
|
|
}
|