Skip to content

[improve][broker] Avoid O(n) pending ack scans for size lookup#26019

Open
void-ptr974 wants to merge 5 commits into
apache:masterfrom
void-ptr974:pending-acks-map-size-o1
Open

[improve][broker] Avoid O(n) pending ack scans for size lookup#26019
void-ptr974 wants to merge 5 commits into
apache:masterfrom
void-ptr974:pending-acks-map-size-o1

Conversation

@void-ptr974

@void-ptr974 void-ptr974 commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Motivation

PendingAcksMap.size() currently computes the pending ack count by scanning all per-ledger pending ack maps. This makes a simple size lookup O(number of ledgers), even though callers only need the total number of pending ack entries.

On consumers with many pending acknowledgments, this adds unnecessary CPU cost to paths that inspect or drain pending acks.

Modifications

Maintain a cached pending ack count in PendingAcksMap.

The counter is updated when pending ack entries are added, removed, range-removed, or cleared. With this, PendingAcksMap.size() becomes an O(1) read instead of scanning nested maps.

The change keeps the existing TreeMap structure and only optimizes size accounting.

Correctness

PendingAcksMapTest verifies size accounting across these paths:

  • successful add
  • rejected add
  • add after close
  • duplicate add and updateRemainingUnacked
  • single-entry remove
  • batch/sticky-key checked remove, including a failed remove attempt
  • removeAndGet, including not-found
  • removeAllUpTo within one ledger and across multiple ledgers
  • forEachAndClear and forEachAndClose

Performance

Simple local size lookup microbenchmark on OpenJDK 25.0.2. The benchmark compares the previous map-scan size calculation with the cached counter read, both under a ReentrantReadWriteLock read lock.

Entries Ledgers Map scan Cached counter Speedup
65,536 1,024 6,454.5 ns/op 23.8 ns/op 271.7x
1,048,576 16,384 687,867.5 ns/op 23.7 ns/op 29,011.7x

The exact numbers are environment-dependent, but the result shows the intended change in complexity: size lookup no longer grows with the number of tracked ledgers.

Verifying this change

  • ./gradlew :pulsar-broker:test --tests org.apache.pulsar.broker.service.PendingAcksMapTest
  • ./gradlew :pulsar-broker:checkstyleMain

@lhotari lhotari left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM, thanks

@lhotari

lhotari commented Jun 13, 2026

Copy link
Copy Markdown
Member

Entries Ledgers Map scan Cached counter Speedup
65,536 1,024 6,454.5 ns/op 23.8 ns/op 271.7x
1,048,576 16,384 687,867.5 ns/op 23.7 ns/op 29,011.7x

In practice a single consumer won't be handling very many ledgers at once so this isn't a bottleneck or major waste of CPU. Regardless, this is a useful improvement.

try {
readLock.lock();
return pendingAcks.values().stream().mapToInt(TreeMap::size).sum();
return size;

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.

We could remove the read lock here if we make the size as volatile (and still update it while holding the lock).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, thanks for the suggestion. This makes size() even cheaper.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants