Show figures of the routing table on the home page (closes #154)
check / check (push) Waiting to run
check / check (push) Waiting to run
The home page gets cards for the newest and oldest kept route, the newest and oldest updated AS, the AS with the most prefixes, the ASes with a single prefix and fifteen well-known networks. Two more cards count the bogon announcements and the prefixes with several origin ASes. They link to the new pages /bogons and /multiple-origins, which list up to 1,000 entries each. The new package internal/bogon tells reserved prefixes and AS numbers, from the IANA special-purpose and address space registries. Table.Overview walks the table and lets the read lock go every 10,000 prefixes. The server works the cards out once a minute and serves that copy. A new index on asns(last_seen) finds the newest and oldest updated AS. Model: opus-5-5
This commit is contained in:
@@ -972,6 +972,34 @@ func (d *Database) GetAS(ctx context.Context, asn int) (*ASN, bool, error) {
|
||||
return &asnInfo, true, nil
|
||||
}
|
||||
|
||||
// The queries of GetLastSeenASes. The index on last_seen answers each without
|
||||
// reading the whole table.
|
||||
const (
|
||||
newestLastSeenASQuery = `
|
||||
SELECT asn, handle, description, first_seen, last_seen
|
||||
FROM asns ORDER BY last_seen DESC LIMIT 1`
|
||||
oldestLastSeenASQuery = `
|
||||
SELECT asn, handle, description, first_seen, last_seen
|
||||
FROM asns ORDER BY last_seen ASC LIMIT 1`
|
||||
)
|
||||
|
||||
// GetLastSeenASes returns two ASes of the asns table, with their handles,
|
||||
// descriptions and seen times: the one last seen most recently, and the one
|
||||
// last seen longest ago. Both are nil when the table holds no AS.
|
||||
func (d *Database) GetLastSeenASes(ctx context.Context) (*ASN, *ASN, error) {
|
||||
newest, found, err := d.readOneAS(ctx, newestLastSeenASQuery)
|
||||
if err != nil || !found {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
oldest, _, err := d.readOneAS(ctx, oldestLastSeenASQuery)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return newest, oldest, nil
|
||||
}
|
||||
|
||||
// asWHOISQuery reads the handle, description and WHOIS details of one AS, and
|
||||
// when the WHOIS details were fetched.
|
||||
const asWHOISQuery = `
|
||||
@@ -1379,6 +1407,29 @@ func (d *Database) Ping(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// readOneAS returns the AS that query reads, and whether it read one.
|
||||
func (d *Database) readOneAS(ctx context.Context, query string) (*ASN, bool, error) {
|
||||
var (
|
||||
asnInfo ASN
|
||||
handle, description sql.NullString
|
||||
)
|
||||
|
||||
err := d.db.QueryRowContext(ctx, query).Scan(&asnInfo.ASN, &handle,
|
||||
&description, &asnInfo.FirstSeen, &asnInfo.LastSeen)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to query AS by last seen: %w", err)
|
||||
}
|
||||
|
||||
asnInfo.Handle = handle.String
|
||||
asnInfo.Description = description.String
|
||||
|
||||
return &asnInfo, true, nil
|
||||
}
|
||||
|
||||
// truncateWAL runs the TRUNCATE checkpoint of Checkpoint on a WAL file of
|
||||
// walBytes, with the writing connection's busy timeout lowered to
|
||||
// truncateBusyTimeoutMs for it, and logs how long it took. The caller holds the
|
||||
|
||||
@@ -1003,6 +1003,62 @@ func TestGetASReadsTheRecordOrTheBuiltInList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetLastSeenASes stores three ASes, each last seen at another time, and
|
||||
// expects the one last seen most recently and the one last seen longest ago.
|
||||
// An empty asns table gives neither.
|
||||
func TestGetLastSeenASes(t *testing.T) {
|
||||
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() }()
|
||||
|
||||
newest, oldest, err := db.GetLastSeenASes(t.Context())
|
||||
if err != nil || newest != nil || oldest != nil {
|
||||
t.Fatalf("GetLastSeenASes() on an empty table = %+v, %+v, %v; want "+
|
||||
"neither", newest, oldest, err)
|
||||
}
|
||||
|
||||
const (
|
||||
newestASN = 13335
|
||||
middleASN = 15169
|
||||
oldestASN = 3
|
||||
)
|
||||
|
||||
start := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
|
||||
|
||||
err = db.GetOrCreateASNBatch(map[int]SeenTimes{
|
||||
middleASN: {First: start, Last: start.Add(time.Hour)},
|
||||
newestASN: {First: start, Last: start.Add(2 * time.Hour)},
|
||||
oldestASN: {First: start, Last: start.Add(time.Minute)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("GetOrCreateASNBatch: %v", err)
|
||||
}
|
||||
|
||||
newest, oldest, err = db.GetLastSeenASes(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("GetLastSeenASes: %v", err)
|
||||
}
|
||||
|
||||
listed, _ := asinfo.Get(newestASN)
|
||||
|
||||
if newest == nil || newest.ASN != newestASN ||
|
||||
!newest.LastSeen.Equal(start.Add(2*time.Hour)) ||
|
||||
newest.Description != listed.Description {
|
||||
t.Errorf("newest = %+v, want AS%d (%s) last seen %v", newest, newestASN,
|
||||
listed.Description, start.Add(2*time.Hour))
|
||||
}
|
||||
|
||||
if oldest == nil || oldest.ASN != oldestASN ||
|
||||
!oldest.LastSeen.Equal(start.Add(time.Minute)) {
|
||||
t.Errorf("oldest = %+v, want AS%d last seen %v", oldest, oldestASN,
|
||||
start.Add(time.Minute))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetASWHOISReadsTheStoredDetails stores an AS with WHOIS details and asks
|
||||
// GetASWHOIS for it and for an AS the asns table does not hold. The first gets
|
||||
// its stored handle, description and details, and the time they were fetched.
|
||||
|
||||
@@ -62,6 +62,7 @@ type Store interface {
|
||||
|
||||
// AS and prefix detail operations
|
||||
GetAS(ctx context.Context, asn int) (*ASN, bool, error)
|
||||
GetLastSeenASes(ctx context.Context) (*ASN, *ASN, error)
|
||||
GetASWHOIS(ctx context.Context, asn int) (*ASN, error)
|
||||
GetPrefixFirstSeen(ctx context.Context, prefix netip.Prefix) (time.Time, error)
|
||||
GetASPeers(asn int) ([]ASPeer, error)
|
||||
|
||||
@@ -83,6 +83,9 @@ CREATE INDEX IF NOT EXISTS idx_peerings_lookup ON peerings(as_a, as_b);
|
||||
-- Indexes for asns table
|
||||
CREATE INDEX IF NOT EXISTS idx_asns_asn ON asns(asn);
|
||||
CREATE INDEX IF NOT EXISTS idx_asns_whois_updated_at ON asns(whois_updated_at);
|
||||
-- Finds the AS last seen most recently and the one last seen longest ago, for
|
||||
-- the home page.
|
||||
CREATE INDEX IF NOT EXISTS idx_asns_last_seen ON asns(last_seen);
|
||||
|
||||
-- Indexes for bgp_peers table
|
||||
CREATE INDEX IF NOT EXISTS idx_bgp_peers_asn ON bgp_peers(peer_asn);
|
||||
|
||||
Reference in New Issue
Block a user