Record origin changes and show them on the prefix page (closes #161)
check / check (push) Successful in 4m6s

The routing table records each prefix and origin AS pair's origin changes:
when it gains its first peer and when it loses its last. Each row holds the
prefix, the origin AS, announced or withdrawn, the peer count and the time.
The prefix handler writes them to a new origin_changes table, and the database
maintainer prunes each prefix to its newest 500 rows and 72 hours. The prefix
page lists them newest first, at most 5,000.

Model: deepseek-v4-pro
This commit is contained in:
2026-11-05 04:07:39 +00:00
parent 15bf20a894
commit 347e1456a2
16 changed files with 1127 additions and 27 deletions
+27 -1
View File
@@ -2,7 +2,9 @@
// and origin AS, it keeps the peers whose current route to the prefix has that
// origin AS. It also keeps the two newest of those routes in full. It applies
// announcements, withdrawals and peers' sessions going down. It answers the
// route reads that the pages and the API need.
// route reads that the pages and the API need. It also records each prefix and
// origin AS pair's origin changes: when it gains its first peer and when it
// loses its last.
package routetable
import (
@@ -91,6 +93,9 @@ type Params struct {
// peer not in it gets the next number above all of them the first time it
// announces a route.
PeerNumbers map[string]uint
// Now returns the time RouteWatch applies a change at. It is time.Now
// unless a test gives one.
Now func() time.Time
}
// Table is the routing table. Its methods are safe for concurrent use.
@@ -126,11 +131,24 @@ type Table struct {
// originsWithoutKeptRoute counts the origins that peers carry but that
// keep no route.
originsWithoutKeptRoute int
// now returns the time RouteWatch applies a change at.
now func() time.Time
// originChanges holds the origin changes that wait to be written, oldest
// first. originChangesDropped counts those dropped since the last
// TakeOriginChanges.
originChanges []OriginChange
originChangesDropped int
}
// New returns an empty table that numbers new peers on from
// params.PeerNumbers.
func New(params Params) *Table {
now := params.Now
if now == nil {
now = time.Now
}
t := &Table{
ipv4: newFamily(ipv4Bits, minLookupMaskLengthV4),
ipv6: newFamily(ipv6Bits, minLookupMaskLengthV6),
@@ -139,6 +157,7 @@ func New(params Params) *Table {
peerAddresses: make(map[uint]string, len(params.PeerNumbers)),
newPeers: make(map[string]uint),
changed: make(map[pair]struct{}),
now: now,
keptTimes: btree.NewG(keptTimesDegree, func(a, b keptTime) bool {
return a.time.Before(b.time)
}),
@@ -469,6 +488,8 @@ func (t *Table) join(prefix netip.Prefix, o *origin, r route) {
wasWithoutKeptRoute := o.withoutKeptRoute()
joined := !o.peers.Test(r.peer)
firstPeer := joined && o.peers.None()
if joined {
o.peers.Set(r.peer)
@@ -482,6 +503,10 @@ func (t *Table) join(prefix netip.Prefix, o *origin, r route) {
if joined || dropped || kept {
t.recordChange(prefix, o, wasWithoutKeptRoute)
}
if firstPeer {
t.recordOriginChange(prefix, o.asn, true, t.prefixPeerCount(prefix))
}
}
// leave takes peer out of the peer set of o, an origin of prefix, and drops
@@ -549,6 +574,7 @@ func (t *Table) recordChange(
t.countOriginWithoutKeptRoute(o, wasWithoutKeptRoute)
if o.peers.None() {
t.recordOriginChange(prefix, o.asn, false, t.prefixPeerCount(prefix))
t.removeOrigin(prefix, o)
}
}