Remove the per-peer route tables (closes #122)
check / check (push) Canceled after 0s

The routing table in memory now holds every route, so the two per-peer route
tables go, with every write, delete, count and read that served them. The
prefix handler applies each update to the table, writes the table's changes
every second, and stores each new prefix with its first-seen time. The peer
delete, its retry on the next flush and the updates held for it go too. The
types and errors of the server's answers move to internal/server.

The image's memory settings fit a full table of about 1 GB: GOMEMLIMIT of
2.5 GiB, a 512 MiB page cache for the writing connection, and SQLite heap
limits of 768 MiB and 1 GiB. The README describes the new table, its
database copy and the memory budget.

Model: opus-5-5
This commit was merged in pull request #195.
This commit is contained in:
2026-10-27 17:22:32 +01:00
parent 506e289a63
commit 1c65656b58
23 changed files with 701 additions and 5273 deletions
@@ -8,6 +8,7 @@ import (
"net/netip"
"reflect"
"slices"
"strings"
"testing"
"time"
@@ -84,13 +85,25 @@ func TestPrefixHandlerAppliesUpdatesToTheRoutingTable(t *testing.T) {
testV6Prefix: {},
},
},
{
name: "an announcement after the peer's session went down",
messages: []*ristypes.RISMessage{
peerAnnouncement(testPeerIP, testV4Prefix),
sessionDown(),
peerAnnouncement(testPeerIP, otherV4Prefix),
},
want: map[string]map[int][]string{
testV4Prefix: {},
otherV4Prefix: {testASNB: {testPeerIP}},
},
},
})
}
// TestPrefixHandlerAppliesAPathWithoutAnOriginToTheRoutingTable hands the
// prefix handler announcements whose AS path gives no origin. One whose path
// holds AS 0 removes the peer's route, as a withdrawal does; one whose path ends
// in an AS set of several members is kept with origin 0.
// is empty or holds AS 0 removes the peer's route, as a withdrawal does; one
// whose path ends in an AS set of several members is kept with origin 0.
func TestPrefixHandlerAppliesAPathWithoutAnOriginToTheRoutingTable(t *testing.T) {
t.Parallel()
@@ -110,6 +123,14 @@ func TestPrefixHandlerAppliesAPathWithoutAnOriginToTheRoutingTable(t *testing.T)
},
want: map[string]map[int][]string{testV4Prefix: {}},
},
{
name: "an empty path",
messages: []*ristypes.RISMessage{
peerAnnouncement(testPeerIP, testV4Prefix),
announceWithPath(ristypes.ASPath{}),
},
want: map[string]map[int][]string{testV4Prefix: {}},
},
{
name: "a path with AS 0 before its end",
messages: []*ristypes.RISMessage{
@@ -128,6 +149,55 @@ func TestPrefixHandlerAppliesAPathWithoutAnOriginToTheRoutingTable(t *testing.T)
})
}
// TestPrefixHandlerKeepsThePathOfAPathEndingInASet announces a prefix with AS
// paths that end in an AS set. Any member of a set of several may be the
// origin, so the routing table keeps the route with origin 0 (none); a set of
// one member counts as that AS. The kept AS path lists the set's members after
// the rest of the path.
func TestPrefixHandlerKeepsThePathOfAPathEndingInASet(t *testing.T) {
t.Parallel()
tests := []struct {
name string
path []any
wantOrigin int
wantPath []int
}{
{
name: "a set of several members",
path: []any{testASNA, testASNB, []int{testASNC, testASND}},
wantPath: []int{testASNA, testASNB, testASNC, testASND},
},
{
name: "a set of one member",
path: []any{testASNA, testASNB, []int{testASNC}},
wantOrigin: testASNC,
wantPath: []int{testASNA, testASNB, testASNC},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
msg := peerAnnouncement(testPeerIP, testV4Prefix)
msg.Path = decodePath(t, tt.path...)
h := prefixHandlerWithoutTimer(newMockStore())
h.HandleMessage(msg)
origins := h.table.Origins(netip.MustParsePrefix(testV4Prefix))
if len(origins) != 1 || origins[0].ASN != tt.wantOrigin ||
len(origins[0].Routes) != 1 ||
!slices.Equal(origins[0].Routes[0].ASPath, tt.wantPath) {
t.Errorf("Origins(%s) = %+v, want one route with origin AS%d "+
"and AS path %v", testV4Prefix, origins, tt.wantOrigin,
tt.wantPath)
}
})
}
}
// checkTableCases hands the messages of each case to a new prefix handler and
// checks its routing table right after, with no flush.
func checkTableCases(t *testing.T, tests []tableCase) {
@@ -343,8 +413,9 @@ func TestPrefixHandlerWritesManyTableChangesOverSeveralFlushes(t *testing.T) {
// TestPrefixHandlerStopLogsTheTableChangesLeftUnwritten stops a prefix handler,
// with no deadline, whose routing table holds more changed pairs than one write
// takes. Its last flush writes one write's worth, so Stop must log the pairs
// that no write took. The flush timer must not fire, or its writes could leave
// no pairs for Stop to log.
// that no write took, and must not say that the stop deadline passed. The
// flush timer must not fire, or its writes could leave no pairs for Stop to
// log.
func TestPrefixHandlerStopLogsTheTableChangesLeftUnwritten(t *testing.T) {
t.Parallel()
@@ -364,7 +435,13 @@ func TestPrefixHandlerStopLogsTheTableChangesLeftUnwritten(t *testing.T) {
written += n
}
if logged := lastLogged(t, &logs); logged.TableChanges != pairs-written {
logged := lastLogged(t, &logs)
if strings.Contains(logged.Msg, "deadline") {
t.Errorf("Stop logged %q although its deadline had not passed",
logged.Msg)
}
if logged.TableChanges != pairs-written {
t.Errorf("Stop logged %d table changes not written, want %d",
logged.TableChanges, pairs-written)
}