Never move last seen back or first seen forward (closes #162)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
Some RIS collectors send updates days old while they catch up on a backlog. Every update to a stored AS, peering or peer set last_seen to the update's time, even an older one, and never lowered first_seen. An AS page could then list a peer first seen after it was last seen. The update statements for asns, peerings and bgp_peers now keep the earlier of the stored and new first_seen and the later of the stored and new last_seen, with MIN and MAX in the SQL. The batch and the single-row writes share each statement. GetOrCreateASN returns the times the row now holds. Route times are unchanged. Model: opus-5-5
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/config"
|
||||
"git.eeqj.de/sneak/routewatch/internal/logger"
|
||||
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
||||
"git.eeqj.de/sneak/routewatch/pkg/asinfo"
|
||||
"github.com/mattn/go-sqlite3"
|
||||
)
|
||||
@@ -1960,6 +1961,130 @@ func TestUpdatePeerBatchStoresTheEarliestAndLatestTimesOfANewPeer(
|
||||
}
|
||||
}
|
||||
|
||||
// backlogAge is how much older the second write is than the first in the tests
|
||||
// of first_seen and last_seen, as when a collector catches up on a backlog.
|
||||
const backlogAge = 24 * time.Hour
|
||||
|
||||
// TestOlderWriteKeepsLastSeenAndLowersFirstSeen writes a row of each of asns,
|
||||
// peerings and bgp_peers through each function that writes one: first at a
|
||||
// newer time, then at an older one. Each row keeps the newer last_seen and
|
||||
// takes the older first_seen. Each function writes a row of its own.
|
||||
func TestOlderWriteKeepsLastSeenAndLowersFirstSeen(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() }()
|
||||
|
||||
const (
|
||||
batchASN, singleASN = 64500, 64501
|
||||
peerASN = 64502
|
||||
asnQuery = "SELECT first_seen, last_seen FROM asns WHERE asn = ?"
|
||||
peeringQuery = "SELECT first_seen, last_seen FROM peerings " +
|
||||
"WHERE as_a = ? AND as_b = ?"
|
||||
peerQuery = "SELECT first_seen, last_seen FROM bgp_peers " +
|
||||
"WHERE peer_ip = ?"
|
||||
)
|
||||
|
||||
checkOlderWrite(t, db, func(at time.Time) error {
|
||||
return db.GetOrCreateASNBatch(
|
||||
map[int]SeenTimes{batchASN: {First: at, Last: at}})
|
||||
}, asnQuery, batchASN)
|
||||
|
||||
checkOlderWrite(t, db, func(at time.Time) error {
|
||||
_, err := db.GetOrCreateASN(singleASN, at)
|
||||
|
||||
return err
|
||||
}, asnQuery, singleASN)
|
||||
|
||||
checkOlderWrite(t, db, func(at time.Time) error {
|
||||
return db.RecordPeering(batchASN, singleASN,
|
||||
SeenTimes{First: at, Last: at})
|
||||
}, peeringQuery, batchASN, singleASN)
|
||||
|
||||
checkOlderWrite(t, db, func(at time.Time) error {
|
||||
return db.UpdatePeerBatch(map[string]PeerUpdate{
|
||||
testV4PeerIP: {
|
||||
PeerIP: testV4PeerIP, PeerASN: peerASN,
|
||||
MessageType: ristypes.MessageTypeUpdate,
|
||||
Seen: SeenTimes{First: at, Last: at},
|
||||
},
|
||||
})
|
||||
}, peerQuery, testV4PeerIP)
|
||||
|
||||
checkOlderWrite(t, db, func(at time.Time) error {
|
||||
return db.UpdatePeer(testPeerIP, peerASN, ristypes.MessageTypeUpdate, at)
|
||||
}, peerQuery, testPeerIP)
|
||||
}
|
||||
|
||||
// checkOlderWrite calls write at a newer time, then at a time backlogAge older.
|
||||
// It checks that query, given args, reads first_seen at the older time and
|
||||
// last_seen at the newer one.
|
||||
func checkOlderWrite(
|
||||
t *testing.T, db *Database, write func(at time.Time) error,
|
||||
query string, args ...any,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
newer := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
older := newer.Add(-backlogAge)
|
||||
|
||||
for _, at := range []time.Time{newer, older} {
|
||||
err := write(at)
|
||||
if err != nil {
|
||||
t.Fatalf("writing at %s: %v", at, err)
|
||||
}
|
||||
}
|
||||
|
||||
var firstSeen, lastSeen time.Time
|
||||
|
||||
err := db.db.QueryRowContext(t.Context(), query, args...).
|
||||
Scan(&firstSeen, &lastSeen)
|
||||
if err != nil {
|
||||
t.Fatalf("reading the row: %v", err)
|
||||
}
|
||||
|
||||
if !firstSeen.Equal(older) || !lastSeen.Equal(newer) {
|
||||
t.Errorf("first seen %s, last seen %s; want %s, %s",
|
||||
firstSeen, lastSeen, older, newer)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetOrCreateASNReturnsStoredSeenTimes stores an AS at a newer time, then
|
||||
// at an older one. The second call returns the times the row now holds: the
|
||||
// older first_seen and the newer last_seen.
|
||||
func TestGetOrCreateASNReturnsStoredSeenTimes(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() }()
|
||||
|
||||
const asn = 64500
|
||||
|
||||
newer := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
older := newer.Add(-backlogAge)
|
||||
|
||||
_, err = db.GetOrCreateASN(asn, newer)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreateASN at %s: %v", newer, err)
|
||||
}
|
||||
|
||||
got, err := db.GetOrCreateASN(asn, older)
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreateASN at %s: %v", older, err)
|
||||
}
|
||||
|
||||
if !got.FirstSeen.Equal(older) || !got.LastSeen.Equal(newer) {
|
||||
t.Errorf("first seen %s, last seen %s; want %s, %s",
|
||||
got.FirstSeen, got.LastSeen, older, newer)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpsertLiveRouteBatchWritesInKeyOrder writes new routes of two peers out
|
||||
// of key order, the last one a second announcement of the first, and checks
|
||||
// that the rows were inserted in key order, peer then prefix (each new row's id
|
||||
|
||||
Reference in New Issue
Block a user