| /** | |
| * 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 |