Replace live_routes database table with in-memory routing table
- Remove live_routes table from SQL schema and all related indexes - Create new internal/routingtable package with thread-safe RoutingTable - Implement RouteKey-based indexing with secondary indexes for efficient lookups - Add RoutingTableHandler to manage in-memory routes separately from database - Update DatabaseHandler to only handle persistent database operations - Wire up RoutingTable through fx dependency injection - Update server to get live route count from routing table instead of database - Remove LiveRoutes field from database.Stats struct - Update tests to work with new architecture
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
||||
"git.eeqj.de/sneak/routewatch/internal/routingtable"
|
||||
"git.eeqj.de/sneak/routewatch/internal/server"
|
||||
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
||||
|
||||
@@ -33,30 +34,33 @@ func NewConfig() Config {
|
||||
type Dependencies struct {
|
||||
fx.In
|
||||
|
||||
DB database.Store
|
||||
Streamer *streamer.Streamer
|
||||
Server *server.Server
|
||||
Logger *slog.Logger
|
||||
Config Config `optional:"true"`
|
||||
DB database.Store
|
||||
RoutingTable *routingtable.RoutingTable
|
||||
Streamer *streamer.Streamer
|
||||
Server *server.Server
|
||||
Logger *slog.Logger
|
||||
Config Config `optional:"true"`
|
||||
}
|
||||
|
||||
// RouteWatch represents the main application instance
|
||||
type RouteWatch struct {
|
||||
db database.Store
|
||||
streamer *streamer.Streamer
|
||||
server *server.Server
|
||||
logger *slog.Logger
|
||||
maxRuntime time.Duration
|
||||
db database.Store
|
||||
routingTable *routingtable.RoutingTable
|
||||
streamer *streamer.Streamer
|
||||
server *server.Server
|
||||
logger *slog.Logger
|
||||
maxRuntime time.Duration
|
||||
}
|
||||
|
||||
// New creates a new RouteWatch instance
|
||||
func New(deps Dependencies) *RouteWatch {
|
||||
return &RouteWatch{
|
||||
db: deps.DB,
|
||||
streamer: deps.Streamer,
|
||||
server: deps.Server,
|
||||
logger: deps.Logger,
|
||||
maxRuntime: deps.Config.MaxRuntime,
|
||||
db: deps.DB,
|
||||
routingTable: deps.RoutingTable,
|
||||
streamer: deps.Streamer,
|
||||
server: deps.Server,
|
||||
logger: deps.Logger,
|
||||
maxRuntime: deps.Config.MaxRuntime,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +80,10 @@ func (rw *RouteWatch) Run(ctx context.Context) error {
|
||||
dbHandler := NewDatabaseHandler(rw.db, rw.logger)
|
||||
rw.streamer.RegisterHandler(dbHandler)
|
||||
|
||||
// Register routing table handler to maintain in-memory routing table
|
||||
rtHandler := NewRoutingTableHandler(rw.routingTable, rw.logger)
|
||||
rw.streamer.RegisterHandler(rtHandler)
|
||||
|
||||
// Register peer tracking handler to track all peers
|
||||
peerHandler := NewPeerHandler(rw.db, rw.logger)
|
||||
rw.streamer.RegisterHandler(peerHandler)
|
||||
@@ -154,8 +162,12 @@ func getModule() fx.Option {
|
||||
},
|
||||
fx.As(new(database.Store)),
|
||||
),
|
||||
routingtable.New,
|
||||
streamer.New,
|
||||
server.New,
|
||||
fx.Annotate(
|
||||
server.New,
|
||||
fx.ParamTags(``, ``, ``, ``),
|
||||
),
|
||||
New,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/metrics"
|
||||
"git.eeqj.de/sneak/routewatch/internal/routingtable"
|
||||
"git.eeqj.de/sneak/routewatch/internal/server"
|
||||
"git.eeqj.de/sneak/routewatch/internal/streamer"
|
||||
"github.com/google/uuid"
|
||||
@@ -129,35 +130,6 @@ func (m *mockStore) RecordPeering(fromASNID, toASNID string, _ time.Time) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateLiveRoute mock implementation
|
||||
func (m *mockStore) UpdateLiveRoute(prefixID, originASNID uuid.UUID, peerASN int, _ string, _ time.Time) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
key := prefixID.String() + "_" + originASNID.String() + "_" + string(rune(peerASN))
|
||||
if !m.Routes[key] {
|
||||
m.Routes[key] = true
|
||||
m.RouteCount++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithdrawLiveRoute mock implementation
|
||||
func (m *mockStore) WithdrawLiveRoute(_ uuid.UUID, _ int, _ time.Time) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.WithdrawalCount++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveLiveRoutes mock implementation
|
||||
func (m *mockStore) GetActiveLiveRoutes() ([]database.LiveRoute, error) {
|
||||
return []database.LiveRoute{}, nil
|
||||
}
|
||||
|
||||
// UpdatePeer mock implementation
|
||||
func (m *mockStore) UpdatePeer(peerIP string, peerASN int, messageType string, timestamp time.Time) error {
|
||||
// Simple mock - just return nil
|
||||
@@ -180,7 +152,6 @@ func (m *mockStore) GetStats() (database.Stats, error) {
|
||||
IPv4Prefixes: m.IPv4Prefixes,
|
||||
IPv6Prefixes: m.IPv6Prefixes,
|
||||
Peerings: m.PeeringCount,
|
||||
LiveRoutes: m.RouteCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -197,15 +168,19 @@ func TestRouteWatchLiveFeed(t *testing.T) {
|
||||
// Create streamer
|
||||
s := streamer.New(logger, metricsTracker)
|
||||
|
||||
// Create routing table
|
||||
rt := routingtable.New()
|
||||
|
||||
// Create server
|
||||
srv := server.New(mockDB, s, logger)
|
||||
srv := server.New(mockDB, rt, s, logger)
|
||||
|
||||
// Create RouteWatch with 5 second limit
|
||||
deps := Dependencies{
|
||||
DB: mockDB,
|
||||
Streamer: s,
|
||||
Server: srv,
|
||||
Logger: logger,
|
||||
DB: mockDB,
|
||||
RoutingTable: rt,
|
||||
Streamer: s,
|
||||
Server: srv,
|
||||
Logger: logger,
|
||||
Config: Config{
|
||||
MaxRuntime: 5 * time.Second,
|
||||
},
|
||||
@@ -242,8 +217,4 @@ func TestRouteWatchLiveFeed(t *testing.T) {
|
||||
}
|
||||
t.Logf("Recorded %d AS peering relationships in 5 seconds", stats.Peerings)
|
||||
|
||||
if stats.LiveRoutes == 0 {
|
||||
t.Error("Expected to have some active routes")
|
||||
}
|
||||
t.Logf("Active routes: %d", stats.LiveRoutes)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package routewatch
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
||||
@@ -33,14 +32,6 @@ func (h *DatabaseHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
// Use the pre-parsed timestamp
|
||||
timestamp := msg.ParsedTimestamp
|
||||
|
||||
// Parse peer ASN
|
||||
peerASN, err := strconv.Atoi(msg.PeerASN)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to parse peer ASN", "peer_asn", msg.PeerASN, "error", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get origin ASN from path (last element)
|
||||
var originASN int
|
||||
if len(msg.Path) > 0 {
|
||||
@@ -51,7 +42,7 @@ func (h *DatabaseHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
for _, announcement := range msg.Announcements {
|
||||
for _, prefix := range announcement.Prefixes {
|
||||
// Get or create prefix
|
||||
p, err := h.db.GetOrCreatePrefix(prefix, timestamp)
|
||||
_, err := h.db.GetOrCreatePrefix(prefix, timestamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get/create prefix", "prefix", prefix, "error", err)
|
||||
|
||||
@@ -59,30 +50,13 @@ func (h *DatabaseHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
}
|
||||
|
||||
// Get or create origin ASN
|
||||
asn, err := h.db.GetOrCreateASN(originASN, timestamp)
|
||||
_, err = h.db.GetOrCreateASN(originASN, timestamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get/create ASN", "asn", originASN, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Update live route
|
||||
err = h.db.UpdateLiveRoute(
|
||||
p.ID,
|
||||
asn.ID,
|
||||
peerASN,
|
||||
announcement.NextHop,
|
||||
timestamp,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update live route",
|
||||
"prefix", prefix,
|
||||
"origin_asn", originASN,
|
||||
"peer_asn", peerASN,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Record the announcement in the announcements table
|
||||
// Process AS path to update peerings
|
||||
if len(msg.Path) > 1 {
|
||||
@@ -122,23 +96,13 @@ func (h *DatabaseHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
// Process withdrawals
|
||||
for _, prefix := range msg.Withdrawals {
|
||||
// Get prefix
|
||||
p, err := h.db.GetOrCreatePrefix(prefix, timestamp)
|
||||
_, err := h.db.GetOrCreatePrefix(prefix, timestamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get prefix for withdrawal", "prefix", prefix, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Withdraw the route
|
||||
err = h.db.WithdrawLiveRoute(p.ID, peerASN, timestamp)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to withdraw route",
|
||||
"prefix", prefix,
|
||||
"peer_asn", peerASN,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Record the withdrawal in the withdrawals table
|
||||
// TODO: Record the withdrawal in the announcements table as a withdrawal
|
||||
}
|
||||
}
|
||||
|
||||
133
internal/routewatch/routingtablehandler.go
Normal file
133
internal/routewatch/routingtablehandler.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package routewatch
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
|
||||
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
||||
"git.eeqj.de/sneak/routewatch/internal/routingtable"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// RoutingTableHandler handles BGP messages and updates the in-memory routing table
|
||||
type RoutingTableHandler struct {
|
||||
rt *routingtable.RoutingTable
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// NewRoutingTableHandler creates a new routing table handler
|
||||
func NewRoutingTableHandler(rt *routingtable.RoutingTable, logger *slog.Logger) *RoutingTableHandler {
|
||||
return &RoutingTableHandler{
|
||||
rt: rt,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// WantsMessage returns true if this handler wants to process messages of the given type
|
||||
func (h *RoutingTableHandler) WantsMessage(messageType string) bool {
|
||||
// We only care about UPDATE messages for the routing table
|
||||
return messageType == "UPDATE"
|
||||
}
|
||||
|
||||
// HandleMessage processes a RIS message and updates the routing table
|
||||
func (h *RoutingTableHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
// Use the pre-parsed timestamp
|
||||
timestamp := msg.ParsedTimestamp
|
||||
|
||||
// Parse peer ASN
|
||||
peerASN, err := strconv.Atoi(msg.PeerASN)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to parse peer ASN", "peer_asn", msg.PeerASN, "error", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get origin ASN from path (last element)
|
||||
var originASN int
|
||||
if len(msg.Path) > 0 {
|
||||
originASN = msg.Path[len(msg.Path)-1]
|
||||
}
|
||||
|
||||
// Process announcements
|
||||
for _, announcement := range msg.Announcements {
|
||||
for _, prefix := range announcement.Prefixes {
|
||||
// Generate deterministic UUIDs based on the prefix and origin ASN
|
||||
// This ensures consistency across restarts
|
||||
prefixID := uuid.NewSHA1(uuid.NameSpaceURL, []byte(prefix))
|
||||
originASNID := uuid.NewSHA1(uuid.NameSpaceOID, []byte(strconv.Itoa(originASN)))
|
||||
|
||||
// Create route for the routing table
|
||||
route := &routingtable.Route{
|
||||
PrefixID: prefixID,
|
||||
Prefix: prefix,
|
||||
OriginASNID: originASNID,
|
||||
OriginASN: originASN,
|
||||
PeerASN: peerASN,
|
||||
ASPath: msg.Path,
|
||||
NextHop: announcement.NextHop,
|
||||
AnnouncedAt: timestamp,
|
||||
}
|
||||
|
||||
// Add route to routing table
|
||||
h.rt.AddRoute(route)
|
||||
|
||||
h.logger.Debug("Added route to routing table",
|
||||
"prefix", prefix,
|
||||
"origin_asn", originASN,
|
||||
"peer_asn", peerASN,
|
||||
"path", msg.Path,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Process withdrawals
|
||||
for _, prefix := range msg.Withdrawals {
|
||||
// Generate deterministic UUID for the prefix
|
||||
prefixID := uuid.NewSHA1(uuid.NameSpaceURL, []byte(prefix))
|
||||
|
||||
// Withdraw all routes for this prefix from this peer
|
||||
count := h.rt.WithdrawRoutesByPrefixAndPeer(prefixID, peerASN)
|
||||
|
||||
h.logger.Debug("Withdrew routes from routing table",
|
||||
"prefix", prefix,
|
||||
"peer_asn", peerASN,
|
||||
"routes_removed", count,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// GetRoutingTableStats returns statistics about the routing table
|
||||
func (h *RoutingTableHandler) GetRoutingTableStats() map[string]int {
|
||||
return h.rt.Stats()
|
||||
}
|
||||
|
||||
// GetActiveRouteCount returns the number of active routes
|
||||
func (h *RoutingTableHandler) GetActiveRouteCount() int {
|
||||
return h.rt.Size()
|
||||
}
|
||||
|
||||
// GetRoutesByPrefix returns all routes for a specific prefix
|
||||
func (h *RoutingTableHandler) GetRoutesByPrefix(prefixID uuid.UUID) []*routingtable.Route {
|
||||
return h.rt.GetRoutesByPrefix(prefixID)
|
||||
}
|
||||
|
||||
// GetRoutesByOriginASN returns all routes originated by a specific ASN
|
||||
func (h *RoutingTableHandler) GetRoutesByOriginASN(originASNID uuid.UUID) []*routingtable.Route {
|
||||
return h.rt.GetRoutesByOriginASN(originASNID)
|
||||
}
|
||||
|
||||
// GetRoutesByPeerASN returns all routes received from a specific peer ASN
|
||||
func (h *RoutingTableHandler) GetRoutesByPeerASN(peerASN int) []*routingtable.Route {
|
||||
return h.rt.GetRoutesByPeerASN(peerASN)
|
||||
}
|
||||
|
||||
// GetAllRoutes returns all active routes
|
||||
func (h *RoutingTableHandler) GetAllRoutes() []*routingtable.Route {
|
||||
return h.rt.GetAllRoutes()
|
||||
}
|
||||
|
||||
// ClearRoutingTable clears all routes from the routing table
|
||||
func (h *RoutingTableHandler) ClearRoutingTable() {
|
||||
h.rt.Clear()
|
||||
h.logger.Info("Cleared routing table")
|
||||
}
|
||||
Reference in New Issue
Block a user