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
232 lines
6.7 KiB
Go
232 lines
6.7 KiB
Go
package routewatch
|
|
|
|
// peerhandler.go provides batched peer tracking functionality for BGP route
|
|
// monitoring. It tracks BGP peers from incoming UPDATE messages and maintains
|
|
// peer state in the database.
|
|
|
|
import (
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/logger"
|
|
"git.eeqj.de/sneak/routewatch/internal/ristypes"
|
|
)
|
|
|
|
const (
|
|
// peerHandlerQueueSize is the queue capacity for peer tracking 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 (peerBatchTimeout).
|
|
peerHandlerQueueSize = 20000
|
|
|
|
// peerBatchSize is the number of peer updates to batch together
|
|
peerBatchSize = 10000
|
|
|
|
// peerBatchMaxSize is the most peer updates the batch holds while the
|
|
// flush goroutine writes the previous one: ten batches, about 10 MiB
|
|
// (README "Memory"). At this size HandleMessage waits until the flush
|
|
// goroutine takes the batch.
|
|
peerBatchMaxSize = 100000
|
|
|
|
// peerBatchTimeout is the maximum time to wait before flushing a batch
|
|
peerBatchTimeout = 2 * time.Second
|
|
)
|
|
|
|
// PeerHandler tracks BGP peers from UPDATE messages using batched operations.
|
|
// It maintains a queue of peer updates and periodically flushes them to the database
|
|
// in batches to improve performance. The handler deduplicates peer updates within
|
|
// each batch, keeping only the most recent update for each peer IP address.
|
|
type PeerHandler struct {
|
|
db database.Store
|
|
logger *logger.Logger
|
|
|
|
// Batching. mu guards peerBatch only: the flush goroutine takes the batch
|
|
// under mu and writes it after releasing mu, so HandleMessage waits for
|
|
// a database write only while peerBatch is at peerBatchMaxSize.
|
|
mu sync.Mutex
|
|
peerBatch []peerUpdate
|
|
// batchTaken is signalled when the flush goroutine takes peerBatch; its L
|
|
// is &mu.
|
|
batchTaken sync.Cond
|
|
batchFullCh chan struct{} // wakes the flush goroutine when peerBatch is full
|
|
lastFlush time.Time // used only by the flush goroutine
|
|
stopCh chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
type peerUpdate struct {
|
|
peerIP string
|
|
peerASN int
|
|
messageType string
|
|
timestamp time.Time
|
|
}
|
|
|
|
// NewPeerHandler creates a new PeerHandler with the given database store and logger.
|
|
// It initializes the peer batch buffer and starts a background goroutine that
|
|
// periodically flushes accumulated peer updates to the database. The handler
|
|
// should be stopped by calling Stop when it is no longer needed.
|
|
func NewPeerHandler(db database.Store, logger *logger.Logger) *PeerHandler {
|
|
h := &PeerHandler{
|
|
db: db,
|
|
logger: logger,
|
|
peerBatch: make([]peerUpdate, 0, peerBatchSize),
|
|
batchFullCh: make(chan struct{}, 1),
|
|
lastFlush: time.Now(),
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
h.batchTaken.L = &h.mu
|
|
|
|
// Start the flush timer goroutine
|
|
h.wg.Add(1)
|
|
go h.flushLoop()
|
|
|
|
return h
|
|
}
|
|
|
|
// WantsMessage returns true for UPDATE messages only. A peer whose session
|
|
// never comes up sends STATE messages and nothing else, so a STATE message is
|
|
// not a sign of a peer that carries routes.
|
|
func (h *PeerHandler) WantsMessage(messageType string) bool {
|
|
return messageType == ristypes.MessageTypeUpdate
|
|
}
|
|
|
|
// QueueCapacity returns the desired queue capacity for this handler.
|
|
// The PeerHandler uses a large queue capacity because batching allows
|
|
// for efficient processing of many updates at once.
|
|
func (h *PeerHandler) QueueCapacity() int {
|
|
// Batching allows us to use a larger queue
|
|
return peerHandlerQueueSize
|
|
}
|
|
|
|
// HandleMessage processes a RIS message to track peer information.
|
|
// It extracts the peer IP address and ASN from the message and adds
|
|
// the update to an internal batch. The flush goroutine writes the batch to the
|
|
// database when it reaches peerBatchSize or the batch timeout expires.
|
|
func (h *PeerHandler) HandleMessage(msg *ristypes.RISMessage) {
|
|
// Parse peer ASN from string
|
|
peerASN := 0
|
|
|
|
if msg.PeerASN != "" {
|
|
asn, err := strconv.Atoi(msg.PeerASN)
|
|
if err == nil {
|
|
peerASN = asn
|
|
}
|
|
}
|
|
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
// While the batch is at its maximum size, wait until the flush goroutine
|
|
// takes it. Meanwhile the handler takes nothing off its queue, which
|
|
// fills, and the streamer drops messages for it.
|
|
for len(h.peerBatch) >= peerBatchMaxSize {
|
|
h.batchTaken.Wait()
|
|
}
|
|
|
|
// Add to batch
|
|
h.peerBatch = append(h.peerBatch, peerUpdate{
|
|
peerIP: msg.Peer,
|
|
peerASN: peerASN,
|
|
messageType: msg.Type,
|
|
timestamp: msg.ParsedTimestamp,
|
|
})
|
|
|
|
// A full batch wakes the flush goroutine. If it is writing, the wake-up
|
|
// waits in batchFullCh and the batch keeps growing until the write ends
|
|
// or the batch reaches peerBatchMaxSize.
|
|
if len(h.peerBatch) >= peerBatchSize {
|
|
select {
|
|
case h.batchFullCh <- struct{}{}:
|
|
default: // already woken
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop gracefully stops the handler and flushes remaining batches
|
|
func (h *PeerHandler) Stop() {
|
|
close(h.stopCh)
|
|
h.wg.Wait()
|
|
}
|
|
|
|
// flushLoop runs in a goroutine, the only one that writes the batch: it
|
|
// flushes when the batch is full, periodically, and once more on Stop.
|
|
func (h *PeerHandler) flushLoop() {
|
|
defer h.wg.Done()
|
|
|
|
ticker := time.NewTicker(peerBatchTimeout)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if time.Since(h.lastFlush) >= peerBatchTimeout {
|
|
h.flushBatch()
|
|
}
|
|
case <-h.batchFullCh:
|
|
h.flushBatch()
|
|
case <-h.stopCh:
|
|
// Final flush
|
|
h.flushBatch()
|
|
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// takeBatch returns the pending updates and leaves an empty batch in their
|
|
// place for HandleMessage to fill.
|
|
func (h *PeerHandler) takeBatch() []peerUpdate {
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
|
|
if len(h.peerBatch) == 0 {
|
|
return nil
|
|
}
|
|
|
|
batch := h.peerBatch
|
|
h.peerBatch = make([]peerUpdate, 0, peerBatchSize)
|
|
h.batchTaken.Broadcast()
|
|
|
|
return batch
|
|
}
|
|
|
|
// flushBatch takes the pending batch and writes it to the database, without
|
|
// holding the mutex during the write.
|
|
func (h *PeerHandler) flushBatch() {
|
|
batch := h.takeBatch()
|
|
if len(batch) == 0 {
|
|
return
|
|
}
|
|
|
|
// Deduplicate by peer IP, keeping the latest update for each peer
|
|
peerMap := make(map[string]peerUpdate)
|
|
for _, update := range batch {
|
|
existing, ok := peerMap[update.peerIP]
|
|
if !ok || update.timestamp.After(existing.timestamp) {
|
|
peerMap[update.peerIP] = update
|
|
}
|
|
}
|
|
|
|
// Convert to database format
|
|
dbPeerMap := make(map[string]database.PeerUpdate)
|
|
for peerIP, update := range peerMap {
|
|
dbPeerMap[peerIP] = database.PeerUpdate{
|
|
PeerIP: update.peerIP,
|
|
PeerASN: update.peerASN,
|
|
MessageType: update.messageType,
|
|
Timestamp: update.timestamp,
|
|
}
|
|
}
|
|
|
|
// Process all peers in a single batch transaction
|
|
err := h.db.UpdatePeerBatch(dbPeerMap)
|
|
if err != nil {
|
|
h.logger.Error("Failed to process peer batch",
|
|
"error", err, "count", len(dbPeerMap))
|
|
}
|
|
|
|
h.lastFlush = time.Now()
|
|
}
|