From d64a734e7ef984edcc89c68601f1a703a8397699 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 20 Oct 2026 00:39:26 +0000 Subject: [PATCH] Store a new prefix's earliest timestamp as first_seen (closes #112) A batch's prefix times were taken from each peer's last update for the prefix, and the latest of those was kept. The prefix tables hold only first_seen, written when the prefix is new, so a prefix announced by two peers seconds apart in one batch was stored first seen at the later time, and the IP lookup could answer with a first_seen after the route's last_seen. The flush now keeps, for each prefix, the earliest timestamp among all of the batch's updates for it. Model: opus-5-5 --- internal/routewatch/prefixhandler.go | 20 ++++---- .../routewatch/prefixhandler_internal_test.go | 46 +++++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/internal/routewatch/prefixhandler.go b/internal/routewatch/prefixhandler.go index 329651e..e7f62f9 100644 --- a/internal/routewatch/prefixhandler.go +++ b/internal/routewatch/prefixhandler.go @@ -347,8 +347,19 @@ func (h *PrefixHandler) flushBatch() { lastUpdates := make(map[prefixAndPeer]prefixUpdate) + // Each prefix the batch names, announced or withdrawn, with the earliest + // timestamp among all of its updates, not only each peer's last one: the + // prefix tables store only first_seen, written when the prefix is new and + // never changed after. + prefixesToUpdate := make(map[string]time.Time) + for _, update := range batch { lastUpdates[prefixAndPeer{update.prefix, update.peer}] = update + + seen, exists := prefixesToUpdate[update.prefix] + if !exists || update.timestamp.Before(seen) { + prefixesToUpdate[update.prefix] = update.timestamp + } } // Collect routes to upsert and delete @@ -357,16 +368,7 @@ func (h *PrefixHandler) flushBatch() { routesToDelete []database.LiveRouteDeletion ) - // Collect unique prefixes to update - prefixesToUpdate := make(map[string]time.Time) - for _, update := range lastUpdates { - // Track prefix for both announcements and withdrawals - seen, exists := prefixesToUpdate[update.prefix] - if !exists || update.timestamp.After(seen) { - prefixesToUpdate[update.prefix] = update.timestamp - } - // An announcement is stored when its path gives an origin, or when it // ends in an AS set of several members: that route is stored with // origin 0 (none). An announcement with an empty path is not stored. diff --git a/internal/routewatch/prefixhandler_internal_test.go b/internal/routewatch/prefixhandler_internal_test.go index 12aeb9d..b9cce6b 100644 --- a/internal/routewatch/prefixhandler_internal_test.go +++ b/internal/routewatch/prefixhandler_internal_test.go @@ -83,6 +83,52 @@ func TestPrefixHandlerStoresPrefixesTheIPLookupFinds(t *testing.T) { } } +// TestPrefixHandlerStoresANewPrefixFirstSeenAtItsEarliestUpdate announces a +// prefix the database does not hold yet twice in one batch, the second time 5 +// seconds after the first, through the prefix handler into a real database. The +// prefix is stored first seen at the time of the first announcement, whether +// the two come from two peers or from one, and the IP lookup answers with that +// time. +func TestPrefixHandlerStoresANewPrefixFirstSeenAtItsEarliestUpdate( + t *testing.T, +) { + t.Parallel() + + const ( + otherPeerIP = "2001:db8:ffff::2" + secondAfter = 5 * time.Second + ) + + tests := []struct { + name string + firstPeer, secondPeer string + }{ + {name: "two peers", firstPeer: testPeerIP, secondPeer: otherPeerIP}, + {name: "one peer", firstPeer: testPeerIP, secondPeer: testPeerIP}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + first := peerAnnouncement(tt.firstPeer, testV4Prefix) + second := peerAnnouncement(tt.secondPeer, testV4Prefix) + second.ParsedTimestamp = first.ParsedTimestamp.Add(secondAfter) + db := handleBatches(t, [][]*ristypes.RISMessage{{first, second}}) + + info, err := db.GetIPInfo("192.0.2.1") + if err != nil { + t.Fatalf("GetIPInfo(192.0.2.1): %v", err) + } + + if !info.FirstSeen.Equal(first.ParsedTimestamp) { + t.Errorf("first_seen of %s = %s, want %s", testV4Prefix, + info.FirstSeen, first.ParsedTimestamp) + } + }) + } +} + // TestPrefixHandlerAppliesEachPeersLastUpdateInABatch runs updates for one // prefix through the prefix handler into a real database. All updates carry the // same timestamp, as updates within one second of the feed do. Within a batch,