Skip to content

Admit blocked InsertTx calls in FIFO order and bound the waiter queue - #4184

Merged
masih merged 3 commits into
giga-1from
masih/1789479453-fifo-insert-admission
Sep 16, 2026
Merged

masih merged 3 commits into
giga-1from
masih/1789479453-fifo-insert-admission

Conversation

@masih

@masih masih commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

When the producer mempool is full, every InsertTx call blocked in producer.State.insertTx waited on the mempool Watch, so each ctrl.Updated() (a sealed block, a prune, a new block starting) woke all blocked inserters, each of which re-took the lock and re-evaluated IsFull. Under load-generator traffic the ingesting validator accumulates thousands of such inserters, which makes every mempool update O(N) in lock contention and wake-ups and is consistent with the goroutine and CPU excess seen on that node.

Blocked inserters now take a ticket in a FIFO queue on mempoolInner and wait on a per-ticket AtomicSend. Only the head of the queue is signalled, and only when the mempool has capacity: after a prune, after the head itself is admitted or leaves, and on close. A waiter whose context is cancelled removes its ticket and passes the turn on, and closing the mempool releases every waiter with ErrNotProducing as before. A new producer.Config field MaxPendingInserts (default 4096, wired wherever producer.Config is built) caps the number of blocked inserters; past that InsertTx fails immediately with the mempool-full error so clients back off instead of piling up goroutines. TryInsertTx is unchanged.

New tests cover FIFO order under concurrent inserters, one admission per freed slot, a cancelled waiter not blocking the queue, release on close, and the bound. The producer package passes under -race, and the touched packages pass go vet, golangci-lint v2.13.2 and the formatters.

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@masih
masih marked this pull request as ready for review September 15, 2026 13:52
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow Buf / buf (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 15, 2026, 3:40 PM

@cursor

cursor Bot commented Sep 15, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes hot-path mempool admission and blocking semantics under load; mis-tuning MaxPendingInserts or queue edge cases could affect tx ingestion on validators.

Overview
Replaces the producer mempool’s broadcast wait-on-full behavior with a FIFO admission queue for blocking InsertTx calls. Each waiter gets an insertTicket; only the queue head is signalled when capacity appears (prune, admission, or close), so mempool updates stay O(1) instead of waking every blocked inserter.

Adds Config.MaxPendingInserts (default 4096, wired in node setup and tests). Beyond that cap, InsertTx returns errMempoolFull immediately. TryInsertTx still does not queue and may take capacity ahead of queued waiters. Session teardown close releases all waiters; cancelled contexts dequeue without blocking followers. Admission logic is refactored into appendTx, with new tests for FIFO order, cancel, close, and the pending bound.

Reviewed by Cursor Bugbot for commit 7aec8f7. Bugbot is set up for automated code reviews on this repo. Configure here.

@masih
masih requested review from shemnon and wen-coding September 15, 2026 13:53

@seidroid seidroid Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FIFO admission queue is correct as far as I can trace it — every full→non-full transition (prune, dequeue, close) signals the head, and all admitted stores happen under the mempool lock, so there is no lost-wakeup or spin path. Four non-blocking points: the new MaxPendingInserts default is applied per-caller rather than at a choke point, TryInsertTx can still jump the queue, dequeue is O(N) under the hot lock, and pendingInserts is production state read only by tests.

Findings: 0 blocking | 4 non-blocking | 4 posted inline

Blockers

  • None at the file/PR level.

Non-blocking

  • None at the file/PR level.
  • 4 suggestion(s)/nit(s) flagged inline on specific lines.

Comment thread sei-tendermint/internal/autobahn/producer/state.go
nonce, ok := m.evmNonces[addr]
if !ok {
nonce = s.app.EvmNonce(addr)
if m.IsFull() && !waitIfFull {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] TryInsertTx is checked against IsFull() only and never consults isHead, so it takes capacity ahead of every queued waiter. With the old code all N waiters woke on each ctrl.Updated() and raced the TryInsertTx caller for the lock; now exactly one waiter is signalled per free slot and must re-acquire the lock, so a sustained TryInsertTx stream can win that race repeatedly, driving the head back through the t.admitted.Store(false) path each time.

These are both live RPC paths — broadcast_tx_async calls TryInsertTx and broadcast_tx_sync/broadcast_tx calls InsertTx (internal/rpc/core/mempool.go:61,90) — so under async load-generator traffic (the exact scenario in the PR description) sync broadcasters can be starved for an unbounded time, then eventually rejected once the queue hits MaxPendingInserts. The inline comment at line 340 acknowledges the interaction but the InsertTx godoc ("admitting blocked calls in arrival order") reads as a stronger guarantee than holds.

At minimum, scope the godoc to arrival order among blocked InsertTx calls; if sync broadcasters should not be starvable, TryInsertTx needs to respect a non-empty queue too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scoped the InsertTx godoc in 99d78dc: ordering is among blocked InsertTx calls, and TryInsertTx does not queue and may take freed capacity ahead of them. Keeping TryInsertTx semantics unchanged is a stated requirement of this PR; making it respect a non-empty queue is a behaviour change that should be decided separately.

// dequeue removes t from the admission queue and passes the turn to the next waiter.
func (mp *mempool) dequeue(m *mempoolInner, t *insertTicket) {
if i := slices.Index(m.waiters, t); i >= 0 {
m.waiters = slices.Delete(m.waiters, i, i+1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] slices.Delete(m.waiters, 0, 1) on the common (head) path memmoves the remaining len(m.waiters)-1 pointers while holding the mempool lock, so admission stays O(N) in queue depth — at the 4096 default that is a ~32 KiB copy on the hot lock for every admitted tx. It is far cheaper than the N goroutine wake-ups it replaces, but it is avoidable: utils.RingBuf (libs/utils/ringbuf.go) already exists in-tree, and a head index or linked list makes both isHead and the head dequeue O(1). The cancelled-waiter path can stay O(N) since it is rare.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed it is O(N) in queue depth, bounded by MaxPendingInserts (a ≤32 KiB memmove at the default) and already amortised against the per-tx CheckTx work. Leaving it as a slice for now to keep this PR minimal; happy to switch to utils.RingBuf or a head index if the memmove shows up in profiles.

type mempool struct {
inner utils.Watch[*mempoolInner]
// pendingInserts is the number of InsertTx calls blocked in the admission queue.
pendingInserts utils.AtomicSend[uint64]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] pendingInserts is production state whose only readers are mempool_test.go — nothing outside tests calls Load or Wait on it — and each Store allocates a fresh version plus a channel (utils.AtomicSend.Store), so it adds two allocations per enqueue and per dequeue to the hot path.

Given the PR is motivated by an operational incident (goroutine and CPU excess on an ingesting validator), queue depth and the over-bound rejection count are precisely what an operator would want to see. Publishing it through the existing autobahn metrics facility (internal/autobahn/data/metrics, which already has gauge/counter helpers) would make the field earn its place in the production struct rather than existing solely as a test synchronization handle.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metrics for this path are being added in the sibling PR #4179, which this PR must not depend on; exposing queue depth and over-bound rejections there (or as a follow-up once both land) seems the right place. pendingInserts is kept here as the deterministic synchronization handle the tests need to avoid sleeps.

@shemnon shemnon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@masih
masih enabled auto-merge September 15, 2026 15:00
@masih
masih added this pull request to the merge queue Sep 15, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 15, 2026
…fo-insert-admission

# Conflicts:
#	sei-tendermint/internal/autobahn/producer/mempool.go
#	sei-tendermint/internal/autobahn/producer/mempool_test.go
@codecov

codecov Bot commented Sep 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.59091% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.54%. Comparing base (7e3ca24) to head (7aec8f7).
⚠️ Report is 4 commits behind head on giga-1.

Files with missing lines Patch % Lines
...i-tendermint/internal/autobahn/producer/mempool.go 97.56% 2 Missing ⚠️
sei-tendermint/internal/autobahn/producer/state.go 83.33% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           giga-1    #4184      +/-   ##
==========================================
- Coverage   65.55%   65.54%   -0.01%     
==========================================
  Files        2081     2076       -5     
  Lines      157460   157131     -329     
==========================================
- Hits       103222   102999     -223     
+ Misses      54097    53991     -106     
  Partials      141      141              
Flag Coverage Δ
sei-chain-pr 70.80% <96.59%> (?)
sei-db ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
sei-tendermint/node/setup.go 63.06% <ø> (ø)
sei-tendermint/internal/autobahn/producer/state.go 83.87% <83.33%> (-0.92%) ⬇️
...i-tendermint/internal/autobahn/producer/mempool.go 94.83% <97.56%> (+4.98%) ⬆️

... and 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@masih
masih added this pull request to the merge queue Sep 16, 2026
Merged via the queue into giga-1 with commit d329acd Sep 16, 2026
67 of 69 checks passed
@masih
masih deleted the masih/1789479453-fifo-insert-admission branch September 16, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants