blob: dc490266e54173bca582651dc055d04d6a833cff [file]
/*
* 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.
*/
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.apache.pulsar.client.admin.PulsarAdmin;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.MessageIdAdv;
import org.apache.pulsar.client.api.Producer;
import org.apache.pulsar.client.api.PulsarClient;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.api.PulsarPullConsumer;
import org.apache.pulsar.client.api.Schema;
import org.apache.pulsar.client.api.impl.PulsarPullConsumerImpl;
import org.apache.pulsar.client.common.Constants;
import org.apache.pulsar.client.common.ConsumeStats;
import org.apache.pulsar.client.common.PullRequest;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@Slf4j
public class PulsarPullConsumerTest {
String nonPartitionedTopic = "persistent://public/default/my-topic1";
String partitionedTopic = "persistent://public/default/my-topic2";
PulsarAdmin pulsarAdmin;
PulsarClient pulsarClient;
public PulsarPullConsumerTest() throws PulsarClientException {}
@BeforeClass
public void setup() throws Exception {
try {
pulsarAdmin = SingletonPulsarContainer.createPulsarAdmin();
pulsarClient = SingletonPulsarContainer.createPulsarClient();
pulsarAdmin.topics().createNonPartitionedTopic(nonPartitionedTopic);
pulsarAdmin.topics().createPartitionedTopic(partitionedTopic, 2);
log.info("Created topics: {}, {}", nonPartitionedTopic, partitionedTopic);
} catch (Exception e) {
log.info(
"Topics already exist, skipping creation: {}, {}", nonPartitionedTopic, partitionedTopic);
}
}
@AfterClass
public void cleanup() throws Exception {
pulsarAdmin.topics().delete(nonPartitionedTopic, true);
pulsarAdmin.topics().deletePartitionedTopic(partitionedTopic, true);
if (pulsarClient != null) {
pulsarClient.close();
}
if (pulsarAdmin != null) {
pulsarAdmin.close();
}
}
@DataProvider(name = "testData")
public Object[][] testData() {
return new Object[][] {
{nonPartitionedTopic, Constants.PARTITION_NONE_INDEX},
{partitionedTopic, 0},
{partitionedTopic, 1}
};
}
/**
* Case test design: 1. Single partition topicA 1. Send one thousand messages 2. Create a
* PullConsumer, subscribe to topicA, and pull messages. 3. Verify message Exactly-once 4. Verify
* message consumption status 2. Multi-partition topicB 1. Send one thousand messages 3. Create
* multiple PullConsumers and subscribe to each partition of topicB. 4. Each PullConsumer pulls
* messages and verifies that the message is Exactly-once. 5. Verify message consumption status
*/
@Test(dataProvider = "testData")
public void testPullConsumer(String topic, int partitionIndex) throws Exception {
log.info("Starting testPullConsumer with topic: {}, partitionIndex: {}", topic, partitionIndex);
topic =
partitionIndex == Constants.PARTITION_NONE_INDEX
? topic
: topic + "-partition-" + partitionIndex;
String subscription = "my-subscription";
String brokerCluster = "sit";
@Cleanup
PulsarPullConsumer<byte[]> pullConsumer =
new PulsarPullConsumerImpl<>(
topic,
subscription,
brokerCluster,
Schema.BYTES,
() -> pulsarClient,
pulsarAdmin,
null);
pullConsumer.start();
@Cleanup
Producer<byte[]> producer =
pulsarClient.newProducer(Schema.BYTES).topic(topic).enableBatching(false).create();
Set<String> sent = new HashSet<>();
for (int i = 0; i < 1000; i++) {
String message = "Hello-Pulsar-" + i;
MessageId messageId = producer.send(message.getBytes());
sent.add(message);
log.info("Sent message: {} with id: {}", message, messageId);
}
ConsumeStats consumeStats = pullConsumer.getConsumeStats(partitionIndex);
long offset = consumeStats.getLastConsumedOffset();
Set<String> received = new HashSet<>();
while (true) {
List<Message<byte[]>> messages =
pullConsumer
.pull(
PullRequest.builder()
.offset(offset)
.partition(partitionIndex)
.maxMessages(10)
.maxBytes(1024 * 1024)
.timeout(java.time.Duration.ofSeconds(10))
.build())
.getMessages();
log.info("Pulled {} messages from topic {}", messages.size(), topic);
if (messages.isEmpty()) {
log.info("No more messages to pull, exiting...");
break;
}
for (Message<byte[]> message : messages) {
if (!received.add(new String(message.getData()))) {
log.error("Duplicate message detected: {}", new String(message.getData()));
}
}
long consumedIndex = messages.get(messages.size() - 1).getIndex().get();
pullConsumer.ack(consumedIndex, partitionIndex);
offset = consumedIndex + 1;
log.info("Acknowledged messages up to index: {}", consumedIndex);
}
offset = pullConsumer.getConsumeStats(partitionIndex).getLastConsumedOffset();
log.info("Final consume offset for non-partitioned topic: {}", offset);
log.info(
"received {} unique messages from non-partitioned topic, it is equals to sent {}",
received.size(),
received.equals(sent));
assert received.equals(sent) : "Received messages do not match sent messages";
}
@Test(dataProvider = "testData")
public void testSearchOffset(String topic, int partitionIndex) throws Exception {
topic =
partitionIndex == Constants.PARTITION_NONE_INDEX
? topic
: topic + "-partition-" + partitionIndex;
String subscription = "my-subscription";
String brokerCluster = "sit";
@Cleanup
PulsarPullConsumer<byte[]> pullConsumer =
new PulsarPullConsumerImpl<>(
topic,
subscription,
brokerCluster,
Schema.BYTES,
() -> pulsarClient,
pulsarAdmin,
null);
pullConsumer.start();
@Cleanup
Producer<byte[]> producer =
pulsarClient.newProducer(Schema.BYTES).topic(topic).enableBatching(false).create();
long timestamp = 0;
MessageIdAdv messageId = null;
for (int i = 0; i < 10; i++) {
String message = "Hello-Pulsar-" + i;
timestamp = System.currentTimeMillis();
messageId = (MessageIdAdv) producer.send(message.getBytes());
}
for (int i = 0; i < 10; i++) {
String message = "Hello-Pulsar-" + i;
producer.send(message.getBytes());
System.currentTimeMillis();
}
long offset = pullConsumer.searchOffset(partitionIndex, timestamp);
MessageIdAdv searchedMessageId =
(MessageIdAdv) pulsarAdmin.topics().getMessageIdByIndex(topic, offset);
assert messageId.getEntryId() == searchedMessageId.getEntryId()
&& messageId.getLedgerId() == searchedMessageId.getLedgerId()
: "Searched message ID does not match expected message ID";
}
@Test
public void testGetConsumeStats() {
try {
String subscription = "test-subscription";
String brokerCluster = "sit";
@Cleanup
PulsarPullConsumer<byte[]> pullConsumer =
new PulsarPullConsumerImpl<>(
nonPartitionedTopic,
subscription,
brokerCluster,
Schema.BYTES,
() -> pulsarClient,
pulsarAdmin,
null);
pullConsumer.start();
@Cleanup
Producer<byte[]> producer =
pulsarClient
.newProducer(Schema.BYTES)
.topic(nonPartitionedTopic)
.enableBatching(false)
.create();
for (int i = 0; i < 10; i++) {
String message = "Hello-Pulsar-" + i;
producer.send(message.getBytes());
}
ConsumeStats offset = pullConsumer.getConsumeStats(Constants.PARTITION_NONE_INDEX);
log.info("Initial consume offset for topic {}: {}", nonPartitionedTopic, offset);
Assert.assertEquals(offset.getLastConsumedOffset(), -1L);
Assert.assertEquals(offset.getMaxOffset(), 9L);
Assert.assertEquals(offset.getMinOffset(), -1L);
// Simulate some message processing
pullConsumer.ack(offset.getLastConsumedOffset() + 10, Constants.PARTITION_NONE_INDEX);
Thread.sleep(1000);
ConsumeStats newOffset = pullConsumer.getConsumeStats(Constants.PARTITION_NONE_INDEX);
Assert.assertEquals(newOffset.getLastConsumedOffset(), 9L);
Assert.assertEquals(newOffset.getMaxOffset(), 9L);
Assert.assertEquals(newOffset.getMinOffset(), -1L);
log.info("New consume offset after ack: {}", newOffset);
} catch (Exception e) {
log.error("Error during testExamineConsumeStats", e);
}
}
}