Rename handlers and add PrefixHandler for database routing table

- Renamed BatchedDatabaseHandler to DBHandler
- Renamed BatchedPeerHandler to PeerHandler
- Quadrupled DBHandler batch size from 4000 to 16000
- Created new PrefixHandler using same batching strategy to maintain routing table in database
- Removed verbose batch flush logging from all handlers
- Updated app.go to use renamed handlers and register PrefixHandler
- Fixed test configuration to enable batched database writes
This commit is contained in:
2025-07-28 01:37:19 +02:00
parent 3aef3f9a07
commit cea7c3dfd3
8 changed files with 623 additions and 584 deletions
+270
View File
@@ -0,0 +1,270 @@
package routewatch
import (
"encoding/json"
"sync"
"time"
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/logger"
"git.eeqj.de/sneak/routewatch/internal/ristypes"
)
const (
// prefixHandlerQueueSize is the queue capacity for prefix tracking operations
prefixHandlerQueueSize = 50000
// prefixBatchSize is the number of prefix updates to batch together
prefixBatchSize = 2000
// prefixBatchTimeout is the maximum time to wait before flushing a batch
prefixBatchTimeout = 5 * time.Second
)
// PrefixHandler tracks BGP prefixes and maintains a routing table in the database
type PrefixHandler struct {
db database.Store
logger *logger.Logger
// Batching
mu sync.Mutex
batch []prefixUpdate
lastFlush time.Time
stopCh chan struct{}
wg sync.WaitGroup
}
type prefixUpdate struct {
prefix string
originASN int
peer string
messageType string
timestamp time.Time
path []int
}
// NewPrefixHandler creates a new batched prefix tracking handler
func NewPrefixHandler(db database.Store, logger *logger.Logger) *PrefixHandler {
h := &PrefixHandler{
db: db,
logger: logger,
batch: make([]prefixUpdate, 0, prefixBatchSize),
lastFlush: time.Now(),
stopCh: make(chan struct{}),
}
// Start the flush timer goroutine
h.wg.Add(1)
go h.flushLoop()
return h
}
// WantsMessage returns true if this handler wants to process messages of the given type
func (h *PrefixHandler) WantsMessage(messageType string) bool {
// We only care about UPDATE messages for the routing table
return messageType == "UPDATE"
}
// QueueCapacity returns the desired queue capacity for this handler
func (h *PrefixHandler) QueueCapacity() int {
// Batching allows us to use a larger queue
return prefixHandlerQueueSize
}
// HandleMessage processes a message to track prefix information
func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
// Use the pre-parsed timestamp
timestamp := msg.ParsedTimestamp
// Get origin ASN from path (last element)
var originASN int
if len(msg.Path) > 0 {
originASN = msg.Path[len(msg.Path)-1]
}
h.mu.Lock()
defer h.mu.Unlock()
// Process announcements
for _, announcement := range msg.Announcements {
for _, prefix := range announcement.Prefixes {
h.batch = append(h.batch, prefixUpdate{
prefix: prefix,
originASN: originASN,
peer: msg.Peer,
messageType: "announcement",
timestamp: timestamp,
path: msg.Path,
})
}
}
// Process withdrawals
for _, prefix := range msg.Withdrawals {
h.batch = append(h.batch, prefixUpdate{
prefix: prefix,
originASN: 0, // No origin for withdrawals
peer: msg.Peer,
messageType: "withdrawal",
timestamp: timestamp,
path: nil,
})
}
// Check if we need to flush
if len(h.batch) >= prefixBatchSize {
h.flushBatchLocked()
}
}
// flushLoop runs in a goroutine and periodically flushes batches
func (h *PrefixHandler) flushLoop() {
defer h.wg.Done()
ticker := time.NewTicker(prefixBatchTimeout)
defer ticker.Stop()
for {
select {
case <-ticker.C:
h.mu.Lock()
if time.Since(h.lastFlush) >= prefixBatchTimeout {
h.flushBatchLocked()
}
h.mu.Unlock()
case <-h.stopCh:
// Final flush
h.mu.Lock()
h.flushBatchLocked()
h.mu.Unlock()
return
}
}
}
// flushBatchLocked flushes the prefix batch to the database (must be called with mutex held)
func (h *PrefixHandler) flushBatchLocked() {
if len(h.batch) == 0 {
return
}
// Group updates by prefix to deduplicate
// For each prefix, keep the latest update
prefixMap := make(map[string]prefixUpdate)
for _, update := range h.batch {
key := update.prefix
if existing, ok := prefixMap[key]; !ok || update.timestamp.After(existing.timestamp) {
prefixMap[key] = update
}
}
// Apply updates to database
for _, update := range prefixMap {
// Get or create prefix
prefix, err := h.db.GetOrCreatePrefix(update.prefix, update.timestamp)
if err != nil {
h.logger.Error("Failed to get/create prefix",
"prefix", update.prefix,
"error", err,
)
continue
}
// For announcements, get ASN info and create announcement record
if update.messageType == "announcement" && update.originASN > 0 {
h.processAnnouncement(prefix, update)
} else if update.messageType == "withdrawal" {
h.processWithdrawal(prefix, update)
}
}
// Clear batch
h.batch = h.batch[:0]
h.lastFlush = time.Now()
}
// processAnnouncement handles storing an announcement in the database
func (h *PrefixHandler) processAnnouncement(prefix *database.Prefix, update prefixUpdate) {
// Get or create origin ASN
originASN, err := h.db.GetOrCreateASN(update.originASN, update.timestamp)
if err != nil {
h.logger.Error("Failed to get/create origin ASN",
"asn", update.originASN,
"error", err,
)
return
}
// Get or create peer ASN (first element in path if exists)
var peerASN *database.ASN
if len(update.path) > 0 {
peerASN, err = h.db.GetOrCreateASN(update.path[0], update.timestamp)
if err != nil {
h.logger.Error("Failed to get/create peer ASN",
"asn", update.path[0],
"error", err,
)
return
}
} else {
// If no path, use origin as peer
peerASN = originASN
}
// Encode AS path as JSON
pathJSON, err := json.Marshal(update.path)
if err != nil {
h.logger.Error("Failed to encode AS path",
"path", update.path,
"error", err,
)
return
}
// Create announcement record
announcement := &database.Announcement{
PrefixID: prefix.ID,
ASNID: peerASN.ID,
OriginASNID: originASN.ID,
Path: string(pathJSON),
NextHop: update.peer,
Timestamp: update.timestamp,
IsWithdrawal: false,
}
if err := h.db.RecordAnnouncement(announcement); err != nil {
h.logger.Error("Failed to record announcement",
"prefix", update.prefix,
"error", err,
)
}
}
// processWithdrawal handles storing a withdrawal in the database
func (h *PrefixHandler) processWithdrawal(prefix *database.Prefix, update prefixUpdate) {
// For withdrawals, create a withdrawal record
announcement := &database.Announcement{
PrefixID: prefix.ID,
NextHop: update.peer,
Timestamp: update.timestamp,
IsWithdrawal: true,
}
if err := h.db.RecordAnnouncement(announcement); err != nil {
h.logger.Error("Failed to record withdrawal",
"prefix", update.prefix,
"error", err,
)
}
}
// Stop gracefully stops the handler and flushes remaining batches
func (h *PrefixHandler) Stop() {
close(h.stopCh)
h.wg.Wait()
}