mirror of
https://github.com/boostorg/redis.git
synced 2026-01-19 04:42:09 +00:00
Adds Sentinel support (#345)
close #237 close #269 close #268 close #229
This commit is contained in:
committed by
GitHub
parent
00f3ec9b78
commit
bdd9c327c1
@@ -37,15 +37,20 @@ make_test(test_low_level)
|
||||
make_test(test_request)
|
||||
make_test(test_low_level_sync_sans_io)
|
||||
make_test(test_any_adapter)
|
||||
make_test(test_exec_fsm)
|
||||
make_test(test_log_to_file)
|
||||
make_test(test_conn_logging)
|
||||
make_test(test_exec_fsm)
|
||||
make_test(test_exec_one_fsm)
|
||||
make_test(test_writer_fsm)
|
||||
make_test(test_reader_fsm)
|
||||
make_test(test_connect_fsm)
|
||||
make_test(test_sentinel_resolve_fsm)
|
||||
make_test(test_run_fsm)
|
||||
make_test(test_setup_request_utils)
|
||||
make_test(test_setup_adapter)
|
||||
make_test(test_multiplexer)
|
||||
make_test(test_parse_sentinel_response)
|
||||
make_test(test_update_sentinel_list)
|
||||
|
||||
# Tests that require a real Redis server
|
||||
make_test(test_conn_quit)
|
||||
@@ -68,6 +73,7 @@ make_test(test_conversions)
|
||||
make_test(test_conn_tls)
|
||||
make_test(test_unix_sockets)
|
||||
make_test(test_conn_cancel_after)
|
||||
make_test(test_conn_sentinel)
|
||||
|
||||
# Coverage
|
||||
set(
|
||||
|
||||
@@ -54,15 +54,20 @@ local tests =
|
||||
test_request
|
||||
test_low_level_sync_sans_io
|
||||
test_any_adapter
|
||||
test_exec_fsm
|
||||
test_log_to_file
|
||||
test_conn_logging
|
||||
test_exec_fsm
|
||||
test_exec_one_fsm
|
||||
test_writer_fsm
|
||||
test_reader_fsm
|
||||
test_sentinel_resolve_fsm
|
||||
test_run_fsm
|
||||
test_connect_fsm
|
||||
test_setup_request_utils
|
||||
test_setup_adapter
|
||||
test_multiplexer
|
||||
test_parse_sentinel_response
|
||||
test_update_sentinel_list
|
||||
;
|
||||
|
||||
# Build and run the tests
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace net = boost::asio;
|
||||
|
||||
@@ -71,7 +76,6 @@ void run_coroutine_test(net::awaitable<void> op, std::chrono::steady_clock::dura
|
||||
|
||||
// Finds a value in the output of the CLIENT INFO command
|
||||
// format: key1=value1 key2=value2
|
||||
// TODO: duplicated
|
||||
std::string_view find_client_info(std::string_view client_info, std::string_view key)
|
||||
{
|
||||
std::string prefix{key};
|
||||
@@ -84,3 +88,45 @@ std::string_view find_client_info(std::string_view client_info, std::string_view
|
||||
auto const pos_end = client_info.find(' ', pos_begin);
|
||||
return client_info.substr(pos_begin, pos_end - pos_begin);
|
||||
}
|
||||
|
||||
void create_user(std::string_view port, std::string_view username, std::string_view password)
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
boost::redis::connection conn{ioc};
|
||||
|
||||
boost::redis::config cfg;
|
||||
cfg.addr.port = port;
|
||||
|
||||
// Enable the user and grant them permissions on everything
|
||||
boost::redis::request req;
|
||||
req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all");
|
||||
|
||||
bool run_finished = false, exec_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](boost::system::error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, boost::system::error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
BOOST_TEST(exec_finished);
|
||||
}
|
||||
|
||||
boost::redis::logger make_string_logger(std::string& to)
|
||||
{
|
||||
return {
|
||||
boost::redis::logger::level::info,
|
||||
[&to](boost::redis::logger::level, std::string_view msg) {
|
||||
to += msg;
|
||||
to += '\n';
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/detail/reader_fsm.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/operation.hpp>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
@@ -11,6 +12,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
// The timeout for tests involving communication to a real server.
|
||||
@@ -40,3 +42,8 @@ void run(
|
||||
// Finds a value in the output of the CLIENT INFO command
|
||||
// format: key1=value1 key2=value2
|
||||
std::string_view find_client_info(std::string_view client_info, std::string_view key);
|
||||
|
||||
// Connects to the Redis server at the given port and creates a user
|
||||
void create_user(std::string_view port, std::string_view username, std::string_view password);
|
||||
|
||||
boost::redis::logger make_string_logger(std::string& to);
|
||||
|
||||
28
test/print_node.hpp
Normal file
28
test/print_node.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_TEST_PRINT_NODE_HPP
|
||||
#define BOOST_REDIS_TEST_PRINT_NODE_HPP
|
||||
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <ostream>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
template <class String>
|
||||
std::ostream& operator<<(std::ostream& os, basic_node<String> const& nd)
|
||||
{
|
||||
return os << "node{ .data_type=" << to_string(nd.data_type)
|
||||
<< ", .aggregate_size=" << nd.aggregate_size << ", .depth=" << nd.depth
|
||||
<< ", .value=" << nd.value << "}";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::resp3
|
||||
|
||||
#endif // BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
@@ -4,8 +4,10 @@
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
@@ -72,4 +74,24 @@ logger log_fixture::make_logger()
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<resp3::node> nodes_from_resp3(
|
||||
const std::vector<std::string_view>& msgs,
|
||||
source_location loc)
|
||||
{
|
||||
std::vector<resp3::node> nodes;
|
||||
any_adapter adapter{nodes};
|
||||
|
||||
for (std::string_view resp : msgs) {
|
||||
resp3::parser p;
|
||||
system::error_code ec;
|
||||
bool done = resp3::parse(p, resp, adapter, ec);
|
||||
if (!BOOST_TEST(done))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_EQ(ec, system::error_code()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#define BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
|
||||
#include <boost/assert/source_location.hpp>
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
@@ -50,6 +52,13 @@ constexpr auto to_milliseconds(std::chrono::steady_clock::duration d)
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(d).count();
|
||||
}
|
||||
|
||||
// Creates a vector of nodes from a set of RESP3 messages.
|
||||
// Using the raw RESP values ensures that the correct
|
||||
// node tree is built, which is not always obvious
|
||||
std::vector<resp3::node> nodes_from_resp3(
|
||||
const std::vector<std::string_view>& msgs,
|
||||
source_location loc = BOOST_CURRENT_LOCATION);
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
|
||||
491
test/test_conn_sentinel.cpp
Normal file
491
test/test_conn_sentinel.cpp
Normal file
@@ -0,0 +1,491 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/tree.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "common.hpp"
|
||||
#include "print_node.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using namespace boost::redis;
|
||||
using namespace std::chrono_literals;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
// We can execute requests normally when using Sentinel run
|
||||
void test_exec()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Verify that we're connected to the master
|
||||
request req;
|
||||
req.push("ROLE");
|
||||
|
||||
generic_response resp;
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// ROLE outputs an array, 1st element should be 'master'
|
||||
BOOST_TEST(resp.has_value());
|
||||
BOOST_TEST_GE(resp.value().size(), 2u);
|
||||
BOOST_TEST_EQ(resp.value().at(1u).value, "master");
|
||||
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// We can use receive normally when using Sentinel run
|
||||
void test_receive()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
resp3::tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// Subscribe to a channel. This produces a push message on itself
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "sentinel_channel");
|
||||
|
||||
bool exec_finished = false, receive_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
});
|
||||
|
||||
conn.async_receive2([&](error_code ec2) {
|
||||
receive_finished = true;
|
||||
BOOST_TEST_EQ(ec2, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(receive_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
// We subscribed to channel 'sentinel_channel', and have 1 active subscription
|
||||
const resp3::node expected[] = {
|
||||
{resp3::type::push, 3u, 0u, "" },
|
||||
{resp3::type::blob_string, 1u, 1u, "subscribe" },
|
||||
{resp3::type::blob_string, 1u, 1u, "sentinel_channel"},
|
||||
{resp3::type::number, 1u, 1u, "1" },
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(resp.begin(), resp.end(), std::begin(expected), std::end(expected));
|
||||
}
|
||||
|
||||
// If connectivity to the Redis master fails, we can reconnect
|
||||
void test_reconnect()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Will cause the connection to fail
|
||||
request req_quit;
|
||||
req_quit.push("QUIT");
|
||||
|
||||
// Will succeed if the reconnection succeeds
|
||||
request req_ping;
|
||||
req_ping.push("PING", "sentinel_reconnect");
|
||||
req_ping.get_config().cancel_if_unresponded = false;
|
||||
|
||||
bool quit_finished = false, ping_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req_quit, ignore, [&](error_code ec1, std::size_t) {
|
||||
quit_finished = true;
|
||||
BOOST_TEST_EQ(ec1, error_code());
|
||||
conn.async_exec(req_ping, ignore, [&](error_code ec2, std::size_t) {
|
||||
ping_finished = true;
|
||||
BOOST_TEST_EQ(ec2, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(quit_finished);
|
||||
BOOST_TEST(ping_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// If a Sentinel is not reachable, we try the next one
|
||||
void test_sentinel_not_reachable()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "45678"}, // invalid
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Verify that we're connected to the master, listening at port 6380
|
||||
request req;
|
||||
req.push("PING", "test_sentinel_not_reachable");
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// Both Sentinels and masters may be protected with authorization
|
||||
void test_auth()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_pass");
|
||||
|
||||
cfg.use_setup = true;
|
||||
cfg.setup.clear();
|
||||
cfg.setup.push("HELLO", 3, "AUTH", "redis_user", "redis_pass");
|
||||
|
||||
// Verify that we're authenticated correctly
|
||||
request req;
|
||||
req.push("ACL", "WHOAMI");
|
||||
|
||||
response<std::string> resp;
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST(std::get<0>(resp).has_value());
|
||||
BOOST_TEST_EQ(std::get<0>(resp).value(), "redis_user");
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// TLS might be used with Sentinels. In our setup, nodes don't use TLS,
|
||||
// but this setting is independent from Sentinel.
|
||||
void test_tls()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
net::ssl::context ssl_ctx{net::ssl::context::tlsv13_client};
|
||||
|
||||
// The custom server uses a certificate signed by a CA
|
||||
// that is not trusted by default - skip verification.
|
||||
ssl_ctx.set_verify_mode(net::ssl::verify_none);
|
||||
|
||||
connection conn{ioc, std::move(ssl_ctx)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "36379"},
|
||||
{"localhost", "36380"},
|
||||
{"localhost", "36381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.sentinel.use_ssl = true;
|
||||
|
||||
request req;
|
||||
req.push("PING", "test_sentinel_tls");
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST(ec == error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, {}, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST(ec == net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// We can also connect to replicas
|
||||
void test_replica()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.sentinel.server_role = role::replica;
|
||||
|
||||
// Verify that we're connected to a replica
|
||||
request req;
|
||||
req.push("ROLE");
|
||||
|
||||
generic_response resp;
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// ROLE outputs an array, 1st element should be 'slave'
|
||||
BOOST_TEST(resp.has_value());
|
||||
BOOST_TEST_GE(resp.value().size(), 2u);
|
||||
BOOST_TEST_EQ(resp.value().at(1u).value, "slave");
|
||||
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// If no Sentinel is reachable, an error is issued.
|
||||
// This tests disabling reconnection with Sentinel, too.
|
||||
void test_error_no_sentinel_reachable()
|
||||
{
|
||||
// Setup
|
||||
std::string logs;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "43210"},
|
||||
{"localhost", "43211"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error
|
||||
|
||||
bool run_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, error::sentinel_resolve_failed);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
if (
|
||||
!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:43210: connection establishment error"),
|
||||
std::string::npos) ||
|
||||
!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:43211: connection establishment error"),
|
||||
std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// If Sentinel doesn't know about the configured master,
|
||||
// the appropriate error is returned
|
||||
void test_error_unknown_master()
|
||||
{
|
||||
// Setup
|
||||
std::string logs;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26380"},
|
||||
};
|
||||
cfg.sentinel.master_name = "unknown_master";
|
||||
cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error
|
||||
|
||||
bool run_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, error::sentinel_resolve_failed);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
if (!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:26380: doesn't know about the configured master"),
|
||||
std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// The same applies when connecting to replicas, too
|
||||
void test_error_unknown_master_replica()
|
||||
{
|
||||
// Setup
|
||||
std::string logs;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26380"},
|
||||
};
|
||||
cfg.sentinel.master_name = "unknown_master";
|
||||
cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error
|
||||
cfg.sentinel.server_role = role::replica;
|
||||
|
||||
bool run_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, error::sentinel_resolve_failed);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
if (!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:26380: doesn't know about the configured master"),
|
||||
std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// Create the required users in the master, replicas and sentinels
|
||||
create_user("6379", "redis_user", "redis_pass");
|
||||
create_user("6380", "redis_user", "redis_pass");
|
||||
create_user("6381", "redis_user", "redis_pass");
|
||||
create_user("26379", "sentinel_user", "sentinel_pass");
|
||||
create_user("26380", "sentinel_user", "sentinel_pass");
|
||||
create_user("26381", "sentinel_user", "sentinel_pass");
|
||||
|
||||
// Actual tests
|
||||
test_exec();
|
||||
test_receive();
|
||||
test_reconnect();
|
||||
test_sentinel_not_reachable();
|
||||
test_auth();
|
||||
test_tls();
|
||||
test_replica();
|
||||
|
||||
test_error_no_sentinel_reachable();
|
||||
test_error_unknown_master();
|
||||
test_error_unknown_master_replica();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "common.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -29,37 +28,6 @@ using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
// Creates a user with a known password. Harmless if the user already exists
|
||||
void setup_password()
|
||||
{
|
||||
// Setup
|
||||
asio::io_context ioc;
|
||||
redis::connection conn{ioc};
|
||||
|
||||
// Enable the user and grant them permissions on everything
|
||||
redis::request req;
|
||||
req.push("ACL", "SETUSER", "myuser", "on", ">mypass", "~*", "&*", "+@all");
|
||||
redis::generic_response resp;
|
||||
|
||||
bool run_finished = false, exec_finished = false;
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
|
||||
});
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(resp.has_value());
|
||||
}
|
||||
|
||||
void test_auth_success()
|
||||
{
|
||||
// Setup
|
||||
@@ -96,17 +64,13 @@ void test_auth_success()
|
||||
BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser");
|
||||
}
|
||||
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
void test_auth_failure()
|
||||
{
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
std::ostringstream oss;
|
||||
redis::logger lgr(redis::logger::level::info, [&](redis::logger::level, std::string_view msg) {
|
||||
oss << msg << '\n';
|
||||
});
|
||||
|
||||
// Setup
|
||||
std::string logs;
|
||||
asio::io_context ioc;
|
||||
redis::connection conn{ioc, std::move(lgr)};
|
||||
redis::connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
// Disable reconnection so the hello error causes the connection to exit
|
||||
auto cfg = make_test_config();
|
||||
@@ -126,9 +90,8 @@ void test_auth_failure()
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
// Check the log
|
||||
auto log = oss.str();
|
||||
if (!BOOST_TEST_NE(log.find("WRONGPASS"), std::string::npos)) {
|
||||
std::cerr << "Log was: " << log << std::endl;
|
||||
if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) {
|
||||
std::cerr << "Log was: \n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,17 +238,13 @@ void test_setup_no_hello()
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8");
|
||||
}
|
||||
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
void test_setup_failure()
|
||||
{
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
std::ostringstream oss;
|
||||
redis::logger lgr(redis::logger::level::info, [&](redis::logger::level, std::string_view msg) {
|
||||
oss << msg << '\n';
|
||||
});
|
||||
|
||||
// Setup
|
||||
std::string logs;
|
||||
asio::io_context ioc;
|
||||
redis::connection conn{ioc, std::move(lgr)};
|
||||
redis::connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
// Disable reconnection so the hello error causes the connection to exit
|
||||
auto cfg = make_test_config();
|
||||
@@ -306,9 +265,8 @@ void test_setup_failure()
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
// Check the log
|
||||
auto log = oss.str();
|
||||
if (!BOOST_TEST_NE(log.find("wrong number of arguments"), std::string::npos)) {
|
||||
std::cerr << "Log was: " << log << std::endl;
|
||||
if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +274,8 @@ void test_setup_failure()
|
||||
|
||||
int main()
|
||||
{
|
||||
setup_password();
|
||||
create_user("6379", "myuser", "mypass");
|
||||
|
||||
test_auth_success();
|
||||
test_auth_failure();
|
||||
test_database_index();
|
||||
|
||||
@@ -55,7 +55,7 @@ static config make_tls_config()
|
||||
config cfg;
|
||||
cfg.use_ssl = true;
|
||||
cfg.addr.host = get_server_hostname();
|
||||
cfg.addr.port = "6380";
|
||||
cfg.addr.port = "16380";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
@@ -103,30 +102,15 @@ auto resolver_data = [] {
|
||||
|
||||
// Reduce duplication
|
||||
struct fixture : detail::log_fixture {
|
||||
config cfg;
|
||||
buffered_logger lgr{make_logger()};
|
||||
connect_fsm fsm{cfg, lgr};
|
||||
redis_stream_state st{};
|
||||
connect_fsm fsm{lgr};
|
||||
redis_stream_state st;
|
||||
|
||||
fixture(config&& cfg = {})
|
||||
: cfg{std::move(cfg)}
|
||||
fixture(transport_type type = transport_type::tcp)
|
||||
: st{type, false}
|
||||
{ }
|
||||
};
|
||||
|
||||
config make_ssl_config()
|
||||
{
|
||||
config cfg;
|
||||
cfg.use_ssl = true;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
config make_unix_config()
|
||||
{
|
||||
config cfg;
|
||||
cfg.unix_socket = "/run/redis.sock";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void test_tcp_success()
|
||||
{
|
||||
// Setup
|
||||
@@ -141,20 +125,21 @@ void test_tcp_success()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::tcp);
|
||||
BOOST_TEST_NOT(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
// clang-format off
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
void test_tcp_tls_success()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. No SSL stream reset is performed here
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -167,21 +152,22 @@ void test_tcp_tls_success()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::tcp_tls);
|
||||
BOOST_TEST(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Successfully performed SSL handshake" },
|
||||
// clang-format off
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" },
|
||||
{logger::level::debug, "Connect: SSL handshake succeeded" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
void test_tcp_tls_success_reconnect()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
fix.st.ssl_stream_used = true;
|
||||
|
||||
// Run the algorithm. The stream is used, so it needs to be reset
|
||||
@@ -197,21 +183,22 @@ void test_tcp_tls_success_reconnect()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::tcp_tls);
|
||||
BOOST_TEST(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Successfully performed SSL handshake" },
|
||||
// clang-format off
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" },
|
||||
{logger::level::debug, "Connect: SSL handshake succeeded" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
void test_unix_success()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -222,12 +209,11 @@ void test_unix_success()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::unix_socket);
|
||||
BOOST_TEST_NOT(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Connected to /run/redis.sock"},
|
||||
{logger::level::debug, "Connect: UNIX socket connect succeeded"},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,7 +221,7 @@ void test_unix_success()
|
||||
void test_unix_success_close_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -246,12 +232,11 @@ void test_unix_success_close_error()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::unix_socket);
|
||||
BOOST_TEST_NOT(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Connected to /run/redis.sock"},
|
||||
{logger::level::debug, "Connect: UNIX socket connect succeeded"},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,7 +255,7 @@ void test_tcp_resolve_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Error resolving the server hostname: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::info, "Connect: hostname resolution failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -293,7 +278,7 @@ void test_tcp_resolve_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Error resolving the server hostname: Resolve timeout. [boost.redis:17]"},
|
||||
{logger::level::info, "Connect: hostname resolution failed: Resolve timeout. [boost.redis:17]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -349,8 +334,8 @@ void test_tcp_connect_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Failed to connect to the server: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connect: TCP connect failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -375,8 +360,8 @@ void test_tcp_connect_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Failed to connect to the server: Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connect: TCP connect failed: Connect timeout. [boost.redis:18]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -423,7 +408,7 @@ void test_tcp_connect_cancel_edge()
|
||||
void test_ssl_handshake_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. No SSL stream reset is performed here
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -441,9 +426,9 @@ void test_ssl_handshake_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Failed to perform SSL handshake: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234"},
|
||||
{logger::level::info, "Connect: SSL handshake failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -451,7 +436,7 @@ void test_ssl_handshake_error()
|
||||
void test_ssl_handshake_timeout()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. Timeout = operation_aborted without the cancel type set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -469,9 +454,9 @@ void test_ssl_handshake_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Failed to perform SSL handshake: SSL handshake timeout. [boost.redis:20]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234"},
|
||||
{logger::level::info, "Connect: SSL handshake failed: SSL handshake timeout. [boost.redis:20]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -479,7 +464,7 @@ void test_ssl_handshake_timeout()
|
||||
void test_ssl_handshake_cancel()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. Cancel = operation_aborted with the cancel type set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -501,7 +486,7 @@ void test_ssl_handshake_cancel()
|
||||
void test_ssl_handshake_cancel_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. No error, but the cancel state is set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -524,7 +509,7 @@ void test_ssl_handshake_cancel_edge()
|
||||
void test_unix_connect_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -537,7 +522,7 @@ void test_unix_connect_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Failed to connect to the server: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::info, "Connect: UNIX socket connect failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -545,7 +530,7 @@ void test_unix_connect_error()
|
||||
void test_unix_connect_timeout()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm. Timeout = operation_aborted without a cancel state
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -558,7 +543,7 @@ void test_unix_connect_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Failed to connect to the server: Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Connect: UNIX socket connect failed: Connect timeout. [boost.redis:18]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -566,7 +551,7 @@ void test_unix_connect_timeout()
|
||||
void test_unix_connect_cancel()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm. Cancel = operation_aborted with a cancel state
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -583,7 +568,7 @@ void test_unix_connect_cancel()
|
||||
void test_unix_connect_cancel_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm. No error, but cancel state is set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
|
||||
365
test/test_exec_one_fsm.cpp
Normal file
365
test/test_exec_one_fsm.cpp
Normal file
@@ -0,0 +1,365 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/detail/exec_one_fsm.hpp>
|
||||
#include <boost/redis/detail/read_buffer.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include "print_node.hpp"
|
||||
|
||||
#include <iterator>
|
||||
#include <ostream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using namespace boost::redis;
|
||||
namespace asio = boost::asio;
|
||||
using detail::exec_one_fsm;
|
||||
using detail::exec_one_action;
|
||||
using detail::exec_one_action_type;
|
||||
using detail::read_buffer;
|
||||
using boost::system::error_code;
|
||||
using boost::asio::cancellation_type_t;
|
||||
using parse_event = any_adapter::parse_event;
|
||||
using resp3::type;
|
||||
|
||||
// Operators
|
||||
static const char* to_string(exec_one_action_type value)
|
||||
{
|
||||
switch (value) {
|
||||
case exec_one_action_type::done: return "done";
|
||||
case exec_one_action_type::write: return "write";
|
||||
case exec_one_action_type::read_some: return "read_some";
|
||||
default: return "<unknown writer_action_type>";
|
||||
}
|
||||
}
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
bool operator==(const exec_one_action& lhs, const exec_one_action& rhs) noexcept
|
||||
{
|
||||
return lhs.type == rhs.type && lhs.ec == rhs.ec;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const exec_one_action& act)
|
||||
{
|
||||
os << "exec_one_action{ .type=" << to_string(act.type);
|
||||
if (act.type == exec_one_action_type::done)
|
||||
os << ", ec=" << act.ec;
|
||||
return os << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
namespace {
|
||||
|
||||
struct adapter_event {
|
||||
parse_event type;
|
||||
resp3::node node{};
|
||||
|
||||
friend bool operator==(const adapter_event& lhs, const adapter_event& rhs) noexcept
|
||||
{
|
||||
return lhs.type == rhs.type && lhs.node == rhs.node;
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const adapter_event& value)
|
||||
{
|
||||
switch (value.type) {
|
||||
case parse_event::init: return os << "adapter_event{ .type=init }";
|
||||
case parse_event::done: return os << "adapter_event{ .type=done }";
|
||||
case parse_event::node:
|
||||
return os << "adapter_event{ .type=node, .node=" << value.node << " }";
|
||||
default: return os << "adapter_event{ .type=unknown }";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
any_adapter make_snoop_adapter(std::vector<adapter_event>& events)
|
||||
{
|
||||
return any_adapter::impl_t{[&](parse_event ev, resp3::node_view const& nd, error_code&) {
|
||||
events.push_back({
|
||||
ev,
|
||||
{nd.data_type, nd.aggregate_size, nd.depth, std::string(nd.value)}
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
void copy_to(read_buffer& buff, std::string_view data)
|
||||
{
|
||||
auto const buffer = buff.get_prepared();
|
||||
BOOST_TEST_GE(buffer.size(), data.size());
|
||||
std::copy(data.cbegin(), data.cend(), buffer.begin());
|
||||
}
|
||||
|
||||
void test_success()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read the entire response in one go
|
||||
constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n";
|
||||
copy_to(buff, payload);
|
||||
act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::done);
|
||||
|
||||
// Verify the adapter calls
|
||||
const adapter_event expected[] = {
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::blob_string, 1u, 0u, "hello"}},
|
||||
{parse_event::done},
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::array, 1u, 0u, ""}},
|
||||
{parse_event::node, {type::simple_string, 1u, 1u, "goodbye"}},
|
||||
{parse_event::done},
|
||||
};
|
||||
BOOST_TEST_ALL_EQ(events.begin(), events.end(), std::begin(expected), std::end(expected));
|
||||
}
|
||||
|
||||
// The request didn't have any expected response (e.g. SUBSCRIBE)
|
||||
void test_no_expected_response()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 0u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM shouldn't ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// No adapter calls should be done
|
||||
BOOST_TEST_EQ(events.size(), 0u);
|
||||
}
|
||||
|
||||
// The response is scattered in several smaller fragments
|
||||
void test_short_reads()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read fragments
|
||||
constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n";
|
||||
copy_to(buff, payload.substr(0, 6u));
|
||||
act = fsm.resume(buff, error_code(), 6u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
copy_to(buff, payload.substr(6, 10u));
|
||||
act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
copy_to(buff, payload.substr(16));
|
||||
act = fsm.resume(buff, error_code(), payload.substr(16).size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::done);
|
||||
|
||||
// Verify the adapter calls
|
||||
const adapter_event expected[] = {
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::blob_string, 1u, 0u, "hello"}},
|
||||
{parse_event::done},
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::array, 1u, 0u, ""}},
|
||||
{parse_event::node, {type::simple_string, 1u, 1u, "goodbye"}},
|
||||
{parse_event::done},
|
||||
};
|
||||
BOOST_TEST_ALL_EQ(events.begin(), events.end(), std::begin(expected), std::end(expected));
|
||||
}
|
||||
|
||||
// Errors in write
|
||||
void test_write_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// Write error
|
||||
act = fsm.resume(buff, asio::error::connection_reset, 10u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::connection_reset));
|
||||
}
|
||||
|
||||
void test_write_cancel()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// Edge case where the operation finished successfully but with the cancellation state set
|
||||
act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
}
|
||||
|
||||
// Errors in read
|
||||
void test_read_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read error
|
||||
act = fsm.resume(buff, asio::error::network_reset, 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::network_reset));
|
||||
}
|
||||
|
||||
void test_read_cancelled()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Edge case where the operation finished successfully but with the cancellation state set
|
||||
copy_to(buff, "$5\r\n");
|
||||
act = fsm.resume(buff, error_code(), 4u, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
}
|
||||
|
||||
// Buffer too small
|
||||
void test_buffer_prepare_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
buff.set_config({4096u, 8u}); // max size is 8 bytes
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// When preparing the buffer, we encounter an error
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::exceeds_maximum_read_buffer_size));
|
||||
}
|
||||
|
||||
// An invalid RESP3 message
|
||||
void test_parse_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// The response contains an invalid message
|
||||
constexpr std::string_view payload = "$bad\r\n";
|
||||
copy_to(buff, payload);
|
||||
act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::not_a_number));
|
||||
}
|
||||
|
||||
// Adapter signals an error
|
||||
void test_adapter_error()
|
||||
{
|
||||
// Setup. The adapter will fail in the 2nd node
|
||||
any_adapter adapter{[](parse_event ev, resp3::node_view const&, error_code& ec) {
|
||||
if (ev == parse_event::node)
|
||||
ec = error::empty_field;
|
||||
}};
|
||||
exec_one_fsm fsm{std::move(adapter), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read the entire response in one go
|
||||
constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n";
|
||||
copy_to(buff, payload);
|
||||
act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::empty_field));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_success();
|
||||
test_no_expected_response();
|
||||
test_short_reads();
|
||||
|
||||
test_write_error();
|
||||
test_write_cancel();
|
||||
|
||||
test_read_error();
|
||||
test_read_cancelled();
|
||||
|
||||
test_buffer_prepare_error();
|
||||
test_parse_error();
|
||||
test_adapter_error();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -530,6 +530,11 @@ BOOST_AUTO_TEST_CASE(cover_error)
|
||||
check_error("boost.redis", boost::redis::error::resp3_hello);
|
||||
check_error("boost.redis", boost::redis::error::exceeds_maximum_read_buffer_size);
|
||||
check_error("boost.redis", boost::redis::error::write_timeout);
|
||||
check_error("boost.redis", boost::redis::error::sentinel_unix_sockets_unsupported);
|
||||
check_error("boost.redis", boost::redis::error::sentinel_resolve_failed);
|
||||
check_error("boost.redis", boost::redis::error::role_check_failed);
|
||||
check_error("boost.redis", boost::redis::error::expects_resp3_string);
|
||||
check_error("boost.redis", boost::redis::error::expects_resp3_array);
|
||||
}
|
||||
|
||||
std::string get_type_as_str(boost::redis::resp3::type t)
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include "print_node.hpp"
|
||||
|
||||
#define BOOST_TEST_MODULE low_level_sync_sans_io
|
||||
#include <boost/test/included/unit_test.hpp>
|
||||
|
||||
@@ -337,25 +339,15 @@ BOOST_AUTO_TEST_CASE(check_counter_adapter)
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
template <class String>
|
||||
std::ostream& operator<<(std::ostream& os, basic_node<String> const& nd)
|
||||
{
|
||||
os << "type: " << to_string(nd.data_type) << "\n"
|
||||
<< "aggregate_size: " << nd.aggregate_size << "\n"
|
||||
<< "depth: " << nd.depth << "\n"
|
||||
<< "value: " << nd.value << "\n";
|
||||
return os;
|
||||
}
|
||||
|
||||
template <class String>
|
||||
std::ostream& operator<<(std::ostream& os, basic_tree<String> const& resp)
|
||||
{
|
||||
for (auto const& e: resp)
|
||||
for (auto const& e : resp)
|
||||
os << e << ",";
|
||||
return os;
|
||||
}
|
||||
|
||||
}
|
||||
} // namespace boost::redis::resp3
|
||||
|
||||
node from_node_view(node_view const& v)
|
||||
{
|
||||
@@ -370,7 +362,7 @@ node from_node_view(node_view const& v)
|
||||
tree from_flat(flat_tree const& resp)
|
||||
{
|
||||
tree ret;
|
||||
for (auto const& e: resp.get_view())
|
||||
for (auto const& e : resp.get_view())
|
||||
ret.push_back(from_node_view(e));
|
||||
|
||||
return ret;
|
||||
@@ -379,13 +371,12 @@ tree from_flat(flat_tree const& resp)
|
||||
tree from_flat(generic_flat_response const& resp)
|
||||
{
|
||||
tree ret;
|
||||
for (auto const& e: resp.value().get_view())
|
||||
for (auto const& e : resp.value().get_view())
|
||||
ret.push_back(from_node_view(e));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
// Parses the same data into a tree and a
|
||||
// flat_tree, they should be equal to each other.
|
||||
BOOST_AUTO_TEST_CASE(flat_tree_views_are_set)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "print_node.hpp"
|
||||
#include "sansio_utils.hpp"
|
||||
|
||||
#include <iostream>
|
||||
@@ -33,17 +34,6 @@ using boost::redis::response;
|
||||
using boost::redis::any_adapter;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, node const& nd)
|
||||
{
|
||||
return os << "node{ .data_type=" << to_string(nd.data_type)
|
||||
<< ", .aggregate_size=" << nd.aggregate_size << ", .depth=" << nd.depth
|
||||
<< ", .value=" << nd.value << "}";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::resp3
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, consume_result v)
|
||||
|
||||
727
test/test_parse_sentinel_response.cpp
Normal file
727
test/test_parse_sentinel_response.cpp
Normal file
@@ -0,0 +1,727 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include "sansio_utils.hpp"
|
||||
|
||||
#include <initializer_list>
|
||||
#include <ostream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using namespace boost::redis;
|
||||
using detail::nodes_from_resp3;
|
||||
using detail::parse_sentinel_response;
|
||||
using detail::sentinel_response;
|
||||
using boost::system::error_code;
|
||||
|
||||
// Operators
|
||||
namespace boost::redis {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const address& addr)
|
||||
{
|
||||
return os << "address{ .host=" << addr.host << ", .port=" << addr.port << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
namespace {
|
||||
|
||||
struct fixture {
|
||||
sentinel_response resp{
|
||||
"leftover",
|
||||
{"leftover_host", "6543"},
|
||||
{address()},
|
||||
{address()},
|
||||
};
|
||||
|
||||
void check_response(
|
||||
const address& expected_master_addr,
|
||||
boost::span<const address> expected_replicas,
|
||||
boost::span<const address> expected_sentinels,
|
||||
boost::source_location loc = BOOST_CURRENT_LOCATION) const
|
||||
{
|
||||
if (!BOOST_TEST_EQ(resp.diagnostic, ""))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_EQ(resp.master_addr, expected_master_addr))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_ALL_EQ(
|
||||
resp.replicas.begin(),
|
||||
resp.replicas.end(),
|
||||
expected_replicas.begin(),
|
||||
expected_replicas.end()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_ALL_EQ(
|
||||
resp.sentinels.begin(),
|
||||
resp.sentinels.end(),
|
||||
expected_sentinels.begin(),
|
||||
expected_sentinels.end()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
// Usual response when asking for a master
|
||||
void test_master()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n334\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n134\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, {}, expected_sentinels);
|
||||
}
|
||||
|
||||
// Works correctly even if no Sentinels are present
|
||||
void test_master_no_sentinels()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
fix.check_response({"localhost", "6380"}, {}, {});
|
||||
}
|
||||
|
||||
// The responses corresponding to the user-defined setup request are ignored
|
||||
void test_master_setup_request()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"+OK\r\n",
|
||||
"%6\r\n$6\r\nserver\r\n$5\r\nredis\r\n$7\r\nversion\r\n$5\r\n7.4.2\r\n$5\r\nproto\r\n:3\r\n$2\r\nid\r\n:3\r\n$4\r\nmode\r\n$8\r\nsentinel\r\n$7\r\nmodules\r\n*0\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n334\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n134\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, {}, expected_sentinels);
|
||||
}
|
||||
|
||||
// IP and port can be out of order
|
||||
void test_master_ip_port_out_of_order()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$4\r\nport\r\n$5\r\n26380\r\n$2\r\nip\r\n$8\r\nhost.one\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, {}, expected_sentinels);
|
||||
}
|
||||
|
||||
// Usual response when asking for a replica
|
||||
void test_replica()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%21\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n$4\r\nport\r\n$4\r\n6381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\ncdfa33e2d39958c0b10c0391c0c3d4ab096edfeb\r\n$5\r\nflags\r\n$5\r\nslave\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n134\r\n$15\r\nlast-ping-reply\r\n$3\r\n134\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$12\r\ninfo-refresh\r\n$4\r\n5302\r\n$13\r\nrole-reported\r\n$5\r\nslave\r\n$18\r\nrole-reported-time\r\n$6\r\n442121\r\n"
|
||||
"$21\r\nmaster-link-down-time\r\n$1\r\n0\r\n$18\r\nmaster-link-status\r\n$2\r\nok\r\n$11\r\nmaster-host\r\n$9\r\nlocalhost\r\n"
|
||||
"$11\r\nmaster-port\r\n$4\r\n6380\r\n$14\r\nslave-priority\r\n$3\r\n100\r\n$17\r\nslave-repl-offset\r\n$5\r\n29110\r\n"
|
||||
"$17\r\nreplica-announced\r\n$1\r\n1\r\n"
|
||||
"%21\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6382\r\n$2\r\nip\r\n$9\r\ntest.host\r\n$4\r\nport\r\n$4\r\n6382\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\n11bfea62c25316e211fdf0e1ccd2dbd920e90815\r\n$5\r\nflags\r\n$5\r\nslave\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n134\r\n$15\r\nlast-ping-reply\r\n$3\r\n134\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$12\r\ninfo-refresh\r\n$4\r\n5302\r\n$13\r\nrole-reported\r\n$5\r\nslave\r\n$18\r\nrole-reported-time\r\n$6\r\n442132\r\n"
|
||||
"$21\r\nmaster-link-down-time\r\n$1\r\n0\r\n$18\r\nmaster-link-status\r\n$2\r\nok\r\n$11\r\nmaster-host\r\n$9\r\nlocalhost\r\n"
|
||||
"$11\r\nmaster-port\r\n$4\r\n6380\r\n$14\r\nslave-priority\r\n$3\r\n100\r\n$17\r\nslave-repl-offset\r\n$5\r\n29110\r\n"
|
||||
"$17\r\nreplica-announced\r\n$1\r\n1\r\n",
|
||||
"*2\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n334\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n134\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
{"test.host", "6382"},
|
||||
};
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, expected_replicas, expected_sentinels);
|
||||
}
|
||||
|
||||
// Like the master case
|
||||
void test_replica_no_sentinels()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n$4\r\nport\r\n$4\r\n6381\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6382\r\n$2\r\nip\r\n$9\r\ntest.host\r\n$4\r\nport\r\n$4\r\n6382\r\n",
|
||||
"*0\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
{"test.host", "6382"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, expected_replicas, {});
|
||||
}
|
||||
|
||||
// Asking for replicas, but there is none
|
||||
void test_replica_no_replicas()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
"*0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
fix.check_response({"localhost", "6380"}, {}, {});
|
||||
}
|
||||
|
||||
// Setup requests work with replicas, too
|
||||
void test_replica_setup_request()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n+OK\r\n+OK\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n$4\r\nport\r\n$4\r\n6381\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6382\r\n$2\r\nip\r\n$9\r\ntest.host\r\n$4\r\nport\r\n$4\r\n6382\r\n",
|
||||
"*2\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
{"test.host", "6382"},
|
||||
};
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, expected_replicas, expected_sentinels);
|
||||
}
|
||||
|
||||
// IP and port can be out of order
|
||||
void test_replica_ip_port_out_of_order()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6389\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$4\r\nport\r\n$4\r\n6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n",
|
||||
"*0\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
};
|
||||
fix.check_response({"test.host", "6389"}, expected_replicas, {});
|
||||
}
|
||||
|
||||
void test_errors()
|
||||
{
|
||||
const struct {
|
||||
std::string_view name;
|
||||
role server_role;
|
||||
std::vector<std::string_view> responses;
|
||||
std::string_view expected_diagnostic;
|
||||
error_code expected_ec;
|
||||
} test_cases[]{
|
||||
// clang-format off
|
||||
{
|
||||
// A RESP3 simple error
|
||||
"setup_error_simple",
|
||||
role::master,
|
||||
{
|
||||
"-WRONGPASS invalid username-password pair or user is disabled.\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"WRONGPASS invalid username-password pair or user is disabled.",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// A RESP3 blob error
|
||||
"setup_error_blob",
|
||||
role::master,
|
||||
{
|
||||
"!3\r\nBad\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"Bad",
|
||||
error::resp3_blob_error
|
||||
},
|
||||
{
|
||||
// Errors in intermediate nodes of the user-supplied request
|
||||
"setup_error_intermediate",
|
||||
role::master,
|
||||
{
|
||||
"+OK\r\n",
|
||||
"-Something happened!\r\n",
|
||||
"+OK\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"Something happened!",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// Only the first error is processed (e.g. auth failure may cause subsequent cmds to fail)
|
||||
"setup_error_intermediate",
|
||||
role::master,
|
||||
{
|
||||
"-Something happened!\r\n",
|
||||
"-Something worse happened!\r\n",
|
||||
"-Bad\r\n",
|
||||
"-Worse\r\n",
|
||||
},
|
||||
"Something happened!",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// This works for replicas, too
|
||||
"setup_error_replicas",
|
||||
role::replica,
|
||||
{
|
||||
"-Something happened!\r\n",
|
||||
"-Something worse happened!\r\n",
|
||||
"-Bad\r\n",
|
||||
"-Worse\r\n",
|
||||
},
|
||||
"Something happened!",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME
|
||||
{
|
||||
// Unknown master. This returns NULL and causes SENTINEL SENTINELS to fail
|
||||
"getmasteraddr_unknown_master",
|
||||
role::master,
|
||||
{
|
||||
"_\r\n",
|
||||
"-ERR Unknown master\r\n",
|
||||
},
|
||||
"",
|
||||
error::resp3_null
|
||||
},
|
||||
{
|
||||
// The request errors for any other reason
|
||||
"getmasteraddr_error",
|
||||
role::master,
|
||||
{
|
||||
"-ERR something happened\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"ERR something happened",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// Same, for replicas
|
||||
"getmasteraddr_unknown_master_replica",
|
||||
role::replica,
|
||||
{
|
||||
"_\r\n",
|
||||
"-ERR Unknown master\r\n",
|
||||
"-ERR Unknown master\r\n",
|
||||
},
|
||||
"",
|
||||
error::resp3_null
|
||||
},
|
||||
{
|
||||
// Root node should be a list
|
||||
"getmasteraddr_not_array",
|
||||
role::master,
|
||||
{
|
||||
"+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_array
|
||||
},
|
||||
{
|
||||
// Root node should have exactly 2 elements
|
||||
"getmasteraddr_array_size_1",
|
||||
role::master,
|
||||
{
|
||||
"*1\r\n$5\r\nhello\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::incompatible_size
|
||||
},
|
||||
{
|
||||
// Root node should have exactly 2 elements
|
||||
"getmasteraddr_array_size_3",
|
||||
role::master,
|
||||
{
|
||||
"*3\r\n$5\r\nhello\r\n$3\r\nbye\r\n$3\r\nabc\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::incompatible_size
|
||||
},
|
||||
{
|
||||
// IP should be a string
|
||||
"getmasteraddr_ip_not_string",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n+OK\r\n$5\r\nhello\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
// Port should be a string
|
||||
"getmasteraddr_port_not_string",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$5\r\nhello\r\n+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
|
||||
// SENTINEL SENTINELS
|
||||
{
|
||||
// The request errors
|
||||
"sentinels_error",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"-ERR something went wrong\r\n",
|
||||
},
|
||||
"ERR something went wrong",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// The root node should be an array
|
||||
"sentinels_not_array",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"+OK\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_array
|
||||
},
|
||||
{
|
||||
// Each Sentinel object should be a map
|
||||
"sentinels_subobject_not_map",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n*1\r\n$9\r\nlocalhost\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_map
|
||||
},
|
||||
{
|
||||
// Keys in the Sentinel object should be strings
|
||||
"sentinels_keys_not_strings",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n*0\r\n$5\r\nhello\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
// Values in the Sentinel object should be strings
|
||||
"sentinels_keys_not_strings",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$5\r\nhello\r\n*1\r\n+OK\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
"sentinels_ip_not_found",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$4\r\nport\r\n$4\r\n6380\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
},
|
||||
{
|
||||
"sentinels_port_not_found",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$2\r\nip\r\n$9\r\nlocalhost\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
},
|
||||
|
||||
// SENTINEL REPLICAS
|
||||
{
|
||||
// The request errors
|
||||
"replicas_error",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"-ERR something went wrong\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"ERR something went wrong",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// The root node should be an array
|
||||
"replicas_not_array",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_array
|
||||
},
|
||||
{
|
||||
// Each replica object should be a map
|
||||
"replicas_subobject_not_map",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n*1\r\n$9\r\nlocalhost\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_map
|
||||
},
|
||||
{
|
||||
// Keys in the replica object should be strings
|
||||
"replicas_keys_not_strings",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n*0\r\n$5\r\nhello\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
// Values in the replica object should be strings
|
||||
"replicas_keys_not_strings",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$5\r\nhello\r\n*1\r\n+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
"replicas_ip_not_found",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$4\r\nport\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
},
|
||||
{
|
||||
"replicas_port_not_found",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$2\r\nip\r\n$9\r\nlocalhost\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
}
|
||||
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
// Setup
|
||||
std::cerr << "Running error test case: " << tc.name << std::endl;
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3(tc.responses);
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, tc.server_role, fix.resp);
|
||||
BOOST_TEST_EQ(ec, tc.expected_ec);
|
||||
BOOST_TEST_EQ(fix.resp.diagnostic, tc.expected_diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_master();
|
||||
test_master_no_sentinels();
|
||||
test_master_setup_request();
|
||||
test_master_ip_port_out_of_order();
|
||||
|
||||
test_replica();
|
||||
test_replica_no_sentinels();
|
||||
test_replica_no_replicas();
|
||||
test_replica_setup_request();
|
||||
test_replica_ip_port_out_of_order();
|
||||
|
||||
test_errors();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -39,6 +39,7 @@ static const char* to_string(run_action_type value)
|
||||
switch (value) {
|
||||
case run_action_type::done: return "run_action_type::done";
|
||||
case run_action_type::immediate: return "run_action_type::immediate";
|
||||
case run_action_type::sentinel_resolve: return "run_action_type::sentinel_resolve";
|
||||
case run_action_type::connect: return "run_action_type::connect";
|
||||
case run_action_type::parallel_group: return "run_action_type::parallel_group";
|
||||
case run_action_type::cancel_receive: return "run_action_type::cancel_receive";
|
||||
@@ -142,6 +143,30 @@ void test_config_error_unix_ssl()
|
||||
});
|
||||
}
|
||||
|
||||
void test_config_error_unix_sentinel()
|
||||
{
|
||||
// Setup
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
cfg.unix_socket = "/var/sock";
|
||||
fixture fix{std::move(cfg)};
|
||||
|
||||
// Launching the operation fails immediately
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::immediate);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_unix_sockets_unsupported));
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::err,
|
||||
"Invalid configuration: The configuration specified UNIX sockets with Sentinel, which is "
|
||||
"not supported. [boost.redis:28]"},
|
||||
});
|
||||
}
|
||||
|
||||
// An error in connect with reconnection enabled triggers a reconnection
|
||||
void test_connect_error()
|
||||
{
|
||||
@@ -162,10 +187,83 @@ void test_connect_error()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at 127.0.0.1:6379 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// Check logs for other transport types
|
||||
void test_connect_error_ssl()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.addr = {"my_hostname", "10000"};
|
||||
fix.st.cfg.use_ssl = true;
|
||||
|
||||
// Launch the operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Connect errors. We sleep and try to connect again
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// This time we succeed and we launch the parallel group
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at my_hostname:10000 (TLS enabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at my_hostname:10000 (TLS enabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at my_hostname:10000 (TLS enabled)" },
|
||||
{logger::level::info, "Connected to Redis server at my_hostname:10000 (TLS enabled)" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
void test_connect_error_unix()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.unix_socket = "/tmp/sock";
|
||||
|
||||
// Launch the operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Connect errors. We sleep and try to connect again
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// This time we succeed and we launch the parallel group
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at '/tmp/sock'" },
|
||||
{logger::level::info, "Failed to connect to Redis server at '/tmp/sock': Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at '/tmp/sock'" },
|
||||
{logger::level::info, "Connected to Redis server at '/tmp/sock'" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// An error in connect without reconnection enabled makes the operation finish
|
||||
void test_connect_error_no_reconnect()
|
||||
{
|
||||
@@ -180,8 +278,13 @@ void test_connect_error_no_reconnect()
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::connect_timeout));
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at 127.0.0.1:6379 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// A cancellation in connect makes the operation finish even with reconnection enabled
|
||||
@@ -198,9 +301,10 @@ void test_connect_cancel()
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (1)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::debug, "Run: cancelled (1)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,9 +322,10 @@ void test_connect_cancel_edge()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (1)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::debug, "Run: cancelled (1)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,8 +352,13 @@ void test_parallel_group_error()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
});
|
||||
}
|
||||
|
||||
// An error in the parallel group makes the operation exit if reconnection is disabled
|
||||
@@ -269,8 +379,11 @@ void test_parallel_group_error_no_reconnect()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::empty_field));
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
});
|
||||
}
|
||||
|
||||
// A cancellation in the parallel group makes it exit, even if reconnection is enabled.
|
||||
@@ -292,9 +405,11 @@ void test_parallel_group_cancel()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (2)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (2)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,9 +430,11 @@ void test_parallel_group_cancel_no_reconnect()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (2)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (2)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -343,9 +460,11 @@ void test_wait_cancel()
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (3)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (3)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,9 +489,11 @@ void test_wait_cancel_edge()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (3)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (3)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -409,9 +530,16 @@ void test_several_reconnections()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// The cancellation was logged
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (2)"}
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at 127.0.0.1:6379 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (2)" } // clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
@@ -481,7 +609,11 @@ void test_setup_request_success()
|
||||
|
||||
// Check log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Setup request execution: success"}
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Setup request execution: success"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
@@ -501,8 +633,13 @@ void test_setup_request_empty()
|
||||
// Nothing was added to the multiplexer
|
||||
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 0u);
|
||||
|
||||
// Check log
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// A server error would cause the reader to exit
|
||||
@@ -510,7 +647,7 @@ void test_setup_request_server_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.setup_diagnostic = "leftover"; // simulate a leftover from previous runs
|
||||
fix.st.diagnostic = "leftover"; // simulate a leftover from previous runs
|
||||
fix.st.cfg.setup.clear();
|
||||
fix.st.cfg.setup.push("HELLO", 3);
|
||||
|
||||
@@ -533,9 +670,147 @@ void test_setup_request_server_error()
|
||||
|
||||
// Check log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info,
|
||||
"Setup request execution: The server response to the setup request sent during connection "
|
||||
"establishment contains an error. [boost.redis:23] (ERR: wrong command)"}
|
||||
"establishment contains an error. [boost.redis:23] (ERR: wrong command)" }
|
||||
});
|
||||
}
|
||||
|
||||
// When using Sentinel, reconnection works normally
|
||||
void test_sentinel_reconnection()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Resolve succeeds, and a connection is attempted
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"host1", "1000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// This errors, so we sleep and resolve again
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"host2", "2000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Sentinel involves always a setup request containing the role check. Run it.
|
||||
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 1u);
|
||||
BOOST_TEST(fix.st.mpx.commit_write(fix.st.mpx.get_write_buffer().size()));
|
||||
read(fix.st.mpx, "*1\r\n$6\r\nmaster\r\n");
|
||||
error_code ec;
|
||||
auto res = fix.st.mpx.consume(ec);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST(res.first == detail::consume_result::got_response);
|
||||
|
||||
// The parallel group errors, so we sleep and resolve again
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
|
||||
act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"host3", "3000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Cancel
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at host1:1000 (TLS disabled)"},
|
||||
{logger::level::info, "Failed to connect to Redis server at host1:1000 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at host2:2000 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at host2:2000 (TLS disabled)"},
|
||||
{logger::level::info, "Setup request execution: success"},
|
||||
{logger::level::info, "Trying to connect to Redis server at host3:3000 (TLS disabled)"},
|
||||
{logger::level::debug, "Run: cancelled (1)"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// If the Sentinel resolve operation errors, we try again
|
||||
void test_sentinel_resolve_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Start the Sentinel resolve operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
|
||||
// It fails with an error, so we go to sleep
|
||||
act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
|
||||
// Retrying it succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"myhost", "10000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at myhost:10000 (TLS disabled)"},
|
||||
});
|
||||
}
|
||||
|
||||
// The reconnection setting affects Sentinel reconnection, too
|
||||
void test_sentinel_resolve_error_no_reconnect()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{config_no_reconnect()};
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Start the Sentinel resolve operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
|
||||
// It fails with an error, so we exit
|
||||
act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed));
|
||||
|
||||
// Log
|
||||
fix.check_log({});
|
||||
}
|
||||
|
||||
void test_sentinel_resolve_cancel()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Start the Sentinel resolve operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (4)"},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -547,8 +822,13 @@ int main()
|
||||
test_config_error_unix();
|
||||
#endif
|
||||
test_config_error_unix_ssl();
|
||||
test_config_error_unix_sentinel();
|
||||
|
||||
test_connect_error();
|
||||
test_connect_error_ssl();
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
test_connect_error_unix();
|
||||
#endif
|
||||
test_connect_error_no_reconnect();
|
||||
test_connect_cancel();
|
||||
test_connect_cancel_edge();
|
||||
@@ -568,5 +848,10 @@ int main()
|
||||
test_setup_request_empty();
|
||||
test_setup_request_server_error();
|
||||
|
||||
test_sentinel_reconnection();
|
||||
test_sentinel_resolve_error();
|
||||
test_sentinel_resolve_error_no_reconnect();
|
||||
test_sentinel_resolve_cancel();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
|
||||
682
test/test_sentinel_resolve_fsm.cpp
Normal file
682
test/test_sentinel_resolve_fsm.cpp
Normal file
@@ -0,0 +1,682 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/sentinel_resolve_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include "sansio_utils.hpp"
|
||||
|
||||
#include <iterator>
|
||||
|
||||
using namespace boost::redis;
|
||||
namespace asio = boost::asio;
|
||||
using detail::sentinel_resolve_fsm;
|
||||
using detail::sentinel_action;
|
||||
using detail::connection_state;
|
||||
using detail::nodes_from_resp3;
|
||||
using boost::system::error_code;
|
||||
using boost::asio::cancellation_type_t;
|
||||
|
||||
static char const* to_string(sentinel_action::type t)
|
||||
{
|
||||
switch (t) {
|
||||
case sentinel_action::type::done: return "sentinel_action::type::done";
|
||||
case sentinel_action::type::connect: return "sentinel_action::type::connect";
|
||||
case sentinel_action::type::request: return "sentinel_action::type::request";
|
||||
default: return "sentinel_action::type::<invalid type>";
|
||||
}
|
||||
}
|
||||
|
||||
// Operators
|
||||
namespace boost::redis::detail {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, sentinel_action::type type)
|
||||
{
|
||||
os << to_string(type);
|
||||
return os;
|
||||
}
|
||||
|
||||
bool operator==(sentinel_action lhs, sentinel_action rhs) noexcept
|
||||
{
|
||||
if (lhs.get_type() != rhs.get_type())
|
||||
return false;
|
||||
else if (lhs.get_type() == sentinel_action::type::done)
|
||||
return lhs.error() == rhs.error();
|
||||
else if (lhs.get_type() == sentinel_action::type::connect)
|
||||
return lhs.connect_addr() == rhs.connect_addr();
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, sentinel_action act)
|
||||
{
|
||||
os << "exec_action{ .type=" << act.get_type();
|
||||
if (act.get_type() == sentinel_action::type::done)
|
||||
os << ", .error=" << act.error();
|
||||
else if (act.get_type() == sentinel_action::type::connect)
|
||||
os << ", .addr=" << act.connect_addr().host << ":" << act.connect_addr().port;
|
||||
return os << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const address& addr)
|
||||
{
|
||||
return os << "address{ .host=" << addr.host << ", .port=" << addr.port << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
namespace {
|
||||
|
||||
struct fixture : detail::log_fixture {
|
||||
connection_state st{{make_logger()}};
|
||||
sentinel_resolve_fsm fsm;
|
||||
|
||||
fixture()
|
||||
{
|
||||
st.sentinels = {
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"host1", "1000"},
|
||||
{"host4", "4000"},
|
||||
};
|
||||
st.cfg.sentinel.master_name = "mymaster";
|
||||
}
|
||||
};
|
||||
|
||||
void test_success()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Now send the request
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// We received a valid request, so we're done
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// The Sentinel list is updated
|
||||
const address expected_sentinels[] = {
|
||||
{"host1", "1000" },
|
||||
{"host.one", "26380"},
|
||||
{"host4", "4000" },
|
||||
};
|
||||
BOOST_TEST_ALL_EQ(
|
||||
fix.st.sentinels.begin(),
|
||||
fix.st.sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000 resolved the server address to test.host:6380"},
|
||||
});
|
||||
}
|
||||
|
||||
void test_success_replica()
|
||||
{
|
||||
// Setup. Seed the engine so that it returns index 1
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.server_role = role::replica;
|
||||
fix.st.eng.get().seed(static_cast<std::uint_fast32_t>(183984887232u));
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Now send the request
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*3\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.one\r\n$4\r\nport\r\n$4\r\n6379\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.two\r\n$4\r\nport\r\n$4\r\n6379\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.thr\r\n$4\r\nport\r\n$4\r\n6379\r\n",
|
||||
"*0\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// We received a valid request, so we're done
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The address of one of the replicas is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"replica.two", "6379"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of a replica of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000 resolved the server address to replica.two:6379" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel fails connection, but subsequent ones succeed
|
||||
void test_one_connect_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// This errors, so we connect to the 2nd sentinel
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
|
||||
// Now send the request
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
|
||||
// We received a valid request, so we're done
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: connection establishment error: Connect timeout. [boost.redis:18]" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel fails while executing the request, but subsequent ones succeed
|
||||
void test_one_request_network_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
|
||||
// It fails, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: error while executing request: Timeout while writing data to the server. [boost.redis:27]"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel responds with an invalid message, but subsequent ones succeed
|
||||
void test_one_request_parse_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"+OK\r\n",
|
||||
"+OK\r\n",
|
||||
});
|
||||
|
||||
// This fails parsing, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: error parsing response (maybe forgot to upgrade to RESP3?): "
|
||||
"Expects a RESP3 array, but got a different data type. [boost.redis:32]"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel responds with an error (e.g. failed auth), but subsequent ones succeed
|
||||
void test_one_request_error_node()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"-ERR needs authentication\r\n",
|
||||
"-ERR needs authentication\r\n",
|
||||
});
|
||||
|
||||
// This fails, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: responded with an error: ERR needs authentication"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel doesn't know about the master, but others do
|
||||
void test_one_master_unknown()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"_\r\n",
|
||||
"-ERR unknown master\r\n",
|
||||
});
|
||||
|
||||
// It doesn't know about our master, so we connect to the 2nd sentinel.
|
||||
// This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: doesn't know about the configured master" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel thinks there are no replicas (stale data?), but others do
|
||||
void test_one_no_replicas()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.server_role = role::replica;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
|
||||
// This errors, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.one\r\n$4\r\nport\r\n$4\r\n6379\r\n",
|
||||
"*0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The replica's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"replica.one", "6379"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of a replica of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: the configured master has no replicas" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to replica.one:6379"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// If no Sentinel is available, the operation fails. A comprehensive error is logged.
|
||||
void test_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// 1st Sentinel doesn't know about the master
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"_\r\n",
|
||||
"-ERR unknown master\r\n",
|
||||
});
|
||||
|
||||
// Move to the 2nd Sentinel, which fails to connect
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
|
||||
// Move to the 3rd Sentinel, which has authentication misconfigured
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host3", "3000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"-ERR unauthorized\r\n",
|
||||
"-ERR unauthorized\r\n",
|
||||
});
|
||||
|
||||
// Sentinel list exhausted
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed));
|
||||
|
||||
// The Sentinel list is not updated
|
||||
BOOST_TEST_EQ(fix.st.sentinels.size(), 3u);
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: doesn't know about the configured master" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000: connection establishment error: Connect timeout. [boost.redis:18]" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host3:3000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host3:3000" },
|
||||
{logger::level::info, "Sentinel at host3:3000: responded with an error: ERR unauthorized"},
|
||||
|
||||
{logger::level::err, "Failed to resolve the address of master 'mymaster'. Tried the following Sentinels:"
|
||||
"\n Sentinel at host1:1000: doesn't know about the configured master"
|
||||
"\n Sentinel at host2:2000: connection establishment error: Connect timeout. [boost.redis:18]"
|
||||
"\n Sentinel at host3:3000: responded with an error: ERR unauthorized"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The replica error text is slightly different
|
||||
void test_error_replica()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.sentinels = {
|
||||
{"host1", "1000"}
|
||||
};
|
||||
fix.st.cfg.sentinel.server_role = role::replica;
|
||||
|
||||
// Initiate, connect to the only Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of a replica of master 'mymaster' using Sentinel" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: the configured master has no replicas" },
|
||||
|
||||
{logger::level::err, "Failed to resolve the address of a replica of master 'mymaster'. Tried the following Sentinels:"
|
||||
"\n Sentinel at host1:1000: the configured master has no replicas"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// Cancellations
|
||||
void test_cancel_connect()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Cancellation
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (1)" },
|
||||
});
|
||||
}
|
||||
|
||||
void test_cancel_connect_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Cancellation (without error code)
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (1)" },
|
||||
});
|
||||
}
|
||||
|
||||
void test_cancel_request()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (2)" },
|
||||
});
|
||||
}
|
||||
|
||||
void test_cancel_request_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (2)" },
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_success();
|
||||
test_success_replica();
|
||||
|
||||
test_one_connect_error();
|
||||
test_one_request_network_error();
|
||||
test_one_request_parse_error();
|
||||
test_one_request_error_node();
|
||||
test_one_master_unknown();
|
||||
test_one_no_replicas();
|
||||
|
||||
test_error();
|
||||
test_error_replica();
|
||||
|
||||
test_cancel_connect();
|
||||
test_cancel_connect_edge();
|
||||
test_cancel_request();
|
||||
test_cancel_request_edge();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
349
test/test_setup_adapter.cpp
Normal file
349
test/test_setup_adapter.cpp
Normal file
@@ -0,0 +1,349 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/impl/setup_request_utils.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/result.hpp>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
using namespace boost::redis;
|
||||
using detail::setup_adapter;
|
||||
using detail::connection_state;
|
||||
using detail::compose_setup_request;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
void test_success()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.push("SELECT", 2);
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "%1\r\n$6\r\nserver\r\n$5\r\nredis\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Response to the SELECT command
|
||||
p.reset();
|
||||
done = resp3::parse(p, "+OK\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// No diagnostic
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
void test_simple_error()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO contains an error
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "-ERR unauthorized\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::resp3_hello);
|
||||
BOOST_TEST_EQ(st.diagnostic, "ERR unauthorized");
|
||||
}
|
||||
|
||||
void test_blob_error()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.push("SELECT", 1);
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "%1\r\n$6\r\nserver\r\n$5\r\nredis\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Response to select contains an error
|
||||
p.reset();
|
||||
done = resp3::parse(p, "!3\r\nBad\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::resp3_hello);
|
||||
BOOST_TEST_EQ(st.diagnostic, "Bad");
|
||||
}
|
||||
|
||||
// A NULL is not an error
|
||||
void test_null()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "_\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// No diagnostic
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
// Sentinel adds a ROLE command and checks its output.
|
||||
// These are real wire values.
|
||||
constexpr std::string_view role_master_response =
|
||||
"*3\r\n$6\r\nmaster\r\n:567942\r\n*2\r\n"
|
||||
"*3\r\n$9\r\nlocalhost\r\n$4\r\n6381\r\n$6\r\n567809\r\n*3\r\n$9\r\nlocalhost\r\n"
|
||||
"$4\r\n6382\r\n$6\r\n567809\r\n";
|
||||
constexpr std::string_view role_replica_response =
|
||||
"*5\r\n$5\r\nslave\r\n$9\r\nlocalhost\r\n:6380\r\n$9\r\nconnected\r\n:617355\r\n";
|
||||
|
||||
void test_sentinel_master()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.push("SELECT", 2);
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "%1\r\n$6\r\nserver\r\n$5\r\nredis\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Response to the SELECT command
|
||||
p.reset();
|
||||
done = resp3::parse(p, "+OK\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Response to the ROLE command
|
||||
p.reset();
|
||||
done = resp3::parse(p, role_master_response, adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// No diagnostic
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
void test_sentinel_replica()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
st.cfg.sentinel.server_role = role::replica;
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "%1\r\n$6\r\nserver\r\n$5\r\nredis\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Response to the ROLE command
|
||||
p.reset();
|
||||
done = resp3::parse(p, role_replica_response, adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// No diagnostic
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
// If the role is not the one expected, a role failed error is issued
|
||||
void test_sentinel_role_check_failed_master()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "%1\r\n$6\r\nserver\r\n$5\r\nredis\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Response to the ROLE command
|
||||
p.reset();
|
||||
done = resp3::parse(p, role_replica_response, adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::role_check_failed);
|
||||
|
||||
// No diagnostic
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
void test_sentinel_role_check_failed_replica()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
st.cfg.sentinel.server_role = role::replica;
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to HELLO
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "%1\r\n$6\r\nserver\r\n$5\r\nredis\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Response to the ROLE command
|
||||
p.reset();
|
||||
done = resp3::parse(p, role_master_response, adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::role_check_failed);
|
||||
|
||||
// No diagnostic
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
// If the role command errors or has an unexpected format, we fail
|
||||
void test_sentinel_role_error_node()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.clear();
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to ROLE
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "-ERR unauthorized\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::resp3_hello);
|
||||
BOOST_TEST_EQ(st.diagnostic, "ERR unauthorized");
|
||||
}
|
||||
|
||||
void test_sentinel_role_not_array()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.clear();
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to ROLE
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "+OK\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::invalid_data_type);
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
void test_sentinel_role_empty_array()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.clear();
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to ROLE
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "*0\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::incompatible_size);
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
void test_sentinel_role_first_element_not_string()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.clear();
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
compose_setup_request(st.cfg);
|
||||
setup_adapter adapter{st};
|
||||
|
||||
// Response to ROLE
|
||||
resp3::parser p;
|
||||
error_code ec;
|
||||
bool done = resp3::parse(p, "*1\r\n:2000\r\n", adapter, ec);
|
||||
BOOST_TEST(done);
|
||||
BOOST_TEST_EQ(ec, error::invalid_data_type);
|
||||
BOOST_TEST_EQ(st.diagnostic, "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_success();
|
||||
test_simple_error();
|
||||
test_blob_error();
|
||||
test_null();
|
||||
|
||||
test_sentinel_master();
|
||||
test_sentinel_replica();
|
||||
test_sentinel_role_check_failed_master();
|
||||
test_sentinel_role_check_failed_replica();
|
||||
test_sentinel_role_error_node();
|
||||
test_sentinel_role_not_array();
|
||||
test_sentinel_role_empty_array();
|
||||
test_sentinel_role_first_element_not_string();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -23,6 +23,8 @@ namespace redis = boost::redis;
|
||||
using redis::detail::compose_setup_request;
|
||||
using boost::system::error_code;
|
||||
|
||||
// TODO: rename the file
|
||||
|
||||
namespace {
|
||||
|
||||
void test_compose_setup()
|
||||
@@ -178,6 +180,50 @@ void test_compose_setup_use_setup_flags()
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
// When using Sentinel, a ROLE command is added. This works
|
||||
// both with the old HELLO and new setup strategies.
|
||||
void test_compose_setup_sentinel_auth()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
cfg.clientname = "";
|
||||
cfg.username = "foo";
|
||||
cfg.password = "bar";
|
||||
|
||||
compose_setup_request(cfg);
|
||||
|
||||
std::string_view const expected =
|
||||
"*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"
|
||||
"*1\r\n$4\r\nROLE\r\n";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
void test_compose_setup_sentinel_use_setup()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
cfg.use_setup = true;
|
||||
cfg.setup.push("SELECT", 42);
|
||||
|
||||
compose_setup_request(cfg);
|
||||
|
||||
std::string_view const expected =
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*2\r\n$6\r\nSELECT\r\n$2\r\n42\r\n"
|
||||
"*1\r\n$4\r\nROLE\r\n";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -191,6 +237,8 @@ int main()
|
||||
test_compose_setup_use_setup();
|
||||
test_compose_setup_use_setup_no_hello();
|
||||
test_compose_setup_use_setup_flags();
|
||||
test_compose_setup_sentinel_auth();
|
||||
test_compose_setup_sentinel_use_setup();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -131,7 +131,7 @@ void test_switch_between_transports()
|
||||
// Create configurations for TLS and UNIX connections
|
||||
auto tcp_tls_cfg = make_test_config();
|
||||
tcp_tls_cfg.use_ssl = true;
|
||||
tcp_tls_cfg.addr.port = "6380";
|
||||
tcp_tls_cfg.addr.port = "16380";
|
||||
auto unix_cfg = make_test_config();
|
||||
unix_cfg.unix_socket = unix_socket_path;
|
||||
|
||||
@@ -194,7 +194,7 @@ void test_error_unix_tls()
|
||||
connection conn{ioc};
|
||||
auto cfg = make_test_config();
|
||||
cfg.use_ssl = true;
|
||||
cfg.addr.port = "6380";
|
||||
cfg.addr.port = "16380";
|
||||
cfg.unix_socket = unix_socket_path;
|
||||
bool finished = false;
|
||||
|
||||
|
||||
212
test/test_update_sentinel_list.cpp
Normal file
212
test/test_update_sentinel_list.cpp
Normal file
@@ -0,0 +1,212 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp>
|
||||
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
using namespace boost::redis;
|
||||
using detail::update_sentinel_list;
|
||||
using boost::system::error_code;
|
||||
|
||||
// Operators
|
||||
namespace boost::redis {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const address& addr)
|
||||
{
|
||||
return os << "address{ .host=" << addr.host << ", .port=" << addr.port << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
namespace {
|
||||
|
||||
// The only Sentinel resolved the address successfully, and there's no newly discovered Sentinels
|
||||
void test_single_sentinel()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"}
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, {}, initial_sentinels);
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
initial_sentinels.begin(),
|
||||
initial_sentinels.end());
|
||||
}
|
||||
|
||||
// Some new Sentinels were discovered using SENTINEL SENTINELS
|
||||
void test_new_sentinels()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"}
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// Some of the new Sentinels are already in the list
|
||||
void test_new_sentinels_known()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// The Sentinel that succeeded should be placed first
|
||||
void test_success_sentinel_not_first()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
const address new_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 2u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host3", "3000"},
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// If a discovered Sentinel is not returned in subsequent iterations, it's removed from the list
|
||||
void test_new_sentinel_removed()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
};
|
||||
std::vector<address> sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host4", "4000"},
|
||||
};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// Bootstrap Sentinels are never removed
|
||||
void test_bootstrap_sentinel_removed()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
std::vector<address> sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
{"host4", "4000"},
|
||||
{"host5", "5000"},
|
||||
};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host4", "4000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host4", "4000"},
|
||||
{"host3", "3000"}, // bootstrap Sentinels placed last
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_single_sentinel();
|
||||
test_new_sentinels();
|
||||
test_new_sentinels_known();
|
||||
test_success_sentinel_not_first();
|
||||
test_new_sentinel_removed();
|
||||
test_bootstrap_sentinel_removed();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
Reference in New Issue
Block a user