From edba2973748b783833d2e7e86aa0de581be5676c Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Sat, 23 Nov 2024 18:14:53 +0100 Subject: [PATCH] Rework tutorial 6 to skip section on cancellations --- doc/qbk/03_6_tutorial_connection_pool.qbk | 72 ++++++-------------- example/1_tutorial/6_connection_pool.cpp | 39 ++++------- test/integration/test/snippets/tutorials.cpp | 50 ++------------ 3 files changed, 43 insertions(+), 118 deletions(-) diff --git a/doc/qbk/03_6_tutorial_connection_pool.qbk b/doc/qbk/03_6_tutorial_connection_pool.qbk index b8a82ff8..6533a4ac 100644 --- a/doc/qbk/03_6_tutorial_connection_pool.qbk +++ b/doc/qbk/03_6_tutorial_connection_pool.qbk @@ -5,7 +5,7 @@ file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) ] -[section:tutorial_connection_pool Tutorial 6: Connection pools and timeouts] +[section:tutorial_connection_pool Tutorial 6: Connection pools] All our programs until now have used one-shot connections. They also didn't feature any fault tolerance: @@ -83,6 +83,19 @@ Subsequent [refmemunq connection_pool async_get_connection] calls may retrieve the same connection. This improves efficiency, since session establishment is costly. +[refmemunq connection_pool async_get_connection] waits +for a client connection to become available before completing. +If the server is unavailable or credentials are invalid, +it may wait indefinitely. This is a problem for both development and production. +We can solve this by using [asioreflink cancel_after cancel_after], +which allows setting timeouts to async operations: + +[tutorial_connection_pool_get_connection_timeout] + +Don't worry if you don't fully understand how this works. +We will go into more detail on [asioreflink cancel_after cancel_after], +cancellations and completion tokens in the next tutorial. + Putting all pieces together, our coroutine becomes: [tutorial_connection_pool_db] @@ -104,66 +117,25 @@ invoking the database access logic in the process: We now need logic to accept incoming TCP connections. We will use an `asio::ip::tcp::acceptor` object -in our main coroutine to accomplish it: - -[tutorial_connection_pool_acceptor_setup] - -Our program will accept connections in a loop: - -[tutorial_connection_pool_acceptor_loop] - - +to accomplish it, listening for connections in a loop +until the server is stopped: - - -[heading Setting a timeout for client sessions] - -We mentioned that [refmemunq connection_pool async_get_connection] waits -for a client connection to become available before completing. -If the server is unavailable or credentials are invalid, -`async_get_connection` may wait indefinitely. This is a problem -for both development and production. -[asioreflink cancel_after cancel_after] can help us, but we -should first understand what a -[@boost:/doc/html/boost_asio/reference/asynchronous_operations.html#boost_asio.reference.asynchronous_operations.completion_tokens_and_handlers completion token] -is. - -A completion token is an object that determines what to do -when an asynchronous operation completes, and can be passed -as the last argument to async functions. If nothing is passed, -the default completion token applies, which returns an object -that can be co_await'ed. This is what we have been doing until now. - -A callback is also a completion token. When a callback is passed to -an initiation function, it will be invoked when the operation completes. -We've also used this before, with `co_spawn`. - -Completion tokens are usually generic: once you learn how to use one, -it can be applied to all Asio-compliant async operations. - -[asioreflink cancel_after cancel_after] wraps a completion token -to produce a new token that issues a cancellation if the -operation it's passed to doesn't complete in a certain amount -of time. We can apply it to operations in Boost.MySQL like this: - -[tutorial_connection_pool_get_connection_timeout] - -While this works, it's usually better to set the timeout -at the session level. `asio::co_spawn` is an async operation, too, -so we can use completion tokens with it. Let's modify our acceptor loop: - -[tutorial_connection_pool_coro_timeout] +[tutorial_connection_pool_listener] [heading Waiting for signals] -We need a way to stop our program. We will use an `asio::signal_set` object +Finally, we need a way to stop our program. We will use an `asio::signal_set` object to catch signals, and call `io_context::stop` when Ctrl-C is pressed: [tutorial_connection_pool_signals] +Putting all these pieces together, our main program becomes: + +[tutorial_connection_pool_main] + diff --git a/example/1_tutorial/6_connection_pool.cpp b/example/1_tutorial/6_connection_pool.cpp index 4a78e6a5..9c3ba778 100644 --- a/example/1_tutorial/6_connection_pool.cpp +++ b/example/1_tutorial/6_connection_pool.cpp @@ -80,12 +80,15 @@ struct employee // Given an employee_id, retrieves the employee details to be sent to the client. asio::awaitable get_employee_details(mysql::connection_pool& pool, std::int64_t employee_id) { - //[tutorial_connection_pool_get_connection + //[tutorial_connection_pool_get_connection_timeout // Get a connection from the pool. // This will wait until a healthy connection is ready to be used. // pooled_connection grants us exclusive access to the connection until - // the object is destroyed - mysql::pooled_connection conn = co_await pool.async_get_connection(); + // the object is destroyed. + // Fail the operation if no connection becomes available in the next 20 seconds. + mysql::pooled_connection conn = co_await pool.async_get_connection( + asio::cancel_after(std::chrono::seconds(20)) + ); //] //[tutorial_connection_pool_use @@ -137,9 +140,9 @@ asio::awaitable handle_session(mysql::connection_pool& pool, asio::ip::tcp } //] +//[tutorial_connection_pool_listener asio::awaitable listener(mysql::connection_pool& pool, unsigned short port) { - //[tutorial_connection_pool_acceptor_setup // An object that accepts incoming TCP connections. asio::ip::tcp::acceptor acc(co_await asio::this_coro::executor); @@ -159,9 +162,7 @@ asio::awaitable listener(mysql::connection_pool& pool, unsigned short port // Start listening for connections acc.listen(); std::cout << "Server listening at " << acc.local_endpoint() << std::endl; - //] - //[tutorial_connection_pool_coro_timeout // Start the accept loop while (true) { @@ -171,8 +172,6 @@ asio::awaitable listener(mysql::connection_pool& pool, unsigned short port // Launch a coroutine that runs our session logic. // We don't co_await this coroutine so we can listen // to new connections while the session is running. - // co_spawn is actually an async operation, so we - // can pass completion tokens to it. asio::co_spawn( // Use the same executor as the current coroutine co_await asio::this_coro::executor, @@ -180,25 +179,15 @@ asio::awaitable listener(mysql::connection_pool& pool, unsigned short port // Session logic. Take ownership of the socket [&pool, sock = std::move(sock)]() mutable { return handle_session(pool, std::move(sock)); }, - // Completion token for the coroutine. - // If the coroutine hasn't finished after 60 seconds, Asio will cancel - // any I/O operation the coroutine is waiting for. - // The coroutine will see a failure in the I/O operation it's waiting - // for and throw, as it would for a network error. - // We pass an explicit completion token to cancel_after - // (a callback, in this case). The callback - // will be invoked when the coroutine completes, even if it's cancelled. - asio::cancel_after( - std::chrono::seconds(60), - [](std::exception_ptr ex) { - if (ex) - std::rethrow_exception(ex); - } - ) + // Propagate exceptions thrown in handle_session + [](std::exception_ptr ex) { + if (ex) + std::rethrow_exception(ex); + } ); } - //] } +//] void main_impl(int argc, char** argv) { @@ -212,6 +201,7 @@ void main_impl(int argc, char** argv) const char* password = argv[2]; const char* server_hostname = argv[3]; + //[tutorial_connection_pool_main //[tutorial_connection_pool_create // Create an I/O context, required by all I/O objects asio::io_context ctx; @@ -266,6 +256,7 @@ void main_impl(int argc, char** argv) // Calling run will actually execute the coroutine until completion ctx.run(); + //] } int main(int argc, char** argv) diff --git a/test/integration/test/snippets/tutorials.cpp b/test/integration/test/snippets/tutorials.cpp index 253f3ffa..830171b8 100644 --- a/test/integration/test/snippets/tutorials.cpp +++ b/test/integration/test/snippets/tutorials.cpp @@ -201,42 +201,6 @@ asio::awaitable tutorial_updates_transactions(mysql::any_connection& conn) } } -asio::awaitable handle_session(mysql::connection_pool&, asio::ip::tcp::socket) { co_return; } - -// For simplicity, we don't run this (we just check that it builds) -[[maybe_unused]] -asio::awaitable tutorial_connection_pool_unused( - mysql::connection_pool& pool, - asio::ip::tcp::acceptor acc -) -{ - //[tutorial_connection_pool_acceptor_loop - // Start the accept loop - while (true) - { - // Accept a new connection - auto sock = co_await acc.async_accept(); - - // Launch a coroutine that runs our session logic. - // We don't co_await this coroutine so we can listen - // to new connections while the session is running - asio::co_spawn( - // Use the same executor as the current coroutine - co_await asio::this_coro::executor, - - // Session logic. Take ownership of the socket - [&pool, sock = std::move(sock)]() mutable { return handle_session(pool, std::move(sock)); }, - - // Propagate exceptions thrown in handle_session - [](std::exception_ptr ex) { - if (ex) - std::rethrow_exception(ex); - } - ); - } - //] -} - void log_error(const char*, boost::system::error_code) {} // Version without diagnostics @@ -327,14 +291,12 @@ BOOST_FIXTURE_TEST_CASE(section_tutorials, snippets_fixture) mysql::connection_pool pool(ctx, create_pool_params()); pool.async_run(asio::detached); - // TODO: duplicated in connection_pool.cpp - //[tutorial_connection_pool_get_connection_timeout - // Get a connection from the pool, but don't wait more than 30 seconds. - // asio::cancel_after wraps the default completion token to produce an object - // that may be awaited, while also applying a timeout. - mysql::pooled_connection conn = co_await pool.async_get_connection( - asio::cancel_after(std::chrono::seconds(30)) - ); + //[tutorial_connection_pool_get_connection + // Get a connection from the pool. + // This will wait until a healthy connection is ready to be used. + // pooled_connection grants us exclusive access to the connection until + // the object is destroyed + mysql::pooled_connection conn = co_await pool.async_get_connection(); //] }); #endif