Re-vendor the shared files from sneak/prompts at 0653579 (closes #52)
check / check (push) Canceled after 0s
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
This commit was merged in pull request #85.
This commit is contained in:
@@ -41,9 +41,9 @@ const (
|
||||
ipv6Version = 6
|
||||
)
|
||||
|
||||
// PrefixHandler tracks BGP prefixes and maintains a live routing table in the database.
|
||||
// Routes are added on announcement and deleted on withdrawal, and all routes of
|
||||
// a peer are deleted when its BGP session goes down.
|
||||
// PrefixHandler tracks BGP prefixes and maintains a live routing table in the
|
||||
// database. Routes are added on announcement and deleted on withdrawal, and all
|
||||
// routes of a peer are deleted when its BGP session goes down.
|
||||
type PrefixHandler struct {
|
||||
db database.Store
|
||||
logger *logger.Logger
|
||||
@@ -52,10 +52,14 @@ type PrefixHandler struct {
|
||||
// Batching. mu guards batch and peersDown only: the flush goroutine takes
|
||||
// both under mu and writes them after releasing mu, so HandleMessage
|
||||
// waits for a database write only while batch is at prefixBatchMaxSize.
|
||||
mu sync.Mutex
|
||||
batch []prefixUpdate
|
||||
peersDown []string // peers whose BGP session went down since the batch was taken
|
||||
batchTaken sync.Cond // signalled when the flush goroutine takes batch; its L is &mu
|
||||
mu sync.Mutex
|
||||
batch []prefixUpdate
|
||||
// peersDown holds the peers whose BGP session went down since the batch
|
||||
// was taken.
|
||||
peersDown []string
|
||||
// batchTaken is signalled when the flush goroutine takes batch; its L is
|
||||
// &mu.
|
||||
batchTaken sync.Cond
|
||||
batchFullCh chan struct{} // wakes the flush goroutine when batch is full
|
||||
lastFlush time.Time // used only by the flush goroutine
|
||||
stopCh chan struct{}
|
||||
@@ -95,11 +99,13 @@ func (h *PrefixHandler) SetMetricsTracker(metrics *metrics.Tracker) {
|
||||
h.metrics = metrics
|
||||
}
|
||||
|
||||
// WantsMessage returns true if this handler wants to process messages of the given type
|
||||
// WantsMessage returns true if this handler wants to process messages of the
|
||||
// given type
|
||||
func (h *PrefixHandler) WantsMessage(messageType string) bool {
|
||||
// UPDATE messages change routes; a STATE message can say a peer's
|
||||
// session went down, which removes all of its routes.
|
||||
return messageType == ristypes.MessageTypeUpdate || messageType == ristypes.MessageTypeState
|
||||
return messageType == ristypes.MessageTypeUpdate ||
|
||||
messageType == ristypes.MessageTypeState
|
||||
}
|
||||
|
||||
// QueueCapacity returns the desired queue capacity for this handler
|
||||
@@ -183,6 +189,12 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully stops the handler and flushes remaining batches
|
||||
func (h *PrefixHandler) Stop() {
|
||||
close(h.stopCh)
|
||||
h.wg.Wait()
|
||||
}
|
||||
|
||||
// peerSessionDown notes that the peer's BGP session went down, so that the next
|
||||
// flush deletes all of the peer's live routes before it writes the batch. The
|
||||
// peer's updates still in the batch came before the down, which replaces them,
|
||||
@@ -192,7 +204,9 @@ func (h *PrefixHandler) peerSessionDown(peer string) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
h.batch = slices.DeleteFunc(h.batch, func(update prefixUpdate) bool { return update.peer == peer })
|
||||
h.batch = slices.DeleteFunc(h.batch, func(update prefixUpdate) bool {
|
||||
return update.peer == peer
|
||||
})
|
||||
h.peersDown = append(h.peersDown, peer)
|
||||
}
|
||||
|
||||
@@ -200,6 +214,7 @@ func (h *PrefixHandler) peerSessionDown(peer string) {
|
||||
// flushes when the batch is full, periodically, and once more on Stop.
|
||||
func (h *PrefixHandler) flushLoop() {
|
||||
defer h.wg.Done()
|
||||
|
||||
ticker := time.NewTicker(prefixBatchTimeout)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -230,6 +245,7 @@ func (h *PrefixHandler) takeBatch() ([]prefixUpdate, []string) {
|
||||
if len(h.batch) == 0 && len(h.peersDown) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
batch, peersDown := h.batch, h.peersDown
|
||||
h.batch = make([]prefixUpdate, 0, prefixBatchSize)
|
||||
h.peersDown = nil
|
||||
@@ -250,43 +266,34 @@ func (h *PrefixHandler) flushBatch() {
|
||||
batchSize := len(batch)
|
||||
|
||||
// The routes of a peer whose session went down are deleted first: the
|
||||
// batch holds only the updates the peer sent after the down. Most downs
|
||||
// come from peers that have no routes, so only a delete that removed
|
||||
// routes is logged.
|
||||
for _, peer := range peersDown {
|
||||
deleted, err := h.db.DeleteLiveRoutesOfPeer(peer)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to delete the live routes of a peer whose session went down",
|
||||
"peer", peer, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
if deleted > 0 {
|
||||
h.logger.Info("Deleted the live routes of a peer whose session went down",
|
||||
"peer", peer, "routes", deleted)
|
||||
}
|
||||
}
|
||||
// batch holds only the updates the peer sent after the down.
|
||||
h.deleteLiveRoutesOfPeers(peersDown)
|
||||
|
||||
// For each prefix and peer, keep only the last update the feed delivered.
|
||||
// Updates from different peers for the same prefix are all kept. Arrival
|
||||
// order decides, not the timestamp: timestamps are whole seconds, so
|
||||
// updates within one second carry the same one.
|
||||
type prefixAndPeer struct{ prefix, peer string }
|
||||
|
||||
lastUpdates := make(map[prefixAndPeer]prefixUpdate)
|
||||
|
||||
for _, update := range batch {
|
||||
lastUpdates[prefixAndPeer{update.prefix, update.peer}] = update
|
||||
}
|
||||
|
||||
// Collect routes to upsert and delete
|
||||
var routesToUpsert []*database.LiveRoute
|
||||
var routesToDelete []database.LiveRouteDeletion
|
||||
var (
|
||||
routesToUpsert []*database.LiveRoute
|
||||
routesToDelete []database.LiveRouteDeletion
|
||||
)
|
||||
|
||||
// Collect unique prefixes to update
|
||||
prefixesToUpdate := make(map[string]time.Time)
|
||||
|
||||
for _, update := range lastUpdates {
|
||||
// Track prefix for both announcements and withdrawals
|
||||
if _, exists := prefixesToUpdate[update.prefix]; !exists || update.timestamp.After(prefixesToUpdate[update.prefix]) {
|
||||
seen, exists := prefixesToUpdate[update.prefix]
|
||||
if !exists || update.timestamp.After(seen) {
|
||||
prefixesToUpdate[update.prefix] = update.timestamp
|
||||
}
|
||||
|
||||
@@ -300,7 +307,8 @@ func (h *PrefixHandler) flushBatch() {
|
||||
// Parse CIDR to get IP version
|
||||
_, ipVersion, err := parseCIDR(update.prefix)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to parse CIDR for withdrawal", "prefix", update.prefix, "error", err)
|
||||
h.logger.Error("Failed to parse CIDR for withdrawal",
|
||||
"prefix", update.prefix, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -314,30 +322,7 @@ func (h *PrefixHandler) flushBatch() {
|
||||
}
|
||||
}
|
||||
|
||||
// Process batch operations
|
||||
successCount := 0
|
||||
if len(routesToUpsert) > 0 {
|
||||
if err := h.db.UpsertLiveRouteBatch(routesToUpsert); err != nil {
|
||||
h.logger.Error("Failed to upsert route batch", "error", err, "count", len(routesToUpsert))
|
||||
} else {
|
||||
successCount += len(routesToUpsert)
|
||||
}
|
||||
}
|
||||
|
||||
if len(routesToDelete) > 0 {
|
||||
if err := h.db.DeleteLiveRouteBatch(routesToDelete); err != nil {
|
||||
h.logger.Error("Failed to delete route batch", "error", err, "count", len(routesToDelete))
|
||||
} else {
|
||||
successCount += len(routesToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// Update prefix tables
|
||||
if len(prefixesToUpdate) > 0 {
|
||||
if err := h.db.UpdatePrefixesBatch(prefixesToUpdate); err != nil {
|
||||
h.logger.Error("Failed to update prefix batch", "error", err, "count", len(prefixesToUpdate))
|
||||
}
|
||||
}
|
||||
successCount := h.writeBatch(routesToUpsert, routesToDelete, prefixesToUpdate)
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
h.logger.Debug("Flushed prefix batch",
|
||||
@@ -351,6 +336,69 @@ func (h *PrefixHandler) flushBatch() {
|
||||
h.lastFlush = time.Now()
|
||||
}
|
||||
|
||||
// deleteLiveRoutesOfPeers deletes all live routes of each of peers, the peers
|
||||
// whose BGP session went down. Most downs come from peers that have no routes,
|
||||
// so only a delete that removed routes is logged.
|
||||
func (h *PrefixHandler) deleteLiveRoutesOfPeers(peers []string) {
|
||||
for _, peer := range peers {
|
||||
deleted, err := h.db.DeleteLiveRoutesOfPeer(peer)
|
||||
if err != nil {
|
||||
h.logger.Error(
|
||||
"Failed to delete the live routes of a peer whose session went down",
|
||||
"peer", peer, "error", err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if deleted > 0 {
|
||||
h.logger.Info(
|
||||
"Deleted the live routes of a peer whose session went down",
|
||||
"peer", peer, "routes", deleted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeBatch writes the collected route upserts, route deletions and prefix
|
||||
// updates to the database and returns how many routes it wrote.
|
||||
func (h *PrefixHandler) writeBatch(
|
||||
routesToUpsert []*database.LiveRoute,
|
||||
routesToDelete []database.LiveRouteDeletion,
|
||||
prefixesToUpdate map[string]time.Time,
|
||||
) int {
|
||||
successCount := 0
|
||||
|
||||
if len(routesToUpsert) > 0 {
|
||||
err := h.db.UpsertLiveRouteBatch(routesToUpsert)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to upsert route batch",
|
||||
"error", err, "count", len(routesToUpsert))
|
||||
} else {
|
||||
successCount += len(routesToUpsert)
|
||||
}
|
||||
}
|
||||
|
||||
if len(routesToDelete) > 0 {
|
||||
err := h.db.DeleteLiveRouteBatch(routesToDelete)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to delete route batch",
|
||||
"error", err, "count", len(routesToDelete))
|
||||
} else {
|
||||
successCount += len(routesToDelete)
|
||||
}
|
||||
}
|
||||
|
||||
// Update prefix tables
|
||||
if len(prefixesToUpdate) > 0 {
|
||||
err := h.db.UpdatePrefixesBatch(prefixesToUpdate)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update prefix batch",
|
||||
"error", err, "count", len(prefixesToUpdate))
|
||||
}
|
||||
}
|
||||
|
||||
return successCount
|
||||
}
|
||||
|
||||
// canonicalPrefix returns prefix in the text form net/netip prints, the form
|
||||
// the IP lookup builds when it looks a prefix up. The feed sends IPv6
|
||||
// announcements compressed ("2001:db8::/32") but IPv6 withdrawals uncompressed
|
||||
@@ -366,8 +414,8 @@ func canonicalPrefix(prefix string) string {
|
||||
return p.Masked().String()
|
||||
}
|
||||
|
||||
// parseCIDR extracts the mask length and IP version from a prefix string
|
||||
func parseCIDR(prefix string) (maskLength int, ipVersion int, err error) {
|
||||
// parseCIDR returns the mask length and the IP version of a prefix string
|
||||
func parseCIDR(prefix string) (int, int, error) {
|
||||
_, ipNet, err := net.ParseCIDR(prefix)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@@ -381,50 +429,6 @@ func parseCIDR(prefix string) (maskLength int, ipVersion int, err error) {
|
||||
return ones, ipv4Version, nil
|
||||
}
|
||||
|
||||
// processAnnouncement handles storing an announcement in the database
|
||||
// nolint:unused // kept for potential future use
|
||||
func (h *PrefixHandler) processAnnouncement(_ *database.Prefix, update prefixUpdate) {
|
||||
// Parse CIDR to get mask length
|
||||
maskLength, ipVersion, err := parseCIDR(update.prefix)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to parse CIDR",
|
||||
"prefix", update.prefix,
|
||||
"error", err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Track route update metrics
|
||||
if h.metrics != nil {
|
||||
if ipVersion == ipv4Version {
|
||||
h.metrics.RecordIPv4Update()
|
||||
} else {
|
||||
h.metrics.RecordIPv6Update()
|
||||
}
|
||||
}
|
||||
|
||||
// Create live route record
|
||||
liveRoute := &database.LiveRoute{
|
||||
ID: uuid.New(),
|
||||
Prefix: update.prefix,
|
||||
MaskLength: maskLength,
|
||||
IPVersion: ipVersion,
|
||||
OriginASN: update.originASN,
|
||||
PeerIP: update.peer,
|
||||
ASPath: update.path,
|
||||
NextHop: update.peer, // Using peer as next hop
|
||||
LastUpdated: update.timestamp,
|
||||
}
|
||||
|
||||
if err := h.db.UpsertLiveRoute(liveRoute); err != nil {
|
||||
h.logger.Error("Failed to upsert live route",
|
||||
"prefix", update.prefix,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// createLiveRoute creates a LiveRoute from a prefix update
|
||||
func (h *PrefixHandler) createLiveRoute(update prefixUpdate) *database.LiveRoute {
|
||||
// Parse CIDR to get mask length
|
||||
@@ -462,77 +466,3 @@ func (h *PrefixHandler) createLiveRoute(update prefixUpdate) *database.LiveRoute
|
||||
|
||||
return liveRoute
|
||||
}
|
||||
|
||||
// processAnnouncementDirect handles storing an announcement directly without prefix table
|
||||
// nolint:unused // kept for potential future use
|
||||
func (h *PrefixHandler) processAnnouncementDirect(update prefixUpdate) {
|
||||
// Parse CIDR to get mask length
|
||||
maskLength, ipVersion, err := parseCIDR(update.prefix)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to parse CIDR",
|
||||
"prefix", update.prefix,
|
||||
"error", err,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Track route update metrics
|
||||
if h.metrics != nil {
|
||||
if ipVersion == ipv4Version {
|
||||
h.metrics.RecordIPv4Update()
|
||||
} else {
|
||||
h.metrics.RecordIPv6Update()
|
||||
}
|
||||
}
|
||||
|
||||
// Create live route record
|
||||
liveRoute := &database.LiveRoute{
|
||||
ID: uuid.New(),
|
||||
Prefix: update.prefix,
|
||||
MaskLength: maskLength,
|
||||
IPVersion: ipVersion,
|
||||
OriginASN: update.originASN,
|
||||
PeerIP: update.peer,
|
||||
ASPath: update.path,
|
||||
NextHop: update.peer, // Using peer as next hop
|
||||
LastUpdated: update.timestamp,
|
||||
}
|
||||
|
||||
if err := h.db.UpsertLiveRoute(liveRoute); err != nil {
|
||||
h.logger.Error("Failed to upsert live route",
|
||||
"prefix", update.prefix,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// processWithdrawalDirect handles removing a route directly without prefix table
|
||||
// nolint:unused // kept for potential future use
|
||||
func (h *PrefixHandler) processWithdrawalDirect(update prefixUpdate) {
|
||||
if err := h.db.DeleteLiveRoute(update.prefix, update.peer); err != nil {
|
||||
h.logger.Error("Failed to delete live route",
|
||||
"prefix", update.prefix,
|
||||
"peer", update.peer,
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// processWithdrawal handles removing a route from the live routing table
|
||||
// nolint:unused // kept for potential future use
|
||||
func (h *PrefixHandler) processWithdrawal(_ *database.Prefix, update prefixUpdate) {
|
||||
if err := h.db.DeleteLiveRoute(update.prefix, update.peer); err != nil {
|
||||
h.logger.Error("Failed to delete live route",
|
||||
"prefix", update.prefix,
|
||||
"peer", update.peer,
|
||||
"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