Serve the /api/v1/stats prefix distribution from memory (closes #30)
check / check (push) Waiting to run

The prefix distribution was the last query on the stats path. It read one
index entry per live route on every request, so on a large database it took
the whole 4-second deadline and /api/v1/stats answered 500.

The distribution now lives next to the in-memory counts: seeded once at
startup from the same query, then kept exact by the live-route writes. A new
route whose prefix had no live route adds one at its mask length; a delete
that leaves a prefix with no live route takes one away. Each check is one
lookup on the prefix index, made only for new and removed routes, inside the
write.

Model: opus-5-5
This commit is contained in:
2026-09-29 11:05:28 +00:00
parent 6422d9fa0c
commit 539d15c0a5
5 changed files with 394 additions and 60 deletions
+160 -4
View File
@@ -2,6 +2,7 @@ package database
import (
"context"
"slices"
"sync"
"testing"
"time"
@@ -11,7 +12,8 @@ import (
"github.com/google/uuid"
)
// mkV4Route builds an IPv4 live route with its range columns populated.
// mkV4Route builds an IPv4 live route with its mask length and range columns
// taken from the prefix.
func mkV4Route(t *testing.T, prefix string, asn int, ts time.Time) *LiveRoute {
t.Helper()
@@ -19,11 +21,15 @@ func mkV4Route(t *testing.T, prefix string, asn int, ts time.Time) *LiveRoute {
if err != nil {
t.Fatalf("CalculateIPv4Range(%s): %v", prefix, err)
}
maskLength, err := prefixMaskLength(prefix)
if err != nil {
t.Fatalf("prefixMaskLength(%s): %v", prefix, err)
}
return &LiveRoute{
ID: uuid.New(),
Prefix: prefix,
MaskLength: 24,
MaskLength: maskLength,
IPVersion: ipVersionV4,
OriginASN: asn,
PeerIP: "192.0.2.1",
@@ -143,8 +149,8 @@ func TestLiveCountsTrackWritesInRealtime(t *testing.T) {
}
// TestLiveCountsSeededFromDatabaseAtStartup writes rows, reopens the same
// database file, and checks the counts come back from the seed scan rather than
// starting at zero.
// 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) {
cfg := &config.Config{StateDir: t.TempDir()}
@@ -184,6 +190,145 @@ func TestLiveCountsSeededFromDatabaseAtStartup(t *testing.T) {
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,
[]PrefixDistribution{{MaskLength: 32, Count: 1}})
}
// TestPrefixDistributionTracksWrites checks that the prefix distribution the
// stats read reports stays exact across each kind of write, through both the
// batch and the single-route methods, and that after every step it equals what
// the distribution query reads from the route tables.
func TestPrefixDistributionTracksWrites(t *testing.T) {
cfg := &config.Config{StateDir: t.TempDir()}
db, err := New(cfg, logger.New())
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer func() { _ = db.Close() }()
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)
all := []PrefixDistribution{{MaskLength: 16, Count: 1}, {MaskLength: 24, Count: 2}}
v6Only := []PrefixDistribution{{MaskLength: 32, Count: 1}}
steps := []struct {
name string
write func() error
wantV4 []PrefixDistribution
wantV6 []PrefixDistribution
}{
{
name: "empty database",
write: func() error { return nil },
},
{
name: "new prefixes",
write: func() error { return db.UpsertLiveRouteBatch([]*LiveRoute{shared, other, wide, v6}) },
wantV4: all, wantV6: v6Only,
},
{
name: "re-announcement",
write: func() error { return db.UpsertLiveRouteBatch([]*LiveRoute{shared, other, wide, v6}) },
wantV4: all, wantV6: v6Only,
},
{
name: "second peer announces a prefix that already has a route",
write: func() error { return db.UpsertLiveRoute(sharedSecondPeer) },
wantV4: all, wantV6: v6Only,
},
{
name: "withdrawal of a route that is not the last for its prefix",
write: func() error {
return db.DeleteLiveRouteBatch([]LiveRouteDeletion{
{Prefix: shared.Prefix, OriginASN: shared.OriginASN, PeerIP: shared.PeerIP, IPVersion: ipVersionV4},
})
},
wantV4: all, wantV6: v6Only,
},
{
name: "withdrawal of the last route for a prefix",
write: func() error {
return db.DeleteLiveRoute(sharedSecondPeer.Prefix, sharedSecondPeer.OriginASN, sharedSecondPeer.PeerIP)
},
wantV4: []PrefixDistribution{{MaskLength: 16, Count: 1}, {MaskLength: 24, Count: 1}},
wantV6: v6Only,
},
{
name: "withdrawal of every remaining route, one without an origin ASN",
write: func() error {
return db.DeleteLiveRouteBatch([]LiveRouteDeletion{
{Prefix: other.Prefix, PeerIP: other.PeerIP, IPVersion: ipVersionV4},
{Prefix: wide.Prefix, OriginASN: wide.OriginASN, PeerIP: wide.PeerIP, IPVersion: ipVersionV4},
{Prefix: v6.Prefix, OriginASN: v6.OriginASN, PeerIP: v6.PeerIP, IPVersion: ipVersionV6},
})
},
},
}
ctx := context.Background()
for _, step := range steps {
if err := step.write(); err != nil {
t.Fatalf("%s: %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)
}
}
// TestStatsDistributionNeedsNoQuery checks that the stats read serves the prefix
// distribution from memory. The read's deadline has already passed, so any
// query it ran would fail at once. The distribution query it used to run read
// every live route and passed the /api/v1/stats deadline on a large database
// (issue 30).
func TestStatsDistributionNeedsNoQuery(t *testing.T) {
cfg := &config.Config{StateDir: t.TempDir()}
db, err := New(cfg, logger.New())
if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer func() { _ = db.Close() }()
ts := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
if err := db.UpsertLiveRouteBatch([]*LiveRoute{
mkV4Route(t, "198.51.100.0/24", 64500, ts),
mkV6Route("2001:db8::/32", 64501, ts),
}); err != nil {
t.Fatalf("UpsertLiveRouteBatch: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
stats, err := db.GetStatsContext(ctx)
if err != nil {
t.Fatalf("GetStatsContext: %v", err)
}
assertDistribution(t, "IPv4 distribution", stats.IPv4PrefixDistribution,
[]PrefixDistribution{{MaskLength: 24, Count: 1}})
assertDistribution(t, "IPv6 distribution", stats.IPv6PrefixDistribution,
[]PrefixDistribution{{MaskLength: 32, Count: 1}})
}
// TestStatsRouteTimestamps checks the oldest/newest route timestamps are read
@@ -296,6 +441,9 @@ func TestLiveCountsConcurrentReadWrite(t *testing.T) {
if want := writers * 25; stats.IPv6Routes != want {
t.Errorf("IPv6Routes = %d, want %d", stats.IPv6Routes, want)
}
// Every writer announced the same prefix, so it counts once.
assertDistribution(t, "IPv6 distribution", stats.IPv6PrefixDistribution,
[]PrefixDistribution{{MaskLength: 32, Count: 1}})
}
type wantCounts struct {
@@ -333,3 +481,11 @@ func assertCounts(t *testing.T, when string, got Stats, want wantCounts) {
t.Errorf("%s: LiveRoutes = %d, want %d", when, got.LiveRoutes, want.liveRoutes)
}
}
func assertDistribution(t *testing.T, what string, got, want []PrefixDistribution) {
t.Helper()
if !slices.Equal(got, want) {
t.Errorf("%s = %v, want %v", what, got, want)
}
}