Skip to content

Commit e988c9a

Browse files
authored
Added docs on using asio::cancel_after with pool_params::thread_safe
close #402
1 parent 33660e2 commit e988c9a

File tree

5 files changed

+176
-38
lines changed

5 files changed

+176
-38
lines changed

doc/images/connection_pool_impl.svg

Lines changed: 4 additions & 0 deletions
Loading

doc/qbk/12_connection_pool.qbk

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,25 +166,54 @@ In short:
166166
setting [refmem pool_params ping_interval] to zero.
167167

168168

169-
[heading Thread-safety]
169+
[heading:thread_safe Thread-safety]
170170

171171
By default, [reflink connection_pool] is [*not thread-safe], but it can
172172
be easily made thread-safe by setting [refmem pool_params thread_safe]:
173173

174-
[connection_pool_thread_safe]
174+
[connection_pool_thread_safe_create]
175175

176-
Thread-safe connection pools create internally a [asioreflink strand strand],
177-
Asio's method to enable concurrency without explicit locking.
178-
Enabling thread-safety will ensure that all intermediate handlers run through
179-
the created strand, avoiding data races at the cost of some performance.
176+
To correctly understand what is protected by [refmem pool_params thread_safe]
177+
and what is not, we need a grasp of how pools are implemented.
178+
Both [reflink connection_pool] and individual [reflink pooled_connection]'s
179+
hold pointers to a shared state object containing all data required by the pool:
180180

181-
[note
182-
Thread-safety only protects the pool. Individual connections are [*not] thread-safe.
183-
Assignments aren't thread-safe, either. See [reflink connection_pool] docs for more info.
184-
]
181+
[$mysql/images/connection_pool_impl.svg [align center]]
182+
183+
Thread-safe connection pools internally create an [asioreflink strand strand]
184+
that protects the connection pool's state. Operations like
185+
[refmemunq connection_pool async_get_connection], [refmemunq connection_pool async_run]
186+
and [reflink pooled_connection]'s destructor will run through the strand,
187+
and are safe to be run from any thread. Operations that mutate
188+
state handles (the internal `std::shared_ptr`), like [*assignment operators,
189+
are not thread-safe].
190+
191+
Data outside the pool's state is not protected. In particular,
192+
[*`asio::cancel_after` creates an internal timer that can cause
193+
inadvertent race conditions]. For example:
194+
195+
[connection_pool_thread_safe_use]
196+
197+
This coroutine must be run within a strand:
198+
199+
[connection_pool_thread_safe_spawn]
200+
201+
202+
If we don't use `asio::make_shared`, we have the following race condition:
203+
204+
* The thread calling `async_get_connection` sets up the timer required by `asio::cancel_after`.
205+
* In parallel, the thread running the execution context sees that there is a healthy connection
206+
and completes the `async_get_connection` operation. As a result, the timer is cancelled.
207+
Thus, the timer is accessed concurrently from both threads without protection.
208+
209+
210+
If you're using callbacks, code gets slightly more convoluted. The
211+
above coroutine can be rewritten as:
212+
213+
[connection_pool_thread_safe_callbacks]
185214

186-
Thread-safety extends to per-operation cancellation, too.
187-
Cancelling an operation on a thread-safe pool is safe.
215+
Thread-safety is disabled by default because strands impose a performance
216+
penalty that is avoidable in single-threaded programs.
188217

189218

190219
[heading Transport types and TLS]

include/boost/mysql/connection_pool.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ class pooled_connection
278278
* like \ref pooled_connection, have their own state handle,
279279
* and thus interact only with the pool state.
280280
*
281+
* If configured to be thread-safe, the protection applies only to the pool's state.
282+
* In particular, be careful when using `asio::cancel_after` and similar tokens.
283+
* Please read
284+
* <a href="../connection_pool.html#mysql.connection_pool.thread_safe">this page</a> for more info.
285+
*
281286
* In summary:
282287
*
283288
* - Distinct objects: safe. \n
@@ -662,6 +667,11 @@ class connection_pool
662667
* Otherwise, intermediate handlers are executed using
663668
* `token`'s associated executor if it has one, or `this->get_executor()` if it hasn't.
664669
*
670+
* **Caution**: be careful when using thread-safety and `asio::cancel_after`, as it
671+
* can result in inadvertent race conditions. Please refer to
672+
* <a href="../../../connection_pool.html#mysql.connection_pool.thread_safe">this
673+
* page</a> for more info.
674+
*
665675
* \par Per-operation cancellation
666676
* This operation supports per-operation cancellation.
667677
* Cancelling `async_get_connection` has no observable side effects.

include/boost/mysql/pool_params.hpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,12 @@ struct pool_params
173173
* will be run through the created strand.
174174
*
175175
* Thread-safety doesn't extend to individual connections: \ref pooled_connection
176-
* objects can't be shared between threads.
176+
* objects can't be shared between threads. Thread-safety does not protect
177+
* objects that don't belong to the pool. For instance, `asio::cancel_after`
178+
* creates a timer that must be protected with a strand.
179+
* Refer to
180+
* <a href="../../connection_pool.html#mysql.connection_pool.thread_safe">this
181+
* page</a> for more info.
177182
*/
178183
bool thread_safe{false};
179184

test/integration/test/snippets/connection_pool.cpp

Lines changed: 115 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
#include <boost/mysql/with_diagnostics.hpp>
1414

1515
#include <boost/asio/awaitable.hpp>
16+
#include <boost/asio/bind_executor.hpp>
1617
#include <boost/asio/cancel_after.hpp>
18+
#include <boost/asio/co_spawn.hpp>
1719
#include <boost/asio/detached.hpp>
1820
#include <boost/asio/io_context.hpp>
21+
#include <boost/asio/strand.hpp>
1922
#include <boost/asio/thread_pool.hpp>
2023
#include <boost/asio/use_future.hpp>
24+
#include <boost/config.hpp>
25+
#include <boost/system/error_code.hpp>
2126
#include <boost/test/unit_test.hpp>
2227

2328
#include <chrono>
@@ -26,8 +31,9 @@
2631
#include "test_integration/run_coro.hpp"
2732
#include "test_integration/snippets/credentials.hpp"
2833

29-
using namespace boost::mysql;
30-
using namespace boost::mysql::test;
34+
namespace asio = boost::asio;
35+
namespace mysql = boost::mysql;
36+
using namespace mysql::test;
3137

3238
namespace {
3339

@@ -36,48 +42,112 @@ namespace {
3642
// Use connection pools for functions that will be called
3743
// repeatedly during the application lifetime.
3844
// An HTTP server handler function is a good candidate.
39-
boost::asio::awaitable<std::int64_t> get_num_employees(boost::mysql::connection_pool& pool)
45+
asio::awaitable<std::int64_t> get_num_employees(mysql::connection_pool& pool)
4046
{
4147
// Get a fresh connection from the pool.
4248
// pooled_connection is a proxy to an any_connection object.
43-
boost::mysql::pooled_connection conn = co_await pool.async_get_connection();
49+
mysql::pooled_connection conn = co_await pool.async_get_connection();
4450

4551
// Use pooled_connection::operator-> to access the underlying any_connection.
4652
// Let's use the connection
47-
results result;
53+
mysql::results result;
4854
co_await conn->async_execute("SELECT COUNT(*) FROM employee", result);
4955
co_return result.rows().at(0).at(0).as_int64();
5056

5157
// When conn is destroyed, the connection is returned to the pool
5258
}
5359
//]
5460

55-
boost::asio::awaitable<void> return_without_reset(boost::mysql::connection_pool& pool)
61+
asio::awaitable<void> return_without_reset(mysql::connection_pool& pool)
5662
{
5763
//[connection_pool_return_without_reset
5864
// Get a connection from the pool
59-
boost::mysql::pooled_connection conn = co_await pool.async_get_connection();
65+
mysql::pooled_connection conn = co_await pool.async_get_connection();
6066

6167
// Use the connection in a way that doesn't mutate session state.
6268
// We're not setting variables, preparing statements or starting transactions,
6369
// so it's safe to skip reset
64-
boost::mysql::results result;
70+
mysql::results result;
6571
co_await conn->async_execute("SELECT COUNT(*) FROM employee", result);
6672

6773
// Explicitly return the connection to the pool, skipping reset
6874
conn.return_without_reset();
6975
//]
7076
}
7177

72-
boost::asio::awaitable<void> apply_timeout(boost::mysql::connection_pool& pool)
78+
asio::awaitable<void> apply_timeout(mysql::connection_pool& pool)
7379
{
7480
//[connection_pool_apply_timeout
7581
// Get a connection from the pool, but don't wait more than 5 seconds
76-
auto conn = co_await pool.async_get_connection(boost::asio::cancel_after(std::chrono::seconds(5)));
82+
auto conn = co_await pool.async_get_connection(asio::cancel_after(std::chrono::seconds(5)));
7783
//]
7884

7985
conn.return_without_reset();
8086
}
87+
88+
//[connection_pool_thread_safe_use
89+
// A function that handles a user session in a server
90+
asio::awaitable<void> handle_session(mysql::connection_pool& pool)
91+
{
92+
// CAUTION: asio::cancel_after creates a timer that is *not* part of the pool's state.
93+
// The timer is not protected by the pool's strand.
94+
// This coroutine must be run within a strand for this to be safe
95+
using namespace std::chrono_literals;
96+
co_await pool.async_get_connection(asio::cancel_after(30s));
97+
98+
// Use the connection
99+
}
100+
//]
101+
#endif
102+
103+
#ifndef BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES
104+
//[connection_pool_thread_safe_callbacks
105+
// Holds per-session state
106+
class session_handler : public std::enable_shared_from_this<session_handler>
107+
{
108+
// The connection pool
109+
mysql::connection_pool& pool_;
110+
111+
// A strand object, unique to this session
112+
asio::strand<asio::any_io_executor> strand_;
113+
114+
public:
115+
// pool.get_executor() points to the execution context that was used
116+
// to create the pool, and never to the pool's internal strand
117+
session_handler(mysql::connection_pool& pool)
118+
: pool_(pool), strand_(asio::make_strand(pool.get_executor()))
119+
{
120+
}
121+
122+
void start()
123+
{
124+
// Enters the strand. The passed function will be executed through the strand.
125+
// If the initiation is run outside the strand, a race condition will occur.
126+
asio::dispatch(asio::bind_executor(strand_, [self = shared_from_this()] { self->get_connection(); }));
127+
}
128+
129+
void get_connection()
130+
{
131+
// This function will run within the strand. Binding the passed callback to
132+
// the strand will make async_get_connection run it within the strand, too.
133+
pool_.async_get_connection(asio::cancel_after(
134+
std::chrono::seconds(30),
135+
asio::bind_executor(
136+
strand_,
137+
[self = shared_from_this()](boost::system::error_code, mysql::pooled_connection) {
138+
// Use the connection as required
139+
}
140+
)
141+
));
142+
}
143+
};
144+
145+
void handle_session_v2(mysql::connection_pool& pool)
146+
{
147+
// Start the callback chain
148+
std::make_shared<session_handler>(pool)->start();
149+
}
150+
//]
81151
#endif
82152

83153
BOOST_AUTO_TEST_CASE(section_connection_pool)
@@ -90,39 +160,39 @@ BOOST_AUTO_TEST_CASE(section_connection_pool)
90160
// You must specify enough information to establish a connection,
91161
// including the server address and credentials.
92162
// You can configure a lot of other things, like pool limits
93-
boost::mysql::pool_params params;
163+
mysql::pool_params params;
94164
params.server_address.emplace_host_and_port(server_hostname);
95165
params.username = mysql_username;
96166
params.password = mysql_password;
97167
params.database = "boost_mysql_examples";
98168

99169
// The I/O context, required by all I/O operations
100-
boost::asio::io_context ctx;
170+
asio::io_context ctx;
101171

102172
// Construct a pool of connections. The context will be used internally
103173
// to create the connections and other I/O objects
104-
boost::mysql::connection_pool pool(ctx, std::move(params));
174+
mysql::connection_pool pool(ctx, std::move(params));
105175

106176
// You need to call async_run on the pool before doing anything useful with it.
107177
// async_run creates connections and keeps them healthy. It must be called
108178
// only once per pool.
109179
// The detached completion token means that we don't want to be notified when
110180
// the operation ends. It's similar to a no-op callback.
111-
pool.async_run(boost::asio::detached);
181+
pool.async_run(asio::detached);
112182
//]
113183

114184
#ifdef BOOST_ASIO_HAS_CO_AWAIT
115-
run_coro(ctx, [&pool]() -> boost::asio::awaitable<void> {
185+
run_coro(ctx, [&pool]() -> asio::awaitable<void> {
116186
co_await get_num_employees(pool);
117187
pool.cancel();
118188
});
119189
#endif
120190
}
121191
{
122-
boost::asio::io_context ctx;
192+
asio::io_context ctx;
123193

124194
//[connection_pool_configure_size
125-
boost::mysql::pool_params params;
195+
mysql::pool_params params;
126196

127197
// Set the usual params
128198
params.server_address.emplace_host_and_port(server_hostname);
@@ -134,38 +204,58 @@ BOOST_AUTO_TEST_CASE(section_connection_pool)
134204
params.initial_size = 10;
135205
params.max_size = 1000;
136206

137-
boost::mysql::connection_pool pool(ctx, std::move(params));
207+
mysql::connection_pool pool(ctx, std::move(params));
138208
//]
139209

140210
#ifdef BOOST_ASIO_HAS_CO_AWAIT
141-
pool.async_run(boost::asio::detached);
142-
run_coro(ctx, [&pool]() -> boost::asio::awaitable<void> {
211+
pool.async_run(asio::detached);
212+
run_coro(ctx, [&pool]() -> asio::awaitable<void> {
143213
co_await return_without_reset(pool);
144214
co_await apply_timeout(pool);
145215
pool.cancel();
146216
});
147217
#endif
148218
}
149219
{
150-
//[connection_pool_thread_safe
151-
// The I/O context, required by all I/O operations
152-
boost::asio::io_context ctx;
220+
//[connection_pool_thread_safe_create
221+
// The I/O context, required by all I/O operations.
222+
// This is like an io_context, but with 5 threads running it.
223+
asio::thread_pool ctx(5);
153224

154225
// The usual pool configuration params
155-
boost::mysql::pool_params params;
226+
mysql::pool_params params;
156227
params.server_address.emplace_host_and_port(server_hostname);
157228
params.username = mysql_username;
158229
params.password = mysql_password;
159230
params.database = "boost_mysql_examples";
160231
params.thread_safe = true; // enable thread safety
161232

162233
// Construct a thread-safe pool
163-
boost::mysql::connection_pool pool(ctx, std::move(params));
234+
mysql::connection_pool pool(ctx, std::move(params));
235+
pool.async_run(asio::detached);
164236

165237
// We can now pass a reference to pool to other threads,
166238
// and call async_get_connection concurrently without problem.
167239
// Individual connections are still not thread-safe.
168240
//]
241+
242+
#ifdef BOOST_ASIO_HAS_CO_AWAIT
243+
//[connection_pool_thread_safe_spawn
244+
// OK: the entire coroutine runs within a strand.
245+
// In a typical server setup, each request usually gets its own strand,
246+
// so it can run in parallel with other requests.
247+
asio::co_spawn(
248+
asio::make_strand(ctx), // If we removed this make_strand, we would have a race condition
249+
[&pool] { return handle_session(pool); },
250+
asio::detached
251+
);
252+
//]
253+
#endif
254+
#ifndef BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES
255+
handle_session_v2(pool);
256+
#endif
257+
pool.cancel();
258+
ctx.join();
169259
}
170260
}
171261

0 commit comments

Comments
 (0)