2
0
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:
Anarthal (Rubén Pérez)
2025-11-19 22:31:19 +01:00
committed by GitHub
parent 00f3ec9b78
commit bdd9c327c1
52 changed files with 5123 additions and 427 deletions

View File

@@ -3,6 +3,7 @@
* xref:cancellation.adoc[]
* xref:serialization.adoc[]
* xref:logging.adoc[]
* xref:sentinel.adoc[]
* xref:benchmarks.adoc[]
* xref:comparison.adoc[]
* xref:examples.adoc[]

View File

@@ -15,7 +15,7 @@ The examples below show how to use the features discussed throughout this docume
* {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp]: Shows how to send and receive STL containers and how to use transactions.
* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: Shows how to serialize types using Boost.Json.
* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: Shows how to serialize types using protobuf.
* {site-url}/example/cpp20_resolve_with_sentinel.cpp[cpp20_resolve_with_sentinel.cpp]: Shows how to resolve a master address using sentinels.
* {site-url}/example/cpp20_sentinel.cpp[cpp20_sentinel.cpp]: Shows how to use the library with a Sentinel deployment.
* {site-url}/example/cpp20_subscriber.cpp[cpp20_subscriber.cpp]: Shows how to implement pubsub with reconnection re-subscription.
* {site-url}/example/cpp20_echo_server.cpp[cpp20_echo_server.cpp]: A simple TCP echo server.
* {site-url}/example/cpp20_chat_room.cpp[cpp20_chat_room.cpp]: A command line chat built on Redis pubsub.

View File

@@ -25,8 +25,12 @@ xref:reference:boost/redis/basic_connection.adoc[`basic_connection`]
xref:reference:boost/redis/address.adoc[`address`]
xref:reference:boost/redis/role.adoc[`role`]
xref:reference:boost/redis/config.adoc[`config`]
xref:reference:boost/redis/sentinel_config.adoc[`sentinel_config`]
xref:reference:boost/redis/error.adoc[`error`]
xref:reference:boost/redis/logger.adoc[`logger`]

View File

@@ -0,0 +1,152 @@
//
// 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)
//
= Sentinel
Boost.Redis supports Redis Sentinel deployments. Sentinel handling
in `connection` is built-in: xref:reference:boost/redis/basic_connection/async_run-04.adoc[`async_run`]
automatically connects to Sentinels, resolves the master's address, and connects to the master.
Configuration is done using xref:reference:boost/redis/sentinel_config.adoc[`config::sentinel`]:
[source,cpp]
----
config cfg;
// To enable Sentinel, set this field to a non-empty list
// of (hostname, port) pairs where Sentinels are listening
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
// Set master_name to the identifier that you configured
// in the "sentinel monitor" statement of your sentinel.conf file
cfg.sentinel.master_name = "mymaster";
----
Once set, the connection object can be used normally. See our
our {site-url}/example/cpp20_sentinel.cpp[Sentinel example]
for a full program.
== Connecting to replicas
By default, the library connects to the Redis master.
You can connect to one of its replicas by using
xref:reference:boost/redis/sentinel_config/server_role.adoc[`config::sentinel::server_role`].
This can be used to balance load, if all your commands read data from
the server and never write to it. The particular replica will be chosen randomly.
[source,cpp]
----
config cfg;
// Set up Sentinel
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
cfg.sentinel.master_name = "mymaster";
// Ask the library to connect to a random replica of 'mymaster', rather than the master node
cfg.sentinel.server_role = role::replica;
----
== Sentinel authentication
If your Sentinels require authentication,
you can use xref:reference:boost/redis/sentinel_config/setup.adoc[`config::sentinel::setup`]
to provide credentials.
This request is executed immediately after connecting to Sentinels, and
before any other command:
[source,cpp]
----
// Set up Sentinel
config cfg;
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
cfg.sentinel.master_name = "mymaster";
// By default, setup contains a 'HELLO 3' command.
// Override it to add an AUTH clause to it with out credentials.
cfg.sentinel.setup.clear();
cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_password");
// cfg.sentinel.setup applies to Sentinels, only.
// Use cfg.setup to authenticate to masters/replicas.
cfg.use_setup = true; // Required for cfg.setup to be used, for historic reasons
cfg.setup.clear();
cfg.setup.push("HELLO", 3, "AUTH", "master_user", "master_password");
----
== Using TLS with Sentinels
You might use TLS with Sentinels only, masters/replicas only, or both by adjusting
xref:reference:boost/redis/sentinel_config/use_ssl.adoc[`config::sentinel::use_ssl`]
and xref:reference:boost/redis/config/use_ssl.adoc[`config::use_ssl`]:
[source,cpp]
----
// Set up Sentinel
config cfg;
cfg.sentinel.addresses = {
{"sentinel1.example.com", "26379"},
{"sentinel2.example.com", "26379"},
{"sentinel3.example.com", "26379"},
};
cfg.sentinel.master_name = "mymaster";
// Adjust these switches to enable/disable TLS
cfg.use_ssl = true; // Applies to masters and replicas
cfg.sentinel.use_ssl = true; // Applies to Sentinels
----
== Sentinel algorithm
This section details how `async_run` interacts with Sentinel.
Most of the algorithm follows
https://redis.io/docs/latest/develop/reference/sentinel-clients/[the official Sentinel client guidelines].
Some of these details may vary between library versions.
* Connections maintain an internal list of Sentinels, bootstrapped from
xref:reference:boost/redis/sentinel_config/addresses.adoc[`config::sentinel::addresses`].
* The first Sentinel in the list is contacted by performing the following:
** A physical connection is established.
** The setup request is executed.
** The master's address is resolved using
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL GET-MASTER-NAME-BY-ADDR`].
** If `config::sentinel::server_role` is `role::replica`, replica addresses are obtained using
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL REPLICAS`].
One replica is chosen randomly.
** The address of other Sentinels also monitoring this master are retrieved using
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL SENTINELS`].
* If a Sentinel is unreachable, doesn't know about the configured master,
or returns an error while executing the above requests, the next Sentinel in the list is tried.
* If all Sentinels have been tried without success, `config::reconnect_wait_interval`
is waited, and the process starts again.
* After a successful Sentinel response, the internal Sentinel list is updated
with any newly discovered Sentinels.
Sentinels in `config::sentinel::addresses` are always kept in the list,
even if they weren't present in the output of `SENTINEL SENTINELS`.
* The retrieved address is used
to establish a connection with the master or replica.
A `ROLE` command is added at the end of the setup request.
This is used to detect situations where a Sentinel returns outdated
information due to a failover in process. If `ROLE` doesn't output
the expected role (`"master"` or `"slave"`, depending on `config::sentinel::server_role`)
`config::reconnect_wait_interval` is waited and Sentinel is contacted again.
* The connection to the master/replica is run like any other connection.
If network errors or timeouts happen, `config::reconnect_wait_interval`
is waited and Sentinel is contacted again.

View File

@@ -28,11 +28,11 @@ make_testable_example(cpp20_containers 20)
make_testable_example(cpp20_json 20)
make_testable_example(cpp20_unix_sockets 20)
make_testable_example(cpp20_timeouts 20)
make_testable_example(cpp20_sentinel 20)
make_example(cpp20_subscriber 20)
make_example(cpp20_streams 20)
make_example(cpp20_echo_server 20)
make_example(cpp20_resolve_with_sentinel 20)
make_example(cpp20_intro_tls 20)
# We test the protobuf example only on gcc.

View File

@@ -1,77 +0,0 @@
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
*/
#include <boost/redis/connection.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/redirect_error.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using endpoints = asio::ip::tcp::resolver::results_type;
using boost::redis::request;
using boost::redis::response;
using boost::redis::ignore_t;
using boost::redis::config;
using boost::redis::address;
using boost::redis::connection;
auto redir(boost::system::error_code& ec) { return asio::redirect_error(asio::use_awaitable, ec); }
// For more info see
// - https://redis.io/docs/manual/sentinel.
// - https://redis.io/docs/reference/sentinel-clients.
auto resolve_master_address(std::vector<address> const& addresses) -> asio::awaitable<address>
{
request req;
req.push("SENTINEL", "get-master-addr-by-name", "mymaster");
req.push("QUIT");
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
response<std::optional<std::array<std::string, 2>>, ignore_t> resp;
for (auto addr : addresses) {
boost::system::error_code ec;
config cfg;
cfg.addr = addr;
// TODO: async_run and async_exec should be lauched in
// parallel here so we can wait for async_run completion
// before eventually calling it again.
conn->async_run(cfg, asio::consign(asio::detached, conn));
co_await conn->async_exec(req, resp, redir(ec));
conn->cancel();
if (!ec && std::get<0>(resp))
co_return address{
std::get<0>(resp).value().value().at(0),
std::get<0>(resp).value().value().at(1)};
}
co_return address{};
}
auto co_main(config cfg) -> asio::awaitable<void>
{
// A list of sentinel addresses from which only one is responsive.
// This simulates sentinels that are down.
std::vector<address> const addresses{
address{"foo", "26379"},
address{"bar", "26379"},
cfg.addr
};
auto const ep = co_await resolve_master_address(addresses);
std::clog << "Host: " << ep.host << "\n"
<< "Port: " << ep.port << "\n"
<< std::flush;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -0,0 +1,60 @@
//
// 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/connection.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/consign.hpp>
#include <boost/asio/detached.hpp>
#include <iostream>
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
namespace asio = boost::asio;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::connection;
// Called from the main function (see main.cpp)
auto co_main(config cfg) -> asio::awaitable<void>
{
// Boost.Redis has built-in support for Sentinel deployments.
// To enable it, set the fields in config shown here.
// sentinel.addresses should contain a list of (hostname, port) pairs
// where Sentinels are listening. IPs can also be used.
cfg.sentinel.addresses = {
{"localhost", "26379"},
{"localhost", "26380"},
{"localhost", "26381"},
};
// Set master_name to the identifier that you configured
// in the "sentinel monitor" statement of your sentinel.conf file
cfg.sentinel.master_name = "mymaster";
// async_run will contact the Sentinels, obtain the master address,
// connect to it and keep the connection healthy. If a failover happens,
// the address will be resolved again and the new elected master will be contacted.
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
conn->async_run(cfg, asio::consign(asio::detached, conn));
// You can now use the connection normally, as you would use a connection to a single master.
request req;
req.push("PING", "Hello world");
response<std::string> resp;
// Execute the request.
co_await conn->async_exec(req, resp);
conn->cancel();
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
}
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

View File

@@ -12,9 +12,7 @@
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <functional>
#include <string_view>
#include <type_traits>
namespace boost::redis {
@@ -50,20 +48,7 @@ public:
using impl_t = std::function<void(parse_event, resp3::node_view const&, system::error_code&)>;
template <class T>
static auto create_impl(T& resp) -> impl_t
{
using namespace boost::redis::adapter;
return [adapter2 = boost_redis_adapt(resp)](
any_adapter::parse_event ev,
resp3::node_view const& nd,
system::error_code& ec) mutable {
switch (ev) {
case parse_event::init: adapter2.on_init(); break;
case parse_event::node: adapter2.on_node(nd, ec); break;
case parse_event::done: adapter2.on_done(); break;
}
};
}
static auto create_impl(T& resp) -> impl_t;
/// Contructs from a type erased adaper
any_adapter(impl_t fn = [](parse_event, resp3::node_view const&, system::error_code&) { })
@@ -109,6 +94,32 @@ private:
impl_t impl_;
};
namespace detail {
template <class Adapter>
any_adapter::impl_t make_any_adapter_impl(Adapter&& value)
{
return [adapter = std::move(value)](
any_adapter::parse_event ev,
resp3::node_view const& nd,
system::error_code& ec) mutable {
switch (ev) {
case any_adapter::parse_event::init: adapter.on_init(); break;
case any_adapter::parse_event::node: adapter.on_node(nd, ec); break;
case any_adapter::parse_event::done: adapter.on_done(); break;
}
};
}
} // namespace detail
} // namespace boost::redis
template <class T>
auto boost::redis::any_adapter::create_impl(T& resp) -> impl_t
{
using adapter::boost_redis_adapt;
return detail::make_any_adapter_impl(boost_redis_adapt(resp));
}
#endif

View File

@@ -13,6 +13,7 @@
#include <limits>
#include <optional>
#include <string>
#include <vector>
namespace boost::redis {
@@ -24,12 +25,130 @@ struct address {
std::string port = "6379";
};
/// Configure parameters used by the connection classes.
struct config {
/// Uses SSL instead of a plain connection.
/** @brief Compares two addresses for equality.
* @relates address
*
* @param a Left hand side address.
* @param b Right hand side address.
*/
inline bool operator==(address const& a, address const& b)
{
return a.host == b.host && a.port == b.port;
}
/** @brief Compares two addresses for inequality.
* @relates address
*
* @param a Left hand side address.
* @param b Right hand side address.
*/
inline bool operator!=(address const& a, address const& b) { return !(a == b); }
/// Identifies the possible roles of a Redis server.
enum class role
{
/// The server is a master.
master,
/// The server is a replica.
replica,
};
/// Configuration values to use when using Sentinel.
struct sentinel_config {
/**
* @brief A list of (hostname, port) pairs where the Sentinels are listening.
*
* Sentinels in this list will be contacted in order, until a successful
* connection is made. At this point, the `SENTINEL SENTINELS` command
* will be used to retrieve any additional Sentinels monitoring the configured master.
* Thus, it is not required to keep this list comprehensive - if Sentinels are added
* later, they will be detected at runtime.
*
* Sentinel will only be used if this value is not empty.
*
* Numeric IP addresses are also allowed as hostnames.
*/
std::vector<address> addresses{};
/**
* @brief The name of the master to connect to, as configured in the
* `sentinel monitor` statement in `sentinel.conf`.
*
* This field is required even when connecting to replicas.
*/
std::string master_name{};
/**
* @brief Whether connections to Sentinels should use TLS or not.
* Does not affect connections to masters.
*
* When set to `true`, physical connections to Sentinels will be established
* using TLS. This setting does *not* influence how masters and replicas are contacted.
* To use TLS when connecting to these, set @ref config::use_ssl to `true`.
*/
bool use_ssl = false;
/// For TCP connections, hostname and port of the Redis server.
/**
* @brief A request to be sent to Sentinels upon connection establishment.
*
* This request is executed every time a Sentinel is contacted, and before
* commands like `SENTINEL GET-MASTER-NAME-BY-ADDR` are run.
* By default, this field contains a `HELLO 3` command.
* You can use this request to set up any authorization required by Sentinels.
*
* This request should ensure that the connection is upgraded to RESP3
* by executing `HELLO 3` or similar. RESP2 is not supported yet.
*/
request setup = detail::make_hello_request();
/**
* @brief Time span that the Sentinel resolve operation is allowed to elapse.
* Does not affect connections to masters and replicas, controlled by @ref config::resolve_timeout.
*/
std::chrono::steady_clock::duration resolve_timeout = std::chrono::milliseconds{500};
/**
* @brief Time span that the Sentinel connect operation is allowed to elapse.
* Does not affect connections to masters and replicas, controlled by @ref config::connect_timeout.
*/
std::chrono::steady_clock::duration connect_timeout = std::chrono::milliseconds{500};
/**
* @brief Time span that the Sentinel TLS handshake operation is allowed to elapse.
* Does not affect connections to masters and replicas, controlled by @ref config::ssl_handshake_timeout.
*/
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{5};
/**
* @brief Time span that the Sentinel request/response exchange is allowed to elapse.
* Includes executing the commands in @ref setup and the commands required to
* resolve the server's address.
*/
std::chrono::steady_clock::duration request_timeout = std::chrono::seconds{5};
/**
* @brief Whether to connect to a Redis master or to a replica.
*
* The library resolves and connects to the Redis master, by default.
* Set this value to @ref role::replica to connect to one of the replicas
* of the master identified by @ref master_name.
* The particular replica will be chosen randomly.
*/
role server_role = role::master;
};
/// Configure parameters used by the connection classes.
struct config {
/**
* @brief Whether to use TLS instead of plaintext connections.
*
* When using Sentinel, configures whether to use TLS when connecting to masters and replicas.
* Use @ref sentinel_config::use_ssl to control TLS for Sentinels.
*/
bool use_ssl = false;
/// For TCP connections, hostname and port of the Redis server. Ignored when using Sentinel.
address addr = address{"127.0.0.1", "6379"};
/**
@@ -37,8 +156,11 @@ struct config {
*
* If non-empty, communication with the server will happen using
* UNIX domain sockets, and @ref addr will be ignored.
*
* UNIX domain sockets can't be used with SSL: if `unix_socket` is non-empty,
* @ref use_ssl must be `false`.
* @ref use_ssl must be `false`. UNIX domain sockets can't be used with Sentinel, either.
*
* UNIX domain sockets can't be used with Sentinel.
*/
std::string unix_socket;
@@ -51,6 +173,9 @@ struct config {
* If the username equals the literal `"default"` (the default)
* and no password is specified, the `HELLO` command is sent
* without authentication parameters.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
*/
std::string username = "default";
@@ -63,6 +188,9 @@ struct config {
* If the username equals the literal `"default"` (the default)
* and no password is specified, the `HELLO` command is sent
* without authentication parameters.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
*/
std::string password;
@@ -71,6 +199,9 @@ struct config {
* If @ref use_setup is false (the default), during connection establishment,
* a `HELLO` command is sent. If this field is not empty, the `HELLO` command
* will contain a `SETNAME` subcommand containing this value.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup to configure this value for Sentinels.
*/
std::string clientname = "Boost.Redis";
@@ -80,6 +211,8 @@ struct config {
* non-empty optional, and its value is different than zero,
* a `SELECT` command will be issued during connection establishment to set the logical
* database index. By default, no `SELECT` command is sent.
*
* When using Sentinel, this setting applies to masters and replicas.
*/
std::optional<int> database_index = 0;
@@ -95,13 +228,22 @@ struct config {
*/
std::string log_prefix = "(Boost.Redis) ";
/// Time span that the resolve operation is allowed to elapse.
/**
* @brief Time span that the resolve operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10};
/// Time span that the connect operation is allowed to elapse.
/**
* @brief Time span that the connect operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10};
/// Time span that the SSL handshake operation is allowed to elapse.
/**
* @brief Time span that the SSL handshake operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10};
/** @brief Time span between successive health checks.
@@ -123,11 +265,19 @@ struct config {
*
* The exact timeout values are *not* part of the interface, and might change
* in future versions.
*
* When using Sentinel, this setting applies to masters and replicas.
* Sentinels are not health-checked.
*/
std::chrono::steady_clock::duration health_check_interval = std::chrono::seconds{2};
/** @brief Time span to wait between successive connection retries.
* Set to zero to disable reconnection.
*
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
* If none of the configured Sentinels can be contacted, this time span will
* be waited before trying again. After a connection error with a master or replica
* is encountered, this time span will be waited before contacting Sentinels again.
*/
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
@@ -135,6 +285,8 @@ struct config {
*
* Sets a limit on how much data is allowed to be read into the
* read buffer. It can be used to prevent DDOS.
*
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
*/
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
@@ -144,6 +296,8 @@ struct config {
* needed. This can help avoiding some memory allocations. Once the
* maximum size is reached no more memory allocations are made
* since the buffer is reused.
*
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
*/
std::size_t read_buffer_append_size = 4096;
@@ -168,6 +322,9 @@ struct config {
* systems that don't support `HELLO`.
*
* By default, this field is false, and @ref setup will not be used.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup for Sentinels.
*/
bool use_setup = false;
@@ -177,8 +334,17 @@ struct config {
* @ref use_setup docs for more info.
*
* By default, `setup` contains a `"HELLO 3"` command.
*
* When using Sentinel, this setting applies to masters and replicas.
* Use @ref sentinel_config::setup for Sentinels.
*/
request setup = detail::make_hello_request();
/**
* @brief Configuration values for Sentinel. Sentinel is enabled only if
* @ref sentinel_config::addresses is not empty.
*/
sentinel_config sentinel{};
};
} // namespace boost::redis

View File

@@ -12,10 +12,12 @@
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/exec_fsm.hpp>
#include <boost/redis/detail/exec_one_fsm.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/reader_fsm.hpp>
#include <boost/redis/detail/redis_stream.hpp>
#include <boost/redis/detail/run_fsm.hpp>
#include <boost/redis/detail/sentinel_resolve_fsm.hpp>
#include <boost/redis/detail/writer_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
@@ -32,9 +34,11 @@
#include <boost/asio/bind_cancellation_slot.hpp>
#include <boost/asio/bind_executor.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/cancel_after.hpp>
#include <boost/asio/cancel_at.hpp>
#include <boost/asio/cancellation_signal.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/compose.hpp>
#include <boost/asio/deferred.hpp>
#include <boost/asio/error.hpp>
#include <boost/asio/experimental/cancellation_condition.hpp>
@@ -45,6 +49,7 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/write.hpp>
#include <boost/assert.hpp>
#include <boost/config.hpp>
@@ -230,6 +235,7 @@ struct connection_impl {
template <class CompletionToken>
auto async_receive2(CompletionToken&& token)
{
// clang-format off
return
receive_channel_.async_receive(
asio::deferred(
@@ -250,9 +256,104 @@ struct connection_impl {
}
)
)(std::forward<CompletionToken>(token));
// clang-format on
}
};
template <class Executor>
struct exec_one_op {
connection_impl<Executor>* conn_;
const request* req_;
exec_one_fsm fsm_;
explicit exec_one_op(connection_impl<Executor>& conn, const request& req, any_adapter resp)
: conn_(&conn)
, req_(&req)
, fsm_(std::move(resp), req.get_expected_responses())
{ }
template <class Self>
void operator()(Self& self, system::error_code ec = {}, std::size_t bytes_written = 0u)
{
exec_one_action act = fsm_.resume(
conn_->st_.mpx.get_read_buffer(),
ec,
bytes_written,
self.get_cancellation_state().cancelled());
switch (act.type) {
case exec_one_action_type::done: self.complete(act.ec); return;
case exec_one_action_type::write:
asio::async_write(conn_->stream_, asio::buffer(req_->payload()), std::move(self));
return;
case exec_one_action_type::read_some:
conn_->stream_.async_read_some(
conn_->st_.mpx.get_read_buffer().get_prepared(),
std::move(self));
return;
}
}
};
template <class Executor, class CompletionToken>
auto async_exec_one(
connection_impl<Executor>& conn,
const request& req,
any_adapter resp,
CompletionToken&& token)
{
return asio::async_compose<CompletionToken, void(system::error_code)>(
exec_one_op<Executor>{conn, req, std::move(resp)},
token,
conn);
}
template <class Executor>
struct sentinel_resolve_op {
connection_impl<Executor>* conn_;
sentinel_resolve_fsm fsm_;
explicit sentinel_resolve_op(connection_impl<Executor>& conn)
: conn_(&conn)
{ }
template <class Self>
void operator()(Self& self, system::error_code ec = {})
{
auto* conn = conn_; // Prevent potential use-after-move errors with cancel_after
sentinel_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled());
switch (act.get_type()) {
case sentinel_action::type::done: self.complete(act.error()); return;
case sentinel_action::type::connect:
conn->stream_.async_connect(
make_sentinel_connect_params(conn->st_.cfg, act.connect_addr()),
conn->st_.logger,
std::move(self));
return;
case sentinel_action::type::request:
async_exec_one(
*conn,
conn->st_.cfg.sentinel.setup,
make_sentinel_adapter(conn->st_),
asio::cancel_after(
conn->reconnect_timer_, // should be safe to re-use this
conn->st_.cfg.sentinel.request_timeout,
std::move(self)));
return;
}
}
};
template <class Executor, class CompletionToken>
auto async_sentinel_resolve(connection_impl<Executor>& conn, CompletionToken&& token)
{
return asio::async_compose<CompletionToken, void(system::error_code)>(
sentinel_resolve_op<Executor>{conn},
token,
conn);
}
template <class Executor>
struct writer_op {
connection_impl<Executor>* conn_;
@@ -379,8 +480,14 @@ public:
case run_action_type::immediate:
asio::async_immediate(self.get_io_executor(), std::move(self));
return;
case run_action_type::sentinel_resolve:
async_sentinel_resolve(*conn_, std::move(self));
return;
case run_action_type::connect:
conn_->stream_.async_connect(conn_->st_.cfg, conn_->st_.logger, std::move(self));
conn_->stream_.async_connect(
make_run_connect_params(conn_->st_),
conn_->st_.logger,
std::move(self));
return;
case run_action_type::parallel_group:
asio::experimental::make_parallel_group(
@@ -531,6 +638,8 @@ public:
* This function establishes a connection to the Redis server and keeps
* it healthy by performing the following operations:
*
* @li For Sentinel deployments (`config::sentinel::addresses` is not empty),
* contacts Sentinels to obtain the address of the configured master.
* @li For TCP connections, resolves the server hostname passed in
* @ref boost::redis::config::addr.
* @li Establishes a physical connection to the server. For TCP connections,
@@ -728,10 +837,7 @@ public:
* @returns The number of bytes read from the socket.
*/
BOOST_DEPRECATED("Please, use async_receive2 instead.")
std::size_t receive(system::error_code& ec)
{
return impl_->receive(ec);
}
std::size_t receive(system::error_code& ec) { return impl_->receive(ec); }
/** @brief Executes commands on the Redis server asynchronously.
*
@@ -1119,10 +1225,7 @@ public:
/// @copydoc basic_connection::receive
BOOST_DEPRECATED("Please use async_receive2 instead.")
std::size_t receive(system::error_code& ec)
{
return impl_.impl_->receive(ec);
}
std::size_t receive(system::error_code& ec) { return impl_.impl_->receive(ec); }
/**
* @brief Calls @ref boost::redis::basic_connection::async_exec.

View File

@@ -9,8 +9,6 @@
#ifndef BOOST_REDIS_CONNECT_FSM_HPP
#define BOOST_REDIS_CONNECT_FSM_HPP
#include <boost/redis/config.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/system/error_code.hpp>
@@ -62,17 +60,13 @@ struct connect_action {
class connect_fsm {
int resume_point_{0};
const config* cfg_{nullptr};
buffered_logger* lgr_{nullptr};
public:
connect_fsm(const config& cfg, buffered_logger& lgr) noexcept
: cfg_(&cfg)
, lgr_(&lgr)
connect_fsm(buffered_logger& lgr) noexcept
: lgr_(&lgr)
{ }
const config& get_config() const { return *cfg_; }
connect_action resume(
system::error_code ec,
const asio::ip::tcp::resolver::results_type& resolver_results,

View File

@@ -0,0 +1,65 @@
//
// 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)
//
#ifndef BOOST_REDIS_CONNECT_PARAMS_HPP
#define BOOST_REDIS_CONNECT_PARAMS_HPP
// Parameters used by redis_stream::async_connect
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <chrono>
#include <string_view>
namespace boost::redis::detail {
// Fully identifies where a server is listening. Reference type.
class any_address_view {
transport_type type_;
union {
const address* tcp_;
std::string_view unix_;
};
public:
any_address_view(const address& addr, bool use_ssl) noexcept
: type_(use_ssl ? transport_type::tcp_tls : transport_type::tcp)
, tcp_(&addr)
{ }
explicit any_address_view(std::string_view unix_socket) noexcept
: type_(transport_type::unix_socket)
, unix_(unix_socket)
{ }
transport_type type() const { return type_; }
const address& tcp_address() const
{
BOOST_ASSERT(type_ == transport_type::tcp || type_ == transport_type::tcp_tls);
return *tcp_;
}
std::string_view unix_socket() const
{
BOOST_ASSERT(type_ == transport_type::unix_socket);
return unix_;
}
};
struct connect_params {
any_address_view addr;
std::chrono::steady_clock::duration resolve_timeout;
std::chrono::steady_clock::duration connect_timeout;
std::chrono::steady_clock::duration ssl_handshake_timeout;
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -13,20 +13,46 @@
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/response.hpp>
#include <random>
#include <string>
#include <vector>
namespace boost::redis::detail {
// A random engine that gets seeded lazily.
// Seeding with std::random_device is not trivial and might fail.
class lazy_random_engine {
bool seeded_{};
std::minstd_rand eng_;
public:
lazy_random_engine() = default;
std::minstd_rand& get()
{
if (!seeded_) {
eng_.seed(static_cast<std::minstd_rand::result_type>(std::random_device{}()));
seeded_ = true;
}
return eng_;
}
};
// Contains all the members in connection that don't depend on the Executor.
// Makes implementing sans-io algorithms easier
struct connection_state {
buffered_logger logger;
config cfg{};
multiplexer mpx{};
std::string setup_diagnostic{};
std::string diagnostic{}; // Used by the setup request and Sentinel
request ping_req{};
// Sentinel stuff
lazy_random_engine eng{};
std::vector<address> sentinels{};
std::vector<resp3::node> sentinel_resp_nodes{}; // for parsing
};
} // namespace boost::redis::detail

View File

@@ -0,0 +1,69 @@
//
// 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)
//
#ifndef BOOST_REDIS_EXEC_ONE_FSM_HPP
#define BOOST_REDIS_EXEC_ONE_FSM_HPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
// Sans-io algorithm for async_exec_one, as a finite state machine
namespace boost::redis::detail {
class read_buffer;
// What should we do next?
enum class exec_one_action_type
{
done, // Call the final handler
write, // Write the request
read_some, // Read into the read buffer
};
struct exec_one_action {
exec_one_action_type type;
system::error_code ec;
exec_one_action(exec_one_action_type type) noexcept
: type{type}
{ }
exec_one_action(system::error_code ec) noexcept
: type{exec_one_action_type::done}
, ec{ec}
{ }
};
class exec_one_fsm {
int resume_point_{0};
any_adapter adapter_;
std::size_t remaining_responses_;
resp3::parser parser_;
public:
exec_one_fsm(any_adapter resp, std::size_t expected_responses)
: adapter_(std::move(resp))
, remaining_responses_(expected_responses)
{ }
exec_one_action resume(
read_buffer& buffer,
system::error_code ec,
std::size_t bytes_transferred,
asio::cancellation_type_t cancel_state);
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -181,6 +181,12 @@ public:
return std::string_view{write_buffer_}.substr(write_offset_);
}
[[nodiscard]]
auto get_read_buffer() noexcept -> read_buffer&
{
return read_buffer_;
}
[[nodiscard]]
auto get_prepared_read_buffer() noexcept -> read_buffer::span_type;

View File

@@ -9,6 +9,7 @@
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_fsm.hpp>
#include <boost/redis/detail/connect_params.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/logger.hpp>
@@ -24,6 +25,7 @@
#include <boost/asio/ssl/stream.hpp>
#include <boost/asio/ssl/stream_base.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <utility>
@@ -48,12 +50,14 @@ class redis_stream {
struct connect_op {
redis_stream& obj_;
connect_fsm fsm_;
connect_params params_;
template <class Self>
void execute_action(Self& self, connect_action act)
{
auto& obj = this->obj_; // prevent use-after-move errors
const auto& cfg = fsm_.get_config();
// Prevent use-after-move errors
auto& obj = this->obj_;
auto params = this->params_;
switch (act.type) {
case connect_action_type::unix_socket_close:
@@ -70,8 +74,8 @@ class redis_stream {
case connect_action_type::unix_socket_connect:
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
obj.unix_socket_.async_connect(
cfg.unix_socket,
asio::cancel_after(obj.timer_, cfg.connect_timeout, std::move(self)));
params.addr.unix_socket(),
asio::cancel_after(obj.timer_, params.connect_timeout, std::move(self)));
#else
BOOST_ASSERT(false);
#endif
@@ -79,9 +83,9 @@ class redis_stream {
case connect_action_type::tcp_resolve:
obj.resolv_.async_resolve(
cfg.addr.host,
cfg.addr.port,
asio::cancel_after(obj.timer_, cfg.resolve_timeout, std::move(self)));
params.addr.tcp_address().host,
params.addr.tcp_address().port,
asio::cancel_after(obj.timer_, params.resolve_timeout, std::move(self)));
return;
case connect_action_type::ssl_stream_reset:
obj.reset_stream();
@@ -91,7 +95,7 @@ class redis_stream {
case connect_action_type::ssl_handshake:
obj.stream_.async_handshake(
asio::ssl::stream_base::client,
asio::cancel_after(obj.timer_, cfg.ssl_handshake_timeout, std::move(self)));
asio::cancel_after(obj.timer_, params.ssl_handshake_timeout, std::move(self)));
return;
case connect_action_type::done: self.complete(act.ec); break;
// Connect should use the specialized handler, where resolver results are available
@@ -124,11 +128,11 @@ class redis_stream {
{
auto act = fsm_.resume(ec, endpoints, obj_.st_, self.get_cancellation_state().cancelled());
if (act.type == connect_action_type::tcp_connect) {
auto& obj = this->obj_; // prevent use-after-free errors
auto& obj = this->obj_; // prevent use-after-move errors
asio::async_connect(
obj.stream_.next_layer(),
std::move(endpoints),
asio::cancel_after(obj.timer_, fsm_.get_config().connect_timeout, std::move(self)));
asio::cancel_after(obj.timer_, params_.connect_timeout, std::move(self)));
} else {
execute_action(self, act);
}
@@ -172,10 +176,11 @@ public:
// I/O
template <class CompletionToken>
auto async_connect(const config& cfg, buffered_logger& l, CompletionToken&& token)
auto async_connect(const connect_params& params, buffered_logger& l, CompletionToken&& token)
{
this->st_.type = params.addr.type();
return asio::async_compose<CompletionToken, void(system::error_code)>(
connect_op{*this, connect_fsm(cfg, l)},
connect_op{*this, connect_fsm{l}, params},
token);
}

View File

@@ -9,6 +9,8 @@
#ifndef BOOST_REDIS_RUN_FSM_HPP
#define BOOST_REDIS_RUN_FSM_HPP
#include <boost/redis/detail/connect_params.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/system/error_code.hpp>
@@ -25,6 +27,7 @@ enum class run_action_type
done, // Call the final handler
immediate, // Call asio::async_immediate
connect, // Transport connection establishment
sentinel_resolve, // Contact Sentinels to resolve the master's address
parallel_group, // Run the reader, writer and friends
cancel_receive, // Cancel the receiver channel
wait_for_reconnection, // Sleep for the reconnection period
@@ -57,6 +60,8 @@ public:
asio::cancellation_type_t cancel_state);
};
connect_params make_run_connect_params(const connection_state& st);
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -0,0 +1,93 @@
//
// 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)
//
#ifndef BOOST_REDIS_SENTINEL_RESOLVE_FSM_HPP
#define BOOST_REDIS_SENTINEL_RESOLVE_FSM_HPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_params.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
// Sans-io algorithm for async_sentinel_resolve, as a finite state machine
namespace boost::redis::detail {
// Forward decls
struct connection_state;
class sentinel_action {
public:
enum class type
{
done, // Call the final handler
connect, // Transport connection establishment
request, // Send the Sentinel request
};
sentinel_action(system::error_code ec) noexcept
: type_(type::done)
, ec_(ec)
{ }
sentinel_action(const address& addr) noexcept
: type_(type::connect)
, connect_(&addr)
{ }
static sentinel_action request() { return {type::request}; }
type get_type() const { return type_; }
[[nodiscard]]
system::error_code error() const
{
BOOST_ASSERT(type_ == type::done);
return ec_;
}
const address& connect_addr() const
{
BOOST_ASSERT(type_ == type::connect);
return *connect_;
}
private:
type type_;
union {
system::error_code ec_;
const address* connect_;
};
sentinel_action(type type) noexcept
: type_(type)
{ }
};
class sentinel_resolve_fsm {
int resume_point_{0};
std::size_t idx_{0u};
public:
sentinel_resolve_fsm() = default;
sentinel_action resume(
connection_state& st,
system::error_code ec,
asio::cancellation_type_t cancel_state);
};
connect_params make_sentinel_connect_params(const config& cfg, const address& sentinel_addr);
any_adapter make_sentinel_adapter(connection_state& st);
} // namespace boost::redis::detail
#endif // BOOST_REDIS_CONNECTOR_HPP

View File

@@ -94,6 +94,24 @@ enum class error
/// Timeout while writing data to the server.
write_timeout,
/// The configuration specified UNIX sockets with Sentinel, which is not supported.
sentinel_unix_sockets_unsupported,
/// No Sentinel could be used to obtain the address of the Redis server.
/// Sentinels might be unreachable, have authentication misconfigured or may not know about
/// the configured master. Turn logging on for details.
sentinel_resolve_failed,
/// The contacted server is not a master as expected.
/// This is likely a transient failure caused by a Sentinel failover in progress.
role_check_failed,
/// Expects a RESP3 string, but got a different data type.
expects_resp3_string,
/// Expects a RESP3 array, but got a different data type.
expects_resp3_array,
};
/**

View File

@@ -61,20 +61,6 @@ struct log_traits<asio::ip::tcp::resolver::results_type> {
}
};
inline transport_type transport_from_config(const config& cfg)
{
if (cfg.unix_socket.empty()) {
if (cfg.use_ssl) {
return transport_type::tcp_tls;
} else {
return transport_type::tcp;
}
} else {
BOOST_ASSERT(!cfg.use_ssl);
return transport_type::unix_socket;
}
}
inline system::error_code translate_timeout_error(
system::error_code io_ec,
asio::cancellation_type_t cancel_state,
@@ -105,9 +91,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Error resolving the server hostname: ", ec);
log_info(*lgr_, "Connect: hostname resolution failed: ", ec);
} else {
log_info(*lgr_, "Resolve results: ", resolver_results);
log_debug(*lgr_, "Connect: hostname resolution results: ", resolver_results);
}
// Delegate to the regular resume function
@@ -125,9 +111,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Failed to connect to the server: ", ec);
log_info(*lgr_, "Connect: TCP connect failed: ", ec);
} else {
log_info(*lgr_, "Connected to ", selected_endpoint);
log_debug(*lgr_, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint);
}
// Delegate to the regular resume function
@@ -142,9 +128,6 @@ connect_action connect_fsm::resume(
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
// Record the transport that we will be using
st.type = transport_from_config(*cfg_);
if (st.type == transport_type::unix_socket) {
// Reset the socket, to discard any previous state. Ignore any errors
BOOST_REDIS_YIELD(resume_point_, 1, connect_action_type::unix_socket_close)
@@ -160,9 +143,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Failed to connect to the server: ", ec);
log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec);
} else {
log_info(*lgr_, "Connected to ", cfg_->unix_socket);
log_debug(*lgr_, "Connect: UNIX socket connect succeeded");
}
// If this failed, we can't continue
@@ -178,7 +161,7 @@ connect_action connect_fsm::resume(
// Must be done before anything else is done on the stream.
// We don't need to close the TCP socket if using plaintext TCP
// because range-connect closes open sockets, while individual connect doesn't
if (cfg_->use_ssl && st.ssl_stream_used) {
if (st.type == transport_type::tcp_tls && st.ssl_stream_used) {
BOOST_REDIS_YIELD(resume_point_, 3, connect_action_type::ssl_stream_reset)
}
@@ -200,7 +183,7 @@ connect_action connect_fsm::resume(
return ec;
}
if (cfg_->use_ssl) {
if (st.type == transport_type::tcp_tls) {
// Mark the SSL stream as used
st.ssl_stream_used = true;
@@ -212,9 +195,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Failed to perform SSL handshake: ", ec);
log_info(*lgr_, "Connect: SSL handshake failed: ", ec);
} else {
log_info(*lgr_, "Successfully performed SSL handshake");
log_debug(*lgr_, "Connect: SSL handshake succeeded");
}
// If this failed, we can't continue

View File

@@ -55,8 +55,19 @@ struct error_category_impl : system::error_category {
case error::exceeds_maximum_read_buffer_size:
return "Reading data from the socket would exceed the maximum size allowed of the read "
"buffer.";
case error::write_timeout:
return "Timeout while writing data to the server.";
case error::write_timeout: return "Timeout while writing data to the server.";
case error::sentinel_unix_sockets_unsupported:
return "The configuration specified UNIX sockets with Sentinel, which is not "
"supported.";
case error::sentinel_resolve_failed:
return "No Sentinel could be used to obtain the address of the Redis server.";
case error::role_check_failed:
return "The contacted server does not have the expected role. "
"This is likely a transient failure caused by a Sentinel failover in progress.";
case error::expects_resp3_string:
return "Expects a RESP3 string, but got a different data type.";
case error::expects_resp3_array:
return "Expects a RESP3 array, but got a different data type.";
default: BOOST_ASSERT(false); return "Boost.Redis error.";
}
}

View File

@@ -0,0 +1,95 @@
//
// 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)
//
#ifndef BOOST_REDIS_EXEC_ONE_FSM_IPP
#define BOOST_REDIS_EXEC_ONE_FSM_IPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/exec_one_fsm.hpp>
#include <boost/redis/detail/read_buffer.hpp>
#include <boost/redis/impl/is_terminal_cancel.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/parser.hpp>
#include <boost/asio/cancellation_type.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
namespace boost::redis::detail {
exec_one_action exec_one_fsm::resume(
read_buffer& buffer,
system::error_code ec,
std::size_t bytes_transferred,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
// Send the request to the server
BOOST_REDIS_YIELD(resume_point_, 1, exec_one_action_type::write)
// Errors and cancellations
if (is_terminal_cancel(cancel_state))
return system::error_code{asio::error::operation_aborted};
if (ec)
return ec;
// If the request didn't expect any response, we're done
if (remaining_responses_ == 0u)
return system::error_code{};
// Read responses until we're done
buffer.clear();
while (true) {
// Prepare the buffer to read some data
ec = buffer.prepare();
if (ec)
return ec;
// Read data
BOOST_REDIS_YIELD(resume_point_, 2, exec_one_action_type::read_some)
// Errors and cancellations
if (is_terminal_cancel(cancel_state))
return system::error_code{asio::error::operation_aborted};
if (ec)
return ec;
// Commit the data into the buffer
buffer.commit(bytes_transferred);
// Consume the data until we run out or all the responses have been read
while (resp3::parse(parser_, buffer.get_commited(), adapter_, ec)) {
// Check for errors
if (ec)
return ec;
// We've finished parsing a response
buffer.consume(parser_.get_consumed());
parser_.reset();
// When no more responses remain, we're done.
// Don't read ahead, even if more data is available
if (--remaining_responses_ == 0u)
return system::error_code{};
}
}
}
BOOST_ASSERT(false);
return system::error_code();
}
} // namespace boost::redis::detail
#endif

View File

@@ -7,6 +7,7 @@
#ifndef BOOST_REDIS_LOG_UTILS_HPP
#define BOOST_REDIS_LOG_UTILS_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/logger.hpp>
#include <boost/core/ignore_unused.hpp>
@@ -48,6 +49,16 @@ struct log_traits<system::error_code> {
}
};
template <>
struct log_traits<address> {
static inline void log(std::string& to, const address& value)
{
to += value.host;
to += ':';
to += value.port;
}
};
template <class... Args>
void format_log_args(std::string& to, const Args&... args)
{

View File

@@ -8,12 +8,15 @@
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_params.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/multiplexer.hpp>
#include <boost/redis/detail/run_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/impl/is_terminal_cancel.hpp>
#include <boost/redis/impl/log_utils.hpp>
#include <boost/redis/impl/sentinel_utils.hpp>
#include <boost/redis/impl/setup_request_utils.hpp>
#include <boost/asio/cancellation_type.hpp>
@@ -28,6 +31,8 @@ inline system::error_code check_config(const config& cfg)
if (!cfg.unix_socket.empty()) {
if (cfg.use_ssl)
return error::unix_sockets_ssl_unsupported;
if (use_sentinel(cfg))
return error::sentinel_unix_sockets_unsupported;
#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS
return error::unix_sockets_unsupported;
#endif
@@ -41,45 +46,44 @@ inline void compose_ping_request(const config& cfg, request& to)
to.push("PING", cfg.health_check_id);
}
inline void process_setup_node(
connection_state& st,
resp3::basic_node<std::string_view> const& nd,
system::error_code& ec)
{
switch (nd.data_type) {
case resp3::type::simple_error:
case resp3::type::blob_error:
case resp3::type::null:
ec = redis::error::resp3_hello;
st.setup_diagnostic = nd.value;
break;
default:;
}
}
inline any_adapter make_setup_adapter(connection_state& st)
{
return any_adapter{
[&st](any_adapter::parse_event evt, resp3::node_view const& nd, system::error_code& ec) {
if (evt == any_adapter::parse_event::node)
process_setup_node(st, nd, ec);
}};
}
inline void on_setup_done(const multiplexer::elem& elm, connection_state& st)
{
const auto ec = elm.get_error();
if (ec) {
if (st.setup_diagnostic.empty()) {
if (st.diagnostic.empty()) {
log_info(st.logger, "Setup request execution: ", ec);
} else {
log_info(st.logger, "Setup request execution: ", ec, " (", st.setup_diagnostic, ")");
log_info(st.logger, "Setup request execution: ", ec, " (", st.diagnostic, ")");
}
} else {
log_info(st.logger, "Setup request execution: success");
}
}
inline any_address_view get_server_address(const connection_state& st)
{
if (st.cfg.unix_socket.empty()) {
return {st.cfg.addr, st.cfg.use_ssl};
} else {
return any_address_view{st.cfg.unix_socket};
}
}
template <>
struct log_traits<any_address_view> {
static inline void log(std::string& to, any_address_view value)
{
if (value.type() == transport_type::unix_socket) {
to += '\'';
to += value.unix_socket();
to += '\'';
} else {
log_traits<address>::log(to, value.tcp_address());
to += value.type() == transport_type::tcp_tls ? " (TLS enabled)" : " (TLS disabled)";
}
}
};
run_action run_fsm::resume(
connection_state& st,
system::error_code ec,
@@ -103,9 +107,34 @@ run_action run_fsm::resume(
// Compose the PING request. Same as above
compose_ping_request(st.cfg, st.ping_req);
if (use_sentinel(st.cfg)) {
// Sentinel request. Same as above
compose_sentinel_request(st.cfg);
// Bootstrap the sentinel list with the ones configured by the user
st.sentinels = st.cfg.sentinel.addresses;
}
for (;;) {
// Sentinel resolve, if required. This leaves the address in st.cfg.address
if (use_sentinel(st.cfg)) {
// This operation does the logging for us.
BOOST_REDIS_YIELD(resume_point_, 2, run_action_type::sentinel_resolve)
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
log_debug(st.logger, "Run: cancelled (4)");
return {asio::error::operation_aborted};
}
// Check for errors
if (ec)
goto sleep_and_reconnect;
}
// Try to connect
BOOST_REDIS_YIELD(resume_point_, 2, run_action_type::connect)
log_info(st.logger, "Trying to connect to Redis server at ", get_server_address(st));
BOOST_REDIS_YIELD(resume_point_, 4, run_action_type::connect)
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
@@ -113,15 +142,27 @@ run_action run_fsm::resume(
return system::error_code(asio::error::operation_aborted);
}
// If we were successful, run all the connection tasks
if (!ec) {
if (ec) {
// There was an error. Skip to the reconnection loop
log_info(
st.logger,
"Failed to connect to Redis server at ",
get_server_address(st),
": ",
ec);
goto sleep_and_reconnect;
}
// We were successful
log_info(st.logger, "Connected to Redis server at ", get_server_address(st));
// Initialization
st.mpx.reset();
st.setup_diagnostic.clear();
st.diagnostic.clear();
// Add the setup request to the multiplexer
if (st.cfg.setup.get_commands() != 0u) {
auto elm = make_elem(st.cfg.setup, make_setup_adapter(st));
auto elm = make_elem(st.cfg.setup, make_any_adapter_impl(setup_adapter{st}));
elm->set_done_callback([&elem_ref = *elm, &st] {
on_setup_done(elem_ref, st);
});
@@ -129,7 +170,7 @@ run_action run_fsm::resume(
}
// Run the tasks
BOOST_REDIS_YIELD(resume_point_, 3, run_action_type::parallel_group)
BOOST_REDIS_YIELD(resume_point_, 5, run_action_type::parallel_group)
// Store any error yielded by the tasks for later
stored_ec_ = ec;
@@ -141,11 +182,10 @@ run_action run_fsm::resume(
// The receive operation must be cancelled because channel
// subscription does not survive a reconnection but requires
// re-subscription.
BOOST_REDIS_YIELD(resume_point_, 4, run_action_type::cancel_receive)
BOOST_REDIS_YIELD(resume_point_, 6, run_action_type::cancel_receive)
// Restore the error
ec = stored_ec_;
}
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
@@ -153,13 +193,15 @@ run_action run_fsm::resume(
return system::error_code(asio::error::operation_aborted);
}
sleep_and_reconnect:
// If we are not going to try again, we're done
if (st.cfg.reconnect_wait_interval.count() == 0) {
return ec;
}
// Wait for the reconnection interval
BOOST_REDIS_YIELD(resume_point_, 5, run_action_type::wait_for_reconnection)
BOOST_REDIS_YIELD(resume_point_, 7, run_action_type::wait_for_reconnection)
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
@@ -174,4 +216,14 @@ run_action run_fsm::resume(
return system::error_code();
}
connect_params make_run_connect_params(const connection_state& st)
{
return {
get_server_address(st),
st.cfg.resolve_timeout,
st.cfg.connect_timeout,
st.cfg.ssl_handshake_timeout,
};
}
} // namespace boost::redis::detail

View File

@@ -0,0 +1,182 @@
//
// 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)
//
#ifndef BOOST_REDIS_SENTINEL_RESOLVE_FSM_IPP
#define BOOST_REDIS_SENTINEL_RESOLVE_FSM_IPP
#include <boost/redis/adapter/any_adapter.hpp>
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connect_params.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/detail/coroutine.hpp>
#include <boost/redis/detail/sentinel_resolve_fsm.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/impl/is_terminal_cancel.hpp>
#include <boost/redis/impl/log_utils.hpp>
#include <boost/redis/impl/sentinel_utils.hpp>
#include <boost/asio/error.hpp>
#include <boost/assert.hpp>
#include <cstddef>
#include <random>
#include <string_view>
namespace boost::redis::detail {
// Logs an error at info level, and also stores it in the state,
// so it can be logged at error level if all Sentinels fail.
template <class... Args>
void log_sentinel_error(connection_state& st, std::size_t current_idx, const Args&... args)
{
st.diagnostic += "\n ";
std::size_t size_before = st.diagnostic.size();
format_log_args(st.diagnostic, "Sentinel at ", st.sentinels[current_idx], ": ", args...);
log_info(st.logger, std::string_view{st.diagnostic}.substr(size_before));
}
sentinel_action sentinel_resolve_fsm::resume(
connection_state& st,
system::error_code ec,
asio::cancellation_type_t cancel_state)
{
switch (resume_point_) {
BOOST_REDIS_CORO_INITIAL
st.diagnostic.clear();
log_info(
st.logger,
"Trying to resolve the address of ",
st.cfg.sentinel.server_role == role::master ? "master" : "a replica of master",
" '",
st.cfg.sentinel.master_name,
"' using Sentinel");
// Try all Sentinels in order. Upon any errors, save the diagnostic and try with the next one.
// If none of them are available, print an error diagnostic and fail.
for (idx_ = 0u; idx_ < st.sentinels.size(); ++idx_) {
log_debug(st.logger, "Trying to contact Sentinel at ", st.sentinels[idx_]);
// Try to connect
BOOST_REDIS_YIELD(resume_point_, 1, st.sentinels[idx_])
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
log_debug(st.logger, "Sentinel resolve: cancelled (1)");
return system::error_code(asio::error::operation_aborted);
}
// Check for errors
if (ec) {
log_sentinel_error(st, idx_, "connection establishment error: ", ec);
continue;
}
// Execute the Sentinel request
log_debug(st.logger, "Executing Sentinel request at ", st.sentinels[idx_]);
st.sentinel_resp_nodes.clear();
BOOST_REDIS_YIELD(resume_point_, 2, sentinel_action::request())
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
log_debug(st.logger, "Sentinel resolve: cancelled (2)");
return system::error_code(asio::error::operation_aborted);
}
// Check for errors
if (ec) {
log_sentinel_error(st, idx_, "error while executing request: ", ec);
continue;
}
// Parse the response
sentinel_response resp;
ec = parse_sentinel_response(st.sentinel_resp_nodes, st.cfg.sentinel.server_role, resp);
if (ec) {
if (ec == error::resp3_simple_error || ec == error::resp3_blob_error) {
log_sentinel_error(st, idx_, "responded with an error: ", resp.diagnostic);
} else if (ec == error::resp3_null) {
log_sentinel_error(st, idx_, "doesn't know about the configured master");
} else {
log_sentinel_error(
st,
idx_,
"error parsing response (maybe forgot to upgrade to RESP3?): ",
ec);
}
continue;
}
// When asking for replicas, we might get no replicas
if (st.cfg.sentinel.server_role == role::replica && resp.replicas.empty()) {
log_sentinel_error(st, idx_, "the configured master has no replicas");
continue;
}
// Store the resulting address in a well-known place
if (st.cfg.sentinel.server_role == role::master) {
st.cfg.addr = resp.master_addr;
} else {
// Choose a random replica
std::uniform_int_distribution<std::size_t> dist{0u, resp.replicas.size() - 1u};
const auto idx = dist(st.eng.get());
st.cfg.addr = resp.replicas[idx];
}
// Sentinel knows about this master. Log and update our config
log_info(
st.logger,
"Sentinel at ",
st.sentinels[idx_],
" resolved the server address to ",
st.cfg.addr);
update_sentinel_list(st.sentinels, idx_, resp.sentinels, st.cfg.sentinel.addresses);
st.sentinel_resp_nodes.clear(); // reduce memory consumption
return system::error_code();
}
// No Sentinel resolved our address
log_err(
st.logger,
"Failed to resolve the address of ",
st.cfg.sentinel.server_role == role::master ? "master" : "a replica of master",
" '",
st.cfg.sentinel.master_name,
"'. Tried the following Sentinels:",
st.diagnostic);
return {error::sentinel_resolve_failed};
}
// We should never get here
BOOST_ASSERT(false);
return system::error_code();
}
connect_params make_sentinel_connect_params(const config& cfg, const address& addr)
{
return {
any_address_view{addr, cfg.sentinel.use_ssl},
cfg.sentinel.resolve_timeout,
cfg.sentinel.connect_timeout,
cfg.sentinel.ssl_handshake_timeout,
};
}
any_adapter make_sentinel_adapter(connection_state& st)
{
return any_adapter(st.sentinel_resp_nodes);
}
} // namespace boost::redis::detail
#endif

View File

@@ -0,0 +1,277 @@
//
// 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)
//
#ifndef BOOST_REDIS_SENTINEL_UTILS_HPP
#define BOOST_REDIS_SENTINEL_UTILS_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/assert.hpp>
#include <boost/core/ignore_unused.hpp>
#include <boost/core/span.hpp>
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace boost::redis::detail {
// Returns true if Sentinel should be used
inline bool use_sentinel(const config& cfg) { return !cfg.sentinel.addresses.empty(); }
// Composes the request to send to Sentinel modifying cfg.sentinel.setup
inline void compose_sentinel_request(config& cfg)
{
// These commands should go after the user-supplied setup, as this might involve authentication.
// We ask for the master even when connecting to replicas to correctly detect when the master doesn't exist
cfg.sentinel.setup.push("SENTINEL", "GET-MASTER-ADDR-BY-NAME", cfg.sentinel.master_name);
if (cfg.sentinel.server_role == role::replica)
cfg.sentinel.setup.push("SENTINEL", "REPLICAS", cfg.sentinel.master_name);
cfg.sentinel.setup.push("SENTINEL", "SENTINELS", cfg.sentinel.master_name);
// Note that we don't care about request flags because this is a one-time request
}
// Parses a list of replicas or sentinels
inline system::error_code parse_server_list(
const resp3::node*& first,
const resp3::node* last,
std::vector<address>& out)
{
const auto* it = first;
ignore_unused(last);
// The root node must be an array
BOOST_ASSERT(it != last);
BOOST_ASSERT(it->depth == 0u);
if (it->data_type != resp3::type::array)
return {error::expects_resp3_array};
const std::size_t num_servers = it->aggregate_size;
++it;
// Each element in the array represents a server
out.resize(num_servers);
for (std::size_t i = 0u; i < num_servers; ++i) {
// A server is a map (resp3) or array (resp2, currently unsupported)
BOOST_ASSERT(it != last);
BOOST_ASSERT(it->depth == 1u);
if (it->data_type != resp3::type::map)
return {error::expects_resp3_map};
const std::size_t num_key_values = it->aggregate_size;
++it;
// The server object is composed by a set of key/value pairs.
// Skip everything except for the ones we care for.
bool ip_seen = false, port_seen = false;
for (std::size_t j = 0; j < num_key_values; ++j) {
// Key. It should be a string
BOOST_ASSERT(it != last);
BOOST_ASSERT(it->depth == 2u);
if (it->data_type != resp3::type::blob_string)
return {error::expects_resp3_string};
const std::string_view key = it->value;
++it;
// Value. All values seem to be strings, too.
BOOST_ASSERT(it != last);
BOOST_ASSERT(it->depth == 2u);
if (it->data_type != resp3::type::blob_string)
return {error::expects_resp3_string};
// Record it
if (key == "ip") {
ip_seen = true;
out[i].host = it->value;
} else if (key == "port") {
port_seen = true;
out[i].port = it->value;
}
++it;
}
// Check that the response actually contained the fields we wanted
if (!ip_seen || !port_seen)
return {error::empty_field};
}
// Done
first = it;
return system::error_code();
}
// The output type of parse_sentinel_response
struct sentinel_response {
std::string diagnostic; // In case the server returned an error
address master_addr; // Always populated
std::vector<address> replicas; // Populated only when connecting to replicas
std::vector<address> sentinels;
};
// Parses an array of nodes into a sentinel_response.
// The request originating this response should be:
// <user-supplied commands, as per sentinel_config::setup>
// SENTINEL GET-MASTER-ADDR-BY-NAME
// SENTINEL REPLICAS (only if server_role is replica)
// SENTINEL SENTINELS
// SENTINEL SENTINELS and SENTINEL REPLICAS error when the master name is unknown. Error nodes
// should be allowed in the node array.
// This means that we can't use generic_response, since its adapter errors on error nodes.
// SENTINEL GET-MASTER-ADDR-BY-NAME is sent even when connecting to replicas
// for better diagnostics when the master name is unknown.
// Preconditions:
// * There are at least 2 (master)/3 (replica) root nodes.
// * The node array originates from parsing a valid RESP3 message.
// E.g. we won't check that the first node has depth 0.
inline system::error_code parse_sentinel_response(
span<const resp3::node> nodes,
role server_role,
sentinel_response& out)
{
auto check_errors = [&out](const resp3::node& nd) {
switch (nd.data_type) {
case resp3::type::simple_error:
out.diagnostic = nd.value;
return system::error_code(error::resp3_simple_error);
case resp3::type::blob_error:
out.diagnostic = nd.value;
return system::error_code(error::resp3_blob_error);
default: return system::error_code();
}
};
// Clear the output
out.diagnostic.clear();
out.sentinels.clear();
out.replicas.clear();
// Find the first root node of interest. It's the 2nd or 3rd, starting with the end
auto find_first = [nodes, server_role] {
const std::size_t expected_roots = server_role == role::master ? 2u : 3u;
std::size_t roots_seen = 0u;
for (auto it = nodes.rbegin();; ++it) {
BOOST_ASSERT(it != nodes.rend());
if (it->depth == 0u && ++roots_seen == expected_roots)
return &*it;
}
};
const resp3::node* lib_first = find_first();
// Iterators
const resp3::node* it = nodes.begin();
const resp3::node* last = nodes.end();
ignore_unused(last);
// Go through all the responses to user-supplied requests checking for errors
for (; it != lib_first; ++it) {
if (auto ec = check_errors(*it))
return ec;
}
// SENTINEL GET-MASTER-ADDR-BY-NAME
// Check for errors
if (auto ec = check_errors(*it))
return ec;
// If the root node is NULL, Sentinel doesn't know about this master.
// We use resp3_null to signal this fact. This doesn't reach the end user.
if (it->data_type == resp3::type::null) {
return {error::resp3_null};
}
// If the root node is an array, an IP and port follow
if (it->data_type != resp3::type::array)
return {error::expects_resp3_array};
if (it->aggregate_size != 2u)
return {error::incompatible_size};
++it;
// IP
BOOST_ASSERT(it != last);
BOOST_ASSERT(it->depth == 1u);
if (it->data_type != resp3::type::blob_string)
return {error::expects_resp3_string};
out.master_addr.host = it->value;
++it;
// Port
BOOST_ASSERT(it != last);
BOOST_ASSERT(it->depth == 1u);
if (it->data_type != resp3::type::blob_string)
return {error::expects_resp3_string};
out.master_addr.port = it->value;
++it;
if (server_role == role::replica) {
// SENTINEL REPLICAS
// This request fails if Sentinel doesn't know about this master.
// However, that's not the case if we got here.
// Check for other errors.
if (auto ec = check_errors(*it))
return ec;
// Actual parsing
if (auto ec = parse_server_list(it, last, out.replicas))
return ec;
}
// SENTINEL SENTINELS
// This request fails if Sentinel doesn't know about this master.
// However, that's not the case if we got here.
// Check for other errors.
if (auto ec = check_errors(*it))
return ec;
// Actual parsing
if (auto ec = parse_server_list(it, last, out.sentinels))
return ec;
// Done
return system::error_code();
}
// Updates the internal Sentinel list.
// to should never be empty
inline void update_sentinel_list(
std::vector<address>& to,
std::size_t current_index, // the one to maintain and place first
span<const address> gossip_sentinels, // the ones that SENTINEL SENTINELS returned
span<const address> bootstrap_sentinels // the ones the user supplied
)
{
BOOST_ASSERT(!to.empty());
// Remove everything, except the Sentinel that succeeded
if (current_index != 0u)
std::swap(to.front(), to[current_index]);
to.resize(1u);
// Add one group. These Sentinels are always unique and don't include the one we're currently connected to.
to.insert(to.end(), gossip_sentinels.begin(), gossip_sentinels.end());
// Insert any user-supplied sentinels, if not already present.
// This is O(n^2), but is okay because n will be small.
// The list can't be sorted, anyway
for (const auto& sentinel : bootstrap_sentinels) {
if (std::find(to.begin(), to.end(), sentinel) == to.end())
to.push_back(sentinel);
}
}
} // namespace boost::redis::detail
#endif

View File

@@ -8,19 +8,27 @@
#define BOOST_REDIS_SETUP_REQUEST_UTILS_HPP
#include <boost/redis/config.hpp>
#include <boost/redis/detail/connection_state.hpp>
#include <boost/redis/error.hpp>
#include <boost/redis/impl/sentinel_utils.hpp> // use_sentinel
#include <boost/redis/request.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/response.hpp>
#include <cstddef>
namespace boost::redis::detail {
// Modifies config::setup to make a request suitable to be sent
// to the server using async_exec
inline void compose_setup_request(config& cfg)
{
auto& req = cfg.setup;
if (!cfg.use_setup) {
// We're not using the setup request as-is, but should compose one based on
// the values passed by the user
auto& req = cfg.setup;
req.clear();
// Which parts of the command should we send?
@@ -46,14 +54,71 @@ inline void compose_setup_request(config& cfg)
req.push("SELECT", cfg.database_index.value());
}
// When using Sentinel, we should add a role check.
// This must happen after the other commands, as it requires authentication.
if (use_sentinel(cfg))
req.push("ROLE");
// In any case, the setup request should have the priority
// flag set so it's executed before any other request.
// The setup request should never be retried.
request_access::set_priority(cfg.setup, true);
cfg.setup.get_config().cancel_if_unresponded = true;
cfg.setup.get_config().cancel_on_connection_lost = true;
request_access::set_priority(req, true);
req.get_config().cancel_if_unresponded = true;
req.get_config().cancel_on_connection_lost = true;
}
class setup_adapter {
connection_state* st_;
std::size_t response_idx_{0u};
bool role_seen_{false};
system::error_code on_node_impl(const resp3::node_view& nd)
{
// An error node is always an error
switch (nd.data_type) {
case resp3::type::simple_error:
case resp3::type::blob_error: st_->diagnostic = nd.value; return error::resp3_hello;
default: ;
}
// When using Sentinel, we add a ROLE command at the end.
// We need to ensure that this instance is a master.
if (use_sentinel(st_->cfg) && response_idx_ == st_->cfg.setup.get_expected_responses() - 1u) {
// ROLE's response should be an array of at least 1 element
if (nd.depth == 0u) {
if (nd.data_type != resp3::type::array)
return error::invalid_data_type;
if (nd.aggregate_size == 0u)
return error::incompatible_size;
}
// The first node should be 'master' if we're connecting to a primary,
// 'slave' if we're connecting to a replica
if (nd.depth == 1u && !role_seen_) {
role_seen_ = true;
if (nd.data_type != resp3::type::blob_string)
return error::invalid_data_type;
const char* expected_role = st_->cfg.sentinel.server_role == role::master ? "master"
: "slave";
if (nd.value != expected_role)
return error::role_check_failed;
}
}
return system::error_code();
}
public:
explicit setup_adapter(connection_state& st) noexcept
: st_(&st)
{ }
void on_init() { }
void on_done() { ++response_idx_; }
void on_node(const resp3::node_view& node, system::error_code& ec) { ec = on_node_impl(node); }
};
} // namespace boost::redis::detail
#endif // BOOST_REDIS_RUNNER_HPP

View File

@@ -8,6 +8,7 @@
#include <boost/redis/impl/connection.ipp>
#include <boost/redis/impl/error.ipp>
#include <boost/redis/impl/exec_fsm.ipp>
#include <boost/redis/impl/exec_one_fsm.ipp>
#include <boost/redis/impl/ignore.ipp>
#include <boost/redis/impl/logger.ipp>
#include <boost/redis/impl/multiplexer.ipp>
@@ -16,6 +17,7 @@
#include <boost/redis/impl/request.ipp>
#include <boost/redis/impl/response.ipp>
#include <boost/redis/impl/run_fsm.ipp>
#include <boost/redis/impl/sentinel_resolve_fsm.ipp>
#include <boost/redis/impl/writer_fsm.ipp>
#include <boost/redis/impl/flat_tree.ipp>
#include <boost/redis/resp3/impl/parser.ipp>

View File

@@ -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(

View File

@@ -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

View File

@@ -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';
}};
}

View File

@@ -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
View 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

View File

@@ -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

View File

@@ -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
View 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();
}

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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
View 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();
}

View File

@@ -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)

View File

@@ -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,16 +339,6 @@ 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)
{
@@ -355,7 +347,7 @@ std::ostream& operator<<(std::ostream& os, basic_tree<String> const& resp)
return os;
}
}
} // namespace boost::redis::resp3
node from_node_view(node_view const& v)
{
@@ -385,7 +377,6 @@ tree from_flat(generic_flat_response const& resp)
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)

View File

@@ -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)

View 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();
}

View File

@@ -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,8 +301,9 @@ 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::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
{logger::level::debug, "Run: cancelled (1)" }
});
}
@@ -218,8 +322,9 @@ 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::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,8 +405,10 @@ 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::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,8 +430,10 @@ 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::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,8 +460,10 @@ 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::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,8 +489,10 @@ 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::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,12 +670,150 @@ 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)" }
});
}
// 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)"},
});
}
} // namespace
int main()
@@ -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();
}

View 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
View 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();
}

View File

@@ -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();
}

View File

@@ -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;

View 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();
}

View File

@@ -1,19 +1,140 @@
services:
redis:
redis-master:
container_name: redis-master
image: ${SERVER_IMAGE}
entrypoint: "/docker/entrypoint.sh"
network_mode: host
command: >
sh -c 'chmod 777 /tmp/redis-socks &&
redis-server \
--replica-announce-ip localhost \
--port 6379 \
--tls-port 16379 \
--tls-cert-file /docker/tls/server.crt \
--tls-key-file /docker/tls/server.key \
--tls-ca-cert-file /docker/tls/ca.crt \
--tls-auth-clients no \
--unixsocket /tmp/redis-socks/redis.sock \
--unixsocketperm 777'
volumes:
- ./docker:/docker
- /tmp/redis-socks:/tmp/redis-socks
ports:
- 6379:6379
- 6380:6380
redis-replica-1:
container_name: redis-replica-1
image: ${SERVER_IMAGE}
network_mode: host
command:
[
"redis-server",
"--replica-announce-ip", "localhost",
"--replicaof", "localhost", "6379",
"--port", "6380",
"--tls-port", "16380",
"--tls-cert-file", "/docker/tls/server.crt",
"--tls-key-file", "/docker/tls/server.key",
"--tls-ca-cert-file", "/docker/tls/ca.crt",
"--tls-auth-clients", "no",
]
volumes:
- ./docker:/docker
redis-replica-2:
container_name: redis-replica-2
image: ${SERVER_IMAGE}
network_mode: host
command:
[
"redis-server",
"--replica-announce-ip", "localhost",
"--replicaof", "localhost", "6379",
"--port", "6381",
"--tls-port", "16381",
"--tls-cert-file", "/docker/tls/server.crt",
"--tls-key-file", "/docker/tls/server.key",
"--tls-ca-cert-file", "/docker/tls/ca.crt",
"--tls-auth-clients", "no",
]
volumes:
- ./docker:/docker
sentinel-1:
container_name: sentinel-1
image: ${SERVER_IMAGE}
network_mode: host
command: >
sh -c 'cat << EOF > /etc/sentinel.conf && redis-sentinel /etc/sentinel.conf
port 26379
tls-port 36379
tls-cert-file /docker/tls/server.crt
tls-key-file /docker/tls/server.key
tls-ca-cert-file /docker/tls/ca.crt
tls-auth-clients no
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel announce-ip localhost
sentinel monitor mymaster localhost 6379 2
sentinel down-after-milliseconds mymaster 10000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
EOF'
volumes:
- ./docker:/docker
sentinel-2:
container_name: sentinel-2
image: ${SERVER_IMAGE}
network_mode: host
command: >
sh -c 'cat << EOF > /etc/sentinel.conf && redis-sentinel /etc/sentinel.conf
port 26380
tls-port 36380
tls-cert-file /docker/tls/server.crt
tls-key-file /docker/tls/server.key
tls-ca-cert-file /docker/tls/ca.crt
tls-auth-clients no
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel announce-ip localhost
sentinel monitor mymaster localhost 6379 2
sentinel down-after-milliseconds mymaster 10000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
EOF'
volumes:
- ./docker:/docker
sentinel-3:
container_name: sentinel-3
image: ${SERVER_IMAGE}
network_mode: host
command: >
sh -c 'cat << EOF > /etc/sentinel.conf && redis-sentinel /etc/sentinel.conf
port 26381
tls-port 36381
tls-cert-file /docker/tls/server.crt
tls-key-file /docker/tls/server.key
tls-ca-cert-file /docker/tls/ca.crt
tls-auth-clients no
sentinel resolve-hostnames yes
sentinel announce-hostnames yes
sentinel announce-ip localhost
sentinel monitor mymaster localhost 6379 2
sentinel down-after-milliseconds mymaster 10000
sentinel failover-timeout mymaster 10000
sentinel parallel-syncs mymaster 1
EOF'
volumes:
- ./docker:/docker
builder:
image: ${BUILDER_IMAGE}
container_name: builder
image: ${BUILDER_IMAGE}
network_mode: host
tty: true
environment:
- BOOST_REDIS_TEST_SERVER=redis
volumes:
- ../:/boost-redis
- /tmp/redis-socks:/tmp/redis-socks

View File

@@ -1,16 +0,0 @@
#!/bin/sh
# The Redis container entrypoint. Runs the server with the required
# flags and makes the socket accessible
set -e
chmod 777 /tmp/redis-socks
redis-server \
--tls-port 6380 \
--tls-cert-file /docker/tls/server.crt \
--tls-key-file /docker/tls/server.key \
--tls-ca-cert-file /docker/tls/ca.crt \
--tls-auth-clients no \
--unixsocket /tmp/redis-socks/redis.sock \
--unixsocketperm 777