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:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user