-
Notifications
You must be signed in to change notification settings - Fork 34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document interaction between asio::cancel_after and pool_params::thread_safe #402
Comments
Hi! From the top of my head, I don't see anything evidently wrong, so it might be an actual bug. I will look into this tomorrow. In the meanwhile, just to discard potential problems, would you mind trying the following?
Many thanks, |
Thanks for replying! With atomic: #include <boost/asio/io_context.hpp>
#include <boost/mysql/connection_pool.hpp>
#include <iostream>
#include <thread>
std::atomic<int> counter = 0;
void GetConnection(boost::mysql::connection_pool& pool, int index) {
pool.async_get_connection(boost::asio::cancel_after(
std::chrono::seconds(6),
[index](boost::mysql::error_code error, boost::mysql::pooled_connection /*connection*/) {
if (error) {
std::cout << error.what() << std::endl;
return;
}
std::cout << index << std::endl;
--counter;
}));
}
boost::mysql::pool_params GetParams() {
boost::mysql::pool_params params;
params.server_address.emplace_host_and_port("127.0.0.1", 57761);
params.username = "root";
params.password = "root";
params.database = "test_0";
params.max_size = 4;
params.initial_size = 4;
params.thread_safe = true;
params.multi_queries = true;
params.connect_timeout = std::chrono::seconds(2);
params.retry_interval = std::chrono::seconds(1);
return params;
}
int main() {
for (int retries = 0; retries < 100; ++retries) {
std::cout << "Retry: " << retries << std::endl;
boost::asio::io_context io_context;
boost::mysql::connection_pool pool(io_context, GetParams());
pool.async_run(boost::asio::detached);
std::vector<std::thread> threads;
for (int i = 0; i < 4; i++) {
threads.emplace_back([&io_context]() { io_context.run(); });
}
// Must get 2 connections. With just 1 connection it won't deadlock.
counter = 2;
for (int i = 0; i < 2; ++i) {
GetConnection(pool, i);
}
while (true) {
if (counter == 0) {
break;
}
}
io_context.stop();
for (auto& thread : threads) {
thread.join();
}
}
} With asio::thread_pool: #include <boost/asio.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/mysql/connection_pool.hpp>
#include <iostream>
#include <thread>
std::atomic<int> counter = 0;
void GetConnection(boost::mysql::connection_pool& pool, int index) {
pool.async_get_connection(boost::asio::cancel_after(
std::chrono::seconds(6),
[index](boost::mysql::error_code error, boost::mysql::pooled_connection /*connection*/) {
if (error) {
std::cout << error.what() << std::endl;
return;
}
std::cout << index << std::endl;
--counter;
}));
}
boost::mysql::pool_params GetParams() {
boost::mysql::pool_params params;
params.server_address.emplace_host_and_port("127.0.0.1", 57761);
params.username = "root";
params.password = "root";
params.database = "test_0";
params.max_size = 4;
params.initial_size = 4;
params.thread_safe = true;
params.multi_queries = true;
params.connect_timeout = std::chrono::seconds(2);
params.retry_interval = std::chrono::seconds(1);
return params;
}
int main() {
for (int retries = 0; retries < 100; ++retries) {
std::cout << "Retry: " << retries << std::endl;
boost::asio::thread_pool thread_pool(4);
boost::mysql::connection_pool pool(thread_pool, GetParams());
pool.async_run(boost::asio::detached);
// Must get 2 connections. With just 1 connection it won't deadlock.
counter = 2;
for (int i = 0; i < 2; ++i) {
GetConnection(pool, i);
}
while (true) {
if (counter == 0) {
break;
}
}
thread_pool.stop();
thread_pool.join();
}
} Same deadlock happens with both versions. |
After running and inspecting the code, there's a race condition with
The scenario above modifies the timer concurrently from both T1 and T2, causing undefined behavior. The correct way to handle this is by using a void GetConnection(boost::mysql::connection_pool& pool, int index)
{
// Create a strand for the task
auto s = boost::asio::make_strand(pool.get_executor());
// Run the initiation within the strand
boost::asio::dispatch(boost::asio::bind_executor(s, [&pool, s, index]() {
pool.async_get_connection(boost::asio::cancel_after(
std::chrono::seconds(6),
boost::asio::bind_executor(
s,
[index](boost::mysql::error_code error, boost::mysql::pooled_connection /*connection*/) {
if (error)
{
std::cout << error.what() << std::endl;
return;
}
std::cout << index << std::endl;
--counter;
}
)
));
}));
} Could you please test it and verify whether it works for you? Some points in the code above:
Unfortunately, the timer in I hope this helps. Regards, |
Also, note that all the machinery required to support multi-threading comes at a performance cost. You might consider alternatives, like running an |
Thank you so much! It worked, I can't reproduce a deadlock now. What is odd is that I compiled it with -fsanitize=thread and it didn't accuse any warnings. I think this behavior should be documented in the examples, so that more people won't fall into this pitfall like me!
By having a single thread per io_context, I can only have one thread per pool? I think for my use-case, the database itself will be the bottleneck (write/read QPS), but I will give it a try. Thanks again for your help! |
To add to the above, when using the connection with async_execute, I was able to reproduce the deadlock again: #include <boost/asio.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/mysql/connection_pool.hpp>
#include <iostream>
#include <thread>
std::atomic<int> counter = 0;
void GetConnection(boost::mysql::connection_pool& pool, int index) {
auto strand = boost::asio::make_strand(pool.get_executor());
boost::asio::dispatch(strand, [&pool, index, strand]() mutable {
pool.async_get_connection(boost::asio::cancel_after(
std::chrono::seconds(6),
boost::asio::bind_executor(
strand, [index, strand](boost::mysql::error_code error,
boost::mysql::pooled_connection connection) mutable {
if (error) {
std::cout << error.what() << std::endl;
return;
}
// Don't really need to dispatch here since this executes in the strand,
// but just in case..
boost::asio::dispatch(strand, [connection = std::move(connection), strand,
index]() mutable {
auto results = std::make_unique<boost::mysql::results>();
connection->async_execute(
"SELECT 1", *results,
boost::asio::bind_executor(
strand, boost::asio::cancel_after(
std::chrono::seconds(6), [results = std::move(results), index,
connection = std::move(connection)](
boost::mysql::error_code error) {
if (error) {
std::cout << error.what() << std::endl;
return;
}
std::cout << "Got result " << index << std::endl;
counter--;
})));
});
})));
});
}
boost::mysql::pool_params GetParams() {
boost::mysql::pool_params params;
params.server_address.emplace_host_and_port("127.0.0.1", 57761);
params.username = "root";
params.password = "root";
params.database = "test_0";
params.max_size = 4;
params.initial_size = 4;
params.thread_safe = true;
params.multi_queries = true;
params.connect_timeout = std::chrono::seconds(2);
params.retry_interval = std::chrono::seconds(1);
return params;
}
int main() {
for (int retries = 0; retries < 100; ++retries) {
std::cout << "Retry: " << retries << std::endl;
boost::asio::thread_pool thread_pool(4);
boost::mysql::connection_pool pool(thread_pool, GetParams());
pool.async_run(boost::asio::detached);
// Must get 2 connections. With just 1 connection it won't deadlock.
counter = 2;
for (int i = 0; i < 2; ++i) {
GetConnection(pool, i);
}
while (true) {
if (counter == 0) {
break;
}
}
thread_pool.stop();
thread_pool.join();
}
} I am dispatching everything to the strand. Am I missing something? Thanks in advance |
Yup, you got the order of bind_executor/cancel_after wrong in async_execute. This is how it should be: connection->async_execute(
"SELECT 1",
*results,
boost::asio::cancel_after(
std::chrono::seconds(6),
boost::asio::bind_executor(
strand,
[results = std::move(results),
index,
connection = std::move(connection)](boost::mysql::error_code error) {
if (error)
{
std::cout << error.what() << std::endl;
return;
}
std::printf("Got result %d\n", index);
counter--;
}
)
)
); By doing this, cancel_after sees the bound executor and uses it. Could you please run the code with this change and confirm? Thanks, |
Dang, thanks for pointing that out! Still wrapping my head around asio and completion tokens :D Would this also be an issue if I were using coroutines? This is the final version that works (IIUC connection->async_execute doesn't need dispatching since the handler is being executed within the strand): void GetConnection(boost::mysql::connection_pool& pool, int index) {
auto strand = boost::asio::make_strand(pool.get_executor());
boost::asio::dispatch(strand, [&pool, index, strand]() mutable {
pool.async_get_connection(boost::asio::cancel_after(
std::chrono::seconds(6),
boost::asio::bind_executor(strand, [index, strand](
boost::mysql::error_code error,
boost::mysql::pooled_connection connection) mutable {
if (error) {
std::cout << error.what() << std::endl;
return;
}
auto results = std::make_unique<boost::mysql::results>();
connection->async_execute(
"SELECT 1", *results,
boost::asio::cancel_after(
std::chrono::seconds(6),
boost::asio::bind_executor(
strand, [results = std::move(results), index,
connection = std::move(connection)](boost::mysql::error_code error) {
if (error) {
std::cout << error.what() << std::endl;
return;
}
std::cout << "Got result " << index << std::endl;
counter--;
})));
})));
});
} Thank you!!! (please feel free to close this issue) |
If you spawn your coroutine on a strand, all operations will run as-if bound to that strand. So this should be safe: asio::awaitable<void> GetConnection(boost::mysql::connection_pool& pool, int index) {
// We're already running in the strand here
auto conn = co_await pool.async_get_connection(asio::cancel_after(6s));
// Check errors
mysql::results r;
co_await conn->async_execute("SELECT 1", r);
// Check errors
} As long as you pass a strand to I'll leave this open to document what you found, since I agree it's counter-intuitive behavior. |
Should you need any more help, let me know. |
Hi!
I think I may have found a deadlock that I boiled down to
cancel_after
. Here is the minimal code:Compiled with:
$ clang++ -std=gnu++17 -stdlib=libc++ test.cpp -lboost_thread -lboost_charconv -lssl -lcrypto -g
The output is something like:
When I remove the
cancel_after
, it never gets stuck, and it works as expected (but no deadline is set).Am I misusing the
cancel_after
? Or am I doing something unexpected to cause this deadlock?Thanks in advance
The text was updated successfully, but these errors were encountered: