Skip to content

Commit

Permalink
with_diagnostics now passes through unknown signatures
Browse files Browse the repository at this point in the history
This makes asio::as_tuple and asio::redirect_error usable as partial
completion tokens with any_connection and connection_pool.
Fixed a Jamfile issue that caused warnings in dependencies to fail CI builds.
Removed leftover comment in the pipeline example.
  • Loading branch information
anarthal authored Nov 2, 2024
1 parent 272533c commit 8b2727f
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 30 deletions.
2 changes: 0 additions & 2 deletions example/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

import os ;

project : requirements <library>/boost/mysql//boost_mysql ;

path-constant this_dir : . ;

# The hostname to use for examples
Expand Down
3 changes: 1 addition & 2 deletions example/pipeline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,7 @@ asio::awaitable<std::vector<boost::mysql::statement>> batch_prepare(
for (auto stmt_sql : statements)
req.add_prepare_statement(stmt_sql);

// Run the pipeline. Using as_tuple prevents async_run_pipeline from throwing.
// This allows us to include the diagnostics object diag in the thrown exception.
// Run the pipeline.
// stage_response is a variant-like type that can hold the response of any stage type.
std::vector<boost::mysql::stage_response> pipe_res;
co_await conn.async_run_pipeline(req, pipe_res);
Expand Down
86 changes: 70 additions & 16 deletions include/boost/mysql/impl/with_diagnostics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,14 @@ struct with_diag_handler_fn
std::shared_ptr<diagnostics> owning_diag;
};

// By default, don't modify the signature.
// This makes asio::as_tuple(with_diagnostics(X)) equivalent
// to asio::as_tuple(X).
template <typename Signature>
struct with_diag_signature;
struct with_diag_signature
{
using type = Signature;
};

template <typename R, typename... Args>
struct with_diag_signature<R(error_code, Args...)>
Expand Down Expand Up @@ -146,14 +152,23 @@ struct with_diag_init : public Initiation
}
};

} // namespace detail
} // namespace mysql
// Did with_diagnostics modify any of the signatures?
// We really support only modifying all or none, and that's enough.
template <class Signature>
using with_diag_has_original_signature = std::
is_same<Signature, typename with_diag_signature<Signature>::type>;

namespace asio {
template <class... Signatures>
using with_diag_has_original_signatures = mp11::
mp_all_of<mp11::mp_list<Signatures...>, with_diag_has_original_signature>;

template <typename CompletionToken, bool has_original_signatures, typename... Signatures>
struct with_diagnostics_async_result;

// async_result when the signature was modified
template <typename CompletionToken, typename... Signatures>
struct async_result<mysql::with_diagnostics_t<CompletionToken>, Signatures...>
: async_result<CompletionToken, typename mysql::detail::with_diag_signature<Signatures>::type...>
struct with_diagnostics_async_result<CompletionToken, false, Signatures...>
: asio::async_result<CompletionToken, typename with_diag_signature<Signatures>::type...>
{
template <class RawCompletionToken>
using maybe_const_token_t = typename std::conditional<
Expand All @@ -163,26 +178,65 @@ struct async_result<mysql::with_diagnostics_t<CompletionToken>, Signatures...>

template <typename Initiation, typename RawCompletionToken, typename... Args>
static auto initiate(Initiation&& initiation, RawCompletionToken&& token, Args&&... args)
-> decltype(async_initiate<
-> decltype(asio::async_initiate<
maybe_const_token_t<RawCompletionToken>,
typename mysql::detail::with_diag_signature<Signatures>::type...>(
std::declval<mysql::detail::with_diag_init<typename std::decay<Initiation>::type>>(),
mysql::detail::access::get_impl(token),
typename with_diag_signature<Signatures>::type...>(
with_diag_init<typename std::decay<Initiation>::type>{std::forward<Initiation>(initiation)},
access::get_impl(token),
std::forward<Args>(args)...
))
{
return async_initiate<
return asio::async_initiate<
maybe_const_token_t<RawCompletionToken>,
typename mysql::detail::with_diag_signature<Signatures>::type...>(
mysql::detail::with_diag_init<typename std::decay<Initiation>::type>{
std::forward<Initiation>(initiation)
},
mysql::detail::access::get_impl(token),
typename with_diag_signature<Signatures>::type...>(
with_diag_init<typename std::decay<Initiation>::type>{std::forward<Initiation>(initiation)},
access::get_impl(token),
std::forward<Args>(args)...
);
}
};

// async_result when the signature wasn't modified (pass-through)
template <typename CompletionToken, typename... Signatures>
struct with_diagnostics_async_result<CompletionToken, true, Signatures...>
: asio::async_result<CompletionToken, Signatures...>
{
template <class RawCompletionToken>
using maybe_const_token_t = typename std::conditional<
std::is_const<typename std::remove_reference<RawCompletionToken>::type>::value,
const CompletionToken,
CompletionToken>::type;

template <typename Initiation, typename RawCompletionToken, typename... Args>
static auto initiate(Initiation&& initiation, RawCompletionToken&& token, Args&&... args)
-> decltype(asio::async_initiate<maybe_const_token_t<RawCompletionToken>, Signatures...>(
std::forward<Initiation>(initiation),
access::get_impl(token),
std::forward<Args>(args)...
))
{
return asio::async_initiate<maybe_const_token_t<RawCompletionToken>, Signatures...>(
std::forward<Initiation>(initiation),
access::get_impl(token),
std::forward<Args>(args)...
);
}
};

} // namespace detail
} // namespace mysql

namespace asio {

template <typename CompletionToken, typename... Signatures>
struct async_result<mysql::with_diagnostics_t<CompletionToken>, Signatures...>
: mysql::detail::with_diagnostics_async_result<
CompletionToken,
mysql::detail::with_diag_has_original_signatures<Signatures...>::value,
Signatures...>
{
};

} // namespace asio
} // namespace boost

Expand Down
20 changes: 13 additions & 7 deletions include/boost/mysql/with_diagnostics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,26 @@ namespace mysql {
* Uses knowledge of Boost.MySQL internals to grab any \ref diagnostics
* that the operation may produce to create a `std::exception_ptr` pointing to an \ref error_with_diagnostics
* object. On success, the generated `std::exception_ptr` will be `nullptr`.
* \n
*
* Using `with_diagnostics` to wrap tokens that throw exceptions (like `deferred` + `co_await` or
* `yield_context`) enhances the thrown exceptions with diagnostics information, matching the ones thrown by
* sync functions. If you don't use this token, Asio will use `system_error` exceptions, containing less info.
* \n
*
* This token can only be used with operations involving Boost.MySQL, as it relies on its internals.
* \n
*
* Like `asio::as_tuple`, this class wraps another completion token. For instance,
* `with_diagnostics(asio::deferred)` will generate a deferred operation with an adapted
* signature, which will throw `error_with_diagnostics` when `co_await`'ed.
* \n
* This token can only be used for operations with `void(error_code, T...)` signature.
* If this precondition is broken, a compile-time error will be issued. In particular,
* `with_diagnostics(asio::as_tuple(X))` is an error.
*
* If this token is applied to a function with a handler signature that
* does not match `void(error_code, T...)`, the token acts as a pass-through:
* it does not modify the signature, and calls the underlying token's initiation directly.
* This has the following implications:
*
* - `asio::as_tuple(with_diagnostics(X))` is equivalent to `asio::as_tuple(X)`.
* - `asio::redirect_error(with_diagnostics(X))` is equivalent to `asio::redirect_error(X)`.
* - Tokens like `asio::as_tuple` and `asio::redirect_error` can be used as partial tokens
* when `with_diagnostics` is the default completion token, as is the case for \ref any_connection.
*/
template <class CompletionToken>
class with_diagnostics_t
Expand Down
5 changes: 2 additions & 3 deletions test/Jamfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import ac ;
import indirect ;
import config : requires ;

project : requirements <library>/boost/mysql//boost_mysql ;

# Support header-only builds
feature.feature boost.mysql.separate-compilation : on off : propagated composite ;

Expand Down Expand Up @@ -67,6 +65,7 @@ local requirements =
<toolset>msvc:<define>_SILENCE_CXX17_ALLOCATOR_VOID_DEPRECATION_WARNING
<toolset>msvc:<define>_SILENCE_CXX17_ADAPTOR_TYPEDEFS_DEPRECATION_WARNING
<toolset>msvc:<define>_SILENCE_CXX17_ITERATOR_BASE_CLASS_DEPRECATION_WARNING
<toolset>msvc:<define>_CRT_SECURE_NO_WARNINGS # Required by Asio
# gcc-13+ doesn't understand view types and issues array bound warnings that don't make sense.
# -Wno-implicit-fallthrough is required by Asio SSL components
<toolset>gcc:<cxxflags>"-Wno-dangling-reference -Wno-array-bounds -Wno-implicit-fallthrough"
Expand All @@ -90,7 +89,7 @@ local requirements =

alias boost_mysql
:
/boost/charconv//boost_charconv
$(boost_dependencies)/<warnings-as-errors>off
/openssl//ssl/<link>shared
/openssl//crypto/<link>shared
: requirements
Expand Down
43 changes: 43 additions & 0 deletions test/integration/test/any_connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@

#include <boost/mysql/impl/internal/variant_stream.hpp>

#include <boost/asio/as_tuple.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/local/basic_endpoint.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/test/data/test_case.hpp>

#include <chrono>
Expand Down Expand Up @@ -317,6 +319,47 @@ BOOST_FIXTURE_TEST_CASE(default_token_cancel_after, any_connection_fixture)
});
}

// Using as_tuple as partial token works
BOOST_FIXTURE_TEST_CASE(default_token_as_tuple, any_connection_fixture)
{
run_coro(ctx, [&]() -> asio::awaitable<void> {
// connect
auto [ec] = co_await conn.async_connect(connect_params_builder().build(), asio::as_tuple);
BOOST_TEST_REQUIRE(ec == error_code());

// Returning a value works
auto [ec2, stmt] = co_await conn.async_prepare_statement("SELECT ?", asio::as_tuple);
BOOST_TEST_REQUIRE(ec2 == error_code());
BOOST_TEST(stmt.valid());

// Error case
results result;
auto [ec3] = co_await conn.async_execute("SELECT * FROM bad_table", result, asio::as_tuple);
BOOST_TEST(ec3 == common_server_errc::er_no_such_table);
});
}

// Using redirect_error as partial token works
BOOST_FIXTURE_TEST_CASE(default_token_redirect_error, any_connection_fixture)
{
run_coro(ctx, [&]() -> asio::awaitable<void> {
// connect
error_code ec;
co_await conn.async_connect(connect_params_builder().build(), asio::redirect_error(ec));
BOOST_TEST_REQUIRE(ec == error_code());

// Returning a value works
auto stmt = co_await conn.async_prepare_statement("SELECT ?", asio::redirect_error(ec));
BOOST_TEST_REQUIRE(ec == error_code());
BOOST_TEST(stmt.valid());

// Error case
results result;
co_await conn.async_execute("SELECT * FROM bad_table", result, asio::redirect_error(ec));
BOOST_TEST(ec == common_server_errc::er_no_such_table);
});
}

// Spotcheck: immediate completions dispatched to the immediate executor
BOOST_FIXTURE_TEST_CASE(immediate_completions, any_connection_fixture)
{
Expand Down
57 changes: 57 additions & 0 deletions test/unit/test/with_diagnostics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,61 @@ static_assert(
""
);

// Applying with_diagnostics to an unkown signature is a pass-through
struct no_ec_initiation
{
template <class Handler, class T1, class T2, class T3>
void operator()(Handler&& handler, T1&& arg1, T2&& arg2, T3&& arg3)
{
// T1 should be a non-const lvalue
static_assert(std::is_same<T1, std::shared_ptr<int>&>::value, "");
BOOST_TEST(arg1 != nullptr);

// T2 should be a const lvalue
static_assert(std::is_same<T2, const std::shared_ptr<int>&>::value, "");
BOOST_TEST(arg2 != nullptr);

// T3 should be a rvalue
static_assert(std::is_same<T3, std::shared_ptr<int>>::value, "");
BOOST_TEST(arg3 != nullptr);
auto arg3_move = std::move(arg3);
boost::ignore_unused(arg3_move);

// Just call the handler
std::move(handler)(42);
}
};

template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(int)) CompletionToken>
void async_no_ec(
std::shared_ptr<int>& arg1,
const std::shared_ptr<int>& arg2,
std::shared_ptr<int>&& arg3,
CompletionToken&& token
)
{
asio::async_initiate<CompletionToken, void(int)>(no_ec_initiation{}, token, arg1, arg2, std::move(arg3));
}

BOOST_AUTO_TEST_CASE(signature_no_ec)
{
// Setup
auto arg1 = std::make_shared<int>(42);
auto arg2 = arg1;
auto arg3 = arg1;
bool called = false;
auto handler = [&](int val) {
BOOST_TEST(val == 42);
called = true;
};

// Call the operation
async_no_ec(arg1, arg2, std::move(arg3), with_diagnostics(handler));

// lvalues not moved, rvalue moved
BOOST_TEST(arg1 != nullptr);
BOOST_TEST(arg2 != nullptr);
BOOST_TEST(arg3 == nullptr);
}

BOOST_AUTO_TEST_SUITE_END()

0 comments on commit 8b2727f

Please sign in to comment.