check / check (push) Failing after 15m27s
Stop now counts the origin changes inside a running write, in a new writingOriginChanges field, and adds them to the count it logs. The prune test checks which rows survive, including a prefix with rows on both sides of 72 hours. The prefix page tests cover 5,000 and 5,001 rows and a row's time and peer count. New tests cover the dropped-rows warning and the dropped count's reset. The prune floor is named minOriginChangesPerPrefix. The README, TODO, package doc and doc comments are corrected to match. Model: deepseek-v4-pro
203 lines
6.0 KiB
Go
203 lines
6.0 KiB
Go
package database
|
|
|
|
import (
|
|
"net/netip"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/routetable"
|
|
)
|
|
|
|
// TestOriginChangesWriteThenRead writes three origin changes and reads them
|
|
// back newest first, honouring the limit.
|
|
func TestOriginChangesWriteThenRead(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := newRoutesTestDatabase(t)
|
|
prefix := netip.MustParsePrefix(tablePrefixV4)
|
|
|
|
changes := []routetable.OriginChange{
|
|
{Prefix: prefix, OriginASN: asA, Announced: true, PeerCount: 1,
|
|
Time: tableTime(10)},
|
|
{Prefix: prefix, OriginASN: asA, Announced: false, PeerCount: 0,
|
|
Time: tableTime(20)},
|
|
{Prefix: prefix, OriginASN: asB, Announced: true, PeerCount: 1,
|
|
Time: tableTime(30)},
|
|
}
|
|
|
|
err := db.WriteOriginChanges(changes)
|
|
if err != nil {
|
|
t.Fatalf("WriteOriginChanges: %v", err)
|
|
}
|
|
|
|
got, err := db.GetOriginChanges(t.Context(), prefix, 2)
|
|
if err != nil {
|
|
t.Fatalf("GetOriginChanges: %v", err)
|
|
}
|
|
|
|
want := []routetable.OriginChange{
|
|
{Prefix: prefix, OriginASN: asB, Announced: true, PeerCount: 1,
|
|
Time: tableTime(30)},
|
|
{Prefix: prefix, OriginASN: asA, Announced: false, PeerCount: 0,
|
|
Time: tableTime(20)},
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("GetOriginChanges = %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
// TestOriginChangesPrune checks that the prune keeps, for a prefix with more
|
|
// than 500 rows, its newest 500 rows and every row newer than 72 hours, and
|
|
// deletes the rest. It checks which rows survive by their times, not only how
|
|
// many.
|
|
func TestOriginChangesPrune(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := newRoutesTestDatabase(t)
|
|
oldPrefix := netip.MustParsePrefix(tablePrefixV4)
|
|
straddling := netip.MustParsePrefix(tablePrefixShortV4)
|
|
other := netip.MustParsePrefix("10.20.0.0/16")
|
|
|
|
// Whole seconds, so the stored times round-trip exactly.
|
|
now := time.Now().Truncate(time.Second)
|
|
|
|
// 600 rows for oldPrefix, all older than 72 hours, oldest first. The
|
|
// prune keeps the newest 500 and deletes the oldest 100.
|
|
changes := rows(oldPrefix, now.Add(-100*time.Hour), 600)
|
|
// 600 rows for straddling: 400 older than 72 hours, then 200 newer. The
|
|
// prune keeps all 200 newer and the newest 300 of the 400 older.
|
|
changes = append(changes, rows(straddling, now.Add(-100*time.Hour), 400)...)
|
|
changes = append(changes, rows(straddling, now.Add(-71*time.Hour), 200)...)
|
|
// One row for other, which the prune leaves alone.
|
|
changes = append(changes, rows(other, now.Add(-100*time.Hour), 1)...)
|
|
|
|
err := db.WriteOriginChanges(changes)
|
|
if err != nil {
|
|
t.Fatalf("WriteOriginChanges: %v", err)
|
|
}
|
|
|
|
err = db.PruneOriginChanges(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("PruneOriginChanges: %v", err)
|
|
}
|
|
|
|
oldTimes := survivorTimes(t, db, oldPrefix, 1000)
|
|
if len(oldTimes) != 500 {
|
|
t.Errorf("GetOriginChanges(oldPrefix) = %d rows, want 500", len(oldTimes))
|
|
}
|
|
|
|
// The newest row survives, and the oldest survivor is the 100th oldest,
|
|
// so a prune that kept the oldest 500 instead would fail.
|
|
wantNewest := now.Add(-100 * time.Hour).Add(599 * time.Minute)
|
|
if !oldTimes[0].Equal(wantNewest) {
|
|
t.Errorf("the newest surviving row's time = %v, want %v",
|
|
oldTimes[0], wantNewest)
|
|
}
|
|
|
|
wantOldest := now.Add(-100 * time.Hour).Add(100 * time.Minute)
|
|
if !oldTimes[499].Equal(wantOldest) {
|
|
t.Errorf("the oldest surviving row's time = %v, want %v",
|
|
oldTimes[499], wantOldest)
|
|
}
|
|
|
|
straddlingTimes := survivorTimes(t, db, straddling, 1000)
|
|
if len(straddlingTimes) != 500 {
|
|
t.Errorf("GetOriginChanges(straddling) = %d rows, want 500",
|
|
len(straddlingTimes))
|
|
}
|
|
|
|
// The newest of the newer-than-72-hours rows survives, and the oldest
|
|
// survivor is the 100th oldest of the older-than-72-hours rows.
|
|
wantNewest = now.Add(-71 * time.Hour).Add(199 * time.Minute)
|
|
if !straddlingTimes[0].Equal(wantNewest) {
|
|
t.Errorf("the newest surviving row's time = %v, want %v",
|
|
straddlingTimes[0], wantNewest)
|
|
}
|
|
|
|
wantOldest = now.Add(-100 * time.Hour).Add(100 * time.Minute)
|
|
if !straddlingTimes[499].Equal(wantOldest) {
|
|
t.Errorf("the oldest surviving row's time = %v, want %v",
|
|
straddlingTimes[499], wantOldest)
|
|
}
|
|
|
|
gotOther, err := db.GetOriginChanges(t.Context(), other, 10)
|
|
if err != nil {
|
|
t.Fatalf("GetOriginChanges: %v", err)
|
|
}
|
|
|
|
if len(gotOther) != 1 {
|
|
t.Errorf("GetOriginChanges(other) = %d rows, want 1", len(gotOther))
|
|
}
|
|
}
|
|
|
|
// rows returns n origin changes for prefix, at times from plus one minute per
|
|
// step, oldest first.
|
|
func rows(prefix netip.Prefix, from time.Time, n int) []routetable.OriginChange {
|
|
changes := make([]routetable.OriginChange, 0, n)
|
|
|
|
for i := range n {
|
|
changes = append(changes, routetable.OriginChange{
|
|
Prefix: prefix, OriginASN: asA, Announced: true, PeerCount: 1,
|
|
Time: from.Add(time.Duration(i) * time.Minute),
|
|
})
|
|
}
|
|
|
|
return changes
|
|
}
|
|
|
|
// survivorTimes returns the times of prefix's rows after the prune, newest
|
|
// first.
|
|
func survivorTimes(
|
|
t *testing.T, db *Database, prefix netip.Prefix, limit int,
|
|
) []time.Time {
|
|
t.Helper()
|
|
|
|
rows, err := db.GetOriginChanges(t.Context(), prefix, limit)
|
|
if err != nil {
|
|
t.Fatalf("GetOriginChanges: %v", err)
|
|
}
|
|
|
|
times := make([]time.Time, len(rows))
|
|
for i, row := range rows {
|
|
times[i] = row.Time
|
|
}
|
|
|
|
return times
|
|
}
|
|
|
|
// TestOriginChangesSurviveRouteDeletion checks that deleting a routes row
|
|
// leaves the prefix's origin_changes rows.
|
|
func TestOriginChangesSurviveRouteDeletion(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db := newRoutesTestDatabase(t)
|
|
prefix := netip.MustParsePrefix(tablePrefixV4)
|
|
|
|
err := db.WriteOriginChanges([]routetable.OriginChange{{
|
|
Prefix: prefix, OriginASN: asA, Announced: true, PeerCount: 1,
|
|
Time: tableTime(10),
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("WriteOriginChanges: %v", err)
|
|
}
|
|
|
|
// Write a route, then delete it.
|
|
table := routetable.New(routetable.Params{})
|
|
announceTo(table, tablePrefixV4, asA, peerA, 10)
|
|
writeTableChanges(t, db, table)
|
|
|
|
table.Withdraw(prefix, peerA)
|
|
writeTableChanges(t, db, table)
|
|
|
|
got, err := db.GetOriginChanges(t.Context(), prefix, 10)
|
|
if err != nil {
|
|
t.Fatalf("GetOriginChanges: %v", err)
|
|
}
|
|
|
|
if len(got) != 1 {
|
|
t.Errorf("GetOriginChanges = %d rows, want 1", len(got))
|
|
}
|
|
}
|