Skip to content
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

Socket timeout tidyup #3704

Merged
merged 10 commits into from
Feb 3, 2022
253 changes: 252 additions & 1 deletion nano/core_test/socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include <gtest/gtest.h>

#include <boost/asio/read.hpp>

#include <map>
#include <memory>
#include <utility>
Expand Down Expand Up @@ -358,7 +360,7 @@ TEST (socket, disconnection_of_silent_connections)
nano::node_config config;
// Increasing the timer timeout, so we don't let the connection to timeout due to the timer checker.
config.tcp_io_timeout = std::chrono::seconds::max ();
config.network_params.network.socket_dev_idle_timeout = std::chrono::seconds::max ();
config.network_params.network.idle_timeout = std::chrono::seconds::max ();
// Silent connections are connections open by external peers that don't contribute with any data.
config.network_params.network.silent_connection_tolerance_time = std::chrono::seconds{ 5 };

Expand Down Expand Up @@ -605,3 +607,252 @@ TEST (socket, concurrent_writes)
t.join ();
}
}

/**
* Check that the socket correctly handles a tcp_io_timeout during tcp connect
* Steps:
* set timeout to one second
* do a tcp connect that will block for at least a few seconds at the tcp level
* check that the connect returns error and that the correct counters have been incremented
*/
TEST (socket_timeout, connect)
{
// create one node and set timeout to 1 second
nano::system system (1);
std::shared_ptr<nano::node> node = system.nodes[0];
node->config.tcp_io_timeout = std::chrono::seconds (1);

// try to connect to an IP address that most likely does not exist and will not reply
// we want the tcp stack to not receive a negative reply, we want it to see silence and to keep trying
// I use the un-routable IP address 10.255.254.253, which is likely to not exist
boost::asio::ip::tcp::endpoint endpoint (boost::asio::ip::make_address_v6 ("::ffff:10.255.254.253"), nano::get_available_port ());

// create a client socket and try to connect to the IP address that wil not respond
auto socket = std::make_shared<nano::client_socket> (*node);
std::atomic<bool> done = false;
boost::system::error_code ec;
socket->async_connect (endpoint, [&ec, &done] (boost::system::error_code const & ec_a) {
if (ec_a)
{
ec = ec_a;
done = true;
}
});

// check that the callback was called and we got an error
ASSERT_TIMELY (6s, done == true);
ASSERT_TRUE (ec);
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_connect_error, nano::stat::dir::in));

// check that the socket was closed due to tcp_io_timeout timeout
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_io_timeout_drop, nano::stat::dir::out));
}

TEST (socket_timeout, read)
{
// create one node and set timeout to 1 second
nano::system system (1);
std::shared_ptr<nano::node> node = system.nodes[0];
node->config.tcp_io_timeout = std::chrono::seconds (2);

// create a server socket
boost::asio::ip::tcp::endpoint endpoint (boost::asio::ip::address_v6::loopback (), nano::get_available_port ());
boost::asio::ip::tcp::acceptor acceptor (system.io_ctx);
acceptor.open (endpoint.protocol ());
acceptor.bind (endpoint);
acceptor.listen (boost::asio::socket_base::max_listen_connections);

// asynchronously accept an incoming connection and create a newsock and do not send any data
boost::asio::ip::tcp::socket newsock (system.io_ctx);
acceptor.async_accept (newsock, [] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
});

// create a client socket to connect and call async_read, which should time out
auto socket = std::make_shared<nano::client_socket> (*node);
std::atomic<bool> done = false;
boost::system::error_code ec;
socket->async_connect (endpoint, [&socket, &ec, &done] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
auto buffer = std::make_shared<std::vector<uint8_t>> (1);
socket->async_read (buffer, 1, [&ec, &done] (boost::system::error_code const & ec_a, size_t size_a) {
if (ec_a)
{
ec = ec_a;
done = true;
}
});
});

// check that the callback was called and we got an error
ASSERT_TIMELY (10s, done == true);
ASSERT_TRUE (ec);
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_read_error, nano::stat::dir::in));

// check that the socket was closed due to tcp_io_timeout timeout
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_io_timeout_drop, nano::stat::dir::out));
}

TEST (socket_timeout, write)
{
// create one node and set timeout to 1 second
nano::system system (1);
std::shared_ptr<nano::node> node = system.nodes[0];
node->config.tcp_io_timeout = std::chrono::seconds (2);

// create a server socket
boost::asio::ip::tcp::endpoint endpoint (boost::asio::ip::address_v6::loopback (), nano::get_available_port ());
boost::asio::ip::tcp::acceptor acceptor (system.io_ctx);
acceptor.open (endpoint.protocol ());
acceptor.bind (endpoint);
acceptor.listen (boost::asio::socket_base::max_listen_connections);

// asynchronously accept an incoming connection and create a newsock and do not receive any data
boost::asio::ip::tcp::socket newsock (system.io_ctx);
acceptor.async_accept (newsock, [] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
});

// create a client socket and send lots of data to fill the socket queue on the local and remote side
// eventually, the all tcp queues should fill up and async_write will not be able to progress
// and the timeout should kick in and close the socket, which will cause the async_write to return an error
auto socket = std::make_shared<nano::client_socket> (*node);
std::atomic<bool> done = false;
boost::system::error_code ec;
socket->async_connect (endpoint, [&socket, &ec, &done] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
auto buffer = std::make_shared<std::vector<uint8_t>> (128 * 1024);
for (auto i = 0; i < 1024; ++i)
{
socket->async_write (nano::shared_const_buffer{ buffer }, [&ec, &done] (boost::system::error_code const & ec_a, size_t size_a) {
if (ec_a)
{
ec = ec_a;
done = true;
}
});
}
});

// check that the callback was called and we got an error
ASSERT_TIMELY (10s, done == true);
ASSERT_TRUE (ec);
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_write_error, nano::stat::dir::in));

// check that the socket was closed due to tcp_io_timeout timeout
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_io_timeout_drop, nano::stat::dir::out));
}

TEST (socket_timeout, read_overlapped)
{
// create one node and set timeout to 1 second
nano::system system (1);
std::shared_ptr<nano::node> node = system.nodes[0];
node->config.tcp_io_timeout = std::chrono::seconds (2);

// create a server socket
boost::asio::ip::tcp::endpoint endpoint (boost::asio::ip::address_v6::loopback (), nano::get_available_port ());
boost::asio::ip::tcp::acceptor acceptor (system.io_ctx);
acceptor.open (endpoint.protocol ());
acceptor.bind (endpoint);
acceptor.listen (boost::asio::socket_base::max_listen_connections);

// asynchronously accept an incoming connection and send one byte only
boost::asio::ip::tcp::socket newsock (system.io_ctx);
acceptor.async_accept (newsock, [&newsock] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
auto buffer = std::make_shared<std::vector<uint8_t>> (1);
nano::async_write (newsock, nano::shared_const_buffer (buffer), [] (boost::system::error_code const & ec_a, size_t size_a) {
debug_assert (!ec_a);
debug_assert (size_a == 1);
});
});

// create a client socket to connect and call async_read twice, the second call should time out
auto socket = std::make_shared<nano::client_socket> (*node);
std::atomic<bool> done = false;
boost::system::error_code ec;
socket->async_connect (endpoint, [&socket, &ec, &done] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
auto buffer = std::make_shared<std::vector<uint8_t>> (1);

socket->async_read (buffer, 1, [] (boost::system::error_code const & ec_a, size_t size_a) {
debug_assert (size_a == 1);
});

socket->async_read (buffer, 1, [&ec, &done] (boost::system::error_code const & ec_a, size_t size_a) {
debug_assert (size_a == 0);
if (ec_a)
{
ec = ec_a;
done = true;
}
});
});

// check that the callback was called and we got an error
ASSERT_TIMELY (10s, done == true);
ASSERT_TRUE (ec);
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_read_error, nano::stat::dir::in));

// check that the socket was closed due to tcp_io_timeout timeout
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_io_timeout_drop, nano::stat::dir::out));
}

TEST (socket_timeout, write_overlapped)
{
// create one node and set timeout to 1 second
nano::system system (1);
std::shared_ptr<nano::node> node = system.nodes[0];
node->config.tcp_io_timeout = std::chrono::seconds (2);

// create a server socket
boost::asio::ip::tcp::endpoint endpoint (boost::asio::ip::address_v6::loopback (), nano::get_available_port ());
boost::asio::ip::tcp::acceptor acceptor (system.io_ctx);
acceptor.open (endpoint.protocol ());
acceptor.bind (endpoint);
acceptor.listen (boost::asio::socket_base::max_listen_connections);

// asynchronously accept an incoming connection and read 2 bytes only
boost::asio::ip::tcp::socket newsock (system.io_ctx);
auto buffer = std::make_shared<std::vector<uint8_t>> (1);
acceptor.async_accept (newsock, [&newsock, &buffer] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
boost::asio::async_read (newsock, boost::asio::buffer (buffer->data (), buffer->size ()), [] (boost::system::error_code const & ec_a, size_t size_a) {
debug_assert (size_a == 1);
});
});

// create a client socket and send lots of data to fill the socket queue on the local and remote side
// eventually, the all tcp queues should fill up and async_write will not be able to progress
// and the timeout should kick in and close the socket, which will cause the async_write to return an error
auto socket = std::make_shared<nano::client_socket> (*node);
std::atomic<bool> done = false;
boost::system::error_code ec;
socket->async_connect (endpoint, [&socket, &ec, &done] (boost::system::error_code const & ec_a) {
debug_assert (!ec_a);
auto buffer1 = std::make_shared<std::vector<uint8_t>> (1);
auto buffer2 = std::make_shared<std::vector<uint8_t>> (128 * 1024);
socket->async_write (nano::shared_const_buffer{ buffer1 }, [] (boost::system::error_code const & ec_a, size_t size_a) {
debug_assert (size_a == 1);
});
for (auto i = 0; i < 1024; ++i)
{
socket->async_write (nano::shared_const_buffer{ buffer2 }, [&ec, &done] (boost::system::error_code const & ec_a, size_t size_a) {
if (ec_a)
{
ec = ec_a;
done = true;
}
});
}
});

// check that the callback was called and we got an error
ASSERT_TIMELY (10s, done == true);
ASSERT_TRUE (ec);
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_write_error, nano::stat::dir::in));

// check that the socket was closed due to tcp_io_timeout timeout
ASSERT_EQ (1, node->stats.count (nano::stat::type::tcp, nano::stat::detail::tcp_io_timeout_drop, nano::stat::dir::out));
}
2 changes: 0 additions & 2 deletions nano/lib/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ class network_constants
: 47000;
request_interval_ms = is_dev_network () ? 20 : 500;
cleanup_period = is_dev_network () ? std::chrono::seconds (1) : std::chrono::seconds (60);
socket_dev_idle_timeout = std::chrono::seconds (2);
idle_timeout = is_dev_network () ? cleanup_period * 15 : cleanup_period * 2;
silent_connection_tolerance_time = std::chrono::seconds (120);
syn_cookie_cutoff = std::chrono::seconds (5);
Expand Down Expand Up @@ -190,7 +189,6 @@ class network_constants
return cleanup_period * 5;
}
/** Default maximum idle time for a socket before it's automatically closed */
std::chrono::seconds socket_dev_idle_timeout;
std::chrono::seconds idle_timeout;
std::chrono::seconds silent_connection_tolerance_time;
std::chrono::seconds syn_cookie_cutoff;
Expand Down
2 changes: 1 addition & 1 deletion nano/node/bootstrap/bootstrap_connections.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ void nano::bootstrap_connections::pool_connection (std::shared_ptr<nano::bootstr
auto const & socket_l = client_a->socket;
if (!stopped && !client_a->pending_stop && !node.network.excluded_peers.check (client_a->channel->get_tcp_endpoint ()))
{
socket_l->start_timer (node.network_params.network.idle_timeout);
socket_l->set_timeout (node.network_params.network.idle_timeout);
// Push into idle deque
if (!push_front)
{
Expand Down
4 changes: 2 additions & 2 deletions nano/node/bootstrap/bootstrap_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ void nano::bootstrap_server::stop ()
void nano::bootstrap_server::receive ()
{
// Increase timeout to receive TCP header (idle server socket)
socket->timeout_set (node->network_params.network.idle_timeout);
socket->set_default_timeout_value (node->network_params.network.idle_timeout);
auto this_l (shared_from_this ());
socket->async_read (receive_buffer, 8, [this_l] (boost::system::error_code const & ec, std::size_t size_a) {
// Set remote_endpoint
Expand All @@ -188,7 +188,7 @@ void nano::bootstrap_server::receive ()
this_l->remote_endpoint = this_l->socket->remote_endpoint ();
}
// Decrease timeout to default
this_l->socket->timeout_set (this_l->node->config.tcp_io_timeout);
this_l->socket->set_default_timeout_value (this_l->node->config.tcp_io_timeout);
// Receive header
this_l->receive_header_action (ec, size_a);
});
Expand Down