Compare commits

..
1 Commits
Author SHA1 Message Date
clawbot 5d98b2e73f Keep one entry per AS in the AS handler's batch (closes #217)
check / check (push) Canceled after 0s
HandleMessage added one entry to the batch for every AS of every path,
and waited for the write in progress once the batch held 300,000. In a
burst of the feed its queue then filled, and the streamer dropped
messages.

The batch is now a map from AS number to the earliest and the latest
time of its updates. It holds one entry per AS, so it no longer grows
with the feed's rate. HandleMessage never waits for a write.
asnBatchSize now counts ASes. Stop logs the entries of the batch being
written and of the next batch under "batch_entries". An AS in both
counts twice.

Model: opus-5-5
2026-11-02 00:48:27 +00:00
4 changed files with 33 additions and 32 deletions

No files matched your search

+8 -7
View File
@@ -638,14 +638,15 @@ What happens at each limit:
- The AS handler's batch holds one entry per AS, with the earliest and the
latest time of its updates. About 80,000 ASes appear in the feed, so the
batch takes at most about 10 MiB, however fast the feed runs.
- The peer and prefix handlers' batches stop at ten times their batch size:
100,000 updates for the peer handler (`peerBatchMaxSize`, about 10 MiB),
and 250,000 for the prefix handler (`prefixBatchMaxSize`, about 15 MiB).
At that size the handler takes nothing more off its queue until the write
ends. The queue then fills, and the streamer drops messages as above.
- The peer handler's batch stops at 100,000 updates (`peerBatchMaxSize`),
about 10 MiB.
- The prefix handler's batch stops at 250,000 updates
(`prefixBatchMaxSize`), about 15 MiB.
With the batches being written, the three handlers' batches take at most
about 0.1 GiB of the Go heap.
At that maximum the peer or prefix handler takes nothing more off its queue
until the write ends. The queue then fills, and the streamer drops messages
as above. With the batches being written, the three handlers' batches take
at most about 0.1 GiB of the Go heap.
With `DEBUG=routewatch` the daemon logs a `System stats` line every 60 seconds
with the goroutine count and Go memory figures.
+4 -4
View File
@@ -36,10 +36,10 @@ tested.
# Completed Steps
- 2026-11-02: the AS handler never waits for a database write. Its batch holds
one entry per AS, with the earliest and the latest time of its updates, so it
no longer grows with the feed's rate. It used to hold one entry for each AS of
each path and to wait once it held 300,000. In a burst of the feed its queue
then filled, and the streamer dropped messages for it (closes #217)
one entry per AS, with the earliest and the latest time of its updates. So the
batch no longer grows with the feed's rate. It used to hold one entry for each
AS of each path and to wait once it held 300,000. In a burst of the feed its
queue then filled, and the streamer dropped messages for it (closes #217)
- 2026-10-27: the tests of `internal/routewatch` no longer fail on a loaded
host. Five of them gave `HandleMessage` two seconds to apply a full prefix
batch, which under the race detector can take longer. A test now waits up to
+14 -14
View File
@@ -27,16 +27,16 @@ const (
asnBatchTimeout = 2 * time.Second
)
// ASHandler records every AS in the AS paths of BGP UPDATE messages. It
// collects, for each AS, the earliest and the latest time of its updates in a
// batch. The flush goroutine writes the batch to the database when it holds
// asnBatchSize ASes, and every asnBatchTimeout.
// ASHandler records every AS in the AS paths of BGP UPDATE messages. Its batch
// holds the earliest and the latest time of each AS's updates. The flush
// goroutine writes the batch to the database when it holds asnBatchSize ASes,
// and every asnBatchTimeout.
type ASHandler struct {
db database.Store
logger *logger.Logger
// Batching. mu guards batch and writing only: the flush goroutine takes
// the batch under mu and writes it after releasing mu, so HandleMessage
// Batching. mu guards batch and writing only. The flush goroutine takes
// the batch under mu and writes it after releasing mu. So HandleMessage
// never waits for a database write.
mu sync.Mutex
// batch holds the earliest and the latest time of the updates for each AS
@@ -56,8 +56,7 @@ type ASHandler struct {
// NewASHandler creates and returns a new ASHandler instance. It initializes
// the batching system and starts a background goroutine that periodically
// writes the collected ASes to the database. The caller must call Stop when
// finished to ensure all pending ASes are written and the background goroutine
// is terminated.
// finished, to write the pending ASes and end the background goroutine.
func NewASHandler(db database.Store, logger *logger.Logger) *ASHandler {
return newASHandler(db, logger, asnBatchTimeout)
}
@@ -101,7 +100,7 @@ func (h *ASHandler) QueueCapacity() int {
// HandleMessage adds the time of msg to the batch entry of each AS in its AS
// path, the origin AS and the members of an AS set included. When the batch
// holds asnBatchSize ASes, it wakes the flush goroutine to write it.
// holds asnBatchSize ASes, HandleMessage wakes the flush goroutine to write it.
func (h *ASHandler) HandleMessage(msg *ristypes.RISMessage) {
h.mu.Lock()
defer h.mu.Unlock()
@@ -126,10 +125,11 @@ func (h *ASHandler) HandleMessage(msg *ristypes.RISMessage) {
}
}
// Stop stops the flush goroutine, which writes the ASes the handler holds before
// it returns, and waits for it until ctx ends. Once ctx has ended the flush
// goroutine skips that last write, and a write still running is lost when the
// process exits, so Stop logs how many ASes are not written.
// Stop stops the flush goroutine and waits for it until ctx ends. The flush
// goroutine writes the batch before it returns, unless ctx has ended. A write
// still running when the process exits is lost. Stop logs how many entries are
// not written, in the batch being written and in the next batch. An AS in both
// batches counts twice.
func (h *ASHandler) Stop(ctx context.Context) {
h.stopCh <- ctx
@@ -144,7 +144,7 @@ func (h *ASHandler) Stop(ctx context.Context) {
if unwritten > 0 {
h.logger.Error("Stop deadline passed before the AS handler wrote "+
"its ASes", "asns", unwritten)
"its batches", "batch_entries", unwritten)
}
}
@@ -178,7 +178,7 @@ type stopLog struct {
Updates int `json:"updates"`
TableChanges int `json:"table_changes"`
Paths int `json:"paths"`
ASNs int `json:"asns"`
BatchEntries int `json:"batch_entries"`
}
// newErrorLogger returns a logger that writes only errors, as JSON lines, to
@@ -497,12 +497,12 @@ func TestPrefixHandlerStopCountsTheTableChangesOfAWriteInProgress(t *testing.T)
}
// TestASHandlerSkipsItsLastWriteAfterTheStopDeadline also checks what Stop
// logs: the asnBatchSize ASes of the held write and testASNB, handled since,
// are not written.
// logs: the asnBatchSize entries of the held write and the entry for testASNB,
// handled since, are not written.
func TestASHandlerSkipsItsLastWriteAfterTheStopDeadline(t *testing.T) {
t.Parallel()
const wantASNs = asnBatchSize + 1
const wantEntries = asnBatchSize + 1
var logs bytes.Buffer
@@ -519,9 +519,9 @@ func TestASHandlerSkipsItsLastWriteAfterTheStopDeadline(t *testing.T) {
t.Errorf("AS%d was written after the stop deadline", testASNB)
}
if logged := lastLogged(t, &logs); logged.ASNs != wantASNs {
t.Errorf("Stop logged %d ASes not written, want %d",
logged.ASNs, wantASNs)
if logged := lastLogged(t, &logs); logged.BatchEntries != wantEntries {
t.Errorf("Stop logged %d batch entries not written, want %d",
logged.BatchEntries, wantEntries)
}
}