Keep taking messages while a peer's session goes down (closes #216)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The table's PeerDown visits every prefix under the write lock, which takes over a second on a full table. The prefix handler called it inline, so its queue filled and the streamer dropped updates. PeerDown now holds the lock for 10,000 prefixes at a time, then lets the lock go for as long again. The handler runs PeerDown on a separate goroutine, one peer at a time. The handler holds back the down peer's later messages until that PeerDown returns. Then the handler applies them in order, one at a time, and takes new messages in between. It holds back at most 20,000 messages. Stop waits for all of this before its last flush. Model: opus-5-5
This commit was merged in pull request #220.
This commit is contained in:
1 parent
df15e2b2cd
commit
26d939ad72
9 files changed
+608
-54
No files matched your search
@@ -7,8 +7,10 @@ import (
|
||||
"log/slog"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -101,6 +103,228 @@ func TestPrefixHandlerAppliesUpdatesToTheRoutingTable(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestPrefixHandlerTakesMessagesWhileAPeerGoesDown holds up the PeerDown of a
|
||||
// peer whose session went down, as another peer's long PeerDown would. The
|
||||
// handler must go on taking messages. It applies another peer's announcement
|
||||
// at once. It holds back these messages of the down peer, in this order:
|
||||
// - an announcement;
|
||||
// - its session going down again;
|
||||
// - a second announcement.
|
||||
//
|
||||
// Once the PeerDowns have returned, the down peer must carry only the prefix
|
||||
// of the second announcement.
|
||||
func TestPrefixHandlerTakesMessagesWhileAPeerGoesDown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := prefixHandlerWithoutTimer(newMockStore())
|
||||
h.HandleMessage(peerAnnouncement(testPeerIP, testV4Prefix))
|
||||
|
||||
h.peerDownMu.Lock()
|
||||
|
||||
handled := returnsInTime(func() {
|
||||
for _, msg := range []*ristypes.RISMessage{
|
||||
sessionDown(),
|
||||
peerAnnouncement(secondPeerIP, otherV4Prefix),
|
||||
peerAnnouncement(testPeerIP, testV6Prefix),
|
||||
sessionDown(),
|
||||
peerAnnouncement(testPeerIP, setV4Prefix),
|
||||
} {
|
||||
h.HandleMessage(msg)
|
||||
}
|
||||
})
|
||||
if !handled {
|
||||
t.Fatalf("the handler did not take the messages after the session "+
|
||||
"went down within %s", stepTimeout)
|
||||
}
|
||||
|
||||
checkTableOrigins(t, h.table, map[string]map[int][]string{
|
||||
testV4Prefix: {testASNB: {testPeerIP}},
|
||||
otherV4Prefix: {testASNB: {secondPeerIP}},
|
||||
testV6Prefix: {},
|
||||
setV4Prefix: {},
|
||||
})
|
||||
|
||||
h.peerDownMu.Unlock()
|
||||
|
||||
if !returnsInTime(h.peerDowns.Wait) {
|
||||
t.Fatalf("the PeerDowns did not return within %s", stepTimeout)
|
||||
}
|
||||
|
||||
checkTableOrigins(t, h.table, map[string]map[int][]string{
|
||||
testV4Prefix: {},
|
||||
otherV4Prefix: {testASNB: {secondPeerIP}},
|
||||
testV6Prefix: {},
|
||||
setV4Prefix: {testASNB: {testPeerIP}},
|
||||
})
|
||||
}
|
||||
|
||||
// TestPrefixHandlerAppliesMessagesWhilePeerDownRuns gives a peer 50,000
|
||||
// prefixes and hands the prefix handler that peer's session going down. The
|
||||
// table's PeerDown then visits the prefixes in five batches. Once PeerDown has
|
||||
// begun, the handler is handed 1,000 announcements of another peer. The
|
||||
// handler must apply them all before PeerDown ends, while the down peer still
|
||||
// carries some of its prefixes.
|
||||
func TestPrefixHandlerAppliesMessagesWhilePeerDownRuns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
// downPeerPrefixes is five times the 10,000 prefixes PeerDown visits
|
||||
// in one hold of the table's lock.
|
||||
downPeerPrefixes = 50000
|
||||
// otherPeerMessages is far more messages than the handler applies
|
||||
// while PeerDown runs, if PeerDown takes the lock back at once after
|
||||
// each batch. The handler then applies only a few for each batch.
|
||||
otherPeerMessages = 1000
|
||||
// firstOtherAddr is the address of the other peer's first prefix.
|
||||
firstOtherAddr = "2001:db8:2::"
|
||||
)
|
||||
|
||||
h := prefixHandlerWithoutTimer(newMockStore())
|
||||
announceManyPrefixes(h.table, downPeerPrefixes)
|
||||
|
||||
h.HandleMessage(sessionDown())
|
||||
|
||||
// Wait until PeerDown has begun: the down peer has left some of its
|
||||
// prefixes. The down peer's prefixes are IPv4 and the other peer's are
|
||||
// IPv6, so the counts tell them apart.
|
||||
for h.table.Counts().IPv4Routes == downPeerPrefixes {
|
||||
runtime.Gosched()
|
||||
}
|
||||
|
||||
// Each announcement names the /128 of the next address.
|
||||
addr := netip.MustParseAddr(firstOtherAddr)
|
||||
for range otherPeerMessages {
|
||||
h.HandleMessage(peerAnnouncement(secondPeerIP,
|
||||
netip.PrefixFrom(addr, addr.BitLen()).String()))
|
||||
|
||||
addr = addr.Next()
|
||||
}
|
||||
|
||||
counts := h.table.Counts()
|
||||
if counts.IPv6Routes != otherPeerMessages {
|
||||
t.Errorf("the other peer carries %d prefixes, want %d",
|
||||
counts.IPv6Routes, otherPeerMessages)
|
||||
}
|
||||
|
||||
if counts.IPv4Routes == 0 {
|
||||
t.Errorf("PeerDown ended before the handler applied the other peer's "+
|
||||
"%d announcements", otherPeerMessages)
|
||||
}
|
||||
|
||||
if !returnsInTime(h.peerDowns.Wait) {
|
||||
t.Fatalf("the PeerDown did not return within %s", stepTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrefixHandlerTakesMessagesWhileItAppliesHeldBackOnes holds back many
|
||||
// messages of a peer whose session went down. Each names a prefix that does
|
||||
// not parse, so applying it logs an error. Each log write takes a
|
||||
// millisecond. While the handler applies the held-back messages, it must take
|
||||
// another peer's announcement.
|
||||
func TestPrefixHandlerTakesMessagesWhileItAppliesHeldBackOnes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
// heldMessages take the handler at least a second to apply.
|
||||
heldMessages = 1000
|
||||
badPrefix = "not a prefix"
|
||||
)
|
||||
|
||||
logs := &slowWriter{firstWrite: make(chan struct{})}
|
||||
h := newPrefixHandler(newMockStore(), newErrorLogger(logs),
|
||||
routetable.New(routetable.Params{}), longerThanAnyTest,
|
||||
maxTableChangesPerWrite)
|
||||
|
||||
defer stopInTime(t, h)
|
||||
|
||||
h.peerDownMu.Lock()
|
||||
h.HandleMessage(sessionDown())
|
||||
|
||||
for range heldMessages {
|
||||
h.HandleMessage(peerAnnouncement(testPeerIP, badPrefix))
|
||||
}
|
||||
|
||||
h.peerDownMu.Unlock()
|
||||
|
||||
select {
|
||||
case <-logs.firstWrite:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatal("the handler did not start to apply the held-back messages")
|
||||
}
|
||||
|
||||
if !returnsInTime(func() {
|
||||
h.HandleMessage(peerAnnouncement(secondPeerIP, otherV4Prefix))
|
||||
}) {
|
||||
t.Fatalf("HandleMessage did not return within %s", stepTimeout)
|
||||
}
|
||||
|
||||
h.mu.Lock()
|
||||
left := len(h.heldBack[testPeerIP])
|
||||
h.mu.Unlock()
|
||||
|
||||
if left == 0 {
|
||||
t.Error("the handler took another peer's announcement only once it " +
|
||||
"had applied every held-back message")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrefixHandlerWaitsAtTheMaximumHeldBack holds up the PeerDown of a peer
|
||||
// whose session went down. It hands the handler maxHeldBackMessages messages
|
||||
// of that peer. HandleMessage must then wait, even for another peer's
|
||||
// message, until the PeerDown returns and a held-back message is applied.
|
||||
func TestPrefixHandlerWaitsAtTheMaximumHeldBack(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h := newPrefixHandler(newMockStore(), logger.New(),
|
||||
routetable.New(routetable.Params{}), longerThanAnyTest,
|
||||
maxTableChangesPerWrite)
|
||||
defer stopInTime(t, h)
|
||||
|
||||
h.peerDownMu.Lock()
|
||||
h.HandleMessage(sessionDown())
|
||||
|
||||
held := peerAnnouncement(testPeerIP, testV6Prefix)
|
||||
for range maxHeldBackMessages {
|
||||
h.HandleMessage(held)
|
||||
}
|
||||
|
||||
returned := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
h.HandleMessage(peerAnnouncement(secondPeerIP, otherV4Prefix))
|
||||
close(returned)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-returned:
|
||||
t.Error("HandleMessage returned while maxHeldBackMessages messages " +
|
||||
"were held back")
|
||||
case <-time.After(stillWaitingAfter):
|
||||
}
|
||||
|
||||
h.peerDownMu.Unlock()
|
||||
|
||||
select {
|
||||
case <-returned:
|
||||
case <-time.After(stepTimeout):
|
||||
t.Fatal("HandleMessage did not return after the PeerDown returned")
|
||||
}
|
||||
}
|
||||
|
||||
// slowWriter takes a millisecond over each write and drops what it is given.
|
||||
// It closes firstWrite at the first write.
|
||||
type slowWriter struct {
|
||||
firstWrite chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func (w *slowWriter) Write(p []byte) (int, error) {
|
||||
w.once.Do(func() { close(w.firstWrite) })
|
||||
time.Sleep(time.Millisecond)
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// TestPrefixHandlerAppliesAPathWithoutAnOriginToTheRoutingTable hands the
|
||||
// prefix handler announcements whose AS path gives no origin. One whose path
|
||||
// is empty or holds AS 0 removes the peer's route, as a withdrawal does; one
|
||||
@@ -199,8 +423,9 @@ func TestPrefixHandlerKeepsThePathOfAPathEndingInASet(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// checkTableCases hands the messages of each case to a new prefix handler and
|
||||
// checks its routing table right after, with no flush.
|
||||
// checkTableCases hands the messages of each case to a new prefix handler. It
|
||||
// checks the handler's routing table once the handler's PeerDowns have
|
||||
// returned, with no flush.
|
||||
func checkTableCases(t *testing.T, tests []tableCase) {
|
||||
t.Helper()
|
||||
|
||||
@@ -213,17 +438,28 @@ func checkTableCases(t *testing.T, tests []tableCase) {
|
||||
h.HandleMessage(msg)
|
||||
}
|
||||
|
||||
for prefix, want := range tt.want {
|
||||
got := tableOrigins(h.table, prefix)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("peers of %s by origin AS = %v, want %v",
|
||||
prefix, got, want)
|
||||
}
|
||||
}
|
||||
h.peerDowns.Wait()
|
||||
checkTableOrigins(t, h.table, tt.want)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// checkTableOrigins checks, for each prefix in want, the peers that carry it
|
||||
// in table with each origin AS.
|
||||
func checkTableOrigins(
|
||||
t *testing.T, table *routetable.Table, want map[string]map[int][]string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
for prefix, wantOrigins := range want {
|
||||
got := tableOrigins(table, prefix)
|
||||
if !reflect.DeepEqual(got, wantOrigins) {
|
||||
t.Errorf("peers of %s by origin AS = %v, want %v",
|
||||
prefix, got, wantOrigins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tableWritesStore is a real database that hands each write of the routing
|
||||
// table's changes to written once it is made.
|
||||
type tableWritesStore struct {
|
||||
@@ -498,6 +734,7 @@ func TestARestartReadsTheRoutingTableBack(t *testing.T) {
|
||||
h.HandleMessage(msg)
|
||||
}
|
||||
|
||||
h.peerDowns.Wait()
|
||||
h.flushBatch()
|
||||
|
||||
var logs bytes.Buffer
|
||||
@@ -536,6 +773,7 @@ func TestARestartReadsTheRoutingTableBack(t *testing.T) {
|
||||
restarted.HandleMessage(msg)
|
||||
}
|
||||
|
||||
h.peerDowns.Wait()
|
||||
restarted.Stop(t.Context())
|
||||
|
||||
read, err := db.ReadRouteTable(t.Context())
|
||||
|
||||
Reference in new issue
Block a user