]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: Add run_coro utility
authorAdam C. Emerson <aemerson@redhat.com>
Fri, 18 Apr 2025 07:27:36 +0000 (03:27 -0400)
committerAdam C. Emerson <aemerson@redhat.com>
Wed, 6 Aug 2025 20:03:04 +0000 (16:03 -0400)
A convenience function for turning coroutines that return values and
use exceptions, `error_code`, or similar into `int`-returning
functions that take references to out parameters.

Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
src/common/async/concepts.h
src/common/async/librados_completion.h
src/common/error_code.h
src/rgw/async_utils.h [new file with mode: 0644]

index ad960b6ab8d318cd35fa4a27052bc2030c8cfb18..3be2a9e13a3cb1847fe5d5cd3a0aaaa8a6eb4e61 100644 (file)
@@ -17,6 +17,8 @@
 
 #include <type_traits>
 
+#include <boost/asio/execution/executor.hpp>
+
 #include <boost/asio/disposition.hpp>
 #include <boost/asio/execution_context.hpp>
 
@@ -32,7 +34,6 @@ template<typename ExecutionContext>
 concept execution_context =
   std::is_convertible_v<ExecutionContext&,
                         boost::asio::execution_context&>;
-
 /// A concept for Asio 'disposition's, a generalization of error
 /// codes/exception pointers, etc.
 template<typename T>
index 7bb380895e80b80aa3c4f57772a674a85d1766d1..450759f8cd4012cad07241144b28f9b53267efdb 100644 (file)
@@ -97,7 +97,8 @@ struct librados_handler {
   }
 
   void operator ()(std::exception_ptr e) {
-    (*this)(ceph::from_exception(e));
+    std::string what;
+    (*this)(ceph::from_exception(e, &what));
   }
 };
 } // namespace detail
index f37d7ce617df4b029768480b5c1b091de3911e7c..8ad3eef8d203a07183588b9c6bba88dcd2d574a0 100644 (file)
@@ -94,33 +94,57 @@ inline boost::system::error_condition make_error_condition(errc e) noexcept {
 #pragma GCC diagnostic pop
 #pragma clang diagnostic pop
 
-[[nodiscard]] inline int from_exception(std::exception_ptr eptr) {
+[[nodiscard]]
+inline int from_exception(std::exception_ptr eptr, std::string* what = nullptr)
+{
   if (!eptr) [[likely]] {
     return 0;
   }
   try {
     std::rethrow_exception(eptr);
   } catch (const boost::system::system_error& e) {
+    if (what)
+      *what = e.what();
     return from_error_code(e.code());
   } catch (const std::system_error& e) {
+    if (what)
+      *what = e.what();
     return from_error_code(e.code());
-  } catch (const std::invalid_argument&) {
+  } catch (const std::invalid_argument& e) {
+    if (what)
+      *what = e.what();
     return -EINVAL;
-  } catch (const std::domain_error&) {
+  } catch (const std::domain_error& e) {
+    if (what)
+      *what = e.what();
     return -EDOM;
-  } catch (const std::length_error&) {
+  } catch (const std::length_error& e) {
+    if (what)
+      *what = e.what();
     return -ERANGE;
-  } catch (const std::out_of_range&) {
+  } catch (const std::out_of_range& e) {
+    if (what)
+      *what = e.what();
     return -ERANGE;
-  } catch (const std::range_error&) {
+  } catch (const std::range_error& e) {
+    if (what)
+      *what = e.what();
     return -ERANGE;
-  } catch (const std::overflow_error&) {
+  } catch (const std::overflow_error& e) {
+    if (what)
+      *what = e.what();
     return -EOVERFLOW;
-  } catch (const std::underflow_error&) {
+  } catch (const std::underflow_error& e) {
+    if (what)
+      *what = e.what();
     return -EOVERFLOW;
-  } catch (const std::bad_alloc&) {
+  } catch (const std::bad_alloc& e) {
+    if (what)
+      *what = e.what();
     return -ENOMEM;
   } catch (const std::regex_error& e) {
+    if (what)
+      *what = e.what();
     using namespace std::regex_constants;
     switch (e.code()) {
     case error_space:
@@ -134,28 +158,53 @@ inline boost::system::error_condition make_error_condition(errc e) noexcept {
 #ifdef __has_include
 #  if __has_include(<format>)
   } catch (const std::format_error& e) {
+    if (what)
+      *what = e.what();
     return -EINVAL;
 #  endif
 #endif
   } catch (const fmt::format_error& e) {
+    if (what)
+      *what = e.what();
     return -EINVAL;
-  } catch (const std::bad_typeid&) {
+  } catch (const std::bad_typeid& e) {
+    if (what)
+      *what = e.what();
     return -EFAULT;
-  } catch (const std::bad_cast&) {
+  } catch (const std::bad_cast& e) {
+    if (what)
+      *what = e.what();
     return -EFAULT;
-  } catch (const std::bad_optional_access&) {
+  } catch (const std::bad_optional_access& e) {
+    if (what)
+      *what = e.what();
     return -EFAULT;
-  } catch (const std::bad_weak_ptr&) {
+  } catch (const std::bad_weak_ptr& e) {
+    if (what)
+      *what = e.what();
     return -EFAULT;
-  } catch (const std::bad_function_call&) {
+  } catch (const std::bad_function_call& e) {
+    if (what)
+      *what = e.what();
     return -EFAULT;
-  } catch (const std::bad_exception&) {
+  } catch (const std::bad_exception& e) {
+    if (what)
+      *what = e.what();
     return -EFAULT;
-  } catch (const std::bad_variant_access&) {
+  } catch (const std::bad_variant_access& e) {
+    if (what)
+      *what = e.what();
     return -EFAULT;
+  } catch (const std::exception& e) {
+    if (what)
+      *what = e.what();
+    return -EIO;
   } catch (...) {
+    if (what)
+      *what = "Unknown exception";
     return -EIO;
   }
+  return -EIO;
 }
 }
 
diff --git a/src/rgw/async_utils.h b/src/rgw/async_utils.h
new file mode 100644 (file)
index 0000000..5bc7de2
--- /dev/null
@@ -0,0 +1,359 @@
+// -*- 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 contributors to the Ceph project
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+ */
+
+#pragma once
+
+#include <exception>
+#include <string>
+#include <string_view>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+
+#include <boost/asio/awaitable.hpp>
+#include <boost/asio/execution/executor.hpp>
+#include <boost/asio/co_spawn.hpp>
+
+#include "common/async/blocked_completion.h"
+#include "common/async/concepts.h"
+#include "common/async/yield_context.h"
+
+#include "common/dout.h"
+#include "common/dout_fmt.h"
+#include "common/error_code.h"
+
+#include "rgw/rgw_asio_thread.h"
+
+/// \file rgw/async_utils
+///
+/// \brief Utilities for asynchrony
+
+namespace rgw {
+
+namespace asio = boost::asio;
+namespace async = ceph::async;
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin
+template<asio::execution::executor Executor,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<Executor, AwaitableExecutor>
+inline int run_coro(
+  const DoutPrefixProvider* dpp, //< In case we're blocking
+  Executor executor, ///< Executor on which to run the coroutine
+  asio::awaitable<void, AwaitableExecutor> coro, ///< The coroutine itself
+  std::string* what ///< Where to store the result of `what()` on exception
+  ) noexcept
+{
+  try {
+    maybe_warn_about_blocking(dpp);
+    asio::co_spawn(executor, std::move(coro), async::use_blocked);
+  } catch (const std::exception&) {
+    return ceph::from_exception(std::current_exception(), what);
+  }
+  return 0;
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin
+template<async::execution_context ExecutionContext,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<typename ExecutionContext::executor_type,
+                              AwaitableExecutor>
+inline int run_coro(
+  const DoutPrefixProvider* dpp, //< In case we're blocking
+  ExecutionContext& execution_context, ///< Execution context on which to run
+                                      ///  the coroutine
+  asio::awaitable<void, AwaitableExecutor> coro, ///< The coroutine itself
+  std::string* what ///< Where to store the result of `what()` on exception
+  ) noexcept
+{
+  return run_coro(dpp, execution_context.get_executor(), std::move(coro), what);
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string. This overload supports coroutines that return something
+/// other than void.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin.
+template<asio::execution::executor Executor,
+        typename T,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<Executor, AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, //< In case we're blocking
+  Executor executor, ///< Executor on which to run the coroutine
+  asio::awaitable<T, AwaitableExecutor> coro, ///< The coroutine itself
+  T& val, ///< Where to store the returned value
+  std::string* what ///< Where to store the result of `what()`.
+  ) noexcept
+{
+  try {
+    val = asio::co_spawn(executor, std::move(coro), async::use_blocked);
+    maybe_warn_about_blocking(dpp);
+  } catch (const std::exception& e) {
+    return ceph::from_exception(std::current_exception(), what);
+  }
+  return 0;
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string. This overload supports coroutines that return something
+/// other than void.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin.
+template<async::execution_context ExecutionContext,
+        typename T,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<typename ExecutionContext::executor_type,
+                              AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, //< In case we're blocking
+  ExecutionContext& execution_context, ///< Execution context on which to run the coroutine
+  asio::awaitable<T, AwaitableExecutor> coro, ///< The coroutine itself
+  T& val, ///< Where to store the returned value
+  std::string* what ///< Where to store the result of `what()`.
+  ) noexcept
+{
+  return run_coro(dpp, execution_context.get_executor(), std::move(coro), val, what);
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string. This overload supports coroutines that return multiple
+/// values as a tuple.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin.
+template<asio::execution::executor Executor,
+        asio::execution::executor AwaitableExecutor,
+        typename ...Ts>
+requires std::is_convertible_v<Executor, AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, //< In case we're blocking
+  Executor executor, ///< Executor on which to run the coroutine
+  asio::awaitable<std::tuple<Ts...>, AwaitableExecutor> coro, ///< The coroutine itself
+  std::tuple<Ts&...>&& vals, ///< Supply with std::tie
+  std::string* what ///< Where to store the result of `what()`.
+  ) noexcept
+{
+  try {
+    maybe_warn_about_blocking(dpp);
+    vals = asio::co_spawn(executor, std::move(coro), async::use_blocked);
+  } catch (const std::exception& e) {
+    return ceph::from_exception(std::current_exception(), what);
+  }
+  return 0;
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string. This overload supports coroutines that return something
+/// other than void.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin.
+template<async::execution_context ExecutionContext,
+        typename... Ts,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<typename ExecutionContext::executor_type,
+                              AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, //< In case we're blocking
+  ExecutionContext& execution_context, ///< Execution context on which to run the coroutine
+  asio::awaitable<std::tuple<Ts...>, AwaitableExecutor> coro, ///< The coroutine itself
+  std::tuple<Ts&...>&& vals, ///< Supply with std::tie
+  std::string* what ///< Where to store the result of `what()`.
+  ) noexcept
+{
+  return run_coro(dpp, execution_context.get_executor(), std::move(coro),
+                 std::move(vals), what);
+}
+
+/// Call a C++ coroutine from a stacful coroutine if we can, but block
+/// if we get `null_yield.` handling exceptions by returning negative
+/// error codes and passing out the `what()` string.
+template<asio::execution::executor Executor,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<Executor, AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, /// For logging
+  const Executor& executor, ///< Executor on which to run the coroutine
+  asio::awaitable<void, AwaitableExecutor> coro, ///< The coroutine itself
+  std::string_view name, ///< Name, for logging errors
+  optional_yield y, /// Stackful coroutine context…hopefully
+  int log_level = 5 /// What level to log at
+  ) noexcept
+{
+  try {
+    if (y) {
+      auto& yield = y.get_yield_context();
+      asio::co_spawn(yield.get_executor(), std::move(coro), yield);
+    } else {
+      maybe_warn_about_blocking(dpp);
+      asio::co_spawn(executor, std::move(coro), async::use_blocked);
+    }
+  } catch (const std::exception& e) {
+    ldpp_dout_fmt(dpp, log_level, "{}: failed: {}", name, e.what());
+    return ceph::from_exception(std::current_exception());
+  }
+  return 0;
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin
+template<async::execution_context ExecutionContext,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<typename ExecutionContext::executor_type,
+                              AwaitableExecutor>
+inline int run_coro(
+  const DoutPrefixProvider* dpp, /// For logging
+  ExecutionContext& execution_context, ///< Execution context on which to run
+                                      ///  the coroutine
+  asio::awaitable<void, AwaitableExecutor> coro, ///< The coroutine itself
+  std::string_view name, ///< Name, for logging errors
+  optional_yield y, /// Stackful coroutine context…hopefully
+  int log_level = 5 /// What level to log at
+  ) noexcept
+{
+  return run_coro(dpp, execution_context.get_executor(), std::move(coro), name, y,
+                 log_level);
+}
+
+
+/// Call a C++ coroutine from a stackful coroutine if we can, but block
+/// if we get `null_yield.` handling exceptions by returning negative
+/// error codes and passing out the `what()` string. This overload
+/// supports coroutines that return something other than void.
+template<asio::execution::executor Executor,
+        typename T,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<Executor, AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, /// For logging
+  Executor executor, ///< Executor on which to run the coroutine
+  asio::awaitable<T, AwaitableExecutor> coro, ///< The coroutine itself
+  T& val, ///< Where to store the returned value
+  std::string_view name, ///< Name, for logging errors
+  optional_yield y, /// Stackful coroutine context…hopefully
+  int log_level = 5 /// What level to log at
+  ) noexcept
+{
+  try {
+    if (y) {
+      auto& yield = y.get_yield_context();
+      val = asio::co_spawn(yield.get_executor(), std::move(coro), yield);
+    } else {
+      maybe_warn_about_blocking(dpp);
+      val = asio::co_spawn(executor, std::move(coro), async::use_blocked);
+    }
+  } catch (const std::exception& e) {
+    ldpp_dout_fmt(dpp, log_level, "{}: failed: {}", name, e.what());
+    return ceph::from_exception(std::current_exception());
+  }
+
+  return 0;
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string. This overload supports coroutines that return something
+/// other than void.
+template<async::execution_context ExecutionContext,
+        typename T,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<typename ExecutionContext::executor_type,
+                              AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, /// For logging
+  ExecutionContext& execution_context, ///< Execution context on which to run the coroutine
+  asio::awaitable<T, AwaitableExecutor> coro, ///< The coroutine itself
+  T& val, ///< Where to store the returned value
+  std::string_view name, ///< Name, for logging errors
+  optional_yield y, /// Stackful coroutine context…hopefully
+  int log_level = 5 /// What level to log at
+  ) noexcept
+{
+  return run_coro(dpp, execution_context.get_executor(), std::move(coro), val, name,
+                 y, log_level);
+}
+
+/// Call a C++ coroutine from a stackful coroutine if we can, but block
+/// if we get `null_yield.` handling exceptions by returning negative
+/// error codes and passing out the `what()` string. This overload
+/// supports coroutines that return multiple values with a tuple.
+template<asio::execution::executor Executor,
+        asio::execution::executor AwaitableExecutor,
+        typename ...Ts>
+requires std::is_convertible_v<Executor, AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, /// For logging
+  Executor executor, ///< Executor on which to run the coroutine
+  asio::awaitable<std::tuple<Ts...>, AwaitableExecutor> coro, ///< The coroutine itself
+  std::tuple<Ts&...>&& vals, ///< Supply with std::tie
+  std::string_view name, ///< Name, for logging errors
+  optional_yield y, /// Stackful coroutine context…hopefully
+  int log_level = 5 /// What level to log at
+  ) noexcept
+{
+  try {
+    if (y) {
+      auto& yield = y.get_yield_context();
+      vals = asio::co_spawn(yield.get_executor(), std::move(coro), yield);
+    } else {
+      maybe_warn_about_blocking(dpp);
+      vals = asio::co_spawn(executor, std::move(coro), async::use_blocked);
+    }
+  } catch (const std::exception& e) {
+    ldpp_dout_fmt(dpp, log_level, "{}: failed: {}", name, e.what());
+    return ceph::from_exception(std::current_exception());
+  }
+  return 0;
+}
+
+/// Call a coroutine and block until it completes, handling exceptions
+/// by returning negative error codes and passing out the `what()`
+/// string. This overload supports coroutines that return something
+/// other than void.
+///
+/// Intended for use in interactive front-ends, e.g. radosgw-admin.
+template<async::execution_context ExecutionContext,
+        typename... Ts,
+        asio::execution::executor AwaitableExecutor>
+requires std::is_convertible_v<typename ExecutionContext::executor_type,
+                              AwaitableExecutor>
+int run_coro(
+  const DoutPrefixProvider* dpp, /// For logging
+  ExecutionContext& execution_context, ///< Execution context on which to run the coroutine
+  asio::awaitable<std::tuple<Ts...>, AwaitableExecutor> coro, ///< The coroutine itself
+  std::tuple<Ts&...>&& vals, ///< Supply with std::tie
+  std::string_view name, ///< Name, for logging errors
+  optional_yield y, /// Stackful coroutine context…hopefully
+  int log_level = 5 /// What level to log at
+  ) noexcept
+{
+  return run_coro(dpp, execution_context.get_executor(), std::move(coro),
+                 std::move(vals), name, y, log_level);
+}
+}