Skip to content

Commit

Permalink
Rework tutorial 6 to skip section on cancellations
Browse files Browse the repository at this point in the history
  • Loading branch information
anarthal committed Nov 23, 2024
1 parent a684791 commit edba297
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 118 deletions.
72 changes: 22 additions & 50 deletions doc/qbk/03_6_tutorial_connection_pool.qbk
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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]
Expand All @@ -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]




Expand Down
39 changes: 15 additions & 24 deletions example/1_tutorial/6_connection_pool.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,15 @@ struct employee
// Given an employee_id, retrieves the employee details to be sent to the client.
asio::awaitable<std::string> 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
Expand Down Expand Up @@ -137,9 +140,9 @@ asio::awaitable<void> handle_session(mysql::connection_pool& pool, asio::ip::tcp
}
//]

//[tutorial_connection_pool_listener
asio::awaitable<void> 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);

Expand All @@ -159,9 +162,7 @@ asio::awaitable<void> 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)
{
Expand All @@ -171,34 +172,22 @@ asio::awaitable<void> 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,

// 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)
{
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
50 changes: 6 additions & 44 deletions test/integration/test/snippets/tutorials.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -201,42 +201,6 @@ asio::awaitable<void> tutorial_updates_transactions(mysql::any_connection& conn)
}
}

asio::awaitable<void> 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<void> 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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit edba297

Please sign in to comment.