Write each handler batch from its flush goroutine, outside the mutex (closes #65)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
The AS, peer and prefix handlers wrote their batch to the database while holding the mutex HandleMessage needs, so while a write ran nothing was taken off their queues and the streamer dropped updates. Each handler now writes only from its flush goroutine: it takes the batch under the mutex, leaves an empty one in its place, and writes after releasing it. A full batch wakes the flush goroutine. While a write runs the next batch may grow to ten batch sizes; at that size HandleMessage waits, so the queue fills and the streamer drops as before. README "Memory" gives the numbers and their cost. Model: opus-5-5
This commit was merged in pull request #83.
This commit is contained in:
@@ -24,6 +24,12 @@ const (
|
||||
// prefixBatchSize is the number of prefix updates to batch together
|
||||
prefixBatchSize = 25000
|
||||
|
||||
// prefixBatchMaxSize is the most prefix updates the batch holds while the
|
||||
// flush goroutine writes the previous one: ten batches, about 60 MiB
|
||||
// (README "Memory"). At this size HandleMessage waits until the flush
|
||||
// goroutine takes the batch.
|
||||
prefixBatchMaxSize = 250000
|
||||
|
||||
// prefixBatchTimeout is the maximum time to wait before flushing a batch
|
||||
// DO NOT reduce this timeout - larger batches are more efficient
|
||||
prefixBatchTimeout = 1 * time.Second
|
||||
@@ -40,12 +46,16 @@ type PrefixHandler struct {
|
||||
logger *logger.Logger
|
||||
metrics *metrics.Tracker
|
||||
|
||||
// Batching
|
||||
mu sync.Mutex
|
||||
batch []prefixUpdate
|
||||
lastFlush time.Time
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
// Batching. mu guards batch only: the flush goroutine takes the batch
|
||||
// under mu and writes it after releasing mu, so HandleMessage waits for
|
||||
// a database write only while batch is at prefixBatchMaxSize.
|
||||
mu sync.Mutex
|
||||
batch []prefixUpdate
|
||||
batchTaken sync.Cond // signalled when the flush goroutine takes batch; its L is &mu
|
||||
batchFullCh chan struct{} // wakes the flush goroutine when batch is full
|
||||
lastFlush time.Time // used only by the flush goroutine
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type prefixUpdate struct {
|
||||
@@ -60,12 +70,14 @@ type prefixUpdate struct {
|
||||
// NewPrefixHandler creates a new batched prefix tracking handler
|
||||
func NewPrefixHandler(db database.Store, logger *logger.Logger) *PrefixHandler {
|
||||
h := &PrefixHandler{
|
||||
db: db,
|
||||
logger: logger,
|
||||
batch: make([]prefixUpdate, 0, prefixBatchSize),
|
||||
lastFlush: time.Now(),
|
||||
stopCh: make(chan struct{}),
|
||||
db: db,
|
||||
logger: logger,
|
||||
batch: make([]prefixUpdate, 0, prefixBatchSize),
|
||||
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)
|
||||
@@ -105,6 +117,13 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
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.batch) >= prefixBatchMaxSize {
|
||||
h.batchTaken.Wait()
|
||||
}
|
||||
|
||||
// Withdrawals are queued before announcements, because the batch keeps
|
||||
// each peer's last update for a prefix: a message that lists a prefix as
|
||||
// both withdrawn and announced keeps the route (RFC 4271 section 4.3).
|
||||
@@ -140,13 +159,19 @@ func (h *PrefixHandler) HandleMessage(msg *ristypes.RISMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to flush
|
||||
// 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 prefixBatchMaxSize.
|
||||
if len(h.batch) >= prefixBatchSize {
|
||||
h.flushBatchLocked()
|
||||
select {
|
||||
case h.batchFullCh <- struct{}{}:
|
||||
default: // already woken
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushLoop runs in a goroutine and periodically flushes batches
|
||||
// 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 *PrefixHandler) flushLoop() {
|
||||
defer h.wg.Done()
|
||||
ticker := time.NewTicker(prefixBatchTimeout)
|
||||
@@ -155,30 +180,46 @@ func (h *PrefixHandler) flushLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
h.mu.Lock()
|
||||
if time.Since(h.lastFlush) >= prefixBatchTimeout {
|
||||
h.flushBatchLocked()
|
||||
h.flushBatch()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
case <-h.batchFullCh:
|
||||
h.flushBatch()
|
||||
case <-h.stopCh:
|
||||
// Final flush
|
||||
h.mu.Lock()
|
||||
h.flushBatchLocked()
|
||||
h.mu.Unlock()
|
||||
h.flushBatch()
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flushBatchLocked flushes the prefix batch to the database (must be called with mutex held)
|
||||
func (h *PrefixHandler) flushBatchLocked() {
|
||||
// takeBatch returns the pending updates and leaves an empty batch in their
|
||||
// place for HandleMessage to fill.
|
||||
func (h *PrefixHandler) takeBatch() []prefixUpdate {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
if len(h.batch) == 0 {
|
||||
return nil
|
||||
}
|
||||
batch := h.batch
|
||||
h.batch = make([]prefixUpdate, 0, prefixBatchSize)
|
||||
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 *PrefixHandler) flushBatch() {
|
||||
batch := h.takeBatch()
|
||||
if len(batch) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
batchSize := len(h.batch)
|
||||
batchSize := len(batch)
|
||||
|
||||
// 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
|
||||
@@ -186,7 +227,7 @@ func (h *PrefixHandler) flushBatchLocked() {
|
||||
// updates within one second carry the same one.
|
||||
type prefixAndPeer struct{ prefix, peer string }
|
||||
lastUpdates := make(map[prefixAndPeer]prefixUpdate)
|
||||
for _, update := range h.batch {
|
||||
for _, update := range batch {
|
||||
lastUpdates[prefixAndPeer{update.prefix, update.peer}] = update
|
||||
}
|
||||
|
||||
@@ -260,8 +301,6 @@ func (h *PrefixHandler) flushBatchLocked() {
|
||||
"duration_ms", elapsed.Milliseconds(),
|
||||
)
|
||||
|
||||
// Clear batch
|
||||
h.batch = h.batch[:0]
|
||||
h.lastFlush = time.Now()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user