Skip to content

fix(p2p): trim the simple priority queue below its limit - #1597

Open
QuantumExplorer wants to merge 2 commits into
devfrom
fix/p2p-queue-trim-below-limit
Open

QuantumExplorer wants to merge 2 commits into
devfrom
fix/p2p-queue-trim-below-limit

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Basic explanation

When the router's simple priority queue overflows, it sorts its whole backlog to find the lowest-ranked envelopes to drop, and it accepts and delivers nothing while it sorts. It trimmed back to exactly its limit, so the next overflowing push had to sort again. The vote channel's capacity of 4096 gives a limit of 16,777,216 envelopes, and at that size a full queue took 2 to 3 seconds per push with nobody draining it.

This PR trims to 15/16 of the limit instead, so one sort covers the next 1/16 of the limit's worth of pushes.

This changes what a full queue does under a flood, and reviewers should decide whether that is wanted. The old queue's sorting slowed senders down to about the rate the consumer took envelopes, so it dropped very little. With this PR senders rarely wait, and the queue drops what the consumer cannot keep up with. Those drops are the same silent kind the queue already makes: there is no drop counter and no peer error.

Stacked on #1596, so merge that first.

Detailed discussion

What was done

  • internal/p2p/rqueue.go: the overflow path moves into simpleQueue.trim. It sorts as before, notifies the dropped envelopes as before, and keeps maxSize - maxSize/16. Queues with a limit under 16 have no margin and trim exactly what overflowed. It also clears the dropped slots, since later pushes would otherwise reclaim them only one at a time and up to 1/16 of the limit would stay reachable.
  • internal/p2p/rqueue_test.go: TestSimpleQueueTrimDropsLowestRankedBelowMaxSize pins the size after a trim, that the oldest envelopes are the ones dropped, that their senders are released and their slots cleared, and that the result is still a valid heap.
  • internal/p2p/load_router_queue_test.go: the ceiling comment follows the new trim. The ceiling itself does not change, because the heap still never holds more than maxSize once a push finishes.

Trade-offs to weigh

  • Senders no longer wait. The old back-pressure looks accidental, but it had an effect: a full consensus queue slowed the connections feeding it. TestLoadConsensusChannelsDoNotSilentlyTimeOutOnEnqueue argues that a full consensus queue should make the sender wait rather than quietly drop, because nothing counts the drops. This PR moves away from that for floods that fill the heap. On the other hand, one flooding peer no longer holds up every other peer on a queue they all share.
  • Larger drops at once. Each overflow drops up to 1/16 of the limit, about 1M envelopes on the vote channel. If a flood stops right after an overflow, those envelopes are gone where the old queue would have kept most of them.
  • Sorting still pauses the queue. At the vote channel's size each sort still takes a few seconds; it just happens once per roughly 1M pushes instead of on most pushes. That is why delivery in the 4096 row with an unlimited flood stays well under the consumer's rate.
  • All users of the queue are affected. simple-priority is the default p2p.queue-type, and the per-peer outbound queues use the same code (queueBufferDefault is 32, so a limit of 1024 and a margin of 64). The 1/16 is a judgement call, not a tuned value.

A drop counter on this queue would make any of these choices visible to operators. It is not part of this PR.

Testing

All measurements are scratch runs that are not committed: macOS on 30 cores, no -race, empty votes, one run per cell over a 20-second window, and the queue already full at the start.

Pushes accepted per second with nobody draining:

Capacity Limit Before #1596 #1596 This PR
128 16,384 1,350 989 1,262,491
1024 1,048,576 13.7 9.5 371,888
4096 16,777,216 0.4 (9 pushes) 0.3 (7 pushes) 186,697

Accepted and delivered per second, with a consumer limited to 5,000 per second. Both that rate and the 50,000 per second flood are arbitrary choices:

Capacity Flood Before #1596 #1596 This PR
1024 unlimited 2,685 / 2,696 2,666 / 2,677 418,345 / 4,890
4096 unlimited 313 / 411 296 / 391 291,701 / 1,245
1024 50,000/s 3,230 / 3,220 3,348 / 3,351 47,095 / 4,930
4096 50,000/s 316 / 411 549 / 642 40,975 / 4,197

Accepted minus delivered is roughly what was dropped, since the heap cannot grow past its limit. Drops were not counted directly.

Tests under -race, each run the number of times shown:

-cpu Runs of each test Failures Highest retained (ceiling 84)
default 500 0 80
2 300 0 80

The tests were TestLoadRouterSharedQueueOccupancyAndDrops, TestSimpleQueueTrimDropsLowestRankedBelowMaxSize, TestSimpleQueue and TestSimplePriorityQueueDoesNotPopWhenOutputIsFull.

Reverting the margin, the slot clearing or the delivery notification in trim each makes the new unit test fail. The other queue and router tests in internal/p2p pass under -race; gofmt and go vet are clean.

Not tested: a real multi-node network under flood, or the effect on consensus progress. These numbers only cover the queue on its own.

Breaking changes

None to APIs or the protocol. The runtime behaviour under a flood changes as described above.

Prior work

🤖 Generated with Claude Code

PR Hygiene · fb3e02b

  • Bots — thepastaclaw ✓
  • Self-review — post /self-reviewed
  • Build green
  • Approvals
    • files with no dedicated owner (internal/p2p/load_router_queue_test.go, internal/p2p/rqueue.go, internal/p2p/rqueue_test.go) — lklimek or shumkov

When every merge requirement is met, the PR Hygiene check passes. Reviewer limits do not block merging; other required GitHub checks and protections still apply.

QuantumExplorer and others added 2 commits November 5, 2026 18:04
The simple priority queue trimmed its heap on push only when the signal
channel was already pending. A push that refilled an empty signal skipped
the trim, so with nobody draining, the heap ran past maxSize until the
next 10ms tick. Instrumented under -race, a 2560-envelope flood at
capacity 8 took the heap 7 to 18 envelopes past maxSize every time.

TestLoadRouterSharedQueueOccupancyAndDrops checks that the queue retains
no more than maxSize + cap(input) + cap(output) and failed when the drain
caught that overshoot (85 and 87 retained against a ceiling of 84). The
ceiling is right for the intended queue, so the queue now trims after
every push and the ticker only re-signals. The test comment spells out
why the envelope the run loop holds between input and heap cannot add to
the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An overflowing heap was sorted whole and trimmed back to exactly maxSize,
so the next overflowing push had to sort again, and the run loop accepts
and delivers nothing while it sorts. The vote channel's capacity of 4096
gives a limit of 16,777,216 envelopes, where a full queue took 2 to 3
seconds per push with nobody draining it.

Trim to 15/16 of the limit so one sort covers the pushes that follow, and
clear the dropped slots so those envelopes can be collected. A full queue
at that capacity now accepts about 187,000 pushes per second.

This changes behaviour under a flood. The cost of sorting used to slow
senders to about the consumer's rate, so little was dropped. Senders now
rarely wait, and the queue drops what the consumer cannot keep up with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@thepastaclaw

thepastaclaw commented Nov 7, 2026 •

Copy link
Copy Markdown

✅ Final review complete — no blockers (commit fb3e02b) · triage: low

@QuantumExplorer
QuantumExplorer marked this pull request as ready for review November 8, 2026 22:23

@thepastaclaw thepastaclaw 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.

Final validation — Phase 1 + Phase 2

At fb3e02b, the trim implementation preserves the post-push size bound, completes dropped-envelope notifications, and clears discarded references, with focused unit coverage. One non-blocking suggestion remains: expose the materially larger overflow batches through drop accounting. This was a static review; current build, test, lint, and analysis checks pass, with the automated review itself still pending.

🟡 1 suggestion(s)

Review provenance

Source: reviewer 1: glm-5.3-flash (agent: phase1-reviewer, role: general); reviewer 2: gpt-6.1-sol (agent: phase2-reviewer, role: general); reviewer 3: gpt-6.1-sol (agent: phase2-reviewer, role: tenderdash-consensus-security); final verifier: gpt-6.1-sol (agent: sol-verifier, role: final-verifier)

  • Triage: low by gpt-6.1-sol (effort low) — The diff makes a small, contained change to queue overflow trimming with focused tests, rather than a large or intricate change to a critical surface.
  • Phase 1 reviewers: glm-5.3-flash — general (completed, effort high); agent phase1-reviewer
  • Phase 1 model: glm-5.3-flash — zai quota: 5h 99% left, weekly 28% left; passed over gemini-3.8-flash-high (antigravity quota lookup failed)
  • Fresh verifier: gpt-6.1-sol — final-verifier; agent sol-verifier
  • Phase 2 reviewers: gpt-6.1-sol — general (completed, effort medium); agent phase2-reviewer, gpt-6.1-sol — tenderdash-consensus-security (completed, effort medium); agent phase2-reviewer
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `internal/p2p/rqueue.go`:
- [SUGGESTION] internal/p2p/rqueue.go:127-135: Count the larger overflow batches
  Each overflow now discards maxSize/16 + 1 envelopes rather than one: 1,048,577 on the vote channel and 65 on a per-peer outbound queue. Neither this loop nor its caller records those drops. Even when a flood stops immediately after an overflow, the new policy discards a substantial backlog that the base retained, without giving operators a way to distinguish local eviction from loss elsewhere.

  The missing accounting predates this PR, but the changed eviction policy materially increases its operational impact. The sibling pqScheduler already increments PeerQueueDroppedMsgs with a ch_id label, and Router.createQueueFactory has r.metrics available. Pass metrics into the simple queue and count discarded envelopes by channel, preferably aggregating each batch before updating the counters to avoid a metric operation per envelope in million-message trims. Add coverage for the counts alongside the existing notification and reference-clearing assertions. This is an observability suggestion, not evidence that the queue previously guaranteed lossless consensus delivery.
Out-of-scope follow-up suggestions (1)

These are valid observations, but they are outside this PR's scope and should be handled in separate issues or author/maintainer-requested PRs rather than blocking this review.

  • Bound per-peer admission before the shared consensus backlog — Router.receivePeer admits decoded envelopes into the shared channel queue before the consensus reactor applies its per-peer rate limits. Those downstream limits therefore do not protect shared backlog capacity from a flooding peer. This ordering already exists in the base; the queue throughput measurements do not establish honest-peer consensus progress under a flood.
    • Follow-up: A separate follow-up PR should bound per-peer admission before the shared backlog and add a multi-peer flood test that verifies honest consensus progress.

Comment thread internal/p2p/rqueue.go
Comment on lines +127 to +135
func (q *simpleQueue) trim(pq priorityQueue) priorityQueue {
sort.Sort(pq)
keep := q.maxSize - q.maxSize/simpleQueueTrimFraction
for _, dropped := range pq[keep:] {
dropped.envelope.NotifyDelivery()
}
// Later pushes reclaim these slots only one at a time, so release the
// dropped envelopes now.
clear(pq[keep:])

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: Count the larger overflow batches

Each overflow now discards maxSize/16 + 1 envelopes rather than one: 1,048,577 on the vote channel and 65 on a per-peer outbound queue. Neither this loop nor its caller records those drops. Even when a flood stops immediately after an overflow, the new policy discards a substantial backlog that the base retained, without giving operators a way to distinguish local eviction from loss elsewhere.

The missing accounting predates this PR, but the changed eviction policy materially increases its operational impact. The sibling pqScheduler already increments PeerQueueDroppedMsgs with a ch_id label, and Router.createQueueFactory has r.metrics available. Pass metrics into the simple queue and count discarded envelopes by channel, preferably aggregating each batch before updating the counters to avoid a metric operation per envelope in million-message trims. Add coverage for the counts alongside the existing notification and reference-clearing assertions. This is an observability suggestion, not evidence that the queue previously guaranteed lossless consensus delivery.

source: glm-5.3-flash (phase1-reviewer: general)

Base automatically changed from test/p2p-shared-queue-ceiling to dev November 9, 2026 14:58
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Bots are done — your move: post /self-reviewed.
Full checklist in the description.

@github-actions github-actions Bot added the waiting-self-review Waiting for the author to post /self-reviewed label Nov 9, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-self-review Waiting for the author to post /self-reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants