blob: e9fe1cf16ac28e332105d889f9179b98640552a8 [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 "StProducerImpl.h"
#include <pulsar/MessageBuilder.h>
#include <algorithm>
#include <chrono>
#include <memory>
#include <string>
#include <unordered_set>
#include <utility>
#include <variant>
#include <vector>
#include "MessageIdImpl.h"
#include "lib/ExecutorService.h"
#include "lib/LogUtils.h"
DECLARE_LOG_OBJECT()
namespace pulsar::st {
StProducerImpl::StProducerImpl(pulsar::ClientImplPtr classic, ProducerConfig config)
: classic_(std::move(classic)),
config_(std::move(config)),
topic_(config_.topic),
producerName_(config_.producerName.value_or(std::string{})),
currentLayout_(std::make_shared<SegmentLayout>()) {}
Future<void> StProducerImpl::start() {
dagWatch_ = std::make_shared<DagWatchSession>(classic_, config_.topic, /*createIfMissing*/ true);
std::weak_ptr<StProducerImpl> weak = weak_from_this();
// Register the layout listener BEFORE start() so the very first layout is delivered to it.
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 StProducerImpl::onStartResult(const Expected<SegmentLayout>& result) {
// Only the failure path is handled here: the DagWatchSession fails start()'s future (without
// invoking the layout listener) when the lookup fails before the first layout arrives. On
// success the listener runs immediately after and drives completeStart().
if (!result) {
startPromise_.setError(result.error());
}
}
void StProducerImpl::onLayoutChange(const SegmentLayout& newLayout, const SegmentLayout& /*oldLayout*/) {
std::vector<Future<pulsar::Producer>> retired;
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> active;
for (const auto& segment : newLayout.activeSegments()) active.insert(segment.segmentId);
for (auto it = segmentProducers_.begin(); it != segmentProducers_.end();) {
if (active.find(it->first) == active.end()) {
retired.push_back(std::move(it->second));
it = segmentProducers_.erase(it);
} else {
++it;
}
}
}
// Close producers for segments that left the layout, fire-and-forget.
for (auto& future : retired) {
future.addListener([](const Expected<pulsar::Producer>& result) {
if (result) {
pulsar::Producer producer = *result;
producer.closeAsync([](pulsar::Result) {});
}
});
}
if (first) {
completeStart();
} else if (requiresExclusiveAttach(config_.accessMode)) {
// Exclusive modes claim newly-active segments eagerly (best-effort; failures are logged).
for (const auto& segment : newLayout.activeSegments()) {
getOrCreateSegmentProducerAsync(segment.segmentId);
}
}
}
void StProducerImpl::completeStart() {
if (!requiresExclusiveAttach(config_.accessMode)) {
startPromise_.setSuccess(); // Shared: segment producers are created lazily on first send.
return;
}
auto layout = snapshotLayout();
const auto& segments = layout->activeSegments();
if (segments.empty()) {
startPromise_.setSuccess();
return;
}
// Exclusive: eagerly attach every active segment; the create fails up front if the exclusive
// claim is refused, exactly like the Java strict eager attach.
auto remaining = std::make_shared<std::atomic<int>>(static_cast<int>(segments.size()));
for (const auto& segment : segments) {
getOrCreateSegmentProducerAsync(segment.segmentId)
.addListener([self = shared_from_this(), remaining](const Expected<pulsar::Producer>& result) {
if (!result) {
self->startPromise_.setError(
result.error()); // first error wins (complete is idempotent)
return;
}
if (remaining->fetch_sub(1) == 1) self->startPromise_.setSuccess();
});
}
}
pulsar::ProducerConfiguration StProducerImpl::buildSegmentConfiguration(const Segment& segment) const {
// Build a FRESH config every time: pulsar::ProducerConfiguration's copy constructor shares its
// impl, so copy-then-mutate would clobber every other segment's config.
pulsar::ProducerConfiguration conf;
conf.setSchema(config_.schema);
conf.setAccessMode(toClassicAccessMode(config_.accessMode));
if (config_.sendTimeoutMs) conf.setSendTimeout(static_cast<int>(*config_.sendTimeoutMs));
conf.setBlockIfQueueFull(config_.blockIfQueueFull);
if (config_.initialSequenceId) conf.setInitialSequenceId(*config_.initialSequenceId);
for (const auto& [key, value] : config_.properties) conf.setProperty(key, value);
if (config_.producerName) {
conf.setProducerName(*config_.producerName + "-seg-" + std::to_string(segment.segmentId));
}
// Legacy segments wrap an externally-managed persistent:// topic; mark the connection so the
// regular->scalable migration precheck (PIP-475) recognizes it. Real segment:// topics aren't.
if (segment.isLegacy()) {
conf.setProperty("__pulsar.v5.managed", "true");
}
// PIP-486 entry-bucketing: only the encryption-disables-batching branch is portable today; the
// EntryBucketBatcherBuilder branch has no C++ seam yet (TODO when a batcher-builder lands).
if (conf.isEncryptionEnabled()) {
conf.setBatchingEnabled(false);
}
return conf;
}
Future<pulsar::Producer> StProducerImpl::getOrCreateSegmentProducerAsync(std::uint64_t segmentId) {
std::lock_guard<std::mutex> lock(mutex_);
if (auto it = segmentProducers_.find(segmentId); it != segmentProducers_.end()) {
return it->second; // concurrent cold senders share the one creation attempt
}
const Segment* segment = nullptr;
for (const auto& candidate : currentLayout_->activeSegments()) {
if (candidate.segmentId == segmentId) {
segment = &candidate;
break;
}
}
detail::Promise<pulsar::Producer> promise;
if (segment == nullptr) {
// Transient — don't cache, so a later routing attempt re-resolves against a fresh layout.
promise.setError(Error{ResultUnknownError,
"segment " + std::to_string(segmentId) + " is not in the active layout"});
return promise.getFuture();
}
pulsar::ProducerConfiguration conf = buildSegmentConfiguration(*segment);
const std::string attachTopic = segment->attachTopicName();
// Pin to the DAG-provided owner broker, picking the address that matches how this client
// connects — the same choice the classic lookup makes (BinaryProtoLookupService uses
// `useTls() ? brokerUrlTls : brokerUrl`). If the DAG did not advertise the address we need,
// leave the pin unset and fall back to the normal segment:// lookup, which resolves it.
const bool useTls = classic_->getServiceInfo().useTls();
const std::string* url =
useTls ? currentLayout_->brokerUrlTls(segmentId) : currentLayout_->brokerUrl(segmentId);
std::optional<std::string> assignedBrokerUrl;
if (url != nullptr) {
assignedBrokerUrl = *url;
}
auto future = promise.getFuture();
segmentProducers_.insert_or_assign(segmentId, future);
classic_->createSegmentProducerAsync(
attachTopic, conf,
[promise](std::variant<pulsar::Error, pulsar::Producer> result) {
if (auto* producer = std::get_if<pulsar::Producer>(&result)) {
promise.setValue(std::move(*producer));
} else {
promise.setError(std::get<pulsar::Error>(result));
}
},
assignedBrokerUrl);
return future;
}
Future<MessageId> StProducerImpl::sendAsync(OutgoingMessage message) {
detail::Promise<MessageId> userPromise;
auto userFuture = userPromise.getFuture();
if (closed_.load()) {
userPromise.setError(Error{ResultAlreadyClosed, "producer is closed"});
return userFuture;
}
if (message.transaction) {
userPromise.setError(Error{ResultOperationNotSupported,
"transactions are not implemented yet in the scalable-topics client"});
return userFuture;
}
// Register in the in-flight set (for flush) BEFORE dispatching: a warm send can complete
// synchronously, which would otherwise run the removal listener before insertion.
std::uint64_t inFlightId = 0;
{
std::lock_guard<std::mutex> lock(mutex_);
inFlightId = nextInFlightId_++;
inFlight_.insert_or_assign(inFlightId, userFuture);
}
std::weak_ptr<StProducerImpl> weak = weak_from_this();
userFuture.addListener([weak, inFlightId](const Expected<MessageId>&) {
if (auto self = weak.lock()) {
std::lock_guard<std::mutex> lock(self->mutex_);
self->inFlight_.erase(inFlightId);
}
});
auto layout = snapshotLayout();
Expected<std::uint64_t> route =
message.key.has_value() ? router_.route(*message.key, *layout) : router_.routeRoundRobin(*layout);
if (!route) {
// Route failure (no active segment yet / uncovered hash) is terminal, not retried.
userPromise.setError(route.error());
return userFuture;
}
dispatchSend(std::move(message), *route, /*attempt*/ 0, userPromise);
return userFuture;
}
void StProducerImpl::dispatchSend(OutgoingMessage message, std::uint64_t segmentId, int attempt,
const detail::Promise<MessageId>& userPromise) {
auto self = shared_from_this();
getOrCreateSegmentProducerAsync(segmentId).addListener(
[self, message = std::move(message), segmentId, attempt,
userPromise](const Expected<pulsar::Producer>& created) mutable {
if (!created) {
self->handleSegmentFailure(created.error(), std::move(message), segmentId, attempt,
userPromise);
return;
}
pulsar::Producer producer = *created;
pulsar::Message classicMessage = self->buildClassicMessage(message);
producer.sendAsync(
classicMessage, [self, message = std::move(message), segmentId, attempt, userPromise](
pulsar::Result result, const pulsar::MessageId& v4Id) mutable {
if (result == pulsar::ResultOk) {
userPromise.setValue(
MessageIdFactory::create(v4Id, static_cast<std::int64_t>(segmentId)));
} else {
self->handleSegmentFailure(Error{result, ""}, std::move(message), segmentId, attempt,
userPromise);
}
});
});
}
void StProducerImpl::handleSegmentFailure(Error error, OutgoingMessage message, std::uint64_t segmentId,
int attempt, const detail::Promise<MessageId>& userPromise) {
if (!isSegmentGoneError(error.result, error.message) || attempt >= kSendRetryMaxAttempts ||
closed_.load()) {
userPromise.setError(std::move(error));
return;
}
// The segment was sealed/terminated under us. Drop its cached producer so the retry recreates it
// against the layout the DAG watch is about to deliver, then re-route after a short backoff.
{
std::lock_guard<std::mutex> lock(mutex_);
segmentProducers_.erase(segmentId);
}
LOG_INFO("[" << topic_ << "] segment " << segmentId << " is gone; retrying send, attempt "
<< (attempt + 1) << " of " << kSendRetryMaxAttempts);
auto timer = classic_->getIOExecutorProvider()->get()->createDeadlineTimer();
const std::int64_t delayMs = std::min<std::int64_t>(100 * (attempt + 1), kSendRetryMaxBackoffMs);
timer->expires_from_now(std::chrono::milliseconds(delayMs));
// Capture a weak ref: closeAsync() does not cancel these timers, so a strong one would keep the
// producer alive until the backoff elapses. If the producer is gone by then, fail the send
// instead of resurrecting it. (`timer` is captured to keep itself alive until it fires.)
std::weak_ptr<StProducerImpl> weak = weak_from_this();
timer->async_wait([weak, message = std::move(message), attempt, userPromise,
timer](const ASIO_ERROR& ec) mutable {
auto self = weak.lock();
if (ec || !self || self->closed_.load()) {
userPromise.setError(Error{ResultAlreadyClosed, "producer closed during send retry"});
return;
}
auto layout = self->snapshotLayout();
Expected<std::uint64_t> route = message.key.has_value() ? self->router_.route(*message.key, *layout)
: self->router_.routeRoundRobin(*layout);
if (!route) {
userPromise.setError(route.error());
return;
}
self->dispatchSend(std::move(message), *route, attempt + 1, userPromise);
});
}
pulsar::Message StProducerImpl::buildClassicMessage(const OutgoingMessage& message) const {
pulsar::MessageBuilder builder;
// The classic client copies the content synchronously, so publishing the borrowed view here
// still honors the zero-copy (BytesView) lifetime contract.
if (message.usesView) {
builder.setContent(message.payloadView.data(), message.payloadView.size());
} else {
builder.setContent(message.payload.data(), message.payload.size());
}
if (message.key) builder.setPartitionKey(*message.key);
for (const auto& [key, value] : message.properties) builder.setProperty(key, value);
if (message.eventTime) {
builder.setEventTimestamp(static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(message.eventTime->time_since_epoch())
.count()));
}
if (message.sequenceId) builder.setSequenceId(*message.sequenceId);
if (message.deliverAt) {
builder.setDeliverAt(static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(message.deliverAt->time_since_epoch())
.count()));
}
if (!message.replicationClusters.empty()) builder.setReplicationClusters(message.replicationClusters);
return builder.build();
}
std::optional<std::int64_t> StProducerImpl::lastSequenceId() const {
std::int64_t max = config_.initialSequenceId.value_or(-1);
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& [segmentId, future] : segmentProducers_) {
if (future.isReady()) {
auto result = future.get();
if (result) {
max = std::max(max, result->getLastSequenceId());
}
}
}
return max < 0 ? std::nullopt : std::optional<std::int64_t>(max);
}
Future<void> StProducerImpl::flushAsync() {
std::vector<Future<MessageId>> pending;
std::vector<Future<pulsar::Producer>> producers;
{
std::lock_guard<std::mutex> lock(mutex_);
pending.reserve(inFlight_.size());
for (const auto& [id, future] : inFlight_) pending.push_back(future);
producers.reserve(segmentProducers_.size());
for (const auto& [id, future] : segmentProducers_) producers.push_back(future);
}
detail::Promise<void> promise;
// +1 base count so an empty snapshot completes immediately.
auto remaining = std::make_shared<std::atomic<int>>(static_cast<int>(pending.size()) + 1);
auto reportedError = std::make_shared<std::atomic<bool>>(false);
auto finishOne = [promise, remaining]() {
if (remaining->fetch_sub(1) == 1) promise.setSuccess(); // no-op if already errored
};
// Nudge the ready classic producers so any batched sends complete; the actual completion is
// tracked by the in-flight user futures below.
for (auto& producerFuture : producers) {
if (producerFuture.isReady()) {
auto result = producerFuture.get();
if (result) {
pulsar::Producer producer = *result;
producer.flushAsync([](pulsar::Result) {});
}
}
}
for (auto& future : pending) {
future.addListener([promise, reportedError, finishOne](const Expected<MessageId>& result) {
if (!result && !reportedError->exchange(true)) promise.setError(result.error());
finishOne();
});
}
finishOne();
return promise.getFuture();
}
Future<void> StProducerImpl::closeAsync() {
if (closed_.exchange(true)) {
detail::Promise<void> promise;
promise.setSuccess(); // idempotent: already closed
return promise.getFuture();
}
if (dagWatch_) dagWatch_->close();
std::vector<Future<pulsar::Producer>> producers;
{
std::lock_guard<std::mutex> lock(mutex_);
producers.reserve(segmentProducers_.size());
for (auto& [id, future] : segmentProducers_) producers.push_back(future);
segmentProducers_.clear();
}
detail::Promise<void> promise;
auto remaining = std::make_shared<std::atomic<int>>(static_cast<int>(producers.size()) + 1);
auto finishOne = [promise, remaining]() {
if (remaining->fetch_sub(1) == 1) promise.setSuccess();
};
for (auto& producerFuture : producers) {
producerFuture.addListener([finishOne](const Expected<pulsar::Producer>& result) {
if (result) {
pulsar::Producer producer = *result;
producer.closeAsync(
[finishOne](pulsar::Result) { finishOne(); }); // swallow per-producer errors
} else {
finishOne(); // creation itself failed — nothing to close
}
});
}
finishOne();
return promise.getFuture();
}
std::shared_ptr<const SegmentLayout> StProducerImpl::snapshotLayout() const {
std::lock_guard<std::mutex> lock(mutex_);
return currentLayout_;
}
bool StProducerImpl::isSegmentGoneError(pulsar::Result result, const std::string& message) {
if (result == pulsar::ResultTopicTerminated || result == pulsar::ResultAlreadyClosed) return true;
// Some paths surface a sealed segment as a generic error carrying the terminated text.
return message.find("TopicTerminated") != std::string::npos ||
message.find("terminated") != std::string::npos;
}
bool StProducerImpl::requiresExclusiveAttach(ProducerAccessMode mode) {
return mode != ProducerAccessMode::Shared;
}
pulsar::ProducerConfiguration::ProducerAccessMode StProducerImpl::toClassicAccessMode(
ProducerAccessMode mode) {
// The st and classic enums do NOT share numeric values — map by name.
switch (mode) {
case ProducerAccessMode::Shared:
return pulsar::ProducerConfiguration::Shared;
case ProducerAccessMode::Exclusive:
return pulsar::ProducerConfiguration::Exclusive;
case ProducerAccessMode::ExclusiveWithFencing:
return pulsar::ProducerConfiguration::ExclusiveWithFencing;
case ProducerAccessMode::WaitForExclusive:
return pulsar::ProducerConfiguration::WaitForExclusive;
}
return pulsar::ProducerConfiguration::Shared;
}
} // namespace pulsar::st