387 lines
15 KiB
C++
387 lines
15 KiB
C++
/*
|
|
Copyright (c) 2018, 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 "gmock/gmock.h"
|
|
#include "keyring/keyring_manager.h"
|
|
#include "mock_server_rest_client.h"
|
|
#include "mock_server_testutils.h"
|
|
#include "mysql_session.h"
|
|
#include "mysqlrouter/rest_client.h"
|
|
#include "rest_api_testutils.h"
|
|
#include "router_component_test.h"
|
|
#include "tcp_port_pool.h"
|
|
|
|
#include <chrono>
|
|
#include <thread>
|
|
|
|
using mysqlrouter::MySQLSession;
|
|
using ::testing::PrintToString;
|
|
using namespace std::chrono_literals;
|
|
|
|
class MetadataChacheTTLTest : public RouterComponentTest {
|
|
protected:
|
|
std::string get_metadata_cache_section(
|
|
std::vector<uint16_t> metadata_server_ports,
|
|
const std::string &ttl = "0.5") {
|
|
std::string bootstrap_server_addresses;
|
|
bool use_comma = false;
|
|
for (const auto &port : metadata_server_ports) {
|
|
if (use_comma) {
|
|
bootstrap_server_addresses += ",";
|
|
} else {
|
|
use_comma = true;
|
|
}
|
|
bootstrap_server_addresses += "mysql://localhost:" + std::to_string(port);
|
|
}
|
|
return "[metadata_cache:test]\n"
|
|
"router_id=1\n"
|
|
"bootstrap_server_addresses=" +
|
|
bootstrap_server_addresses + "\n" +
|
|
"user=mysql_router1_user\n"
|
|
"connect_timeout=1\n"
|
|
"metadata_cluster=test\n" +
|
|
(ttl.empty() ? "" : std::string("ttl=" + ttl + "\n")) + "\n";
|
|
}
|
|
|
|
std::string get_metadata_cache_routing_section(uint16_t router_port,
|
|
const std::string &role,
|
|
const std::string &strategy,
|
|
const std::string &mode = "") {
|
|
std::string result =
|
|
"[routing:test_default]\n"
|
|
"bind_port=" +
|
|
std::to_string(router_port) + "\n" +
|
|
"destinations=metadata-cache://test/default?role=" + role + "\n" +
|
|
"protocol=classic\n";
|
|
|
|
if (!strategy.empty())
|
|
result += std::string("routing_strategy=" + strategy + "\n");
|
|
if (!mode.empty()) result += std::string("mode=" + mode + "\n");
|
|
|
|
return result;
|
|
}
|
|
|
|
int get_ttl_queries_count(const std::string &json_string) {
|
|
rapidjson::Document json_doc;
|
|
json_doc.Parse(json_string.c_str());
|
|
EXPECT_TRUE(json_doc.HasMember("md_query_count"));
|
|
EXPECT_TRUE(json_doc["md_query_count"].IsInt());
|
|
|
|
return json_doc["md_query_count"].GetInt();
|
|
}
|
|
|
|
bool wait_for_refresh_thread_started(const ProcessWrapper &router,
|
|
std::chrono::milliseconds timeout) {
|
|
if (getenv("WITH_VALGRIND")) {
|
|
timeout *= 10;
|
|
}
|
|
|
|
const auto MSEC_STEP = 10ms;
|
|
bool thread_started = false;
|
|
const auto started = std::chrono::steady_clock::now();
|
|
do {
|
|
const std::string log_content = router.get_full_logfile();
|
|
const std::string needle = "Starting metadata cache refresh thread";
|
|
thread_started = (log_content.find(needle) != log_content.npos);
|
|
if (!thread_started) {
|
|
auto step = std::min(timeout, MSEC_STEP);
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(step));
|
|
timeout -= step;
|
|
}
|
|
} while (!thread_started &&
|
|
timeout > std::chrono::steady_clock::now() - started);
|
|
|
|
return thread_started;
|
|
}
|
|
|
|
auto &launch_router(const std::string &temp_test_dir,
|
|
const std::string &conf_dir,
|
|
const std::string &metadata_cache_section,
|
|
const std::string &routing_section,
|
|
const int expected_exitcode,
|
|
bool wait_for_md_refresh_started = false) {
|
|
auto default_section = get_DEFAULT_defaults();
|
|
init_keyring(default_section, temp_test_dir);
|
|
|
|
// enable debug logs for better diagnostics in case of failure
|
|
std::string logger_section = "[logger]\nlevel = DEBUG\n";
|
|
|
|
if (!wait_for_md_refresh_started) {
|
|
default_section["logging_folder"] = "";
|
|
}
|
|
|
|
// launch the router
|
|
const std::string conf_file = create_config_file(
|
|
conf_dir, logger_section + metadata_cache_section + routing_section,
|
|
&default_section);
|
|
auto &router = ProcessManager::launch_router(
|
|
{"-c", conf_file}, expected_exitcode, true, false);
|
|
if (wait_for_md_refresh_started) {
|
|
bool ready = wait_for_refresh_thread_started(router, 5000ms);
|
|
EXPECT_TRUE(ready) << router.get_full_logfile();
|
|
}
|
|
|
|
return router;
|
|
}
|
|
|
|
TcpPortPool port_pool_;
|
|
};
|
|
|
|
struct MetadataTTLTestParams {
|
|
// ttl value we want to set (floating point decimal in seconds)
|
|
std::string ttl;
|
|
// how long do we run the router and count the metadata queries
|
|
std::chrono::milliseconds router_uptime;
|
|
// how many metadata queries we expect over this period
|
|
int expected_md_queries_count;
|
|
// if true expected_md_queries_count is only a minimal expected
|
|
// value, we should not check for maximum
|
|
bool at_least;
|
|
|
|
MetadataTTLTestParams(std::string ttl_,
|
|
std::chrono::milliseconds router_uptime_ = 0ms,
|
|
int expected_md_queries_count_ = 0,
|
|
bool at_least_ = false)
|
|
: ttl(ttl_),
|
|
router_uptime(router_uptime_),
|
|
expected_md_queries_count(expected_md_queries_count_),
|
|
at_least(at_least_) {}
|
|
};
|
|
|
|
std::ostream &operator<<(std::ostream &os, const MetadataTTLTestParams ¶m) {
|
|
return os << "(" << param.ttl << ", " << param.router_uptime.count() << "ms, "
|
|
<< param.expected_md_queries_count << ", " << param.at_least << ")";
|
|
}
|
|
|
|
class MetadataChacheTTLTestParam
|
|
: public MetadataChacheTTLTest,
|
|
public ::testing::WithParamInterface<MetadataTTLTestParams> {
|
|
protected:
|
|
virtual void SetUp() { MetadataChacheTTLTest::SetUp(); }
|
|
};
|
|
|
|
MATCHER_P2(IsBetween, a, b,
|
|
std::string(negation ? "isn't" : "is") + " between " +
|
|
PrintToString(a) + " and " + PrintToString(b)) {
|
|
return a <= arg && arg <= b;
|
|
}
|
|
|
|
TEST_P(MetadataChacheTTLTestParam, CheckTTLValid) {
|
|
auto test_params = GetParam();
|
|
|
|
// create and RAII-remove tmp dirs
|
|
TempDirectory temp_test_dir;
|
|
TempDirectory conf_dir("conf");
|
|
|
|
SCOPED_TRACE(
|
|
"// launch the server mock (it's our metadata server and single cluster "
|
|
"node)");
|
|
auto md_server_port = port_pool_.get_next_available();
|
|
auto md_server_http_port = port_pool_.get_next_available();
|
|
const std::string json_metadata =
|
|
get_data_dir().join("metadata_1_node_repeat.js").str();
|
|
|
|
auto &metadata_server = launch_mysql_server_mock(
|
|
json_metadata, md_server_port, EXIT_SUCCESS, false, md_server_http_port);
|
|
ASSERT_NO_FATAL_FAILURE(check_port_ready(metadata_server, md_server_port));
|
|
|
|
SCOPED_TRACE("// launch the router with metadata-cache configuration");
|
|
const auto router_port = port_pool_.get_next_available();
|
|
const std::string metadata_cache_section =
|
|
get_metadata_cache_section({md_server_port}, test_params.ttl);
|
|
const std::string routing_section = get_metadata_cache_routing_section(
|
|
router_port, "PRIMARY", "first-available");
|
|
auto &router =
|
|
launch_router(temp_test_dir.name(), conf_dir.name(),
|
|
metadata_cache_section, routing_section, EXIT_SUCCESS,
|
|
/*wait_for_md_refresh_started=*/true);
|
|
|
|
// keep the router running to see how many times it queries for metadata
|
|
std::this_thread::sleep_for(test_params.router_uptime);
|
|
|
|
// let's ask the mock how many metadata queries it got after
|
|
std::string server_globals =
|
|
MockServerRestClient(md_server_http_port).get_globals_as_json_string();
|
|
int ttl_count = get_ttl_queries_count(server_globals);
|
|
|
|
if (!test_params.at_least) {
|
|
// it is timing based test so to decrease random failures chances let's
|
|
// take some error marigin, we kverify that number of metadata queries
|
|
// falls into <expected_count-1, expected_count+1>
|
|
EXPECT_THAT(ttl_count, IsBetween(test_params.expected_md_queries_count - 1,
|
|
test_params.expected_md_queries_count + 1))
|
|
<< router.get_full_output();
|
|
} else {
|
|
// we only check that the TTL was queried at least N times
|
|
EXPECT_GE(ttl_count, test_params.expected_md_queries_count);
|
|
}
|
|
|
|
ASSERT_THAT(router.kill(), testing::Eq(0));
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(
|
|
CheckTTLIsUsedCorrectly, MetadataChacheTTLTestParam,
|
|
::testing::Values(
|
|
MetadataTTLTestParams("0.4", std::chrono::milliseconds(600), 2),
|
|
MetadataTTLTestParams("1", std::chrono::milliseconds(2500), 3),
|
|
// check that default is 0.5 if not provided:
|
|
MetadataTTLTestParams("", std::chrono::milliseconds(1750), 4),
|
|
// check that for 0 there are multiple ttl queries (we can't really
|
|
// guess how many there will be, but we should be able to safely assume
|
|
// that in 1 second it shold be at least 5 queries)
|
|
MetadataTTLTestParams("0", std::chrono::milliseconds(1000), 5,
|
|
/*at_least=*/true)));
|
|
|
|
class MetadataChacheTTLTestParamInvalid
|
|
: public MetadataChacheTTLTest,
|
|
public ::testing::WithParamInterface<MetadataTTLTestParams> {
|
|
protected:
|
|
virtual void SetUp() { MetadataChacheTTLTest::SetUp(); }
|
|
};
|
|
|
|
TEST_P(MetadataChacheTTLTestParamInvalid, CheckTTLInvalid) {
|
|
auto test_params = GetParam();
|
|
|
|
// create and RAII-remove tmp dirs
|
|
TempDirectory temp_test_dir;
|
|
TempDirectory conf_dir("conf");
|
|
|
|
// launch the server mock (it's our metadata server and single cluster node)
|
|
auto md_server_port = port_pool_.get_next_available();
|
|
auto md_server_http_port = port_pool_.get_next_available();
|
|
const std::string json_metadata =
|
|
get_data_dir().join("metadata_1_node_repeat.js").str();
|
|
|
|
auto &metadata_server = launch_mysql_server_mock(
|
|
json_metadata, md_server_port, false, md_server_http_port);
|
|
ASSERT_NO_FATAL_FAILURE(check_port_ready(metadata_server, md_server_port));
|
|
|
|
// launch the router with metadata-cache configuration
|
|
const auto router_port = port_pool_.get_next_available();
|
|
const std::string metadata_cache_section =
|
|
get_metadata_cache_section({md_server_port}, test_params.ttl);
|
|
const std::string routing_section = get_metadata_cache_routing_section(
|
|
router_port, "PRIMARY", "first-available");
|
|
auto &router =
|
|
launch_router(temp_test_dir.name(), conf_dir.name(),
|
|
metadata_cache_section, routing_section, EXIT_FAILURE,
|
|
/*wait_for_md_refresh_started=*/false);
|
|
|
|
check_exit_code(router, EXIT_FAILURE);
|
|
EXPECT_THAT(router.exit_code(), testing::Ne(0));
|
|
EXPECT_TRUE(router.expect_output(
|
|
"Configuration error: option ttl in [metadata_cache:test] needs value "
|
|
"between 0 and 3600 inclusive"));
|
|
}
|
|
|
|
INSTANTIATE_TEST_CASE_P(CheckInvalidTTLRefusesStart,
|
|
MetadataChacheTTLTestParamInvalid,
|
|
::testing::Values(MetadataTTLTestParams("-0.001"),
|
|
MetadataTTLTestParams("3600.001"),
|
|
MetadataTTLTestParams("INVALID"),
|
|
MetadataTTLTestParams("1,1")));
|
|
|
|
static size_t count_str_occurences(const std::string &s,
|
|
const std::string &needle) {
|
|
if (needle.length() == 0) return 0;
|
|
size_t result = 0;
|
|
for (size_t pos = s.find(needle); pos != std::string::npos;) {
|
|
++result;
|
|
pos = s.find(needle, pos + needle.length());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @test Checks that when for some reason the metadata server starts
|
|
* returning the information about the cluster nodes in different order we
|
|
* will not treat this as a change (Bug#29264764).
|
|
*/
|
|
TEST_F(MetadataChacheTTLTest, InstancesListUnordered) {
|
|
// create and RAII-remove tmp dirs
|
|
TempDirectory temp_test_dir;
|
|
TempDirectory conf_dir("conf");
|
|
|
|
const std::string kGroupID = "";
|
|
|
|
SCOPED_TRACE("// launch 2 server mocks");
|
|
std::vector<ProcessWrapper *> nodes;
|
|
std::vector<uint16_t> node_classic_ports;
|
|
std::vector<uint16_t> node_http_ports;
|
|
const std::string json_metadata =
|
|
get_data_dir().join("metadata_dynamic_nodes.js").str();
|
|
for (size_t i = 0; i < 2; ++i) {
|
|
node_classic_ports.push_back(port_pool_.get_next_available());
|
|
node_http_ports.push_back(port_pool_.get_next_available());
|
|
|
|
nodes.push_back(
|
|
&launch_mysql_server_mock(json_metadata, node_classic_ports[i],
|
|
EXIT_SUCCESS, false, node_http_ports[i]));
|
|
ASSERT_NO_FATAL_FAILURE(check_port_ready(*nodes[i], node_classic_ports[i]));
|
|
|
|
ASSERT_TRUE(
|
|
MockServerRestClient(node_http_ports[i]).wait_for_rest_endpoint_ready())
|
|
<< nodes[i]->get_full_output();
|
|
}
|
|
|
|
for (size_t i = 0; i < 2; ++i) {
|
|
set_mock_metadata(node_http_ports[i], kGroupID, node_classic_ports);
|
|
}
|
|
|
|
SCOPED_TRACE("// launch the router with metadata-cache configuration");
|
|
const auto router_port = port_pool_.get_next_available();
|
|
const std::string metadata_cache_section =
|
|
get_metadata_cache_section(node_classic_ports, "0.1");
|
|
const std::string routing_section = get_metadata_cache_routing_section(
|
|
router_port, "PRIMARY", "first-available");
|
|
auto &router = launch_router(temp_test_dir.name(), conf_dir.name(),
|
|
metadata_cache_section, routing_section,
|
|
EXIT_SUCCESS, true);
|
|
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
|
SCOPED_TRACE("// instruct the mocks to return nodes in reverse order");
|
|
std::vector<uint16_t> node_classic_ports_reverse(node_classic_ports.rbegin(),
|
|
node_classic_ports.rend());
|
|
for (size_t i = 0; i < 2; ++i) {
|
|
set_mock_metadata(node_http_ports[i], kGroupID, node_classic_ports_reverse,
|
|
1);
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
|
|
|
SCOPED_TRACE("// check it is not treated as a change");
|
|
const std::string needle = "Potential changes detected in cluster";
|
|
const std::string log_content = router.get_full_logfile();
|
|
|
|
// 1 is expected, that comes from the inital reading of the metadata
|
|
EXPECT_EQ(1, count_str_occurences(log_content, needle)) << log_content;
|
|
}
|
|
|
|
int main(int argc, char *argv[]) {
|
|
init_windows_sockets();
|
|
ProcessManager::set_origin(Path(argv[0]).dirname());
|
|
::testing::InitGoogleTest(&argc, argv);
|
|
return RUN_ALL_TESTS();
|
|
}
|