blob: f12007d85f39880cbb2cffd9db272c20b9fbb313 [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 "paimon/rest/rest_commit_lock.h"
#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <system_error>
#include <thread>
#include <utility>
#include "fmt/format.h"
#include "paimon/rest/rest_api.h"
#include "paimon/rest/rest_http_client.h"
#include "paimon/rest/rest_messages.h"
namespace paimon {
namespace {
// Longest pause between two acquisition requests while another writer holds the lease.
constexpr int64_t kMaxPollIntervalMillis = 100;
Status ValidateGrant(const CommitLockResponse& response, const std::string& commit_user) {
const std::optional<std::string>& owner = response.GetCommitUser();
if (owner != commit_user) {
return Status::Invalid(fmt::format(
"the server returned an invalid commit lease: expected owner '{}', got {}", commit_user,
owner ? fmt::format("'{}'", owner.value()) : std::string("null")));
}
if (response.GetLeaseMillis() <= 0) {
return Status::Invalid(fmt::format(
"the server returned an invalid commit lease: expected a positive leaseMillis, got {}",
response.GetLeaseMillis()));
}
return Status::OK();
}
// A granted lease, shared by its scope and the thread renewing it, which may outlive the scope.
class LeaseState {
public:
LeaseState(std::shared_ptr<RestApi> api, Identifier identifier, std::string table_uuid,
std::string commit_user, RestCommitLock::Clock clock, int64_t lease_millis,
int64_t requested_at_millis, std::shared_ptr<Logger> logger)
: api_(std::move(api)),
identifier_(std::move(identifier)),
table_uuid_(std::move(table_uuid)),
commit_user_(std::move(commit_user)),
clock_(std::move(clock)),
lease_millis_(lease_millis),
logger_(std::move(logger)),
renewed_at_millis_(requested_at_millis) {}
Status EnsureValid() {
std::lock_guard<std::mutex> lock(mutex_);
return CheckValidLocked();
}
void Close() {
{
std::lock_guard<std::mutex> lock(mutex_);
closed_ = true;
}
closed_cv_.notify_all();
renewal_cancellation_.Cancel();
}
void RenewLoop() {
const std::chrono::milliseconds interval(std::max<int64_t>(1, lease_millis_ / 3));
std::unique_lock<std::mutex> lock(mutex_);
while (true) {
const bool closed = closed_cv_.wait_for(lock, interval, [this]() { return closed_; });
if (closed) {
return;
}
if (!CheckValidLocked().ok()) {
return;
}
const int64_t requested_at = clock_();
lock.unlock();
Status renewed = Renew();
lock.lock();
// A late answer cannot revive a scope that ended or expired while it was in flight.
if (closed_ || !failure_.ok()) {
return;
}
if (!renewed.ok()) {
failure_ = renewed;
PAIMON_LOG_WARN(logger_, "Stopped renewing the commit lease of %s: %s",
identifier_.ToString().c_str(), renewed.ToString().c_str());
return;
}
renewed_at_millis_ = requested_at;
}
}
private:
Status CheckValidLocked() {
// Measured from when the last grant was requested, so the deadline also covers the
// response time, and enforced without relying on the renewal thread having run.
if (failure_.ok() && clock_() - renewed_at_millis_ >= lease_millis_) {
failure_ = Status::Invalid("the commit lease has expired locally");
}
if (!failure_.ok()) {
return failure_.WithMessage(
fmt::format("the commit lease of {} held by commit user {} is no longer usable: {}",
identifier_.ToString(), commit_user_, failure_.message()));
}
return Status::OK();
}
Status Renew() const {
PAIMON_ASSIGN_OR_RAISE(
CommitLockResponse response,
api_->RenewCommitLock(identifier_, table_uuid_, commit_user_, &renewal_cancellation_));
if (!response.IsAcquired()) {
return Status::Invalid("the commit lease has expired");
}
PAIMON_RETURN_NOT_OK(ValidateGrant(response, commit_user_));
if (response.GetLeaseMillis() != lease_millis_) {
return Status::Invalid(fmt::format(
"the commit lease duration changed during renewal: expected leaseMillis {}, got {}",
lease_millis_, response.GetLeaseMillis()));
}
return Status::OK();
}
const std::shared_ptr<RestApi> api_;
const Identifier identifier_;
const std::string table_uuid_;
const std::string commit_user_;
const RestCommitLock::Clock clock_;
const int64_t lease_millis_;
const std::shared_ptr<Logger> logger_;
RestHttpClient::Cancellation renewal_cancellation_;
// Guards the fields below, shared with the renewal thread.
std::mutex mutex_;
std::condition_variable closed_cv_;
bool closed_ = false;
Status failure_;
int64_t renewed_at_millis_;
};
// The scope of one granted lease, renewed in the background until the scope ends or the lease
// fails.
class RestCommitLease : public CommitLockScope {
public:
explicit RestCommitLease(std::shared_ptr<LeaseState> state) : state_(std::move(state)) {}
~RestCommitLease() override {
state_->Close();
}
Status StartRenewal(const RestCommitLock::ThreadStarter& start_thread) {
// Detached rather than joined when the scope ends: a renewal can block where no
// cancellation reaches, as while refreshing credentials, and must not hold up the commit.
return start_thread([state = state_]() { state->RenewLoop(); });
}
Status EnsureValid() override {
return state_->EnsureValid();
}
private:
std::shared_ptr<LeaseState> state_;
};
} // namespace
int64_t RestCommitLock::SteadyClockMillis() {
return std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
Status RestCommitLock::StartDetachedThread(std::function<void()> task) {
try {
std::thread(std::move(task)).detach();
} catch (const std::system_error& e) {
return Status::UnknownError(std::string("failed to start a thread: ") + e.what());
}
return Status::OK();
}
RestCommitLock::RestCommitLock(const std::shared_ptr<RestApi>& api, const Identifier& identifier,
const std::string& table_uuid, const std::string& commit_user,
int64_t acquire_timeout_millis, Clock clock,
ThreadStarter start_thread)
: api_(api),
identifier_(identifier),
table_uuid_(table_uuid),
commit_user_(commit_user),
acquire_timeout_millis_(acquire_timeout_millis),
clock_(std::move(clock)),
start_thread_(std::move(start_thread)),
logger_(Logger::GetLogger("RestCommitLock")) {}
Result<std::unique_ptr<RestCommitLock>> RestCommitLock::Create(
const std::shared_ptr<RestApi>& api, const Identifier& identifier,
const std::optional<std::string>& table_uuid, const std::string& commit_user,
int64_t acquire_timeout_millis, Clock clock, ThreadStarter start_thread) {
if (api == nullptr || !clock || !start_thread) {
return Status::Invalid("a commit lease requires a rest api, a clock and a thread starter");
}
if (!table_uuid || table_uuid.value().empty()) {
return Status::Invalid(
"a commit lease requires the table UUID; pass Table::CatalogUuid() to WithTableId()");
}
if (commit_user.empty()) {
return Status::Invalid("a commit lease requires a commit user");
}
if (acquire_timeout_millis < 0) {
return Status::Invalid("the commit lock acquisition timeout cannot be negative");
}
return std::unique_ptr<RestCommitLock>(
new RestCommitLock(api, identifier, table_uuid.value(), commit_user, acquire_timeout_millis,
std::move(clock), std::move(start_thread)));
}
Result<std::unique_ptr<CommitLockScope>> RestCommitLock::Acquire() {
const int64_t started_at = clock_();
bool waiting = false;
while (true) {
const int64_t requested_at = clock_();
Result<CommitLockResponse> response =
api_->AcquireCommitLock(identifier_, table_uuid_, commit_user_);
if (!response.ok()) {
// Any error, including the rejection from a server without the lease protocol, fails
// the acquisition instead of letting the commit proceed unprotected.
const Status& status = response.status();
return status.WithMessage(fmt::format("failed to acquire the commit lock of {}: {}",
identifier_.ToString(), status.message()));
}
const CommitLockResponse& grant = response.value();
if (grant.IsAcquired()) {
PAIMON_RETURN_NOT_OK(ValidateGrant(grant, commit_user_));
auto lease = std::make_unique<RestCommitLease>(
std::make_shared<LeaseState>(api_, identifier_, table_uuid_, commit_user_, clock_,
grant.GetLeaseMillis(), requested_at, logger_));
// A grant that took longer than its lease to arrive protects nothing.
PAIMON_RETURN_NOT_OK(lease->EnsureValid());
Status started = lease->StartRenewal(start_thread_);
if (!started.ok()) {
return started.WithMessage(
fmt::format("failed to start renewing the commit lease of {}: {}",
identifier_.ToString(), started.message()));
}
PAIMON_LOG_DEBUG(logger_, "Acquired the commit lease of %s for %ld ms",
identifier_.ToString().c_str(), grant.GetLeaseMillis());
return std::unique_ptr<CommitLockScope>(std::move(lease));
}
const int64_t waited = clock_() - started_at;
if (waited >= acquire_timeout_millis_) {
return Status::IOError(fmt::format(
"timed out acquiring the commit lock of {} after {} ms, another writer holds it",
identifier_.ToString(), waited));
}
if (!waiting) {
PAIMON_LOG_INFO(logger_, "Another writer holds the commit lease of %s, waiting for it",
identifier_.ToString().c_str());
waiting = true;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(std::min(kMaxPollIntervalMillis, acquire_timeout_millis_)));
}
}
} // namespace paimon