check / check (push) Canceled after 0s
The prefix distribution was the last query on the stats path. It read one index entry per live route on every request, so on a large database it took the whole 4-second deadline and /api/v1/stats answered 500. The distribution now lives next to the in-memory counts: seeded once at startup from the same query, then kept exact by the live-route writes. A new route whose prefix had no live route adds one at its mask length; a delete that leaves a prefix with no live route takes one away. Each check is one lookup on the prefix index, made only for new and removed routes, inside the write. Model: opus-5-5
204 lines
5.9 KiB
Go
204 lines
5.9 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
)
|
|
|
|
// liveCounts holds the running row counts and the prefix distribution that the
|
|
// stats endpoints report. They are seeded once at startup from the tables and
|
|
// then adjusted on every write, so a stats read serves them from memory instead
|
|
// of running a query over the tables. The COUNT(*) scans (issue 27) and then
|
|
// the prefix distribution query (issue 30) each grew with the database until
|
|
// they took the whole request timeout and made /api/v1/stats return 500.
|
|
//
|
|
// A single mutex guards all fields so the stats reader takes a consistent
|
|
// snapshot at one instant and writers, which already run under the database
|
|
// write lock, adjust the counts after their transaction commits.
|
|
type liveCounts struct {
|
|
mu sync.RWMutex
|
|
asns int
|
|
prefixesV4 int
|
|
prefixesV6 int
|
|
peerings int
|
|
peers int
|
|
routesV4 int
|
|
routesV6 int
|
|
// The prefix distribution: for each mask length, the number of distinct
|
|
// prefixes that have at least one live route.
|
|
distributionV4 [ipv4Bits + 1]int
|
|
distributionV6 [ipv6Bits + 1]int
|
|
}
|
|
|
|
// seed sets every count to the value read from the tables at startup. It runs
|
|
// before any writer, so it needs no coordination with the adjust methods.
|
|
func (c *liveCounts) seed(asns, prefixesV4, prefixesV6, peerings, peers, routesV4, routesV6 int,
|
|
distributionV4, distributionV6 []PrefixDistribution) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
c.asns = asns
|
|
c.prefixesV4 = prefixesV4
|
|
c.prefixesV6 = prefixesV6
|
|
c.peerings = peerings
|
|
c.peers = peers
|
|
c.routesV4 = routesV4
|
|
c.routesV6 = routesV6
|
|
for _, entry := range distributionV4 {
|
|
addAtMaskLength(c.distributionV4[:], entry.MaskLength, entry.Count)
|
|
}
|
|
for _, entry := range distributionV6 {
|
|
addAtMaskLength(c.distributionV6[:], entry.MaskLength, entry.Count)
|
|
}
|
|
}
|
|
|
|
// addASNs adds n to the ASN count.
|
|
func (c *liveCounts) addASNs(n int) {
|
|
c.mu.Lock()
|
|
c.asns += n
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// addPrefixes adds to the IPv4 and IPv6 prefix counts.
|
|
func (c *liveCounts) addPrefixes(v4, v6 int) {
|
|
c.mu.Lock()
|
|
c.prefixesV4 += v4
|
|
c.prefixesV6 += v6
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// addPeerings adds n to the peering count.
|
|
func (c *liveCounts) addPeerings(n int) {
|
|
c.mu.Lock()
|
|
c.peerings += n
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// addPeers adds n to the BGP peer count.
|
|
func (c *liveCounts) addPeers(n int) {
|
|
c.mu.Lock()
|
|
c.peers += n
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// addRoutes adds to the IPv4 and IPv6 live-route counts. Deletions pass
|
|
// negative values.
|
|
func (c *liveCounts) addRoutes(v4, v6 int) {
|
|
c.mu.Lock()
|
|
c.routesV4 += v4
|
|
c.routesV6 += v6
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// addToDistribution adds n to the IPv4 and IPv6 prefix distributions once for
|
|
// each listed mask length. A write lists the mask lengths of the prefixes it
|
|
// gave their first live route with n = 1, and of the prefixes it left with no
|
|
// live route with n = -1.
|
|
func (c *liveCounts) addToDistribution(maskLengthsV4, maskLengthsV6 []int, n int) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
for _, maskLength := range maskLengthsV4 {
|
|
addAtMaskLength(c.distributionV4[:], maskLength, n)
|
|
}
|
|
for _, maskLength := range maskLengthsV6 {
|
|
addAtMaskLength(c.distributionV6[:], maskLength, n)
|
|
}
|
|
}
|
|
|
|
// addAtMaskLength adds n to counts[maskLength]. A mask length the array has no
|
|
// entry for is ignored, so a malformed route cannot crash the daemon.
|
|
func addAtMaskLength(counts []int, maskLength, n int) {
|
|
if maskLength >= 0 && maskLength < len(counts) {
|
|
counts[maskLength] += n
|
|
}
|
|
}
|
|
|
|
// fill copies the counts into a Stats, including the derived totals, under a
|
|
// single read lock so the reader sees one consistent snapshot.
|
|
func (c *liveCounts) fill(s *Stats) {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
|
|
s.ASNs = c.asns
|
|
s.IPv4Prefixes = c.prefixesV4
|
|
s.IPv6Prefixes = c.prefixesV6
|
|
s.Prefixes = c.prefixesV4 + c.prefixesV6
|
|
s.Peerings = c.peerings
|
|
s.Peers = c.peers
|
|
s.IPv4Routes = c.routesV4
|
|
s.IPv6Routes = c.routesV6
|
|
s.LiveRoutes = c.routesV4 + c.routesV6
|
|
s.IPv4PrefixDistribution = distributionList(c.distributionV4[:])
|
|
s.IPv6PrefixDistribution = distributionList(c.distributionV6[:])
|
|
}
|
|
|
|
// distributionList lists the mask lengths that have at least one prefix, in
|
|
// ascending order, the way the distribution query returns them.
|
|
func distributionList(counts []int) []PrefixDistribution {
|
|
var list []PrefixDistribution
|
|
for maskLength, count := range counts {
|
|
if count > 0 {
|
|
list = append(list, PrefixDistribution{MaskLength: maskLength, Count: count})
|
|
}
|
|
}
|
|
|
|
return list
|
|
}
|
|
|
|
// countRows returns the number of rows in the named table. It is used only at
|
|
// startup to seed the in-memory counters, so a full COUNT(*) is acceptable.
|
|
func (d *Database) countRows(ctx context.Context, table string) (int, error) {
|
|
var n int
|
|
// table is one of a fixed set of literals below, never external input.
|
|
if err := d.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM "+table).Scan(&n); err != nil {
|
|
return 0, fmt.Errorf("failed to count %s: %w", table, err)
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
// seedCounts reads the current row counts and prefix distribution from the
|
|
// tables into the in-memory counters. It runs once at startup, before the
|
|
// streamer begins writing.
|
|
func (d *Database) seedCounts(ctx context.Context) error {
|
|
asns, err := d.countRows(ctx, "asns")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prefixesV4, err := d.countRows(ctx, "prefixes_v4")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prefixesV6, err := d.countRows(ctx, "prefixes_v6")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
peerings, err := d.countRows(ctx, "peerings")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
peers, err := d.countRows(ctx, "bgp_peers")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
routesV4, err := d.countRows(ctx, "live_routes_v4")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
routesV6, err := d.countRows(ctx, "live_routes_v6")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
distributionV4, distributionV6, err := d.GetPrefixDistributionContext(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.counts.seed(asns, prefixesV4, prefixesV6, peerings, peers, routesV4, routesV6,
|
|
distributionV4, distributionV6)
|
|
|
|
return nil
|
|
}
|