Files
routewatch/internal/routewatch/app.go
T
clawbot 1c65656b58
check / check (push) Canceled after 0s
Remove the per-peer route tables (closes #122)
The routing table in memory now holds every route, so the two per-peer route
tables go, with every write, delete, count and read that served them. The
prefix handler applies each update to the table, writes the table's changes
every second, and stores each new prefix with its first-seen time. The peer
delete, its retry on the next flush and the updates held for it go too. The
types and errors of the server's answers move to internal/server.

The image's memory settings fit a full table of about 1 GB: GOMEMLIMIT of
2.5 GiB, a 512 MiB page cache for the writing connection, and SQLite heap
limits of 768 MiB and 1 GiB. The README describes the new table, its
database copy and the memory budget.

Model: opus-5-5
2026-10-27 17:22:32 +01:00

306 lines
8.6 KiB
Go

// Package routewatch contains the primary RouteWatch type that represents a
// running instance of the application and contains pointers to its core
// dependencies, and is responsible for initialization. It also contains the
// ASN WHOIS fetcher for background updates and the database maintainer for
// background maintenance tasks. The package also holds the handlers that
// write the feed's updates to the database in batches, and the flush loop
// that the AS, peer and prefix handlers share.
package routewatch
import (
"context"
"errors"
"fmt"
"path/filepath"
"sync"
"time"
"git.eeqj.de/sneak/routewatch/internal/blocklist"
"git.eeqj.de/sneak/routewatch/internal/config"
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/logger"
"git.eeqj.de/sneak/routewatch/internal/metrics"
"git.eeqj.de/sneak/routewatch/internal/routetable"
"git.eeqj.de/sneak/routewatch/internal/server"
"git.eeqj.de/sneak/routewatch/internal/streamer"
"go.uber.org/fx"
)
// errNonBatchedNotImplemented is returned by Run when batched database writes
// are disabled, since only the batched handlers exist.
var errNonBatchedNotImplemented = errors.New("non-batched handlers not implemented")
// Dependencies contains all dependencies for RouteWatch. Table is the routing
// table read from the database at startup, which the server reads too.
type Dependencies struct {
fx.In
DB database.Store
Table *routetable.Table
Streamer *streamer.Streamer
Server *server.Server
Logger *logger.Logger
Config *config.Config
}
// RouteWatch represents the main application instance
type RouteWatch struct {
db database.Store
table *routetable.Table
streamer *streamer.Streamer
server *server.Server
logger *logger.Logger
maxRuntime time.Duration
shutdown bool
mu sync.Mutex
config *config.Config
dbHandler *ASHandler
peerHandler *PeerHandler
prefixHandler *PrefixHandler
peeringHandler *PeeringHandler
asnFetcher *ASNFetcher
dbMaintainer *DBMaintainer
blocklists *blocklist.Blocklists
}
// blocklistsDirName is the directory in the state directory that holds the
// blocklist copies.
const blocklistsDirName = "blocklists"
// New creates a new RouteWatch instance
func New(deps Dependencies) *RouteWatch {
rw := &RouteWatch{
db: deps.DB,
table: deps.Table,
streamer: deps.Streamer,
server: deps.Server,
logger: deps.Logger,
maxRuntime: deps.Config.MaxRuntime,
config: deps.Config,
}
return rw
}
// Run starts the RouteWatch application
func (rw *RouteWatch) Run(ctx context.Context) error {
rw.logger.Info("Starting RouteWatch")
// Apply runtime limit if specified
if rw.maxRuntime > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, rw.maxRuntime)
defer cancel()
rw.logger.Info("Running with time limit", "max_runtime", rw.maxRuntime)
}
// Register database handler to process BGP UPDATE messages
if rw.config.EnableBatchedDatabaseWrites {
rw.logger.Info("Using batched database handlers for improved performance")
// ASHandler maintains the asns table
rw.dbHandler = NewASHandler(rw.db, rw.logger)
rw.streamer.RegisterHandler(rw.dbHandler)
// PeerHandler maintains the bgp_peers table
rw.peerHandler = NewPeerHandler(rw.db, rw.logger)
rw.streamer.RegisterHandler(rw.peerHandler)
// PrefixHandler keeps the routing table in memory current, and writes
// its changes to the routes table and new prefixes to the prefix
// tables
rw.prefixHandler = NewPrefixHandler(rw.db, rw.logger, rw.table)
rw.prefixHandler.SetMetricsTracker(rw.streamer.GetMetricsTracker())
rw.streamer.RegisterHandler(rw.prefixHandler)
// PeeringHandler maintains the asn_peerings table
rw.peeringHandler = NewPeeringHandler(rw.db, rw.logger)
rw.streamer.RegisterHandler(rw.peeringHandler)
} else {
// Non-batched handlers not implemented yet
rw.logger.Error("Non-batched handlers not implemented")
return errNonBatchedNotImplemented
}
// Start streaming
err := rw.streamer.Start(ctx)
if err != nil {
return err
}
// Start ASN WHOIS fetcher for background updates. The HTTP handlers read
// the server's fetcher without a lock, so it is set before they run.
rw.asnFetcher = NewASNFetcher(rw.db, rw.logger.Logger)
rw.asnFetcher.Start(ctx)
rw.server.SetASNFetcher(rw.asnFetcher)
// Load the blocklist copies from disk and start downloading the lists.
// The HTTP handlers read the server's blocklists without a lock, so they
// are set before the handlers run.
blocklists := blocklist.New(blocklist.Params{
Dir: filepath.Join(rw.config.StateDir, blocklistsDirName),
Lists: blocklist.Lists(),
Logger: rw.logger.Logger,
})
blocklists.Start(ctx)
rw.blocklists = blocklists
rw.server.SetBlocklists(blocklists)
// Start HTTP server
err = rw.server.Start(ctx)
if err != nil {
return err
}
// Start database maintenance goroutine
rw.dbMaintainer = NewDBMaintainer(rw.db, rw.logger.Logger)
rw.dbMaintainer.Start(ctx)
// Wait for context cancellation
<-ctx.Done()
return nil
}
// Shutdown stops all services in order: the streamer, the handlers, which
// write what they hold, the WHOIS fetcher, the database maintainer, the
// blocklist downloads and the HTTP server, and last the database, which it
// checkpoints and closes. Each stage waits at most until ctx ends, so
// Shutdown returns soon after: a handler logs what it could not write by
// then, and the HTTP server closes the connections of requests still
// running.
func (rw *RouteWatch) Shutdown(ctx context.Context) {
rw.mu.Lock()
if rw.shutdown {
rw.mu.Unlock()
return
}
rw.shutdown = true
rw.mu.Unlock()
// Stop the streamer first: it returns once the handlers have been handed
// every message it queued, or when ctx ends. Then each handler writes
// what it holds.
rw.streamer.Stop(ctx)
if rw.dbHandler != nil {
rw.logger.Info("Flushing database handler")
rw.dbHandler.Stop(ctx)
}
if rw.peerHandler != nil {
rw.logger.Info("Flushing peer handler")
rw.peerHandler.Stop(ctx)
}
if rw.prefixHandler != nil {
rw.logger.Info("Flushing prefix handler")
rw.prefixHandler.Stop(ctx)
}
if rw.peeringHandler != nil {
rw.logger.Info("Flushing peering handler")
rw.peeringHandler.Stop(ctx)
}
// Stop ASN WHOIS fetcher
if rw.asnFetcher != nil {
rw.asnFetcher.Stop(ctx)
}
// Stop database maintainer
if rw.dbMaintainer != nil {
rw.dbMaintainer.Stop(ctx)
}
if rw.blocklists != nil {
rw.blocklists.Stop(ctx)
}
err := rw.server.Stop(ctx)
if err != nil {
rw.logger.Error("Stopped the HTTP server before every request finished",
"error", err)
}
rw.closeDatabase(ctx)
// Log final metrics
metrics := rw.streamer.GetMetrics()
rw.logger.Info("Final metrics",
"total_messages", metrics.TotalMessages,
"total_bytes", metrics.TotalBytes,
"messages_per_sec", metrics.MessagesPerSec,
"bits_per_sec", metrics.BitsPerSec,
"duration", time.Since(metrics.ConnectedSince),
)
}
// closeDatabase checkpoints and closes the database, unless ctx has ended: a
// write may then still be running, and both would wait for it. SQLite rolls
// back a write cut off by the exit when the database next opens.
func (rw *RouteWatch) closeDatabase(ctx context.Context) {
if ctx.Err() != nil {
rw.logger.Error("Stop deadline passed; the database is not closed")
return
}
err := rw.db.Checkpoint(ctx)
if err != nil {
rw.logger.Error("Failed to checkpoint the database", "error", err)
}
err = rw.db.Close()
if err != nil {
rw.logger.Error("Failed to close the database", "error", err)
}
}
// readRouteTable reads the routing table from the database and logs how many
// prefix and origin AS pairs and peers it holds and how long the read took.
// fx calls it once, to build the server and RouteWatch, before the streamer
// connects, so the prefix handler applies the feed's updates to all of it. No
// start timeout limits the read: fx builds them before it runs a start hook.
func readRouteTable(
db database.Store, logger *logger.Logger,
) (*routetable.Table, error) {
start := time.Now()
table, err := db.ReadRouteTable(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to read the routing table: %w", err)
}
counts := table.Counts()
logger.Info("Read the routing table", "pairs", counts.Pairs,
"peers", counts.Peers, "duration_ms", time.Since(start).Milliseconds())
return table, nil
}
// getModule provides all fx dependencies
func getModule() []fx.Option {
return []fx.Option{
fx.Provide(
logger.New,
config.New,
metrics.New,
fx.Annotate(
database.New,
fx.As(new(database.Store)),
),
readRouteTable,
streamer.New,
server.New,
New,
),
}
}