1 Commits
Author SHA1 Message Date
clawbot 423fb0fc55 Apply each peer's last update for a prefix in a batch (closes #54)
check / check (push) Canceled after 0s
The prefix handler's batch kept one update per prefix across all peers,
chosen by timestamp. One routing change reaches many peers within a second,
so most peers' announcements and withdrawals in a batch were dropped, and
for updates in the same second the first one won.

The batch now keeps the last update per prefix and peer, in the order the
feed delivered them, so updates from every peer are applied. A message's
withdrawals are queued before its announcements, so a message that lists a
prefix as both keeps the route (RFC 4271 section 4.3). Tests cover each case
and check the in-memory route count and prefix distribution against the
route table.

Model: opus-5-5
2026-10-14 13:28:44 +00:00
2 changed files with 33 additions and 17 deletions
+18 -17
View File
@@ -105,7 +105,24 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
h.mu.Lock()
defer h.mu.Unlock()
// Process announcements
// Withdrawals are queued before announcements, because the batch keeps
// each peer's last update for a prefix: a message that lists a prefix as
// both withdrawn and announced keeps the route (RFC 4271 section 4.3).
for _, prefix := range msg.Withdrawals {
h.batch = append(h.batch, prefixUpdate{
prefix: canonicalPrefix(prefix),
originASN: originASN, // Use the originASN from path if available
peer: msg.Peer,
messageType: "withdrawal",
timestamp: timestamp,
path: msg.Path,
})
// Record withdrawal in metrics
if h.metrics != nil {
h.metrics.RecordWithdrawal()
}
}
for _, announcement := range msg.Announcements {
for _, prefix := range announcement.Prefixes {
h.batch = append(h.batch, prefixUpdate{
@@ -123,22 +140,6 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
}
}
// Process withdrawals
for _, prefix := range msg.Withdrawals {
h.batch = append(h.batch, prefixUpdate{
prefix: canonicalPrefix(prefix),
originASN: originASN, // Use the originASN from path if available
peer: msg.Peer,
messageType: "withdrawal",
timestamp: timestamp,
path: msg.Path,
})
// Record withdrawal in metrics
if h.metrics != nil {
h.metrics.RecordWithdrawal()
}
}
// Check if we need to flush
if len(h.batch) >= prefixBatchSize {
h.flushBatchLocked()
+15
View File
@@ -100,6 +100,12 @@ func TestPrefixHandlerAppliesEachPeersLastUpdateInABatch(t *testing.T) {
withdraw := func(peer string) *ristypes.RISMessage {
return &ristypes.RISMessage{Peer: peer, ParsedTimestamp: ts, Withdrawals: []string{prefix}}
}
announceAndWithdraw := func(peer string) *ristypes.RISMessage {
msg := announce(peer)
msg.Withdrawals = []string{prefix}
return msg
}
tests := []struct {
name string
@@ -133,6 +139,15 @@ func TestPrefixHandlerAppliesEachPeersLastUpdateInABatch(t *testing.T) {
},
wantPeers: []string{testPeerIP},
},
{
// RFC 4271 section 4.3: the announcement counts, not the withdrawal.
name: "one message announces and withdraws the prefix",
batches: [][]*ristypes.RISMessage{
{announce(testPeerIP)},
{announceAndWithdraw(testPeerIP)},
},
wantPeers: []string{testPeerIP},
},
}
for _, tt := range tests {