blob: 62604b09a3d2cd7e6fbeccb8e0c3e8ea857b877e [file]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
#include "QueueConsumerImpl.h"
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <optional>
#include <string>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
#include "MessageIdImpl.h"
#include "MessageImpl.h"
#include "lib/LogUtils.h"
DECLARE_LOG_OBJECT()
namespace pulsar::st {
namespace {
pulsar::InitialPosition toClassicInitialPosition(SubscriptionInitialPosition position) {
return position == SubscriptionInitialPosition::Earliest ? pulsar::InitialPositionEarliest
: pulsar::InitialPositionLatest;
}
// Close a segment consumer once its creation future resolves (a no-op if creation failed).
void closeWhenReady(Future<pulsar::Consumer> future) {
future.addListener([](const Expected<pulsar::Consumer>& result) {
if (result) {
pulsar::Consumer consumer = *result;
consumer.closeAsync([](pulsar::Result) {});
}
});
}
} // namespace
QueueConsumerImpl::QueueConsumerImpl(pulsar::ClientImplPtr classic, QueueConsumerConfig config)
: classic_(std::move(classic)),
config_(std::move(config)),
topic_(config_.topic),
subscription_(config_.subscriptionName),
consumerName_(config_.consumerName.value_or(std::string{})),
executor_(classic_->getIOExecutorProvider()->get()),
receiveQueue_(std::make_shared<ReceiveQueue>(executor_, kReceiveQueueCapacity)),
currentLayout_(std::make_shared<SegmentLayout>()) {}
Future<void> QueueConsumerImpl::start() {
if (config_.deadLetterPolicy) {
// Dead-lettering is not implemented yet: fail loudly rather than silently accepting a
// policy that would never fire.
startPromise_.setError(Error{ResultOperationNotSupported,
"deadLetterPolicy is not implemented yet in the scalable-topics "
"client; unset it to subscribe"});
return startPromise_.getFuture();
}
dagWatch_ = std::make_shared<DagWatchSession>(classic_, config_.topic, /*createIfMissing*/ true);
std::weak_ptr<QueueConsumerImpl> weak = weak_from_this();
dagWatch_->setLayoutChangeListener(
[weak](const SegmentLayout& newLayout, const SegmentLayout& oldLayout) {
if (auto self = weak.lock()) self->onLayoutChange(newLayout, oldLayout);
});
dagWatch_->start().addListener([weak](const Expected<SegmentLayout>& result) {
if (auto self = weak.lock()) self->onStartResult(result);
});
return startPromise_.getFuture();
}
void QueueConsumerImpl::onStartResult(const Expected<SegmentLayout>& result) {
// Only the failure path (see StProducerImpl::onStartResult): the layout listener drives the
// success path — subscribe the initial segments and complete startPromise_.
if (!result) startPromise_.setError(result.error());
}
void QueueConsumerImpl::onLayoutChange(const SegmentLayout& newLayout, const SegmentLayout& /*oldLayout*/) {
// Subscribe active AND sealed segments: a sealed segment may still hold undrained messages.
std::vector<Segment> target;
target.reserve(newLayout.activeSegments().size() + newLayout.sealedSegments().size());
for (const auto& segment : newLayout.activeSegments()) target.push_back(segment);
for (const auto& segment : newLayout.sealedSegments()) target.push_back(segment);
std::vector<Future<pulsar::Consumer>> retired;
std::vector<Segment> toAdd;
bool first = false;
{
std::lock_guard<std::mutex> lock(mutex_);
first = !sawFirstLayout_;
sawFirstLayout_ = true;
currentLayout_ = std::make_shared<SegmentLayout>(newLayout);
std::unordered_set<std::uint64_t> targetIds;
for (const auto& segment : target) targetIds.insert(segment.segmentId);
for (auto it = segmentConsumers_.begin(); it != segmentConsumers_.end();) {
if (targetIds.find(it->first) == targetIds.end()) {
retired.push_back(std::move(it->second));
outstanding_.erase(it->first);
terminatedSegments_.erase(it->first);
it = segmentConsumers_.erase(it);
} else {
++it;
}
}
// Forget segments that have left the DAG so a future segment id can never be mistaken for a
// previously-drained one.
for (auto it = drainedSegments_.begin(); it != drainedSegments_.end();) {
it = targetIds.count(*it) ? std::next(it) : drainedSegments_.erase(it);
}
for (const auto& segment : target) {
if (segmentConsumers_.find(segment.segmentId) == segmentConsumers_.end() &&
drainedSegments_.find(segment.segmentId) == drainedSegments_.end())
toAdd.push_back(segment);
}
}
for (auto& future : retired) closeWhenReady(future);
if (first) {
if (toAdd.empty()) {
startPromise_.setSuccess();
return;
}
auto remaining = std::make_shared<std::atomic<int>>(static_cast<int>(toAdd.size()));
for (const auto& segment : toAdd) {
getOrCreateSegmentConsumerAsync(segment).addListener(
[self = shared_from_this(), remaining](const Expected<pulsar::Consumer>& result) {
if (!result) {
self->startPromise_.setError(result.error()); // first error wins (idempotent)
return;
}
if (remaining->fetch_sub(1) == 1) self->startPromise_.setSuccess();
});
}
} else {
// Off the start path no error can surface to a caller, so back a failed subscribe with a
// bounded retry — the DAG may stay quiet for a long time and the next push is the only
// other thing that would re-attempt the segment.
for (const auto& segment : toAdd) subscribeSegmentWithRetry(segment, /*attempt*/ 0);
}
}
pulsar::ConsumerConfiguration QueueConsumerImpl::buildSegmentConfiguration(const Segment& segment) const {
// Build a FRESH config every time (pulsar::ConsumerConfiguration's copy ctor shares its impl).
pulsar::ConsumerConfiguration conf;
conf.setConsumerType(pulsar::ConsumerShared);
conf.setSchema(config_.schema);
conf.setSubscriptionInitialPosition(toClassicInitialPosition(config_.initialPosition));
if (config_.consumerName) {
conf.setConsumerName(*config_.consumerName + "-seg-" + std::to_string(segment.segmentId));
}
if (config_.ackPolicy.groupTime) {
conf.setAckGroupingTimeMs(static_cast<long>(config_.ackPolicy.groupTime->count()));
}
if (config_.ackPolicy.negativeAckRedeliveryDelay) {
conf.setNegativeAckRedeliveryDelayMs(
static_cast<long>(config_.ackPolicy.negativeAckRedeliveryDelay->count()));
}
for (const auto& [key, value] : config_.properties) conf.setProperty(key, value);
if (segment.isLegacy()) conf.setProperty("__pulsar.v5.managed", "true");
return conf;
}
Future<pulsar::Consumer> QueueConsumerImpl::getOrCreateSegmentConsumerAsync(const Segment& segment) {
detail::Promise<pulsar::Consumer> promise;
{
std::lock_guard<std::mutex> lock(mutex_);
if (auto it = segmentConsumers_.find(segment.segmentId); it != segmentConsumers_.end()) {
return it->second;
}
segmentConsumers_.insert_or_assign(segment.segmentId, promise.getFuture());
}
const pulsar::ConsumerConfiguration conf = buildSegmentConfiguration(segment);
const std::string attachTopic = segment.attachTopicName();
const std::uint64_t segmentId = segment.segmentId;
auto self = shared_from_this();
classic_->subscribeSegmentAsync(
attachTopic, config_.subscriptionName, conf,
[self, promise, segmentId](std::variant<pulsar::Error, pulsar::Consumer> result) {
if (auto* consumer = std::get_if<pulsar::Consumer>(&result)) {
// pulsar::Consumer is a copyable handle (its virtual dtor suppresses the move ctor),
// so this is a shared-impl copy, not a deep copy.
pulsar::Consumer c = *consumer;
self->startReceiveLoop(c, segmentId);
promise.setValue(c);
} else {
// Evict the failed subscribe so a later reconcile retries this segment.
{
std::lock_guard<std::mutex> lock(self->mutex_);
self->segmentConsumers_.erase(segmentId);
}
promise.setError(std::get<pulsar::Error>(result));
}
});
return promise.getFuture();
}
bool QueueConsumerImpl::isSegmentStillWantedLocked(std::uint64_t segmentId) const {
if (drainedSegments_.count(segmentId) != 0) return false;
for (const auto& segment : currentLayout_->activeSegments()) {
if (segment.segmentId == segmentId) return true;
}
for (const auto& segment : currentLayout_->sealedSegments()) {
if (segment.segmentId == segmentId) return true;
}
return false;
}
void QueueConsumerImpl::subscribeSegmentWithRetry(const Segment& segment, int attempt) {
std::weak_ptr<QueueConsumerImpl> weak = weak_from_this();
getOrCreateSegmentConsumerAsync(segment).addListener([weak, segment,
attempt](const Expected<pulsar::Consumer>& result) {
auto self = weak.lock();
if (result || !self || self->closed_.load()) return;
if (attempt + 1 >= kSubscribeRetryMaxAttempts) {
LOG_ERROR("[" << self->topic_ << "] segment " << segment.segmentId << " subscribe failed after "
<< kSubscribeRetryMaxAttempts
<< " attempts; giving up until the next DAG update: " << result.error());
return;
}
{
std::lock_guard<std::mutex> lock(self->mutex_);
if (!self->isSegmentStillWantedLocked(segment.segmentId)) return;
}
LOG_WARN("[" << self->topic_ << "] segment " << segment.segmentId
<< " subscribe failed; retrying, attempt " << (attempt + 1) << " of "
<< kSubscribeRetryMaxAttempts << ": " << result.error());
auto timer = self->executor_->createDeadlineTimer();
const std::int64_t delayMs = std::min<std::int64_t>(100 * (attempt + 1), kSubscribeRetryMaxBackoffMs);
timer->expires_from_now(std::chrono::milliseconds(delayMs));
// Weak ref: closeAsync() does not cancel these timers, so a strong one would keep the
// consumer alive until the backoff elapses. (`timer` keeps itself alive until it fires.)
timer->async_wait([weak, segment, attempt, timer](const ASIO_ERROR& ec) {
auto self = weak.lock();
if (ec || !self || self->closed_.load()) return;
self->subscribeSegmentWithRetry(segment, attempt + 1);
});
});
}
void QueueConsumerImpl::startReceiveLoop(pulsar::Consumer consumer, std::uint64_t segmentId) {
if (closed_.load()) return;
auto self = shared_from_this();
consumer.receiveAsync([self, consumer, segmentId](pulsar::Result result, const pulsar::Message& message) {
if (result != pulsar::ResultOk) {
if (result == pulsar::ResultTopicTerminated) {
// The sealed segment's backlog is fully delivered — but end-of-topic only means
// the classic prefetch queue drained. Messages already fanned into the mux queue
// (or in the application's hands) still need this consumer to route their acks, so
// defer the close until every outstanding message settles (onMessageSettled
// finishes the drain then). Either way the segment is never re-subscribed.
std::optional<Future<pulsar::Consumer>> toClose;
{
std::lock_guard<std::mutex> lock(self->mutex_);
auto outstanding = self->outstanding_.find(segmentId);
if (outstanding == self->outstanding_.end() || outstanding->second == 0) {
toClose = self->takeDrainedSegmentLocked(segmentId);
} else {
self->terminatedSegments_.insert(segmentId);
}
}
if (toClose) closeWhenReady(*toClose);
}
// Otherwise (AlreadyClosed / consumer closing) just stop the loop.
return;
}
MessageId id = MessageIdFactory::create(message.getMessageId(), static_cast<std::int64_t>(segmentId));
// Report the scalable topic as the source, not the internal segment:// backing topic.
auto messageImpl = std::make_shared<MessageImpl>(message, std::move(id), self->topic_);
self->onMessageFannedIn(segmentId);
// Re-arm only once the fan-in queue has room, so a slow consumer throttles this segment —
// and hop through the executor rather than continuing inline: receiveAsync completes
// inline when a message is already prefetched and offer()'s future is already complete
// while the queue has room, so an inline continuation would recurse once per message and
// can exhaust the stack on a large backlog.
self->receiveQueue_->offer(std::move(messageImpl))
.addListener([self, consumer, segmentId](const Expected<void>&) {
self->executor_->postWork(
[self, consumer, segmentId] { self->startReceiveLoop(consumer, segmentId); });
});
});
}
void QueueConsumerImpl::onMessageFannedIn(std::uint64_t segmentId) {
std::lock_guard<std::mutex> lock(mutex_);
++outstanding_[segmentId];
}
void QueueConsumerImpl::onMessageSettled(std::uint64_t segmentId) {
std::optional<Future<pulsar::Consumer>> toClose;
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = outstanding_.find(segmentId);
if (it == outstanding_.end() || it->second == 0) return; // unknown segment or already balanced
if (--(it->second) == 0 && terminatedSegments_.count(segmentId) != 0) {
toClose = takeDrainedSegmentLocked(segmentId);
}
}
if (toClose) closeWhenReady(*toClose);
}
std::optional<Future<pulsar::Consumer>> QueueConsumerImpl::takeDrainedSegmentLocked(std::uint64_t segmentId) {
std::optional<Future<pulsar::Consumer>> future;
if (auto it = segmentConsumers_.find(segmentId); it != segmentConsumers_.end()) {
future = std::move(it->second);
segmentConsumers_.erase(it);
}
terminatedSegments_.erase(segmentId);
outstanding_.erase(segmentId);
drainedSegments_.insert(segmentId);
return future;
}
Future<MessageImplPtr> QueueConsumerImpl::receiveAsync() { return receiveQueue_->receiveAsync(); }
Future<MessageImplPtr> QueueConsumerImpl::receiveAsync(std::chrono::milliseconds timeout) {
return receiveQueue_->receiveAsync(timeout);
}
Future<pulsar::Consumer> QueueConsumerImpl::segmentConsumerFor(const MessageId& id) const {
const auto& impl = MessageIdFactory::impl(id);
if (impl) {
std::lock_guard<std::mutex> lock(mutex_);
if (auto it = segmentConsumers_.find(static_cast<std::uint64_t>(impl->segmentId));
it != segmentConsumers_.end()) {
return it->second;
}
}
detail::Promise<pulsar::Consumer> promise;
promise.setError(Error{ResultUnknownError, "no consumer for the message's segment"});
return promise.getFuture();
}
void QueueConsumerImpl::acknowledge(const MessageId& id) {
const auto& impl = MessageIdFactory::impl(id);
if (!impl) return;
const pulsar::MessageId v4 = impl->v4MessageId;
const auto segmentId = static_cast<std::uint64_t>(impl->segmentId);
auto self = shared_from_this();
segmentConsumerFor(id).addListener([self, v4, segmentId](const Expected<pulsar::Consumer>& result) {
if (result) {
pulsar::Consumer consumer = *result;
consumer.acknowledgeAsync(v4, [](pulsar::Result) {});
}
// Settle after the ack is enqueued, so a drain-deferred close still flushes it first.
self->onMessageSettled(segmentId);
});
}
void QueueConsumerImpl::acknowledge(const MessageId& /*id*/, const Transaction& /*txn*/) {
// Transactions are not implemented yet in the scalable-topics client, and an ack is
// fire-and-forget void (no error channel). Drop it — the message is simply redelivered.
LOG_WARN("[" << topic_ << "] transactional acknowledge is not implemented yet; dropping the ack");
}
void QueueConsumerImpl::negativeAcknowledge(const MessageId& id) {
const auto& impl = MessageIdFactory::impl(id);
if (!impl) return;
const pulsar::MessageId v4 = impl->v4MessageId;
const auto segmentId = static_cast<std::uint64_t>(impl->segmentId);
auto self = shared_from_this();
segmentConsumerFor(id).addListener([self, v4, segmentId](const Expected<pulsar::Consumer>& result) {
if (result) {
pulsar::Consumer consumer = *result;
consumer.negativeAcknowledge(v4);
}
// A nack settles the message too: on a terminated segment its redelivery cannot reach this
// consumer again (the receive loop has ended), so the broker's cursor simply retains it
// for the subscription's next attach.
self->onMessageSettled(segmentId);
});
}
Future<void> QueueConsumerImpl::closeAsync() {
if (closed_.exchange(true)) {
detail::Promise<void> promise;
promise.setSuccess(); // idempotent
return promise.getFuture();
}
if (dagWatch_) dagWatch_->close();
if (receiveQueue_) receiveQueue_->close(); // fail pending receives
std::vector<Future<pulsar::Consumer>> consumers;
{
std::lock_guard<std::mutex> lock(mutex_);
consumers.reserve(segmentConsumers_.size());
for (auto& [segmentId, future] : segmentConsumers_) consumers.push_back(future);
segmentConsumers_.clear();
outstanding_.clear();
terminatedSegments_.clear();
drainedSegments_.clear();
}
detail::Promise<void> promise;
auto remaining = std::make_shared<std::atomic<int>>(static_cast<int>(consumers.size()) + 1);
auto finishOne = [promise, remaining]() {
if (remaining->fetch_sub(1) == 1) promise.setSuccess();
};
for (auto& future : consumers) {
future.addListener([finishOne](const Expected<pulsar::Consumer>& result) {
if (result) {
pulsar::Consumer consumer = *result;
consumer.closeAsync([finishOne](pulsar::Result) { finishOne(); }); // swallow errors
} else {
finishOne();
}
});
}
finishOne();
return promise.getFuture();
}
} // namespace pulsar::st