Compare commits

..
1 Commits
Author SHA1 Message Date
sneak 25c7b87991 Never move last seen back or first seen forward (closes #162)
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
2026-10-26 23:58:48 +00:00
+63 -45
View File
@@ -1961,29 +1961,26 @@ func TestUpdatePeerBatchStoresTheEarliestAndLatestTimesOfANewPeer(
} }
} }
// backlogAge is how much older the second write is than the first in the tests // backlogAge is how much older one write is than the other in the tests of
// of first_seen and last_seen, as when a collector catches up on a backlog. // first_seen and last_seen, as when a collector catches up on a backlog.
const backlogAge = 24 * time.Hour const backlogAge = 24 * time.Hour
// batchSpan is how much earlier a batch's First is than its Last in the tests // batchSpan is how much earlier a batch's First is than its Last in the tests
// of first_seen and last_seen. // of first_seen and last_seen.
const batchSpan = time.Hour const batchSpan = time.Hour
// TestOlderWriteKeepsLastSeenAndLowersFirstSeen writes a row of each of asns, // TestWritesKeepEarliestFirstSeenAndLatestLastSeen writes a row of each of
// peerings and bgp_peers through each function that writes one: first at a // asns, peerings and bgp_peers through each function that writes one, at a
// newer time, then at an older one. Each row keeps the newer last_seen and // newer time and at an older one. Each row takes the older write's First as
// takes the older first_seen. Each function writes a row of its own. A batch // first_seen and the newer write's Last as last_seen. The test writes in both
// write's First is batchSpan before its Last, so the test catches the two // orders, each in a database of its own. Newer then older is a collector
// swapped. A single-row write takes one time. // catching up on a backlog; older then newer is a feed that arrives in order.
func TestOlderWriteKeepsLastSeenAndLowersFirstSeen(t *testing.T) { // Each function writes a row of its own. A batch write's First is batchSpan
// before its Last, so the test catches the two swapped. A single-row write
// takes one time.
func TestWritesKeepEarliestFirstSeenAndLatestLastSeen(t *testing.T) {
t.Parallel() 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 ( const (
batchASN, singleASN = 64500, 64501 batchASN, singleASN = 64500, 64501
peerASN = 64502 peerASN = 64502
@@ -1994,42 +1991,58 @@ func TestOlderWriteKeepsLastSeenAndLowersFirstSeen(t *testing.T) {
"WHERE peer_ip = ?" "WHERE peer_ip = ?"
) )
checkOlderWrite(t, db, func(seen SeenTimes) error { for name, olderFirst := range map[string]bool{
return db.GetOrCreateASNBatch(map[int]SeenTimes{batchASN: seen}) "newer then older": false,
}, batchSpan, asnQuery, batchASN) "older then newer": true,
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
checkOlderWrite(t, db, func(seen SeenTimes) error { db, err := New(&config.Config{StateDir: t.TempDir()}, logger.New())
_, err := db.GetOrCreateASN(singleASN, seen.Last) if err != nil {
t.Fatalf("failed to create database: %v", err)
}
defer func() { _ = db.Close() }()
return err checkSeenTimes(t, db, olderFirst, func(seen SeenTimes) error {
}, 0, asnQuery, singleASN) return db.GetOrCreateASNBatch(map[int]SeenTimes{batchASN: seen})
}, batchSpan, asnQuery, batchASN)
checkOlderWrite(t, db, func(seen SeenTimes) error { checkSeenTimes(t, db, olderFirst, func(seen SeenTimes) error {
return db.RecordPeering(batchASN, singleASN, seen) _, err := db.GetOrCreateASN(singleASN, seen.Last)
}, batchSpan, peeringQuery, batchASN, singleASN)
checkOlderWrite(t, db, func(seen SeenTimes) error { return err
return db.UpdatePeerBatch(map[string]PeerUpdate{ }, 0, asnQuery, singleASN)
testV4PeerIP: {
PeerIP: testV4PeerIP, PeerASN: peerASN, checkSeenTimes(t, db, olderFirst, func(seen SeenTimes) error {
MessageType: ristypes.MessageTypeUpdate, return db.RecordPeering(batchASN, singleASN, seen)
Seen: seen, }, batchSpan, peeringQuery, batchASN, singleASN)
},
checkSeenTimes(t, db, olderFirst, func(seen SeenTimes) error {
return db.UpdatePeerBatch(map[string]PeerUpdate{
testV4PeerIP: {
PeerIP: testV4PeerIP, PeerASN: peerASN,
MessageType: ristypes.MessageTypeUpdate,
Seen: seen,
},
})
}, batchSpan, peerQuery, testV4PeerIP)
checkSeenTimes(t, db, olderFirst, func(seen SeenTimes) error {
return db.UpdatePeer(testPeerIP, peerASN,
ristypes.MessageTypeUpdate, seen.Last)
}, 0, peerQuery, testPeerIP)
}) })
}, batchSpan, peerQuery, testV4PeerIP) }
checkOlderWrite(t, db, func(seen SeenTimes) error {
return db.UpdatePeer(testPeerIP, peerASN, ristypes.MessageTypeUpdate,
seen.Last)
}, 0, peerQuery, testPeerIP)
} }
// checkOlderWrite calls write twice, each time with First span before Last: // checkSeenTimes calls write twice, each time with First span before Last: at
// first at newer times, then at times backlogAge older. It checks that query, // newer times and at times backlogAge older. It writes the older times first
// given args, reads the older write's First as first_seen and the newer write's // when olderFirst is true. It checks that query, given args, reads the older
// Last as last_seen. // write's First as first_seen and the newer write's Last as last_seen.
func checkOlderWrite( func checkSeenTimes(
t *testing.T, db *Database, write func(seen SeenTimes) error, t *testing.T, db *Database, olderFirst bool,
write func(seen SeenTimes) error,
span time.Duration, query string, args ...any, span time.Duration, query string, args ...any,
) { ) {
t.Helper() t.Helper()
@@ -2041,7 +2054,12 @@ func checkOlderWrite(
Last: newer.Last.Add(-backlogAge), Last: newer.Last.Add(-backlogAge),
} }
for _, seen := range []SeenTimes{newer, older} { writes := []SeenTimes{newer, older}
if olderFirst {
writes = []SeenTimes{older, newer}
}
for _, seen := range writes {
err := write(seen) err := write(seen)
if err != nil { if err != nil {
t.Fatalf("writing first %s, last %s: %v", seen.First, seen.Last, err) t.Fatalf("writing first %s, last %s: %v", seen.First, seen.Last, err)