1 Commits
Author SHA1 Message Date
sneak 14ef9e5da2 Add internal/routetable, the routing table in memory (part of #122)
check / check (push) Canceled after 0s
A new package holds every prefix some peer carries. For each prefix and
origin AS it keeps the set of peers that carry it, one bit per peer,
and the two newest of their routes in full. It applies announcements,
withdrawals and peers' sessions going down by the rule confirmed on the
issue, and records which pairs changed for the database writes to come.
It answers the IP lookup, an AS's prefixes, a prefix's origins, a sample
of one mask length, and the counts the stats show. Nothing uses it yet.

The peer sets use github.com/bits-and-blooms/bitset. The kept routes'
times are held in order in github.com/google/btree, so the oldest and
newest need no scan.

Model: opus-5-5
2026-10-27 06:22:52 +00:00
2 changed files with 127 additions and 16 deletions
+36 -16
View File
@@ -1,8 +1,8 @@
// Package routetable holds the whole routing table in memory. For each prefix
// and origin AS it keeps the set of peers whose current route that is, and the
// two newest of those routes. It applies announcements, withdrawals and peers'
// sessions going down, and it answers the route reads the pages and the API
// need.
// and origin AS, it keeps the peers whose current route to the prefix has that
// origin AS. It also keeps the two newest of those routes in full. It applies
// announcements, withdrawals and peers' sessions going down. It answers the
// route reads that the pages and the API need.
package routetable
import (
@@ -326,20 +326,29 @@ func (t *Table) removeOrigin(prefix netip.Prefix, o *origin) {
// join puts the peer of r in the peer set of o, an origin of prefix, and keeps
// r in full when it is one of the two newest routes of o. The peer's earlier
// route, if o keeps one, is replaced: it is no longer the peer's current route.
// The pair is recorded as changed only when its peer set or kept routes change.
func (t *Table) join(prefix netip.Prefix, o *origin, r route) {
if slices.ContainsFunc(o.routes, r.equal) {
// The peer announced its kept route again.
return
}
wasWithoutKeptRoute := o.withoutKeptRoute()
if !o.peers.Test(r.peer) {
joined := !o.peers.Test(r.peer)
if joined {
o.peers.Set(r.peer)
t.familyOf(prefix.Addr()).routes++
t.peerPrefixes[r.peer]++
}
t.dropKeptRoute(o, r.peer)
t.keepIfNewest(o, r)
dropped := t.dropKeptRoute(o, r.peer)
kept := t.keepIfNewest(o, r)
t.recordChange(prefix, o, wasWithoutKeptRoute)
if joined || dropped || kept {
t.recordChange(prefix, o, wasWithoutKeptRoute)
}
}
// leave takes peer out of the peer set of o, an origin of prefix, and drops
@@ -356,10 +365,10 @@ func (t *Table) leave(prefix netip.Prefix, o *origin, peer uint) {
t.recordChange(prefix, o, wasWithoutKeptRoute)
}
// keepIfNewest keeps r in o unless both kept routes of o are newer. When o
// already keeps two routes, the older one is dropped, and its peer keeps only
// its bit.
func (t *Table) keepIfNewest(o *origin, r route) {
// keepIfNewest keeps r in o unless both kept routes of o are newer, and says
// whether it kept r. When o already keeps two routes, the older one is
// dropped, and its peer keeps only its bit.
func (t *Table) keepIfNewest(o *origin, r route) bool {
// The kept routes stay newest first: r goes before the first one that is
// not newer than it.
i := 0
@@ -368,7 +377,7 @@ func (t *Table) keepIfNewest(o *origin, r route) {
}
if i == keptRoutesPerOrigin {
return
return false
}
if len(o.routes) == keptRoutesPerOrigin {
@@ -377,18 +386,22 @@ func (t *Table) keepIfNewest(o *origin, r route) {
o.routes = slices.Insert(o.routes, i, r)
t.countKeptTime(r.time, 1)
return true
}
// dropKeptRoute drops the peer's route from the kept routes of o, if o keeps
// one.
func (t *Table) dropKeptRoute(o *origin, peer uint) {
// one, and says whether it did.
func (t *Table) dropKeptRoute(o *origin, peer uint) bool {
i := slices.IndexFunc(o.routes, func(r route) bool { return r.peer == peer })
if i < 0 {
return
return false
}
t.countKeptTime(o.routes[i].time, -1)
o.routes = slices.Delete(o.routes, i, i+1)
return true
}
// recordChange records that the pair of prefix and the AS of o changed, and
@@ -545,6 +558,13 @@ type route struct {
time time.Time
}
// equal says whether r and other are the same peer's route with the same path
// and time.
func (r route) equal(other route) bool {
return r.peer == other.peer && r.time.Equal(other.time) &&
slices.Equal(r.path, other.path)
}
// pair names one prefix and origin AS.
type pair struct {
prefix netip.Prefix
+91
View File
@@ -4,6 +4,7 @@ import (
"net/netip"
"reflect"
"slices"
"sync"
"testing"
"time"
@@ -425,6 +426,96 @@ func TestTakeChanges(t *testing.T) {
}, map[string]uint{})
}
func TestTakeChangesHoldsOnlyPairsThatChanged(t *testing.T) {
t.Parallel()
table := routetable.New(routetable.Params{})
v4 := netip.MustParsePrefix(prefixV4)
v6 := netip.MustParsePrefix(prefixV6)
// peerA, peerB and peerC are numbered 0, 1 and 2. The routes of peerC and
// peerB to v4 are kept; peerA keeps only its bit.
announceAll(table, prefixV4, asA, 10, peerA, peerB, peerC)
table.Announce(v6, asA, routeOf(peerB, asA, 10))
table.TakeChanges()
// peerA announces again, with a time older than both kept routes.
table.Announce(v4, asA, routeOf(peerA, asA, 15))
checkChanges(t, table, []change{}, map[string]uint{})
// peerC announces its kept route again.
table.Announce(v4, asA, routeOf(peerC, asA, 30))
checkChanges(t, table, []change{}, map[string]uint{})
// peerC announces the same path with a newer time, so its kept route
// changes.
table.Announce(v4, asA, routeOf(peerC, asA, 40))
checkChanges(t, table, []change{
{Prefix: prefixV4, OriginASN: asA, Peers: []uint{0, 1, 2},
Routes: []routetable.Route{
routeOf(peerC, asA, 40), routeOf(peerB, asA, 20),
}},
}, map[string]uint{})
// peerA withdraws v6, which it does not carry.
table.Withdraw(v6, peerA)
checkChanges(t, table, []change{}, map[string]uint{})
// peerA going down changes v4, the one prefix it carries.
table.PeerDown(peerA)
checkChanges(t, table, []change{
{Prefix: prefixV4, OriginASN: asA, Peers: []uint{1, 2},
Routes: []routetable.Route{
routeOf(peerC, asA, 40), routeOf(peerB, asA, 20),
}},
}, map[string]uint{})
// peerA, which carries nothing now, goes down again.
table.PeerDown(peerA)
checkChanges(t, table, []change{}, map[string]uint{})
}
// TestConcurrentUse has goroutines update one table while others read it. The
// race detector, which make test runs, fails it when a method uses the table
// without its lock.
func TestConcurrentUse(t *testing.T) {
t.Parallel()
const (
// rounds is how many times each goroutine calls its method.
rounds = 1000
ipVersion = 4
sampleSize = 10
)
table := routetable.New(routetable.Params{})
prefix := netip.MustParsePrefix(prefixV4)
uses := []func(round int){
func(round int) { table.Announce(prefix, asA, routeOf(peerA, asA, round)) },
func(round int) { table.Announce(prefix, asB, routeOf(peerB, asB, round)) },
func(int) { table.Withdraw(prefix, peerA) },
func(int) { table.PeerDown(peerB) },
func(int) { table.TakeChanges() },
func(int) { table.Lookup(prefix.Addr()) },
func(int) { table.Origins(prefix) },
func(int) { table.OriginatedPrefixes(asA) },
func(int) { table.SamplePrefixes(ipVersion, prefix.Bits(), sampleSize) },
func(int) { table.Counts() },
}
var wg sync.WaitGroup
for _, use := range uses {
wg.Go(func() {
for round := range rounds {
use(round)
}
})
}
wg.Wait()
}
// checkChanges checks that TakeChanges returns want and wantPeers.
func checkChanges(
t *testing.T, table *routetable.Table, want []change,