Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 43 additions & 18 deletions src/v/ssx/future-util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -314,22 +301,60 @@ 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<typename Future>
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
/// places where success is optional. The caller will never see
/// gate_closed_exception or abort_requested_exception, avoiding log
/// noise on shutdown if the caller is logging exceptions.
template<typename Func>
inline auto spawn_with_gate_then(seastar::gate& g, Func&& func) noexcept {
return ignore_shutdown_exceptions(
seastar::try_with_gate(g, std::forward<Func>(func)));
inline seastar::future<>
spawn_with_gate_then(seastar::gate& g, Func&& func) noexcept {
Comment thread
StephanDollberg marked this conversation as resolved.
if (g.is_closed()) [[unlikely]] {
return seastar::make_ready_future<>();
}
auto gate_holder = g.hold();
return seastar::futurize_invoke(std::forward<Func>(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
Expand Down
13 changes: 13 additions & 0 deletions src/v/ssx/tests/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
97 changes: 97 additions & 0 deletions src/v/ssx/tests/future_util_bench.cc
Original file line number Diff line number Diff line change
@@ -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 <seastar/core/coroutine.hh>
#include <seastar/core/future.hh>
#include <seastar/testing/perf_tests.hh>

#include <stdexcept>

namespace {

constexpr size_t iterations = 10000;

template<typename Func>
ss::future<size_t> 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);
}