Connections now support immediate executors

Immediate completions in connection and any_connection are now correctly
    dispatched to the token's immediate executor using asio::async_immediate
    instead of plain asio::post.
Added a section on executors in the reference docs of async functions in
    connection and any_connection
Disabled TSAN connection_pool_cancel_get_connection for libc++ builds

close #301
This commit is contained in:
Anarthal (Rubén Pérez) 2024-10-03 20:19:51 +02:00 committed by GitHub
parent 7ef6ff8773
commit 268aa33a84
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 339 additions and 13 deletions

View File

@ -389,6 +389,16 @@ public:
*
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(error_code))
@ -751,6 +761,16 @@ public:
* The handler signature for this operation is
* `void(boost::mysql::error_code, std::size_t)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*
* \par Object lifetimes
* The storage that `output` references must be kept alive until the operation completes.
*/
@ -799,6 +819,16 @@ public:
* The handler signature for this operation is
* `void(boost::mysql::error_code, std::size_t)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*
* \par Object lifetimes
* The storage that `output` references must be kept alive until the operation completes.
*/
@ -903,6 +933,16 @@ public:
* \n
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
@ -1017,6 +1057,16 @@ public:
* \n
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
@ -1076,6 +1126,16 @@ public:
* \details
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
BOOST_ASIO_COMPLETION_TOKEN_FOR(void(error_code))
@ -1143,6 +1203,16 @@ public:
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*
* \par Object lifetimes
* The request and response objects must be kept alive and should not be modified
* until the operation completes.

View File

@ -247,6 +247,16 @@ public:
*
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
typename EndpointType,
@ -322,6 +332,16 @@ public:
*
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
@ -393,6 +413,16 @@ public:
*
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest,
@ -495,6 +525,16 @@ public:
*
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
BOOST_MYSQL_EXECUTION_REQUEST ExecutionRequest,
@ -576,6 +616,16 @@ public:
*
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code, boost::mysql::statement)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, ::boost::mysql::statement))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
@ -633,6 +683,16 @@ public:
*
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
@ -693,6 +753,16 @@ public:
* \par Handler signature
* The handler signature for this operation is
* `void(boost::mysql::error_code, boost::mysql::rows_view)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code, ::boost::mysql::rows_view))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
@ -828,6 +898,16 @@ public:
* The handler signature for this operation is
* `void(boost::mysql::error_code, std::size_t)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*
* \par Object lifetimes
* The storage that `output` references must be kept alive until the operation completes.
*/
@ -878,6 +958,16 @@ public:
* The handler signature for this operation is
* `void(boost::mysql::error_code, std::size_t)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*
* \par Object lifetimes
* The storage that `output` references must be kept alive until the operation completes.
*/
@ -943,6 +1033,16 @@ public:
* \par Handler signature
* The handler signature for this operation is
* `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <
BOOST_MYSQL_EXECUTION_STATE_TYPE ExecutionStateType,
@ -999,6 +1099,16 @@ public:
* \n
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
@ -1071,6 +1181,16 @@ public:
* \n
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
@ -1128,6 +1248,16 @@ public:
* \details
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>
@ -1186,6 +1316,16 @@ public:
* \details
* \par Handler signature
* The handler signature for this operation is `void(boost::mysql::error_code)`.
*
* \par Executor
* Intermediate completion handlers, as well as the final handler, are executed using
* `token`'s associated executor, or `this->get_executor()` if the token doesn't have an associated
* executor.
*
* If the final handler has an associated immediate executor, and the operation
* completes immediately, the final handler is dispatched to it.
* Otherwise, the final handler is called as if it was submitted using `asio::post`,
* and is never be called inline from within this function.
*/
template <BOOST_ASIO_COMPLETION_TOKEN_FOR(void(::boost::mysql::error_code))
CompletionToken BOOST_ASIO_DEFAULT_COMPLETION_TOKEN_TYPE(executor_type)>

View File

@ -19,6 +19,7 @@
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/immediate.hpp>
#include <boost/asio/post.hpp>
#include <boost/assert.hpp>
@ -66,7 +67,7 @@ struct run_algo_op
BOOST_MYSQL_YIELD(
resume_point_,
1,
asio::post(stream_.get_executor(), std::move(self))
asio::async_immediate(stream_.get_executor(), std::move(self))
)
}
self.complete(stored_ec_);

View File

@ -53,6 +53,11 @@ struct BOOST_ATTRIBUTE_NODISCARD network_result_base
{
error_code err;
diagnostics diag;
bool was_immediate{};
network_result_base(error_code ec = {}, diagnostics d = {}) noexcept : err(ec), diag(std::move(d)) {}
void validate_immediate(bool expect_immediate, source_location loc = BOOST_MYSQL_CURRENT_LOCATION) const;
void validate_no_error(source_location loc = BOOST_MYSQL_CURRENT_LOCATION) const;
@ -100,6 +105,16 @@ struct BOOST_ATTRIBUTE_NODISCARD network_result : network_result_base
{
}
// Allow chaining
network_result<R>& validate_immediate(
bool expect_immediate,
source_location loc = BOOST_MYSQL_CURRENT_LOCATION
)
{
network_result_base::validate_immediate(expect_immediate, loc);
return *this;
}
BOOST_ATTRIBUTE_NODISCARD
value_type get(source_location loc = BOOST_MYSQL_CURRENT_LOCATION) &&
{
@ -127,6 +142,7 @@ struct BOOST_ATTRIBUTE_NODISCARD runnable_network_result
create_server_diag("network_result_v2 - diagnostics not cleared")
};
bool done{false};
bool was_immediate{false};
impl_t(asio::io_context& ctx) : ctx(ctx) {}
};

View File

@ -28,6 +28,13 @@ void poll_until(
source_location loc = BOOST_MYSQL_CURRENT_LOCATION
);
// run fn() in ctx, then poll until it completes
void run_in_context(
asio::io_context& ctx,
const std::function<void()>& fn,
source_location loc = BOOST_MYSQL_CURRENT_LOCATION
);
} // namespace test
} // namespace mysql
} // namespace boost

View File

@ -22,6 +22,8 @@
#include <boost/mysql/detail/access.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/execution/blocking.hpp>
#include <boost/asio/execution/relationship.hpp>
#include <boost/asio/execution_context.hpp>
@ -414,6 +416,20 @@ void boost::mysql::test::poll_until(
}
}
void boost::mysql::test::run_in_context(
asio::io_context& ctx,
const std::function<void()>& fn,
source_location loc
)
{
bool finished = false;
asio::dispatch(asio::bind_executor(ctx.get_executor(), [fn, &finished]() {
fn();
finished = true;
}));
poll_until(ctx, &finished, loc);
}
//
// io_context_fixture.hpp
//
@ -427,6 +443,13 @@ boost::mysql::test::io_context_fixture::~io_context_fixture()
//
// network_result.hpp
//
void boost::mysql::test::network_result_base::validate_immediate(bool expect_immediate, source_location loc)
const
{
BOOST_TEST_CONTEXT("Called from " << loc) { BOOST_TEST(was_immediate == expect_immediate); }
}
void boost::mysql::test::network_result_base::validate_no_error(source_location loc) const
{
validate_error(error_code(), diagnostics(), loc);
@ -503,6 +526,9 @@ void boost::mysql::test::test_detail::as_netres_handler_base::complete_base(
network_result_base& netres
) const
{
// Are we in an immediate completion?
bool is_immediate = is_initiation_function();
// Check executor. The passed executor must be the top one in all cases.
// Immediate completions must be dispatched through the immediate executor, too.
// In all cases, we may encounter a bigger stack because of previous immediate completions.
@ -512,9 +538,8 @@ void boost::mysql::test::test_detail::as_netres_handler_base::complete_base(
};
// Expected top of the executor stack
boost::span<const int> expected_stack_top = is_initiation_function()
? boost::span<const int>(stack_data_immediate)
: boost::span<const int>(stack_data_regular);
boost::span<const int> expected_stack_top = is_immediate ? boost::span<const int>(stack_data_immediate)
: boost::span<const int>(stack_data_regular);
// Actual top of the executor stack
auto actual_stack_top = executor_stack().last(
@ -530,4 +555,7 @@ void boost::mysql::test::test_detail::as_netres_handler_base::complete_base(
netres.diag = *diag_ptr;
else
netres.diag = create_server_diag("<diagnostics unavailable>");
// Record immediate-ness
netres.was_immediate = is_immediate;
}

View File

@ -35,8 +35,8 @@
#include "test_common/create_basic.hpp"
#include "test_common/create_diagnostics.hpp"
#include "test_common/network_result.hpp"
#include "test_common/poll_until.hpp"
#include "test_common/printing.hpp"
#include "test_common/tracker_executor.hpp"
#include "test_integration/any_connection_fixture.hpp"
#include "test_integration/connect_params_builder.hpp"
#include "test_integration/run_coro.hpp"
@ -317,6 +317,25 @@ BOOST_FIXTURE_TEST_CASE(default_token_cancel_after, any_connection_fixture)
});
}
// Spotcheck: immediate completions dispatched to the immediate executor
BOOST_FIXTURE_TEST_CASE(immediate_completions, any_connection_fixture)
{
run_in_context(ctx, [this]() {
// Setup
connect();
results r;
// Prepare a statement
auto stmt = conn.async_prepare_statement("SELECT 1", as_netresult).get();
// Executing with the wrong number of params is an immediate error
conn.async_execute(stmt.bind(0), r, as_netresult)
.run()
.validate_immediate(true)
.validate_error(client_errc::wrong_num_params);
});
}
#endif
BOOST_AUTO_TEST_SUITE_END()

View File

@ -24,6 +24,12 @@ local tests =
for test in $(tests)
{
# TSAN seems to raise a false positive with libc++ shared_ptr/weak_ptr, used in this test
local libcpp_exclusion ;
if $(test) = "connection_pool_cancel_get_connection" {
libcpp_exclusion = <stdlib>libc++:<build>no ;
}
# In OSX, tsan reports a race condition in Asio's kqueue reactor. Ignore this for now.
# Net TS executors use std::atomic_thread_fence, unsupported by TSAN, which yields false positives.
# gcc-5 seems to emit incorrect tsan instrumentation code for static initializations
@ -36,6 +42,7 @@ for test in $(tests)
<target-os>darwin:<build>no
<boost.mysql.use-ts-executor>on:<build>no
<toolset>gcc-5:<build>no
$(libcpp_exclusion)
<thread-sanitizer>norecover
: target-name $(test)
;

View File

@ -16,9 +16,11 @@
#include <boost/asio/any_completion_handler.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/async_result.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/coroutine.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/dispatch.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/post.hpp>
#include <boost/test/unit_test.hpp>
@ -32,6 +34,7 @@
#include "test_common/io_context_fixture.hpp"
#include "test_common/netfun_maker.hpp"
#include "test_common/network_result.hpp"
#include "test_common/poll_until.hpp"
#include "test_unit/printing.hpp"
using namespace boost::mysql::test;
@ -417,18 +420,15 @@ BOOST_AUTO_TEST_CASE(stream_errors)
}
// Returning an error or next_action() from resume in the first call works correctly
BOOST_AUTO_TEST_CASE(resume_error_immediate)
BOOST_AUTO_TEST_CASE(resume_error_immediate_sync)
{
struct
{
const char* name;
signature_t fn;
error_code ec;
} test_cases[] = {
{"success_sync", sync_fn, error_code() },
{"success_async", async_fn, error_code() },
{"error_sync", sync_fn, asio::error::no_data},
{"error_async", async_fn, asio::error::no_data},
{"success", error_code() },
{"error", asio::error::no_data},
};
for (const auto& tc : test_cases)
@ -440,13 +440,51 @@ BOOST_AUTO_TEST_CASE(resume_error_immediate)
mock_algo algo(next_action(tc.ec));
test_engine eng{fix.ctx.get_executor()};
tc.fn(eng, any_resumable_ref(algo))
sync_fn(eng, any_resumable_ref(algo))
.validate_error(tc.ec, create_server_diag("<diagnostics unavailable>"));
BOOST_TEST(eng.value.stream().calls.size() == 0u);
algo.check_calls({
{error_code(), 0u}
});
// Note: the testing infrastructure already checks that we post correctly in the async versions
}
}
}
BOOST_AUTO_TEST_CASE(resume_error_immediate_async)
{
struct
{
const char* name;
error_code ec;
} test_cases[] = {
{"success", error_code() },
{"error", asio::error::no_data},
};
for (const auto& tc : test_cases)
{
BOOST_TEST_CONTEXT(tc.name)
{
// We need to call the initiation function from a context thread
// to get immediate completions
io_context_fixture fix;
run_in_context(fix.ctx, [&]() {
// Setup
mock_algo algo(next_action(tc.ec));
test_engine eng{fix.ctx.get_executor()};
// Run the function
eng.async_run(any_resumable_ref(algo), as_netresult)
.run()
.validate_immediate(true)
.validate_error(tc.ec, create_server_diag("<diagnostics unavailable>"));
// Check
BOOST_TEST(eng.value.stream().calls.size() == 0u);
algo.check_calls({
{error_code(), 0u}
});
});
}
}
}