Write the routing table every second and read it at startup (part of #122)
check / check (push) Canceled after 0s

The prefix handler applies each update to the routing table in memory as
it handles it, sorted into announcements and withdrawals as for the live
route tables. Each flush first writes at most 20,000 of the table's
changed pairs, with every new peer; the rest follow in the next flushes.
A failed write puts its changes and new peers back in the table. Stop
makes one last flush, and logs the changed pairs left.

Startup reads the table before the streamer connects, and logs its pairs,
peers and time. The per-peer counts are a slice, so the read takes about
a third less time. The health check's start period is 60 seconds, so a
full table's read fits before upaas looks.

Model: opus-5-5
This commit was merged in pull request #187.
This commit is contained in:
2026-10-27 10:27:11 +01:00
parent eda37da369
commit bac531c76d
16 changed files with 1023 additions and 114 deletions
@@ -16,6 +16,7 @@ import (
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/logger"
"git.eeqj.de/sneak/routewatch/internal/ristypes"
"git.eeqj.de/sneak/routewatch/internal/routetable"
)
const testPeerIP = "2001:db8:ffff::1"
@@ -40,9 +41,7 @@ func TestPrefixHandlerStoresPrefixesTheIPLookupFinds(t *testing.T) {
t.Parallel()
db := newTestDatabase(t)
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
h := &PrefixHandler{db: db, logger: logger.New()}
h := prefixHandlerWithoutTimer(db)
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
h.HandleMessage(&ristypes.RISMessage{
@@ -462,8 +461,7 @@ func TestPrefixHandlerDoesNotStoreAPrefixThatDoesNotParse(t *testing.T) {
t.Fatalf("failed to create database: %v", err)
}
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
h := &PrefixHandler{db: db, logger: logger.New()}
h := prefixHandlerWithoutTimer(db)
withdrawal := &ristypes.RISMessage{
Type: ristypes.MessageTypeUpdate,
@@ -694,8 +692,7 @@ func TestPrefixHandlerHandlesADownWhileAFlushWrites(t *testing.T) {
release: make(chan struct{}),
}
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
h := &PrefixHandler{db: store, logger: logger.New()}
h := prefixHandlerWithoutTimer(store)
h.HandleMessage(peerAnnouncement(testPeerIP, beforeDown))
flushed := make(chan struct{})
@@ -832,10 +829,7 @@ func TestPrefixHandlerRetriesAFailedDeleteOfAPeersRoutes(t *testing.T) {
db := newTestDatabase(t)
store := &firstPeerDeleteFailsStore{Database: db}
// Built without NewPrefixHandler's flush timer, so the test
// flushes itself.
h := &PrefixHandler{db: store, logger: logger.New()}
h := prefixHandlerWithoutTimer(store)
store.whileFailing = func() {
for _, msg := range tt.whileFailing {
h.HandleMessage(msg)
@@ -897,11 +891,7 @@ func TestPrefixHandlerKeepsAtMostABatchOfUpdatesWhileADeleteKeepsFailing(
otherPeersPrefixes := []string{testV4Prefix, testV6Prefix, "2001:db8:2::/48"}
db := newTestDatabase(t)
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
h := &PrefixHandler{
db: peerDeleteAlwaysFailsStore{Database: db}, logger: logger.New(),
}
h := prefixHandlerWithoutTimer(peerDeleteAlwaysFailsStore{Database: db})
h.HandleMessage(sessionDown())
// The peer announces a /32 for each address from 10.0.0.0 on, each
@@ -953,7 +943,7 @@ func TestPrefixHandlerStopLogsAFailedDeleteWithoutTheDeadline(t *testing.T) {
var logs bytes.Buffer
h := NewPrefixHandler(peerDeleteAlwaysFailsStore{Database: newTestDatabase(t)},
newErrorLogger(&logs))
newErrorLogger(&logs), routetable.New(routetable.Params{}))
h.HandleMessage(sessionDown())
h.HandleMessage(peerAnnouncement(testPeerIP, testV4Prefix))
h.Stop(t.Context())
@@ -979,7 +969,8 @@ func TestPrefixHandlerStopLogsNothingAfterADeleteThatSucceeds(t *testing.T) {
var logs bytes.Buffer
h := NewPrefixHandler(newTestDatabase(t), newErrorLogger(&logs))
h := NewPrefixHandler(newTestDatabase(t), newErrorLogger(&logs),
routetable.New(routetable.Params{}))
h.HandleMessage(sessionDown())
h.Stop(t.Context())
@@ -989,6 +980,17 @@ func TestPrefixHandlerStopLogsNothingAfterADeleteThatSucceeds(t *testing.T) {
}
}
// prefixHandlerWithoutTimer returns a prefix handler that writes to store and
// applies the updates to an empty routing table. It is built without
// NewPrefixHandler's flush goroutine, so the test flushes itself.
func prefixHandlerWithoutTimer(store database.Store) *PrefixHandler {
return &PrefixHandler{
db: store,
logger: logger.New(),
table: routetable.New(routetable.Params{}),
}
}
// newTestDatabase opens a database in a new temporary directory. It is closed
// when the test ends.
func newTestDatabase(t *testing.T) *database.Database {
@@ -1012,9 +1014,7 @@ func handleBatches(
t.Helper()
db := newTestDatabase(t)
// Built without NewPrefixHandler's flush timer, so the test flushes itself.
h := &PrefixHandler{db: db, logger: logger.New()}
h := prefixHandlerWithoutTimer(db)
for _, batch := range batches {
for _, msg := range batch {