Record origin changes and show them on the prefix page (closes #161)
check / check (push) Successful in 1m3s

The routing table records a row each time a prefix and origin AS gets its first peer (announced) or loses its last (withdrawn), with the time and how many peers carry the prefix just after. The prefix handler writes the rows to a new origin_changes table, apart from routes. Every hour the database maintainer keeps each prefix's rows from the last 72 hours or its newest 500, whichever keeps more. The prefix page lists them newest first, at most 5,000.

At most 100,000 rows wait in memory to be written; past that the oldest are dropped and logged. A stop loses the rows past its last write.

Model: deepseek-v4-pro (implementation); opus-5-5 (commit message)
This commit was merged in pull request #231.
This commit is contained in:
2026-11-05 06:57:41 +01:00
parent 15bf20a894
commit e58c6635e7
20 changed files with 1651 additions and 88 deletions
+75 -19
View File
@@ -88,6 +88,10 @@ const (
ipVersionV6 = 6
)
// maxOriginChangesOnPage is the most origin changes the prefix page shows. A
// prefix with more shows the newest ones and says so.
const maxOriginChangesOnPage = 5000
// HealthCheckResponse represents the health check response.
type HealthCheckResponse struct {
Status string `json:"status"`
@@ -1534,27 +1538,24 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
routes := prefixRoutes(parsed, origins)
// Prepare template data
data := struct {
Prefix string
MaskLength int
IPVersion int
Routes []EnhancedRoute
Origins []*ASNInfo
PeerCount int
OriginCount int
CSPNonce string
}{
Prefix: parsed.String(),
MaskLength: parsed.Bits(),
IPVersion: ipVersionOf(parsed),
Routes: routesWithASPathHandles(routes),
Origins: originInfos,
PeerCount: len(routes),
OriginCount: len(originInfos),
CSPNonce: cspNonce(r.Context()),
changes, moreChanges, err := s.prefixOriginChanges(r.Context(), parsed)
if deadlinePassed(r.Context()) {
http.Error(w, requestTimeoutMessage, http.StatusGatewayTimeout)
return
}
if err != nil {
s.logger.Error("Failed to get the origin changes", "error", err)
http.Error(w, internalErrorMessage, http.StatusInternalServerError)
return
}
// Prepare template data
data := newPrefixDetailData(parsed, routes, originInfos, changes,
moreChanges, cspNonce(r.Context()))
// Check if context is still valid before writing response
select {
case <-r.Context().Done():
@@ -1606,6 +1607,61 @@ func (s *Server) originASNs(
return infos, nil
}
// prefixOriginChanges returns prefix's origin changes for the page, newest
// first, at most maxOriginChangesOnPage, and whether there are more.
func (s *Server) prefixOriginChanges(
ctx context.Context, prefix netip.Prefix,
) ([]routetable.OriginChange, bool, error) {
// One more is read to know whether there are more.
changes, err := s.db.GetOriginChanges(ctx, prefix, maxOriginChangesOnPage+1)
if err != nil {
return nil, false, err
}
more := len(changes) > maxOriginChangesOnPage
if more {
changes = changes[:maxOriginChangesOnPage]
}
return changes, more, nil
}
// prefixDetailData is what the prefix detail page is rendered with.
type prefixDetailData struct {
Prefix string
MaskLength int
IPVersion int
Routes []EnhancedRoute
Origins []*ASNInfo
PeerCount int
OriginCount int
OriginChanges []routetable.OriginChange
MoreOriginChanges bool
// MaxOriginChanges is the most origin changes the page shows.
MaxOriginChanges int
CSPNonce string
}
// newPrefixDetailData builds the prefix detail page's data.
func newPrefixDetailData(
parsed netip.Prefix, routes []LiveRoute, originInfos []*ASNInfo,
changes []routetable.OriginChange, moreChanges bool, nonce string,
) prefixDetailData {
return prefixDetailData{
Prefix: parsed.String(),
MaskLength: parsed.Bits(),
IPVersion: ipVersionOf(parsed),
Routes: routesWithASPathHandles(routes),
Origins: originInfos,
PeerCount: len(routes),
OriginCount: len(originInfos),
OriginChanges: changes,
MoreOriginChanges: moreChanges,
MaxOriginChanges: maxOriginChangesOnPage,
CSPNonce: nonce,
}
}
// prefixRoutes returns one route for each peer that carries prefix, by origin
// AS in the order of origins. Each origin's kept routes come first, newest
// first, with their AS path, next hop and time. Its other peers follow without