check / check (push) Canceled after 0s
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
306 lines
8.6 KiB
Go
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,
|
|
),
|
|
}
|
|
}
|