Re-vendor the shared files from sneak/prompts at 0653579 (closes #52)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the workflow and REPO_POLICIES.md from sneak/prompts 0653579, with this repository's own entries after the shared content. Lint (golangci-lint v2.14.0) and test are Dockerfile phases that script/lint and script/test build alone with a new CACHEBUST value, keeping Go's and golangci-lint's caches; the image build depends on both. make fmt and make fmt-check also format the markdown with prettier, pinned in yarn.lock. The agent guidance moves to AGENTS.md. The Go code is fixed for what the new configuration finds, and the AS and prefix detail reads now return the rows' error after their loops. Model: opus-5-5
This commit was merged in pull request #85.
This commit is contained in:
@@ -33,8 +33,10 @@ type liveCounts struct {
|
||||
|
||||
// seed sets every count to the value read from the tables at startup. It runs
|
||||
// before any writer, so it needs no coordination with the adjust methods.
|
||||
func (c *liveCounts) seed(asns, prefixesV4, prefixesV6, peerings, peers, routesV4, routesV6 int,
|
||||
distributionV4, distributionV6 []PrefixDistribution) {
|
||||
func (c *liveCounts) seed(
|
||||
asns, prefixesV4, prefixesV6, peerings, peers, routesV4, routesV6 int,
|
||||
distributionV4, distributionV6 []PrefixDistribution,
|
||||
) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -45,9 +47,11 @@ func (c *liveCounts) seed(asns, prefixesV4, prefixesV6, peerings, peers, routesV
|
||||
c.peers = peers
|
||||
c.routesV4 = routesV4
|
||||
c.routesV6 = routesV6
|
||||
|
||||
for _, entry := range distributionV4 {
|
||||
addAtMaskLength(c.distributionV4[:], entry.MaskLength, entry.Count)
|
||||
}
|
||||
|
||||
for _, entry := range distributionV6 {
|
||||
addAtMaskLength(c.distributionV6[:], entry.MaskLength, entry.Count)
|
||||
}
|
||||
@@ -102,6 +106,7 @@ func (c *liveCounts) addToDistribution(maskLengthsV4, maskLengthsV6 []int, n int
|
||||
for _, maskLength := range maskLengthsV4 {
|
||||
addAtMaskLength(c.distributionV4[:], maskLength, n)
|
||||
}
|
||||
|
||||
for _, maskLength := range maskLengthsV6 {
|
||||
addAtMaskLength(c.distributionV6[:], maskLength, n)
|
||||
}
|
||||
@@ -138,9 +143,11 @@ func (c *liveCounts) fill(s *Stats) {
|
||||
// ascending order, the way the distribution query returns them.
|
||||
func distributionList(counts []int) []PrefixDistribution {
|
||||
var list []PrefixDistribution
|
||||
|
||||
for maskLength, count := range counts {
|
||||
if count > 0 {
|
||||
list = append(list, PrefixDistribution{MaskLength: maskLength, Count: count})
|
||||
list = append(list,
|
||||
PrefixDistribution{MaskLength: maskLength, Count: count})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +159,8 @@ func distributionList(counts []int) []PrefixDistribution {
|
||||
func (d *Database) countRows(ctx context.Context, table string) (int, error) {
|
||||
var n int
|
||||
// table is one of a fixed set of literals below, never external input.
|
||||
if err := d.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table).Scan(&n); err != nil {
|
||||
err := d.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table).Scan(&n)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to count %s: %w", table, err)
|
||||
}
|
||||
|
||||
@@ -167,30 +175,37 @@ func (d *Database) seedCounts(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefixesV4, err := d.countRows(ctx, "prefixes_v4")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
prefixesV6, err := d.countRows(ctx, "prefixes_v6")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerings, err := d.countRows(ctx, "peerings")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peers, err := d.countRows(ctx, "bgp_peers")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
routesV4, err := d.countRows(ctx, "live_routes_v4")
|
||||
|
||||
routesV4, err := d.countRows(ctx, tableLiveRoutesV4)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
routesV6, err := d.countRows(ctx, "live_routes_v6")
|
||||
|
||||
routesV6, err := d.countRows(ctx, tableLiveRoutesV6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
distributionV4, distributionV6, err := d.GetPrefixDistributionContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -12,6 +12,12 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// testV4PeerIP is the peer the IPv4 routes in these tests are learned from.
|
||||
const testV4PeerIP = "192.0.2.1"
|
||||
|
||||
// testV6Prefix is the prefix of the IPv6 routes in these tests.
|
||||
const testV6Prefix = "2001:db8::/32"
|
||||
|
||||
// mkV4Route builds an IPv4 live route with its mask length taken from the
|
||||
// prefix.
|
||||
func mkV4Route(t *testing.T, prefix string, asn int, ts time.Time) *LiveRoute {
|
||||
@@ -28,7 +34,7 @@ func mkV4Route(t *testing.T, prefix string, asn int, ts time.Time) *LiveRoute {
|
||||
MaskLength: maskLength,
|
||||
IPVersion: ipVersionV4,
|
||||
OriginASN: asn,
|
||||
PeerIP: "192.0.2.1",
|
||||
PeerIP: testV4PeerIP,
|
||||
ASPath: []int{asn},
|
||||
NextHop: "192.0.2.254",
|
||||
LastUpdated: ts,
|
||||
@@ -56,6 +62,8 @@ func mkV6Route(prefix string, asn int, ts time.Time) *LiveRoute {
|
||||
// deleted. These counts are what /api/v1/stats reports; before this change the
|
||||
// endpoint recomputed them with a COUNT(*) over each table on every request.
|
||||
func TestLiveCountsTrackWritesInRealtime(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -70,6 +78,7 @@ func TestLiveCountsTrackWritesInRealtime(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext on empty database: %v", err)
|
||||
}
|
||||
|
||||
if empty.ASNs != 0 || empty.Prefixes != 0 || empty.Peerings != 0 ||
|
||||
empty.Peers != 0 || empty.LiveRoutes != 0 {
|
||||
t.Fatalf("empty database counts nonzero: %+v", empty)
|
||||
@@ -77,30 +86,16 @@ func TestLiveCountsTrackWritesInRealtime(t *testing.T) {
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
|
||||
if err := db.GetOrCreateASNBatch(map[int]time.Time{64500: ts, 64501: ts}); err != nil {
|
||||
t.Fatalf("GetOrCreateASNBatch: %v", err)
|
||||
}
|
||||
if err := db.UpdatePrefixesBatch(map[string]time.Time{
|
||||
"198.51.100.0/24": ts,
|
||||
"2001:db8::/32": ts,
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdatePrefixesBatch: %v", err)
|
||||
}
|
||||
if err := db.UpdatePeerBatch(map[string]PeerUpdate{
|
||||
"192.0.2.1": {PeerIP: "192.0.2.1", PeerASN: 64500, MessageType: "UPDATE", Timestamp: ts},
|
||||
}); err != nil {
|
||||
t.Fatalf("UpdatePeerBatch: %v", err)
|
||||
}
|
||||
if err := db.RecordPeering(64500, 64501, ts); err != nil {
|
||||
t.Fatalf("RecordPeering: %v", err)
|
||||
}
|
||||
writeASNsPrefixesAndPeers(t, db, ts)
|
||||
|
||||
routes := []*LiveRoute{
|
||||
mkV4Route(t, "198.51.100.0/24", 64500, ts),
|
||||
mkV4Route(t, "203.0.113.0/24", 64501, ts.Add(time.Minute)),
|
||||
mkV6Route("2001:db8::/32", 64502, ts.Add(2*time.Minute)),
|
||||
mkV6Route(testV6Prefix, 64502, ts.Add(2*time.Minute)),
|
||||
}
|
||||
if err := db.UpsertLiveRouteBatch(routes); err != nil {
|
||||
|
||||
err = db.UpsertLiveRouteBatch(routes)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
@@ -108,44 +103,87 @@ func TestLiveCountsTrackWritesInRealtime(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext: %v", err)
|
||||
}
|
||||
|
||||
assertCounts(t, "after inserts", stats, wantCounts{
|
||||
asns: 2, prefixes: 2, peerings: 1, peers: 1,
|
||||
ipv4Routes: 2, ipv6Routes: 1, liveRoutes: 3,
|
||||
})
|
||||
|
||||
// Re-announcing the same routes is an update, not an insert: counts hold.
|
||||
if err := db.UpsertLiveRouteBatch(routes); err != nil {
|
||||
err = db.UpsertLiveRouteBatch(routes)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch (re-announce): %v", err)
|
||||
}
|
||||
|
||||
stats, err = db.GetStatsContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext: %v", err)
|
||||
}
|
||||
|
||||
assertCounts(t, "after re-announce", stats, wantCounts{
|
||||
asns: 2, prefixes: 2, peerings: 1, peers: 1,
|
||||
ipv4Routes: 2, ipv6Routes: 1, liveRoutes: 3,
|
||||
})
|
||||
|
||||
// A withdrawal removes one route.
|
||||
if err := db.DeleteLiveRouteBatch([]LiveRouteDeletion{
|
||||
{Prefix: "203.0.113.0/24", PeerIP: "192.0.2.1", IPVersion: ipVersionV4},
|
||||
}); err != nil {
|
||||
err = db.DeleteLiveRouteBatch([]LiveRouteDeletion{
|
||||
{Prefix: "203.0.113.0/24", PeerIP: testV4PeerIP, IPVersion: ipVersionV4},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
stats, err = db.GetStatsContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext: %v", err)
|
||||
}
|
||||
|
||||
assertCounts(t, "after delete", stats, wantCounts{
|
||||
asns: 2, prefixes: 2, peerings: 1, peers: 1,
|
||||
ipv4Routes: 1, ipv6Routes: 1, liveRoutes: 2,
|
||||
})
|
||||
}
|
||||
|
||||
// writeASNsPrefixesAndPeers writes two ASNs, two prefixes, one peer and one
|
||||
// peering: the rows besides live routes that the stats count.
|
||||
func writeASNsPrefixesAndPeers(t *testing.T, db *Database, ts time.Time) {
|
||||
t.Helper()
|
||||
|
||||
err := db.GetOrCreateASNBatch(map[int]time.Time{64500: ts, 64501: ts})
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreateASNBatch: %v", err)
|
||||
}
|
||||
|
||||
err = db.UpdatePrefixesBatch(map[string]time.Time{
|
||||
"198.51.100.0/24": ts,
|
||||
testV6Prefix: ts,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePrefixesBatch: %v", err)
|
||||
}
|
||||
|
||||
err = db.UpdatePeerBatch(map[string]PeerUpdate{
|
||||
testV4PeerIP: {
|
||||
PeerIP: testV4PeerIP, PeerASN: 64500,
|
||||
MessageType: "UPDATE", Timestamp: ts,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePeerBatch: %v", err)
|
||||
}
|
||||
|
||||
err = db.RecordPeering(64500, 64501, ts)
|
||||
if err != nil {
|
||||
t.Fatalf("RecordPeering: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLiveCountsSeededFromDatabaseAtStartup writes rows, reopens the same
|
||||
// database file, and checks the counts and the prefix distribution come back
|
||||
// from the seed scan rather than starting at zero.
|
||||
func TestLiveCountsSeededFromDatabaseAtStartup(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -154,16 +192,22 @@ func TestLiveCountsSeededFromDatabaseAtStartup(t *testing.T) {
|
||||
}
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
if err := db.GetOrCreateASNBatch(map[int]time.Time{64500: ts, 64501: ts, 64502: ts}); err != nil {
|
||||
|
||||
err = db.GetOrCreateASNBatch(map[int]time.Time{64500: ts, 64501: ts, 64502: ts})
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreateASNBatch: %v", err)
|
||||
}
|
||||
if err := db.UpsertLiveRouteBatch([]*LiveRoute{
|
||||
|
||||
err = db.UpsertLiveRouteBatch([]*LiveRoute{
|
||||
mkV4Route(t, "198.51.100.0/24", 64500, ts),
|
||||
mkV6Route("2001:db8::/32", 64502, ts),
|
||||
}); err != nil {
|
||||
mkV6Route(testV6Prefix, 64502, ts),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Close: %v", err)
|
||||
}
|
||||
|
||||
@@ -177,13 +221,16 @@ func TestLiveCountsSeededFromDatabaseAtStartup(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext after reopen: %v", err)
|
||||
}
|
||||
|
||||
if stats.ASNs != 3 {
|
||||
t.Errorf("seeded ASNs = %d, want 3", stats.ASNs)
|
||||
}
|
||||
|
||||
if stats.IPv4Routes != 1 || stats.IPv6Routes != 1 || stats.LiveRoutes != 2 {
|
||||
t.Errorf("seeded routes = (v4 %d, v6 %d, total %d), want (1, 1, 2)",
|
||||
stats.IPv4Routes, stats.IPv6Routes, stats.LiveRoutes)
|
||||
}
|
||||
|
||||
assertDistribution(t, "seeded IPv4 distribution", stats.IPv4PrefixDistribution,
|
||||
[]PrefixDistribution{{MaskLength: 24, Count: 1}})
|
||||
assertDistribution(t, "seeded IPv6 distribution", stats.IPv6PrefixDistribution,
|
||||
@@ -196,26 +243,91 @@ func TestLiveCountsSeededFromDatabaseAtStartup(t *testing.T) {
|
||||
// tables. The steps run once through the batch methods the prefix handler uses
|
||||
// and once through the single-route methods.
|
||||
func TestPrefixDistributionTracksWrites(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
steps := prefixDistributionSteps(t)
|
||||
|
||||
for _, batch := range []bool{true, false} {
|
||||
name := "single-route writes"
|
||||
if batch {
|
||||
name = "batch writes"
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create database: %v", err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for _, step := range steps {
|
||||
err := announceRoutes(db, batch, step.announce)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: announce: %v", step.name, err)
|
||||
}
|
||||
|
||||
err = withdrawRoutes(db, batch, step.withdraw)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: withdraw: %v", step.name, err)
|
||||
}
|
||||
|
||||
stats, err := db.GetStatsContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: GetStatsContext: %v", step.name, err)
|
||||
}
|
||||
|
||||
queryV4, queryV6, err := db.GetPrefixDistributionContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: GetPrefixDistributionContext: %v", step.name, err)
|
||||
}
|
||||
|
||||
assertDistribution(t, step.name+": IPv4 distribution",
|
||||
stats.IPv4PrefixDistribution, step.wantV4)
|
||||
assertDistribution(t, step.name+": IPv6 distribution",
|
||||
stats.IPv6PrefixDistribution, step.wantV6)
|
||||
assertDistribution(t, step.name+": IPv4 distribution query",
|
||||
queryV4, step.wantV4)
|
||||
assertDistribution(t, step.name+": IPv6 distribution query",
|
||||
queryV6, step.wantV6)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// prefixDistributionStep is one step of TestPrefixDistributionTracksWrites:
|
||||
// the routes it announces, then the routes it withdraws, then the
|
||||
// distributions both the stats and the distribution query must report.
|
||||
type prefixDistributionStep struct {
|
||||
name string
|
||||
announce []*LiveRoute
|
||||
withdraw []*LiveRoute
|
||||
wantV4 []PrefixDistribution
|
||||
wantV6 []PrefixDistribution
|
||||
}
|
||||
|
||||
// prefixDistributionSteps returns the steps of
|
||||
// TestPrefixDistributionTracksWrites, in the order they run.
|
||||
func prefixDistributionSteps(t *testing.T) []prefixDistributionStep {
|
||||
t.Helper()
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
shared := mkV4Route(t, "198.51.100.0/24", 64500, ts)
|
||||
sharedSecondPeer := mkV4Route(t, "198.51.100.0/24", 64500, ts)
|
||||
sharedSecondPeer.PeerIP = "192.0.2.2"
|
||||
other := mkV4Route(t, "203.0.113.0/24", 64501, ts)
|
||||
wide := mkV4Route(t, "172.16.0.0/16", 64502, ts)
|
||||
v6 := mkV6Route("2001:db8::/32", 64503, ts)
|
||||
v6SecondPeer := mkV6Route("2001:db8::/32", 64503, ts)
|
||||
v6 := mkV6Route(testV6Prefix, 64503, ts)
|
||||
v6SecondPeer := mkV6Route(testV6Prefix, 64503, ts)
|
||||
v6SecondPeer.PeerIP = "2001:db8::2"
|
||||
|
||||
all := []PrefixDistribution{{MaskLength: 16, Count: 1}, {MaskLength: 24, Count: 2}}
|
||||
v6Only := []PrefixDistribution{{MaskLength: 32, Count: 1}}
|
||||
|
||||
steps := []struct {
|
||||
name string
|
||||
announce []*LiveRoute
|
||||
withdraw []*LiveRoute
|
||||
wantV4 []PrefixDistribution
|
||||
wantV6 []PrefixDistribution
|
||||
}{
|
||||
return []prefixDistributionStep{
|
||||
{
|
||||
name: "new routes",
|
||||
announce: []*LiveRoute{shared, other, wide, v6},
|
||||
@@ -239,7 +351,9 @@ func TestPrefixDistributionTracksWrites(t *testing.T) {
|
||||
{
|
||||
name: "withdrawal of the last route for a prefix",
|
||||
withdraw: []*LiveRoute{sharedSecondPeer, v6SecondPeer},
|
||||
wantV4: []PrefixDistribution{{MaskLength: 16, Count: 1}, {MaskLength: 24, Count: 1}},
|
||||
wantV4: []PrefixDistribution{
|
||||
{MaskLength: 16, Count: 1}, {MaskLength: 24, Count: 1},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "withdrawal of every remaining route",
|
||||
@@ -270,45 +384,6 @@ func TestPrefixDistributionTracksWrites(t *testing.T) {
|
||||
wantV6: v6Only,
|
||||
},
|
||||
}
|
||||
|
||||
for _, batch := range []bool{true, false} {
|
||||
name := "single-route writes"
|
||||
if batch {
|
||||
name = "batch writes"
|
||||
}
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
db, err := New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create database: %v", err)
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
for _, step := range steps {
|
||||
if err := announceRoutes(db, batch, step.announce); err != nil {
|
||||
t.Fatalf("%s: announce: %v", step.name, err)
|
||||
}
|
||||
if err := withdrawRoutes(db, batch, step.withdraw); err != nil {
|
||||
t.Fatalf("%s: withdraw: %v", step.name, err)
|
||||
}
|
||||
|
||||
stats, err := db.GetStatsContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: GetStatsContext: %v", step.name, err)
|
||||
}
|
||||
queryV4, queryV6, err := db.GetPrefixDistributionContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: GetPrefixDistributionContext: %v", step.name, err)
|
||||
}
|
||||
|
||||
assertDistribution(t, step.name+": IPv4 distribution", stats.IPv4PrefixDistribution, step.wantV4)
|
||||
assertDistribution(t, step.name+": IPv6 distribution", stats.IPv6PrefixDistribution, step.wantV6)
|
||||
assertDistribution(t, step.name+": IPv4 distribution query", queryV4, step.wantV4)
|
||||
assertDistribution(t, step.name+": IPv6 distribution query", queryV6, step.wantV6)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// announceRoutes writes routes in one UpsertLiveRouteBatch, or with one
|
||||
@@ -317,8 +392,10 @@ func announceRoutes(db *Database, batch bool, routes []*LiveRoute) error {
|
||||
if batch {
|
||||
return db.UpsertLiveRouteBatch(routes)
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
if err := db.UpsertLiveRoute(route); err != nil {
|
||||
err := db.UpsertLiveRoute(route)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -331,7 +408,8 @@ func announceRoutes(db *Database, batch bool, routes []*LiveRoute) error {
|
||||
func withdrawRoutes(db *Database, batch bool, routes []*LiveRoute) error {
|
||||
if !batch {
|
||||
for _, route := range routes {
|
||||
if err := db.DeleteLiveRoute(route.Prefix, route.PeerIP); err != nil {
|
||||
err := db.DeleteLiveRoute(route.Prefix, route.PeerIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -354,6 +432,8 @@ func withdrawRoutes(db *Database, batch bool, routes []*LiveRoute) error {
|
||||
// routes stay, and the route counts and prefix distribution held in memory
|
||||
// equal what is read from the route tables.
|
||||
func TestDeleteLiveRoutesOfPeer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
peerIP = "192.0.2.1"
|
||||
otherPeerIP = "192.0.2.2"
|
||||
@@ -375,14 +455,16 @@ func TestDeleteLiveRoutesOfPeer(t *testing.T) {
|
||||
}
|
||||
onlyV6 := mkV6Route("2001:db8:1::/48", 64503, ts)
|
||||
onlyV6.MaskLength = 48
|
||||
if err := db.UpsertLiveRouteBatch([]*LiveRoute{
|
||||
|
||||
err = db.UpsertLiveRouteBatch([]*LiveRoute{
|
||||
ofPeer(peerIP, mkV4Route(t, sharedV4, 64500, ts)),
|
||||
ofPeer(otherPeerIP, mkV4Route(t, sharedV4, 64500, ts)),
|
||||
ofPeer(peerIP, mkV4Route(t, "203.0.113.0/24", 64501, ts)),
|
||||
ofPeer(peerIP, mkV6Route(sharedV6, 64502, ts)),
|
||||
ofPeer(otherPeerIP, mkV6Route(sharedV6, 64502, ts)),
|
||||
ofPeer(peerIP, onlyV6),
|
||||
}); err != nil {
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
@@ -392,40 +474,61 @@ func TestDeleteLiveRoutesOfPeer(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteLiveRoutesOfPeer: %v", err)
|
||||
}
|
||||
|
||||
if deleted != want {
|
||||
t.Errorf("DeleteLiveRoutesOfPeer deleted %d routes, want %d", deleted, want)
|
||||
t.Errorf("DeleteLiveRoutesOfPeer deleted %d routes, want %d",
|
||||
deleted, want)
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, prefix := range []string{sharedV4, sharedV6} {
|
||||
routes, _, err := db.GetPrefixDetailsContext(ctx, prefix)
|
||||
routes, _, err := db.GetPrefixDetailsContext(context.Background(), prefix)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPrefixDetailsContext(%s): %v", prefix, err)
|
||||
}
|
||||
|
||||
if len(routes) != 1 || routes[0].PeerIP != otherPeerIP {
|
||||
t.Errorf("live routes for %s = %+v, want only the one from %s", prefix, routes, otherPeerIP)
|
||||
t.Errorf("live routes for %s = %+v, want only the one from %s",
|
||||
prefix, routes, otherPeerIP)
|
||||
}
|
||||
}
|
||||
|
||||
checkOneLiveRoutePerIPVersion(t, db)
|
||||
}
|
||||
|
||||
// checkOneLiveRoutePerIPVersion checks that the database has one IPv4 live
|
||||
// route, for a /24, and one IPv6 live route, for a /32, both as counted in
|
||||
// memory and as read from the route tables.
|
||||
func checkOneLiveRoutePerIPVersion(t *testing.T, db *Database) {
|
||||
t.Helper()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
stats, err := db.GetStatsContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext: %v", err)
|
||||
}
|
||||
|
||||
tableV4, tableV6, err := db.GetLiveRouteCountsContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetLiveRouteCountsContext: %v", err)
|
||||
}
|
||||
if stats.IPv4Routes != 1 || tableV4 != 1 || stats.IPv6Routes != 1 || tableV6 != 1 {
|
||||
t.Errorf("live routes (IPv4, IPv6): (%d, %d) in memory, (%d, %d) in the tables, want (1, 1)",
|
||||
|
||||
if stats.IPv4Routes != 1 || tableV4 != 1 || stats.IPv6Routes != 1 ||
|
||||
tableV6 != 1 {
|
||||
t.Errorf("live routes (IPv4, IPv6): (%d, %d) in memory, (%d, %d) in "+
|
||||
"the tables, want (1, 1)",
|
||||
stats.IPv4Routes, stats.IPv6Routes, tableV4, tableV6)
|
||||
}
|
||||
|
||||
queryV4, queryV6, err := db.GetPrefixDistributionContext(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPrefixDistributionContext: %v", err)
|
||||
}
|
||||
|
||||
wantV4 := []PrefixDistribution{{MaskLength: 24, Count: 1}}
|
||||
wantV6 := []PrefixDistribution{{MaskLength: 32, Count: 1}}
|
||||
|
||||
assertDistribution(t, "IPv4 distribution", stats.IPv4PrefixDistribution, wantV4)
|
||||
assertDistribution(t, "IPv6 distribution", stats.IPv6PrefixDistribution, wantV6)
|
||||
assertDistribution(t, "IPv4 distribution query", queryV4, wantV4)
|
||||
@@ -437,6 +540,8 @@ func TestDeleteLiveRoutesOfPeer(t *testing.T) {
|
||||
// MIN/MAX union query read its result into *time.Time, which the driver could
|
||||
// not parse, so it logged a warning every call and left both timestamps nil.
|
||||
func TestStatsRouteTimestamps(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -451,6 +556,7 @@ func TestStatsRouteTimestamps(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext on empty database: %v", err)
|
||||
}
|
||||
|
||||
if empty.OldestRoute != nil || empty.NewestRoute != nil {
|
||||
t.Fatalf("empty database timestamps = (%v, %v), want (nil, nil)",
|
||||
empty.OldestRoute, empty.NewestRoute)
|
||||
@@ -459,11 +565,13 @@ func TestStatsRouteTimestamps(t *testing.T) {
|
||||
base := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
oldest := base
|
||||
newest := base.Add(2 * time.Minute)
|
||||
if err := db.UpsertLiveRouteBatch([]*LiveRoute{
|
||||
|
||||
err = db.UpsertLiveRouteBatch([]*LiveRoute{
|
||||
mkV4Route(t, "198.51.100.0/24", 64500, base.Add(time.Minute)),
|
||||
mkV4Route(t, "203.0.113.0/24", 64501, oldest),
|
||||
mkV6Route("2001:db8::/32", 64502, newest),
|
||||
}); err != nil {
|
||||
mkV6Route(testV6Prefix, 64502, newest),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
@@ -471,9 +579,11 @@ func TestStatsRouteTimestamps(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext: %v", err)
|
||||
}
|
||||
|
||||
if stats.OldestRoute == nil || !stats.OldestRoute.Equal(oldest) {
|
||||
t.Errorf("OldestRoute = %v, want %v", stats.OldestRoute, oldest)
|
||||
}
|
||||
|
||||
if stats.NewestRoute == nil || !stats.NewestRoute.Equal(newest) {
|
||||
t.Errorf("NewestRoute = %v, want %v", stats.NewestRoute, newest)
|
||||
}
|
||||
@@ -482,6 +592,8 @@ func TestStatsRouteTimestamps(t *testing.T) {
|
||||
// TestLiveCountsConcurrentReadWrite runs writers and stats readers at once so
|
||||
// the race detector proves the counters are safe under concurrent use.
|
||||
func TestLiveCountsConcurrentReadWrite(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -493,17 +605,21 @@ func TestLiveCountsConcurrentReadWrite(t *testing.T) {
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
|
||||
const writers = 4
|
||||
var wg sync.WaitGroup
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(writers)
|
||||
|
||||
for w := range writers {
|
||||
go func(base int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i := range 25 {
|
||||
asn := 65000 + base*100 + i
|
||||
route := mkV6Route("2001:db8::/32", asn, ts)
|
||||
route := mkV6Route(testV6Prefix, asn, ts)
|
||||
route.PeerIP = "2001:db8::" + uuid.NewString()
|
||||
if err := db.UpsertLiveRoute(route); err != nil {
|
||||
|
||||
err := db.UpsertLiveRoute(route)
|
||||
if err != nil {
|
||||
t.Errorf("UpsertLiveRoute: %v", err)
|
||||
|
||||
return
|
||||
@@ -514,15 +630,19 @@ func TestLiveCountsConcurrentReadWrite(t *testing.T) {
|
||||
|
||||
var readerWG sync.WaitGroup
|
||||
readerWG.Add(1)
|
||||
|
||||
stop := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
defer readerWG.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
if _, err := db.GetStatsContext(context.Background()); err != nil {
|
||||
_, err := db.GetStatsContext(context.Background())
|
||||
if err != nil {
|
||||
t.Errorf("GetStatsContext: %v", err)
|
||||
|
||||
return
|
||||
@@ -539,6 +659,7 @@ func TestLiveCountsConcurrentReadWrite(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetStatsContext: %v", err)
|
||||
}
|
||||
|
||||
if want := writers * 25; stats.IPv6Routes != want {
|
||||
t.Errorf("IPv6Routes = %d, want %d", stats.IPv6Routes, want)
|
||||
}
|
||||
@@ -563,21 +684,27 @@ func assertCounts(t *testing.T, when string, got Stats, want wantCounts) {
|
||||
if got.ASNs != want.asns {
|
||||
t.Errorf("%s: ASNs = %d, want %d", when, got.ASNs, want.asns)
|
||||
}
|
||||
|
||||
if got.Prefixes != want.prefixes {
|
||||
t.Errorf("%s: Prefixes = %d, want %d", when, got.Prefixes, want.prefixes)
|
||||
}
|
||||
|
||||
if got.Peerings != want.peerings {
|
||||
t.Errorf("%s: Peerings = %d, want %d", when, got.Peerings, want.peerings)
|
||||
}
|
||||
|
||||
if got.Peers != want.peers {
|
||||
t.Errorf("%s: Peers = %d, want %d", when, got.Peers, want.peers)
|
||||
}
|
||||
|
||||
if got.IPv4Routes != want.ipv4Routes {
|
||||
t.Errorf("%s: IPv4Routes = %d, want %d", when, got.IPv4Routes, want.ipv4Routes)
|
||||
}
|
||||
|
||||
if got.IPv6Routes != want.ipv6Routes {
|
||||
t.Errorf("%s: IPv6Routes = %d, want %d", when, got.IPv6Routes, want.ipv6Routes)
|
||||
}
|
||||
|
||||
if got.LiveRoutes != want.liveRoutes {
|
||||
t.Errorf("%s: LiveRoutes = %d, want %d", when, got.LiveRoutes, want.liveRoutes)
|
||||
}
|
||||
+1301
-971
File diff suppressed because it is too large
Load Diff
+231
-114
@@ -78,6 +78,8 @@ const (
|
||||
// address, ErrNoRoute when none covers it, and the next less specific prefix
|
||||
// once the only route of the most specific one is withdrawn.
|
||||
func TestGetIPInfoFindsMostSpecificLiveRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -95,24 +97,7 @@ func TestGetIPInfoFindsMostSpecificLiveRoute(t *testing.T) {
|
||||
"2001:db8:1::/48": 64501,
|
||||
"2001:db8:1:2::/64": 64502,
|
||||
}
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
routes := make([]*LiveRoute, 0, len(origins))
|
||||
for prefix, asn := range origins {
|
||||
routes = append(routes, &LiveRoute{
|
||||
ID: uuid.New(),
|
||||
Prefix: prefix,
|
||||
MaskLength: netip.MustParsePrefix(prefix).Bits(),
|
||||
IPVersion: detectIPVersion(prefix),
|
||||
OriginASN: asn,
|
||||
PeerIP: testPeerIP,
|
||||
ASPath: []int{asn},
|
||||
NextHop: testPeerIP,
|
||||
LastUpdated: ts,
|
||||
})
|
||||
}
|
||||
if err := db.UpsertLiveRouteBatch(routes); err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
writeOriginRoutes(t, db, origins)
|
||||
|
||||
// lookup checks that ip resolves to the live prefix want, or to ErrNoRoute
|
||||
// when want is empty.
|
||||
@@ -127,11 +112,13 @@ func TestGetIPInfoFindsMostSpecificLiveRoute(t *testing.T) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("GetIPInfo(%s): %v", ip, err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if info.Netblock != want || info.MaskLength != netip.MustParsePrefix(want).Bits() ||
|
||||
info.ASN != origins[want] {
|
||||
t.Errorf("GetIPInfo(%s) = %s (mask %d) AS%d, want %s AS%d",
|
||||
@@ -166,6 +153,8 @@ func TestGetIPInfoFindsMostSpecificLiveRoute(t *testing.T) {
|
||||
// route for an address, and that a route of exactly /8 or /16, or longer, still
|
||||
// is. The prefixes are written out so that a wrong limit fails the test.
|
||||
func TestGetIPInfoIgnoresShortRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -174,24 +163,11 @@ func TestGetIPInfoIgnoresShortRoutes(t *testing.T) {
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
// announce writes one live route for prefix, originated by asn.
|
||||
announce := func(prefix string, asn int) {
|
||||
t.Helper()
|
||||
|
||||
err := db.UpsertLiveRouteBatch([]*LiveRoute{{
|
||||
ID: uuid.New(),
|
||||
Prefix: prefix,
|
||||
MaskLength: netip.MustParsePrefix(prefix).Bits(),
|
||||
IPVersion: detectIPVersion(prefix),
|
||||
OriginASN: asn,
|
||||
PeerIP: testPeerIP,
|
||||
ASPath: []int{asn},
|
||||
NextHop: testPeerIP,
|
||||
LastUpdated: ts,
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch(%s): %v", prefix, err)
|
||||
}
|
||||
writeOriginRoutes(t, db, map[string]int{prefix: asn})
|
||||
}
|
||||
|
||||
// lookup checks that ip resolves to the live prefix netblock from asn.
|
||||
@@ -204,8 +180,10 @@ func TestGetIPInfoIgnoresShortRoutes(t *testing.T) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if info.Netblock != netblock || info.ASN != asn {
|
||||
t.Errorf("GetIPInfo(%s) = %s AS%d, want %s AS%d", ip, info.Netblock, info.ASN, netblock, asn)
|
||||
t.Errorf("GetIPInfo(%s) = %s AS%d, want %s AS%d",
|
||||
ip, info.Netblock, info.ASN, netblock, asn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,8 +194,10 @@ func TestGetIPInfoIgnoresShortRoutes(t *testing.T) {
|
||||
announce("192.0.0.0/7", 64500)
|
||||
|
||||
for _, ip := range []string{"2001:db8::1", "192.0.2.1"} {
|
||||
if info, err := db.GetIPInfo(ip); !errors.Is(err, ErrNoRoute) {
|
||||
t.Errorf("GetIPInfo(%s) with only short routes = %+v, %v; want ErrNoRoute", ip, info, err)
|
||||
info, err := db.GetIPInfo(ip)
|
||||
if !errors.Is(err, ErrNoRoute) {
|
||||
t.Errorf("GetIPInfo(%s) with only short routes = %+v, %v; "+
|
||||
"want ErrNoRoute", ip, info, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +218,8 @@ func TestGetIPInfoIgnoresShortRoutes(t *testing.T) {
|
||||
// origin AS. An upsert updates the peer's route before it would insert one, so
|
||||
// only an insert straight into the table shows what the table allows.
|
||||
func TestLiveRouteTablesHoldOneRoutePerPrefixAndPeer(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
db, err := New(&config.Config{StateDir: t.TempDir()}, logger.New())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create database: %v", err)
|
||||
@@ -245,33 +227,76 @@ func TestLiveRouteTablesHoldOneRoutePerPrefixAndPeer(t *testing.T) {
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
const otherOriginASN = 64501
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
|
||||
for _, tt := range []struct {
|
||||
route *LiveRoute
|
||||
insertSQL string
|
||||
}{
|
||||
{route: mkV4Route(t, "198.51.100.0/24", 64500, ts), insertSQL: insertLiveRouteV4SQL},
|
||||
{route: mkV6Route("2001:db8::/32", 64500, ts), insertSQL: insertLiveRouteV6SQL},
|
||||
{
|
||||
route: mkV4Route(t, "198.51.100.0/24", 64500, ts),
|
||||
insertSQL: insertLiveRouteV4SQL,
|
||||
},
|
||||
{
|
||||
route: mkV6Route(testV6Prefix, 64500, ts),
|
||||
insertSQL: insertLiveRouteV6SQL,
|
||||
},
|
||||
} {
|
||||
if err := db.UpsertLiveRoute(tt.route); err != nil {
|
||||
err := db.UpsertLiveRoute(tt.route)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRoute(%s): %v", tt.route.Prefix, err)
|
||||
}
|
||||
|
||||
_, err := db.db.Exec(tt.insertSQL, uuid.NewString(), tt.route.Prefix, tt.route.MaskLength,
|
||||
otherOriginASN, tt.route.PeerIP, "[]", tt.route.NextHop, ts)
|
||||
_, err = db.db.ExecContext(t.Context(), tt.insertSQL, uuid.NewString(),
|
||||
tt.route.Prefix, tt.route.MaskLength, otherOriginASN,
|
||||
tt.route.PeerIP, "[]", tt.route.NextHop, ts)
|
||||
|
||||
var sqliteErr sqlite3.Error
|
||||
if !errors.As(err, &sqliteErr) || sqliteErr.ExtendedCode != sqlite3.ErrConstraintUnique {
|
||||
t.Errorf("second route from peer %s for %s: error %v, want a UNIQUE constraint error",
|
||||
if !errors.As(err, &sqliteErr) ||
|
||||
sqliteErr.ExtendedCode != sqlite3.ErrConstraintUnique {
|
||||
t.Errorf("second route from peer %s for %s: error %v, "+
|
||||
"want a UNIQUE constraint error",
|
||||
tt.route.PeerIP, tt.route.Prefix, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeOriginRoutes writes one live route from testPeerIP for each prefix in
|
||||
// origins, originated by the AS it maps to.
|
||||
func writeOriginRoutes(t *testing.T, db *Database, origins map[string]int) {
|
||||
t.Helper()
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
routes := make([]*LiveRoute, 0, len(origins))
|
||||
|
||||
for prefix, asn := range origins {
|
||||
routes = append(routes, &LiveRoute{
|
||||
ID: uuid.New(),
|
||||
Prefix: prefix,
|
||||
MaskLength: netip.MustParsePrefix(prefix).Bits(),
|
||||
IPVersion: detectIPVersion(prefix),
|
||||
OriginASN: asn,
|
||||
PeerIP: testPeerIP,
|
||||
ASPath: []int{asn},
|
||||
NextHop: testPeerIP,
|
||||
LastUpdated: ts,
|
||||
})
|
||||
}
|
||||
|
||||
err := db.UpsertLiveRouteBatch(routes)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConnectionPoolPragmas holds several pooled connections open at once and
|
||||
// checks each one carries the per-connection settings from the DSN, plus the
|
||||
// process-wide hard heap limit, and sees the new file with auto_vacuum
|
||||
// incremental.
|
||||
func TestConnectionPoolPragmas(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -291,57 +316,64 @@ func TestConnectionPoolPragmas(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 0; i < heldConnections; i++ {
|
||||
for i := range heldConnections {
|
||||
c, err := db.db.Conn(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to open connection %d: %v", i, err)
|
||||
}
|
||||
|
||||
conns = append(conns, c)
|
||||
}
|
||||
|
||||
for i, c := range conns {
|
||||
var cacheSize int
|
||||
if err := c.QueryRowContext(ctx, "PRAGMA cache_size").Scan(&cacheSize); err != nil {
|
||||
t.Fatalf("conn %d: failed to read cache_size: %v", i, err)
|
||||
}
|
||||
cacheSize := readPragma(ctx, t, c, i, "cache_size")
|
||||
if cacheSize != sqliteCacheSizeKiB {
|
||||
t.Errorf("conn %d: cache_size = %d, want %d", i, cacheSize, sqliteCacheSizeKiB)
|
||||
t.Errorf("conn %d: cache_size = %d, want %d",
|
||||
i, cacheSize, sqliteCacheSizeKiB)
|
||||
}
|
||||
|
||||
var busyTimeout int
|
||||
if err := c.QueryRowContext(ctx, "PRAGMA busy_timeout").Scan(&busyTimeout); err != nil {
|
||||
t.Fatalf("conn %d: failed to read busy_timeout: %v", i, err)
|
||||
}
|
||||
busyTimeout := readPragma(ctx, t, c, i, "busy_timeout")
|
||||
if busyTimeout != sqliteBusyTimeoutMs {
|
||||
t.Errorf("conn %d: busy_timeout = %d, want %d", i, busyTimeout, sqliteBusyTimeoutMs)
|
||||
t.Errorf("conn %d: busy_timeout = %d, want %d",
|
||||
i, busyTimeout, sqliteBusyTimeoutMs)
|
||||
}
|
||||
|
||||
var tempStore int
|
||||
if err := c.QueryRowContext(ctx, "PRAGMA temp_store").Scan(&tempStore); err != nil {
|
||||
t.Fatalf("conn %d: failed to read temp_store: %v", i, err)
|
||||
}
|
||||
tempStore := readPragma(ctx, t, c, i, "temp_store")
|
||||
if tempStore == tempStoreMemory {
|
||||
t.Errorf("conn %d: temp_store = %d, want anything but %d (MEMORY)", i, tempStore, tempStoreMemory)
|
||||
t.Errorf("conn %d: temp_store = %d, want anything but %d (MEMORY)",
|
||||
i, tempStore, tempStoreMemory)
|
||||
}
|
||||
|
||||
var hardHeapLimit int64
|
||||
if err := c.QueryRowContext(ctx, "PRAGMA hard_heap_limit").Scan(&hardHeapLimit); err != nil {
|
||||
t.Fatalf("conn %d: failed to read hard_heap_limit: %v", i, err)
|
||||
}
|
||||
hardHeapLimit := readPragma(ctx, t, c, i, "hard_heap_limit")
|
||||
if hardHeapLimit != sqliteHardHeapLimitBytes {
|
||||
t.Errorf("conn %d: hard_heap_limit = %d, want %d", i, hardHeapLimit, sqliteHardHeapLimitBytes)
|
||||
t.Errorf("conn %d: hard_heap_limit = %d, want %d",
|
||||
i, hardHeapLimit, sqliteHardHeapLimitBytes)
|
||||
}
|
||||
|
||||
var autoVacuum int
|
||||
if err := c.QueryRowContext(ctx, "PRAGMA auto_vacuum").Scan(&autoVacuum); err != nil {
|
||||
t.Fatalf("conn %d: failed to read auto_vacuum: %v", i, err)
|
||||
}
|
||||
autoVacuum := readPragma(ctx, t, c, i, "auto_vacuum")
|
||||
if autoVacuum != autoVacuumIncremental {
|
||||
t.Errorf("conn %d: auto_vacuum = %d, want %d (incremental)", i, autoVacuum, autoVacuumIncremental)
|
||||
t.Errorf("conn %d: auto_vacuum = %d, want %d (incremental)",
|
||||
i, autoVacuum, autoVacuumIncremental)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// readPragma reads the integer value of PRAGMA name on connection i, c.
|
||||
func readPragma(
|
||||
ctx context.Context, t *testing.T, c *sql.Conn, i int, name string,
|
||||
) int64 {
|
||||
t.Helper()
|
||||
|
||||
var value int64
|
||||
|
||||
err := c.QueryRowContext(ctx, "PRAGMA "+name).Scan(&value)
|
||||
if err != nil {
|
||||
t.Fatalf("conn %d: failed to read %s: %v", i, name, err)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// TestVacuumReturnsFreePages checks that after routes are deleted from a new
|
||||
// database, one Vacuum call returns every page they used. The deletes leave
|
||||
// fewer free pages than the 1000 Vacuum frees per call, so none may remain.
|
||||
@@ -349,6 +381,8 @@ func TestConnectionPoolPragmas(t *testing.T) {
|
||||
// the free pages stayed in the file, and with the PRAGMA run by ExecContext
|
||||
// Vacuum freed only one page per call.
|
||||
func TestVacuumReturnsFreePages(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -359,10 +393,12 @@ func TestVacuumReturnsFreePages(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
ts := time.Now().UTC()
|
||||
|
||||
const asn = 64500
|
||||
|
||||
routes := make([]*LiveRoute, 0, vacuumTestRoutes)
|
||||
deletions := make([]LiveRouteDeletion, 0, vacuumTestRoutes)
|
||||
|
||||
for i := range vacuumTestRoutes {
|
||||
route := mkV4Route(t, fmt.Sprintf("10.%d.%d.0/24", i/256, i%256), asn, ts)
|
||||
routes = append(routes, route)
|
||||
@@ -373,29 +409,39 @@ func TestVacuumReturnsFreePages(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
if err := db.UpsertLiveRouteBatch(routes); err != nil {
|
||||
err = db.UpsertLiveRouteBatch(routes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write routes: %v", err)
|
||||
}
|
||||
if err := db.DeleteLiveRouteBatch(deletions); err != nil {
|
||||
|
||||
err = db.DeleteLiveRouteBatch(deletions)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to delete routes: %v", err)
|
||||
}
|
||||
|
||||
var before int
|
||||
if err := db.db.QueryRowContext(ctx, "PRAGMA freelist_count").Scan(&before); err != nil {
|
||||
|
||||
err = db.db.QueryRowContext(ctx, "PRAGMA freelist_count").Scan(&before)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read freelist_count: %v", err)
|
||||
}
|
||||
|
||||
if before == 0 {
|
||||
t.Fatalf("no free pages after deleting %d routes", vacuumTestRoutes)
|
||||
}
|
||||
|
||||
if err := db.Vacuum(ctx); err != nil {
|
||||
err = db.Vacuum(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Vacuum failed: %v", err)
|
||||
}
|
||||
|
||||
var after int
|
||||
if err := db.db.QueryRowContext(ctx, "PRAGMA freelist_count").Scan(&after); err != nil {
|
||||
|
||||
err = db.db.QueryRowContext(ctx, "PRAGMA freelist_count").Scan(&after)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read freelist_count: %v", err)
|
||||
}
|
||||
|
||||
if after != 0 {
|
||||
t.Errorf("free pages after Vacuum = %d of %d, want 0", after, before)
|
||||
}
|
||||
@@ -410,6 +456,8 @@ func TestVacuumReturnsFreePages(t *testing.T) {
|
||||
// no batch is dropped. The checkpoint runs without the Database mutex, exactly
|
||||
// as the background maintainer does in production.
|
||||
func TestBatchWriteDuringCheckpoint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -422,8 +470,10 @@ func TestBatchWriteDuringCheckpoint(t *testing.T) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -435,12 +485,14 @@ func TestBatchWriteDuringCheckpoint(t *testing.T) {
|
||||
}()
|
||||
|
||||
ts := time.Now().UTC()
|
||||
for i := 0; i < contentionIterations; i++ {
|
||||
for i := range contentionIterations {
|
||||
asns := map[int]time.Time{
|
||||
i % contendedASNCount: ts,
|
||||
(i % contendedASNCount) + asnSecondBand: ts,
|
||||
}
|
||||
if err := db.GetOrCreateASNBatch(asns); err != nil {
|
||||
|
||||
err := db.GetOrCreateASNBatch(asns)
|
||||
if err != nil {
|
||||
cancel()
|
||||
wg.Wait()
|
||||
t.Fatalf("batch write failed under checkpoint contention: %v", err)
|
||||
@@ -455,6 +507,8 @@ func TestBatchWriteDuringCheckpoint(t *testing.T) {
|
||||
// family learned from two peers in the same second and checks that the
|
||||
// prefix-length sample lists each of them once.
|
||||
func TestGetRandomPrefixesByLengthListsPrefixOnce(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -467,18 +521,24 @@ func TestGetRandomPrefixesByLengthListsPrefixOnce(t *testing.T) {
|
||||
v4First := mkV4Route(t, "198.51.100.0/24", 64500, ts)
|
||||
v4Second := mkV4Route(t, "198.51.100.0/24", 64500, ts)
|
||||
v4Second.PeerIP = secondV4PeerIP
|
||||
v6First := mkV6Route("2001:db8::/32", 64500, ts)
|
||||
v6Second := mkV6Route("2001:db8::/32", 64500, ts)
|
||||
v6First := mkV6Route(testV6Prefix, 64500, ts)
|
||||
v6Second := mkV6Route(testV6Prefix, 64500, ts)
|
||||
v6Second.PeerIP = secondV6PeerIP
|
||||
if err := db.UpsertLiveRouteBatch([]*LiveRoute{v4First, v4Second, v6First, v6Second}); err != nil {
|
||||
|
||||
err = db.UpsertLiveRouteBatch(
|
||||
[]*LiveRoute{v4First, v4Second, v6First, v6Second})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
for _, want := range []*LiveRoute{v4First, v6First} {
|
||||
routes, err := db.GetRandomPrefixesByLength(want.MaskLength, want.IPVersion, sampleLimit)
|
||||
routes, err := db.GetRandomPrefixesByLength(want.MaskLength,
|
||||
want.IPVersion, sampleLimit)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRandomPrefixesByLength(%d, %d): %v", want.MaskLength, want.IPVersion, err)
|
||||
t.Fatalf("GetRandomPrefixesByLength(%d, %d): %v",
|
||||
want.MaskLength, want.IPVersion, err)
|
||||
}
|
||||
|
||||
if len(routes) != 1 || routes[0].Prefix != want.Prefix {
|
||||
t.Errorf("GetRandomPrefixesByLength(%d, %d) = %+v, want %s listed once",
|
||||
want.MaskLength, want.IPVersion, routes, want.Prefix)
|
||||
@@ -490,6 +550,8 @@ func TestGetRandomPrefixesByLengthListsPrefixOnce(t *testing.T) {
|
||||
// two peers at different times, the older route first, and checks that the
|
||||
// prefix-length sample lists the newer route.
|
||||
func TestGetRandomPrefixesByLengthListsNewestRoute(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -502,17 +564,21 @@ func TestGetRandomPrefixesByLengthListsNewestRoute(t *testing.T) {
|
||||
olderRoute := mkV4Route(t, "198.51.100.0/24", 64500, older)
|
||||
newerRoute := mkV4Route(t, "198.51.100.0/24", 64500, older.Add(time.Minute))
|
||||
newerRoute.PeerIP = secondV4PeerIP
|
||||
if err := db.UpsertLiveRouteBatch([]*LiveRoute{olderRoute, newerRoute}); err != nil {
|
||||
|
||||
err = db.UpsertLiveRouteBatch([]*LiveRoute{olderRoute, newerRoute})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
routes, err := db.GetRandomPrefixesByLength(newerRoute.MaskLength, ipVersionV4, sampleLimit)
|
||||
routes, err := db.GetRandomPrefixesByLength(newerRoute.MaskLength,
|
||||
ipVersionV4, sampleLimit)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRandomPrefixesByLength: %v", err)
|
||||
}
|
||||
|
||||
if len(routes) != 1 || routes[0].PeerIP != newerRoute.PeerIP {
|
||||
t.Errorf("GetRandomPrefixesByLength = %+v, want only the newer route, from %s",
|
||||
routes, newerRoute.PeerIP)
|
||||
t.Errorf("GetRandomPrefixesByLength = %+v, want only the newer route, "+
|
||||
"from %s", routes, newerRoute.PeerIP)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,6 +589,8 @@ func TestGetRandomPrefixesByLengthListsNewestRoute(t *testing.T) {
|
||||
// shuffled prefixes, and the random lookups used for a mask length with more
|
||||
// than maxPrefixesToShuffle prefixes.
|
||||
func TestGetRandomPrefixesByLengthSamplesManyPrefixes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -532,8 +600,11 @@ func TestGetRandomPrefixesByLengthSamplesManyPrefixes(t *testing.T) {
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
|
||||
var routes []*LiveRoute
|
||||
var stored []string
|
||||
|
||||
stored := make([]string, 0, sampledPrefixCount)
|
||||
|
||||
for i := range sampledPrefixCount {
|
||||
prefix := fmt.Sprintf("10.0.%d.0/24", i)
|
||||
for _, p := range []string{prefix, fmt.Sprintf("172.%d.0.0/16", i)} {
|
||||
@@ -542,22 +613,29 @@ func TestGetRandomPrefixesByLengthSamplesManyPrefixes(t *testing.T) {
|
||||
second.PeerIP = secondV4PeerIP
|
||||
routes = append(routes, first, second)
|
||||
}
|
||||
|
||||
stored = append(stored, prefix)
|
||||
}
|
||||
if err := db.UpsertLiveRouteBatch(routes); err != nil {
|
||||
|
||||
err = db.UpsertLiveRouteBatch(routes)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
slices.Sort(stored)
|
||||
|
||||
maskLength := routes[0].MaskLength
|
||||
|
||||
sample, err := db.GetRandomPrefixesByLength(maskLength, ipVersionV4, sampleLimit)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRandomPrefixesByLength: %v", err)
|
||||
}
|
||||
var listed []string
|
||||
|
||||
listed := make([]string, 0, len(sample))
|
||||
for _, route := range sample {
|
||||
listed = append(listed, route.Prefix)
|
||||
}
|
||||
|
||||
checkPrefixSample(t, "GetRandomPrefixesByLength", listed, stored)
|
||||
|
||||
looked, err := db.samplePrefixesByLength(
|
||||
@@ -565,6 +643,7 @@ func TestGetRandomPrefixesByLengthSamplesManyPrefixes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("samplePrefixesByLength: %v", err)
|
||||
}
|
||||
|
||||
checkPrefixSample(t, "samplePrefixesByLength", looked, stored)
|
||||
}
|
||||
|
||||
@@ -577,15 +656,20 @@ func checkPrefixSample(t *testing.T, name string, sample, stored []string) {
|
||||
if len(sample) != sampleLimit {
|
||||
t.Errorf("%s listed %d prefixes, want %d", name, len(sample), sampleLimit)
|
||||
}
|
||||
|
||||
listed := make(map[string]bool)
|
||||
|
||||
for _, prefix := range sample {
|
||||
if listed[prefix] || !slices.Contains(stored, prefix) {
|
||||
t.Errorf("%s lists %s twice or it is not one of the /24s", name, prefix)
|
||||
}
|
||||
|
||||
listed[prefix] = true
|
||||
}
|
||||
|
||||
if slices.Equal(slices.Sorted(slices.Values(sample)), stored[:sampleLimit]) {
|
||||
t.Errorf("%s lists just the first %d prefixes in text order", name, sampleLimit)
|
||||
t.Errorf("%s lists just the first %d prefixes in text order",
|
||||
name, sampleLimit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,6 +679,8 @@ func checkPrefixSample(t *testing.T, name string, sample, stored []string) {
|
||||
// order, and that the sample lists the prefixes asked for, each once, some of
|
||||
// them after those first ones, which only the random lookups reach.
|
||||
func TestGetRandomPrefixesByLengthSamplesPastFirstPrefixes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -604,51 +690,64 @@ func TestGetRandomPrefixesByLengthSamplesPastFirstPrefixes(t *testing.T) {
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
var routes []*LiveRoute
|
||||
var stored []string
|
||||
routes := make([]*LiveRoute, 0, largeSamplePrefixCount)
|
||||
stored := make([]string, 0, largeSamplePrefixCount)
|
||||
|
||||
for i := range largeSamplePrefixCount {
|
||||
route := mkV6Route(fmt.Sprintf("2001:%x::/32", i), 64500, ts)
|
||||
routes = append(routes, route)
|
||||
stored = append(stored, route.Prefix)
|
||||
}
|
||||
if err := db.UpsertLiveRouteBatch(routes); err != nil {
|
||||
|
||||
err = db.UpsertLiveRouteBatch(routes)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
slices.Sort(stored)
|
||||
firstPrefixes := stored[:maxPrefixesToShuffle+1]
|
||||
|
||||
maskLength := routes[0].MaskLength
|
||||
read, err := db.firstPrefixesByLength(
|
||||
context.Background(), liveRoutesV6Table, maskLength, maxPrefixesToShuffle+1)
|
||||
|
||||
read, err := db.firstPrefixesByLength(context.Background(),
|
||||
liveRoutesV6Table, maskLength, maxPrefixesToShuffle+1)
|
||||
if err != nil {
|
||||
t.Fatalf("firstPrefixesByLength: %v", err)
|
||||
}
|
||||
|
||||
if !slices.Equal(read, firstPrefixes) {
|
||||
t.Errorf("firstPrefixesByLength read %d prefixes, want the first %d in text order",
|
||||
len(read), len(firstPrefixes))
|
||||
t.Errorf("firstPrefixesByLength read %d prefixes, want the first %d "+
|
||||
"in text order", len(read), len(firstPrefixes))
|
||||
}
|
||||
|
||||
sample, err := db.GetRandomPrefixesByLength(maskLength, ipVersionV6, largeSampleLimit)
|
||||
sample, err := db.GetRandomPrefixesByLength(maskLength, ipVersionV6,
|
||||
largeSampleLimit)
|
||||
if err != nil {
|
||||
t.Fatalf("GetRandomPrefixesByLength: %v", err)
|
||||
}
|
||||
|
||||
if len(sample) != largeSampleLimit {
|
||||
t.Errorf("GetRandomPrefixesByLength listed %d prefixes, want %d", len(sample), largeSampleLimit)
|
||||
t.Errorf("GetRandomPrefixesByLength listed %d prefixes, want %d",
|
||||
len(sample), largeSampleLimit)
|
||||
}
|
||||
|
||||
listed := make(map[string]bool)
|
||||
pastFirstPrefixes := 0
|
||||
|
||||
for _, route := range sample {
|
||||
if listed[route.Prefix] {
|
||||
t.Errorf("GetRandomPrefixesByLength lists %s twice", route.Prefix)
|
||||
}
|
||||
|
||||
listed[route.Prefix] = true
|
||||
|
||||
if !slices.Contains(firstPrefixes, route.Prefix) {
|
||||
pastFirstPrefixes++
|
||||
}
|
||||
}
|
||||
|
||||
if pastFirstPrefixes == 0 {
|
||||
t.Errorf("GetRandomPrefixesByLength lists only prefixes among the first %d in text order",
|
||||
len(firstPrefixes))
|
||||
t.Errorf("GetRandomPrefixesByLength lists only prefixes among the first "+
|
||||
"%d in text order", len(firstPrefixes))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,6 +756,8 @@ func TestGetRandomPrefixesByLengthSamplesPastFirstPrefixes(t *testing.T) {
|
||||
// sampleLimit prefixes: the stored one, then the shuffled ones in order. The
|
||||
// stored prefix is the second shuffled one, so the fill-up must skip it.
|
||||
func TestSamplePrefixesByLengthFillsUpFromShuffled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -665,20 +766,25 @@ func TestSamplePrefixesByLengthFillsUpFromShuffled(t *testing.T) {
|
||||
}
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
var shuffled []string
|
||||
shuffled := make([]string, 0, sampledPrefixCount)
|
||||
for i := range sampledPrefixCount {
|
||||
shuffled = append(shuffled, fmt.Sprintf("10.0.%d.0/24", i))
|
||||
}
|
||||
route := mkV4Route(t, shuffled[1], 64500, time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC))
|
||||
if err := db.UpsertLiveRouteBatch([]*LiveRoute{route}); err != nil {
|
||||
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
route := mkV4Route(t, shuffled[1], 64500, ts)
|
||||
|
||||
err = db.UpsertLiveRouteBatch([]*LiveRoute{route})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
|
||||
sample, err := db.samplePrefixesByLength(
|
||||
context.Background(), liveRoutesV4Table, route.MaskLength, sampleLimit, shuffled)
|
||||
sample, err := db.samplePrefixesByLength(context.Background(),
|
||||
liveRoutesV4Table, route.MaskLength, sampleLimit, shuffled)
|
||||
if err != nil {
|
||||
t.Fatalf("samplePrefixesByLength: %v", err)
|
||||
}
|
||||
|
||||
want := append([]string{shuffled[1], shuffled[0]}, shuffled[2:sampleLimit]...)
|
||||
if !slices.Equal(sample, want) {
|
||||
t.Errorf("samplePrefixesByLength = %v, want %v", sample, want)
|
||||
@@ -691,6 +797,8 @@ func TestSamplePrefixesByLengthFillsUpFromShuffled(t *testing.T) {
|
||||
// left, and that only the prefixes that still have a route get their newest
|
||||
// route listed.
|
||||
func TestGetRandomPrefixesByLengthSkipsWithdrawnRoutes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := &config.Config{StateDir: t.TempDir()}
|
||||
|
||||
db, err := New(cfg, logger.New())
|
||||
@@ -702,29 +810,38 @@ func TestGetRandomPrefixesByLengthSkipsWithdrawnRoutes(t *testing.T) {
|
||||
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
kept := mkV4Route(t, "198.51.100.0/24", 64500, ts)
|
||||
withdrawn := mkV4Route(t, "172.16.0.0/16", 64500, ts)
|
||||
if err := db.UpsertLiveRouteBatch([]*LiveRoute{kept, withdrawn}); err != nil {
|
||||
|
||||
err = db.UpsertLiveRouteBatch([]*LiveRoute{kept, withdrawn})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertLiveRouteBatch: %v", err)
|
||||
}
|
||||
if err := db.DeleteLiveRoute(withdrawn.Prefix, withdrawn.PeerIP); err != nil {
|
||||
|
||||
err = db.DeleteLiveRoute(withdrawn.Prefix, withdrawn.PeerIP)
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteLiveRoute: %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
sample, err := db.samplePrefixesByLength(
|
||||
ctx, liveRoutesV4Table, withdrawn.MaskLength, sampleLimit, []string{withdrawn.Prefix})
|
||||
|
||||
sample, err := db.samplePrefixesByLength(ctx, liveRoutesV4Table,
|
||||
withdrawn.MaskLength, sampleLimit, []string{withdrawn.Prefix})
|
||||
if err != nil {
|
||||
t.Fatalf("samplePrefixesByLength: %v", err)
|
||||
}
|
||||
|
||||
if len(sample) != 0 {
|
||||
t.Errorf("samplePrefixesByLength = %v, want none for a mask length with no route left", sample)
|
||||
t.Errorf("samplePrefixesByLength = %v, want none for a mask length "+
|
||||
"with no route left", sample)
|
||||
}
|
||||
|
||||
routes, err := db.newestRouteOfEachPrefix(
|
||||
ctx, liveRoutesV4Table, ipVersionV4, []string{withdrawn.Prefix, kept.Prefix})
|
||||
routes, err := db.newestRouteOfEachPrefix(ctx, liveRoutesV4Table,
|
||||
ipVersionV4, []string{withdrawn.Prefix, kept.Prefix})
|
||||
if err != nil {
|
||||
t.Fatalf("newestRouteOfEachPrefix: %v", err)
|
||||
}
|
||||
|
||||
if len(routes) != 1 || routes[0].Prefix != kept.Prefix {
|
||||
t.Errorf("newestRouteOfEachPrefix = %+v, want only the route of %s", routes, kept.Prefix)
|
||||
t.Errorf("newestRouteOfEachPrefix = %+v, want only the route of %s",
|
||||
routes, kept.Prefix)
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ type Stats struct {
|
||||
// Store defines the interface for database operations. It provides methods for
|
||||
// managing ASNs, prefixes, announcements, peerings, BGP peers, and live routes.
|
||||
// Implementations must be safe for concurrent use.
|
||||
//
|
||||
//nolint:interfacebloat // the one seam handlers and tests depend on
|
||||
type Store interface {
|
||||
// ASN operations
|
||||
GetOrCreateASN(number int, timestamp time.Time) (*ASN, error)
|
||||
@@ -58,8 +60,10 @@ type Store interface {
|
||||
DeleteLiveRoute(prefix, peerIP string) error
|
||||
DeleteLiveRouteBatch(deletions []LiveRouteDeletion) error
|
||||
DeleteLiveRoutesOfPeer(peerIP string) (deleted int, err error)
|
||||
GetPrefixDistribution() (ipv4 []PrefixDistribution, ipv6 []PrefixDistribution, err error)
|
||||
GetPrefixDistributionContext(ctx context.Context) (ipv4 []PrefixDistribution, ipv6 []PrefixDistribution, err error)
|
||||
GetPrefixDistribution() (
|
||||
ipv4 []PrefixDistribution, ipv6 []PrefixDistribution, err error)
|
||||
GetPrefixDistributionContext(ctx context.Context) (
|
||||
ipv4 []PrefixDistribution, ipv6 []PrefixDistribution, err error)
|
||||
GetLiveRouteCounts() (ipv4Count, ipv6Count int, err error)
|
||||
GetLiveRouteCountsContext(ctx context.Context) (ipv4Count, ipv6Count int, err error)
|
||||
|
||||
@@ -78,9 +82,13 @@ type Store interface {
|
||||
GetASPeers(asn int) ([]ASPeer, error)
|
||||
GetASPeersContext(ctx context.Context, asn int) ([]ASPeer, error)
|
||||
GetPrefixDetails(prefix string) ([]LiveRoute, map[int]ASN, error)
|
||||
GetPrefixDetailsContext(ctx context.Context, prefix string) ([]LiveRoute, map[int]ASN, error)
|
||||
GetPrefixDetailsContext(
|
||||
ctx context.Context, prefix string,
|
||||
) ([]LiveRoute, map[int]ASN, error)
|
||||
GetRandomPrefixesByLength(maskLength, ipVersion, limit int) ([]LiveRoute, error)
|
||||
GetRandomPrefixesByLengthContext(ctx context.Context, maskLength, ipVersion, limit int) ([]LiveRoute, error)
|
||||
GetRandomPrefixesByLengthContext(
|
||||
ctx context.Context, maskLength, ipVersion, limit int,
|
||||
) ([]LiveRoute, error)
|
||||
|
||||
// Lifecycle
|
||||
Close() error
|
||||
|
||||
+26
-19
@@ -1,4 +1,3 @@
|
||||
// Package database provides SQLite storage for BGP routing data.
|
||||
package database
|
||||
|
||||
import (
|
||||
@@ -9,6 +8,8 @@ import (
|
||||
|
||||
// ASN represents an Autonomous System Number with its metadata including
|
||||
// handle, description, WHOIS data, and first/last seen timestamps.
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type ASN struct {
|
||||
ASN int `json:"asn"`
|
||||
Handle string `json:"handle"`
|
||||
@@ -36,37 +37,39 @@ type ASN struct {
|
||||
// Prefix represents an IP prefix (CIDR block) with its IP version (4 or 6)
|
||||
// and first/last seen timestamps.
|
||||
type Prefix struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Prefix string `json:"prefix"`
|
||||
IPVersion int `json:"ip_version"` // 4 or 6
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
ID uuid.UUID
|
||||
Prefix string
|
||||
IPVersion int // 4 or 6
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// Announcement represents a BGP announcement or withdrawal event,
|
||||
// containing the prefix, AS path, origin ASN, peer ASN, next hop, and timestamp.
|
||||
type Announcement struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
PrefixID uuid.UUID `json:"prefix_id"`
|
||||
PeerASN int `json:"peer_asn"`
|
||||
OriginASN int `json:"origin_asn"`
|
||||
Path string `json:"path"` // JSON-encoded AS path
|
||||
NextHop string `json:"next_hop"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
IsWithdrawal bool `json:"is_withdrawal"`
|
||||
ID uuid.UUID
|
||||
PrefixID uuid.UUID
|
||||
PeerASN int
|
||||
OriginASN int
|
||||
Path string // JSON-encoded AS path
|
||||
NextHop string
|
||||
Timestamp time.Time
|
||||
IsWithdrawal bool
|
||||
}
|
||||
|
||||
// ASNPeering represents a peering relationship between two ASNs,
|
||||
// stored with the lower ASN as ASA and the higher as ASB.
|
||||
type ASNPeering struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ASA int `json:"as_a"`
|
||||
ASB int `json:"as_b"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
ID uuid.UUID
|
||||
ASA int
|
||||
ASB int
|
||||
FirstSeen time.Time
|
||||
LastSeen time.Time
|
||||
}
|
||||
|
||||
// LiveRoute represents a route in the live routing table
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type LiveRoute struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Prefix string `json:"prefix"`
|
||||
@@ -80,12 +83,16 @@ type LiveRoute struct {
|
||||
}
|
||||
|
||||
// PrefixDistribution represents the distribution of prefixes by mask length
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type PrefixDistribution struct {
|
||||
MaskLength int `json:"mask_length"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// IPInfo represents comprehensive IP information for the /ip endpoint
|
||||
//
|
||||
//nolint:tagliatelle // field names are the existing JSON API
|
||||
type IPInfo struct {
|
||||
IP string `json:"ip"`
|
||||
PTR []string `json:"ptr,omitempty"`
|
||||
|
||||
@@ -18,30 +18,12 @@ func logSlowQuery(logger *logger.Logger, query string, start time.Time) {
|
||||
}
|
||||
}
|
||||
|
||||
// queryRow wraps QueryRow with slow query logging
|
||||
// nolint:unused // kept for consistency with other query wrappers
|
||||
func (d *Database) queryRow(query string, args ...interface{}) *sql.Row {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(d.logger, query, start)
|
||||
|
||||
return d.db.QueryRow(query, args...)
|
||||
}
|
||||
|
||||
// query wraps Query with slow query logging
|
||||
// nolint:unused // kept for future use to ensure all queries go through slow query logging
|
||||
func (d *Database) query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(d.logger, query, start)
|
||||
|
||||
return d.db.Query(query, args...)
|
||||
}
|
||||
|
||||
// exec wraps Exec with slow query logging
|
||||
func (d *Database) exec(query string, args ...interface{}) error {
|
||||
func (d *Database) exec(query string, args ...any) error {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(d.logger, query, start)
|
||||
|
||||
_, err := d.db.Exec(query, args...)
|
||||
_, err := d.db.ExecContext(context.Background(), query, args...)
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -49,51 +31,51 @@ func (d *Database) exec(query string, args ...interface{}) error {
|
||||
// loggingTx wraps sql.Tx to log slow queries
|
||||
type loggingTx struct {
|
||||
*sql.Tx
|
||||
|
||||
logger *logger.Logger
|
||||
}
|
||||
|
||||
// QueryRow wraps sql.Tx.QueryRow to log slow queries
|
||||
func (tx *loggingTx) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||
func (tx *loggingTx) QueryRow(query string, args ...any) *sql.Row {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(tx.logger, query, start)
|
||||
|
||||
return tx.Tx.QueryRow(query, args...)
|
||||
}
|
||||
|
||||
// Query wraps sql.Tx.Query to log slow queries
|
||||
func (tx *loggingTx) Query(query string, args ...interface{}) (*sql.Rows, error) {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(tx.logger, query, start)
|
||||
|
||||
return tx.Tx.Query(query, args...)
|
||||
}
|
||||
|
||||
// QueryContext wraps sql.Tx.QueryContext to log slow queries
|
||||
func (tx *loggingTx) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(tx.logger, query, start)
|
||||
|
||||
return tx.Tx.QueryContext(ctx, query, args...)
|
||||
return tx.Tx.QueryRowContext(context.Background(), query, args...)
|
||||
}
|
||||
|
||||
// QueryRowContext wraps sql.Tx.QueryRowContext to log slow queries
|
||||
func (tx *loggingTx) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
|
||||
func (tx *loggingTx) QueryRowContext(
|
||||
ctx context.Context, query string, args ...any,
|
||||
) *sql.Row {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(tx.logger, query, start)
|
||||
|
||||
return tx.Tx.QueryRowContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// Exec wraps sql.Tx.Exec to log slow queries
|
||||
func (tx *loggingTx) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
// QueryContext wraps sql.Tx.QueryContext to log slow queries
|
||||
func (tx *loggingTx) QueryContext(
|
||||
ctx context.Context, query string, args ...any,
|
||||
) (*sql.Rows, error) {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(tx.logger, query, start)
|
||||
|
||||
return tx.Tx.Exec(query, args...)
|
||||
//nolint:sqlclosecheck // returns the rows, which the caller closes
|
||||
return tx.Tx.QueryContext(ctx, query, args...)
|
||||
}
|
||||
|
||||
// Exec wraps sql.Tx.Exec to log slow queries
|
||||
func (tx *loggingTx) Exec(query string, args ...any) (sql.Result, error) {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(tx.logger, query, start)
|
||||
|
||||
return tx.Tx.ExecContext(context.Background(), query, args...)
|
||||
}
|
||||
|
||||
// ExecContext wraps sql.Tx.ExecContext to log slow queries
|
||||
func (tx *loggingTx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
|
||||
func (tx *loggingTx) ExecContext(
|
||||
ctx context.Context, query string, args ...any,
|
||||
) (sql.Result, error) {
|
||||
start := time.Now()
|
||||
defer logSlowQuery(tx.logger, query, start)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ func prefixMaskLength(prefix string) (int, error) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("invalid prefix %s: %w", prefix, err)
|
||||
}
|
||||
|
||||
maskLength, _ := network.Mask.Size()
|
||||
|
||||
return maskLength, nil
|
||||
|
||||
Reference in New Issue
Block a user