Skip to content

Commit 4a864f2

Browse files
[feature][client-cpp] Support inclusive seek for cpp client (#17209)
Fixes #17186 ### Motivation There are some cases in which it is useful to be able to include current position of the message when reset of cursor was made. ### Modifications * Support inclusive seek in c++ consumers. * Add a unit test to verify.
1 parent d69953b commit 4a864f2

11 files changed

+257
-79
lines changed

include/pulsar/ConsumerConfiguration.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,20 @@ class PULSAR_PUBLIC ConsumerConfiguration {
499499
*/
500500
bool isAutoAckOldestChunkedMessageOnQueueFull() const;
501501

502+
/**
503+
* Set the consumer to include the given position of any reset operation like Consumer::seek.
504+
*
505+
* Default: false
506+
*
507+
* @param startMessageIdInclusive whether to include the reset position
508+
*/
509+
ConsumerConfiguration& setStartMessageIdInclusive(bool startMessageIdInclusive);
510+
511+
/**
512+
* The associated getter of setStartMessageIdInclusive
513+
*/
514+
bool isStartMessageIdInclusive() const;
515+
502516
friend class PulsarWrapper;
503517

504518
private:

lib/ClientImpl.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,8 @@ void ClientImpl::handleSubscribe(const Result result, const LookupDataResultPtr
378378
partitionMetadata->getPartitions(),
379379
subscriptionName, conf, lookupServicePtr_);
380380
} else {
381-
auto consumerImpl = std::make_shared<ConsumerImpl>(shared_from_this(), topicName->toString(),
382-
subscriptionName, conf);
381+
auto consumerImpl = std::make_shared<ConsumerImpl>(
382+
shared_from_this(), topicName->toString(), subscriptionName, conf, topicName->isPersistent());
383383
consumerImpl->setPartitionIndex(topicName->getPartitionIndex());
384384
consumer = consumerImpl;
385385
}

lib/ConsumerConfiguration.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,4 +260,11 @@ bool ConsumerConfiguration::isAutoAckOldestChunkedMessageOnQueueFull() const {
260260
return impl_->autoAckOldestChunkedMessageOnQueueFull;
261261
}
262262

263+
ConsumerConfiguration& ConsumerConfiguration::setStartMessageIdInclusive(bool startMessageIdInclusive) {
264+
impl_->startMessageIdInclusive = startMessageIdInclusive;
265+
return *this;
266+
}
267+
268+
bool ConsumerConfiguration::isStartMessageIdInclusive() const { return impl_->startMessageIdInclusive; }
269+
263270
} // namespace pulsar

lib/ConsumerConfigurationImpl.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ struct ConsumerConfigurationImpl {
5353
KeySharedPolicy keySharedPolicy;
5454
size_t maxPendingChunkedMessage{10};
5555
bool autoAckOldestChunkedMessageOnQueueFull{false};
56+
bool startMessageIdInclusive{false};
5657
};
5758
} // namespace pulsar
5859
#endif /* LIB_CONSUMERCONFIGURATIONIMPL_H_ */

lib/ConsumerImpl.cc

Lines changed: 102 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ DECLARE_LOG_OBJECT()
3838

3939
ConsumerImpl::ConsumerImpl(const ClientImplPtr client, const std::string& topic,
4040
const std::string& subscriptionName, const ConsumerConfiguration& conf,
41+
bool isPersistent,
4142
const ExecutorServicePtr listenerExecutor /* = NULL by default */,
4243
bool hasParent /* = false by default */,
4344
const ConsumerTopicType consumerTopicType /* = NonPartitioned by default */,
@@ -47,6 +48,7 @@ ConsumerImpl::ConsumerImpl(const ClientImplPtr client, const std::string& topic,
4748
config_(conf),
4849
subscription_(subscriptionName),
4950
originalSubscriptionName_(subscriptionName),
51+
isPersistent_(isPersistent),
5052
messageListener_(config_.getMessageListener()),
5153
eventListener_(config_.getConsumerEventListener()),
5254
hasParent_(hasParent),
@@ -169,14 +171,17 @@ void ConsumerImpl::connectionOpened(const ClientConnectionPtr& cnx) {
169171
// sending the subscribe request.
170172
cnx->registerConsumer(consumerId_, shared_from_this());
171173

172-
Lock lockForMessageId(mutexForMessageId_);
173-
Optional<MessageId> firstMessageInQueue = clearReceiveQueue();
174-
if (subscriptionMode_ == Commands::SubscriptionModeNonDurable) {
175-
// Update startMessageId so that we can discard messages after delivery
176-
// restarts
177-
startMessageId_ = firstMessageInQueue;
174+
if (duringSeek_) {
175+
ackGroupingTrackerPtr_->flushAndClean();
178176
}
179-
const auto startMessageId = startMessageId_;
177+
178+
Lock lockForMessageId(mutexForMessageId_);
179+
// Update startMessageId so that we can discard messages after delivery restarts
180+
const auto startMessageId = clearReceiveQueue();
181+
const auto subscribeMessageId = (subscriptionMode_ == Commands::SubscriptionModeNonDurable)
182+
? startMessageId
183+
: Optional<MessageId>::empty();
184+
startMessageId_ = startMessageId;
180185
lockForMessageId.unlock();
181186

182187
unAckedMessageTrackerPtr_->clear();
@@ -186,7 +191,7 @@ void ConsumerImpl::connectionOpened(const ClientConnectionPtr& cnx) {
186191
uint64_t requestId = client->newRequestId();
187192
SharedBuffer cmd = Commands::newSubscribe(
188193
topic_, subscription_, consumerId_, requestId, getSubType(), consumerName_, subscriptionMode_,
189-
startMessageId, readCompacted_, config_.getProperties(), config_.getSubscriptionProperties(),
194+
subscribeMessageId, readCompacted_, config_.getProperties(), config_.getSubscriptionProperties(),
190195
config_.getSchema(), getInitialPosition(), config_.isReplicateSubscriptionStateEnabled(),
191196
config_.getKeySharedPolicy(), config_.getPriorityLevel());
192197
cnx->sendRequestWithId(cmd, requestId)
@@ -397,12 +402,12 @@ void ConsumerImpl::messageReceived(const ClientConnectionPtr& cnx, const proto::
397402
return;
398403
}
399404

400-
const bool isMessageDecryptable =
401-
metadata.encryption_keys_size() <= 0 || config_.getCryptoKeyReader().get() ||
405+
const bool isMessageUndecryptable =
406+
metadata.encryption_keys_size() > 0 && !config_.getCryptoKeyReader().get() &&
402407
config_.getCryptoFailureAction() == ConsumerCryptoFailureAction::CONSUME;
403408

404409
const bool isChunkedMessage = metadata.num_chunks_from_msg() > 1;
405-
if (isMessageDecryptable && !isChunkedMessage) {
410+
if (!isMessageUndecryptable && !isChunkedMessage) {
406411
if (!uncompressMessageIfNeeded(cnx, msg.message_id(), metadata, payload, true)) {
407412
// Message was discarded on decompression error
408413
return;
@@ -446,6 +451,16 @@ void ConsumerImpl::messageReceived(const ClientConnectionPtr& cnx, const proto::
446451
Lock lock(mutex_);
447452
numOfMessageReceived = receiveIndividualMessagesFromBatch(cnx, m, msg.redelivery_count());
448453
} else {
454+
const auto startMessageId = startMessageId_.get();
455+
if (isPersistent_ && startMessageId.is_present() &&
456+
m.getMessageId().ledgerId() == startMessageId.value().ledgerId() &&
457+
m.getMessageId().entryId() == startMessageId.value().entryId() &&
458+
isPriorEntryIndex(m.getMessageId().entryId())) {
459+
LOG_DEBUG(getName() << " Ignoring message from before the startMessageId: "
460+
<< startMessageId.value());
461+
return;
462+
}
463+
449464
Lock lock(pendingReceiveMutex_);
450465
// if asyncReceive is waiting then notify callback without adding to incomingMessages queue
451466
bool asyncReceivedWaiting = !pendingReceives_.empty();
@@ -533,9 +548,7 @@ uint32_t ConsumerImpl::receiveIndividualMessagesFromBatch(const ClientConnection
533548
batchAcknowledgementTracker_.receivedMessage(batchedMessage);
534549
LOG_DEBUG("Received Batch messages of size - " << batchSize
535550
<< " -- msgId: " << batchedMessage.getMessageId());
536-
Lock lock(mutexForMessageId_);
537-
const auto startMessageId = startMessageId_;
538-
lock.unlock();
551+
const auto startMessageId = startMessageId_.get();
539552

540553
int skippedMessages = 0;
541554

@@ -550,9 +563,9 @@ uint32_t ConsumerImpl::receiveIndividualMessagesFromBatch(const ClientConnection
550563

551564
// If we are receiving a batch message, we need to discard messages that were prior
552565
// to the startMessageId
553-
if (msgId.ledgerId() == startMessageId.value().ledgerId() &&
566+
if (isPersistent_ && msgId.ledgerId() == startMessageId.value().ledgerId() &&
554567
msgId.entryId() == startMessageId.value().entryId() &&
555-
msgId.batchIndex() <= startMessageId.value().batchIndex()) {
568+
isPriorBatchIndex(msgId.batchIndex())) {
556569
LOG_DEBUG(getName() << "Ignoring message from before the startMessageId"
557570
<< msg.getMessageId());
558571
++skippedMessages;
@@ -842,6 +855,12 @@ void ConsumerImpl::messageProcessed(Message& msg, bool track) {
842855
* not seen by the application
843856
*/
844857
Optional<MessageId> ConsumerImpl::clearReceiveQueue() {
858+
bool expectedDuringSeek = true;
859+
if (duringSeek_.compare_exchange_strong(expectedDuringSeek, false)) {
860+
return Optional<MessageId>::of(seekMessageId_.get());
861+
} else if (subscriptionMode_ == Commands::SubscriptionModeDurable) {
862+
return startMessageId_.get();
863+
}
845864
Message nextMessageInQueue;
846865
if (incomingMessages_.peekAndClear(nextMessageInQueue)) {
847866
// There was at least one message pending in the queue
@@ -862,7 +881,7 @@ Optional<MessageId> ConsumerImpl::clearReceiveQueue() {
862881
} else {
863882
// No message was received or dequeued by this consumer. Next message would still be the
864883
// startMessageId
865-
return startMessageId_;
884+
return startMessageId_.get();
866885
}
867886
}
868887

@@ -1175,18 +1194,6 @@ void ConsumerImpl::brokerConsumerStatsListener(Result res, BrokerConsumerStatsIm
11751194
}
11761195
}
11771196

1178-
void ConsumerImpl::handleSeek(Result result, ResultCallback callback) {
1179-
if (result == ResultOk) {
1180-
Lock lock(mutexForMessageId_);
1181-
lastDequedMessageId_ = MessageId::earliest();
1182-
lock.unlock();
1183-
LOG_INFO(getName() << "Seek successfully");
1184-
} else {
1185-
LOG_ERROR(getName() << "Failed to seek: " << strResult(result));
1186-
}
1187-
callback(result);
1188-
}
1189-
11901197
void ConsumerImpl::seekAsync(const MessageId& msgId, ResultCallback callback) {
11911198
const auto state = state_.load();
11921199
if (state == Closed || state == Closing) {
@@ -1197,25 +1204,13 @@ void ConsumerImpl::seekAsync(const MessageId& msgId, ResultCallback callback) {
11971204
return;
11981205
}
11991206

1200-
this->ackGroupingTrackerPtr_->flushAndClean();
1201-
ClientConnectionPtr cnx = getCnx().lock();
1202-
if (cnx) {
1203-
ClientImplPtr client = client_.lock();
1204-
uint64_t requestId = client->newRequestId();
1205-
LOG_DEBUG(getName() << " Sending seek Command for Consumer - " << getConsumerId() << ", requestId - "
1206-
<< requestId);
1207-
Future<Result, ResponseData> future =
1208-
cnx->sendRequestWithId(Commands::newSeek(consumerId_, requestId, msgId), requestId);
1209-
1210-
if (callback) {
1211-
future.addListener(
1212-
std::bind(&ConsumerImpl::handleSeek, shared_from_this(), std::placeholders::_1, callback));
1213-
}
1207+
ClientImplPtr client = client_.lock();
1208+
if (!client) {
1209+
LOG_ERROR(getName() << "Client is expired when seekAsync " << msgId);
12141210
return;
12151211
}
1216-
1217-
LOG_ERROR(getName() << " Client Connection not ready for Consumer");
1218-
callback(ResultNotConnected);
1212+
const auto requestId = client->newRequestId();
1213+
seekAsyncInternal(requestId, Commands::newSeek(consumerId_, requestId, msgId), msgId, 0L, callback);
12191214
}
12201215

12211216
void ConsumerImpl::seekAsync(uint64_t timestamp, ResultCallback callback) {
@@ -1228,24 +1223,14 @@ void ConsumerImpl::seekAsync(uint64_t timestamp, ResultCallback callback) {
12281223
return;
12291224
}
12301225

1231-
ClientConnectionPtr cnx = getCnx().lock();
1232-
if (cnx) {
1233-
ClientImplPtr client = client_.lock();
1234-
uint64_t requestId = client->newRequestId();
1235-
LOG_DEBUG(getName() << " Sending seek Command for Consumer - " << getConsumerId() << ", requestId - "
1236-
<< requestId);
1237-
Future<Result, ResponseData> future =
1238-
cnx->sendRequestWithId(Commands::newSeek(consumerId_, requestId, timestamp), requestId);
1239-
1240-
if (callback) {
1241-
future.addListener(
1242-
std::bind(&ConsumerImpl::handleSeek, shared_from_this(), std::placeholders::_1, callback));
1243-
}
1226+
ClientImplPtr client = client_.lock();
1227+
if (!client) {
1228+
LOG_ERROR(getName() << "Client is expired when seekAsync " << timestamp);
12441229
return;
12451230
}
1246-
1247-
LOG_ERROR(getName() << " Client Connection not ready for Consumer");
1248-
callback(ResultNotConnected);
1231+
const auto requestId = client->newRequestId();
1232+
seekAsyncInternal(requestId, Commands::newSeek(consumerId_, requestId, timestamp), MessageId::earliest(),
1233+
timestamp, callback);
12491234
}
12501235

12511236
bool ConsumerImpl::isReadCompacted() { return readCompacted_; }
@@ -1255,9 +1240,10 @@ inline bool hasMoreMessages(const MessageId& lastMessageIdInBroker, const Messag
12551240
}
12561241

12571242
void ConsumerImpl::hasMessageAvailableAsync(HasMessageAvailableCallback callback) {
1243+
const auto startMessageId = startMessageId_.get();
12581244
Lock lock(mutexForMessageId_);
12591245
const auto messageId =
1260-
(lastDequedMessageId_ == MessageId::earliest()) ? startMessageId_.value() : lastDequedMessageId_;
1246+
(lastDequedMessageId_ == MessageId::earliest()) ? startMessageId.value() : lastDequedMessageId_;
12611247

12621248
if (messageId == MessageId::latest()) {
12631249
lock.unlock();
@@ -1380,4 +1366,57 @@ bool ConsumerImpl::isConnected() const { return !getCnx().expired() && state_ ==
13801366

13811367
uint64_t ConsumerImpl::getNumberOfConnectedConsumer() { return isConnected() ? 1 : 0; }
13821368

1369+
void ConsumerImpl::seekAsyncInternal(long requestId, SharedBuffer seek, const MessageId& seekId,
1370+
long timestamp, ResultCallback callback) {
1371+
ClientConnectionPtr cnx = getCnx().lock();
1372+
if (!cnx) {
1373+
LOG_ERROR(getName() << " Client Connection not ready for Consumer");
1374+
callback(ResultNotConnected);
1375+
return;
1376+
}
1377+
1378+
const auto originalSeekMessageId = seekMessageId_.get();
1379+
seekMessageId_ = seekId;
1380+
duringSeek_ = true;
1381+
if (timestamp > 0) {
1382+
LOG_INFO(getName() << " Seeking subscription to " << timestamp);
1383+
} else {
1384+
LOG_INFO(getName() << " Seeking subscription to " << seekId);
1385+
}
1386+
1387+
std::weak_ptr<ConsumerImpl> weakSelf{shared_from_this()};
1388+
1389+
cnx->sendRequestWithId(seek, requestId)
1390+
.addListener([this, weakSelf, callback, originalSeekMessageId](Result result,
1391+
const ResponseData& responseData) {
1392+
auto self = weakSelf.lock();
1393+
if (!self) {
1394+
callback(result);
1395+
return;
1396+
}
1397+
if (result == ResultOk) {
1398+
LOG_INFO(getName() << "Seek successfully");
1399+
ackGroupingTrackerPtr_->flushAndClean();
1400+
Lock lock(mutexForMessageId_);
1401+
lastDequedMessageId_ = MessageId::earliest();
1402+
lock.unlock();
1403+
} else {
1404+
LOG_ERROR(getName() << "Failed to seek: " << result);
1405+
seekMessageId_ = originalSeekMessageId;
1406+
duringSeek_ = false;
1407+
}
1408+
callback(result);
1409+
});
1410+
}
1411+
1412+
bool ConsumerImpl::isPriorBatchIndex(int32_t idx) {
1413+
return config_.isStartMessageIdInclusive() ? idx < startMessageId_.get().value().batchIndex()
1414+
: idx <= startMessageId_.get().value().batchIndex();
1415+
}
1416+
1417+
bool ConsumerImpl::isPriorEntryIndex(int64_t idx) {
1418+
return config_.isStartMessageIdInclusive() ? idx < startMessageId_.get().value().entryId()
1419+
: idx <= startMessageId_.get().value().entryId();
1420+
}
1421+
13831422
} /* namespace pulsar */

lib/ConsumerImpl.h

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
#include <lib/stats/ConsumerStatsDisabled.h>
4747
#include <queue>
4848
#include <atomic>
49+
#include "Synchronized.h"
4950

5051
using namespace pulsar;
5152

@@ -69,7 +70,7 @@ class ConsumerImpl : public ConsumerImplBase,
6970
public std::enable_shared_from_this<ConsumerImpl> {
7071
public:
7172
ConsumerImpl(const ClientImplPtr client, const std::string& topic, const std::string& subscriptionName,
72-
const ConsumerConfiguration&,
73+
const ConsumerConfiguration&, bool isPersistent,
7374
const ExecutorServicePtr listenerExecutor = ExecutorServicePtr(), bool hasParent = false,
7475
const ConsumerTopicType consumerTopicType = NonPartitioned,
7576
Commands::SubscriptionMode = Commands::SubscriptionModeDurable,
@@ -138,7 +139,6 @@ class ConsumerImpl : public ConsumerImplBase,
138139

139140
virtual void redeliverMessages(const std::set<MessageId>& messageIds);
140141

141-
void handleSeek(Result result, ResultCallback callback);
142142
virtual bool isReadCompacted();
143143
virtual void hasMessageAvailableAsync(HasMessageAvailableCallback callback);
144144
virtual void getLastMessageIdAsync(BrokerGetLastMessageIdCallback callback);
@@ -169,6 +169,8 @@ class ConsumerImpl : public ConsumerImplBase,
169169
void drainIncomingMessageQueue(size_t count);
170170
uint32_t receiveIndividualMessagesFromBatch(const ClientConnectionPtr& cnx, Message& batchedMessage,
171171
int redeliveryCount);
172+
bool isPriorBatchIndex(int32_t idx);
173+
bool isPriorEntryIndex(int64_t idx);
172174
void brokerConsumerStatsListener(Result, BrokerConsumerStatsImpl, BrokerConsumerStatsCallback);
173175

174176
bool decryptMessageIfNeeded(const ClientConnectionPtr& cnx, const proto::CommandMessage& msg,
@@ -187,11 +189,14 @@ class ConsumerImpl : public ConsumerImplBase,
187189
BrokerGetLastMessageIdCallback callback);
188190

189191
Optional<MessageId> clearReceiveQueue();
192+
void seekAsyncInternal(long requestId, SharedBuffer seek, const MessageId& seekId, long timestamp,
193+
ResultCallback callback);
190194

191195
std::mutex mutexForReceiveWithZeroQueueSize;
192196
const ConsumerConfiguration config_;
193197
const std::string subscription_;
194198
std::string originalSubscriptionName_;
199+
const bool isPersistent_;
195200
MessageListener messageListener_;
196201
ConsumerEventListenerPtr eventListener_;
197202
ExecutorServicePtr listenerExecutor_;
@@ -220,12 +225,15 @@ class ConsumerImpl : public ConsumerImplBase,
220225
MessageCryptoPtr msgCrypto_;
221226
const bool readCompacted_;
222227

223-
// Make the access to `startMessageId_`, `lastDequedMessageId_` and `lastMessageIdInBroker_` thread safe
228+
// Make the access to `lastDequedMessageId_` and `lastMessageIdInBroker_` thread safe
224229
mutable std::mutex mutexForMessageId_;
225-
Optional<MessageId> startMessageId_;
226230
MessageId lastDequedMessageId_{MessageId::earliest()};
227231
MessageId lastMessageIdInBroker_{MessageId::earliest()};
228232

233+
std::atomic_bool duringSeek_{false};
234+
Synchronized<Optional<MessageId>> startMessageId_{Optional<MessageId>::empty()};
235+
Synchronized<MessageId> seekMessageId_{MessageId::earliest()};
236+
229237
class ChunkedMessageCtx {
230238
public:
231239
ChunkedMessageCtx() : totalChunks_(0) {}

0 commit comments

Comments
 (0)