/**
* Copyright 2016 Yahoo Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yahoo.pulsar.broker.service;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.bookkeeper.mledger.ManagedCursor;
import org.apache.bookkeeper.mledger.impl.EntryCacheImpl;
import org.apache.bookkeeper.mledger.impl.ManagedLedgerFactoryImpl;
import org.apache.bookkeeper.mledger.impl.ManagedLedgerImpl;
import org.apache.bookkeeper.mledger.impl.PositionImpl;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import com.google.common.collect.Sets;
import com.yahoo.pulsar.broker.service.persistent.PersistentDispatcherMultipleConsumers;
import com.yahoo.pulsar.broker.service.persistent.PersistentSubscription;
import com.yahoo.pulsar.broker.service.persistent.PersistentTopic;
import com.yahoo.pulsar.client.admin.PulsarAdminException;
import com.yahoo.pulsar.client.api.CompressionType;
import com.yahoo.pulsar.client.api.Consumer;
import com.yahoo.pulsar.client.api.ConsumerConfiguration;
import com.yahoo.pulsar.client.api.Message;
import com.yahoo.pulsar.client.api.MessageBuilder;
import com.yahoo.pulsar.client.api.MessageId;
import com.yahoo.pulsar.client.api.Producer;
import com.yahoo.pulsar.client.api.ProducerConfiguration;
import com.yahoo.pulsar.client.api.PulsarClient;
import com.yahoo.pulsar.client.api.PulsarClientException;
import com.yahoo.pulsar.client.api.SubscriptionType;
import com.yahoo.pulsar.client.impl.ConsumerImpl;
import com.yahoo.pulsar.client.impl.MessageIdImpl;
import com.yahoo.pulsar.client.impl.ProducerImpl;
import com.yahoo.pulsar.common.api.proto.PulsarApi.CommandSubscribe.SubType;
import com.yahoo.pulsar.common.naming.DestinationName;
import com.yahoo.pulsar.common.policies.data.loadbalancer.NamespaceBundleStats;
import com.yahoo.pulsar.common.stats.Metrics;
/**
*/
@Test
public class PersistentTopicE2ETest extends BrokerTestBase {
@BeforeMethod
@Override
protected void setup() throws Exception {
super.baseSetup();
}
@AfterMethod
@Override
protected void cleanup() throws Exception {
super.internalCleanup();
}
@Test
public void testSimpleProducerEvents() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic0";
// 1. producer connect
Producer producer = pulsarClient.createProducer(topicName);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
assertNotNull(topicRef);
assertEquals(topicRef.getProducers().size(), 1);
// 2. producer publish messages
for (int i = 0; i < 10; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
rolloverPerIntervalStats();
assertTrue(topicRef.getProducers().values().iterator().next().getStats().msgRateIn > 0.0);
// 3. producer disconnect
producer.close();
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(topicRef.getProducers().size(), 0);
}
@Test
public void testSimpleConsumerEvents() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic1";
final String subName = "sub1";
final int numMsgs = 10;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
// 1. client connect
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
assertNotNull(topicRef);
assertNotNull(subRef);
assertTrue(subRef.getDispatcher().isConsumerConnected());
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(getAvailablePermits(subRef), 1000 /* default */);
Producer producer = pulsarClient.createProducer(topicName);
for (int i = 0; i < numMsgs * 2; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
assertTrue(subRef.getDispatcher().isConsumerConnected());
rolloverPerIntervalStats();
assertEquals(subRef.getNumberOfEntriesInBacklog(), numMsgs * 2);
// 2. messages pushed before client receive
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(getAvailablePermits(subRef), 1000 - numMsgs * 2);
Message msg = null;
for (int i = 0; i < numMsgs; i++) {
msg = consumer.receive();
// 3. in-order message delivery
assertEquals(new String(msg.getData()), "my-message-" + i);
consumer.acknowledge(msg);
}
rolloverPerIntervalStats();
// 4. messages deleted on individual acks
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(subRef.getNumberOfEntriesInBacklog(), numMsgs);
for (int i = 0; i < numMsgs; i++) {
msg = consumer.receive();
if (i == numMsgs - 1) {
consumer.acknowledgeCumulative(msg);
}
}
rolloverPerIntervalStats();
// 5. messages deleted on cumulative acks
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(subRef.getNumberOfEntriesInBacklog(), 0);
// 6. consumer unsubscribe
consumer.unsubscribe();
// 6. consumer graceful close
consumer.close();
// 7. consumer unsubscribe
try {
consumer.unsubscribe();
fail("Should have failed");
} catch (PulsarClientException.AlreadyClosedException e) {
// ok
}
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
subRef = topicRef.getPersistentSubscription(subName);
assertNull(subRef);
producer.close();
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
}
@Test
public void testConsumerFlowControl() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic2";
final String subName = "sub2";
Message msg;
int recvQueueSize = 4;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
conf.setReceiverQueueSize(recvQueueSize);
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
Producer producer = pulsarClient.createProducer(topicName);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
assertNotNull(topicRef);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
assertNotNull(subRef);
// 1. initial receive queue size recorded
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(getAvailablePermits(subRef), recvQueueSize);
for (int i = 0; i < recvQueueSize / 2; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
msg = consumer.receive();
consumer.acknowledge(msg);
}
// 2. queue size re-adjusted after successful receive of half of window size
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(getAvailablePermits(subRef), recvQueueSize);
consumer.close();
assertFalse(subRef.getDispatcher().isConsumerConnected());
}
/**
* Validation: 1. validates active-cursor after active subscription 2. validate active-cursor with subscription 3.
* unconsumed messages should be present into cache 4. cache and active-cursor should be empty once subscription is
* closed
*
* @throws Exception
*/
@Test
public void testActiveSubscriptionWithCache() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic2";
final String subName = "sub2";
Message msg;
int recvQueueSize = 4;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
conf.setReceiverQueueSize(recvQueueSize);
// (1) Create subscription
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
Producer producer = pulsarClient.createProducer(topicName);
// (2) Produce Messages
for (int i = 0; i < recvQueueSize / 2; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
msg = consumer.receive();
consumer.acknowledge(msg);
}
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
// (3) Get Entry cache
ManagedLedgerImpl ledger = (ManagedLedgerImpl) topicRef.getManagedLedger();
Field cacheField = ManagedLedgerImpl.class.getDeclaredField("entryCache");
cacheField.setAccessible(true);
EntryCacheImpl entryCache = (EntryCacheImpl) cacheField.get(ledger);
/************* Validation on non-empty active-cursor **************/
// (4) Get ActiveCursor : which is list of active subscription
Iterable<ManagedCursor> activeCursors = ledger.getActiveCursors();
ManagedCursor curosr = activeCursors.iterator().next();
// (4.1) Validate: active Cursor must be non-empty
assertNotNull(curosr);
// (4.2) Validate: validate cursor name
assertEquals(subName, curosr.getName());
// (4.3) Validate: entryCache should have cached messages
assertTrue(entryCache.getSize() != 0);
/************* Validation on empty active-cursor **************/
// (5) Close consumer: which (1)removes activeConsumer and (2)clears the entry-cache
consumer.close();
Thread.sleep(1000);
// (5.1) Validate: active-consumer must be empty
assertFalse(ledger.getActiveCursors().iterator().hasNext());
// (5.2) Validate: Entry-cache must be cleared
assertTrue(entryCache.getSize() == 0);
}
// some race conditions needs to be handled
// disabling the test for now to not block commit jobs
@Test(enabled = false)
public void testConcurrentConsumerThreads() throws Exception {
// test concurrent consumer threads on same consumerId
final String topicName = "persistent://prop/use/ns-abc/topic3";
final String subName = "sub3";
final int recvQueueSize = 100;
final int numConsumersThreads = 10;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
conf.setReceiverQueueSize(recvQueueSize);
ExecutorService executor = Executors.newCachedThreadPool();
final CyclicBarrier barrier = new CyclicBarrier(numConsumersThreads + 1);
for (int i = 0; i < numConsumersThreads; i++) {
executor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
barrier.await();
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
for (int i = 0; i < recvQueueSize / numConsumersThreads; i++) {
Message msg = consumer.receive();
consumer.acknowledge(msg);
}
return null;
}
});
}
Producer producer = pulsarClient.createProducer(topicName);
for (int i = 0; i < recvQueueSize * numConsumersThreads; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
barrier.await();
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
// 1. cumulatively all threads drain the backlog
assertEquals(subRef.getNumberOfEntriesInBacklog(), 0);
// 2. flow control works the same as single consumer single thread
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertEquals(getAvailablePermits(subRef), recvQueueSize);
}
@Test(enabled = false)
// TODO: enable this after java client supports graceful close
public void testGracefulClose() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic4";
final String subName = "sub4";
Producer producer = pulsarClient.createProducer(topicName);
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
assertNotNull(topicRef);
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch latch = new CountDownLatch(1);
executor.submit(() -> {
for (int i = 0; i < 10; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
latch.countDown();
return null;
});
producer.close();
// 1. verify there are no pending publish acks once the producer close
// is completed on client
assertEquals(topicRef.getProducers().values().iterator().next().getPendingPublishAcks(), 0);
// safety latch in case of failure,
// wait for the spawned thread to complete
latch.await();
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
assertNotNull(subRef);
Message msg = null;
for (int i = 0; i < 10; i++) {
msg = consumer.receive();
}
// 2. verify consumer close fails when there are outstanding
// message acks
try {
consumer.close();
fail("should have failed");
} catch (IllegalStateException e) {
// Expected - messages not acked
}
consumer.acknowledgeCumulative(msg);
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
// 3. verify consumer close succeeds once all messages are ack'ed
consumer.close();
Thread.sleep(ASYNC_EVENT_COMPLETION_WAIT);
assertTrue(subRef.getDispatcher().isConsumerConnected());
}
@Test
public void testSimpleCloseTopic() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic5";
final String subName = "sub5";
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
Producer producer = pulsarClient.createProducer(topicName);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
assertNotNull(topicRef);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
assertNotNull(subRef);
Message msg;
for (int i = 0; i < 10; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
msg = consumer.receive();
consumer.acknowledge(msg);
}
producer.close();
consumer.close();
topicRef.close().get();
assertNull(pulsar.getBrokerService().getTopicReference(topicName));
}
@Test
public void testSingleClientMultipleSubscriptions() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic6";
final String subName = "sub6";
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
pulsarClient.subscribe(topicName, subName, conf);
pulsarClient.createProducer(topicName);
try {
pulsarClient.subscribe(topicName, subName, conf);
fail("Should have thrown an exception since one consumer is already connected");
} catch (PulsarClientException cce) {
Assert.assertTrue(cce.getMessage().contains("Exclusive consumer is already connected"));
}
}
@Test
public void testMultipleClientsMultipleSubscriptions() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic7";
final String subName = "sub7";
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
PulsarClient client1 = PulsarClient.create(brokerUrl.toString());
PulsarClient client2 = PulsarClient.create(brokerUrl.toString());
try {
client1.subscribe(topicName, subName, conf);
client1.createProducer(topicName);
client2.createProducer(topicName);
client2.subscribe(topicName, subName, conf);
fail("Should have thrown an exception since one consumer is already connected");
} catch (PulsarClientException cce) {
Assert.assertTrue(cce.getMessage().contains("Exclusive consumer is already connected"));
} finally {
client2.shutdown();
client1.shutdown();
}
}
@Test
public void testTopicDeleteWithDisconnectedSubscription() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic8";
final String subName = "sub1";
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
// 1. client connect
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
assertNotNull(topicRef);
assertNotNull(subRef);
assertTrue(subRef.getDispatcher().isConsumerConnected());
// 2. client disconnect
consumer.close();
assertFalse(subRef.getDispatcher().isConsumerConnected());
// 3. delete topic
admin.persistentTopics().delete(topicName);
try {
admin.persistentTopics().getStats(topicName);
} catch (PulsarAdminException e) {
// ok
}
}
int getAvailablePermits(PersistentSubscription sub) {
return sub.getDispatcher().getConsumers().get(0).getAvailablePermits();
}
@Test(enabled = false)
public void testUnloadNamespace() throws Exception {
String topicName = "persistent://prop/use/ns-abc/topic-9";
DestinationName destinationName = DestinationName.get(topicName);
pulsarClient.createProducer(topicName);
pulsarClient.close();
assertTrue(pulsar.getBrokerService().getTopicReference(topicName) != null);
assertTrue(((ManagedLedgerFactoryImpl) pulsar.getManagedLedgerFactory()).getManagedLedgers()
.containsKey(destinationName.getPersistenceNamingEncoding()));
admin.namespaces().unload("prop/use/ns-abc");
int i = 0;
for (i = 0; i < 30; i++) {
if (pulsar.getBrokerService().getTopicReference(topicName) == null) {
break;
}
Thread.sleep(1000);
}
if (i == 30) {
fail("The topic reference should be null");
}
// ML should have been closed as well
assertFalse(((ManagedLedgerFactoryImpl) pulsar.getManagedLedgerFactory()).getManagedLedgers()
.containsKey(destinationName.getPersistenceNamingEncoding()));
}
@Test
public void testGC() throws Exception {
// 1. Simple successful GC
String topicName = "persistent://prop/use/ns-abc/topic-10";
Producer producer = pulsarClient.createProducer(topicName);
producer.close();
assertNotNull(pulsar.getBrokerService().getTopicReference(topicName));
runGC();
assertNull(pulsar.getBrokerService().getTopicReference(topicName));
// 2. Topic is not GCed with live connection
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
String subName = "sub1";
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
runGC();
assertNotNull(pulsar.getBrokerService().getTopicReference(topicName));
// 3. Topic with subscription is not GCed even with no connections
consumer.close();
runGC();
assertNotNull(pulsar.getBrokerService().getTopicReference(topicName));
// 4. Topic can be GCed after unsubscribe
admin.persistentTopics().deleteSubscription(topicName, subName);
runGC();
assertNull(pulsar.getBrokerService().getTopicReference(topicName));
}
@Test
public void testMessageExpiry() throws Exception {
int messageTTLSecs = 1;
String namespaceName = "prop/use/expiry-check";
admin.namespaces().createNamespace(namespaceName);
admin.namespaces().setNamespaceMessageTTL(namespaceName, messageTTLSecs);
final String topicName = "persistent://prop/use/expiry-check/topic1";
final String subName = "sub1";
final int numMsgs = 10;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
consumer.close();
assertFalse(subRef.getDispatcher().isConsumerConnected());
Producer producer = pulsarClient.createProducer(topicName);
for (int i = 0; i < numMsgs; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
rolloverPerIntervalStats();
assertEquals(subRef.getNumberOfEntriesInBacklog(), numMsgs);
Thread.sleep(TimeUnit.SECONDS.toMillis(messageTTLSecs));
runMessageExpiryCheck();
// 1. check all messages expired for this unconnected subscription
assertEquals(subRef.getNumberOfEntriesInBacklog(), 0);
// clean-up
producer.close();
consumer.close();
admin.persistentTopics().deleteSubscription(topicName, subName);
admin.persistentTopics().delete(topicName);
admin.namespaces().deleteNamespace(namespaceName);
}
@Test
public void testMessageExpiryWithFewExpiredBacklog() throws Exception {
int messageTTLSecs = 10;
String namespaceName = "prop/use/expiry-check-1";
admin.namespaces().createNamespace(namespaceName);
admin.namespaces().setNamespaceMessageTTL(namespaceName, messageTTLSecs);
final String topicName = "persistent://prop/use/expiry-check-1/topic1";
final String subName = "sub1";
final int numMsgs = 10;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
pulsarClient.subscribe(topicName, subName, conf);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
assertTrue(subRef.getDispatcher().isConsumerConnected());
Producer producer = pulsarClient.createProducer(topicName);
for (int i = 0; i < numMsgs; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
rolloverPerIntervalStats();
assertEquals(subRef.getNumberOfEntriesInBacklog(), numMsgs);
Thread.sleep(TimeUnit.SECONDS.toMillis(messageTTLSecs));
runMessageExpiryCheck();
assertEquals(subRef.getNumberOfEntriesInBacklog(), numMsgs);
Thread.sleep(TimeUnit.SECONDS.toMillis(messageTTLSecs / 2));
runMessageExpiryCheck();
assertEquals(subRef.getNumberOfEntriesInBacklog(), 0);
}
@Test
public void testSubscriptionTypeTransitions() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/shared-topic2";
final String subName = "sub2";
ConsumerConfiguration conf1 = new ConsumerConfiguration();
conf1.setSubscriptionType(SubscriptionType.Exclusive);
ConsumerConfiguration conf2 = new ConsumerConfiguration();
conf2.setSubscriptionType(SubscriptionType.Shared);
ConsumerConfiguration conf3 = new ConsumerConfiguration();
conf3.setSubscriptionType(SubscriptionType.Failover);
Consumer consumer1 = pulsarClient.subscribe(topicName, subName, conf1);
Consumer consumer2 = null;
Consumer consumer3 = null;
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
// 1. shared consumer on an exclusive sub fails
try {
consumer2 = pulsarClient.subscribe(topicName, subName, conf2);
fail("should have failed");
} catch (PulsarClientException e) {
assertTrue(e.getMessage().contains("Subscription is of different type"));
}
// 2. failover consumer on an exclusive sub fails
try {
consumer3 = pulsarClient.subscribe(topicName, subName, conf3);
fail("should have failed");
} catch (PulsarClientException e) {
assertTrue(e.getMessage().contains("Subscription is of different type"));
}
// 3. disconnected sub can be converted in shared
consumer1.close();
try {
consumer2 = pulsarClient.subscribe(topicName, subName, conf2);
assertEquals(subRef.getDispatcher().getType(), SubType.Shared);
} catch (PulsarClientException e) {
fail("should not fail");
}
// 4. exclusive fails on shared sub
try {
consumer1 = pulsarClient.subscribe(topicName, subName, conf1);
fail("should have failed");
} catch (PulsarClientException e) {
assertTrue(e.getMessage().contains("Subscription is of different type"));
}
// 5. disconnected sub can be converted in failover
consumer2.close();
try {
consumer3 = pulsarClient.subscribe(topicName, subName, conf3);
assertEquals(subRef.getDispatcher().getType(), SubType.Failover);
} catch (PulsarClientException e) {
fail("should not fail");
}
// 5. exclusive consumer can connect after failover disconnects
consumer3.close();
try {
consumer1 = pulsarClient.subscribe(topicName, subName, conf1);
assertEquals(subRef.getDispatcher().getType(), SubType.Exclusive);
} catch (PulsarClientException e) {
fail("should not fail");
}
consumer1.close();
admin.persistentTopics().delete(topicName);
}
@Test
public void testReceiveWithTimeout() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic-receive-timeout";
final String subName = "sub";
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Exclusive);
conf.setReceiverQueueSize(1000);
ConsumerImpl consumer = (ConsumerImpl) pulsarClient.subscribe(topicName, subName, conf);
Producer producer = pulsarClient.createProducer(topicName);
assertEquals(consumer.getAvailablePermits(), 0);
Message msg = consumer.receive(10, TimeUnit.MILLISECONDS);
assertNull(msg);
assertEquals(consumer.getAvailablePermits(), 0);
producer.send("test".getBytes());
Thread.sleep(100);
assertEquals(consumer.getAvailablePermits(), 0);
msg = consumer.receive(10, TimeUnit.MILLISECONDS);
assertNotNull(msg);
assertEquals(consumer.getAvailablePermits(), 1);
msg = consumer.receive(10, TimeUnit.MILLISECONDS);
assertNull(msg);
assertEquals(consumer.getAvailablePermits(), 1);
}
@Test
public void testProducerReturnedMessageId() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic-xyz";
// 1. producer connect
Producer producer = pulsarClient.createProducer(topicName);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
assertNotNull(topicRef);
assertEquals(topicRef.getProducers().size(), 1);
ManagedLedgerImpl managedLedger = (ManagedLedgerImpl) topicRef.getManagedLedger();
long ledgerId = managedLedger.getLedgersInfoAsList().get(0).getLedgerId();
// 2. producer publish messages
final int SyncMessages = 10;
for (int i = 0; i < SyncMessages; i++) {
String message = "my-message-" + i;
MessageId receivedMessageId = producer.send(message.getBytes());
assertEquals(receivedMessageId, new MessageIdImpl(ledgerId, i, -1));
}
// 3. producer publish messages async
final int AsyncMessages = 10;
final CountDownLatch counter = new CountDownLatch(AsyncMessages);
for (int i = SyncMessages; i < (SyncMessages + AsyncMessages); i++) {
String content = "my-message-" + i;
Message msg = MessageBuilder.create().setContent(content.getBytes()).build();
final int index = i;
producer.sendAsync(msg).thenRun(() -> {
assertEquals(msg.getMessageId(), new MessageIdImpl(ledgerId, index, -1));
counter.countDown();
}).exceptionally((ex) -> {
return null;
});
}
counter.await();
// 4. producer disconnect
producer.close();
}
@Test
public void testProducerQueueFullBlocking() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic-xyzx";
final int messages = 10;
PulsarClient client = PulsarClient.create(brokerUrl.toString());
// 1. Producer connect
ProducerConfiguration producerConfiguration = new ProducerConfiguration().setMaxPendingMessages(messages)
.setBlockIfQueueFull(true).setSendTimeout(1, TimeUnit.SECONDS);
ProducerImpl producer = (ProducerImpl) client.createProducer(topicName, producerConfiguration);
// 2. Stop broker
cleanup();
// 2. producer publish messages
long startTime = System.nanoTime();
for (int i = 0; i < messages; i++) {
// Should never block
producer.sendAsync("msg".getBytes());
}
// Verify thread was not blocked
long delayNs = System.nanoTime() - startTime;
assertTrue(delayNs < TimeUnit.SECONDS.toNanos(1));
assertEquals(producer.getPendingQueueSize(), messages);
// Next send operation must block, until all the messages in the queue expire
startTime = System.nanoTime();
producer.sendAsync("msg".getBytes());
delayNs = System.nanoTime() - startTime;
assertTrue(delayNs > TimeUnit.MILLISECONDS.toNanos(500));
assertTrue(delayNs < TimeUnit.MILLISECONDS.toNanos(1500));
assertEquals(producer.getPendingQueueSize(), 1);
// 4. producer disconnect
producer.close();
client.close();
// 5. Restart broker
setup();
}
@Test
public void testProducerQueueFullNonBlocking() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic-xyzx";
final int messages = 10;
// 1. Producer connect
PulsarClient client = PulsarClient.create(brokerUrl.toString());
ProducerConfiguration producerConfiguration = new ProducerConfiguration().setMaxPendingMessages(messages)
.setBlockIfQueueFull(false).setSendTimeout(1, TimeUnit.SECONDS);
ProducerImpl producer = (ProducerImpl) client.createProducer(topicName, producerConfiguration);
// 2. Stop broker
cleanup();
// 2. producer publish messages
long startTime = System.nanoTime();
for (int i = 0; i < messages; i++) {
// Should never block
producer.sendAsync("msg".getBytes());
}
// Verify thread was not blocked
long delayNs = System.nanoTime() - startTime;
assertTrue(delayNs < TimeUnit.SECONDS.toNanos(1));
assertEquals(producer.getPendingQueueSize(), messages);
// Next send operation must fail and not block
startTime = System.nanoTime();
try {
producer.send("msg".getBytes());
fail("Send should have failed");
} catch (PulsarClientException.ProducerQueueIsFullError e) {
// Expected
}
delayNs = System.nanoTime() - startTime;
assertTrue(delayNs < TimeUnit.SECONDS.toNanos(1));
assertEquals(producer.getPendingQueueSize(), messages);
// 4. producer disconnect
producer.close();
client.close();
// 5. Restart broker
setup();
}
@Test
public void testDeleteTopics() throws Exception {
BrokerService brokerService = pulsar.getBrokerService();
// 1. producers connect
Producer producer1 = pulsarClient.createProducer("persistent://prop/use/ns-abc/topic-1");
Producer producer2 = pulsarClient.createProducer("persistent://prop/use/ns-abc/topic-2");
brokerService.updateRates();
Map<String, NamespaceBundleStats> bundleStatsMap = brokerService.getBundleStats();
assertEquals(bundleStatsMap.size(), 1);
NamespaceBundleStats bundleStats = bundleStatsMap.get("prop/use/ns-abc/0x00000000_0xffffffff");
assertNotNull(bundleStats);
producer1.close();
admin.persistentTopics().delete("persistent://prop/use/ns-abc/topic-1");
brokerService.updateRates();
bundleStatsMap = brokerService.getBundleStats();
assertEquals(bundleStatsMap.size(), 1);
bundleStats = bundleStatsMap.get("prop/use/ns-abc/0x00000000_0xffffffff");
assertNotNull(bundleStats);
// // Delete 2nd topic as well
// producer2.close();
// admin.persistentTopics().delete("persistent://prop/use/ns-abc/topic-2");
//
// brokerService.updateRates();
//
// bundleStatsMap = brokerService.getBundleStats();
// assertEquals(bundleStatsMap.size(), 0);
}
@DataProvider(name = "codec")
public Object[][] codecProvider() {
return new Object[][] { { CompressionType.NONE }, { CompressionType.LZ4 }, { CompressionType.ZLIB }, };
}
@Test(dataProvider = "codec")
public void testCompression(CompressionType compressionType) throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic0" + compressionType;
// 1. producer connect
ProducerConfiguration producerConf = new ProducerConfiguration();
producerConf.setCompressionType(compressionType);
Producer producer = pulsarClient.createProducer(topicName, producerConf);
Consumer consumer = pulsarClient.subscribe(topicName, "my-sub");
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
assertNotNull(topicRef);
assertEquals(topicRef.getProducers().size(), 1);
// 2. producer publish messages
for (int i = 0; i < 10; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
for (int i = 0; i < 10; i++) {
Message msg = consumer.receive(5, TimeUnit.SECONDS);
assertNotNull(msg);
assertEquals(msg.getData(), ("my-message-" + i).getBytes());
}
// 3. producer disconnect
producer.close();
consumer.close();
}
@Test
public void testBrokerTopicStats() throws Exception {
BrokerService brokerService = this.pulsar.getBrokerService();
Field field = BrokerService.class.getDeclaredField("statsUpdater");
field.setAccessible(true);
ScheduledExecutorService statsUpdater = (ScheduledExecutorService) field.get(brokerService);
// disable statsUpdate to calculate rates explicitly
statsUpdater.shutdown();
final String namespace = "prop/use/ns-abc";
ProducerConfiguration producerConf = new ProducerConfiguration();
Producer producer = pulsarClient.createProducer("persistent://" + namespace + "/topic0", producerConf);
// 1. producer publish messages
for (int i = 0; i < 10; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
Metrics metric = null;
// sleep 1 sec to caclulate metrics per second
Thread.sleep(1000);
brokerService.updateRates();
List<Metrics> metrics = brokerService.getDestinationMetrics();
for (int i = 0; i < metrics.size(); i++) {
if (metrics.get(i).getDimension("namespace").equalsIgnoreCase(namespace)) {
metric = metrics.get(i);
break;
}
}
assertNotNull(metric);
double msgInRate = (double) metrics.get(0).getMetrics().get("brk_in_rate");
// rate should be calculated and no must be > 0 as we have produced 10 msgs so far
assertTrue(msgInRate > 0);
}
@Test
public void testPayloadCorruptionDetection() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic1";
// 1. producer connect
Producer producer = pulsarClient.createProducer(topicName);
Consumer consumer = pulsarClient.subscribe(topicName, "my-sub");
Message msg1 = MessageBuilder.create().setContent("message-1".getBytes()).build();
CompletableFuture<MessageId> future1 = producer.sendAsync(msg1);
// Stop the broker, and publishes messages. Messages are accumulated in the producer queue and they're checksums
// would have already been computed. If we change the message content at that point, it should result in a
// checksum validation error
stopBroker();
Message msg2 = MessageBuilder.create().setContent("message-2".getBytes()).build();
CompletableFuture<MessageId> future2 = producer.sendAsync(msg2);
// Taint msg2
msg2.getData()[msg2.getData().length - 1] = '3'; // new content would be 'message-3'
// Restart the broker to have the messages published
startBroker();
future1.get();
try {
future2.get();
fail("since we corrupted the message, it should be rejected by the broker");
} catch (Exception e) {
// ok
}
// We should only receive msg1
Message msg = consumer.receive(1, TimeUnit.SECONDS);
assertEquals(new String(msg.getData()), "message-1");
while ((msg = consumer.receive(1, TimeUnit.SECONDS)) != null) {
assertEquals(new String(msg.getData()), "message-1");
}
}
/**
* Verify: Broker should not replay already acknowledged messages again and should clear them from messageReplay bucket
*
* 1. produce messages
* 2. consume messages and ack all except 1 msg
* 3. Verification: should replay only 1 unacked message
*/
@Test()
public void testMessageRedelivery() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic2";
final String subName = "sub2";
Message msg;
int totalMessages = 10;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Shared);
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
Producer producer = pulsarClient.createProducer(topicName);
// (1) Produce messages
for (int i = 0; i < totalMessages; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
//(2) Consume and ack messages except first message
Message unAckedMsg = null;
for (int i = 0; i < totalMessages; i++) {
msg = consumer.receive();
if (i == 0) {
unAckedMsg = msg;
} else {
consumer.acknowledge(msg);
}
}
consumer.redeliverUnacknowledgedMessages();
// Verify: msg [L:0] must be redelivered
try {
msg = consumer.receive(1, TimeUnit.SECONDS);
assertEquals(new String(msg.getData()), new String(unAckedMsg.getData()));
} catch (Exception e) {
fail("msg should be redelivered ", e);
}
// Verify no other messages are redelivered
msg = consumer.receive(100, TimeUnit.MILLISECONDS);
assertNull(msg);
consumer.close();
producer.close();
}
/**
* Verify:
* 1. Broker should not replay already acknowledged messages
* 2. Dispatcher should not stuck while dispatching new messages due to previous-replay
* of invalid/already-acked messages
*
* @throws Exception
*/
@Test
public void testMessageReplay() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic2";
final String subName = "sub2";
Message msg;
int totalMessages = 10;
int replayIndex = totalMessages / 2;
ConsumerConfiguration conf = new ConsumerConfiguration();
conf.setSubscriptionType(SubscriptionType.Shared);
conf.setReceiverQueueSize(1);
Consumer consumer = pulsarClient.subscribe(topicName, subName, conf);
Producer producer = pulsarClient.createProducer(topicName);
PersistentTopic topicRef = (PersistentTopic) pulsar.getBrokerService().getTopicReference(topicName);
assertNotNull(topicRef);
PersistentSubscription subRef = topicRef.getPersistentSubscription(subName);
PersistentDispatcherMultipleConsumers dispatcher = (PersistentDispatcherMultipleConsumers) subRef
.getDispatcher();
Field replayMap = PersistentDispatcherMultipleConsumers.class.getDeclaredField("messagesToReplay");
replayMap.setAccessible(true);
TreeSet<PositionImpl> messagesToReplay = Sets.newTreeSet();
assertNotNull(subRef);
// (1) Produce messages
for (int i = 0; i < totalMessages; i++) {
String message = "my-message-" + i;
producer.send(message.getBytes());
}
MessageIdImpl firstAckedMsg = null;
// (2) Consume and ack messages except first message
for (int i = 0; i < totalMessages; i++) {
msg = consumer.receive();
consumer.acknowledge(msg);
MessageIdImpl msgId = (MessageIdImpl) msg.getMessageId();
if (i == 0) {
firstAckedMsg = msgId;
}
if (i < replayIndex) {
// (3) accumulate acked messages for replay
messagesToReplay.add(new PositionImpl(msgId.getLedgerId(), msgId.getEntryId()));
}
}
// (4) redelivery : should redeliver only unacked messages
Thread.sleep(1000);
replayMap.set(dispatcher, messagesToReplay);
// (a) redelivery with all acked-message should clear messageReply bucket
dispatcher.redeliverUnacknowledgedMessages(dispatcher.getConsumers().get(0));
assertEquals(messagesToReplay.size(), 0);
// (b) fill messageReplyBucket with already acked entry again: and try to publish new msg and read it
messagesToReplay.add(new PositionImpl(firstAckedMsg.getLedgerId(), firstAckedMsg.getEntryId()));
replayMap.set(dispatcher, messagesToReplay);
// send new message
final String testMsg = "testMsg";
producer.send(testMsg.getBytes());
// consumer should be able to receive only new message and not the
dispatcher.consumerFlow(dispatcher.getConsumers().get(0), 1);
msg = consumer.receive(1, TimeUnit.SECONDS);
assertNotNull(msg);
assertEquals(msg.getData(), testMsg.getBytes());
consumer.close();
producer.close();
}
}