]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
common/async: Add `redirect_error.h`
authorAdam C. Emerson <aemerson@redhat.com>
Fri, 15 Aug 2025 01:37:07 +0000 (21:37 -0400)
committerAdam C. Emerson <aemerson@redhat.com>
Sun, 17 Aug 2025 16:16:54 +0000 (12:16 -0400)
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>
COPYING
src/common/async/redirect_error.h [new file with mode: 0644]
src/test/common/CMakeLists.txt
src/test/common/test_async_redirect_error.cc [new file with mode: 0644]

diff --git a/COPYING b/COPYING
index 3b758d5815511e9ee349098df8b53bed13cd50d5..52394f5bcb57bb5ae5bf80f047862cc89dd8b50a 100644 (file)
--- a/COPYING
+++ b/COPYING
@@ -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
diff --git a/src/common/async/redirect_error.h b/src/common/async/redirect_error.h
new file mode 100644 (file)
index 0000000..e55ee49
--- /dev/null
@@ -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
index f827ce9eff3f325eb6874f19ec678a2787f48e2c..7624abf2deeb623854721697d4ac762308613f33 100644 (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)
diff --git a/src/test/common/test_async_redirect_error.cc b/src/test/common/test_async_redirect_error.cc
new file mode 100644 (file)
index 0000000..82eef2d
--- /dev/null
@@ -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);
+}