Skip to content

Commit 523eec5

Browse files
committed
core: sleep_abortable propagates custom abort exception when already aborted
sleep_abortable(dur, abort_source&) had two abort paths that disagreed on the failure exception. Aborting during the sleep propagated a custom exception set via request_abort_ex, but if the source was already aborted before sleep_abortable subscribed, subscribe() returned a disengaged optional and the fast path always fabricated a fresh sleep_aborted, discarding the custom exception. The resulting exception depended on a race with the subscribe call. Make the already-aborted fast path mirror the during-sleep path: propagate the caller-supplied exception when one was given, otherwise fall back to sleep_aborted. abort_source::get_default_exception() now returns a stable shared exception_ptr, which a plain request_abort() stores; sleep_abortable distinguishes a caller-supplied exception by comparing the stored abort exception against it by identity. A plain request_abort() still yields sleep_aborted as before. This relies on comparing exception_ptr identity, so abort_source is changed to return its default exception_ptr derived from a common thread_local initial value. Closes scylladb#3452
1 parent 58d9b63 commit 523eec5

4 files changed

Lines changed: 42 additions & 5 deletions

File tree

include/seastar/core/abort_source.hh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,15 @@ public:
213213

214214
/// Returns the default exception type (\ref abort_requested_exception) for this abort source.
215215
/// Overridable by derived classes.
216+
///
217+
/// The same exception_ptr object is returned on every call, so callers can
218+
/// compare an abort exception (\ref abort_requested_exception_ptr) against
219+
/// it by identity to tell whether an abort used the default exception or a
220+
/// caller-supplied one. Overrides should preserve this property by likewise
221+
/// returning a stable object.
216222
virtual std::exception_ptr get_default_exception() const noexcept {
217-
return make_exception_ptr(abort_requested_exception());
223+
static const thread_local std::exception_ptr ex = make_exception_ptr(abort_requested_exception());
224+
return ex;
218225
}
219226
};
220227

include/seastar/core/sleep.hh

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,16 @@ future<> sleep_abortable(typename Clock::duration dur);
7777
extern template future<> sleep_abortable<steady_clock_type>(typename steady_clock_type::duration);
7878
extern template future<> sleep_abortable<lowres_clock>(typename lowres_clock::duration);
7979

80-
/// Returns a future which completes after a specified time has elapsed
81-
/// or throws \ref sleep_aborted exception if the sleep is aborted.
80+
/// Returns a future which completes after a specified time has elapsed,
81+
/// or returns a failed future immediately if the sleep is aborted via \p as.
82+
///
83+
/// If the abort carried a custom exception (via \ref abort_source::request_abort_ex),
84+
/// the returned future is failed with that exception; otherwise it is failed
85+
/// with \ref sleep_aborted.
8286
///
8387
/// \param dur minimum amount of time before the returned future becomes
8488
/// ready.
85-
/// \param as the \ref abort_source that eventually notifies that the sleep
89+
/// \param as the \ref abort_source that can be used to notify that the sleep
8690
/// should be aborted.
8791
/// \return A \ref future which becomes ready when the sleep duration elapses.
8892
template <typename Clock = steady_clock_type>

src/core/future-util.cc

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,16 @@ future<> sleep_abortable(typename Clock::duration dur, abort_source& as) {
111111
sc = std::move(*sc_opt);
112112
tmr.arm(dur);
113113
} else {
114-
done.set_exception(sleep_aborted());
114+
// Try to preserve a custom exception if one was provided, relying on
115+
// comparing the exception pointers. This relies on get_default_exception
116+
// always providing a pointer to the same exception object.
117+
// See scylladb/seastar#3452.
118+
const auto& ex = as.abort_requested_exception_ptr();
119+
if (ex != as.get_default_exception()) {
120+
done.set_exception(ex);
121+
} else {
122+
done.set_exception(sleep_aborted());
123+
}
115124
}
116125
}
117126
};

tests/unit/abort_source_test.cc

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,23 @@ SEASTAR_THREAD_TEST_CASE(test_sleep_abortable_already_aborted_with_exception) {
201201
BOOST_REQUIRE(caught_exception);
202202
}
203203

204+
// Companion to the above: when the already-aborted source carries no custom
205+
// exception (plain request_abort), sleep_abortable still fails with
206+
// sleep_aborted, matching the during-sleep path. See scylladb/seastar#3452.
207+
SEASTAR_THREAD_TEST_CASE(test_sleep_abortable_already_aborted_no_exception) {
208+
abort_source as;
209+
as.request_abort();
210+
auto f = sleep_abortable(10s, as);
211+
212+
bool caught_exception = false;
213+
try {
214+
f.get();
215+
} catch (const sleep_aborted&) {
216+
caught_exception = true;
217+
}
218+
BOOST_REQUIRE(caught_exception);
219+
}
220+
204221
SEASTAR_THREAD_TEST_CASE(test_destroy_with_moved_subscriptions) {
205222
auto as = std::make_unique<abort_source>();
206223
int aborted = 0;

0 commit comments

Comments
 (0)