blob: 33d4be375940f4aed1436069bb13ff605ed038df [file] [view]
# PCIP-5: Pull Consumer Implementation for Apache Pulsar
# Background knowledge
- **Pulsar Consumers**: Pulsar currently supports push-based consumption models (exclusive/shared/failover/key-shared).
This proposal adds pull-based consumption.
- **Message Positioning**: Pulsar uses composite MessageIDs (ledgerId + entryId + partitionId), while systems like
Kafka/RocketMQ use monotonic offsets.
- **Offset Mapping**: https://github.com/apache/pulsar/pull/24220 can be used to convert between offsets and Pulsar's
MessageIDs.
# Motivation
System Migration Requirement: The organization plans to migrate from RocketMQ to Pulsar, requiring a unified MQ client
abstraction layer to conceal implementation details and support seamless engine replacement.
Interface Compatibility Issues:
- Pulsar lacks a native offset retrieval interface (pull/fetch model).
- RocketMQ/Kafka use monotonically increasing numeric offsets to locate messages, whereas Pulsar employs a composite
MessageID (ledgerId + entryId + partitionId).
Objective: Implement a RocketMQ-like Pull Consumer to support precise offset control and reduce migration costs.
# Goals
## In Scope
| Goal | Description |
|----------------------------|---------------------------------------------------------------------------|
| **Precise Offset Control** | Supports specifying partition, offset, pull count, and byte size. |
| **Resource Efficiency** | Reuses Reader connections with LRU cache management. |
| **Easy Integration** | Compatible with Pulsars existing API, requiring no Broker modifications. |
## Out of Scope
NA
# High Level Design
```mermaid
graph TD
A[PullConsumer] -->|pull| B[Offset Mapping]
B -->|convert| C[MessageID]
C -->|seek| D[Reader Pool]
D -->|fetch| E[Broker]
A -->|ack| F[Offset-Based Ack]
F -->|convert| G[MessageID]
G -->|cumulative ack| H[Consumer]
```
Key components:
1. **`PulsarPullConsumer` interface**: Standard pull consumer API
2. **Offset MessageID Cache**: Partition-scoped mapping layer
3. **Reader Pool**: Managed resource pool with LRU eviction
4. **Partition Locking**: Thread-safe access coordination
## Detailed Design
### Design & Implementation Details
**Core Interface** `PulsarPullConsumer`:
```java
public interface PulsarPullConsumer<T> extends AutoCloseable {
void start() throws PulsarClientException;
List<Message<T>> pull(PullRequest request);
void ack(long offset, int partition) throws PulsarClientException;
long searchOffset(int partition, long timestamp) throws PulsarAdminException;
long getConsumeStats(int partition) throws PulsarAdminException;
class PullRequest {
private long offset;
private int partition;
private int maxMessages;
private int maxBytes;
private Duration timeout;
}
}
```
**Reader Management** :
```java
public class ReaderCache<T> {
private final Map<String, LoadingCache<Long, Reader<T>>> readerCacheMap;
public Reader<T> getReader(String topic, long offset) {
// 1. Acquire partition lock
// 2. Get/create LRU cache (default: max 100 readers/partition)
// 3. Remove reader from cache for exclusive use
}
public void releaseReader(String topic, long nextOffset, Reader<T> reader) {
// Return reader to cache if still connected
}
}
```
**Offset Mapping**:
```java
public class OffsetToMessageIdCache {
private final Map<String, LoadingCache<Long, MessageId>> partitionCaches;
public MessageId getMessageIdByOffset(String topic, long offset) {
// 1. Check caffeine cache (default: max 1000 entries/partition)
// 2. On cache miss: pulsarAdmin.topics().getMessageIDByOffset()
// 3. Populate cache
}
}
```
### Public-facing Changes
#### Public API
**New Interfaces**:
```java
// Entry point
PulsarPullConsumer<byte[]> pullConsumer1 = new PulsarPullConsumerImpl<>(
nonPartitionedTopic, subscription,
brokerCluster, Schema.BYTES,
pulsarClient, pulsarAdmin);
// Usage
List<Message<byte[]>> messages = pullConsumer1.pull(
PulsarPullConsumer.PullRequest.builder()
.offset(offset)
.partition(PulsarPullConsumer.PARTITION_NONE)
.maxMessages(10)
.maxBytes(1024 * 1024)
.timeout(java.time.Duration.ofSeconds(10))
.build());
```
## Get Started
### Quick Start
```java
// 1. Create pull consumer
PulsarPullConsumer<byte[]> pullConsumer1 = new PulsarPullConsumerImpl<>(
nonPartitionedTopic, subscription,
brokerCluster, Schema.BYTES,
pulsarClient, pulsarAdmin);
consumer.start(); // Initialize connections
// 2. Pull messages from partition 0 starting at offset 200
List<Message<byte[]>> batch = pullConsumer1.pull(
PulsarPullConsumer.PullRequest.builder()
.offset(offset)
.partition(PulsarPullConsumer.PARTITION_NONE)
.maxMessages(10)
.maxBytes(1024 * 1024)
.timeout(java.time.Duration.ofSeconds(10))
.build());
);
// 3. Process messages batch.
forEach(msg ->{
System.out.println("Received: "+new String(msg.getData()));
// Store last offset
});
// 4. Acknowledge up to last offset
consumer.ack(250L,0);
// 5. Close resources
consumer.close();
```