check / check (push) Canceled after 0s
Takes .golangci.yml, .dockerignore, .gitignore, .editorconfig, the workflow and REPO_POLICIES.md from sneak/prompts 0653579, with this repository's own entries after the shared content. Lint (golangci-lint v2.14.0) and test are Dockerfile phases that script/lint and script/test build alone with a new CACHEBUST value, keeping Go's and golangci-lint's caches; the image build depends on both. make fmt and make fmt-check also format the markdown with prettier, pinned in yarn.lock. The agent guidance moves to AGENTS.md. The Go code is fixed for what the new configuration finds, and the AS and prefix detail reads now return the rows' error after their loops. Model: opus-5-5
226 lines
5.7 KiB
Go
226 lines
5.7 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.
|
|
package routewatch
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
"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/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
|
|
type Dependencies struct {
|
|
fx.In
|
|
|
|
DB database.Store
|
|
Streamer *streamer.Streamer
|
|
Server *server.Server
|
|
Logger *logger.Logger
|
|
Config *config.Config
|
|
}
|
|
|
|
// RouteWatch represents the main application instance
|
|
type RouteWatch struct {
|
|
db database.Store
|
|
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
|
|
}
|
|
|
|
// New creates a new RouteWatch instance
|
|
func New(deps Dependencies) *RouteWatch {
|
|
rw := &RouteWatch{
|
|
db: deps.DB,
|
|
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 maintains the prefixes and live_routes tables
|
|
rw.prefixHandler = NewPrefixHandler(rw.db, rw.logger)
|
|
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
|
|
}
|
|
|
|
// No longer need routing table handler - PrefixHandler maintains live_routes table
|
|
|
|
// Start streaming
|
|
err := rw.streamer.Start(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start HTTP server
|
|
err = rw.server.Start(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start ASN WHOIS fetcher for background updates
|
|
rw.asnFetcher = NewASNFetcher(rw.db, rw.logger.Logger)
|
|
rw.asnFetcher.Start(ctx)
|
|
rw.server.SetASNFetcher(rw.asnFetcher)
|
|
|
|
// 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 performs graceful shutdown of all services
|
|
func (rw *RouteWatch) Shutdown(ctx context.Context) {
|
|
rw.mu.Lock()
|
|
if rw.shutdown {
|
|
rw.mu.Unlock()
|
|
|
|
return
|
|
}
|
|
|
|
rw.shutdown = true
|
|
rw.mu.Unlock()
|
|
|
|
// Stop batched handlers first to flush remaining batches
|
|
if rw.dbHandler != nil {
|
|
rw.logger.Info("Flushing database handler")
|
|
rw.dbHandler.Stop()
|
|
}
|
|
|
|
if rw.peerHandler != nil {
|
|
rw.logger.Info("Flushing peer handler")
|
|
rw.peerHandler.Stop()
|
|
}
|
|
|
|
if rw.prefixHandler != nil {
|
|
rw.logger.Info("Flushing prefix handler")
|
|
rw.prefixHandler.Stop()
|
|
}
|
|
|
|
if rw.peeringHandler != nil {
|
|
rw.logger.Info("Flushing peering handler")
|
|
rw.peeringHandler.Stop()
|
|
}
|
|
|
|
// Stop ASN WHOIS fetcher
|
|
if rw.asnFetcher != nil {
|
|
rw.asnFetcher.Stop()
|
|
}
|
|
|
|
// Stop database maintainer
|
|
if rw.dbMaintainer != nil {
|
|
rw.dbMaintainer.Stop()
|
|
}
|
|
|
|
// Stop services
|
|
rw.streamer.Stop()
|
|
|
|
// Stop HTTP server with a timeout of its own, however much of ctx is left
|
|
const serverStopTimeout = 5 * time.Second
|
|
|
|
stopCtx, cancel := context.WithTimeout(
|
|
context.WithoutCancel(ctx), serverStopTimeout)
|
|
defer cancel()
|
|
|
|
err := rw.server.Stop(stopCtx)
|
|
if err != nil {
|
|
rw.logger.Error("Failed to stop HTTP server gracefully", "error", err)
|
|
}
|
|
|
|
// 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),
|
|
)
|
|
}
|
|
|
|
// 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)),
|
|
),
|
|
streamer.New,
|
|
server.New,
|
|
New,
|
|
),
|
|
}
|
|
}
|