polardbxengine/router/tests/component/test_routing.cc

549 lines
20 KiB
C++

/*
Copyright (c) 2017, 2019, Oracle and/or its affiliates. All rights reserved.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2.0,
as published by the Free Software Foundation.
This program is also distributed with certain software (including
but not limited to OpenSSL) that is licensed under separate terms,
as designated in a particular file or component or in included license
documentation. The authors of MySQL hereby grant you an additional
permission to link the program and your derivative works with the
separately licensed software that they have included with MySQL.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <chrono>
#include <cstring>
#include <stdexcept>
#include <thread>
#include <typeinfo>
#include <gmock/gmock.h>
#include "router_config.h" // defines HAVE_PRLIMIT
#ifdef HAVE_PRLIMIT
#include <sys/resource.h> // prlimit()
#endif
#ifndef _WIN32
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/file.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#else
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#endif
#include "mysql_session.h"
#include "router_component_test.h"
#include "router_test_helpers.h"
#include "socket_operations.h"
#include "tcp_port_pool.h"
using namespace std::chrono_literals;
using mysql_harness::SocketOperations;
using mysqlrouter::MySQLSession;
class RouterRoutingTest : public RouterComponentTest {
protected:
TcpPortPool port_pool_;
};
TEST_F(RouterRoutingTest, RoutingOk) {
const auto server_port = port_pool_.get_next_available();
const auto router_port = port_pool_.get_next_available();
// use the json file that adds additional rows to the metadata to increase the
// packet size to +10MB to verify routing of the big packets
const std::string json_stmts =
get_data_dir().join("bootstrap_big_data.js").str();
TempDirectory bootstrap_dir;
// launch the server mock for bootstrapping
auto &server_mock = launch_mysql_server_mock(
json_stmts, server_port,
false /*expecting huge data, can't print on the console*/);
const std::string routing_section =
"[routing:basic]\n"
"bind_port = " +
std::to_string(router_port) +
"\n"
"mode = read-write\n"
"destinations = 127.0.0.1:" +
std::to_string(server_port) + "\n";
TempDirectory conf_dir("conf");
std::string conf_file = create_config_file(conf_dir.name(), routing_section);
// launch the router with simple static routing configuration
auto &router_static = launch_router({"-c", conf_file});
// wait for both to begin accepting the connections
ASSERT_NO_FATAL_FAILURE(check_port_ready(server_mock, server_port));
ASSERT_NO_FATAL_FAILURE(check_port_ready(router_static, router_port));
// launch another router to do the bootstrap connecting to the mock server
// via first router instance
auto &router_bootstrapping = launch_router({
"--bootstrap=localhost:" + std::to_string(router_port),
"--report-host",
"dont.query.dns",
"-d",
bootstrap_dir.name(),
});
router_bootstrapping.register_response(
"Please enter MySQL password for root: ", "fake-pass\n");
ASSERT_NO_FATAL_FAILURE(check_exit_code(router_bootstrapping, EXIT_SUCCESS));
ASSERT_TRUE(router_bootstrapping.expect_output(
"MySQL Router configured for the InnoDB cluster 'test'"))
<< "bootstrap output: " << router_bootstrapping.get_full_output()
<< std::endl
<< "routing log: " << router_bootstrapping.get_full_logfile() << std::endl
<< "server output: " << server_mock.get_full_output() << std::endl;
}
TEST_F(RouterRoutingTest, RoutingTooManyConnections) {
const auto server_port = port_pool_.get_next_available();
const auto router_port = port_pool_.get_next_available();
// doesn't really matter which file we use here, we are not going to do any
// queries
const std::string json_stmts =
get_data_dir().join("bootstrap_big_data.js").str();
// launch the server mock
auto &server_mock = launch_mysql_server_mock(json_stmts, server_port, false);
// create a config with routing that has max_connections == 2
const std::string routing_section =
"[routing:basic]\n"
"bind_port = " +
std::to_string(router_port) +
"\n"
"mode = read-write\n"
"max_connections = 2\n"
"destinations = 127.0.0.1:" +
std::to_string(server_port) + "\n";
TempDirectory conf_dir("conf");
std::string conf_file = create_config_file(conf_dir.name(), routing_section);
// launch the router with the created configuration
auto &router = launch_router({"-c", conf_file});
// wait for server and router to begin accepting the connections
ASSERT_NO_FATAL_FAILURE(check_port_ready(server_mock, server_port));
ASSERT_NO_FATAL_FAILURE(check_port_ready(router, router_port));
// try to create 3 connections, the third should fail
// because of the max_connections limit being exceeded
mysqlrouter::MySQLSession client1, client2, client3;
EXPECT_NO_THROW(client1.connect("127.0.0.1", router_port, "username",
"password", "", ""));
EXPECT_NO_THROW(client2.connect("127.0.0.1", router_port, "username",
"password", "", ""));
ASSERT_THROW_LIKE(
client3.connect("127.0.0.1", router_port, "username", "password", "", ""),
std::runtime_error, "Too many connections to MySQL Router (1040)");
}
template <class T>
::testing::AssertionResult ThrowsExceptionWith(std::function<void()> callable,
const char *expected_text) {
try {
callable();
return ::testing::AssertionFailure()
<< "Expected exception to throw, but it didn't";
} catch (const T &e) {
if (nullptr == ::strstr(e.what(), expected_text)) {
return ::testing::AssertionFailure()
<< "Expected exception-text to contain: " << expected_text
<< ". Actual: " << e.what();
}
return ::testing::AssertionSuccess();
} catch (...) {
// as T may be std::exception we can't use it as default case and need to do
// this extra round
try {
throw;
} catch (const std::exception &e) {
return ::testing::AssertionFailure()
<< "Expected exception of type " << typeid(T).name()
<< ". Actual: " << typeid(e).name();
} catch (...) {
return ::testing::AssertionFailure()
<< "Expected exception of type " << typeid(T).name()
<< ". Actual: non-std exception";
}
}
}
// this test uses OS-specific methods to restrict thread creation
#ifdef HAVE_PRLIMIT
TEST_F(RouterRoutingTest, RoutingPluginCantSpawnMoreThreads) {
const auto server_port = port_pool_.get_next_available();
const auto router_port = port_pool_.get_next_available();
// doesn't really matter which file we use here, we are not going to do any
// queries
const std::string json_stmts =
get_data_dir().join("bootstrap_big_data.js").str();
// launch the server mock
auto &server_mock = launch_mysql_server_mock(json_stmts, server_port, false);
// create a basic config
//
// DEBUG is needed to synchronize with 'Running.' from the Loader::main_loop()
// to get a stable test.
const std::string routing_section =
"[logger]\n"
"level = DEBUG\n"
"[routing:basic]\n"
"bind_port = " +
std::to_string(router_port) +
"\n"
"mode = read-write\n"
"destinations = 127.0.0.1:" +
std::to_string(server_port) + "\n";
TempDirectory conf_dir("conf");
std::string conf_file = create_config_file(conf_dir.name(), routing_section);
SCOPED_TRACE("// launch the router with the created configuration");
auto &router_static = launch_router({"-c", conf_file});
SCOPED_TRACE("// capture current NPROC");
pid_t pid = router_static.get_pid();
struct rlimit old_limit;
EXPECT_EQ(0, prlimit(pid, RLIMIT_NPROC, nullptr, &old_limit));
SCOPED_TRACE(
"// wait for server and router to begin accepting the connections");
ASSERT_NO_FATAL_FAILURE(check_port_ready(server_mock, server_port));
ASSERT_NO_FATAL_FAILURE(check_port_ready(router_static, router_port));
// without waiting we may otherwise hit a race between the
// signal-handler-thread and the plugin-start thread and get different
// test-scenario which leads to "exit-code 1"
SCOPED_TRACE("// wait for Loader::main_loop() to start");
EXPECT_TRUE(find_in_file(
get_logging_dir().join("mysqlrouter.log").str(),
[](const auto &line) { return pattern_found(line, " Running."); }, 1s))
<< router_static.get_full_logfile();
SCOPED_TRACE("// reducing NPROC to 0");
{
// how many threads Router process is allowed to have. If this number is
// lower than current count, nothing will happen, but new ones will not be
// allowed to be created until count comes down below this limit. Thus 0 is
// a nice number to ensure nothing gets spawned anymore.
rlim_t max_threads = 0;
struct rlimit new_limit {
.rlim_cur = max_threads, .rlim_max = old_limit.rlim_max
};
EXPECT_EQ(0, prlimit(pid, RLIMIT_NPROC, &new_limit, nullptr));
}
// try to create a new connection which should fail as:
//
// - std::thread() in routing plugin will fail to spawn a new thread for this
// new connection
// ... which returns the client "Router couldn't spawn new threads"
//
SCOPED_TRACE(
"// opening connection which creates a new thread which should fail.");
mysqlrouter::MySQLSession client1;
EXPECT_TRUE(ThrowsExceptionWith<std::runtime_error>(
[&client1, router_port]() {
client1.connect("127.0.0.1", router_port, "username", "password", "",
"");
},
"Router couldn't spawn a new thread to service new client connection "
"(1040)"))
<< "mock: " << server_mock.get_full_logfile() << "\n"
<< "router: " << router_static.get_full_logfile();
SCOPED_TRACE("// restoring old NPROC.");
// we need to restore the old limit, otherwise ASAN can't spawn the thread
// that it needs on shutdown and crashes
EXPECT_EQ(0, prlimit(pid, RLIMIT_NPROC, &old_limit, nullptr));
SCOPED_TRACE("// stopping router and wait for shutdown.");
EXPECT_EQ(router_static.send_clean_shutdown_event(), std::error_code{});
EXPECT_NO_THROW(EXPECT_EQ(router_static.wait_for_exit(), 0)
<< router_static.get_full_logfile())
<< router_static.get_full_logfile();
SCOPED_TRACE("// check for expected content.");
EXPECT_THAT(router_static.get_full_logfile(),
::testing::ContainsRegex("Couldn't spawn a new thread to "
"service new client connection"));
}
#endif // #ifndef HAVE_PRLIMIT
#ifndef _WIN32 // named sockets are not supported on Windows;
// on Unix, they're implemented using Unix sockets
TEST_F(RouterRoutingTest, named_socket_has_right_permissions) {
/**
* @test Verify that unix socket has the required file permissions so that it
* can be connected to by all users. According to man 7 unix, only r+w
* permissions are required, but Server sets x as well, so we do the
* same.
*/
// get config dir (we will also stuff our unix socket file there)
TempDirectory bootstrap_dir;
// launch Router with unix socket
const std::string socket_file = bootstrap_dir.name() + "/sockfile";
const std::string routing_section =
"[routing:basic]\n"
"socket = " +
socket_file +
"\n"
"mode = read-write\n"
"destinations = 127.0.0.1:1234\n"; // port can be bogus
TempDirectory conf_dir("conf");
const std::string conf_file =
create_config_file(conf_dir.name(), routing_section);
launch_router({"-c", conf_file});
// loop until socket file appears and has correct permissions
auto wait_for_correct_perms = [&socket_file](int timeout_ms) {
const mode_t expected_mode = S_IFSOCK | S_IRUSR | S_IWUSR | S_IXUSR |
S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH |
S_IWOTH | S_IXOTH;
while (timeout_ms > 0) {
struct stat info;
memset(&info, 0, sizeof(info));
stat(socket_file.c_str(),
&info); // silently ignore error when file doesn't exist yet
if (info.st_mode == expected_mode) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
timeout_ms -= 10;
}
return false;
};
EXPECT_THAT(wait_for_correct_perms(5000), testing::Eq(true));
}
#endif
TEST_F(RouterRoutingTest, RoutingMaxConnectErrors) {
const auto server_port = port_pool_.get_next_available();
const auto router_port = port_pool_.get_next_available();
// json file does not actually matter in this test as we are not going to
const std::string json_stmts =
get_data_dir().join("bootstrap_big_data.js").str();
TempDirectory bootstrap_dir;
// launch the server mock for bootstrapping
auto &server_mock = launch_mysql_server_mock(
json_stmts, server_port,
false /*expecting huge data, can't print on the console*/);
const std::string routing_section =
"[routing:basic]\n"
"bind_port = " +
std::to_string(router_port) +
"\n"
"mode = read-write\n"
"destinations = 127.0.0.1:" +
std::to_string(server_port) +
"\n"
"max_connect_errors = 1\n";
TempDirectory conf_dir("conf");
std::string conf_file = create_config_file(conf_dir.name(), routing_section);
// launch the router
auto &router = launch_router({"-c", conf_file});
// wait for mock server to begin accepting the connections
ASSERT_NO_FATAL_FAILURE(check_port_ready(server_mock, server_port));
// wait for router to begin accepting the connections
// NOTE: this should cause connection/disconnection which
// should be treated as connection error and increment
// connection errors counter. This test relies on that.
ASSERT_NO_FATAL_FAILURE(check_port_ready(router, router_port));
// wait until blocking client host info appears in the log
bool res =
find_in_file(get_logging_dir().str() + "/mysqlrouter.log",
[](const std::string &line) -> bool {
return line.find("blocking client host") != line.npos;
});
ASSERT_TRUE(res) << "Did not found expected entry in log file";
// for the next connection attempt we should get an error as the
// max_connect_errors was exceeded
MySQLSession client;
EXPECT_THROW_LIKE(
client.connect("127.0.0.1", router_port, "username", "password", "", ""),
std::exception, "Too many connection errors");
}
// in following functions we use SocketOperations for convenience: it provides
// Win and Unix implemenatations where needed, thus saving us some #ifdefs
static bool connect_to_host(int &sock, uint16_t port) {
SocketOperations *so = SocketOperations::instance();
struct addrinfo hints, *ainfo;
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_PASSIVE;
int status =
getaddrinfo("127.0.0.1", std::to_string(port).c_str(), &hints, &ainfo);
if (status != 0)
throw std::runtime_error(std::string("getaddrinfo() failed: ") +
gai_strerror(status));
std::shared_ptr<void> exit_freeaddrinfo(nullptr,
[&](void *) { freeaddrinfo(ainfo); });
sock = socket(ainfo->ai_family, ainfo->ai_socktype, ainfo->ai_protocol);
if (sock < 0)
throw std::runtime_error("socket() failed: " +
std::to_string(so->get_errno()));
// return connection success
return (connect(sock, ainfo->ai_addr, ainfo->ai_addrlen) == 0);
}
static void read_until_error(int sock) {
SocketOperations *so = SocketOperations::instance();
char buf[1024];
while (true) {
int bytes_read = so->read(sock, buf, sizeof(buf));
if (bytes_read <= 0) return;
}
}
static void make_bad_connection(uint16_t port) {
SocketOperations *so = SocketOperations::instance();
// TCP-level connection phase
int sock;
if (!connect_to_host(sock, port)) return;
// MySQL protocol handshake phase
// To simplify code, instead of alternating between reading and writing
// protocol packets, we write a lot of garbage upfront, and then read whatever
// Router sends back. Router will read what we wrote in chunks, inbetween its
// writes, thinking they're replies to its handshake packets. Eventually it
// will finish the handshake with error and disconnect.
std::vector<char> bogus_data(1024, 0); // '=' is arbitrary bad value
if (so->write(sock, bogus_data.data(), bogus_data.size()) == -1)
throw std::runtime_error("write() failed: " + std::to_string(errno));
read_until_error(sock); // error triggered by Router disconnecting
}
/**
* @test
* This test Verifies that:
* 1. Router will block a misbehaving client after consecutive
* <max_connect_errors> connection errors
* 2. Router will reset its connection error counter if client establishes a
* successful connection before <max_connect_errors> threshold is hit
*/
TEST_F(RouterRoutingTest, test1) {
const uint16_t server_port = port_pool_.get_next_available();
const uint16_t router_port = port_pool_.get_next_available();
// doesn't really matter which file we use here, we are not going to do any
// queries
const std::string json_stmts =
get_data_dir().join("bootstrap_big_data.js").str();
// launch the server mock
auto &server_mock = launch_mysql_server_mock(json_stmts, server_port, false);
// create a config with max_connect_errors == 3
const std::string routing_section =
"[routing:basic]\n"
"bind_port = " +
std::to_string(router_port) +
"\n"
"mode = read-write\n"
"max_connect_errors = 3\n"
"destinations = 127.0.0.1:" +
std::to_string(server_port) + "\n";
TempDirectory conf_dir("conf");
std::string conf_file = create_config_file(conf_dir.name(), routing_section);
// launch the router with the created configuration
auto &router = launch_router({"-c", conf_file});
// wait for server and router to begin accepting the connections
ASSERT_NO_FATAL_FAILURE(check_port_ready(server_mock, server_port));
ASSERT_NO_FATAL_FAILURE(check_port_ready(router, router_port));
// we loop just for good measure, to additionally test that this behaviour is
// repeatable
for (int i = 0; i < 5; i++) {
// good connection, followed by 2 bad ones. Good one should reset the error
// counter
mysqlrouter::MySQLSession client;
EXPECT_NO_THROW(client.connect("127.0.0.1", router_port, "username",
"password", "", ""));
make_bad_connection(router_port);
make_bad_connection(router_port);
}
// make a 3rd consecutive bad connection - it should cause Router to start
// blocking us
make_bad_connection(router_port);
// we loop just for good measure, to additionally test that this behaviour is
// repeatable
for (int i = 0; i < 5; i++) {
// now trying to make a good connection should fail due to blockage
mysqlrouter::MySQLSession client;
EXPECT_THROW_LIKE(client.connect("127.0.0.1", router_port, "username",
"password", "", ""),
std::exception, "Too many connection errors");
}
}
int main(int argc, char *argv[]) {
init_windows_sockets();
ProcessManager::set_origin(Path(argv[0]).dirname());
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}