check / check (push) Successful in 2m52s
Each handler queue held 100,000 message pointers; a message is retained until the slowest handler drains it, so all four full was a derived worst case near 800 MiB. Twenty thousand is about four seconds of feed at peak and caps that at roughly 160 MiB. The streamer already drops rather than blocks on a full queue, so the smaller bound is safe. Batch sizes are unchanged. The largest, asnBatchSize, is 30,000 and now exceeds its queue, but each queued message contributes every ASN in its path, and every handler also flushes on its own timer regardless of fill, so batches still flush and no size change is warranted. Model: opus-4-8
185 lines
5.2 KiB
Go
185 lines
5.2 KiB
Go
package routewatch
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
|
)
|
|
|
|
const (
|
|
// asHandlerQueueSize is the queue capacity for ASN operations, about 4
|
|
// seconds of feed at peak. The streamer drops rather than blocks when a
|
|
// queue is full, so this bounds memory. Batches still flush on a timer
|
|
// (asnBatchTimeout), so a queue smaller than asnBatchSize is fine.
|
|
asHandlerQueueSize = 20000
|
|
|
|
// asnBatchSize is the number of ASN operations to batch together
|
|
asnBatchSize = 30000
|
|
|
|
// asnBatchTimeout is the maximum time to wait before flushing a batch
|
|
// DO NOT reduce this timeout - larger batches are more efficient
|
|
asnBatchTimeout = 2 * time.Second
|
|
)
|
|
|
|
// ASHandler processes Autonomous System Number (ASN) information extracted from
|
|
// BGP UPDATE messages. It uses batched database operations to efficiently store
|
|
// ASN data, collecting operations into batches that are flushed either when the
|
|
// batch reaches a size threshold or after a timeout period.
|
|
type ASHandler struct {
|
|
db database.Store
|
|
logger *logger.Logger
|
|
|
|
// Batching
|
|
mu sync.Mutex
|
|
batch []asnOp
|
|
lastFlush time.Time
|
|
stopCh chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
type asnOp struct {
|
|
number int
|
|
timestamp time.Time
|
|
}
|
|
|
|
// NewASHandler creates and returns a new ASHandler instance. It initializes
|
|
// the batching system and starts a background goroutine that periodically
|
|
// flushes accumulated ASN operations to the database. The caller must call
|
|
// Stop when finished to ensure all pending operations are flushed and the
|
|
// background goroutine is terminated.
|
|
func NewASHandler(db database.Store, logger *logger.Logger) *ASHandler {
|
|
h := &ASHandler{
|
|
db: db,
|
|
logger: logger,
|
|
batch: make([]asnOp, 0, asnBatchSize),
|
|
lastFlush: time.Now(),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
|
|
// Start the flush timer goroutine
|
|
h.wg.Add(1)
|
|
go h.flushLoop()
|
|
|
|
return h
|
|
}
|
|
|
|
// WantsMessage reports whether this handler should process messages of the
|
|
// given type. ASHandler only processes "UPDATE" messages, as these contain
|
|
// the AS path information needed to track autonomous systems.
|
|
func (h *ASHandler) WantsMessage(messageType string) bool {
|
|
// We only care about UPDATE messages for the database
|
|
return messageType == "UPDATE"
|
|
}
|
|
|
|
// QueueCapacity returns the recommended message queue size for this handler.
|
|
// ASHandler uses a large queue capacity to accommodate high-volume BGP streams,
|
|
// as the batching mechanism allows efficient processing of accumulated messages.
|
|
func (h *ASHandler) QueueCapacity() int {
|
|
// Batching allows us to use a larger queue
|
|
return asHandlerQueueSize
|
|
}
|
|
|
|
// HandleMessage processes a RIS Live BGP message by extracting all ASNs from
|
|
// the AS path and queuing them for batch insertion into the database. The
|
|
// origin ASN (last element in the path) and all transit ASNs are recorded
|
|
// with their associated timestamps. The batch is automatically flushed when
|
|
// it reaches the configured size threshold.
|
|
func (h *ASHandler) 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()
|
|
|
|
// Queue origin ASN operation
|
|
if originASN > 0 {
|
|
h.batch = append(h.batch, asnOp{
|
|
number: originASN,
|
|
timestamp: timestamp,
|
|
})
|
|
}
|
|
|
|
// Also track all ASNs in the path
|
|
for _, asn := range msg.Path {
|
|
if asn > 0 {
|
|
h.batch = append(h.batch, asnOp{
|
|
number: asn,
|
|
timestamp: timestamp,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Check if we need to flush
|
|
if len(h.batch) >= asnBatchSize {
|
|
h.flushBatchLocked()
|
|
}
|
|
}
|
|
|
|
// flushLoop runs in a goroutine and periodically flushes batches
|
|
func (h *ASHandler) flushLoop() {
|
|
defer h.wg.Done()
|
|
ticker := time.NewTicker(asnBatchTimeout)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
h.mu.Lock()
|
|
if time.Since(h.lastFlush) >= asnBatchTimeout {
|
|
h.flushBatchLocked()
|
|
}
|
|
h.mu.Unlock()
|
|
case <-h.stopCh:
|
|
// Final flush
|
|
h.mu.Lock()
|
|
h.flushBatchLocked()
|
|
h.mu.Unlock()
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// flushBatchLocked flushes the ASN batch to the database (must be called with mutex held)
|
|
func (h *ASHandler) flushBatchLocked() {
|
|
if len(h.batch) == 0 {
|
|
return
|
|
}
|
|
|
|
// Process ASNs first (deduped)
|
|
asnMap := make(map[int]time.Time)
|
|
for _, op := range h.batch {
|
|
if existing, ok := asnMap[op.number]; !ok || op.timestamp.After(existing) {
|
|
asnMap[op.number] = op.timestamp
|
|
}
|
|
}
|
|
|
|
// Process all ASNs in a single batch transaction
|
|
if err := h.db.GetOrCreateASNBatch(asnMap); err != nil {
|
|
h.logger.Error("Failed to process ASN batch", "error", err, "count", len(asnMap))
|
|
}
|
|
|
|
// Clear batch
|
|
h.batch = h.batch[:0]
|
|
h.lastFlush = time.Now()
|
|
}
|
|
|
|
// Stop gracefully shuts down the ASHandler by signaling the background flush
|
|
// goroutine to terminate and waiting for it to complete. Any pending ASN
|
|
// operations in the current batch are flushed to the database before Stop
|
|
// returns. This method should be called during application shutdown to ensure
|
|
// no data is lost.
|
|
func (h *ASHandler) Stop() {
|
|
close(h.stopCh)
|
|
h.wg.Wait()
|
|
}
|