utils/exceptions: add try_catch

Introduces a utility function which allows obtaining a pointer to the
exception data held behind an std::exception_ptr if the data matches the
requested type. It can be used to implement manual but concise
try..catch chains.

The `try_catch` has the best performance when used with libstdc++ as it
uses the stdlib specific functions for simulating a try..catch without
having to actually throw. For other stdlibs, the implementation falls
back to a throw surrounded by an actual try..catch.
This commit is contained in:
Piotr Dulikowski
2022-04-13 10:33:19 +02:00
parent c1ac116eb9
commit 18f43fa00e
6 changed files with 253 additions and 0 deletions

View File

@@ -508,6 +508,8 @@ scylla_tests = set([
'test/boost/rate_limiter_test',
'test/boost/per_partition_rate_limit_test',
'test/boost/expr_test',
'test/boost/exceptions_optimized_test',
'test/boost/exceptions_fallback_test',
'test/manual/ec2_snitch_test',
'test/manual/enormous_table_scan_test',
'test/manual/gce_snitch_test',
@@ -1290,6 +1292,8 @@ deps['test/boost/linearizing_input_stream_test'] = [
]
deps['test/boost/expr_test'] = ['test/boost/expr_test.cc'] + scylla_core
deps['test/boost/rate_limiter_test'] = ['test/boost/rate_limiter_test.cc', 'db/rate_limiter.cc']
deps['test/boost/exceptions_optimized_test'] = ['test/boost/exceptions_optimized_test.cc', 'utils/exceptions.cc']
deps['test/boost/exceptions_fallback_test'] = ['test/boost/exceptions_fallback_test.cc', 'utils/exceptions.cc']
deps['test/boost/duration_test'] += ['test/lib/exception_utils.cc']
deps['test/boost/schema_loader_test'] += ['tools/schema_loader.cc']

View File

@@ -0,0 +1,11 @@
/*
* Copyright (C) 2022-present ScyllaDB
*/
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#define NO_OPTIMIZED_EXCEPTION_HANDLING
#include "exceptions_test.inc.cc"

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2022-present ScyllaDB
*/
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
#if defined(NO_OPTIMIZED_EXCEPTION_HANDLING)
#undef NO_OPTIMIZED_EXCEPTION_HANDLING
#endif
#include "utils/exceptions.hh"
#if defined(OPTIMIZED_EXCEPTION_HANDLING_AVAILABLE)
#include "exceptions_test.inc.cc"
#else
#include <boost/test/unit_test_log.hpp>
#include <seastar/testing/test_case.hh>
SEASTAR_TEST_CASE(test_noop) {
BOOST_TEST_MESSAGE("Optimized implementation of handling exceptions "
"without throwing is not available. Skipping tests in this file.");
return seastar::make_ready_future<>();
}
#endif

View File

@@ -0,0 +1,137 @@
/*
* Copyright (C) 2022-present ScyllaDB
*/
/*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
// Common definitions of test cases used in
// handle_exception_optimized_test.cc
// handle_exception_fallback_test.cc
#include <exception>
#include <stdexcept>
#include <type_traits>
#include <cxxabi.h>
#include <boost/test/unit_test_log.hpp>
#include <boost/test/tools/old/interface.hpp>
#include <boost/test/unit_test.hpp>
#include "seastarx.hh"
#include <seastar/core/future.hh>
#include <seastar/testing/test_case.hh>
#include <seastar/util/log.hh>
#include "utils/exceptions.hh"
class base_exception : public std::exception {};
class derived_exception : public base_exception {};
static void dummy_fn(void*) {
//
}
template<typename T>
static std::exception_ptr maybe_wrap_eptr(T&& t) {
if constexpr (std::is_same_v<T, std::exception_ptr>) {
return std::move(t);
} else {
return std::make_exception_ptr(std::move(t));
}
}
static const std::type_info& eptr_typeid(std::exception_ptr eptr) {
try {
std::rethrow_exception(eptr);
} catch (...) {
return *abi::__cxa_current_exception_type();
}
}
template<typename Capture, typename Throw>
static void check_catch(Throw&& ex) {
auto eptr = maybe_wrap_eptr(std::move(ex));
BOOST_TEST_MESSAGE("Checking if " << seastar::pretty_type_name(eptr_typeid(eptr))
<< " is caught as " << seastar::pretty_type_name(typeid(Capture)));
auto typed_eptr = try_catch<Capture>(eptr);
BOOST_CHECK_NE(typed_eptr, nullptr);
// Verify that it's the same as what the usual throwing gives
// TODO: Does this check make sense? Does the standard guarantee
// that this will give the same pointer?
try {
std::rethrow_exception(eptr);
} catch (Capture& t) {
BOOST_CHECK_EQUAL(typed_eptr, &t);
} catch (...) {
// Can happen if the first check fails, just skip
assert(typed_eptr == nullptr);
}
}
template<typename Capture, typename Throw>
static void check_no_catch(Throw&& ex) {
auto eptr = maybe_wrap_eptr(std::move(ex));
BOOST_TEST_MESSAGE("Checking if " << seastar::pretty_type_name(eptr_typeid(eptr))
<< " is NOT caught as " << seastar::pretty_type_name(typeid(Capture)));
auto typed_eptr = try_catch<Capture>(eptr);
BOOST_CHECK_EQUAL(typed_eptr, nullptr);
}
template<typename A, typename B>
static std::exception_ptr make_nested_eptr(A&& a, B&& b) {
try {
throw std::move(b);
} catch (...) {
try {
std::throw_with_nested(std::move(a));
} catch (...) {
return std::current_exception();
}
}
}
SEASTAR_TEST_CASE(test_try_catch) {
// Some standard examples, throwing exceptions derived from std::exception
// and catching them through their base types
check_catch<derived_exception>(derived_exception());
check_catch<base_exception>(derived_exception());
check_catch<std::exception>(derived_exception());
check_no_catch<std::runtime_error>(derived_exception());
check_no_catch<derived_exception>(base_exception());
check_catch<base_exception>(base_exception());
check_catch<std::exception>(base_exception());
check_no_catch<std::runtime_error>(base_exception());
// Catching nested exceptions
check_catch<base_exception>(make_nested_eptr(base_exception(), derived_exception()));
check_catch<std::nested_exception>(make_nested_eptr(base_exception(), derived_exception()));
check_no_catch<derived_exception>(make_nested_eptr(base_exception(), derived_exception()));
// Check that everything works if we throw some crazy stuff
check_catch<int>(int(1));
check_no_catch<std::exception>(int(1));
check_no_catch<int>(nullptr);
check_no_catch<std::exception>(nullptr);
// Catching pointers is not supported, but nothing should break if they are
// being thrown
derived_exception exc;
check_no_catch<int>(&exc);
check_no_catch<std::exception>(&exc);
check_no_catch<int>(&dummy_fn);
check_no_catch<std::exception>(&dummy_fn);
check_no_catch<int>(&std::exception::what);
check_no_catch<std::exception>(&std::exception::what);
return make_ready_future<>();
}

View File

@@ -15,6 +15,7 @@
#include <system_error>
#include <atomic>
#include "exceptions.hh"
#include "utils/abi/eh_ia64.hh"
#include <iostream>
@@ -73,3 +74,25 @@ bool is_timeout_exception(std::exception_ptr e) {
}
return false;
}
#if defined(OPTIMIZED_EXCEPTION_HANDLING_AVAILABLE)
#include <typeinfo>
#include "utils/abi/eh_ia64.hh"
void* utils::internal::try_catch_dynamic(std::exception_ptr& eptr, const std::type_info* catch_type) noexcept {
// In both libstdc++ and libc++, exception_ptr has just one field
// which is a pointer to the exception data
void* raw_ptr = reinterpret_cast<void*&>(eptr);
const std::type_info* ex_type = utils::abi::get_cxa_exception(raw_ptr)->exceptionType;
// __do_catch can return true and set raw_ptr to nullptr, but only in the case
// when catch_type is a pointer and a nullptr is thrown. try_catch_dynamic
// doesn't work with catching pointers.
if (catch_type->__do_catch(ex_type, &raw_ptr, 1)) {
return raw_ptr;
}
return nullptr;
}
#endif // __GLIBCXX__

View File

@@ -8,11 +8,26 @@
#pragma once
#include <cstddef>
#if defined(__GLIBCXX__) && (defined(__x86_64__) || defined(__aarch64__))
#define OPTIMIZED_EXCEPTION_HANDLING_AVAILABLE
#endif
#if !defined(NO_OPTIMIZED_EXCEPTION_HANDLING)
#if defined(OPTIMIZED_EXCEPTION_HANDLING_AVAILABLE)
#define USE_OPTIMIZED_EXCEPTION_HANDLING
#else
#warn "Fast implementation of some of the exception handling routines is not available for this platform. Expect poor exception handling performance."
#endif
#endif
#include <seastar/core/sstring.hh>
#include <seastar/core/on_internal_error.hh>
#include <functional>
#include <system_error>
#include <type_traits>
namespace seastar { class logger; }
@@ -60,3 +75,36 @@ inline void maybe_rethrow_exception(std::exception_ptr ex) {
std::rethrow_exception(std::move(ex));
}
}
namespace utils::internal {
#if defined(OPTIMIZED_EXCEPTION_HANDLING_AVAILABLE)
void* try_catch_dynamic(std::exception_ptr& eptr, const std::type_info* catch_type) noexcept;
#endif
} // utils::internal
/// If the exception_ptr holds an exception which would match on a `catch (T&)`
/// clause, returns a pointer to it. Otherwise, returns `nullptr`.
///
/// The exception behind the pointer is valid as long as the exception
/// behind the exception_ptr is valid.
template<typename T>
inline T* try_catch(std::exception_ptr& eptr) noexcept {
static_assert(!std::is_pointer_v<T>, "catching pointers is not supported");
static_assert(!std::is_lvalue_reference_v<T> && !std::is_rvalue_reference_v<T>,
"T must not be a reference");
#if defined(USE_OPTIMIZED_EXCEPTION_HANDLING)
void* opt_ptr = utils::internal::try_catch_dynamic(eptr, &typeid(std::remove_const_t<T>));
return reinterpret_cast<T*>(opt_ptr);
#else
try {
std::rethrow_exception(eptr);
} catch (T& t) {
return &t;
} catch (...) {
}
return nullptr;
#endif
}