diff --git a/src/v/ssx/future-util.h b/src/v/ssx/future-util.h index 3b693fd236545..140c9e17e90f2 100644 --- a/src/v/ssx/future-util.h +++ b/src/v/ssx/future-util.h @@ -276,19 +276,6 @@ struct background_t { } // namespace detail inline constexpr detail::background_t background; -/// \brief Create a new future, handling common shutdown exception types. -inline seastar::future<> -ignore_shutdown_exceptions(seastar::future<> fut) noexcept { - try { - co_await std::move(fut); - } catch (const seastar::abort_requested_exception&) { - } catch (const seastar::gate_closed_exception&) { - } catch (const seastar::broken_semaphore&) { - } catch (const seastar::broken_promise&) { - } catch (const seastar::broken_condition_variable&) { - } -} - /// \brief Check if the exception is a commonly ignored shutdown exception. /// /// Also checks inside seastar::nested_exception for shutdown exceptions @@ -314,12 +301,43 @@ inline bool is_shutdown_exception(const std::exception_ptr& e) { return false; } +namespace detail { +inline seastar::future<> filter_shutdown_exception(std::exception_ptr ep) { + if (is_shutdown_exception(ep)) { + return seastar::make_ready_future<>(); + } + return seastar::make_exception_future<>(std::move(ep)); +} + +template +inline seastar::future<> filter_shutdown_exceptions(Future fut) { + if (fut.failed()) { + return filter_shutdown_exception(fut.get_exception()); + } + return seastar::make_ready_future<>(); +} +} // namespace detail + +/// \brief Create a new future, handling common shutdown exception types. +/// +/// On the fast path (the input future is already available and succeeded) +/// the future is returned directly without any continuation. +inline seastar::future<> +ignore_shutdown_exceptions(seastar::future<> fut) noexcept { + if (fut.available() && !fut.failed()) { + return fut; + } + return std::move(fut).handle_exception([](std::exception_ptr ep) { + return detail::filter_shutdown_exception(std::move(ep)); + }); +} + /// \brief Create a future holding a gate, handling common shutdown exception /// types. Returns the resulting future, onto which further exception handling /// may be chained. /// -/// \param g Gate to enter, passed through to ss::try_with_gate -/// \param func Function to invoke, passed through to ss::try_with_gate +/// \param g Gate to hold while invoking func +/// \param func Function to invoke while the gate is held /// /// This is an alternative to spawn_with_gate for when the caller wants to /// do extra exception handling, such as ignoring+logging all exceptions in @@ -327,9 +345,16 @@ inline bool is_shutdown_exception(const std::exception_ptr& e) { /// gate_closed_exception or abort_requested_exception, avoiding log /// noise on shutdown if the caller is logging exceptions. template -inline auto spawn_with_gate_then(seastar::gate& g, Func&& func) noexcept { - return ignore_shutdown_exceptions( - seastar::try_with_gate(g, std::forward(func))); +inline seastar::future<> +spawn_with_gate_then(seastar::gate& g, Func&& func) noexcept { + if (g.is_closed()) [[unlikely]] { + return seastar::make_ready_future<>(); + } + auto gate_holder = g.hold(); + return seastar::futurize_invoke(std::forward(func)) + .then_wrapped([gate_holder = std::move(gate_holder)](auto fut) mutable { + return detail::filter_shutdown_exceptions(std::move(fut)); + }); } /// \brief Detach a fiber holding a gate, with exception handling to ignore diff --git a/src/v/ssx/tests/BUILD b/src/v/ssx/tests/BUILD index 4407384a5b4ca..6f2d2bd5b9701 100644 --- a/src/v/ssx/tests/BUILD +++ b/src/v/ssx/tests/BUILD @@ -278,6 +278,19 @@ redpanda_cc_bench( ], ) +redpanda_cc_bench( + name = "future_util_rpbench", + srcs = [ + "future_util_bench.cc", + ], + deps = [ + "//src/v/base", + "//src/v/ssx:future_util", + "@seastar", + "@seastar//:benchmark", + ], +) + redpanda_cc_bench( name = "async_algorithm_rpbench", srcs = [ diff --git a/src/v/ssx/tests/future_util_bench.cc b/src/v/ssx/tests/future_util_bench.cc new file mode 100644 index 0000000000000..94cd62ac79c0a --- /dev/null +++ b/src/v/ssx/tests/future_util_bench.cc @@ -0,0 +1,97 @@ +// Copyright 2026 Redpanda Data, Inc. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.md +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0 + +#include "base/seastarx.h" +#include "ssx/future-util.h" + +#include +#include +#include + +#include + +namespace { + +constexpr size_t iterations = 10000; + +template +ss::future co_await_in_loop(Func func) { + perf_tests::start_measuring_time(); + for (size_t i = 0; i < iterations; ++i) { + co_await func(); + } + perf_tests::stop_measuring_time(); + co_return iterations; +} + +// Input producers for ignore_shutdown_exceptions. +// +// Keep these out of line to preserve the producer call in each measured +// iteration. Already-resolved futures are the common path being measured. + +// Available, successful future. +[[gnu::noinline]] ss::future<> input_ready() { return ss::now(); } + +// Available future failed with a gate_closed_exception (a shutdown exception). +[[gnu::noinline]] ss::future<> input_gate_closed() { + return ss::make_exception_future<>(seastar::gate_closed_exception{}); +} + +// Available future failed with an abort_requested_exception (a shutdown +// exception). +[[gnu::noinline]] ss::future<> input_abort_requested() { + return ss::make_exception_future<>(seastar::abort_requested_exception{}); +} + +// Available future failed with a std::runtime_error: a non-shutdown exception +// that ignore_shutdown_exceptions must rethrow to the caller. +[[gnu::noinline]] ss::future<> input_runtime_error() { + return ss::make_exception_future<>(std::runtime_error("not shutdown")); +} + +struct future_util_bench {}; + +ss::future<> ignore_shutdown_ready() { + return ssx::ignore_shutdown_exceptions(input_ready()); +} + +ss::future<> ignore_shutdown_gate_closed() { + return ssx::ignore_shutdown_exceptions(input_gate_closed()); +} + +ss::future<> ignore_shutdown_abort_requested() { + return ssx::ignore_shutdown_exceptions(input_abort_requested()); +} + +// ignore_shutdown_exceptions rethrows non-shutdown exceptions, so swallow the +// result to keep the loop running. The trailing handle_exception is common-mode +// overhead (present for any caller that handles the rethrow), so the delta +// between builds still isolates the filter/rethrow cost. +ss::future<> ignore_shutdown_non_shutdown() { + return ssx::ignore_shutdown_exceptions(input_runtime_error()) + .handle_exception([](std::exception_ptr) {}); +} + +} // namespace + +PERF_TEST_F(future_util_bench, ignore_shutdown_ready) { + return co_await_in_loop(ignore_shutdown_ready); +} + +PERF_TEST_F(future_util_bench, ignore_shutdown_gate_closed) { + return co_await_in_loop(ignore_shutdown_gate_closed); +} + +PERF_TEST_F(future_util_bench, ignore_shutdown_abort_requested) { + return co_await_in_loop(ignore_shutdown_abort_requested); +} + +PERF_TEST_F(future_util_bench, ignore_shutdown_non_shutdown) { + return co_await_in_loop(ignore_shutdown_non_shutdown); +}