This is an automated email from the ASF dual-hosted git repository.
lhotari pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pulsar.git
The following commit(s) were added to refs/heads/master by this push:
new abca10f9ce9 [fix][broker] Fix replicator getting stuck under rate
limiter throttling and honor readBatchSize/maxReadSizeBytes on the default read
path (#26005)
abca10f9ce9 is described below
commit abca10f9ce93da79a2842455dafda707042d42fa
Author: void-ptr974 <[email protected]>
AuthorDate: Tue Jun 30 00:23:12 2026 +0800
[fix][broker] Fix replicator getting stuck under rate limiter throttling
and honor readBatchSize/maxReadSizeBytes on the default read path (#26005)
Co-authored-by: Lari Hotari <[email protected]>
---
.../persistent/GeoPersistentReplicator.java | 6 +-
.../service/persistent/PersistentReplicator.java | 177 ++++++--------
.../broker/service/OneWayReplicatorTest.java | 80 +++++++
.../PersistentReplicatorInflightTaskTest.java | 265 +++++++++++++--------
4 files changed, 317 insertions(+), 211 deletions(-)
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/GeoPersistentReplicator.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/GeoPersistentReplicator.java
index 84e936256a8..c73a7d04bc1 100644
---
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/GeoPersistentReplicator.java
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/GeoPersistentReplicator.java
@@ -263,9 +263,9 @@ public class GeoPersistentReplicator extends
PersistentReplicator {
* Explain the result of the race-condition between:
* - {@link #readMoreEntries}
* - {@link
#beforeTerminateOrCursorRewinding(ReasonOfWaitForCursorRewinding)}
- * Since {@link #acquirePermitsIfNotFetchingSchema} and
- * {@link
#beforeTerminateOrCursorRewinding(ReasonOfWaitForCursorRewinding)} acquire the
- * same lock, it is safe.
+ * Since the read scheduling path in {@link
#readMoreEntries()} and
+ * {@link
#beforeTerminateOrCursorRewinding(ReasonOfWaitForCursorRewinding)} update
in-flight
+ * read state under the same lock, it is safe.
*/
beforeTerminateOrCursorRewinding(ReasonOfWaitForCursorRewinding.Fetching_Schema);
inFlightTask.incCompletedEntries();
diff --git
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentReplicator.java
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentReplicator.java
index e3d2ec71150..8444205be55 100644
---
a/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentReplicator.java
+++
b/pulsar-broker/src/main/java/org/apache/pulsar/broker/service/persistent/PersistentReplicator.java
@@ -39,7 +39,6 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
-import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import org.apache.bookkeeper.mledger.AsyncCallbacks;
@@ -93,7 +92,7 @@ public abstract class PersistentReplicator extends
AbstractReplicator
protected Optional<DispatchRateLimiter> dispatchRateLimiter =
Optional.empty();
private final Object dispatchRateLimiterLock = new Object();
- private int readBatchSize;
+ private volatile int readBatchSize;
private final int readMaxSizeBytes;
private final int producerQueueThreshold;
@@ -141,10 +140,8 @@ public abstract class PersistentReplicator extends
AbstractReplicator
this.expiryMonitor = new PersistentMessageExpiryMonitor(localTopic,
Codec.decode(cursor.getName()), cursor, null);
- readBatchSize = Math.min(
- producerQueueSize,
-
localTopic.getBrokerService().pulsar().getConfiguration().getDispatcherMaxReadBatchSize());
- readMaxSizeBytes =
localTopic.getBrokerService().pulsar().getConfiguration().getDispatcherMaxReadSizeBytes();
+ readBatchSize = getMaxReadBatchSize();
+ readMaxSizeBytes =
brokerService.pulsar().getConfiguration().getDispatcherMaxReadSizeBytes();
producerQueueThreshold = (int) (producerQueueSize * 0.9);
this.initializeDispatchRateLimiterIfNeeded();
@@ -152,6 +149,10 @@ public abstract class PersistentReplicator extends
AbstractReplicator
startProducer();
}
+ private int getMaxReadBatchSize() {
+ return Math.min(producerQueueSize,
brokerService.pulsar().getConfiguration().getDispatcherMaxReadBatchSize());
+ }
+
@Override
protected void setProducerAndTriggerReadEntries(Producer<byte[]> producer)
{
/**
@@ -218,68 +219,58 @@ public abstract class PersistentReplicator extends
AbstractReplicator
this.cursor.setInactive();
}
- @Data
- @AllArgsConstructor
- private static class AvailablePermits {
- private int messages;
- private long bytes;
-
- /**
- * messages, bytes
- * 0, O: Producer queue is full, no permits.
- * -1, -1: Rate Limiter reaches limit.
- * >0, >0: available permits for read entries.
- */
- public boolean isExceeded() {
- return messages == -1 && bytes == -1;
- }
-
+ private record ReadLimits(int messages, long bytes) {
public boolean isReadable() {
return messages > 0 && bytes > 0;
}
}
/**
- * Calculate available permits for read entries.
+ * Calculate read limits for a read operation. Takes the rate limiter into
account if it's enabled.
+ * Also limits to current readBatchSize and readMaxSizeBytes.
*/
- private AvailablePermits getRateLimiterAvailablePermits(int
availablePermits) {
+ private ReadLimits getReadLimits(int permits) {
// return 0, if Producer queue is full, it will pause read entries.
- if (availablePermits <= 0) {
+ if (permits <= 0) {
log.debug()
- .attr("availablePermits", availablePermits)
+ .attr("permits", permits)
.log("Producer queue is full, pausing reads");
- return new AvailablePermits(0, 0);
+ return new ReadLimits(0, 0);
}
- long availablePermitsOnMsg = -1;
- long availablePermitsOnByte = -1;
+ long readLimitOnMsg;
+ long readLimitOnByte;
// handle rate limit
if (dispatchRateLimiter.isPresent() &&
dispatchRateLimiter.get().isDispatchRateLimitingEnabled()) {
DispatchRateLimiter rateLimiter = dispatchRateLimiter.get();
- // if dispatch-rate is in msg then read only msg according to
available permit
- availablePermitsOnMsg =
rateLimiter.getAvailableDispatchRateLimitOnMsg();
- availablePermitsOnByte =
rateLimiter.getAvailableDispatchRateLimitOnByte();
- // no permits from rate limit
- if (availablePermitsOnByte == 0 || availablePermitsOnMsg == 0) {
+ // rateLimiter returns -1 if there is no rate limit configured
+ readLimitOnMsg = rateLimiter.getAvailableDispatchRateLimitOnMsg();
+ readLimitOnByte =
rateLimiter.getAvailableDispatchRateLimitOnByte();
+ // no permits from rate limit when either limit is 0
+ if (readLimitOnByte == 0 || readLimitOnMsg == 0) {
log.debug()
.attr("dispatchRateOnMsg",
rateLimiter.getDispatchRateOnMsg())
.attr("dispatchRateOnByte",
rateLimiter.getDispatchRateOnByte())
- .attr("backoffMs", MESSAGE_RATE_BACKOFF_MS)
- .log("Message-read exceeded topic replicator
message-rate, scheduling after a delay");
- return new AvailablePermits(-1, -1);
+ .attr("readLimitOnMsg", readLimitOnMsg)
+ .attr("readLimitOnByte", readLimitOnByte)
+ .log("Message-read exceeded topic replicator rate
limit");
+ return new ReadLimits(-1, -1);
}
+ // use given permits if no rate limit configured, otherwise limit
to returned rate limiter permits
+ readLimitOnMsg = readLimitOnMsg == -1 ? permits :
Math.min(permits, readLimitOnMsg);
+ // use readMaxSizeBytes if no rate limit configured, otherwise
limit to returned rate limiter permits
+ readLimitOnByte = readLimitOnByte == -1 ? readMaxSizeBytes :
Math.min(readMaxSizeBytes, readLimitOnByte);
+ } else {
+ readLimitOnMsg = permits;
+ readLimitOnByte = readMaxSizeBytes;
}
- availablePermitsOnMsg =
- availablePermitsOnMsg == -1 ? availablePermits :
Math.min(availablePermits, availablePermitsOnMsg);
- availablePermitsOnMsg = Math.min(availablePermitsOnMsg, readBatchSize);
+ // limit messages to current read batch size
+ readLimitOnMsg = Math.min(readLimitOnMsg, readBatchSize);
- availablePermitsOnByte =
- availablePermitsOnByte == -1 ? readMaxSizeBytes :
Math.min(readMaxSizeBytes, availablePermitsOnByte);
-
- return new AvailablePermits((int) availablePermitsOnMsg,
availablePermitsOnByte);
+ return new ReadLimits((int) readLimitOnMsg, readLimitOnByte);
}
public void disconnectIfNoTrafficAndBacklog() {
@@ -310,54 +301,51 @@ public abstract class PersistentReplicator extends
AbstractReplicator
if (state.equals(Terminated) || state.equals(Terminating)) {
return;
}
- // Acquire permits and check state of producer.
- InFlightTask newInFlightTask = acquirePermitsIfNotFetchingSchema();
+ InFlightTask newInFlightTask = null;
+ ReadLimits readLimits = null;
+ synchronized (inFlightTasks) {
+ if (hasPendingRead()) {
+ log.debug("Skip the reading because there is a pending read
task");
+ } else if (waitForCursorRewindingRefCnf > 0) {
+ log.debug("Skip the reading due to new detected schema");
+ } else if (state != Started) {
+ log.debug("Skip the reading because producer has not started");
+ } else {
+ int permits = getPermitsIfNoPendingRead();
+ if (permits > 0) {
+ if (!isWritable()) {
+ log.debug("Throttling replication traffic to a single
message permit because producer is not "
+ + "writable");
+ // Minimize the read size if the producer is
disconnected or the window is already full.
+ permits = 1;
+ }
+
+ readLimits = getReadLimits(permits);
+ if (readLimits.isReadable()) {
+ newInFlightTask =
createOrRecycleInFlightTaskIntoQueue(cursor.getReadPosition(),
+ readLimits.messages);
+ } else {
+ // no rate limiter permits from rate limit
+ log.debug()
+ .attr("messages", readLimits.messages)
+ .attr("bytes", readLimits.bytes)
+ .log("Throttling replication traffic");
+ }
+ }
+ }
+ }
if (newInFlightTask == null) {
- // no permits from rate limit
log.debug("Not scheduling read due to pending read or no permits");
if (!hasPendingRead()) {
topic.getBrokerService().executor().schedule(
() -> readMoreEntries(), MESSAGE_RATE_BACKOFF_MS,
TimeUnit.MILLISECONDS);
- return;
- } else {
- return;
}
- }
- // If disabled RateLimiter.
- if (!dispatchRateLimiter.isPresent() ||
!dispatchRateLimiter.get().isDispatchRateLimitingEnabled()) {
- cursor.asyncReadEntriesOrWait(newInFlightTask.readingEntries, -1,
this,
- newInFlightTask/* Context object */,
topic.getMaxReadPosition());
return;
}
- // No permits of RateLimiter.
- AvailablePermits availablePermits =
getRateLimiterAvailablePermits(newInFlightTask.readingEntries);
- if (!availablePermits.isReadable()) {
- // no rate limiter permits from rate limit
- log.debug()
- .attr("messages", availablePermits.getMessages())
- .attr("bytes", availablePermits.getBytes())
- .log("Throttling replication traffic");
- topic.getBrokerService().executor().schedule(
- () -> readMoreEntries(), MESSAGE_RATE_BACKOFF_MS,
TimeUnit.MILLISECONDS);
- return;
- }
- // Has permits of RateLimiter.
- int messagesToRead = availablePermits.getMessages();
- long bytesToRead = availablePermits.getBytes();
- if (!isWritable()) {
- log.debug("Throttling replication traffic because producer is not
writable");
- // Minimize the read size if the producer is disconnected or the
window is already full
- messagesToRead = 1;
- }
- // Update acquired permits exceeds limitation.
- if (messagesToRead < newInFlightTask.readingEntries) {
- newInFlightTask.setReadingEntries(messagesToRead);
- }
log.debug()
.attr("readingEntries", newInFlightTask.readingEntries)
- .attr("bytesToRead", bytesToRead)
.log("Scheduling read");
- cursor.asyncReadEntriesOrWait(newInFlightTask.readingEntries,
bytesToRead, this,
+ cursor.asyncReadEntriesOrWait(newInFlightTask.readingEntries,
readLimits.bytes, this,
newInFlightTask/* Context object */,
topic.getMaxReadPosition());
}
@@ -412,7 +400,7 @@ public abstract class PersistentReplicator extends
AbstractReplicator
inFlightTask.setEntries(entries);
// After the replicator starts, the speed will be gradually increased.
- int maxReadBatchSize =
topic.getBrokerService().pulsar().getConfiguration().getDispatcherMaxReadBatchSize();
+ int maxReadBatchSize = getMaxReadBatchSize();
if (readBatchSize < maxReadBatchSize) {
int newReadBatchSize = Math.min(readBatchSize * 2,
maxReadBatchSize);
log.debug()
@@ -569,7 +557,7 @@ public abstract class PersistentReplicator extends
AbstractReplicator
}
// Reduce read batch size to avoid flooding bookies with retries
- readBatchSize =
topic.getBrokerService().pulsar().getConfiguration().getDispatcherMinReadBatchSize();
+ readBatchSize =
brokerService.pulsar().getConfiguration().getDispatcherMinReadBatchSize();
long waitTimeMillis = readFailureBackoff.next().toMillis();
@@ -938,29 +926,6 @@ public abstract class PersistentReplicator extends
AbstractReplicator
}
}
- protected InFlightTask acquirePermitsIfNotFetchingSchema() {
- synchronized (inFlightTasks) {
- if (hasPendingRead()) {
- log.info("Skip the reading because there is a pending read
task");
- return null;
- }
- if (waitForCursorRewindingRefCnf > 0) {
- log.info("Skip the reading due to new detected schema");
- return null;
- }
- if (state != Started) {
- log.info("Skip the reading because producer has not started");
- return null;
- }
- // Guarantee that there is a unique cursor reading task.
- int permits = getPermitsIfNoPendingRead();
- if (permits == 0) {
- return null;
- }
- return
createOrRecycleInFlightTaskIntoQueue(cursor.getReadPosition(), permits);
- }
- }
-
protected int getPermitsIfNoPendingRead() {
synchronized (inFlightTasks) {
for (InFlightTask task : inFlightTasks) {
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/OneWayReplicatorTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/OneWayReplicatorTest.java
index b37fda1fa29..7f69b485509 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/OneWayReplicatorTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/OneWayReplicatorTest.java
@@ -119,6 +119,7 @@ import org.apache.pulsar.common.naming.TopicName;
import org.apache.pulsar.common.partition.PartitionedTopicMetadata;
import org.apache.pulsar.common.policies.data.AutoTopicCreationOverride;
import org.apache.pulsar.common.policies.data.ClusterData;
+import org.apache.pulsar.common.policies.data.DispatchRate;
import org.apache.pulsar.common.policies.data.HierarchyTopicPolicies;
import org.apache.pulsar.common.policies.data.PublishRate;
import org.apache.pulsar.common.policies.data.ReplicatorStats;
@@ -2431,6 +2432,85 @@ public class OneWayReplicatorTest extends
OneWayReplicatorTestBase {
ensureNoBacklogByInflightTask(getReplicator(topicName));
}
+ @DataProvider
+ public Object[][] replicatorDispatchRateLimits() {
+ return new Object[][] {
+ {1, -1L},
+ {-1, 1L}
+ };
+ }
+
+ @Test(timeOut = 90_000, dataProvider = "replicatorDispatchRateLimits")
+ public void testReplicatorContinuesAfterRateLimiterHasNoPermits(int
messageRate, long byteRate) throws Exception {
+ final String topicName = BrokerTestUtil.newUniqueName("persistent://"
+ replicatedNamespace + "/tp_");
+ final String subscriptionName = "sub";
+ final List<String> messages = Arrays.asList("msg-0", "msg-1", "msg-2");
+ DispatchRate dispatchRate = DispatchRate.builder()
+ .dispatchThrottlingRateInMsg(messageRate)
+ .dispatchThrottlingRateInByte(byteRate)
+ .ratePeriodInSecond(2)
+ .build();
+ Producer<String> producer = null;
+ Consumer<String> consumer = null;
+ boolean topicCreated = false;
+ boolean dispatchRateConfigured = false;
+ try {
+ admin1.topics().createNonPartitionedTopic(topicName);
+ topicCreated = true;
+ admin1.topicPolicies().setReplicatorDispatchRate(topicName,
dispatchRate);
+ dispatchRateConfigured = true;
+ GeoPersistentReplicator replicator = getReplicator(topicName);
+ Awaitility.await().untilAsserted(() -> {
+ assertTrue(replicator.getRateLimiter().isPresent());
+
assertEquals(replicator.getRateLimiter().get().getDispatchRateOnMsg(),
messageRate);
+
assertEquals(replicator.getRateLimiter().get().getDispatchRateOnByte(),
byteRate);
+ });
+ consumer = client2.newConsumer(Schema.STRING)
+ .topic(topicName)
+ .subscriptionName(subscriptionName)
+
.subscriptionInitialPosition(SubscriptionInitialPosition.Earliest)
+ .subscribe();
+ producer = client1.newProducer(Schema.STRING)
+ .topic(topicName)
+ .enableBatching(false)
+ .create();
+
+ for (String message : messages) {
+ producer.send(message);
+ }
+
+ Set<String> expected = new HashSet<>(messages);
+ Set<String> received = new HashSet<>();
+ Consumer<String> subscribedConsumer = consumer;
+ Awaitility.await().atMost(Duration.ofSeconds(60)).untilAsserted(()
-> {
+ Message<String> message = subscribedConsumer.receive(1,
TimeUnit.SECONDS);
+ if (message != null) {
+ received.add(message.getValue());
+ subscribedConsumer.acknowledge(message);
+ }
+ assertEquals(received, expected);
+ });
+ waitForReplicationTaskFinish(topicName);
+ ensureNoBacklogByInflightTask(replicator);
+ } finally {
+ if (producer != null) {
+ producer.close();
+ }
+ if (consumer != null) {
+ consumer.close();
+ }
+ if (dispatchRateConfigured) {
+ admin1.topicPolicies().removeReplicatorDispatchRate(topicName);
+ }
+ if (topicCreated) {
+ admin1.topics().setReplicationClusters(topicName,
Arrays.asList(cluster1));
+ waitReplicatorStopped(topicName, false);
+ admin1.topics().delete(topicName, true);
+ admin2.topics().delete(topicName, true);
+ }
+ }
+ }
+
@DataProvider
public Object[] isPartitioned() {
return new Object[]{
diff --git
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/persistent/PersistentReplicatorInflightTaskTest.java
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/persistent/PersistentReplicatorInflightTaskTest.java
index 7df7ac78bdd..96c2c2cbae6 100644
---
a/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/persistent/PersistentReplicatorInflightTaskTest.java
+++
b/pulsar-broker/src/test/java/org/apache/pulsar/broker/service/persistent/PersistentReplicatorInflightTaskTest.java
@@ -18,44 +18,64 @@
*/
package org.apache.pulsar.broker.service.persistent;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
+import io.netty.channel.EventLoopGroup;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import lombok.CustomLog;
import org.apache.bookkeeper.mledger.Entry;
+import org.apache.bookkeeper.mledger.ManagedCursor;
import org.apache.bookkeeper.mledger.ManagedLedgerException;
import org.apache.bookkeeper.mledger.Position;
import org.apache.bookkeeper.mledger.PositionFactory;
import org.apache.bookkeeper.mledger.impl.ManagedLedgerImpl;
import org.apache.bookkeeper.mledger.impl.ManagedLedgerTest;
import org.apache.pulsar.broker.BrokerTestUtil;
+import org.apache.pulsar.broker.PulsarServerException;
+import org.apache.pulsar.broker.PulsarService;
+import org.apache.pulsar.broker.ServiceConfiguration;
import org.apache.pulsar.broker.service.AbstractReplicator;
-import org.apache.pulsar.broker.service.BrokerServiceInternalMethodInvoker;
+import org.apache.pulsar.broker.service.BrokerService;
import org.apache.pulsar.broker.service.OneWayReplicatorTestBase;
import
org.apache.pulsar.broker.service.persistent.PersistentReplicator.InFlightTask;
import
org.apache.pulsar.broker.service.persistent.PersistentReplicator.ProducerSendCallback;
import
org.apache.pulsar.broker.service.persistent.PersistentReplicator.ReasonOfWaitForCursorRewinding;
+import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.Producer;
+import org.apache.pulsar.client.api.ProducerBuilder;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.Schema;
+import org.apache.pulsar.client.impl.PulsarClientImpl;
import org.awaitility.Awaitility;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@CustomLog
@@ -199,6 +219,63 @@ public class PersistentReplicatorInflightTaskTest extends
OneWayReplicatorTestBa
}
}
+ @DataProvider
+ public Object[][] readSchedulingLimits() {
+ return new Object[][] {
+ {"message permits exhausted", 0, -1L, true, false, 0, 0L},
+ {"byte permits exhausted", -1, 0L, true, false, 0, 0L},
+ {"message permits limit read batch", 5, -1L, true, true, 5,
1024L},
+ {"byte permits limit read size", -1, 512L, true, true, 100,
512L},
+ {"non-writable producer limits read batch", 5, 512L, false,
true, 1, 512L}
+ };
+ }
+
+ @Test(dataProvider = "readSchedulingLimits")
+ public void testReadMoreEntriesSchedulesCursorReadWithReadLimits(String
scenario,
+ long
availableMessages,
+ long
availableBytes,
+ boolean
writable,
+ boolean
expectRead,
+ int
expectedMessages,
+ long
expectedBytes) throws Exception {
+ TestReplicatorFixture fixture = newTestReplicatorFixture(writable);
+ PersistentReplicator replicator = fixture.replicator;
+ DispatchRateLimiter rateLimiter = mock(DispatchRateLimiter.class);
+ when(rateLimiter.isDispatchRateLimitingEnabled()).thenReturn(true);
+
when(rateLimiter.getAvailableDispatchRateLimitOnMsg()).thenReturn(availableMessages);
+
when(rateLimiter.getAvailableDispatchRateLimitOnByte()).thenReturn(availableBytes);
+ replicator.dispatchRateLimiter = Optional.of(rateLimiter);
+
+ replicator.readMoreEntries();
+
+ if (expectRead) {
+ assertEquals(replicator.inFlightTasks.size(), 1, scenario);
+ InFlightTask inFlightTask = replicator.inFlightTasks.peek();
+
verify(fixture.cursor).asyncReadEntriesOrWait(eq(expectedMessages),
eq(expectedBytes),
+ same(replicator), same(inFlightTask), any(Position.class));
+ assertEquals(inFlightTask.getReadingEntries(), expectedMessages,
scenario);
+ verify(fixture.executor, never()).schedule(any(Runnable.class),
anyLong(), any(TimeUnit.class));
+ } else {
+ verify(fixture.cursor, never()).asyncReadEntriesOrWait(anyInt(),
anyLong(), any(), any(), any());
+ assertTrue(replicator.inFlightTasks.isEmpty(), scenario);
+ verify(fixture.executor).schedule(any(Runnable.class), eq((long)
PersistentTopic.MESSAGE_RATE_BACKOFF_MS),
+ eq(TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Test
+ public void testReadMoreEntriesSkipsReadWhenPendingReadExists() throws
Exception {
+ TestReplicatorFixture fixture = newTestReplicatorFixture(true);
+ PersistentReplicator replicator = fixture.replicator;
+ replicator.inFlightTasks.add(new
InFlightTask(PositionFactory.create(1, 1), 5, replicator.getReplicatorId()));
+
+ replicator.readMoreEntries();
+
+ verify(fixture.cursor, never()).asyncReadEntriesOrWait(anyInt(),
anyLong(), any(), any(), any());
+ verify(fixture.executor, never()).schedule(any(Runnable.class),
anyLong(), any(TimeUnit.class));
+ assertEquals(replicator.inFlightTasks.size(), 1);
+ }
+
@Test
public void testCreateOrRecycleInFlightTaskIntoQueue() throws Exception {
log.info("Starting testCreateOrRecycleInFlightTaskIntoQueue");
@@ -389,111 +466,94 @@ public class PersistentReplicatorInflightTaskTest
extends OneWayReplicatorTestBa
}
}
- @Test
- public void testAcquirePermitsIfNotFetchingSchema() throws Exception {
- log.info("Starting testAcquirePermitsIfNotFetchingSchema");
- // Get the replicator for the test topic
- PersistentReplicator replicator = getReplicator(topicName);
- Assert.assertNotNull(replicator, "Replicator should not be null");
+ @SuppressWarnings("unchecked")
+ private TestReplicatorFixture newTestReplicatorFixture(boolean writable)
throws Exception {
+ ServiceConfiguration configuration = new ServiceConfiguration();
+ configuration.setClusterName("local");
+ configuration.setReplicationProducerQueueSize(1000);
+ configuration.setDispatcherMaxReadBatchSize(100);
+ configuration.setDispatcherMaxReadSizeBytes(1024);
+
+ PulsarService pulsar = mock(PulsarService.class);
+ when(pulsar.getConfiguration()).thenReturn(configuration);
+ when(pulsar.getConfig()).thenReturn(configuration);
+ when(pulsar.getClient()).thenReturn(mock(PulsarClientImpl.class));
+ when(pulsar.getAdminClient()).thenReturn(mock(PulsarAdmin.class));
+
+ BrokerService brokerService = mock(BrokerService.class);
+ EventLoopGroup executor = mock(EventLoopGroup.class);
+ when(brokerService.pulsar()).thenReturn(pulsar);
+ when(brokerService.getPulsar()).thenReturn(pulsar);
+ when(brokerService.executor()).thenReturn(executor);
+
+ ProducerBuilder<byte[]> producerBuilder = mock(ProducerBuilder.class);
+ when(producerBuilder.topic(anyString())).thenReturn(producerBuilder);
+
when(producerBuilder.messageRoutingMode(any())).thenReturn(producerBuilder);
+
when(producerBuilder.enableBatching(anyBoolean())).thenReturn(producerBuilder);
+ when(producerBuilder.sendTimeout(anyInt(),
any(TimeUnit.class))).thenReturn(producerBuilder);
+
when(producerBuilder.maxPendingMessages(anyInt())).thenReturn(producerBuilder);
+
when(producerBuilder.producerName(anyString())).thenReturn(producerBuilder);
+
+ PulsarClientImpl replicationClient = mock(PulsarClientImpl.class);
+
when(replicationClient.newProducer(any(Schema.class))).thenReturn(producerBuilder);
+
+ PersistentTopic topic = mock(PersistentTopic.class);
+
when(topic.getName()).thenReturn("persistent://prop/ns/test-read-scheduling");
+ when(topic.getReplicatorPrefix()).thenReturn("pulsar.repl");
+ when(topic.getBrokerService()).thenReturn(brokerService);
+ when(topic.getMaxReadPosition()).thenReturn(PositionFactory.create(1,
100));
+
+ ManagedCursor cursor = mock(ManagedCursor.class);
+ when(cursor.getName()).thenReturn("pulsar.repl.remote");
+ when(cursor.getReadPosition()).thenReturn(PositionFactory.create(1,
1));
+
+ TestPersistentReplicator replicator = new
TestPersistentReplicator(topic, cursor, brokerService,
+ replicationClient, mock(PulsarAdmin.class), writable);
+ return new TestReplicatorFixture(replicator, cursor, executor);
+ }
- // Get access to the inFlightTasks list for setup
- LinkedList<InFlightTask> inFlightTasks = replicator.inFlightTasks;
- Assert.assertNotNull(inFlightTasks, "InFlightTasks list should not be
null");
+ private static class TestReplicatorFixture {
+ final TestPersistentReplicator replicator;
+ final ManagedCursor cursor;
+ final EventLoopGroup executor;
- // Save original tasks and clear for testing
- List<InFlightTask> originalTasks = new ArrayList<>(inFlightTasks);
- inFlightTasks.clear();
+ TestReplicatorFixture(TestPersistentReplicator replicator,
ManagedCursor cursor, EventLoopGroup executor) {
+ this.replicator = replicator;
+ this.cursor = cursor;
+ this.executor = executor;
+ }
+ }
- // Save original state
- int originalWaitForCursorRewinding =
replicator.waitForCursorRewindingRefCnf;
- AbstractReplicator.State originalState = replicator.getState();
+ private static class TestPersistentReplicator extends PersistentReplicator
{
+ private final boolean writable;
- try {
- // Test Case 1: Normal case - no pending read, not waiting for
cursor rewinding, state is Started
- // Should return a new InFlightTask
- // First, check the current permits available
- int expectedPermits = replicator.getPermitsIfNoPendingRead();
- Assert.assertTrue(expectedPermits > 0, "Should have available
permits for the test");
- InFlightTask task1 =
replicator.acquirePermitsIfNotFetchingSchema();
- Assert.assertNotNull(task1, "Should return a new InFlightTask in
normal case");
- Assert.assertNotNull(task1.getReadPos(), "Task should have a read
position");
- Assert.assertEquals(task1.getReadingEntries(), expectedPermits,
- "Task readingEntries should equal the number of permits
available");
- Assert.assertTrue(inFlightTasks.contains(task1),
- "Task should be added to the inFlightTasks list");
-
- // Test Case 2: With pending read - should return null
- inFlightTasks.clear();
- Position position1 = PositionFactory.create(1, 1);
- InFlightTask pendingReadTask = new InFlightTask(position1, 5, "");
- // Don't set readoutEntries to simulate pending read
- inFlightTasks.add(pendingReadTask);
- InFlightTask task2 =
replicator.acquirePermitsIfNotFetchingSchema();
- Assert.assertNull(task2, "Should return null when there is a
pending read");
+ TestPersistentReplicator(PersistentTopic topic, ManagedCursor cursor,
BrokerService brokerService,
+ PulsarClientImpl replicationClient,
PulsarAdmin replicationAdmin, boolean writable)
+ throws PulsarServerException {
+ super("local", topic, cursor, "remote", topic.getName(),
brokerService, replicationClient,
+ replicationAdmin);
+ this.writable = writable;
+ this.state = State.Started;
+ }
- // Test Case 3: With waitForCursorRewinding=true - should return
null
- inFlightTasks.clear();
- replicator.waitForCursorRewindingRefCnf = 1;
- InFlightTask task3 =
replicator.acquirePermitsIfNotFetchingSchema();
- Assert.assertNull(task3, "Should return null when waiting for
cursor rewinding");
- // Reset for next test
- replicator.waitForCursorRewindingRefCnf = 0;
-
- // Test Case 4: With state != Started - should return null
- // We need to use reflection to modify the state since it's
protected by AtomicReferenceFieldUpdater
- BrokerServiceInternalMethodInvoker.replicatorSetState(replicator,
AbstractReplicator.State.Starting);
- InFlightTask task4 =
replicator.acquirePermitsIfNotFetchingSchema();
- Assert.assertNull(task4, "Should return null when state is not
Started");
- // Reset state for next test
- BrokerServiceInternalMethodInvoker.replicatorSetState(replicator,
AbstractReplicator.State.Started);
-
- // Test Case 5: With limited permits - verify readingEntries is
set correctly
- inFlightTasks.clear();
- // Add a task with some in-flight messages to reduce available
permits
- Position positionLimited = PositionFactory.create(10, 10);
- InFlightTask limitedTask = new InFlightTask(positionLimited, 5,
"");
- // Add enough entries to leave just a small number of permits
(e.g., 10)
- List<Entry> limitedEntries = new ArrayList<>();
- int entriesCount = 990;
- for (int j = 0; j < entriesCount; j++) {
- limitedEntries.add(mock(Entry.class));
- }
- limitedTask.setEntries(limitedEntries);
- inFlightTasks.add(limitedTask);
- // Check that we have limited permits available
- int limitedPermits = replicator.getPermitsIfNoPendingRead();
- Assert.assertTrue(limitedPermits > 0 && limitedPermits < 20,
- "Should have a small number of permits available for
testing");
- // Now acquire permits and verify readingEntries matches the
limited permits
- InFlightTask task5 =
replicator.acquirePermitsIfNotFetchingSchema();
- Assert.assertNotNull(task5, "Should return a task with limited
permits");
- Assert.assertEquals(task5.getReadingEntries(), limitedPermits,
- "Task readingEntries should equal the limited number of
permits available");
-
- // Test Case 6: With permits=0 - should return null
- inFlightTasks.clear();
- // Add tasks that will make getPermitsIfNoPendingRead() return 0
- // We need enough in-flight messages to equal producerQueueSize
- for (int i = 0; i < 10; i++) {
- Position position = PositionFactory.create(i, i);
- InFlightTask task = new InFlightTask(position, 5, "");
- List<Entry> entries = new ArrayList<>();
- for (int j = 0; j < 100; j++) {
- entries.add(mock(Entry.class));
- }
- task.setEntries(entries);
- inFlightTasks.add(task);
- }
- InFlightTask task6 =
replicator.acquirePermitsIfNotFetchingSchema();
- Assert.assertNull(task6, "Should return null when permits is 0");
- log.info("Completed testAcquirePermitsIfNotFetchingSchema");
- } finally {
- // Restore original state
- replicator.waitForCursorRewindingRefCnf =
originalWaitForCursorRewinding;
- BrokerServiceInternalMethodInvoker.replicatorSetState(replicator,
originalState);
- // Restore original tasks
- inFlightTasks.clear();
- inFlightTasks.addAll(originalTasks);
+ @Override
+ protected void startProducer() {
+ // No-op for scheduling behavior tests.
+ }
+
+ @Override
+ protected String getProducerName() {
+ return "test-replicator";
+ }
+
+ @Override
+ protected boolean isWritable() {
+ return writable;
+ }
+
+ @Override
+ protected boolean replicateEntries(List<Entry> entries, InFlightTask
inFlightTask) {
+ return true;
}
}
@@ -503,7 +563,8 @@ public class PersistentReplicatorInflightTaskTest extends
OneWayReplicatorTestBa
});
replicator.beforeTerminateOrCursorRewinding(PersistentReplicator.ReasonOfWaitForCursorRewinding.Disconnecting);
replicator.doRewindCursor(false);
- InFlightTask inFlightTask =
replicator.createOrRecycleInFlightTaskIntoQueue(PositionFactory.create(1, 1),
1);
+ InFlightTask inFlightTask =
+
replicator.createOrRecycleInFlightTaskIntoQueue(PositionFactory.create(1, 1),
1);
return () -> {
inFlightTask.setEntries(Collections.emptyList());
replicator.readMoreEntries();