/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* Licensed 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.
*/
//
// Docs: https://fburl.com/fbcref_coro_retry
//
#pragma once
#include <folly/CancellationToken.h>
#include <folly/ConstexprMath.h>
#include <folly/ExceptionWrapper.h>
#include <folly/Random.h>
#include <folly/Try.h>
#include <folly/experimental/coro/Coroutine.h>
#include <folly/experimental/coro/Result.h>
#include <folly/experimental/coro/Sleep.h>
#include <folly/experimental/coro/Task.h>
#include <folly/experimental/coro/Traits.h>
#include <cstdint>
#include <random>
#include <utility>
#if FOLLY_HAS_COROUTINES
/**
* \file experimental/coro/Retry.h
*
* Coroutine implementation of futures/Retrying.h
*
* This file provides utility functions (with building blocks) to build retry
* logic. There are three function families:
* - retryWhen: try to a func (which produces an awaitable); if fail (with an
* non folly::OperationCancelled exception), then wait on a delay func (which
* produces an awaitable too). This retry logic can run **forever**, so it is
* not recommended to use it directly. This the auxiliary function to help build
* retryN and retryWithExponentialBackoff.
* - retryN: try the func with limited N times
* - retryWithExponentialBackoff: the retries will be restarted with
* exponiential backoff.
*
* \refcode folly/docs/examples/folly/experimental/coro/Retry.cpp
*/
namespace folly::coro {
/// Execute a given asynchronous operation returned by func(),
/// retrying it on failure, if desired, after awaiting
/// retryDelay(error).
///
/// If 'func()' operation succeeds or completes with OperationCancelled
/// then completes immediately with that result.
///
/// Otherwise, if it fails with an error then the function
/// 'retryDelay()' is invoked with the exception_wrapper for
/// the error and must return another Task<void>.
///
/// If this task completes successfully or completes with then it will retry
/// the func() operation, otherwise if it completes with an
/// error then the whole operation will complete with that error.
///
/// This allows you to do some asynchronous work between retries (such as
/// sleeping for a given duration, but could be some reparatory work in
/// response to particular errors) and the retry will be scheduled once
/// the retryDelay() operation completes successfully.
template <typename Func, typename RetryDelayFunc>
auto retryWhen(Func func, RetryDelayFunc retryDelay)
-> Task<semi_await_result_t<invoke_result_t<Func&>>> {
while (true) {
exception_wrapper error;
try {
auto result = co_await folly::coro::co_awaitTry(func());
if (result.hasValue()) {
co_return std::move(result).value();
} else {
assert(result.hasException());
error = std::move(result.exception());
}
} catch (...) {
error = exception_wrapper(current_exception());
}
if (error.is_compatible_with<folly::OperationCancelled>()) {
co_yield folly::coro::co_error(std::move(error));
}
Try<void> retryResult =
co_await folly::coro::co_awaitTry(retryDelay(std::move(error)));
if (retryResult.hasException()) {
/// Failure (or cancellation) of retryDelay() indicates we should stop
/// retrying.
co_yield folly::coro::co_error(std::move(retryResult.exception()));
}
/// Otherwise we go around the loop again.
}
}
namespace detail {
template <typename Decider>
class RetryImmediatelyWithLimit {
public:
template <typename Decider2>
explicit RetryImmediatelyWithLimit(
uint32_t maxRetries, Decider2&& decider) noexcept
: retriesRemaining_(maxRetries),
decider_(static_cast<Decider2&&>(decider)) {}
Task<void> operator()(exception_wrapper&& ew) & {
if (retriesRemaining_ == 0 || !decider_(ew)) {
co_yield folly::coro::co_error(std::move(ew));
}
const auto& cancelToken = co_await co_current_cancellation_token;
if (cancelToken.isCancellationRequested()) {
co_yield folly::coro::co_error(OperationCancelled{});
}
--retriesRemaining_;
}
private:
uint32_t retriesRemaining_;
Decider decider_;
};
struct AlwaysRetry {
bool operator()(const folly::exception_wrapper&) noexcept { return true; }
};
} // namespace detail
/// Executes the operation returned by func(), retrying it up to
/// 'maxRetries' times on failure with no delay between retries.
template <typename Func, typename Decider>
auto retryN(uint32_t maxRetries, Func&& func, Decider&& decider) {
return folly::coro::retryWhen(
static_cast<Func&&>(func),
detail::RetryImmediatelyWithLimit<remove_cvref_t<Decider>>{
maxRetries, static_cast<Decider&&>(decider)});
}
template <typename Func>
auto retryN(uint32_t maxRetries, Func&& func) {
return folly::coro::retryN(
maxRetries, static_cast<Func&&>(func), detail::AlwaysRetry{});
}
namespace detail {
template <typename URNG, typename Decider>
class ExponentialBackoffWithJitter {
public:
template <typename URNG2, typename Decider2>
explicit ExponentialBackoffWithJitter(
Timekeeper* tk,
uint32_t maxRetries,
Duration minBackoff,
Duration maxBackoff,
double relativeJitterStdDev,
URNG2&& rng,
Decider2&& decider) noexcept
: timeKeeper_(tk),
maxRetries_(maxRetries),
retryCount_(0),
minBackoff_(minBackoff),
maxBackoff_(maxBackoff),
relativeJitterStdDev_(relativeJitterStdDev),
randomGen_(static_cast<URNG2&&>(rng)),
decider_(static_cast<Decider2&&>(decider)) {}
Task<void> operator()(exception_wrapper&& ew) & {
using dist = std::normal_distribution<double>;
if (retryCount_ == maxRetries_ || !decider_(ew)) {
co_yield folly::coro::co_error(std::move(ew));
}
++retryCount_;
/// The jitter will be a value between [e^-stdev]
const auto jitter = relativeJitterStdDev_ > 0
? std::exp(dist{0., relativeJitterStdDev_}(randomGen_))
: 1.;
// TODO T186551522 Calculate backoff in microseconds.
const auto backoffNominal =
Duration(folly::constexpr_clamp_cast<Duration::rep>(
jitter * minBackoff_.count() * std::pow(2, retryCount_ - 1u)));
const Duration backoff = std::clamp(
backoffNominal, minBackoff_, std::max(minBackoff_, maxBackoff_));
co_await folly::coro::sleep(backoff, timeKeeper_);
/// Check to see if we were cancelled during the sleep.
const auto& cancelToken = co_await co_current_cancellation_token;
if (cancelToken.isCancellationRequested()) {
co_yield folly::coro::co_cancelled;
}
}
private:
Timekeeper* timeKeeper_;
const uint32_t maxRetries_;
uint32_t retryCount_;
const Duration minBackoff_;
const Duration maxBackoff_;
const double relativeJitterStdDev_;
URNG randomGen_;
Decider decider_;
};
} // namespace detail
/// Executes the operation returned from 'func()', retrying it on failure
/// up to 'maxRetries' times, with an exponential backoff, doubling the backoff
/// on average for each retry, applying some random jitter, up to the specified
/// maximum backoff, passing each error to decider to decide whether to retry or
/// not.
template <typename Func, typename URNG, typename Decider>
auto retryWithExponentialBackoff(
uint32_t maxRetries,
Duration minBackoff,
Duration maxBackoff,
double relativeJitterStdDev,
Timekeeper* timeKeeper,
URNG&& rng,
Func&& func,
Decider&& decider) {
return folly::coro::retryWhen(
static_cast<Func&&>(func),
detail::ExponentialBackoffWithJitter<
remove_cvref_t<URNG>,
remove_cvref_t<Decider>>{
timeKeeper,
maxRetries,
minBackoff,
maxBackoff,
relativeJitterStdDev,
static_cast<URNG&&>(rng),
static_cast<Decider&&>(decider)});
}
template <typename Func, typename URNG>
auto retryWithExponentialBackoff(
uint32_t maxRetries,
Duration minBackoff,
Duration maxBackoff,
double relativeJitterStdDev,
Timekeeper* timeKeeper,
URNG&& rng,
Func&& func) {
return folly::coro::retryWithExponentialBackoff(
maxRetries,
minBackoff,
maxBackoff,
relativeJitterStdDev,
timeKeeper,
static_cast<URNG&&>(rng),
static_cast<Func&&>(func),
detail::AlwaysRetry{});
}
template <typename Func>
auto retryWithExponentialBackoff(
uint32_t maxRetries,
Duration minBackoff,
Duration maxBackoff,
double relativeJitterStdDev,
Timekeeper* timeKeeper,
Func&& func) {
return folly::coro::retryWithExponentialBackoff(
maxRetries,
minBackoff,
maxBackoff,
relativeJitterStdDev,
timeKeeper,
ThreadLocalPRNG(),
static_cast<Func&&>(func));
}
template <typename Func>
auto retryWithExponentialBackoff(
uint32_t maxRetries,
Duration minBackoff,
Duration maxBackoff,
double relativeJitterStdDev,
Func&& func) {
return folly::coro::retryWithExponentialBackoff(
maxRetries,
minBackoff,
maxBackoff,
relativeJitterStdDev,
static_cast<Timekeeper*>(nullptr),
static_cast<Func&&>(func));
}
template <typename Func, typename Decider>
auto retryWithExponentialBackoff(
uint32_t maxRetries,
Duration minBackoff,
Duration maxBackoff,
double relativeJitterStdDev,
Func&& func,
Decider&& decider) {
return folly::coro::retryWithExponentialBackoff(
maxRetries,
minBackoff,
maxBackoff,
relativeJitterStdDev,
static_cast<Timekeeper*>(nullptr),
ThreadLocalPRNG(),
static_cast<Func&&>(func),
static_cast<Decider&&>(decider));
}
} // namespace folly::coro
#endif // FOLLY_HAS_COROUTINES