/* * Copyright 2022-present ScyllaDB */ /* * SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0 */ #pragma once #include #include #include "utils/result.hh" namespace utils { namespace internal { struct noop_converter { template using wrapped_type = T; template static wrapped_type wrap(T&& t) { return std::move(t); } template static wrapped_type> invoke(const F& f, Args&&... args) { return std::invoke(f, std::forward(args)...); } }; struct futurizing_converter { template using wrapped_type = seastar::futurize_t; template static wrapped_type wrap(T&& t) { if constexpr (seastar::is_future::value) { return t; } else { return seastar::make_ready_future(std::forward(t)); } } template static wrapped_type> invoke(const F& f, Args&&... args) { return seastar::futurize_invoke(f, std::forward(args)...); } }; template concept ConvertsWithTo = std::convertible_to, typename Converter::template wrapped_type>; // We require forall ExceptionHandle. // However, C++ does not support quantification like that in the constraints. // Here, we use the dummy_result to SCYLLA_ASSERT that the handles defined below // at least work with dummy_result. template using dummy_result = result_with_exception; // TODO: Should we make a virtual base class for this? // This would allow using non-generic lambdas in result_catch and result_catch_dots. template concept ExceptionHandle = ExceptionContainerResult> && requires (Handle handle, seastar::promise>& p) { { handle.into_result() } -> std::same_as>; { handle.into_future() } -> std::same_as>>; { handle.forward_to_promise(p) } -> std::same_as; }; template struct failed_result_handle { private: R&& _failed_result; public: failed_result_handle(R&& r) : _failed_result(std::move(r)) {} // Returns the failed result. Can be used only once as this moves out // the failed result. This should be called once during the handler's // lifetime, preferably as `return handler.into_result()`. R into_result() { return std::move(_failed_result); } seastar::future into_future() { return seastar::make_ready_future(std::move(_failed_result)); } template requires std::same_as void forward_to_promise(seastar::promise& p) { p.set_value(std::move(_failed_result).as_failure()); } const typename R::error_type& as_inner() const & { return _failed_result.assume_error(); } typename R::error_type&& as_inner() && { return std::move(_failed_result).assume_error(); } typename R::error_type clone_inner() { return _failed_result.assume_error().clone(); } }; static_assert(ExceptionHandle>>); template struct exception_ptr_handle { private: std::exception_ptr _eptr; public: exception_ptr_handle(std::exception_ptr&& eptr) : _eptr(std::move(eptr)) {} // Throws the exception. [[noreturn]] R into_result() { std::rethrow_exception(std::move(_eptr)); } seastar::future into_future() { return seastar::make_exception_future(std::move(_eptr)); } template requires std::same_as void forward_to_promise(seastar::promise& p) { p.set_exception(std::move(_eptr)); } const std::exception_ptr& as_inner() const & { return _eptr; } std::exception_ptr&& as_inner() && { return std::move(_eptr); } std::exception_ptr clone_inner() { return _eptr; } }; // Check that it is a valid handle with some arbitrary ExceptionContainerResult static_assert(ExceptionHandle>>); template struct result_catcher { private: Cb _cb; public: using exception_type = Ex; using callback_type = Cb; public: result_catcher(Cb&& cb) : _cb(std::move(cb)) {} template typename Converter::template wrapped_type handle_exception_from_result(const auto& ex, R& original_result, Continuation&& cont) { if constexpr (std::is_base_of_v>) { if constexpr (std::is_invocable_v>) { // Invoke with exception reference and handle return Converter::template invoke>(_cb, ex, failed_result_handle(std::move(original_result))); } else { // Simplified interface - invoke with reference to exception only static_assert(std::is_invocable_v, "The handler function does not have a suitable call operator"); return Converter::template invoke(_cb, ex); } } else { // Let another handler try it return std::invoke(std::forward(cont)); } } template auto wrap_in_catch(Continuation&& cont) { return [this, cont = std::forward(cont)] (bool& already_caught) mutable -> typename Converter::template wrapped_type { try { return std::invoke(std::forward(cont), already_caught); } catch (const Ex& ex) { if (already_caught) { throw; } already_caught = true; if constexpr (std::is_invocable_v>) { // Invoke with exception reference and handle return Converter::template invoke>(_cb, ex, exception_ptr_handle(std::current_exception())); } else { // Simplified interface - invoke with reference to exception only static_assert(std::is_invocable_v, "The handler function does not have a suitable call operator"); return Converter::template invoke(_cb, ex); } } }; } }; template struct is_result_catcher : std::false_type {}; template struct is_result_catcher> : std::true_type {}; template concept ResultCatcherCallback = requires (typename Handler::callback_type callback, typename Handler::exception_type& ex, failed_result_handle&& frh, exception_ptr_handle&& eph) { { callback(ex, frh) } -> ConvertsWithTo; { callback(ex, eph) } -> ConvertsWithTo; } || requires (typename Handler::callback_type callback, typename Handler::exception_type& ex) { { callback(ex) } -> ConvertsWithTo; }; template concept ResultCatcher = is_result_catcher::value && ResultCatcherCallback; template struct result_catcher_dots { private: Cb _cb; public: using callback_type = Cb; public: result_catcher_dots(Cb&& cb) : _cb(std::move(cb)) {} template typename Converter::template wrapped_type handle_exception_from_result(const auto& ex, R& original_result, Continuation&& cont) { if constexpr (std::is_invocable_v>) { // Invoke with handle return Converter::template invoke>(_cb, failed_result_handle(std::move(original_result))); } else { // Simplified interface - invoke without arguments static_assert(std::is_invocable_v, "The handler function does not have a suitable call operator"); return Converter::template invoke(_cb); } // Don't propagate to the next handler. The catch (...) is supposed // to match on all errors. } template auto wrap_in_catch(Continuation&& cont) { return [this, cont = std::forward(cont)] (bool& already_caught) mutable -> typename Converter::template wrapped_type { try { return std::invoke(std::forward(cont), already_caught); } catch (...) { if (already_caught) { throw; } already_caught = true; if constexpr (std::is_invocable_v>) { // Invoke with handle return Converter::template invoke>(_cb, exception_ptr_handle(std::current_exception())); } else { // Simplified interface - invoke without arguments static_assert(std::is_invocable_v, "The handler function does not have a suitable call operator"); return Converter::template invoke(_cb); } } }; } }; template struct is_result_catcher_dots : std::false_type {}; template struct is_result_catcher_dots> : std::true_type {}; template concept DotsResultCatcherCallback = requires (typename Handler::callback_type callback, failed_result_handle&& frh, exception_ptr_handle&& eph) { { callback(frh) } -> ConvertsWithTo; { callback(eph) } -> ConvertsWithTo; } || requires (typename Handler::callback_type callback) { { callback() } -> ConvertsWithTo; }; template concept DotsResultCatcher = is_result_catcher_dots::value && DotsResultCatcherCallback; // Constructs an `invoke_in_try_catch` function which allows to call a callback // and handle C++ exceptions using a set of handlers given during construction. // // The `Converter` is used to appropriately invoke the exception handlers. template struct try_catch_chain_impl {}; template struct try_catch_chain_impl { template static typename Converter::template wrapped_type invoke_in_try_catch(Continuation&& cont, FirstCatchHandler& first_handler, CatchHandlers&... catch_handlers) { return try_catch_chain_impl::template invoke_in_try_catch<>( first_handler.template wrap_in_catch(std::forward(cont)), catch_handlers...); } }; template struct try_catch_chain_impl { template static typename Converter::template wrapped_type invoke_in_try_catch(Continuation&& cont) { // Not using an invoker as we always want it to throw bool already_caught = false; return std::invoke(std::forward(cont), already_caught); } }; // Given a set of handlers, constructs a visitor which can be used to inspect // a failed result. // // The `Converter` is used to appropriately invoke the exception handlers // and convert the result to the return type if no handlers match on it. template struct combined_handler_impl {}; template struct combined_handler_impl : protected combined_handler_impl { private: using base = combined_handler_impl; FirstResultHandler& _first_handler; public: combined_handler_impl(FirstResultHandler& first_handler, ResultHandlers&... result_handlers) : base(result_handlers...) , _first_handler(first_handler) { } typename Converter::template wrapped_type handle(const auto& ex, R& original_result) { return _first_handler.template handle_exception_from_result( ex, original_result, [this, &ex, &original_result] () mutable { return base::handle(ex, original_result); }); } auto with_original_result(R& res) { return [this, &res] (const auto& ex) { return this->handle(ex, res); }; } }; template struct combined_handler_impl { public: typename Converter::template wrapped_type handle(const auto& ex, R& original_result) { // No more handlers to try out, just return return Converter::template wrap(std::move(original_result)); } auto with_original_result(R& res) { return [this, &res] (const auto& ex) { return handle(ex, res); }; } }; template [[gnu::always_inline]] inline seastar::future result_handle_available_future(seastar::future&& f, Handlers&... handlers) { using combined_handler_type = internal::combined_handler_impl; using try_catch_chain_type = internal::try_catch_chain_impl; if (!f.failed()) { Result&& res = f.get(); if (res) { return seastar::make_ready_future(std::move(res)); } // Handle the exception in result auto combined_handler = combined_handler_type(handlers...); return res.assume_error().accept(combined_handler.with_original_result(res)); } else { // The future has an exception // We need to create a try..catch chain from the handlers // and rethrow the exception inside it // // futurize_invoke will protect us in case the exception is not handled // by any of the provided handlers. return seastar::futurize_invoke([&] { return try_catch_chain_type::invoke_in_try_catch( [&f] (bool&) mutable -> seastar::future { std::rethrow_exception(std::move(f).get_exception()); }, handlers...); }); } } } template concept ResultCatcherMaybeDots = internal::ResultCatcher || internal::DotsResultCatcher; template concept ResultTryBody = requires (Fun fun) { { fun() } -> ExceptionContainerResult<>; } && std::is_nothrow_move_constructible_v>; template concept ResultFuturizeTryBody = requires (Fun&& fun) { { seastar::futurize_invoke(std::forward(fun)) } -> ExceptionContainerResultFuture<>; } && std::is_nothrow_move_constructible_v>::value_type>; /// \brief Allows to handle C++ exceptions and failed results in a unified way. /// /// When you modify a code path to return a result<> and you encounter /// a try..catch block, you can use it to migrate the block so that it handles /// both C++ exceptions and exceptions stored in the result<>. /// /// \section Example /// /// Let's say that you have the following try..catch chain: /// /// try { /// return a_function_that_may_throw(); /// } catch (const my_exception& ex) { /// return 123; /// } catch (...) { /// throw; /// } /// /// You can add support for results in the following way: /// /// return utils::result_try([&] { /// return a_function_that_may_throw_or_return_a_failed_result(); /// }, utils::result_catch([&] (const Ex&) -> result { /// return 123; /// }), utils::result_catch_dots([&] (auto&& handle) -> result { /// return handle.into_result(); /// }); /// /// Each `result_catch` handler declares the exception type it handles /// and accepts a const reference to it as the first argument. /// The `result_catch_dots` handler is equivalent to `(...)` so it does not /// declare exception type or accept a reference to it. /// /// In addition, each `result_catch` and `result_catch_dots` can optionally /// accept a reference to an exception handle. The exception handle can be used /// to return/rethrow the exception being currently handled. Depending on /// whether the handler is invoked for a C++ exception or a failed result<>, /// it will have a different type and its `into_result()` method will either /// rethrow the exception or return the failed result, respectively. /// /// \section Limitations /// /// The main limitation of result_try is that is is not as flexible with control /// flow as a try..catch block is. If you have some logic after the try..catch /// block and some code paths in it block return and some don't, you need to /// mark it somehow in the return value - for example, you can change result /// to result> and continue if it is std::nullopt, otherwise /// return. /// /// The return type of the first argument must be nothrow move constructible. /// Because this function uses many recursive calls to simulate a multi-catch /// clause try..catch block and NRVO is not in general guaranteed, there is /// a risk that some moves are not elided and one of them can throw /// in the middle of the stack. This exception can be thrown after we already /// exited the try..catch blocks for some handlers, so not all of them will /// participate in handling this exception. Because of the inability to properly /// handle exceptions from failed moves, hence the nothrow-move-constructibility /// requirement. /// /// This function does not work with futures and will trigger a static_assert /// if you use it with a future-returning function. See result_futurize_try /// for a version which additionally works with exceptional futures. template>... Handlers> requires (!seastar::is_future>::value) inline std::invoke_result_t result_try(Fun&& fun, Handlers&&... handlers) { using result_type = std::invoke_result_t; using combined_handler_type = internal::combined_handler_impl; using try_catch_chain_type = internal::try_catch_chain_impl; // Invoke `fun` and catch C++ exceptions if any occur result_type res = try_catch_chain_type::template invoke_in_try_catch<>([&fun] (bool&) { return fun(); }, handlers...); if (res) { return res; } // No C++ exceptions but the result is a failure - inspect using a visitor auto combined_handler = combined_handler_type(handlers...); return res.assume_error().accept(combined_handler.with_original_result(res)); } /// \brief A version of `result_try` which works with futures. It handles /// C++ exceptions, failed results and exceptional futures returned from `fun`. /// /// Migration from a try..catch block or f.handle_exception(...) is similar /// as in the case of `result_try`, with a small number of differences /// described below. /// /// The `fun` function and all exception handlers are futurize_invoked, /// therefore all C++ exceptions are converted to exceptional futures. /// /// In order to perform `throw`/`make_exception_future<>(std::current_exception())`, /// you should prefer `handle.into_future()` instead of `handle.into_result()`. /// Unlike the latter, the former avoids rethrowing the exception and just /// returns an exceptional future from a captured exception pointer. /// /// Important note about preemption: if the `try` body returns a failure /// without preemption, then the `catch` handler will be called without /// preemption, too. template>::value_type>... Handlers> seastar::futurize_t> result_futurize_try(Fun&& fun, Handlers&&... handlers) { using fun_return_type = std::invoke_result_t; using future_type = seastar::futurize_t; using result_type = typename future_type::value_type; using try_catch_chain_type = internal::try_catch_chain_impl; // Try to invoke the function and handle C++ exceptions if there are any bool no_exceptions = false; future_type f = seastar::futurize_invoke([&] () { return try_catch_chain_type::template invoke_in_try_catch<>([&no_exceptions, &fun] (bool&) { // If `fun` didn't throw, we can set the `no_exceptions` flag // so that we know that there was a throw and we shouldn't // run the exception handling logic again. fun_return_type ret = fun(); no_exceptions = true; return seastar::futurize::convert(std::move(ret)); }, handlers...); }); if (!no_exceptions) [[unlikely]] { // There were C++ exceptions, and we handled them already return f; } // Although then_wrapped should call the lambda without preemption, // this is not guaranteed in debug mode, and I feel that the compiler // struggles to generate efficient code for this case in release mode. // We manually check if the future is available so that we can ensure // the guarantee stated in the comment. if (f.available()) { return internal::result_handle_available_future(std::move(f), handlers...); } return f.then_wrapped([...handlers = std::move(handlers)] (future_type&& f) mutable -> future_type { return internal::result_handle_available_future(std::move(f), handlers...); }); } /// \brief Represents a `catch (const Ex& ex) {}` part in a `result_try` chain. /// /// The callback must be a generic lambda, and be callable with one /// of the following sets of arguments: /// /// (const Ex&) /// (const Ex&, Handle&&) /// /// where `Handle` is satisfies the ExceptionHandle concept /// and `R` is the result return type. /// /// See the description of `result_try` for more info. template auto result_catch(Cb&& cb) { return internal::result_catcher(std::move(cb)); } /// \brief Represents a `catch (...) {}` part in a `result_try` chain. /// /// The callback must be a generic lambda, and be callable with one /// of the following sets of arguments: /// /// () /// (Handle&&) /// /// where `Handle` is satisfies the ExceptionHandle concept /// and `R` is the result return type. /// /// See the description of `result_try` for more info. template auto result_catch_dots(Cb&& cb) { return internal::result_catcher_dots(std::move(cb)); } }