Skip to content

[fix][broker] Fix replicator getting stuck under rate limiter throttling and honor readBatchSize/maxReadSizeBytes on the default read path - #26005

Merged
lhotari merged 24 commits into
apache:masterfrom
void-ptr974:fix/replicator-rate-limiter-inflight
Jun 29, 2026
Merged

[fix][broker] Fix replicator getting stuck under rate limiter throttling and honor readBatchSize/maxReadSizeBytes on the default read path#26005
lhotari merged 24 commits into
apache:masterfrom
void-ptr974:fix/replicator-rate-limiter-inflight

Conversation

@void-ptr974

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

Copy link
Copy Markdown
Contributor

Motivation

PersistentReplicator.readMoreEntries() created an InFlightTask before checking the replicator dispatch rate limiter.

When replicator dispatch throttling was enabled and the rate limiter had no message or byte permits, the method scheduled a retry and returned without issuing cursor.asyncReadEntriesOrWait(...). The newly-created task stayed in entries == null, so it looked like a pending cursor read even though no read request existed.

On the next retry, hasPendingRead() returned true and the replicator stopped scheduling further reads. This could leave geo-replication stuck with backlog when replicator dispatch throttling is enabled.

In addition, readBatchSize/maxReadSizeBytes logic wasn't applied on the default read path when the rate limiter isn't configured.

Modifications

Compute producer and rate-limiter permits before creating the in-flight read task.

Create the InFlightTask only after confirming that a real cursor read will be issued. This preserves the invariant that an entries == null in-flight task corresponds to an actual pending cursor read.

Store the computed byte read limit on the InFlightTask, so the task carries both message and byte read limits without an extra wrapper object.

Added a unit test covering the no-permit rate-limiter path for both message and byte throttling, verifying that no pending in-flight read task is created.

Honor readBatchSize/maxReadSizeBytes on the default read path. This was previously ignored as a result of some earlier changes.

Verifying this change

This change added tests and can be verified as follows:

  • ./gradlew :pulsar-broker:test --tests org.apache.pulsar.broker.service.persistent.PersistentReplicatorInflightTaskTest.testRateLimiterWithoutPermitsDoesNotCreateInFlightTask
  • ./gradlew :pulsar-broker:test --tests org.apache.pulsar.broker.service.persistent.PersistentReplicatorInflightTaskTest.testCreateOrRecycleInFlightTaskIntoQueue
  • ./gradlew :pulsar-broker:test --tests org.apache.pulsar.broker.service.persistent.PersistentReplicatorInflightTaskTest.testMaybeCreateInFlightReadTask
  • ./gradlew :pulsar-broker:test --tests org.apache.pulsar.broker.service.ReplicatorRateLimiterTest.testReplicatorRateLimiterMessageReceivedAllMessages
  • ./gradlew :pulsar-broker:test --tests org.apache.pulsar.broker.service.ReplicatorRateLimiterTest.testReplicatorRateLimiterByBytes

Does this pull request potentially affect one of the following parts:

  • Dependencies (add or upgrade a dependency)
  • The public API
  • The schema
  • The default values of configurations
  • The threading model
  • The binary protocol
  • The REST endpoints
  • The admin CLI options
  • The metrics
  • Anything that affects deployment

@void-ptr974
void-ptr974 force-pushed the fix/replicator-rate-limiter-inflight branch from 7fed1bd to 16ad2d7 Compare June 12, 2026 01:59
@void-ptr974
void-ptr974 force-pushed the fix/replicator-rate-limiter-inflight branch from 16ad2d7 to 25ed1ac Compare June 12, 2026 02:29
@void-ptr974
void-ptr974 marked this pull request as ready for review June 12, 2026 02:38
@void-ptr974

Copy link
Copy Markdown
Contributor Author

Hi @poorbarcode, this one adjusts when PersistentReplicator creates in-flight read tasks under dispatch rate limiting. Could you help review the pending-read / permit invariant when you have time? Thanks!

@lhotari lhotari added the triage/lhotari/important lhotari's triaging label for important issues or PRs label Jun 15, 2026

@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

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

There are small details to address. I'd like to get this included in 4.2.3 release which will start very soon. I might just make the changes to this PR and get this merged. I hope that's fine with you.

@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, I addressed the remaining issue about code duplication and the regression related to the isWritable handling. I refactored the original code since the "AvailablePermits" concept was misleading. It's "ReadLimits" now which conveys the meaning in a better way.

In addition, there was a bug in the existing code where the readBatchSize logic had been disabled unless there was a rate limiter configured. That is now fixed since the getReadLimits method handles all logic related to the limits.

@lhotari
lhotari requested a review from poorbarcode June 24, 2026 23:46
@lhotari lhotari changed the title [fix][broker] Prevent replicator from getting stuck when dispatch rate limiter has no permits [fix][broker] Fix replicator getting stuck under rate limiter throttling and honor readBatchSize/maxReadSizeBytes on the default read path Jun 24, 2026
@void-ptr974

Copy link
Copy Markdown
Contributor Author

Thanks @poorbarcode and @lhotari. I pushed the remaining follow-ups.

  • Renamed InFlightTask.bytesToRead to maxBytesToRead. This should make it clearer that the field only carries the max byte limit for cursor.asyncReadEntriesOrWait(...); it is not involved in InFlightTask.isDone(), which still only depends on the read result entries and completed entries.
  • Converted testRateLimiterWithoutPermitsDoesNotCreateInFlightTask to use a TestNG @DataProvider, so the message-permit and byte-permit cases run as separate cases.
  • Added an integration test in OneWayReplicatorTest to cover the real replication path when the replicator dispatch rate limiter temporarily has no permits, and verify replication continues afterwards.

I also ran the focused broker tests locally multiple times, including the new integration test.

@void-ptr974

Copy link
Copy Markdown
Contributor Author

Thanks @poorbarcode and @lhotari. I pushed another follow-up to make the read-limit handling clearer.

What changed:

  • Changed the new integration test to use topic-level replicator dispatch rate policy, so the test does not affect other topics in the namespace.
  • Removed maxBytesToRead from InFlightTask.
  • Split the decision into two parts:
    • ReadLimits decides the next read size, including both message and byte limits.
    • InFlightTask only tracks the in-flight read/publish lifecycle.
  • readMoreEntries() now keeps the byte limit as a local value and passes it directly to cursor.asyncReadEntriesOrWait(...).
  • Updated the unit tests to validate read-limit decisions directly, including both message and byte limits.

The main idea is to avoid mixing a read request parameter into InFlightTask. maxBytesToRead only belongs to the next cursor read, while InFlightTask should only represent the pending/completed entries lifecycle. This should make the intent clearer and avoid an extra wrapper object/allocation on the read scheduling path.

@void-ptr974

Copy link
Copy Markdown
Contributor Author

Pushed a small cleanup follow-up in 8c4e573.

What changed:

  • Removed the @VisibleForTesting maybeGetReadLimitsForNextRead() wrapper and made ReadLimits private again.
  • Removed the tests that directly exercised that internal helper, so the read-limit decision and inFlightTasks locking protocol are no longer exposed only for tests.
  • Updated the schema-rewind comment to describe the readMoreEntries() scheduling path instead of referencing a removed helper.

The regression coverage remains in OneWayReplicatorTest.testReplicatorContinuesAfterRateLimiterHasNoPermits, which exercises the real replication path with replicator dispatch rate limiting and verifies replication continues afterwards. I also reran the focused broker tests, compile, and checkstyle locally.

@lhotari
lhotari merged commit abca10f into apache:master Jun 29, 2026
81 of 83 checks passed
lhotari added a commit that referenced this pull request Jun 29, 2026
…ing and honor readBatchSize/maxReadSizeBytes on the default read path (#26005)

Co-authored-by: Lari Hotari <lhotari@apache.org>
(cherry picked from commit abca10f)
lhotari added a commit that referenced this pull request Jun 29, 2026
…ing and honor readBatchSize/maxReadSizeBytes on the default read path (#26005)

Co-authored-by: Lari Hotari <lhotari@apache.org>
(cherry picked from commit abca10f)
lhotari added a commit to lhotari/pulsar that referenced this pull request Jun 29, 2026
Resolved conflict in PersistentReplicatorInflightTaskTest.java by combining the
apache#26005 read-scheduling test changes (from master) with this PR's added imports and
tests (testCursorRewindSkippedReadCompletesInFlightTask,
testReplicationRecoversAfterPublishFailureRewindWithInflightRead).
PersistentReplicator.java auto-merged: this PR's readEntriesComplete skip-branch fix
and apache#26005's getReadLimits read-batching are both retained.

Assisted-by: Claude Code (Claude Opus 4.8)
Claude-Session: https://claude.ai/code/session_01CMKzAniMcNHvk1jBh6bEKQ
lhotari added a commit to lhotari/pulsar that referenced this pull request Jun 30, 2026
… instead of removed helper

The skip-branch comment referenced acquirePermitsIfNotFetchingSchema(), which apache#26005
removed. Update it to point at readMoreEntries()'s read-scheduling path (the path that, under
the inFlightTasks lock, reads both the free read slot and the cursor read position), so the
concurrency explanation matches the current code. Comment-only change; no behavior change.

Addresses review feedback on PR apache#26106.
@lhotari

lhotari commented Jun 30, 2026

Copy link
Copy Markdown
Member

This PR makes another bug occur more frequently. The bug is fixed by #26106. Explanation in #26106 (comment)

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.

3 participants