common/async: Add redirect_error.h

Straightforwardly update `redirect_error` to work with Asio's
`disposition` concept generally. No point in sending upstream because
Asio never accepts contributions.

Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
This commit is contained in:
Adam C. Emerson 2025-08-14 21:37:07 -04:00
parent 52c80fb413
commit b0f2bb760c
No known key found for this signature in database
4 changed files with 629 additions and 0 deletions

View File

@ -211,6 +211,12 @@ Copyright: 2020 Red Hat <contact@redhat.com>
2003-2019 Christopher M. Kohlhoff <chris@kohlhoff.com>
License: Boost Software License, Version 1.0
Files: src/common/async/redirect_error.h,
src/test/common/test_async_redirect_error.cc
Copyright: 2025 Contributors to the Ceph Project
2003-2024 Christopher M. Kohlhoff <chris@kohlhoff.com>
License: Boost Software License, Version 1.0
Files: src/script/backport-create-issue
Copyright: 2015 Red Hat <contact@redhat.com>
2018 SUSE LLC

View File

@ -0,0 +1,344 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab ft=cpp
/*
* Ceph - scalable distributed file system
*/
// Copyright: 2025 Contributors to the Ceph Project
// Based on boost/asio/redirect_error.hpp and
// boost/asio/impl/redirect_error.hpp which are
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying copy at http://www.boost.org/LICENSE_1_0.txt)
#pragma once
#include <type_traits>
#include <boost/asio/associated_executor.hpp>
#include <boost/asio/async_result.hpp>
#include <boost/asio/default_completion_token.hpp>
#include <boost/asio/disposition.hpp>
#include <boost/asio/handler_continuation_hook.hpp>
/// \file common/async/redirect_error.h
///
/// \brief `redirect_error` that knows about `dispositions`.
///
/// Asio has a very useful concept called a `disposition` that
/// generalizes the notion of an error code. Unfortunately
/// `redirect_error` doesn't know about dispositions, making it less
/// useful.
namespace ceph::async {
/// A @ref completion_token adapter used to specify that an error
/// produced by an asynchronous operation is captured to a specified
/// variable. The variable must be of a `disposition` type.
/**
* The redirect_error_t class is used to indicate that any disposition produced
* by an asynchronous operation is captured to a specified variable.
*/
template<typename CompletionToken,
boost::asio::disposition Disposition>
class redirect_error_t {
public: // Sigh
CompletionToken token;
Disposition& disposition;
template<typename CT>
redirect_error_t(CT&& token, Disposition& disposition)
: token(std::forward<CT>(token)), disposition(disposition) {}
};
/// A function object type that adapts a @ref completion_token to capture
/// disposition values to a variable.
/**
* May also be used directly as a completion token, in which case it adapts the
* asynchronous operation's default completion token (or boost::asio::deferred
* if no default is available).
*/
template<boost::asio::disposition Disposition>
class partial_redirect_error {
public:
Disposition& disposition;
/// Constructor that specifies the variable used to capture disposition values.
explicit partial_redirect_error(Disposition& disposition)
: disposition(disposition) {}
/// Adapt a @ref completion_token to specify that the completion handler
/// should capture disposition values to a variable.
template<typename CompletionToken>
[[nodiscard]] constexpr inline auto
operator ()(CompletionToken&& token) const {
return redirect_error_t<std::decay_t<CompletionToken>, Disposition>{
std::forward<CompletionToken>(token), disposition};
}
};
/// Create a partial completion token adapter that captures disposition values
/// to a variable.
template<boost::asio::disposition Disposition>
[[nodiscard]] inline auto redirect_error(Disposition& d)
{
return partial_redirect_error<std::decay_t<Disposition>>{d};
}
/// Adapt a @ref completion_token to capture disposition values to a variable.
template<typename CompletionToken, boost::asio::disposition Disposition>
[[nodiscard]] inline auto redirect_error(CompletionToken&& token,
Disposition& d)
{
return redirect_error_t<std::decay_t<CompletionToken>,
std::decay_t<Disposition>>{
std::forward<CompletionToken>(token), d};
}
namespace detail {
template<typename Handler, boost::asio::disposition Disposition>
class redirect_error_handler {
public:
Disposition& disposition;
// Essentially a call-once function, invoked as an rvalue.
Handler handler;
using result_type = void;
template<typename CompletionToken>
redirect_error_handler(
redirect_error_t<std::decay_t<CompletionToken>,
std::decay_t<Disposition>> re)
: disposition(re.disposition), handler(std::move(re.token)) {}
template<typename RedirectedHandler>
redirect_error_handler(Disposition &disposition, RedirectedHandler&& handler)
: disposition(disposition),
handler(std::forward<RedirectedHandler>(handler)) {}
void operator ()() {
std::move(handler)();
}
template<typename Arg0, typename ...Args>
std::enable_if_t<!std::is_same_v<std::decay_t<Arg0>,
Disposition>>
operator ()(Arg0&& arg0, Args ...args) {
std::move(handler)(std::forward<Arg0>(arg0), std::forward<Args>(args)...);
}
template<typename... Args>
void operator ()(const Disposition& d, Args&& ...args) {
disposition = d;
std::move(handler)(std::forward<Args>(args)...);
}
};
template<boost::asio::disposition Disposition, typename Handler>
inline bool asio_handler_is_continuation(
redirect_error_handler<Disposition, Handler>* this_handler)
{
using boost::asio::asio_handler_is_continuation;
return asio_handler_is_continuation(&this_handler->handler);
}
template<typename Signature>
struct redirect_error_signature
{
using type = Signature;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(Disposition, Args...)>
{
typedef R type(Args...);
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(const Disposition&, Args...)>
{
typedef R type(Args...);
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(Disposition, Args...) &>
{
typedef R type(Args...) &;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(const Disposition&, Args...) &>
{
typedef R type(Args...) &;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(Disposition, Args...) &&>
{
typedef R type(Args...) &&;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(const Disposition&, Args...) &&>
{
typedef R type(Args...) &&;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(Disposition, Args...) noexcept>
{
typedef R type(Args...) & noexcept;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(const Disposition&, Args...) noexcept>
{
typedef R type(Args...) & noexcept;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(Disposition, Args...) & noexcept>
{
typedef R type(Args...) & noexcept;
};
template <typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(const Disposition&, Args...) & noexcept>
{
typedef R type(Args...) & noexcept;
};
template<typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(Disposition, Args...) && noexcept>
{
typedef R type(Args...) && noexcept;
};
template <typename R, boost::asio::disposition Disposition, typename... Args>
struct redirect_error_signature<R(const Disposition&, Args...) && noexcept>
{
typedef R type(Args...) && noexcept;
};
template<typename Initiation, typename = void>
class initiation_base : public Initiation
{
public:
template<typename I>
explicit initiation_base(I&& initiation)
: Initiation(std::forward<I>(initiation)) {}
};
template<typename Initiation>
class initiation_base<Initiation,
std::enable_if_t<!std::is_class_v<Initiation>>>
{
public:
template<typename I>
explicit initiation_base(I&& initiation)
: initiation(std::forward<I>(initiation)) {}
template<typename... Args>
void operator()(Args&&... args) const
{
initiation(std::forward<Args>(args)...);
}
private:
Initiation initiation;
};
} // namespace detail
} // namespace ceph::async
namespace boost::asio {
template<boost::asio::disposition Disposition, typename CompletionToken,
typename Signature>
struct async_result<::ceph::async::redirect_error_t<CompletionToken,
Disposition>, Signature>
: async_result<CompletionToken,
typename ::ceph::async::detail::redirect_error_signature<
Signature>::type>
{
template<typename Initiation>
struct init_wrapper : ::ceph::async::detail::initiation_base<Initiation>
{
using ::ceph::async::detail::initiation_base<Initiation>::initiation_base;
template<typename Handler, typename... Args>
void operator ()(Handler&& handler, Disposition* d, Args&&... args) &&
{
static_cast<Initiation&&>(*this)(
::ceph::async::detail::redirect_error_handler<
decay_t<Handler>, Disposition>(
*d, std::forward<Handler>(handler)),
std::forward<Args>(args)...);
}
template<typename Handler, typename... Args>
void operator ()(Handler&& handler, Disposition* d, Args&&... args) const &
{
static_cast<const Initiation&>(*this)(
::ceph::async::detail::redirect_error_handler<
decay_t<Handler>, Disposition>(
*d, std::forward<Handler>(handler)),
std::forward<Args>(args)...);
}
};
template<typename Initiation, typename RawCompletionToken, typename... Args>
static auto initiate(Initiation&& initiation,
RawCompletionToken&& token, Args&&... args)
{
return async_initiate<
std::conditional_t<
std::is_const_v<remove_reference_t<RawCompletionToken>>,
const CompletionToken, CompletionToken>,
typename ::ceph::async::detail::redirect_error_signature<Signature>::type>(
init_wrapper<std::decay_t<Initiation>>(
std::forward<Initiation>(initiation)),
token.token, &token.disposition, std::forward<Args>(args)...);
}
};
template<template<typename, typename> class Associator, typename Handler,
typename DefaultCandidate, typename Disposition>
struct associator<Associator,
::ceph::async::detail::redirect_error_handler<Handler,
Disposition>,
DefaultCandidate>
: Associator<Handler, DefaultCandidate>
{
static auto get(const ::ceph::async::detail::redirect_error_handler<
Handler, Disposition>& h) noexcept
{
return Associator<Handler, DefaultCandidate>::get(h.handler);
}
static auto get(const ::ceph::async::detail::redirect_error_handler<
Handler, Disposition>& h,
const DefaultCandidate& c) noexcept
{
return Associator<Handler, DefaultCandidate>::get(h.handler, c);
}
};
template<boost::asio::disposition Disposition, typename... Signatures>
struct async_result<::ceph::async::partial_redirect_error<Disposition>,
Signatures...>
{
template <typename Initiation, typename RawCompletionToken, typename... Args>
static auto initiate(Initiation&& initiation,
RawCompletionToken&& token, Args&&... args)
{
return async_initiate<Signatures...>(
std::forward<Initiation>(initiation),
::ceph::async::redirect_error_t<
default_completion_token_t<associated_executor_t<Initiation>>,
Disposition>(
default_completion_token_t<associated_executor_t<Initiation>>{},
token.disposition), std::forward<Args>(args)...);
}
};
} // namespace boost::asio

View File

@ -506,3 +506,6 @@ add_executable(unittest_async_cond test_async_cond.cc)
target_link_libraries(unittest_async_cond GTest::GTest)
add_ceph_unittest(unittest_async_cond)
add_executable(unittest_async_redirect_error test_async_redirect_error.cc)
target_link_libraries(unittest_async_redirect_error GTest::GTest)
add_ceph_unittest(unittest_async_redirect_error)

View File

@ -0,0 +1,276 @@
// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab ft=cpp
/*
* Ceph - scalable distributed file system
*/
// Based on libs/asio/test/redirect_error.cpp which is
// Copyright (c) 2003-2024 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See
// accompanying copy at http://www.boost.org/LICENSE_1_0.txt)
#include "common/async/redirect_error.h"
#include <exception>
#include <future>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/system_timer.hpp>
#include <boost/asio/use_future.hpp>
#include <gtest/gtest.h>
struct redirect_error_handler
{
int* count;
explicit redirect_error_handler(int* c)
: count(c) {}
void operator ()()
{
++(*count);
}
};
TEST(RedirectError, RedirectError)
{
boost::asio::io_context io1;
boost::asio::io_context io2;
boost::asio::system_timer timer1(io1);
boost::system::error_code ec = boost::asio::error::would_block;
int count = 0;
timer1.expires_after(boost::asio::chrono::seconds(0));
timer1.async_wait(
ceph::async::redirect_error(
boost::asio::bind_executor(io2.get_executor(),
redirect_error_handler(&count)), ec));
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(0, count);
io1.run();
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(0, count);
io2.run();
ASSERT_FALSE(ec);
ASSERT_EQ(1, count);
ec = boost::asio::error::would_block;
timer1.async_wait(
ceph::async::redirect_error(
boost::asio::bind_executor(io2.get_executor(),
boost::asio::deferred), ec))(redirect_error_handler(&count));
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(1, count);
io1.restart();
io1.run();
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(1, count);
io2.restart();
io2.run();
ASSERT_FALSE(ec);
ASSERT_EQ(2, count);
ec = boost::asio::error::would_block;
std::future<void> f = timer1.async_wait(
ceph::async::redirect_error(
boost::asio::bind_executor(io2.get_executor(),
boost::asio::use_future), ec));
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(std::future_status::timeout, f.wait_for(std::chrono::seconds(0)));
io1.restart();
io1.run();
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(std::future_status::timeout, f.wait_for(std::chrono::seconds(0)));
io2.restart();
io2.run();
ASSERT_FALSE(ec);
ASSERT_EQ(std::future_status::ready, f.wait_for(std::chrono::seconds(0)));
}
TEST(RedirectError, RedirectErrorExceptionPtr)
{
boost::asio::io_context io;
std::exception_ptr eptr = nullptr;
int count = 0;
boost::asio::co_spawn(
io, []() -> boost::asio::awaitable<void> {
throw std::exception{};
co_return;
},
ceph::async::redirect_error(
boost::asio::bind_executor(io.get_executor(),
redirect_error_handler(&count)), eptr));
ASSERT_FALSE(eptr);
ASSERT_EQ(0, count);
io.run();
ASSERT_TRUE(eptr);
ASSERT_EQ(1, count);
boost::asio::co_spawn(
io, []() -> boost::asio::awaitable<void> {
co_return;
},
ceph::async::redirect_error(
boost::asio::bind_executor(io.get_executor(),
redirect_error_handler(&count)), eptr));
ASSERT_TRUE(eptr);
ASSERT_EQ(1, count);
io.restart();
io.run();
ASSERT_FALSE(eptr);
ASSERT_EQ(2, count);
}
TEST(RedirectError, PartialRedirectError)
{
boost::asio::io_context io1;
boost::asio::io_context io2;
boost::asio::system_timer timer1(io1);
boost::system::error_code ec = boost::asio::error::would_block;
int count = 0;
timer1.expires_after(boost::asio::chrono::seconds(0));
timer1.async_wait(ceph::async::redirect_error(ec))(
boost::asio::bind_executor(io2.get_executor(),
redirect_error_handler(&count)));
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(0, count);
io1.run();
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(0, count);
io2.run();
ASSERT_FALSE(ec);
ASSERT_EQ(1, count);
ec = boost::asio::error::would_block;
timer1.async_wait(ceph::async::redirect_error(ec))(
boost::asio::bind_executor(io2.get_executor(),
boost::asio::deferred))(redirect_error_handler(&count));
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(1, count);
io1.restart();
io1.run();
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(1, count);
io2.restart();
io2.run();
ASSERT_FALSE(ec);
ASSERT_EQ(2, count);
ec = boost::asio::error::would_block;
timer1.async_wait()(ceph::async::redirect_error(ec))(
boost::asio::bind_executor(io2.get_executor(),
boost::asio::deferred))(redirect_error_handler(&count));
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(2, count);
io1.restart();
io1.run();
ASSERT_EQ(boost::asio::error::would_block, ec);
ASSERT_EQ(2, count);
io2.restart();
io2.run();
ASSERT_FALSE(ec);
ASSERT_EQ(3, count);
ec = boost::asio::error::would_block;
std::future<void> f = timer1.async_wait(ceph::async::redirect_error(ec))(
boost::asio::bind_executor(io2.get_executor(), boost::asio::use_future));
ASSERT_EQ(boost::asio::error::would_block, ec);
EXPECT_EQ(std::future_status::timeout, f.wait_for(std::chrono::seconds(0)));
io1.restart();
io1.run();
EXPECT_EQ(boost::asio::error::would_block, ec);
EXPECT_EQ(std::future_status::timeout, f.wait_for(std::chrono::seconds(0)));
io2.restart();
io2.run();
EXPECT_FALSE(ec);
EXPECT_EQ(std::future_status::ready, f.wait_for(std::chrono::seconds(0)));
}
TEST(RedirectError, PartialRedirectErrorExceptionPtr)
{
boost::asio::io_context io;
std::exception_ptr eptr = nullptr;
int count = 0;
boost::asio::co_spawn(
io, []() -> boost::asio::awaitable<void> {
throw std::exception{};
co_return;
},
ceph::async::redirect_error(eptr)(boost::asio::bind_executor(
io.get_executor(),
redirect_error_handler(&count))));
ASSERT_FALSE(eptr);
ASSERT_EQ(0, count);
io.run();
ASSERT_TRUE(eptr);
ASSERT_EQ(1, count);
boost::asio::co_spawn(
io, []() -> boost::asio::awaitable<void> {
co_return;
},
ceph::async::redirect_error(eptr)(boost::asio::bind_executor(
io.get_executor(),
redirect_error_handler(&count))));
ASSERT_TRUE(eptr);
ASSERT_EQ(1, count);
io.restart();
io.run();
ASSERT_FALSE(eptr);
ASSERT_EQ(2, count);
}