blob: 63cfc61ee86d3a3f55a9898d6d512800813c3f3f [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 "iceberg/util/retry_util.h"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <limits>
#include <optional>
#include <random>
#include <thread>
#include "iceberg/util/retry_util_internal.h"
namespace iceberg {
namespace {
const RetryTestHooks*& ActiveRetryTestHooks() {
// Keep test hooks thread-local so fake retry timing in one test thread does not
// leak into unrelated retry work or require synchronization around a global pointer.
static thread_local const RetryTestHooks* active_retry_test_hooks = nullptr;
return active_retry_test_hooks;
}
RetryTestHooks::TimePoint RetryNow() {
const auto* hooks = GetActiveRetryTestHooks();
if (hooks != nullptr && hooks->now) {
return hooks->now();
}
return RetryTestHooks::Clock::now();
}
void RetrySleepFor(RetryTestHooks::Duration duration) {
const auto* hooks = GetActiveRetryTestHooks();
if (hooks != nullptr && hooks->sleep_for) {
hooks->sleep_for(duration);
return;
}
std::this_thread::sleep_for(duration);
}
int32_t ApplyRetryJitter(int32_t base_delay_ms) {
const auto* hooks = GetActiveRetryTestHooks();
if (hooks != nullptr && hooks->jitter) {
return hooks->jitter(base_delay_ms);
}
static thread_local std::mt19937 gen(std::random_device{}());
const int32_t jitter_range = std::max(1, base_delay_ms / 10);
std::uniform_int_distribution<> dis(0, jitter_range - 1);
const int64_t jittered_delay_ms = static_cast<int64_t>(base_delay_ms) + dis(gen);
return static_cast<int32_t>(
std::min<int64_t>(jittered_delay_ms, std::numeric_limits<int32_t>::max()));
}
} // namespace
const RetryTestHooks* GetActiveRetryTestHooks() { return ActiveRetryTestHooks(); }
void SetActiveRetryTestHooks(const RetryTestHooks* hooks) {
ActiveRetryTestHooks() = hooks;
}
Status detail::RetryRunnerBase::ValidateConfig() const {
if (config_.num_retries < 0) {
return InvalidArgument("num_retries must be non-negative, got {}",
config_.num_retries);
}
if (config_.num_retries == 0) {
return {};
}
if (config_.num_retries == std::numeric_limits<int32_t>::max()) {
return InvalidArgument("num_retries is too large, got {}", config_.num_retries);
}
if (config_.min_wait_ms <= 0) {
return InvalidArgument("min_wait_ms must be positive, got {}", config_.min_wait_ms);
}
if (config_.max_wait_ms <= 0) {
return InvalidArgument("max_wait_ms must be positive, got {}", config_.max_wait_ms);
}
if (config_.max_wait_ms < config_.min_wait_ms) {
return InvalidArgument("max_wait_ms must be greater than or equal to min_wait_ms");
}
if (!std::isfinite(config_.scale_factor) || config_.scale_factor < 1.0) {
return InvalidArgument("scale_factor must be finite and at least 1.0, got {}",
config_.scale_factor);
}
return {};
}
std::optional<detail::RetryRunnerBase::TimePoint>
detail::RetryRunnerBase::ComputeDeadline() const {
if (config_.total_timeout_ms <= 0) {
return std::nullopt;
}
return RetryNow() + Duration(config_.total_timeout_ms);
}
bool detail::RetryRunnerBase::HasTimedOut(
const std::optional<TimePoint>& deadline) const {
return deadline.has_value() && RetryNow() >= *deadline;
}
std::optional<detail::RetryRunnerBase::Duration>
detail::RetryRunnerBase::RetryDelayWithinBudget(
int32_t attempt, const std::optional<TimePoint>& deadline) const {
const auto delay = Duration(CalculateDelay(attempt));
if (!deadline.has_value()) {
return delay;
}
const auto now = RetryNow();
if (now >= *deadline) {
return std::nullopt;
}
const auto remaining = std::chrono::duration_cast<Duration>(*deadline - now);
if (remaining <= Duration::zero() || delay >= remaining) {
return std::nullopt;
}
return delay;
}
bool detail::RetryRunnerBase::WaitForNextAttempt(
int32_t attempt, const std::optional<TimePoint>& deadline) const {
const auto delay = RetryDelayWithinBudget(attempt, deadline);
if (!delay.has_value()) {
return false;
}
RetrySleepFor(*delay);
return !HasTimedOut(deadline);
}
int32_t detail::RetryRunnerBase::CalculateDelay(int32_t attempt) const {
const double base_delay =
config_.min_wait_ms * std::pow(config_.scale_factor, attempt - 1);
const int32_t delay_ms = static_cast<int32_t>(
std::min(base_delay, static_cast<double>(config_.max_wait_ms)));
return std::clamp(ApplyRetryJitter(delay_ms), 1, config_.max_wait_ms);
}
} // namespace iceberg