blob: fb50fdd1638648401d7f3cc404027e58938b729b [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 "DagWatchSession.h"
#include <chrono>
#include <utility>
#include "PulsarApi.pb.h"
#include "lib/Commands.h"
#include "lib/ExecutorService.h"
#include "lib/LogUtils.h"
DECLARE_LOG_OBJECT()
namespace pulsar::st {
namespace {
std::atomic<std::uint64_t> sessionIdGenerator{0};
Result toResult(pulsar::proto::ServerError error) {
return error == pulsar::proto::TopicNotFound ? ResultTopicNotFound : ResultLookupError;
}
} // namespace
DagWatchSession::DagWatchSession(pulsar::ClientImplPtr client, std::string topic, bool createIfMissing)
: client_(std::move(client)),
inputTopic_(std::move(topic)),
createIfMissing_(createIfMissing),
sessionId_(++sessionIdGenerator),
backoff_(std::chrono::milliseconds(100), std::chrono::seconds(30), std::chrono::milliseconds(0)),
reconnectTimer_(client_->getIOExecutorProvider()->get()->createDeadlineTimer()) {}
std::string DagWatchSession::lookupCompatibleTopic(const std::string& topic) {
constexpr std::string_view kTopicScheme = "topic://";
if (topic.rfind(kTopicScheme, 0) == 0) {
return "persistent://" + topic.substr(kTopicScheme.size());
}
return topic;
}
Future<SegmentLayout> DagWatchSession::start() {
auto self = shared_from_this();
client_->getConnection("", lookupCompatibleTopic(inputTopic_), static_cast<size_t>(sessionId_))
.addListener([self](pulsar::Result result, const pulsar::ClientConnectionPtr& cnx) {
if (result == pulsar::ResultOk) {
self->attach(cnx);
} else {
self->initialLayoutPromise_.setError(
Error{result, "failed to get a connection for the scalable-topic lookup"});
}
});
return initialLayoutPromise_.getFuture();
}
void DagWatchSession::attach(const pulsar::ClientConnectionPtr& cnx) {
if (closed_) {
return;
}
if (!cnx->supportsScalableTopics()) {
Error error{ResultUnsupportedVersionError, "the broker does not support scalable topics"};
if (!initialLayoutPromise_.setError(error)) {
// Initial layout already delivered: keep retrying, another broker in
// the cluster may still support the feature.
LOG_WARN("[" << inputTopic_ << "] session " << sessionId_
<< ": reconnect target broker does not support scalable topics");
scheduleReconnect();
}
return;
}
{
std::lock_guard<std::mutex> lock(mutex_);
cnx_ = cnx;
}
auto self = shared_from_this();
bool registered = cnx->registerScalableTopicSession(
sessionId_, [self](pulsar::Result result, const pulsar::proto::CommandScalableTopicUpdate* update) {
self->handleSessionEvent(result, update);
});
if (!registered) {
// The connection closed between acquisition and registration.
handleConnectionClosed();
return;
}
cnx->sendCommand(Commands::newScalableTopicLookup(sessionId_, inputTopic_, createIfMissing_));
// A failed write closes the connection, which notifies the session through
// the registry: no separate write-failure path is needed here.
}
void DagWatchSession::handleSessionEvent(pulsar::Result result,
const pulsar::proto::CommandScalableTopicUpdate* update) {
if (closed_) {
return;
}
if (update == nullptr) {
(void)result;
handleConnectionClosed();
} else if (update->has_error()) {
handleServerError(*update);
} else if (update->has_dag()) {
handleDagUpdate(*update);
} else {
LOG_WARN("[" << inputTopic_ << "] session " << sessionId_
<< ": update carries neither a DAG nor an error; ignoring");
}
}
void DagWatchSession::handleDagUpdate(const pulsar::proto::CommandScalableTopicUpdate& update) {
std::string resolved;
{
std::lock_guard<std::mutex> lock(mutex_);
if (update.has_resolved_topic_name()) {
resolvedTopicName_ = update.resolved_topic_name();
}
resolved = !resolvedTopicName_.empty() ? resolvedTopicName_ : inputTopic_;
}
auto layout = SegmentLayout::fromProto(update.dag(), resolved);
if (!layout) {
LOG_ERROR("[" << inputTopic_ << "] session " << sessionId_
<< ": discarding malformed DAG update: " << layout.error().message);
// A malformed FIRST response must fail start() rather than hang it; after
// that, keep the last good layout.
initialLayoutPromise_.setError(layout.error());
return;
}
SegmentLayout oldLayout;
LayoutChangeListener listener;
{
std::lock_guard<std::mutex> lock(mutex_);
oldLayout = std::move(currentLayout_);
currentLayout_ = *layout;
listener = listener_;
}
// The broker confirmed the session is live and our state is fresh.
backoff_.reset();
LOG_INFO("[" << resolved << "] session " << sessionId_ << ": layout updated, epoch " << oldLayout.epoch()
<< " -> " << layout->epoch() << ", " << layout->activeSegments().size()
<< " active segments");
initialLayoutPromise_.complete(*layout);
if (listener) {
listener(*layout, oldLayout);
}
}
void DagWatchSession::handleServerError(const pulsar::proto::CommandScalableTopicUpdate& update) {
const std::string message = update.has_message() ? update.message() : "scalable-topic lookup failed";
LOG_ERROR("[" << inputTopic_ << "] session " << sessionId_ << ": broker reported " << update.error()
<< ": " << message);
// Before the initial layout this is a lookup failure and start() surfaces it;
// afterwards broker-side errors are transient (a reconnect clears them) and
// the connection-closed path picks them up.
initialLayoutPromise_.setError(Error{toResult(update.error()), message});
}
void DagWatchSession::handleConnectionClosed() {
{
std::lock_guard<std::mutex> lock(mutex_);
cnx_.reset();
}
if (closed_) {
return;
}
if (initialLayoutPromise_.setError(
Error{ResultConnectError, "connection closed while waiting for the scalable-topic layout"})) {
// The initial lookup never completed: surface the failure to start()'s
// caller instead of retrying behind its back.
return;
}
scheduleReconnect();
}
void DagWatchSession::scheduleReconnect() {
if (closed_) {
return;
}
auto delay = backoff_.next();
LOG_INFO("[" << inputTopic_ << "] session " << sessionId_ << ": reconnecting the DAG watch in "
<< std::chrono::duration_cast<std::chrono::milliseconds>(delay).count() << " ms");
std::weak_ptr<DagWatchSession> weakSelf = shared_from_this();
reconnectTimer_->expires_from_now(delay);
reconnectTimer_->async_wait([weakSelf](const ASIO_ERROR& error) {
auto self = weakSelf.lock();
if (self && !error) {
self->reconnect();
}
});
}
void DagWatchSession::reconnect() {
if (closed_) {
return;
}
auto self = shared_from_this();
client_->getConnection("", lookupCompatibleTopic(inputTopic_), static_cast<size_t>(sessionId_))
.addListener([self](pulsar::Result result, const pulsar::ClientConnectionPtr& cnx) {
if (result == pulsar::ResultOk) {
self->attach(cnx);
} else {
LOG_WARN("[" << self->inputTopic_ << "] session " << self->sessionId_
<< ": DAG watch reconnect failed (" << result << "); will retry");
self->scheduleReconnect();
}
});
}
SegmentLayout DagWatchSession::currentLayout() const {
std::lock_guard<std::mutex> lock(mutex_);
return currentLayout_;
}
std::string DagWatchSession::topicName() const {
std::lock_guard<std::mutex> lock(mutex_);
return !resolvedTopicName_.empty() ? resolvedTopicName_ : inputTopic_;
}
void DagWatchSession::setLayoutChangeListener(LayoutChangeListener listener) {
std::lock_guard<std::mutex> lock(mutex_);
listener_ = std::move(listener);
}
void DagWatchSession::close() {
if (closed_.exchange(true)) {
return;
}
ASIO_ERROR ignored;
reconnectTimer_->cancel(ignored);
pulsar::ClientConnectionPtr cnx;
{
std::lock_guard<std::mutex> lock(mutex_);
cnx = cnx_.lock();
cnx_.reset();
}
if (cnx) {
cnx->removeScalableTopicSession(sessionId_);
cnx->sendCommand(Commands::newScalableTopicClose(sessionId_));
}
}
} // namespace pulsar::st