Files
routewatch/internal/routetable/overview.go
T
clawbot 9bd0e91a3b
check / check (push) Canceled after 0s
Let the lock go for as long as it was held during Overview's walk (closes #226)
The home page's cards are worked out once a minute by Overview, which
walks every prefix of the routing table under the table's read lock.
After each batch of 10,000 prefixes, the walk let the lock go and took
it back at once. The prefix handler, waiting for the write lock, then got
in only once per batch. During a walk, its queue could fill, and the
streamer then dropped updates for it.

The walk now lets the lock go for as long as it held the lock for the
batch, as PeerDown does. A test applies another peer's 1,000
announcements while Overview walks a table with more prefixes than five
of the walk's batches.

Model: opus-5-5
2026-11-02 08:00:00 +01:00

265 lines
7.8 KiB
Go

package routetable
import (
"cmp"
"net/netip"
"slices"
"time"
"git.eeqj.de/sneak/routewatch/internal/bogon"
"git.eeqj.de/sneak/routewatch/internal/ristypes"
)
// Overview holds figures about the whole table. Working them out takes a walk
// over every prefix, so a caller asks for them now and then and keeps them.
type Overview struct {
// NewestRoute and OldestRoute are the newest and the oldest kept routes,
// or nil when the table keeps none. Of several with the same time, it is
// any one of them.
NewestRoute *PrefixRoute
OldestRoute *PrefixRoute
// MostPrefixesASN is the AS that originates the most prefixes, and
// MostPrefixes is how many it originates. Of several with as many, it is
// the lowest AS number. AS 0, no known origin, counts neither here nor
// in the single-prefix figures.
MostPrefixesASN int
MostPrefixes int
// SinglePrefixASes counts the ASes that originate one prefix.
SinglePrefixASes int
// NewestSinglePrefix is the newest kept route of those ASes, or nil when
// they keep none.
NewestSinglePrefix *PrefixRoute
// Bogons are the bogon announcements, in prefix order: at most the
// number Overview is asked for. BogonCount counts them all.
Bogons []Bogon
BogonCount int
// MultipleOrigins are the prefixes that peers carry with more than one
// origin AS, in prefix order: at most the number Overview is asked for.
// MultipleOriginCount counts them all.
MultipleOrigins []MultipleOriginPrefix
MultipleOriginCount int
}
// PrefixRoute is the time of a kept route, with its prefix and origin AS.
type PrefixRoute struct {
Prefix netip.Prefix
OriginASN int
Time time.Time
}
// Bogon is a bogon announcement: a prefix and origin AS pair whose prefix or
// origin AS is reserved, as package bogon tells.
type Bogon struct {
Prefix netip.Prefix
OriginASN int
// Peers is how many peers carry the prefix with this origin AS.
Peers int
// Reasons say why the prefix, the origin AS or both are reserved.
Reasons []string
}
// MultipleOriginPrefix is a prefix that peers carry with more than one origin
// AS.
type MultipleOriginPrefix struct {
Prefix netip.Prefix
// Origins are its origin ASes, in AS number order.
Origins []OriginPeers
}
// OriginPeers is one origin AS of a prefix, and how many peers carry the prefix
// with it.
type OriginPeers struct {
ASN int
Peers int
}
// Overview works out the table's overview. It lists at most maxListed bogon
// announcements and at most maxListed prefixes with more than one origin AS:
// the first in prefix order. It visits every prefix in the table, in batches
// of walkBatch. After each batch, it lets the table's lock go for as long as
// it held the lock for the batch.
func (t *Table) Overview(maxListed int) Overview {
var overview Overview
t.walkPrefixes(func(prefix netip.Prefix, origins []*origin) {
overview.noteKeptRoutes(prefix, origins)
overview.noteBogons(prefix, origins)
overview.noteMultipleOrigins(prefix, origins)
})
t.countPrefixesPerOrigin(&overview)
slices.SortFunc(overview.Bogons, func(a, b Bogon) int {
return cmp.Or(a.Prefix.Compare(b.Prefix),
cmp.Compare(a.OriginASN, b.OriginASN))
})
overview.BogonCount = len(overview.Bogons)
overview.Bogons = overview.Bogons[:min(maxListed, overview.BogonCount)]
slices.SortFunc(overview.MultipleOrigins, func(a, b MultipleOriginPrefix) int {
return a.Prefix.Compare(b.Prefix)
})
overview.MultipleOriginCount = len(overview.MultipleOrigins)
overview.MultipleOrigins =
overview.MultipleOrigins[:min(maxListed, overview.MultipleOriginCount)]
return overview
}
// walkPrefixes calls visit with every prefix of the table and its origins. It
// holds the read lock for walkBatch prefixes at a time. After each batch, it
// lets the table's lock go for as long as it held the lock for the batch.
// During a walk over many batches, updates can then take the lock about half
// of the time. The walk may or may not visit a prefix that is added or removed
// while the lock is let go. Go allows this in a range over a map that changes.
// visit must not keep origins once it returns.
func (t *Table) walkPrefixes(visit func(prefix netip.Prefix, origins []*origin)) {
t.mu.RLock()
defer t.mu.RUnlock()
visited := 0
batchStart := time.Now()
for _, f := range []*family{&t.ipv4, &t.ipv6} {
for _, prefixes := range f.byLength {
for prefix, origins := range prefixes {
visit(prefix, origins)
visited++
if visited%walkBatch == 0 {
held := time.Since(batchStart)
t.mu.RUnlock()
time.Sleep(held)
t.mu.RLock()
batchStart = time.Now()
}
}
}
}
}
// countPrefixesPerOrigin works out the overview's figures of how many prefixes
// each AS originates. It holds the read lock once, for one look at each origin
// AS, of which there are far fewer than prefixes.
func (t *Table) countPrefixesPerOrigin(overview *Overview) {
t.mu.RLock()
defer t.mu.RUnlock()
for asn, prefixes := range t.byOrigin {
if asn == ristypes.NoOrigin {
continue
}
count := len(prefixes)
if count > overview.MostPrefixes ||
(count == overview.MostPrefixes && asn < overview.MostPrefixesASN) {
overview.MostPrefixesASN = asn
overview.MostPrefixes = count
}
if count == 1 {
overview.SinglePrefixASes++
overview.noteSinglePrefix(asn, prefixes)
}
}
}
// noteSinglePrefix makes the newest kept route of asn, whose one prefix is in
// prefixes, the overview's newest single-prefix route when it is newer.
func (overview *Overview) noteSinglePrefix(
asn int, prefixes map[netip.Prefix]*origin,
) {
for prefix, o := range prefixes {
newest := o.newestTime()
if newest == nil {
continue
}
if overview.NewestSinglePrefix == nil ||
newest.After(overview.NewestSinglePrefix.Time) {
overview.NewestSinglePrefix = &PrefixRoute{
Prefix: prefix, OriginASN: asn, Time: *newest,
}
}
}
}
// noteKeptRoutes makes the newest and the oldest kept routes of prefix's
// origins the overview's newest and oldest routes when they are newer or older.
func (overview *Overview) noteKeptRoutes(prefix netip.Prefix, origins []*origin) {
for _, o := range origins {
if len(o.routes) == 0 {
continue
}
newest := o.routes[0].time
if overview.NewestRoute == nil || newest.After(overview.NewestRoute.Time) {
overview.NewestRoute = &PrefixRoute{
Prefix: prefix, OriginASN: o.asn, Time: newest,
}
}
oldest := o.routes[len(o.routes)-1].time
if overview.OldestRoute == nil || oldest.Before(overview.OldestRoute.Time) {
overview.OldestRoute = &PrefixRoute{
Prefix: prefix, OriginASN: o.asn, Time: oldest,
}
}
}
}
// noteBogons adds each origin of prefix to the overview's bogon announcements
// when the prefix or the origin AS is reserved.
func (overview *Overview) noteBogons(prefix netip.Prefix, origins []*origin) {
prefixReason, prefixReserved := bogon.Prefix(prefix)
for _, o := range origins {
asnReason, asnReserved := bogon.ASN(o.asn)
if !prefixReserved && !asnReserved {
continue
}
announcement := Bogon{
Prefix: prefix, OriginASN: o.asn, Peers: int(o.peers.Count()),
}
if prefixReserved {
announcement.Reasons = append(announcement.Reasons, prefixReason)
}
if asnReserved {
announcement.Reasons = append(announcement.Reasons, asnReason)
}
overview.Bogons = append(overview.Bogons, announcement)
}
}
// noteMultipleOrigins adds prefix to the overview's prefixes with more than one
// origin AS when it has more than one.
func (overview *Overview) noteMultipleOrigins(
prefix netip.Prefix, origins []*origin,
) {
if len(origins) <= 1 {
return
}
entry := MultipleOriginPrefix{
Prefix: prefix,
Origins: make([]OriginPeers, 0, len(origins)),
}
for _, o := range origins {
entry.Origins = append(entry.Origins,
OriginPeers{ASN: o.asn, Peers: int(o.peers.Count())})
}
overview.MultipleOrigins = append(overview.MultipleOrigins, entry)
}