2
0
mirror of https://github.com/boostorg/redis.git synced 2026-01-20 17:12:09 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Ruben Perez
3324e7ee71 Merge branch 'develop' 2025-11-14 10:53:00 +01:00
Ruben Perez
5af2f9cb72 Merge branch 'develop' 2025-11-03 10:34:28 +01:00
Anarthal (Rubén Pérez)
cd46e39f36 Boost 1.90.0 beta1: merge develop to master 2025-10-27 11:03:05 +01:00
84 changed files with 702 additions and 7782 deletions

View File

@@ -77,7 +77,6 @@ if (BOOST_REDIS_MAIN_PROJECT)
test
json
endian
compat
)
foreach(dep IN LISTS deps)

View File

@@ -87,9 +87,9 @@ them are:
* [Client-side caching](https://redis.io/docs/manual/client-side-caching/).
The connection class supports server pushes by means of the
`connection::async_receive2` function, which can be
`connection::async_receive` function, which can be
called in the same connection that is being used to execute commands.
The coroutine below shows how to use it
The coroutine below shows how to use it:
```cpp
@@ -99,25 +99,26 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
request req;
req.push("SUBSCRIBE", "channel");
flat_tree resp;
generic_response resp;
conn->set_receive_response(resp);
// Loop while reconnection is enabled
while (conn->will_reconnect()) {
// Reconnect to channels.
co_await conn->async_exec(req);
co_await conn->async_exec(req, ignore);
// Loop reading Redis pushes.
for (error_code ec;;) {
co_await conn->async_receive2(resp, redirect_error(ec));
for (;;) {
error_code ec;
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
if (ec)
break; // Connection lost, break so we can reconnect to channels.
// Use the response resp in some way and then clear it.
...
resp.clear();
consume_one(resp);
}
}
}
@@ -125,4 +126,4 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
## Further reading
Full documentation is [here](https://www.boost.org/doc/libs/master/libs/redis/index.html).
Full documentation is [here](https://www.boost.org/doc/libs/master/libs/redis/index.html).

View File

@@ -3,7 +3,6 @@
* 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_sentinel.cpp[cpp20_sentinel.cpp]: Shows how to use the library with a Sentinel deployment.
* {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_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

@@ -97,9 +97,9 @@ them are:
* https://redis.io/docs/manual/client-side-caching/[Client-side caching].
The connection class supports server pushes by means of the
xref:reference:boost/redis/basic_connection/async_receive.adoc[`connection::async_receive2`] function, which can be
xref:reference:boost/redis/basic_connection/async_receive.adoc[`connection::async_receive`] function, which can be
called in the same connection that is being used to execute commands.
The coroutine below shows how to use it
The coroutine below shows how to use it:
[source,cpp]
@@ -110,25 +110,26 @@ receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
request req;
req.push("SUBSCRIBE", "channel");
flat_tree resp;
generic_response resp;
conn->set_receive_response(resp);
// Loop while reconnection is enabled
while (conn->will_reconnect()) {
// Reconnect to channels.
co_await conn->async_exec(req);
co_await conn->async_exec(req, ignore);
// Loop reading Redis pushes.
for (error_code ec;;) {
co_await conn->async_receive2(resp, redirect_error(ec));
for (;;) {
error_code ec;
co_await conn->async_receive(resp, net::redirect_error(net::use_awaitable, ec));
if (ec)
break; // Connection lost, break so we can reconnect to channels.
// Use the response here and then clear it.
// Use the response resp in some way and then clear it.
...
resp.clear();
consume_one(resp);
}
}
}

View File

@@ -25,12 +25,8 @@ 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`]
@@ -55,8 +51,6 @@ xref:reference:boost/redis/response.adoc[`response`]
xref:reference:boost/redis/generic_response.adoc[`generic_response`]
xref:reference:boost/redis/generic_flat_response.adoc[`generic_flat_response`]
xref:reference:boost/redis/consume_one-08.adoc[`consume_one`]
@@ -72,33 +66,25 @@ xref:reference:boost/redis/adapter/result.adoc[`adapter::result`]
xref:reference:boost/redis/any_adapter.adoc[`any_adapter`]
|
xref:reference:boost/redis/resp3/basic_node.adoc[`resp3::basic_node`]
xref:reference:boost/redis/resp3/basic_node.adoc[`basic_node`]
xref:reference:boost/redis/resp3/node.adoc[`resp3::node`]
xref:reference:boost/redis/resp3/node.adoc[`node`]
xref:reference:boost/redis/resp3/node_view.adoc[`resp3::node_view`]
xref:reference:boost/redis/resp3/basic_tree.adoc[`resp3::basic_tree`]
xref:reference:boost/redis/resp3/tree.adoc[`resp3::tree`]
xref:reference:boost/redis/resp3/view_tree.adoc[`resp3::view_tree`]
xref:reference:boost/redis/resp3/flat_tree.adoc[`resp3::flat_tree`]
xref:reference:boost/redis/resp3/node_view.adoc[`node_view`]
xref:reference:boost/redis/resp3/boost_redis_to_bulk-08.adoc[`boost_redis_to_bulk`]
xref:reference:boost/redis/resp3/type.adoc[`resp3::type`]
xref:reference:boost/redis/resp3/type.adoc[`type`]
xref:reference:boost/redis/resp3/is_aggregate.adoc[`resp3::is_aggregate`]
xref:reference:boost/redis/resp3/is_aggregate.adoc[`is_aggregate`]
|
xref:reference:boost/redis/adapter/adapt2.adoc[`adapter::adapt2`]
xref:reference:boost/redis/resp3/parser.adoc[`resp3::parser`]
xref:reference:boost/redis/resp3/parser.adoc[`parser`]
xref:reference:boost/redis/resp3/parse.adoc[`resp3::parse`]
xref:reference:boost/redis/resp3/parse.adoc[`parse`]
|===

View File

@@ -278,8 +278,7 @@ struct basic_node {
----
Any response to a Redis command can be parsed into a
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response]
and its counterpart xref:reference:boost/redis/generic_flat_response.adoc[boost::redis::generic_flat_response].
xref:reference:boost/redis/generic_response.adoc[boost::redis::generic_response].
The vector can be seen as a pre-order view of the response tree.
Using it is not different than using other types:
@@ -293,7 +292,7 @@ co_await conn->async_exec(req, resp);
For example, suppose we want to retrieve a hash data structure
from Redis with `HGETALL`, some of the options are
* `boost::redis::generic_response` and `boost::redis::generic_flat_response`: always works.
* `boost::redis::generic_response`: always works.
* `std::vector<std::string>`: efficient and flat, all elements as string.
* `std::map<std::string, std::string>`: efficient if you need the data as a `std::map`.
* `std::map<U, V>`: efficient if you are storing serialized data. Avoids temporaries and requires `boost_redis_from_bulk` for `U` and `V`.

View File

@@ -1,152 +0,0 @@
//
// 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,4 +1,4 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -30,9 +30,11 @@ using boost::asio::consign;
using boost::asio::detached;
using boost::asio::dynamic_buffer;
using boost::asio::redirect_error;
using boost::asio::use_awaitable;
using boost::redis::config;
using boost::redis::connection;
using boost::redis::generic_flat_response;
using boost::redis::generic_response;
using boost::redis::ignore;
using boost::redis::request;
using boost::system::error_code;
using namespace std::chrono_literals;
@@ -45,24 +47,20 @@ auto receiver(std::shared_ptr<connection> conn) -> awaitable<void>
request req;
req.push("SUBSCRIBE", "channel");
generic_flat_response resp;
generic_response resp;
conn->set_receive_response(resp);
while (conn->will_reconnect()) {
// Subscribe to channels.
co_await conn->async_exec(req);
co_await conn->async_exec(req, ignore);
// Loop reading Redis push messages.
for (error_code ec;;) {
co_await conn->async_receive2(redirect_error(ec));
co_await conn->async_receive(redirect_error(use_awaitable, ec));
if (ec)
break; // Connection lost, break so we can reconnect to channels.
for (auto const& elem: resp.value().get_view())
std::cout << elem.value << "\n";
std::cout << std::endl;
std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " "
<< resp.value().at(3).value << std::endl;
resp.value().clear();
}
}
@@ -76,7 +74,7 @@ auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection
auto n = co_await async_read_until(*in, dynamic_buffer(msg, 1024), "\n");
request req;
req.push("PUBLISH", "channel", msg);
co_await conn->async_exec(req);
co_await conn->async_exec(req, ignore);
msg.erase(0, n);
}
}

View File

@@ -0,0 +1,77 @@
/* 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

@@ -1,60 +0,0 @@
//
// 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

@@ -1,10 +1,11 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* 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/redis/logger.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
@@ -21,8 +22,12 @@
namespace asio = boost::asio;
using namespace std::chrono_literals;
using boost::redis::request;
using boost::redis::generic_flat_response;
using boost::redis::generic_response;
using boost::redis::consume_one;
using boost::redis::logger;
using boost::redis::config;
using boost::redis::ignore;
using boost::redis::error;
using boost::system::error_code;
using boost::redis::connection;
using asio::signal_set;
@@ -49,29 +54,30 @@ auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
request req;
req.push("SUBSCRIBE", "channel");
generic_flat_response resp;
generic_response resp;
conn->set_receive_response(resp);
// Loop while reconnection is enabled
while (conn->will_reconnect()) {
// Reconnect to the channels.
co_await conn->async_exec(req);
co_await conn->async_exec(req, ignore);
// Loop to read Redis push messages.
// Loop reading Redis pushs messages.
for (error_code ec;;) {
// Wait for pushes
co_await conn->async_receive2(asio::redirect_error(ec));
// First tries to read any buffered pushes.
conn->receive(ec);
if (ec == error::sync_receive_push_failed) {
ec = {};
co_await conn->async_receive(asio::redirect_error(asio::use_awaitable, ec));
}
if (ec)
break; // Connection lost, break so we can reconnect to channels.
// The response must be consumed without suspending the
// coroutine i.e. without the use of async operations.
for (auto const& elem: resp.value().get_view())
std::cout << elem.value << "\n";
std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " "
<< resp.value().at(3).value << std::endl;
std::cout << std::endl;
resp.value().clear();
consume_one(resp);
}
}
}

View File

@@ -12,7 +12,9 @@
#include <boost/system/error_code.hpp>
#include <cstddef>
#include <functional>
#include <string_view>
#include <type_traits>
namespace boost::redis {
@@ -48,7 +50,20 @@ 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;
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;
}
};
}
/// Contructs from a type erased adaper
any_adapter(impl_t fn = [](parse_event, resp3::node_view const&, system::error_code&) { })
@@ -94,32 +109,6 @@ 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

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -12,8 +12,6 @@
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/response.hpp>
#include <boost/assert.hpp>
@@ -178,97 +176,6 @@ public:
}
};
template <>
class general_aggregate<resp3::tree> {
private:
resp3::tree* tree_ = nullptr;
public:
explicit general_aggregate(resp3::tree* c = nullptr)
: tree_(c)
{ }
void on_init() { }
void on_done() { }
template <class String>
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
resp3::node tmp;
tmp.data_type = nd.data_type;
tmp.aggregate_size = nd.aggregate_size;
tmp.depth = nd.depth;
tmp.value = std::string{std::cbegin(nd.value), std::cend(nd.value)};
tree_->push_back(std::move(tmp));
}
};
template <>
class general_aggregate<generic_flat_response> {
private:
generic_flat_response* tree_ = nullptr;
public:
explicit general_aggregate(generic_flat_response* c = nullptr)
: tree_(c)
{ }
void on_init() { }
void on_done()
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
if (tree_->has_value()) {
tree_->value().notify_done();
}
}
template <class String>
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
switch (nd.data_type) {
case resp3::type::blob_error:
case resp3::type::simple_error:
*tree_ = error{
nd.data_type,
std::string{std::cbegin(nd.value), std::cend(nd.value)}
};
break;
default:
if (tree_->has_value()) {
(**tree_).push(nd);
}
}
}
};
template <>
class general_aggregate<resp3::flat_tree> {
private:
resp3::flat_tree* tree_ = nullptr;
public:
explicit general_aggregate(resp3::flat_tree* c = nullptr)
: tree_(c)
{ }
void on_init() { }
void on_done()
{
tree_->notify_done();
}
template <class String>
void on_node(resp3::basic_node<String> const& nd, system::error_code&)
{
BOOST_ASSERT_MSG(!!tree_, "Unexpected null pointer");
tree_->push(nd);
}
};
template <class Node>
class general_simple {
private:

View File

@@ -92,32 +92,8 @@ struct response_traits<result<ignore_t>> {
};
template <class String, class Allocator>
struct response_traits<result<resp3::basic_tree<String, Allocator>>> {
using response_type = result<resp3::basic_tree<String, Allocator>>;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class String>
struct response_traits<resp3::basic_tree<String>> {
using response_type = resp3::basic_tree<String>;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct response_traits<resp3::flat_tree> {
using response_type = resp3::flat_tree;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct response_traits<generic_flat_response> {
using response_type = generic_flat_response;
struct response_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }

View File

@@ -13,8 +13,6 @@
#include <boost/redis/error.hpp>
#include <boost/redis/ignore.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/redis/resp3/tree.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/mp11.hpp>
@@ -58,33 +56,12 @@ struct result_traits<result<resp3::basic_node<T>>> {
};
template <class String, class Allocator>
struct result_traits<result<resp3::basic_tree<String, Allocator>>> {
struct result_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
using response_type = result<std::vector<resp3::basic_node<String>, Allocator>>;
using adapter_type = adapter::detail::general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class String>
struct result_traits<resp3::basic_tree<String>> {
using response_type = resp3::basic_tree<String>;
using adapter_type = adapter::detail::general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct result_traits<generic_flat_response> {
using response_type = generic_flat_response;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <>
struct result_traits<resp3::flat_tree> {
using response_type = resp3::flat_tree;
using adapter_type = general_aggregate<response_type>;
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
};
template <class T>
using adapter_t = typename result_traits<std::decay_t<T>>::adapter_type;

View File

@@ -13,7 +13,6 @@
#include <limits>
#include <optional>
#include <string>
#include <vector>
namespace boost::redis {
@@ -25,130 +24,12 @@ struct address {
std::string port = "6379";
};
/** @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;
/**
* @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.
*/
/// Uses SSL instead of a plain connection.
bool use_ssl = false;
/// For TCP connections, hostname and port of the Redis server. Ignored when using Sentinel.
/// For TCP connections, hostname and port of the Redis server.
address addr = address{"127.0.0.1", "6379"};
/**
@@ -156,11 +37,8 @@ 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`. UNIX domain sockets can't be used with Sentinel, either.
*
* UNIX domain sockets can't be used with Sentinel.
* @ref use_ssl must be `false`.
*/
std::string unix_socket;
@@ -173,9 +51,6 @@ 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";
@@ -188,9 +63,6 @@ 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;
@@ -199,9 +71,6 @@ 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";
@@ -211,8 +80,6 @@ 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;
@@ -228,22 +95,13 @@ struct config {
*/
std::string log_prefix = "(Boost.Redis) ";
/**
* @brief Time span that the resolve operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
/// Time span that the resolve operation is allowed to elapse.
std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10};
/**
* @brief Time span that the connect operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
/// Time span that the connect operation is allowed to elapse.
std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10};
/**
* @brief Time span that the SSL handshake operation is allowed to elapse.
* When using Sentinel, this setting applies to masters and replicas.
*/
/// Time span that the SSL handshake operation is allowed to elapse.
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10};
/** @brief Time span between successive health checks.
@@ -265,28 +123,18 @@ 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.
* Set to zero to disable reconnection.
*/
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
/** @brief Maximum size of the socket read-buffer in bytes.
*
* 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.
* Sets a limit on how much data is allowed to be read into the
* read buffer. It can be used to prevent DDOS.
*/
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
@@ -296,8 +144,6 @@ 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;
@@ -322,9 +168,6 @@ 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;
@@ -334,17 +177,8 @@ 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,12 +12,10 @@
#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>
@@ -34,11 +32,9 @@
#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>
@@ -49,7 +45,6 @@
#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>
@@ -212,148 +207,8 @@ struct connection_impl {
{
st_.mpx.set_receive_adapter(std::move(adapter));
}
std::size_t receive(system::error_code& ec)
{
std::size_t size = 0;
auto f = [&](system::error_code const& ec2, std::size_t n) {
ec = ec2;
size = n;
};
auto const res = receive_channel_.try_receive(f);
if (ec)
return 0;
if (!res)
ec = error::sync_receive_push_failed;
return size;
}
template <class CompletionToken>
auto async_receive2(CompletionToken&& token)
{
// clang-format off
return
receive_channel_.async_receive(
asio::deferred(
[this](system::error_code ec, std::size_t)
{
if (!ec) {
auto f = [](system::error_code, std::size_t) {
// There is no point in checking for errors
// here since async_receive just completed
// without errors.
};
// We just want to drain the channel.
while (receive_channel_.try_receive(f));
}
return asio::deferred.values(ec);
}
)
)(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_;
@@ -480,14 +335,8 @@ 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(
make_run_connect_params(conn_->st_),
conn_->st_.logger,
std::move(self));
conn_->stream_.async_connect(conn_->st_.cfg, conn_->st_.logger, std::move(self));
return;
case run_action_type::parallel_group:
asio::experimental::make_parallel_group(
@@ -638,8 +487,6 @@ 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,
@@ -746,7 +593,7 @@ public:
return async_run(config{}, std::forward<CompletionToken>(token));
}
/** @brief (Deprecated) Receives server side pushes asynchronously.
/** @brief Receives server side pushes asynchronously.
*
* When pushes arrive and there is no `async_receive` operation in
* progress, pushed data, requests, and responses will be paused
@@ -776,57 +623,12 @@ public:
* @param token Completion token.
*/
template <class CompletionToken = asio::default_completion_token_t<executor_type>>
BOOST_DEPRECATED("Please use async_receive2 instead.")
auto async_receive(CompletionToken&& token = {})
{
return impl_->receive_channel_.async_receive(std::forward<CompletionToken>(token));
}
/** @brief Wait for server pushes asynchronously
*
* This function suspends until a server push is received by the
* connection. On completion an unspecified number of pushes will
* have been added to the response object set with @ref
* boost::redis::connection::set_receive_response.
*
* To prevent receiving an unbound number of pushes the connection
* blocks further read operations on the socket when 256 pushes
* accumulate internally (we don't make any commitment to this
* exact number). When that happens any `async_exec`s and
* health-checks won't make any progress and the connection may
* eventually timeout. To avoid that Apps should call
* `async_receive2` continuously in a loop.
*
* @Note To avoid deadlocks the task (e.g. coroutine) calling
* `async_receive2` should not call `async_exec` in a way where
* they could block each other.
*
* For an example see cpp20_subscriber.cpp. The completion token
* must have the following signature
*
* @code
* void f(system::error_code);
* @endcode
*
* @par Per-operation cancellation
* This operation supports the following cancellation types:
*
* @li `asio::cancellation_type_t::terminal`.
* @li `asio::cancellation_type_t::partial`.
* @li `asio::cancellation_type_t::total`.
*
* Calling `basic_connection::cancel(operation::receive)` will
* also cancel any ongoing receive operations.
*
* @param token Completion token.
*/
template <class CompletionToken = asio::default_completion_token_t<executor_type>>
auto async_receive2(CompletionToken&& token = {})
{
return impl_->async_receive2(std::forward<CompletionToken>(token));
}
/** @brief (Deprecated) Receives server pushes synchronously without blocking.
/** @brief Receives server pushes synchronously without blocking.
*
* Receives a server push synchronously by calling `try_receive` on
* the underlying channel. If the operation fails because
@@ -836,8 +638,24 @@ public:
* @param ec Contains the error if any occurred.
* @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)
{
std::size_t size = 0;
auto f = [&](system::error_code const& ec2, std::size_t n) {
ec = ec2;
size = n;
};
auto const res = impl_->receive_channel_.try_receive(f);
if (ec)
return 0;
if (!res)
ec = error::sync_receive_push_failed;
return size;
}
/** @brief Executes commands on the Redis server asynchronously.
*
@@ -1019,7 +837,7 @@ public:
"the other member functions to interact with the connection.")
auto const& next_layer() const noexcept { return impl_->stream_.next_layer(); }
/// Sets the response object of @ref async_receive2 operations.
/// Sets the response object of @ref async_receive operations.
template <class Response>
void set_receive_response(Response& resp)
{
@@ -1210,22 +1028,13 @@ public:
/// @copydoc basic_connection::async_receive
template <class CompletionToken = asio::deferred_t>
BOOST_DEPRECATED("Please use async_receive2 instead.")
auto async_receive(CompletionToken&& token = {})
{
return impl_.async_receive(std::forward<CompletionToken>(token));
}
/// @copydoc basic_connection::async_receive2
template <class CompletionToken = asio::deferred_t>
auto async_receive2(CompletionToken&& token = {})
{
return impl_.async_receive2(std::forward<CompletionToken>(token));
}
/// @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_.receive(ec); }
/**
* @brief Calls @ref boost::redis::basic_connection::async_exec.

View File

@@ -9,6 +9,8 @@
#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>
@@ -60,13 +62,17 @@ struct connect_action {
class connect_fsm {
int resume_point_{0};
const config* cfg_{nullptr};
buffered_logger* lgr_{nullptr};
public:
connect_fsm(buffered_logger& lgr) noexcept
: lgr_(&lgr)
connect_fsm(const config& cfg, buffered_logger& lgr) noexcept
: cfg_(&cfg)
, 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

@@ -1,65 +0,0 @@
//
// 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,46 +13,20 @@
#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 diagnostic{}; // Used by the setup request and Sentinel
std::string setup_diagnostic{};
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

@@ -1,69 +0,0 @@
//
// 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,12 +181,6 @@ 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

@@ -61,7 +61,6 @@ public:
private:
config cfg_ = config{};
std::vector<char> buffer_;
std::size_t offset_ = 0;
std::size_t append_buf_begin_ = 0;
};

View File

@@ -9,7 +9,6 @@
#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>
@@ -25,7 +24,6 @@
#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>
@@ -50,14 +48,12 @@ 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)
{
// Prevent use-after-move errors
auto& obj = this->obj_;
auto params = this->params_;
auto& obj = this->obj_; // prevent use-after-move errors
const auto& cfg = fsm_.get_config();
switch (act.type) {
case connect_action_type::unix_socket_close:
@@ -74,8 +70,8 @@ class redis_stream {
case connect_action_type::unix_socket_connect:
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
obj.unix_socket_.async_connect(
params.addr.unix_socket(),
asio::cancel_after(obj.timer_, params.connect_timeout, std::move(self)));
cfg.unix_socket,
asio::cancel_after(obj.timer_, cfg.connect_timeout, std::move(self)));
#else
BOOST_ASSERT(false);
#endif
@@ -83,9 +79,9 @@ class redis_stream {
case connect_action_type::tcp_resolve:
obj.resolv_.async_resolve(
params.addr.tcp_address().host,
params.addr.tcp_address().port,
asio::cancel_after(obj.timer_, params.resolve_timeout, std::move(self)));
cfg.addr.host,
cfg.addr.port,
asio::cancel_after(obj.timer_, cfg.resolve_timeout, std::move(self)));
return;
case connect_action_type::ssl_stream_reset:
obj.reset_stream();
@@ -95,7 +91,7 @@ class redis_stream {
case connect_action_type::ssl_handshake:
obj.stream_.async_handshake(
asio::ssl::stream_base::client,
asio::cancel_after(obj.timer_, params.ssl_handshake_timeout, std::move(self)));
asio::cancel_after(obj.timer_, cfg.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
@@ -128,11 +124,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-move errors
auto& obj = this->obj_; // prevent use-after-free errors
asio::async_connect(
obj.stream_.next_layer(),
std::move(endpoints),
asio::cancel_after(obj.timer_, params_.connect_timeout, std::move(self)));
asio::cancel_after(obj.timer_, fsm_.get_config().connect_timeout, std::move(self)));
} else {
execute_action(self, act);
}
@@ -176,11 +172,10 @@ public:
// I/O
template <class CompletionToken>
auto async_connect(const connect_params& params, buffered_logger& l, CompletionToken&& token)
auto async_connect(const config& cfg, 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{l}, params},
connect_op{*this, connect_fsm(cfg, l)},
token);
}

View File

@@ -9,8 +9,6 @@
#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>
@@ -27,7 +25,6 @@ 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
@@ -60,8 +57,6 @@ 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

@@ -1,93 +0,0 @@
//
// 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

@@ -74,7 +74,7 @@ enum class error
/// SSL handshake timeout
ssl_handshake_timeout,
/// (Deprecated) Can't receive push synchronously without blocking
/// Can't receive push synchronously without blocking
sync_receive_push_failed,
/// Incompatible node depth.
@@ -94,24 +94,6 @@ 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,6 +61,20 @@ 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,
@@ -91,9 +105,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Connect: hostname resolution failed: ", ec);
log_info(*lgr_, "Error resolving the server hostname: ", ec);
} else {
log_debug(*lgr_, "Connect: hostname resolution results: ", resolver_results);
log_info(*lgr_, "Resolve results: ", resolver_results);
}
// Delegate to the regular resume function
@@ -111,9 +125,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Connect: TCP connect failed: ", ec);
log_info(*lgr_, "Failed to connect to the server: ", ec);
} else {
log_debug(*lgr_, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint);
log_info(*lgr_, "Connected to ", selected_endpoint);
}
// Delegate to the regular resume function
@@ -128,6 +142,9 @@ 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)
@@ -143,9 +160,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec);
log_info(*lgr_, "Failed to connect to the server: ", ec);
} else {
log_debug(*lgr_, "Connect: UNIX socket connect succeeded");
log_info(*lgr_, "Connected to ", cfg_->unix_socket);
}
// If this failed, we can't continue
@@ -161,7 +178,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 (st.type == transport_type::tcp_tls && st.ssl_stream_used) {
if (cfg_->use_ssl && st.ssl_stream_used) {
BOOST_REDIS_YIELD(resume_point_, 3, connect_action_type::ssl_stream_reset)
}
@@ -183,7 +200,7 @@ connect_action connect_fsm::resume(
return ec;
}
if (st.type == transport_type::tcp_tls) {
if (cfg_->use_ssl) {
// Mark the SSL stream as used
st.ssl_stream_used = true;
@@ -195,9 +212,9 @@ connect_action connect_fsm::resume(
// Log it
if (ec) {
log_info(*lgr_, "Connect: SSL handshake failed: ", ec);
log_info(*lgr_, "Failed to perform SSL handshake: ", ec);
} else {
log_debug(*lgr_, "Connect: SSL handshake succeeded");
log_info(*lgr_, "Successfully performed SSL handshake");
}
// If this failed, we can't continue

View File

@@ -55,19 +55,8 @@ 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::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.";
case error::write_timeout:
return "Timeout while writing data to the server.";
default: BOOST_ASSERT(false); return "Boost.Redis error.";
}
}

View File

@@ -1,95 +0,0 @@
//
// 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

@@ -1,189 +0,0 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Nikolai Vladimirov (nvladimirov.work@gmail.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/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/tree.hpp>
#include <boost/assert.hpp>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include <string_view>
namespace boost::redis::resp3 {
namespace detail {
// Updates string views by performing pointer arithmetic
inline void rebase_strings(view_tree& nodes, const char* old_base, const char* new_base)
{
for (auto& nd : nodes) {
if (!nd.value.empty()) {
const auto offset = nd.value.data() - old_base;
BOOST_ASSERT(offset >= 0);
nd.value = {new_base + offset, nd.value.size()};
}
}
}
// --- Operations in flat_buffer ---
// Compute the new capacity upon reallocation. We always use powers of 2,
// starting in 512, to prevent many small allocations
inline std::size_t compute_capacity(std::size_t current, std::size_t requested)
{
std::size_t res = (std::max)(current, static_cast<std::size_t>(512u));
while (res < requested)
res *= 2u;
return res;
}
// Copy construction
inline flat_buffer copy_construct(const flat_buffer& other)
{
flat_buffer res{{}, other.size, 0u, 0u};
if (other.size > 0u) {
const std::size_t capacity = compute_capacity(0u, other.size);
res.data.reset(new char[capacity]);
res.capacity = capacity;
res.reallocs = 1u;
std::copy(other.data.get(), other.data.get() + other.size, res.data.get());
}
return res;
}
// Copy assignment
inline void copy_assign(flat_buffer& buff, const flat_buffer& other)
{
// Make space if required
if (buff.capacity < other.size) {
const std::size_t capacity = compute_capacity(buff.capacity, other.size);
buff.data.reset(new char[capacity]);
buff.capacity = capacity;
++buff.reallocs;
}
// Copy the contents
std::copy(other.data.get(), other.data.get() + other.size, buff.data.get());
buff.size = other.size;
}
// Grows the buffer until reaching a target size.
// Might rebase the strings in nodes
inline void grow(flat_buffer& buff, std::size_t new_capacity, view_tree& nodes)
{
if (new_capacity <= buff.capacity)
return;
// Compute the actual capacity that we will be using
new_capacity = compute_capacity(buff.capacity, new_capacity);
// Allocate space
std::unique_ptr<char[]> new_buffer{new char[new_capacity]};
// Copy any data into the newly allocated space
const char* data_before = buff.data.get();
char* data_after = new_buffer.get();
std::copy(data_before, data_before + buff.size, data_after);
// Update the string views so they don't dangle
rebase_strings(nodes, data_before, data_after);
// Replace the buffer. Note that size hasn't changed here
buff.data = std::move(new_buffer);
buff.capacity = new_capacity;
++buff.reallocs;
}
// Appends a string to the buffer.
// Might rebase the string in nodes, but doesn't append any new node.
inline std::string_view append(flat_buffer& buff, std::string_view value, view_tree& nodes)
{
// If there is nothing to copy, do nothing
if (value.empty())
return value;
// Make space for the new string
const std::size_t new_size = buff.size + value.size();
grow(buff, new_size, nodes);
// Copy the new value
const std::size_t offset = buff.size;
std::copy(value.data(), value.data() + value.size(), buff.data.get() + offset);
buff.size = new_size;
return {buff.data.get() + offset, value.size()};
}
} // namespace detail
flat_tree::flat_tree(flat_tree const& other)
: data_{detail::copy_construct(other.data_)}
, view_tree_{other.view_tree_}
, total_msgs_{other.total_msgs_}
{
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());
}
flat_tree& flat_tree::operator=(const flat_tree& other)
{
if (this != &other) {
// Copy the data
detail::copy_assign(data_, other.data_);
// Copy the nodes
view_tree_ = other.view_tree_;
detail::rebase_strings(view_tree_, other.data_.data.get(), data_.data.get());
// Copy the other fields
total_msgs_ = other.total_msgs_;
}
return *this;
}
void flat_tree::reserve(std::size_t bytes, std::size_t nodes)
{
// Space for the strings
detail::grow(data_, bytes, view_tree_);
// Space for the nodes
view_tree_.reserve(nodes);
}
void flat_tree::clear() noexcept
{
data_.size = 0u;
view_tree_.clear();
total_msgs_ = 0u;
}
void flat_tree::push(node_view const& nd)
{
// Add the string
const std::string_view str = detail::append(data_, nd.value, view_tree_);
// Add the node
view_tree_.push_back({
nd.data_type,
nd.aggregate_size,
nd.depth,
str,
});
}
bool operator==(flat_tree const& a, flat_tree const& b)
{
// data is already taken into account by comparing the nodes.
return a.view_tree_ == b.view_tree_ && a.total_msgs_ == b.total_msgs_;
}
} // namespace boost::redis::resp3

View File

@@ -7,7 +7,6 @@
#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>
@@ -49,16 +48,6 @@ 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

@@ -42,13 +42,12 @@ auto read_buffer::get_prepared() noexcept -> span_type
auto read_buffer::get_commited() const noexcept -> std::string_view
{
return {buffer_.data() + offset_, append_buf_begin_ - offset_};
return {buffer_.data(), append_buf_begin_};
}
void read_buffer::clear()
{
buffer_.clear();
offset_ = 0;
append_buf_begin_ = 0;
}
@@ -57,22 +56,14 @@ read_buffer::consume(std::size_t size)
{
// For convenience, if the requested size is larger than the
// committed buffer we cap it to the maximum.
auto const consumable = append_buf_begin_ - offset_;
if (size > consumable)
size = consumable;
if (size > append_buf_begin_)
size = append_buf_begin_;
offset_ += size;
BOOST_ASSERT(offset_ <= append_buf_begin_);
buffer_.erase(buffer_.begin(), buffer_.begin() + size);
auto const rotated = size == 0u ? 0u : buffer_.size();
auto rotated = 0u;
if (offset_ >= 10'000'000 && size > 0u) {
buffer_.erase(buffer_.begin(), buffer_.begin() + offset_);
rotated = buffer_.size();
BOOST_ASSERT(offset_ <= append_buf_begin_);
append_buf_begin_ -= offset_;
offset_ = 0u;
}
BOOST_ASSERT(append_buf_begin_ >= size);
append_buf_begin_ -= size;
return {size, rotated};
}

View File

@@ -9,13 +9,9 @@
#include <boost/assert.hpp>
#include <algorithm>
namespace boost::redis {
namespace detail {
inline void consume_one_impl(generic_response& r, system::error_code& ec)
void consume_one(generic_response& r, system::error_code& ec)
{
if (r.has_error())
return; // Nothing to consume.
@@ -42,14 +38,10 @@ inline void consume_one_impl(generic_response& r, system::error_code& ec)
r.value().erase(std::cbegin(r.value()), match);
}
} // namespace detail
void consume_one(generic_response& r, system::error_code& ec) { detail::consume_one_impl(r, ec); }
void consume_one(generic_response& r)
{
system::error_code ec;
detail::consume_one_impl(r, ec);
consume_one(r, ec);
if (ec)
throw system::system_error(ec);
}

View File

@@ -8,15 +8,12 @@
#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>
@@ -31,8 +28,6 @@ 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
@@ -46,44 +41,45 @@ 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.diagnostic.empty()) {
if (st.setup_diagnostic.empty()) {
log_info(st.logger, "Setup request execution: ", ec);
} else {
log_info(st.logger, "Setup request execution: ", ec, " (", st.diagnostic, ")");
log_info(st.logger, "Setup request execution: ", ec, " (", st.setup_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,
@@ -107,34 +103,9 @@ 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
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)
BOOST_REDIS_YIELD(resume_point_, 2, run_action_type::connect)
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
@@ -142,66 +113,53 @@ run_action run_fsm::resume(
return system::error_code(asio::error::operation_aborted);
}
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;
// If we were successful, run all the connection tasks
if (!ec) {
// Initialization
st.mpx.reset();
st.setup_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));
elm->set_done_callback([&elem_ref = *elm, &st] {
on_setup_done(elem_ref, st);
});
st.mpx.add(elm);
}
// Run the tasks
BOOST_REDIS_YIELD(resume_point_, 3, run_action_type::parallel_group)
// Store any error yielded by the tasks for later
stored_ec_ = ec;
// We've lost connection or otherwise been cancelled.
// Remove from the multiplexer the required requests.
st.mpx.cancel_on_conn_lost();
// 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)
// Restore the error
ec = stored_ec_;
}
// We were successful
log_info(st.logger, "Connected to Redis server at ", get_server_address(st));
// Initialization
st.mpx.reset();
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_any_adapter_impl(setup_adapter{st}));
elm->set_done_callback([&elem_ref = *elm, &st] {
on_setup_done(elem_ref, st);
});
st.mpx.add(elm);
}
// Run the tasks
BOOST_REDIS_YIELD(resume_point_, 5, run_action_type::parallel_group)
// Store any error yielded by the tasks for later
stored_ec_ = ec;
// We've lost connection or otherwise been cancelled.
// Remove from the multiplexer the required requests.
st.mpx.cancel_on_conn_lost();
// The receive operation must be cancelled because channel
// subscription does not survive a reconnection but requires
// re-subscription.
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)) {
log_debug(st.logger, "Run: cancelled (2)");
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_, 7, run_action_type::wait_for_reconnection)
BOOST_REDIS_YIELD(resume_point_, 5, run_action_type::wait_for_reconnection)
// Check for cancellations
if (is_terminal_cancel(cancel_state)) {
@@ -216,14 +174,4 @@ sleep_and_reconnect:
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

@@ -1,182 +0,0 @@
//
// 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

@@ -1,278 +0,0 @@
//
// 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>
#include <algorithm>
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,27 +8,19 @@
#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?
@@ -54,71 +46,14 @@ 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(req, true);
req.get_config().cancel_if_unresponded = true;
req.get_config().cancel_on_connection_lost = true;
request_access::set_priority(cfg.setup, true);
cfg.setup.get_config().cancel_if_unresponded = true;
cfg.setup.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

@@ -10,6 +10,7 @@
#include <boost/redis/resp3/serialization.hpp>
#include <boost/redis/resp3/type.hpp>
#include <algorithm>
#include <string>
#include <tuple>
@@ -33,9 +34,11 @@ struct request_access;
*
* @code
* request r;
* r.push("SET", "k1", "some_value");
* r.push("SET", "k2", "other_value");
* r.push("GET", "k3");
* r.push("HELLO", 3);
* r.push("FLUSHALL");
* r.push("PING");
* r.push("PING", "key");
* r.push("QUIT");
* @endcode
*
* Uses a `std::string` for internal storage.
@@ -143,14 +146,14 @@ public:
*
* @code
* request req;
* req.push("SET", "key", "some string", "EX", 2);
* req.push("SET", "key", "some string", "EX", "2");
* @endcode
*
* This will add a `SET` command with value `"some string"` and an
* expiration of 2 seconds.
*
* Command arguments should either be convertible to `std::string_view`,
* integral types, or support the `boost_redis_to_bulk` function.
* Command arguments should either be convertible to `std::string_view`
* or support the `boost_redis_to_bulk` function.
* This function is a customization point that must be made available
* using ADL and must have the following signature:
*
@@ -162,7 +165,7 @@ public:
* See cpp20_serialization.cpp
*
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
* @param args Command arguments. `args` is allowed to be empty.
* @param args Command arguments. Non-string types will be converted to string by calling `boost_redis_to_bulk` on each argument.
* @tparam Ts Types of the command arguments.
*
*/
@@ -193,36 +196,21 @@ public:
* req.push_range("HSET", "key", map.cbegin(), map.cend());
* @endcode
*
* This will generate the following command:
* Command arguments should either be convertible to `std::string_view`
* or support the `boost_redis_to_bulk` function.
* This function is a customization point that must be made available
* using ADL and must have the following signature:
*
* @code
* HSET key key1 value1 key2 value2 key3 value3
* void boost_redis_to_bulk(std::string& to, T const& t);
* @endcode
*
* *If the passed range is empty, no command is added* and this
* function becomes a no-op.
*
* The value type of the passed range should satisfy one of the following:
*
* @li The type is convertible to `std::string_view`. One argument is added
* per element in the range.
* @li The type is an integral type. One argument is added
* per element in the range.
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
* per element in the range. This function is a customization point that must be made available
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
* the points above. Two arguments are added per element in the range.
* Nested pairs are not allowed.
* @li The type is a `std::tuple` instantiation, with every argument supporting
* one of the points above. N arguments are added per element in the range,
* with N being the tuple size. Nested tuples are not allowed.
*
*
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
* @param key The command key. It will be added as the first argument to the command.
* @param begin Iterator to the begin of the range.
* @param end Iterator to the end of the range.
* @tparam ForwardIterator A forward iterator with an element type that supports one of the points above.
* @tparam ForwardIterator A forward iterator with an element type that is convertible to `std::string_view`
* or supports `boost_redis_to_bulk`.
*
* See cpp20_serialization.cpp
*/
@@ -261,38 +249,23 @@ public:
* { "channel1" , "channel2" , "channel3" };
*
* request req;
* req.push("SUBSCRIBE", channels.cbegin(), channels.cend());
* req.push("SUBSCRIBE", std::cbegin(channels), std::cend(channels));
* @endcode
*
* This will generate the following command:
* Command arguments should either be convertible to `std::string_view`
* or support the `boost_redis_to_bulk` function.
* This function is a customization point that must be made available
* using ADL and must have the following signature:
*
* @code
* SUBSCRIBE channel1 channel2 channel3
* void boost_redis_to_bulk(std::string& to, T const& t);
* @endcode
*
* *If the passed range is empty, no command is added* and this
* function becomes a no-op.
*
* The value type of the passed range should satisfy one of the following:
*
* @li The type is convertible to `std::string_view`. One argument is added
* per element in the range.
* @li The type is an integral type. One argument is added
* per element in the range.
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
* per element in the range. This function is a customization point that must be made available
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
* the points above. Two arguments are added per element in the range.
* Nested pairs are not allowed.
* @li The type is a `std::tuple` instantiation, with every argument supporting
* one of the points above. N arguments are added per element in the range,
* with N being the tuple size. Nested tuples are not allowed.
*
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
* @param begin Iterator to the begin of the range.
* @param end Iterator to the end of the range.
* @tparam ForwardIterator A forward iterator with an element type that supports one of the points above.
* @tparam ForwardIterator A forward iterator with an element type that is convertible to `std::string_view`
* or supports `boost_redis_to_bulk`.
*
* See cpp20_serialization.cpp
*/
@@ -323,31 +296,13 @@ public:
*
* Equivalent to the overload taking a range of begin and end
* iterators.
*
* *If the passed range is empty, no command is added* and this
* function becomes a no-op.
*
* The value type of the passed range should satisfy one of the following:
*
* @li The type is convertible to `std::string_view`. One argument is added
* per element in the range.
* @li The type is an integral type. One argument is added
* per element in the range.
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
* per element in the range. This function is a customization point that must be made available
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
* the points above. Two arguments are added per element in the range.
* Nested pairs are not allowed.
* @li The type is a `std::tuple` instantiation, with every argument supporting
* one of the points above. N arguments are added per element in the range,
* with N being the tuple size. Nested tuples are not allowed.
*
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
* @param key The command key. It will be added as the first argument to the command.
* @param range Range containing the command arguments.
* @tparam Range A type that can be passed to `std::begin()` and `std::end()` to obtain
* iterators.
* iterators. The range elements should be convertible to `std::string_view`
* or support `boost_redis_to_bulk`.
*/
template <class Range>
void push_range(
@@ -365,30 +320,12 @@ public:
*
* Equivalent to the overload taking a range of begin and end
* iterators.
*
* *If the passed range is empty, no command is added* and this
* function becomes a no-op.
*
* The value type of the passed range should satisfy one of the following:
*
* @li The type is convertible to `std::string_view`. One argument is added
* per element in the range.
* @li The type is an integral type. One argument is added
* per element in the range.
* @li The type supports the `boost_redis_to_bulk` function. One argument is added
* per element in the range. This function is a customization point that must be made available
* using ADL and must have the signature `void boost_redis_to_bulk(std::string& to, T const& t);`.
* @li The type is a `std::pair` instantiation, with both arguments supporting one of
* the points above. Two arguments are added per element in the range.
* Nested pairs are not allowed.
* @li The type is a `std::tuple` instantiation, with every argument supporting
* one of the points above. N arguments are added per element in the range,
* with N being the tuple size. Nested tuples are not allowed.
*
* @param cmd The command to execute. It should be a redis or sentinel command, like `"SET"`.
* @param range Range containing the command arguments.
* @tparam Range A type that can be passed to `std::begin()` and `std::end()` to obtain
* iterators.
* iterators. The range elements should be convertible to `std::string_view`
* or support `boost_redis_to_bulk`.
*/
template <class Range>
void push_range(

View File

@@ -1,252 +0,0 @@
//
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
// Nikolai Vladimirov (nvladimirov.work@gmail.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_RESP3_FLAT_TREE_HPP
#define BOOST_REDIS_RESP3_FLAT_TREE_HPP
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/tree.hpp>
#include <cstddef>
#include <memory>
namespace boost::redis {
namespace adapter::detail {
template <class> class general_aggregate;
} // namespace adapter::detail
namespace resp3 {
namespace detail {
struct flat_buffer {
std::unique_ptr<char[]> data;
std::size_t size = 0u;
std::size_t capacity = 0u;
std::size_t reallocs = 0u;
};
} // namespace detail
/** @brief A generic response that stores data contiguously.
*
* Implements a container of RESP3 nodes. It's similar to @ref boost::redis::resp3::tree,
* but node data is stored contiguously. This allows for amortized no allocations
* when re-using `flat_tree` objects. Like `tree`, it can contain the response
* to several Redis commands or several server pushes. Use @ref get_total_msgs
* to obtain how many responses this object contains.
*
* Objects are typically created by the user and passed to @ref connection::async_exec
* to be used as response containers. Call @ref get_view to access the actual RESP3 nodes.
* Once populated, `flat_tree` can't be modified, except for @ref clear and assignment.
*
* A `flat_tree` is conceptually similar to a pair of `std::vector` objects, one holding
* @ref resp3::node_view objects, and another owning the the string data that these views
* point to. The node capacity and the data capacity are the capacities of these two vectors.
*/
class flat_tree {
public:
/**
* @brief Default constructor.
*
* Constructs an empty tree, with no nodes, zero node capacity and zero data capacity.
*
* @par Exception safety
* No-throw guarantee.
*/
flat_tree() = default;
/**
* @brief Move constructor.
*
* Constructs a tree by taking ownership of the nodes in `other`.
*
* @par Object lifetimes
* References to the nodes and strings in `other` remain valid.
*
* @par Exception safety
* No-throw guarantee.
*/
flat_tree(flat_tree&& other) noexcept = default;
/**
* @brief Copy constructor.
*
* Constructs a tree by copying the nodes in `other`. After the copy,
* `*this` and `other` have independent lifetimes (usual copy semantics).
*
* @par Exception safety
* Strong guarantee. Memory allocations might throw.
*/
flat_tree(flat_tree const& other);
/**
* @brief Move assignment.
*
* Replaces the nodes in `*this` by taking ownership of the nodes in `other`.
* `other` is left in a valid but unspecified state.
*
* @par Object lifetimes
* References to the nodes and strings in `other` remain valid.
* References to the nodes and strings in `*this` are invalidated.
*
* @par Exception safety
* No-throw guarantee.
*/
flat_tree& operator=(flat_tree&& other) = default;
/**
* @brief Copy assignment.
*
* Replaces the nodes in `*this` by copying the nodes in `other`.
* After the copy, `*this` and `other` have independent lifetimes (usual copy semantics).
*
* @par Object lifetimes
* References to the nodes and strings in `*this` are invalidated.
*
* @par Exception safety
* Basic guarantee. Memory allocations might throw.
*/
flat_tree& operator=(const flat_tree& other);
friend bool operator==(flat_tree const&, flat_tree const&);
friend bool operator!=(flat_tree const&, flat_tree const&);
/** @brief Reserves capacity for incoming data.
*
* Adding nodes (e.g. by passing the tree to `async_exec`)
* won't cause reallocations until the data or node capacities
* are exceeded, following the usual vector semantics.
* The implementation might reserve more capacity than the one requested.
*
* @par Object lifetimes
* References to the nodes and strings in `*this` are invalidated.
*
* @par Exception safety
* Basic guarantee. Memory allocations might throw.
*
* @param bytes Number of bytes to reserve for data.
* @param nodes Number of nodes to reserve.
*/
void reserve(std::size_t bytes, std::size_t nodes);
/** @brief Clears the tree so it contains no nodes.
*
* Calling this function removes every node, making
* @ref get_view return empty and @ref get_total_msgs
* return zero. It does not modify the object's capacity.
*
* To re-use a `flat_tree` for several requests,
* use `clear()` before each `async_exec` call.
*
* @par Object lifetimes
* References to the nodes and strings in `*this` are invalidated.
*
* @par Exception safety
* No-throw guarantee.
*/
void clear() noexcept;
/** @brief Returns the size of the data buffer, in bytes.
*
* You may use this function to calculate how much capacity
* should be reserved for data when calling @ref reserve.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The number of bytes in use in the data buffer.
*/
auto data_size() const noexcept -> std::size_t { return data_.size; }
/** @brief Returns the capacity of the data buffer, in bytes.
*
* Note that the actual capacity of the data buffer may be bigger
* than the one requested by @ref reserve.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The capacity of the data buffer, in bytes.
*/
auto data_capacity() const noexcept -> std::size_t { return data_.capacity; }
/** @brief Returns a vector with the nodes in the tree.
*
* This is the main way to access the contents of the tree.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The nodes in the tree.
*/
auto get_view() const noexcept -> view_tree const& { return view_tree_; }
/** @brief Returns the number of memory reallocations that took place in the data buffer.
*
* This function returns how many reallocations in the data buffer were performed and
* can be useful to determine how much memory to reserve upfront.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The number of times that the data buffer reallocated its memory.
*/
auto get_reallocs() const noexcept -> std::size_t { return data_.reallocs; }
/** @brief Returns the number of complete RESP3 messages contained in this object.
*
* This value is equal to the number of nodes in the tree with a depth of zero.
*
* @par Exception safety
* No-throw guarantee.
*
* @returns The number of complete RESP3 messages contained in this object.
*/
std::size_t get_total_msgs() const noexcept { return total_msgs_; }
private:
template <class> friend class adapter::detail::general_aggregate;
void notify_done() { ++total_msgs_; }
// Push a new node to the response
void push(node_view const& node);
detail::flat_buffer data_;
view_tree view_tree_;
std::size_t total_msgs_ = 0u;
};
/**
* @brief Equality operator.
* @relates flat_tree
*
* Two trees are equal if they contain the same nodes in the same order.
* Capacities are not taken into account.
*
* @par Exception safety
* No-throw guarantee.
*/
bool operator==(flat_tree const&, flat_tree const&);
/**
* @brief Inequality operator.
* @relates flat_tree
*
* @par Exception safety
* No-throw guarantee.
*/
inline bool operator!=(flat_tree const& lhs, flat_tree const& rhs) { return !(lhs == rhs); }
} // namespace resp3
} // namespace boost::redis
#endif // BOOST_REDIS_RESP3_FLAT_TREE_HPP

View File

@@ -8,8 +8,6 @@
#include <boost/assert.hpp>
#include <ostream>
namespace boost::redis::resp3 {
auto to_string(type t) noexcept -> char const*

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -9,10 +9,6 @@
#include <boost/redis/resp3/type.hpp>
#include <cstddef>
#include <string>
#include <string_view>
namespace boost::redis::resp3 {
/** @brief A node in the response tree.
@@ -47,7 +43,7 @@ struct basic_node {
* @param b Right hand side node object.
*/
template <class String>
bool operator==(basic_node<String> const& a, basic_node<String> const& b)
auto operator==(basic_node<String> const& a, basic_node<String> const& b)
{
// clang-format off
return a.aggregate_size == b.aggregate_size
@@ -57,18 +53,6 @@ bool operator==(basic_node<String> const& a, basic_node<String> const& b)
// clang-format on
};
/** @brief Inequality operator for RESP3 nodes.
* @relates basic_node
*
* @param a Left hand side node object.
* @param b Right hand side node object.
*/
template <class String>
bool operator!=(basic_node<String> const& a, basic_node<String> const& b)
{
return !(a == b);
};
/// A node in the response tree that owns its data.
using node = basic_node<std::string>;

View File

@@ -12,6 +12,7 @@
#include <boost/system/error_code.hpp>
#include <array>
#include <cstdint>
#include <optional>
#include <string_view>

View File

@@ -16,6 +16,9 @@
#include <string>
#include <tuple>
// NOTE: Consider detecting tuples in the type in the parameter pack
// to calculate the header size correctly.
namespace boost::redis::resp3 {
/** @brief Adds a bulk to the request.
@@ -32,10 +35,6 @@ namespace boost::redis::resp3 {
* }
* @endcode
*
* The function must add exactly one bulk string RESP3 node.
* If you're using `boost_redis_to_bulk` with a string argument,
* you're safe.
*
* @param payload Storage on which data will be copied into.
* @param data Data that will be serialized and stored in `payload`.
*/
@@ -101,11 +100,6 @@ struct bulk_counter<std::pair<T, U>> {
static constexpr auto size = 2U;
};
template <class... T>
struct bulk_counter<std::tuple<T...>> {
static constexpr auto size = sizeof...(T);
};
void add_blob(std::string& payload, std::string_view blob);
void add_separator(std::string& payload);

View File

@@ -1,29 +0,0 @@
/* 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_RESP3_TREE_HPP
#define BOOST_REDIS_RESP3_TREE_HPP
#include <boost/redis/resp3/node.hpp>
#include <vector>
#include <string_view>
namespace boost::redis::resp3 {
/// A RESP3 tree that owns its data.
template <class String, class Allocator = std::allocator<basic_node<String>>>
using basic_tree = std::vector<basic_node<String>, Allocator>;
/// A RESP3 tree that owns its data.
using tree = basic_tree<std::string>;
/// A RESP3 tree whose data are `std::string_views`.
using view_tree = basic_tree<std::string_view>;
}
#endif // BOOST_REDIS_RESP3_RESPONSE_HPP

View File

@@ -9,8 +9,9 @@
#include <boost/assert.hpp>
#include <cstddef>
#include <iosfwd>
#include <ostream>
#include <string>
#include <vector>
namespace boost::redis::resp3 {

View File

@@ -8,13 +8,13 @@
#define BOOST_REDIS_RESPONSE_HPP
#include <boost/redis/adapter/result.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/tree.hpp>
#include <boost/system/error_code.hpp>
#include <string>
#include <tuple>
#include <vector>
namespace boost::redis {
@@ -29,12 +29,9 @@ using response = std::tuple<adapter::result<Ts>...>;
* [pre-order](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order,_NLR)
* view of the response tree.
*/
using generic_response = adapter::result<resp3::tree>;
using generic_response = adapter::result<std::vector<resp3::node>>;
/// Similar to @ref boost::redis::generic_response but stores data contiguously.
using generic_flat_response = adapter::result<resp3::flat_tree>;
/** @brief (Deprecated) Consume on response from a generic response
/** @brief Consume on response from a generic response
*
* This function rotates the elements so that the start of the next
* response becomes the new front element. For example the output of
@@ -73,15 +70,13 @@ using generic_flat_response = adapter::result<resp3::flat_tree>;
* @param r The response to modify.
* @param ec Will be populated in case of error.
*/
BOOST_DEPRECATED("This function is not needed anymore to consume server pushes.")
void consume_one(generic_response& r, system::error_code& ec);
/**
* @brief (Deprecated) Throwing overload of `consume_one`.
* @brief Throwing overload of `consume_one`.
*
* @param r The response to modify.
*/
BOOST_DEPRECATED("This function is not needed anymore to consume server pushes.")
void consume_one(generic_response& r);
} // namespace boost::redis

View File

@@ -8,7 +8,6 @@
#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>
@@ -17,9 +16,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>
#include <boost/redis/resp3/impl/serialization.ipp>
#include <boost/redis/resp3/impl/type.ipp>

View File

@@ -35,25 +35,17 @@ endmacro()
# Unit tests
make_test(test_low_level)
make_test(test_request)
make_test(test_serialization)
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_compose_setup_request)
make_test(test_setup_adapter)
make_test(test_setup_request_utils)
make_test(test_multiplexer)
make_test(test_parse_sentinel_response)
make_test(test_update_sentinel_list)
make_test(test_flat_tree)
make_test(test_read_buffer)
# Tests that require a real Redis server
make_test(test_conn_quit)
@@ -64,7 +56,6 @@ make_test(test_conn_run_cancel)
make_test(test_conn_check_health)
make_test(test_conn_exec)
make_test(test_conn_push)
make_test(test_conn_push2)
make_test(test_conn_monitor)
make_test(test_conn_reconnect)
make_test(test_conn_exec_cancel)
@@ -76,7 +67,6 @@ 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

@@ -52,25 +52,17 @@ lib redis_test_common
local tests =
test_low_level
test_request
test_serialization
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_compose_setup_request
test_setup_adapter
test_setup_request_utils
test_multiplexer
test_parse_sentinel_response
test_update_sentinel_list
test_flat_tree
test_read_buffer
;
# Build and run the tests

View File

@@ -1,16 +1,11 @@
#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;
@@ -76,6 +71,7 @@ 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};
@@ -88,45 +84,3 @@ 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,7 +2,6 @@
#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>
@@ -12,7 +11,6 @@
#include <chrono>
#include <memory>
#include <string>
#include <string_view>
// The timeout for tests involving communication to a real server.
@@ -42,8 +40,3 @@ 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);

View File

@@ -1,28 +0,0 @@
/* 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,10 +4,8 @@
* 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>
@@ -74,24 +72,4 @@ 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,7 +8,6 @@
#define BOOST_REDIS_TEST_SANSIO_UTILS_HPP
#include <boost/redis/logger.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/assert/source_location.hpp>
@@ -16,7 +15,6 @@
#include <initializer_list>
#include <string>
#include <string_view>
#include <vector>
namespace boost::redis::detail {
@@ -52,13 +50,6 @@ 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

View File

@@ -13,7 +13,6 @@
#include <boost/test/included/unit_test.hpp>
using boost::redis::generic_response;
using boost::redis::resp3::flat_tree;
using boost::redis::response;
using boost::redis::ignore;
using boost::redis::any_adapter;
@@ -25,12 +24,10 @@ BOOST_AUTO_TEST_CASE(any_adapter_response_types)
response<int> r1;
response<int, std::string> r2;
generic_response r3;
flat_tree r4;
BOOST_CHECK_NO_THROW(any_adapter{r1});
BOOST_CHECK_NO_THROW(any_adapter{r2});
BOOST_CHECK_NO_THROW(any_adapter{r3});
BOOST_CHECK_NO_THROW(any_adapter{r4});
BOOST_CHECK_NO_THROW(any_adapter{ignore});
}

View File

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -27,7 +27,6 @@ using error_code = boost::system::error_code;
using boost::redis::operation;
using boost::redis::request;
using boost::redis::response;
using boost::redis::resp3::flat_tree;
using boost::redis::ignore;
using boost::redis::ignore_t;
using boost::redis::logger;
@@ -55,44 +54,39 @@ std::ostream& operator<<(std::ostream& os, usage const& u)
namespace {
auto
receiver(
connection& conn,
flat_tree& resp,
std::size_t expected) -> net::awaitable<void>
auto push_consumer(connection& conn, int expected) -> net::awaitable<void>
{
std::size_t push_counter = 0;
while (push_counter != expected) {
co_await conn.async_receive2();
push_counter += resp.get_total_msgs();
resp.clear();
int c = 0;
for (error_code ec;;) {
conn.receive(ec);
if (ec == error::sync_receive_push_failed) {
ec = {};
co_await conn.async_receive(net::redirect_error(ec));
} else if (!ec) {
//std::cout << "Skipping suspension." << std::endl;
}
if (ec) {
BOOST_TEST(false, "push_consumer error: " << ec.message());
co_return;
}
if (++c == expected)
break;
}
conn.cancel();
}
auto echo_session(connection& conn, const request& req, std::size_t n) -> net::awaitable<void>
auto echo_session(connection& conn, const request& pubs, int n) -> net::awaitable<void>
{
for (auto i = 0u; i < n; ++i)
co_await conn.async_exec(req);
for (auto i = 0; i < n; ++i)
co_await conn.async_exec(pubs);
}
void rethrow_on_error(std::exception_ptr exc)
{
if (exc) {
BOOST_TEST(false);
if (exc)
std::rethrow_exception(exc);
}
}
request make_pub_req(std::size_t n_pubs)
{
request req;
req.push("PING");
for (std::size_t i = 0u; i < n_pubs; ++i)
req.push("PUBLISH", "channel", "payload");
return req;
}
BOOST_AUTO_TEST_CASE(echo_stress)
@@ -104,22 +98,22 @@ BOOST_AUTO_TEST_CASE(echo_stress)
// Number of coroutines that will send pings sharing the same
// connection to redis.
constexpr std::size_t sessions = 150u;
constexpr int sessions = 150;
// The number of pings that will be sent by each session.
constexpr std::size_t msgs = 200u;
constexpr int msgs = 200;
// The number of publishes that will be sent by each session with
// each message.
constexpr std::size_t n_pubs = 25u;
constexpr int n_pubs = 25;
// This is the total number of pushes we will receive.
constexpr std::size_t total_pushes = sessions * msgs * n_pubs + 1;
constexpr int total_pushes = sessions * msgs * n_pubs + 1;
flat_tree resp;
conn.set_receive_response(resp);
request const pub_req = make_pub_req(n_pubs);
request pubs;
pubs.push("PING");
for (int i = 0; i < n_pubs; ++i)
pubs.push("PUBLISH", "channel", "payload");
// Run the connection
bool run_finished = false, subscribe_finished = false;
@@ -129,10 +123,6 @@ BOOST_AUTO_TEST_CASE(echo_stress)
std::clog << "async_run finished" << std::endl;
});
// Op that will consume the pushes counting down until all expected
// pushes have been received.
net::co_spawn(ctx, receiver(conn, resp, total_pushes), rethrow_on_error);
// Subscribe, then launch the coroutines
request req;
req.push("SUBSCRIBE", "channel");
@@ -140,8 +130,12 @@ BOOST_AUTO_TEST_CASE(echo_stress)
subscribe_finished = true;
BOOST_TEST(ec == error_code());
for (std::size_t i = 0; i < sessions; ++i)
net::co_spawn(ctx, echo_session(conn, pub_req, msgs), rethrow_on_error);
// Op that will consume the pushes counting down until all expected
// pushes have been received.
net::co_spawn(ctx, push_consumer(conn, total_pushes), rethrow_on_error);
for (int i = 0; i < sessions; ++i)
net::co_spawn(ctx, echo_session(conn, pubs, msgs), rethrow_on_error);
});
// Run the test
@@ -150,13 +144,7 @@ BOOST_AUTO_TEST_CASE(echo_stress)
BOOST_TEST(subscribe_finished);
// Print statistics
std::cout
<< "-------------------\n"
<< "Usage data: \n"
<< conn.get_usage() << "\n"
<< "-------------------\n"
<< "Reallocations: " << resp.get_reallocs()
<< std::endl;
std::cout << "-------------------\n" << conn.get_usage() << std::endl;
}
} // namespace

View File

@@ -269,9 +269,9 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax)
generic_response gresp;
conn->set_receive_response(gresp);
auto c3 = [&](error_code ec) {
auto c3 = [&](error_code ec, std::size_t) {
c3_called = true;
std::cout << "async_receive2" << std::endl;
std::cout << "async_receive" << std::endl;
BOOST_TEST(!ec);
BOOST_TEST(gresp.has_error());
BOOST_CHECK_EQUAL(gresp.error().data_type, resp3::type::simple_error);
@@ -281,7 +281,7 @@ BOOST_AUTO_TEST_CASE(subscriber_wrong_syntax)
conn->cancel(operation::reconnection);
};
conn->async_receive2(c3);
conn->async_receive(c3);
run(conn);
@@ -326,4 +326,4 @@ BOOST_AUTO_TEST_CASE(issue_287_generic_response_error_then_success)
BOOST_TEST(resp.error().diagnostic == "ERR wrong number of arguments for 'set' command");
}
} // namespace
} // namespace

View File

@@ -41,7 +41,7 @@ class test_monitor {
void start_receive()
{
conn.async_receive2([this](error_code ec) {
conn.async_receive([this](error_code ec, std::size_t) {
// We should expect one push entry, at least
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST(monitor_resp.has_value());
@@ -118,4 +118,4 @@ int main()
test_monitor{}.run();
return boost::report_errors();
}
}

View File

@@ -1,392 +0,0 @@
/* Copyright (c) 2018-2025 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/redis/logger.hpp>
#include <boost/redis/request.hpp>
#include <boost/redis/response.hpp>
#include <boost/asio/experimental/channel_error.hpp>
#include <boost/system/errc.hpp>
#include <string>
#define BOOST_TEST_MODULE conn_push
#include <boost/test/included/unit_test.hpp>
#include "common.hpp"
#include <cstddef>
#include <iostream>
namespace net = boost::asio;
namespace redis = boost::redis;
using boost::redis::operation;
using boost::redis::connection;
using boost::system::error_code;
using boost::redis::request;
using boost::redis::response;
using boost::redis::resp3::flat_tree;
using boost::redis::ignore;
using boost::redis::ignore_t;
using boost::system::error_code;
using boost::redis::logger;
using namespace std::chrono_literals;
namespace {
BOOST_AUTO_TEST_CASE(receives_push_waiting_resps)
{
request req1;
req1.push("HELLO", 3);
req1.push("PING", "Message1");
request req2;
req2.push("SUBSCRIBE", "channel");
request req3;
req3.push("PING", "Message2");
req3.push("QUIT");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
bool push_received = false, c1_called = false, c2_called = false, c3_called = false;
auto c3 = [&](error_code ec, std::size_t) {
c3_called = true;
std::cout << "c3: " << ec.message() << std::endl;
};
auto c2 = [&, conn](error_code ec, std::size_t) {
c2_called = true;
BOOST_TEST(ec == error_code());
conn->async_exec(req3, ignore, c3);
};
auto c1 = [&, conn](error_code ec, std::size_t) {
c1_called = true;
BOOST_TEST(ec == error_code());
conn->async_exec(req2, ignore, c2);
};
conn->async_exec(req1, ignore, c1);
run(conn, make_test_config(), {});
conn->async_receive2([&, conn](error_code ec) {
std::cout << "async_receive2" << std::endl;
BOOST_TEST(ec == error_code());
push_received = true;
conn->cancel();
});
ioc.run_for(test_timeout);
BOOST_TEST(push_received);
BOOST_TEST(c1_called);
BOOST_TEST(c2_called);
BOOST_TEST(c3_called);
}
BOOST_AUTO_TEST_CASE(push_received1)
{
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
flat_tree resp;
conn->set_receive_response(resp);
// Trick: Uses SUBSCRIBE because this command has no response or
// better said, its response is a server push, which is what we
// want to test.
request req;
req.push("SUBSCRIBE", "channel1");
req.push("SUBSCRIBE", "channel2");
bool push_received = false, exec_finished = false;
conn->async_exec(req, ignore, [&, conn](error_code ec, std::size_t) {
exec_finished = true;
std::cout << "async_exec" << std::endl;
BOOST_TEST(ec == error_code());
});
conn->async_receive2([&, conn](error_code ec) {
push_received = true;
std::cout << "async_receive2" << std::endl;
BOOST_TEST(ec == error_code());
BOOST_CHECK_EQUAL(resp.get_total_msgs(), 2u);
conn->cancel();
});
run(conn);
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(push_received);
}
BOOST_AUTO_TEST_CASE(push_filtered_out)
{
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
request req;
req.push("HELLO", 3);
req.push("PING");
req.push("SUBSCRIBE", "channel");
req.push("QUIT");
response<ignore_t, std::string, std::string> resp;
bool exec_finished = false, push_received = false;
conn->async_exec(req, resp, [conn, &exec_finished](error_code ec, std::size_t) {
exec_finished = true;
BOOST_TEST(ec == error_code());
});
conn->async_receive2([&, conn](error_code ec) {
push_received = true;
BOOST_TEST(ec == error_code());
conn->cancel(operation::reconnection);
});
run(conn);
ioc.run_for(test_timeout);
BOOST_TEST(exec_finished);
BOOST_TEST(push_received);
BOOST_CHECK_EQUAL(std::get<1>(resp).value(), "PONG");
BOOST_CHECK_EQUAL(std::get<2>(resp).value(), "OK");
}
struct response_error_tag { };
response_error_tag error_tag_obj;
struct response_error_adapter {
void on_init() { }
void on_done() { }
void on_node(
boost::redis::resp3::basic_node<std::string_view> const&,
boost::system::error_code& ec)
{
ec = boost::redis::error::incompatible_size;
}
};
auto boost_redis_adapt(response_error_tag&) { return response_error_adapter{}; }
BOOST_AUTO_TEST_CASE(test_push_adapter)
{
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
request req;
req.push("HELLO", 3);
req.push("PING");
req.push("SUBSCRIBE", "channel");
req.push("PING");
conn->set_receive_response(error_tag_obj);
bool push_received = false, exec_finished = false, run_finished = false;
conn->async_receive2([&, conn](error_code ec) {
BOOST_CHECK_EQUAL(ec, boost::asio::experimental::error::channel_cancelled);
push_received = true;
});
conn->async_exec(req, ignore, [&exec_finished](error_code ec, std::size_t) {
BOOST_CHECK_EQUAL(ec, boost::system::errc::errc_t::operation_canceled);
exec_finished = true;
});
auto cfg = make_test_config();
cfg.reconnect_wait_interval = 0s;
conn->async_run(cfg, [&run_finished](error_code ec) {
BOOST_CHECK_EQUAL(ec, redis::error::incompatible_size);
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(push_received);
BOOST_TEST(exec_finished);
BOOST_TEST(run_finished);
// TODO: Reset the ioc reconnect and send a quit to ensure
// reconnection is possible after an error.
}
void launch_push_consumer(std::shared_ptr<connection> conn)
{
conn->async_receive2([conn](error_code ec) {
if (ec) {
BOOST_TEST(ec == net::experimental::error::channel_cancelled);
return;
}
launch_push_consumer(conn);
});
}
BOOST_AUTO_TEST_CASE(many_subscribers)
{
request req0;
req0.get_config().cancel_on_connection_lost = false;
req0.push("HELLO", 3);
request req1;
req1.get_config().cancel_on_connection_lost = false;
req1.push("PING", "Message1");
request req2;
req2.get_config().cancel_on_connection_lost = false;
req2.push("SUBSCRIBE", "channel");
request req3;
req3.get_config().cancel_on_connection_lost = false;
req3.push("QUIT");
net::io_context ioc;
auto conn = std::make_shared<connection>(ioc);
bool finished = false;
auto c11 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->cancel(operation::reconnection);
finished = true;
};
auto c10 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req3, ignore, c11);
};
auto c9 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req2, ignore, c10);
};
auto c8 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req1, ignore, c9);
};
auto c7 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req2, ignore, c8);
};
auto c6 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req2, ignore, c7);
};
auto c5 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req1, ignore, c6);
};
auto c4 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req2, ignore, c5);
};
auto c3 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req1, ignore, c4);
};
auto c2 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req2, ignore, c3);
};
auto c1 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req2, ignore, c2);
};
auto c0 = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
conn->async_exec(req1, ignore, c1);
};
conn->async_exec(req0, ignore, c0);
launch_push_consumer(conn);
run(conn, make_test_config(), {});
ioc.run_for(test_timeout);
BOOST_TEST(finished);
}
BOOST_AUTO_TEST_CASE(test_unsubscribe)
{
net::io_context ioc;
connection conn{ioc};
// Subscribe to 3 channels and 2 patterns. Use CLIENT INFO to verify this took effect
request req_subscribe;
req_subscribe.push("SUBSCRIBE", "ch1", "ch2", "ch3");
req_subscribe.push("PSUBSCRIBE", "ch1*", "ch2*");
req_subscribe.push("CLIENT", "INFO");
// Then, unsubscribe from some of them, and verify again
request req_unsubscribe;
req_unsubscribe.push("UNSUBSCRIBE", "ch1");
req_unsubscribe.push("PUNSUBSCRIBE", "ch2*");
req_unsubscribe.push("CLIENT", "INFO");
// Finally, ping to verify that the connection is still usable
request req_ping;
req_ping.push("PING", "test_unsubscribe");
response<std::string> resp_subscribe, resp_unsubscribe, resp_ping;
bool subscribe_finished = false, unsubscribe_finished = false, ping_finished = false,
run_finished = false;
auto on_ping = [&](error_code ec, std::size_t) {
BOOST_TEST(ec == error_code());
ping_finished = true;
BOOST_TEST(std::get<0>(resp_ping).has_value());
BOOST_TEST(std::get<0>(resp_ping).value() == "test_unsubscribe");
conn.cancel();
};
auto on_unsubscribe = [&](error_code ec, std::size_t) {
unsubscribe_finished = true;
BOOST_TEST(ec == error_code());
BOOST_TEST(std::get<0>(resp_unsubscribe).has_value());
BOOST_TEST(find_client_info(std::get<0>(resp_unsubscribe).value(), "sub") == "2");
BOOST_TEST(find_client_info(std::get<0>(resp_unsubscribe).value(), "psub") == "1");
conn.async_exec(req_ping, resp_ping, on_ping);
};
auto on_subscribe = [&](error_code ec, std::size_t) {
subscribe_finished = true;
BOOST_TEST(ec == error_code());
BOOST_TEST(std::get<0>(resp_subscribe).has_value());
BOOST_TEST(find_client_info(std::get<0>(resp_subscribe).value(), "sub") == "3");
BOOST_TEST(find_client_info(std::get<0>(resp_subscribe).value(), "psub") == "2");
conn.async_exec(req_unsubscribe, resp_unsubscribe, on_unsubscribe);
};
conn.async_exec(req_subscribe, resp_subscribe, on_subscribe);
conn.async_run(make_test_config(), [&run_finished](error_code ec) {
BOOST_TEST(ec == net::error::operation_aborted);
run_finished = true;
});
ioc.run_for(test_timeout);
BOOST_TEST(subscribe_finished);
BOOST_TEST(unsubscribe_finished);
BOOST_TEST(ping_finished);
BOOST_TEST(run_finished);
}
} // namespace

View File

@@ -1,491 +0,0 @@
//
// 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,6 +18,7 @@
#include "common.hpp"
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
@@ -28,6 +29,37 @@ 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
@@ -64,13 +96,17 @@ 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, make_string_logger(logs)};
redis::connection conn{ioc, std::move(lgr)};
// Disable reconnection so the hello error causes the connection to exit
auto cfg = make_test_config();
@@ -90,8 +126,9 @@ void test_auth_failure()
BOOST_TEST(run_finished);
// Check the log
if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) {
std::cerr << "Log was: \n" << logs << std::endl;
auto log = oss.str();
if (!BOOST_TEST_NE(log.find("WRONGPASS"), std::string::npos)) {
std::cerr << "Log was: " << log << std::endl;
}
}
@@ -238,13 +275,17 @@ 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, make_string_logger(logs)};
redis::connection conn{ioc, std::move(lgr)};
// Disable reconnection so the hello error causes the connection to exit
auto cfg = make_test_config();
@@ -265,8 +306,9 @@ void test_setup_failure()
BOOST_TEST(run_finished);
// Check the log
if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) {
std::cerr << "Log was:\n" << logs << std::endl;
auto log = oss.str();
if (!BOOST_TEST_NE(log.find("wrong number of arguments"), std::string::npos)) {
std::cerr << "Log was: " << log << std::endl;
}
}
@@ -274,8 +316,7 @@ void test_setup_failure()
int main()
{
create_user("6379", "myuser", "mypass");
setup_password();
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 = "16380";
cfg.addr.port = "6380";
return cfg;
}

View File

@@ -6,6 +6,7 @@
// 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>
@@ -102,15 +103,30 @@ auto resolver_data = [] {
// Reduce duplication
struct fixture : detail::log_fixture {
config cfg;
buffered_logger lgr{make_logger()};
connect_fsm fsm{lgr};
redis_stream_state st;
connect_fsm fsm{cfg, lgr};
redis_stream_state st{};
fixture(transport_type type = transport_type::tcp)
: st{type, false}
fixture(config&& cfg = {})
: cfg{std::move(cfg)}
{ }
};
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
@@ -125,21 +141,20 @@ 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({
// 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
{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" },
});
}
void test_tcp_tls_success()
{
// Setup
fixture fix{transport_type::tcp_tls};
fixture fix{make_ssl_config()};
// Run the algorithm. No SSL stream reset is performed here
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -152,22 +167,21 @@ 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({
// 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
{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" },
});
}
void test_tcp_tls_success_reconnect()
{
// Setup
fixture fix{transport_type::tcp_tls};
fixture fix{make_ssl_config()};
fix.st.ssl_stream_used = true;
// Run the algorithm. The stream is used, so it needs to be reset
@@ -183,22 +197,21 @@ 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({
// 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
{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" },
});
}
void test_unix_success()
{
// Setup
fixture fix{transport_type::unix_socket};
fixture fix{make_unix_config()};
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -209,11 +222,12 @@ 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::debug, "Connect: UNIX socket connect succeeded"},
{logger::level::info, "Connected to /run/redis.sock"},
});
}
@@ -221,7 +235,7 @@ void test_unix_success()
void test_unix_success_close_error()
{
// Setup
fixture fix{transport_type::unix_socket};
fixture fix{make_unix_config()};
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -232,11 +246,12 @@ 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::debug, "Connect: UNIX socket connect succeeded"},
{logger::level::info, "Connected to /run/redis.sock"},
});
}
@@ -255,7 +270,7 @@ void test_tcp_resolve_error()
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Connect: hostname resolution failed: Expected field value is empty. [boost.redis:5]"},
{logger::level::info, "Error resolving the server hostname: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
@@ -278,7 +293,7 @@ void test_tcp_resolve_timeout()
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Connect: hostname resolution failed: Resolve timeout. [boost.redis:17]"},
{logger::level::info, "Error resolving the server hostname: Resolve timeout. [boost.redis:17]"},
// clang-format on
});
}
@@ -334,8 +349,8 @@ void test_tcp_connect_error()
// Check logging
fix.check_log({
// clang-format off
{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]"},
{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]"},
// clang-format on
});
}
@@ -360,8 +375,8 @@ void test_tcp_connect_timeout()
// Check logging
fix.check_log({
// clang-format off
{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]"},
{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]"},
// clang-format on
});
}
@@ -408,7 +423,7 @@ void test_tcp_connect_cancel_edge()
void test_ssl_handshake_error()
{
// Setup
fixture fix{transport_type::tcp_tls};
fixture fix{make_ssl_config()};
// Run the algorithm. No SSL stream reset is performed here
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -426,9 +441,9 @@ void test_ssl_handshake_error()
// Check logging
fix.check_log({
// 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::info, "Connect: SSL handshake failed: Expected field value is empty. [boost.redis:5]"},
{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]"},
// clang-format on
});
}
@@ -436,7 +451,7 @@ void test_ssl_handshake_error()
void test_ssl_handshake_timeout()
{
// Setup
fixture fix{transport_type::tcp_tls};
fixture fix{make_ssl_config()};
// Run the algorithm. Timeout = operation_aborted without the cancel type set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -454,9 +469,9 @@ void test_ssl_handshake_timeout()
// Check logging
fix.check_log({
// 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::info, "Connect: SSL handshake failed: SSL handshake timeout. [boost.redis:20]"},
{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]"},
// clang-format on
});
}
@@ -464,7 +479,7 @@ void test_ssl_handshake_timeout()
void test_ssl_handshake_cancel()
{
// Setup
fixture fix{transport_type::tcp_tls};
fixture fix{make_ssl_config()};
// Run the algorithm. Cancel = operation_aborted with the cancel type set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -486,7 +501,7 @@ void test_ssl_handshake_cancel()
void test_ssl_handshake_cancel_edge()
{
// Setup
fixture fix{transport_type::tcp_tls};
fixture fix{make_ssl_config()};
// Run the algorithm. No error, but the cancel state is set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -509,7 +524,7 @@ void test_ssl_handshake_cancel_edge()
void test_unix_connect_error()
{
// Setup
fixture fix{transport_type::unix_socket};
fixture fix{make_unix_config()};
// Run the algorithm
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -522,7 +537,7 @@ void test_unix_connect_error()
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Connect: UNIX socket connect failed: Expected field value is empty. [boost.redis:5]"},
{logger::level::info, "Failed to connect to the server: Expected field value is empty. [boost.redis:5]"},
// clang-format on
});
}
@@ -530,7 +545,7 @@ void test_unix_connect_error()
void test_unix_connect_timeout()
{
// Setup
fixture fix{transport_type::unix_socket};
fixture fix{make_unix_config()};
// Run the algorithm. Timeout = operation_aborted without a cancel state
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -543,7 +558,7 @@ void test_unix_connect_timeout()
// Check logging
fix.check_log({
// clang-format off
{logger::level::info, "Connect: UNIX socket connect failed: Connect timeout. [boost.redis:18]"},
{logger::level::info, "Failed to connect to the server: Connect timeout. [boost.redis:18]"},
// clang-format on
});
}
@@ -551,7 +566,7 @@ void test_unix_connect_timeout()
void test_unix_connect_cancel()
{
// Setup
fixture fix{transport_type::unix_socket};
fixture fix{make_unix_config()};
// Run the algorithm. Cancel = operation_aborted with a cancel state
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
@@ -568,7 +583,7 @@ void test_unix_connect_cancel()
void test_unix_connect_cancel_edge()
{
// Setup
fixture fix{transport_type::unix_socket};
fixture fix{make_unix_config()};
// Run the algorithm. No error, but cancel state is set
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);

View File

@@ -1,365 +0,0 @@
//
// 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

@@ -1,881 +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/adapter/adapt.hpp>
#include <boost/redis/resp3/flat_tree.hpp>
#include <boost/redis/resp3/node.hpp>
#include <boost/redis/resp3/type.hpp>
#include <boost/assert/source_location.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/core/span.hpp>
#include "print_node.hpp"
#include <algorithm>
#include <initializer_list>
#include <iostream>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
using boost::redis::adapter::adapt2;
using boost::redis::adapter::result;
using boost::redis::resp3::tree;
using boost::redis::resp3::flat_tree;
using boost::redis::generic_flat_response;
using boost::redis::resp3::type;
using boost::redis::resp3::detail::deserialize;
using boost::redis::resp3::node;
using boost::redis::resp3::node_view;
using boost::redis::resp3::to_string;
using boost::redis::response;
using boost::system::error_code;
namespace {
void add_nodes(
flat_tree& to,
std::string_view data,
boost::source_location loc = BOOST_CURRENT_LOCATION)
{
error_code ec;
deserialize(data, adapt2(to), ec);
if (!BOOST_TEST_EQ(ec, error_code{}))
std::cerr << "Called from " << loc << std::endl;
}
void check_nodes(
const flat_tree& tree,
boost::span<const node_view> expected,
boost::source_location loc = BOOST_CURRENT_LOCATION)
{
if (!BOOST_TEST_ALL_EQ(
tree.get_view().begin(),
tree.get_view().end(),
expected.begin(),
expected.end()))
std::cerr << "Called from " << loc << std::endl;
}
// --- Adding nodes ---
// Adding nodes works, even when reallocations happen.
// Empty nodes don't cause trouble
void test_add_nodes()
{
flat_tree t;
// Add a bunch of nodes. Single allocation. Some nodes are empty.
add_nodes(t, "*2\r\n+hello\r\n+world\r\n");
std::vector<node_view> expected_nodes{
{type::array, 2u, 0u, "" },
{type::simple_string, 1u, 1u, "hello"},
{type::simple_string, 1u, 1u, "world"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 10u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
// Capacity will have raised to 512 bytes, at least. Add some more without reallocations
add_nodes(t, "$3\r\nbye\r\n");
expected_nodes.push_back({type::blob_string, 1u, 0u, "bye"});
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 13u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 2u);
// Add nodes above the first reallocation threshold. Node strings are still valid
const std::string long_value(600u, 'a');
add_nodes(t, "+" + long_value + "\r\n");
expected_nodes.push_back({type::simple_string, 1u, 0u, long_value});
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 613u);
BOOST_TEST_EQ(t.data_capacity(), 1024u);
BOOST_TEST_EQ(t.get_reallocs(), 2u);
BOOST_TEST_EQ(t.get_total_msgs(), 3u);
// Add some more nodes, still within the reallocation threshold
add_nodes(t, "+some_other_value\r\n");
expected_nodes.push_back({type::simple_string, 1u, 0u, "some_other_value"});
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 629u);
BOOST_TEST_EQ(t.data_capacity(), 1024u);
BOOST_TEST_EQ(t.get_reallocs(), 2u);
BOOST_TEST_EQ(t.get_total_msgs(), 4u);
// Add some more, causing another reallocation
add_nodes(t, "+" + long_value + "\r\n");
expected_nodes.push_back({type::simple_string, 1u, 0u, long_value});
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 1229u);
BOOST_TEST_EQ(t.data_capacity(), 2048u);
BOOST_TEST_EQ(t.get_reallocs(), 3u);
BOOST_TEST_EQ(t.get_total_msgs(), 5u);
}
// Strings are really copied into the object
void test_add_nodes_copies()
{
flat_tree t;
// Place the message in dynamic memory
constexpr std::string_view const_msg = "+some_long_value_for_a_node\r\n";
std::unique_ptr<char[]> data{new char[100]{}};
std::copy(const_msg.begin(), const_msg.end(), data.get());
// Add nodes pointing into this message
add_nodes(t, data.get());
// Invalidate the original message
data.reset();
// Check
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "some_long_value_for_a_node"},
};
check_nodes(t, expected_nodes);
}
// Reallocations happen only when we would exceed capacity
void test_add_nodes_capacity_limit()
{
flat_tree t;
// Add a node to reach capacity 512
add_nodes(t, "+hello\r\n");
BOOST_TEST_EQ(t.data_size(), 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
// Fill the rest of the capacity
add_nodes(t, "+" + std::string(507u, 'b') + "\r\n");
BOOST_TEST_EQ(t.data_size(), 512u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
// Adding an empty node here doesn't change capacity
add_nodes(t, "_\r\n");
BOOST_TEST_EQ(t.data_size(), 512u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
// Adding more data causes a reallocation
add_nodes(t, "+a\r\n");
BOOST_TEST_EQ(t.data_size(), 513u);
BOOST_TEST_EQ(t.data_capacity(), 1024);
// Same goes for the next capacity limit
add_nodes(t, "+" + std::string(511u, 'c') + "\r\n");
BOOST_TEST_EQ(t.data_size(), 1024);
BOOST_TEST_EQ(t.data_capacity(), 1024);
// Reallocation
add_nodes(t, "+u\r\n");
BOOST_TEST_EQ(t.data_size(), 1025u);
BOOST_TEST_EQ(t.data_capacity(), 2048u);
// This would continue
add_nodes(t, "+" + std::string(1024u, 'd') + "\r\n");
BOOST_TEST_EQ(t.data_size(), 2049u);
BOOST_TEST_EQ(t.data_capacity(), 4096u);
}
// It's no problem if a node is big enough to surpass several reallocation limits
void test_add_nodes_big_node()
{
flat_tree t;
// Add a bunch of nodes. Single allocation. Some nodes are empty.
const std::string long_value(1500u, 'h');
add_nodes(t, "+" + long_value + "\r\n");
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, long_value},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 1500u);
BOOST_TEST_EQ(t.data_capacity(), 2048u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// --- Reserving space ---
// The usual case, calling it before using it
void test_reserve()
{
flat_tree t;
t.reserve(1024u, 5u);
check_nodes(t, {});
BOOST_TEST_EQ(t.get_view().capacity(), 5u);
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 1024);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
// Adding some nodes now works
add_nodes(t, "+hello\r\n");
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "hello"},
};
check_nodes(t, expected_nodes);
}
// Reserving space uses the same allocation thresholds
void test_reserve_not_power_of_2()
{
flat_tree t;
// First threshold at 512
t.reserve(200u, 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
// Second threshold at 1024
t.reserve(600u, 5u);
BOOST_TEST_EQ(t.data_capacity(), 1024u);
BOOST_TEST_EQ(t.get_reallocs(), 2u);
}
// Requesting a capacity below the current one does nothing
void test_reserve_below_current_capacity()
{
flat_tree t;
// Reserving with a zero capacity does nothing
t.reserve(0u, 0u);
BOOST_TEST_EQ(t.data_capacity(), 0u);
BOOST_TEST_EQ(t.get_reallocs(), 0u);
// Increase capacity
t.reserve(400u, 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
// Reserving again does nothing
t.reserve(400u, 5u);
t.reserve(512u, 5u);
t.reserve(0u, 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
}
// Reserving might reallocate. If there are nodes, strings remain valid
void test_reserve_with_data()
{
flat_tree t;
// Add a bunch of nodes, and then reserve
add_nodes(t, "*2\r\n+hello\r\n+world\r\n");
t.reserve(1000u, 10u);
// Check
std::vector<node_view> expected_nodes{
{type::array, 2u, 0u, "" },
{type::simple_string, 1u, 1u, "hello"},
{type::simple_string, 1u, 1u, "world"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 10u);
BOOST_TEST_EQ(t.data_capacity(), 1024u);
BOOST_TEST_EQ(t.get_reallocs(), 2u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// --- Clear ---
void test_clear()
{
flat_tree t;
// Add a bunch of nodes, then clear
add_nodes(t, "*2\r\n+hello\r\n+world\r\n");
t.clear();
// Nodes are no longer there, but memory hasn't been fred
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// Clearing an empty tree doesn't cause trouble
void test_clear_empty()
{
flat_tree t;
t.clear();
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 0u);
BOOST_TEST_EQ(t.get_reallocs(), 0u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// With clear, memory can be reused
// The response should be reusable.
void test_clear_reuse()
{
flat_tree t;
// First use
add_nodes(t, "~6\r\n+orange\r\n+apple\r\n+one\r\n+two\r\n+three\r\n+orange\r\n");
std::vector<node_view> expected_nodes{
{type::set, 6u, 0u, "" },
{type::simple_string, 1u, 1u, "orange"},
{type::simple_string, 1u, 1u, "apple" },
{type::simple_string, 1u, 1u, "one" },
{type::simple_string, 1u, 1u, "two" },
{type::simple_string, 1u, 1u, "three" },
{type::simple_string, 1u, 1u, "orange"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
// Second use
t.clear();
add_nodes(t, "*2\r\n+hello\r\n+world\r\n");
expected_nodes = {
{type::array, 2u, 0u, "" },
{type::simple_string, 1u, 1u, "hello"},
{type::simple_string, 1u, 1u, "world"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// --- Default ctor ---
void test_default_constructor()
{
flat_tree t;
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.get_reallocs(), 0u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// --- Copy ctor ---
void test_copy_ctor()
{
// Setup
auto t = std::make_unique<flat_tree>();
add_nodes(*t, "*2\r\n+hello\r\n+world\r\n");
std::vector<node_view> expected_nodes{
{type::array, 2u, 0u, "" },
{type::simple_string, 1u, 1u, "hello"},
{type::simple_string, 1u, 1u, "world"},
};
// Construct, then destroy the original copy
flat_tree t2{*t};
t.reset();
// Check
check_nodes(t2, expected_nodes);
BOOST_TEST_EQ(t2.data_size(), 10u);
BOOST_TEST_EQ(t2.data_capacity(), 512u);
BOOST_TEST_EQ(t2.get_reallocs(), 1u);
BOOST_TEST_EQ(t2.get_total_msgs(), 1u);
}
// Copying an empty tree doesn't cause problems
void test_copy_ctor_empty()
{
flat_tree t;
flat_tree t2{t};
check_nodes(t2, {});
BOOST_TEST_EQ(t2.data_size(), 0u);
BOOST_TEST_EQ(t2.data_capacity(), 0u);
BOOST_TEST_EQ(t2.get_reallocs(), 0u);
BOOST_TEST_EQ(t2.get_total_msgs(), 0u);
}
// Copying an object that has no elements but some capacity doesn't cause trouble
void test_copy_ctor_empty_with_capacity()
{
flat_tree t;
t.reserve(300u, 8u);
flat_tree t2{t};
check_nodes(t2, {});
BOOST_TEST_EQ(t2.data_size(), 0u);
BOOST_TEST_EQ(t2.data_capacity(), 0u);
BOOST_TEST_EQ(t2.get_reallocs(), 0u);
BOOST_TEST_EQ(t2.get_total_msgs(), 0u);
}
// Copying an object with more capacity than required adjusts its capacity
void test_copy_ctor_adjust_capacity()
{
// Setup
flat_tree t;
add_nodes(t, "+hello\r\n");
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "hello"},
};
// Cause reallocations
t.reserve(1000u, 10u);
t.reserve(2000u, 10u);
t.reserve(4000u, 10u);
// Copy
flat_tree t2{t};
// The target object has the minimum required capacity,
// and the number of reallocs has been reset
check_nodes(t2, expected_nodes);
BOOST_TEST_EQ(t2.data_size(), 5u);
BOOST_TEST_EQ(t2.data_capacity(), 512u);
BOOST_TEST_EQ(t2.get_reallocs(), 1u);
BOOST_TEST_EQ(t2.get_total_msgs(), 1u);
}
// --- Move ctor ---
void test_move_ctor()
{
flat_tree t;
add_nodes(t, "*2\r\n+hello\r\n+world\r\n");
flat_tree t2{std::move(t)};
std::vector<node_view> expected_nodes{
{type::array, 2u, 0u, "" },
{type::simple_string, 1u, 1u, "hello"},
{type::simple_string, 1u, 1u, "world"},
};
check_nodes(t2, expected_nodes);
BOOST_TEST_EQ(t2.data_size(), 10u);
BOOST_TEST_EQ(t2.data_capacity(), 512u);
BOOST_TEST_EQ(t2.get_reallocs(), 1u);
BOOST_TEST_EQ(t2.get_total_msgs(), 1u);
}
// Moving an empty object doesn't cause trouble
void test_move_ctor_empty()
{
flat_tree t;
flat_tree t2{std::move(t)};
check_nodes(t2, {});
BOOST_TEST_EQ(t2.data_size(), 0u);
BOOST_TEST_EQ(t2.data_capacity(), 0u);
BOOST_TEST_EQ(t2.get_reallocs(), 0u);
BOOST_TEST_EQ(t2.get_total_msgs(), 0u);
}
// Moving an object with capacity but no data doesn't cause trouble
void test_move_ctor_with_capacity()
{
flat_tree t;
t.reserve(1000u, 10u);
flat_tree t2{std::move(t)};
check_nodes(t2, {});
BOOST_TEST_EQ(t2.data_size(), 0u);
BOOST_TEST_EQ(t2.data_capacity(), 1024u);
BOOST_TEST_EQ(t2.get_reallocs(), 1u);
BOOST_TEST_EQ(t2.get_total_msgs(), 0u);
}
// --- Copy assignment ---
void test_copy_assign()
{
flat_tree t;
add_nodes(t, "+some_data\r\n");
auto t2 = std::make_unique<flat_tree>();
add_nodes(*t2, "*2\r\n+hello\r\n+world\r\n");
t = *t2;
// Delete the source object, to check that we copied the contents
t2.reset();
std::vector<node_view> expected_nodes{
{type::array, 2u, 0u, "" },
{type::simple_string, 1u, 1u, "hello"},
{type::simple_string, 1u, 1u, "world"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 10u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// The lhs is empty and doesn't have any capacity
void test_copy_assign_target_empty()
{
flat_tree t;
flat_tree t2;
add_nodes(t2, "+hello\r\n");
t = t2;
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "hello"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// If the target doesn't have enough capacity, a reallocation happens
void test_copy_assign_target_not_enough_capacity()
{
flat_tree t;
add_nodes(t, "+hello\r\n");
const std::string big_node(2000u, 'a');
flat_tree t2;
add_nodes(t2, "+" + big_node + "\r\n");
t = t2;
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, big_node},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 2000u);
BOOST_TEST_EQ(t.data_capacity(), 2048u);
BOOST_TEST_EQ(t.get_reallocs(), 2u); // initial + assignment
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// If the source of the assignment is empty, nothing bad happens
void test_copy_assign_source_empty()
{
flat_tree t;
add_nodes(t, "+hello\r\n");
flat_tree t2;
t = t2;
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 512u); // capacity is kept
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// If the source of the assignment has capacity but no data, we're OK
void test_copy_assign_source_with_capacity()
{
flat_tree t;
add_nodes(t, "+hello\r\n");
flat_tree t2;
t2.reserve(1000u, 4u);
t2.reserve(4000u, 8u);
t = t2;
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 512u); // capacity is kept
BOOST_TEST_EQ(t.get_reallocs(), 1u); // not propagated
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// If the source of the assignment has data with extra capacity
// and a reallocation is needed, the minimum amount of space is allocated
void test_copy_assign_source_with_extra_capacity()
{
flat_tree t;
flat_tree t2;
add_nodes(t2, "+hello\r\n");
t2.reserve(4000u, 8u);
t = t2;
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "hello"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
void test_copy_assign_both_empty()
{
flat_tree t;
flat_tree t2;
t = t2;
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 0u);
BOOST_TEST_EQ(t.get_reallocs(), 0u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// Self-assignment doesn't cause trouble
void test_copy_assign_self()
{
flat_tree t;
add_nodes(t, "+hello\r\n");
const auto& tref = t;
t = tref;
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "hello"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// --- Move assignment ---
void test_move_assign()
{
flat_tree t;
add_nodes(t, "+some_data\r\n");
flat_tree t2;
add_nodes(t2, "*2\r\n+hello\r\n+world\r\n");
t = std::move(t2);
std::vector<node_view> expected_nodes{
{type::array, 2u, 0u, "" },
{type::simple_string, 1u, 1u, "hello"},
{type::simple_string, 1u, 1u, "world"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 10u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// The lhs is empty and doesn't have any capacity
void test_move_assign_target_empty()
{
flat_tree t;
flat_tree t2;
add_nodes(t2, "+hello\r\n");
t = std::move(t2);
std::vector<node_view> expected_nodes{
{type::simple_string, 1u, 0u, "hello"},
};
check_nodes(t, expected_nodes);
BOOST_TEST_EQ(t.data_size(), 5u);
BOOST_TEST_EQ(t.data_capacity(), 512u);
BOOST_TEST_EQ(t.get_reallocs(), 1u);
BOOST_TEST_EQ(t.get_total_msgs(), 1u);
}
// If the source of the assignment is empty, nothing bad happens
void test_move_assign_source_empty()
{
flat_tree t;
add_nodes(t, "+hello\r\n");
flat_tree t2;
t = std::move(t2);
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 0u);
BOOST_TEST_EQ(t.get_reallocs(), 0u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// If both source and target are empty, nothing bad happens
void test_move_assign_both_empty()
{
flat_tree t;
flat_tree t2;
t = std::move(t2);
check_nodes(t, {});
BOOST_TEST_EQ(t.data_size(), 0u);
BOOST_TEST_EQ(t.data_capacity(), 0u);
BOOST_TEST_EQ(t.get_reallocs(), 0u);
BOOST_TEST_EQ(t.get_total_msgs(), 0u);
}
// --- Comparison ---
void test_comparison_different()
{
flat_tree t;
add_nodes(t, "+some_data\r\n");
flat_tree t2;
add_nodes(t2, "*2\r\n+hello\r\n+world\r\n");
BOOST_TEST_NOT(t == t2);
BOOST_TEST(t != t2);
BOOST_TEST_NOT(t2 == t);
BOOST_TEST(t2 != t);
}
// The only difference is node types
void test_comparison_different_node_types()
{
flat_tree t;
add_nodes(t, "+hello\r\n");
flat_tree t2;
add_nodes(t2, "$5\r\nhello\r\n");
BOOST_TEST_NOT(t == t2);
BOOST_TEST(t != t2);
}
void test_comparison_equal()
{
flat_tree t;
add_nodes(t, "+some_data\r\n");
flat_tree t2;
add_nodes(t2, "+some_data\r\n");
BOOST_TEST(t == t2);
BOOST_TEST_NOT(t != t2);
}
// Allocations are not taken into account when comparing
void test_comparison_equal_reallocations()
{
const std::string big_node(2000u, 'a');
flat_tree t;
t.reserve(100u, 5u);
add_nodes(t, "+" + big_node + "\r\n");
BOOST_TEST_EQ(t.get_reallocs(), 2u);
flat_tree t2;
t2.reserve(2048u, 5u);
add_nodes(t2, "+" + big_node + "\r\n");
BOOST_TEST_EQ(t2.get_reallocs(), 1u);
BOOST_TEST(t == t2);
BOOST_TEST_NOT(t != t2);
}
// Capacity is not taken into account when comparing
void test_comparison_equal_capacity()
{
flat_tree t;
add_nodes(t, "+hello\r\n");
flat_tree t2;
t2.reserve(2048u, 5u);
add_nodes(t2, "+hello\r\n");
BOOST_TEST(t == t2);
BOOST_TEST_NOT(t != t2);
}
// Empty containers don't cause trouble
void test_comparison_empty()
{
flat_tree t;
add_nodes(t, "$5\r\nhello\r\n");
flat_tree tempty, tempty2;
BOOST_TEST_NOT(t == tempty);
BOOST_TEST(t != tempty);
BOOST_TEST_NOT(tempty == t);
BOOST_TEST(tempty != t);
BOOST_TEST(tempty == tempty2);
BOOST_TEST_NOT(tempty != tempty2);
}
// Self comparisons don't cause trouble
void test_comparison_self()
{
flat_tree t;
add_nodes(t, "$5\r\nhello\r\n");
flat_tree tempty;
BOOST_TEST(t == t);
BOOST_TEST_NOT(t != t);
BOOST_TEST(tempty == tempty);
BOOST_TEST_NOT(tempty != tempty);
}
} // namespace
int main()
{
test_add_nodes();
test_add_nodes_copies();
test_add_nodes_capacity_limit();
test_add_nodes_big_node();
test_reserve();
test_reserve_not_power_of_2();
test_reserve_below_current_capacity();
test_reserve_with_data();
test_clear();
test_clear_empty();
test_clear_reuse();
test_default_constructor();
test_copy_ctor();
test_copy_ctor_empty();
test_copy_ctor_empty_with_capacity();
test_copy_ctor_adjust_capacity();
test_move_ctor();
test_move_ctor_empty();
test_move_ctor_with_capacity();
test_move_assign();
test_move_assign_target_empty();
test_move_assign_source_empty();
test_move_assign_both_empty();
test_copy_assign();
test_copy_assign_target_empty();
test_copy_assign_target_not_enough_capacity();
test_copy_assign_source_empty();
test_copy_assign_source_with_capacity();
test_copy_assign_source_with_extra_capacity();
test_copy_assign_both_empty();
test_copy_assign_self();
test_comparison_different();
test_comparison_different_node_types();
test_comparison_equal();
test_comparison_equal_reallocations();
test_comparison_equal_capacity();
test_comparison_empty();
test_comparison_self();
return boost::report_errors();
}

View File

@@ -42,15 +42,16 @@ namespace {
// Push consumer
auto receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
{
std::cout << "Entering receiver" << std::endl;
std::cout << "uuu" << std::endl;
while (conn->will_reconnect()) {
std::cout << "Reconnect loop" << std::endl;
// Loop reading Redis pushes.
for (error_code ec;;) {
std::cout << "Receive loop" << std::endl;
co_await conn->async_receive2(net::redirect_error(ec));
std::cout << "dddd" << std::endl;
// Loop reading Redis pushs messages.
for (;;) {
std::cout << "aaaa" << std::endl;
error_code ec;
co_await conn->async_receive(net::redirect_error(ec));
if (ec) {
std::cout << "Error in async_receive2" << std::endl;
std::cout << "Error in async_receive" << std::endl;
break;
}
}

View File

@@ -530,11 +530,6 @@ 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

@@ -1,4 +1,4 @@
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
*
* Distributed under the Boost Software License, Version 1.0. (See
* accompanying file LICENSE.txt)
@@ -22,20 +22,15 @@
using boost::redis::request;
using boost::redis::adapter::adapt2;
using boost::redis::adapter::result;
using boost::redis::resp3::tree;
using boost::redis::resp3::flat_tree;
using boost::redis::generic_flat_response;
using boost::redis::generic_response;
using boost::redis::ignore_t;
using boost::redis::resp3::detail::deserialize;
using boost::redis::resp3::node;
using boost::redis::resp3::node_view;
using boost::redis::resp3::to_string;
using boost::redis::response;
using boost::redis::any_adapter;
using boost::system::error_code;
namespace resp3 = boost::redis::resp3;
#define RESP3_SET_PART1 "~6\r\n+orange\r"
#define RESP3_SET_PART2 "\n+apple\r\n+one"
#define RESP3_SET_PART3 "\r\n+two\r"
@@ -47,9 +42,7 @@ BOOST_AUTO_TEST_CASE(low_level_sync_sans_io)
try {
result<std::set<std::string>> resp;
error_code ec;
deserialize(resp3_set, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
deserialize(resp3_set, adapt2(resp));
for (auto const& e : resp.value())
std::cout << e << std::endl;
@@ -72,9 +65,7 @@ BOOST_AUTO_TEST_CASE(issue_210_empty_set)
char const* wire = "*4\r\n:1\r\n~0\r\n$25\r\nthis_should_not_be_in_set\r\n:2\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
deserialize(wire, adapt2(resp));
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
BOOST_CHECK(std::get<1>(resp.value()).value().empty());
@@ -100,9 +91,7 @@ BOOST_AUTO_TEST_CASE(issue_210_non_empty_set_size_one)
char const*
wire = "*4\r\n:1\r\n~1\r\n$3\r\nfoo\r\n$25\r\nthis_should_not_be_in_set\r\n:2\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
deserialize(wire, adapt2(resp));
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
BOOST_CHECK_EQUAL(std::get<1>(resp.value()).value().size(), 1u);
@@ -129,9 +118,7 @@ BOOST_AUTO_TEST_CASE(issue_210_non_empty_set_size_two)
char const* wire =
"*4\r\n:1\r\n~2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$25\r\nthis_should_not_be_in_set\r\n:2\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
deserialize(wire, adapt2(resp));
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
BOOST_CHECK_EQUAL(std::get<1>(resp.value()).value().at(0), std::string{"foo"});
@@ -153,9 +140,7 @@ BOOST_AUTO_TEST_CASE(issue_210_no_nested)
char const*
wire = "*4\r\n:1\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$25\r\nthis_should_not_be_in_set\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
deserialize(wire, adapt2(resp));
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
BOOST_CHECK_EQUAL(std::get<1>(resp.value()).value(), std::string{"foo"});
@@ -174,10 +159,7 @@ BOOST_AUTO_TEST_CASE(issue_233_array_with_null)
result<std::vector<std::optional<std::string>>> resp;
char const* wire = "*3\r\n+one\r\n_\r\n+two\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
deserialize(wire, adapt2(resp));
BOOST_CHECK_EQUAL(resp.value().at(0).value(), "one");
BOOST_TEST(!resp.value().at(1).has_value());
@@ -195,10 +177,7 @@ BOOST_AUTO_TEST_CASE(issue_233_optional_array_with_null)
result<std::optional<std::vector<std::optional<std::string>>>> resp;
char const* wire = "*3\r\n+one\r\n_\r\n+two\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
deserialize(wire, adapt2(resp));
BOOST_CHECK_EQUAL(resp.value().value().at(0).value(), "one");
BOOST_TEST(!resp.value().value().at(1).has_value());
@@ -210,6 +189,87 @@ BOOST_AUTO_TEST_CASE(issue_233_optional_array_with_null)
}
}
BOOST_AUTO_TEST_CASE(read_buffer_prepare_error)
{
using boost::redis::detail::read_buffer;
read_buffer buf;
// Usual case, max size is bigger then requested size.
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST(!ec);
buf.commit(10);
// Corner case, max size is equal to the requested size.
buf.set_config({10, 20});
ec = buf.prepare();
BOOST_TEST(!ec);
buf.commit(10);
buf.consume(20);
auto const tmp = buf;
// Error case, max size is smaller to the requested size.
buf.set_config({10, 9});
ec = buf.prepare();
BOOST_TEST(ec == error_code{boost::redis::error::exceeds_maximum_read_buffer_size});
// Check that an error call has no side effects.
auto const res = buf == tmp;
BOOST_TEST(res);
}
BOOST_AUTO_TEST_CASE(read_buffer_prepare_consume_only_committed_data)
{
using boost::redis::detail::read_buffer;
read_buffer buf;
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST(!ec);
auto res = buf.consume(5);
// No data has been committed yet so nothing can be consummed.
BOOST_CHECK_EQUAL(res.consumed, 0u);
// If nothing was consumed, nothing got rotated.
BOOST_CHECK_EQUAL(res.rotated, 0u);
buf.commit(10);
res = buf.consume(5);
// All five bytes should have been consumed.
BOOST_CHECK_EQUAL(res.consumed, 5u);
// We added a total of 10 bytes and consumed 5, that means, 5 were
// rotated.
BOOST_CHECK_EQUAL(res.rotated, 5u);
res = buf.consume(7);
// Only the remaining five bytes can be consumed
BOOST_CHECK_EQUAL(res.consumed, 5u);
// No bytes to rotated.
BOOST_CHECK_EQUAL(res.rotated, 0u);
}
BOOST_AUTO_TEST_CASE(read_buffer_check_buffer_size)
{
using boost::redis::detail::read_buffer;
read_buffer buf;
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST(!ec);
BOOST_CHECK_EQUAL(buf.get_prepared().size(), 10u);
}
BOOST_AUTO_TEST_CASE(check_counter_adapter)
{
using boost::redis::any_adapter;
@@ -253,39 +313,3 @@ BOOST_AUTO_TEST_CASE(check_counter_adapter)
BOOST_CHECK_EQUAL(node, 7);
BOOST_CHECK_EQUAL(done, 1);
}
BOOST_AUTO_TEST_CASE(generic_flat_response_simple_error)
{
generic_flat_response resp;
char const* wire = "-Error\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
BOOST_TEST(!resp.has_value());
BOOST_TEST(resp.has_error());
auto const error = resp.error();
BOOST_CHECK_EQUAL(error.data_type, boost::redis::resp3::type::simple_error);
BOOST_CHECK_EQUAL(error.diagnostic, std::string{"Error"});
}
BOOST_AUTO_TEST_CASE(generic_flat_response_blob_error)
{
generic_flat_response resp;
char const* wire = "!5\r\nError\r\n";
error_code ec;
deserialize(wire, adapt2(resp), ec);
BOOST_CHECK_EQUAL(ec, error_code{});
BOOST_TEST(!resp.has_value());
BOOST_TEST(resp.has_error());
auto const error = resp.error();
BOOST_CHECK_EQUAL(error.data_type, boost::redis::resp3::type::blob_error);
BOOST_CHECK_EQUAL(error.diagnostic, std::string{"Error"});
}

View File

@@ -15,7 +15,6 @@
#include <boost/assert/source_location.hpp>
#include <boost/core/lightweight_test.hpp>
#include "print_node.hpp"
#include "sansio_utils.hpp"
#include <iostream>
@@ -34,6 +33,17 @@ 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

@@ -1,727 +0,0 @@
//
// 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

@@ -1,102 +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/detail/read_buffer.hpp>
#include <boost/redis/error.hpp>
#include <boost/core/lightweight_test.hpp>
#include <boost/system/error_code.hpp>
using namespace boost::redis;
using detail::read_buffer;
using boost::system::error_code;
namespace {
void test_prepare_error()
{
read_buffer buf;
// Usual case, max size is bigger then requested size.
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST_EQ(ec, error_code());
buf.commit(10);
// Corner case, max size is equal to the requested size.
buf.set_config({10, 20});
ec = buf.prepare();
BOOST_TEST_EQ(ec, error_code());
buf.commit(10);
buf.consume(20);
auto const tmp = buf;
// Error case, max size is smaller to the requested size.
buf.set_config({10, 9});
ec = buf.prepare();
BOOST_TEST_EQ(ec, error_code{error::exceeds_maximum_read_buffer_size});
// Check that an error call has no side effects.
BOOST_TEST(buf == tmp);
}
void test_prepare_consume_only_committed_data()
{
read_buffer buf;
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST(!ec);
auto res = buf.consume(5);
// No data has been committed yet so nothing can be consummed.
BOOST_TEST_EQ(res.consumed, 0u);
// If nothing was consumed, nothing got rotated.
BOOST_TEST_EQ(res.rotated, 0u);
buf.commit(10);
res = buf.consume(5);
// All five bytes should have been consumed.
BOOST_TEST_EQ(res.consumed, 5u);
// We added a total of 10 bytes and consumed 5, that means, 5 were
// rotated.
BOOST_TEST_EQ(res.rotated, 5u);
res = buf.consume(7);
// Only the remaining five bytes can be consumed
BOOST_TEST_EQ(res.consumed, 5u);
// No bytes to rotated.
BOOST_TEST_EQ(res.rotated, 0u);
}
void test_check_buffer_size()
{
read_buffer buf;
buf.set_config({10, 10});
auto ec = buf.prepare();
BOOST_TEST_EQ(ec, error_code());
BOOST_TEST_EQ(buf.get_prepared().size(), 10u);
}
} // namespace
int main()
{
test_prepare_error();
test_prepare_consume_only_committed_data();
test_check_buffer_size();
return boost::report_errors();
}

View File

@@ -14,6 +14,8 @@
using boost::redis::request;
// TODO: Serialization.
namespace {
void test_push_no_args()

View File

@@ -39,7 +39,6 @@ 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";
@@ -143,30 +142,6 @@ 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()
{
@@ -187,83 +162,10 @@ 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);
// 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
});
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// 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()
{
@@ -278,13 +180,8 @@ 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));
// 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
});
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// A cancellation in connect makes the operation finish even with reconnection enabled
@@ -301,10 +198,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));
// Log
// We log on cancellation only
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)" }
{logger::level::debug, "Run: cancelled (1)"}
});
}
@@ -322,10 +218,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));
// Log
// We log on cancellation only
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)" }
{logger::level::debug, "Run: cancelled (1)"}
});
}
@@ -352,13 +247,8 @@ 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);
// 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)" },
});
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// An error in the parallel group makes the operation exit if reconnection is disabled
@@ -379,11 +269,8 @@ 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));
// 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)" },
});
// Run doesn't log, it's the subordinate tasks that do
fix.check_log({});
}
// A cancellation in the parallel group makes it exit, even if reconnection is enabled.
@@ -405,11 +292,9 @@ 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));
// Log
// We log on cancellation only
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)" }
{logger::level::debug, "Run: cancelled (2)"}
});
}
@@ -430,11 +315,9 @@ 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));
// Log
// We log on cancellation only
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)" }
{logger::level::debug, "Run: cancelled (2)"}
});
}
@@ -460,11 +343,9 @@ 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));
// Log
// We log on cancellation only
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)" }
{logger::level::debug, "Run: cancelled (3)"}
});
}
@@ -489,11 +370,9 @@ 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));
// Log
// We log on cancellation only
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)" }
{logger::level::debug, "Run: cancelled (3)"}
});
}
@@ -530,16 +409,9 @@ 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));
// Log
// The cancellation was logged
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)" },
{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
{logger::level::debug, "Run: cancelled (2)"}
});
}
@@ -609,11 +481,7 @@ void test_setup_request_success()
// Check 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)" },
{logger::level::info, "Setup request execution: success"},
// clang-format on
{logger::level::info, "Setup request execution: success"}
});
}
@@ -633,13 +501,8 @@ void test_setup_request_empty()
// Nothing was added to the multiplexer
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 0u);
// 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
});
// Check log
fix.check_log({});
}
// A server error would cause the reader to exit
@@ -647,7 +510,7 @@ void test_setup_request_server_error()
{
// Setup
fixture fix;
fix.st.diagnostic = "leftover"; // simulate a leftover from previous runs
fix.st.setup_diagnostic = "leftover"; // simulate a leftover from previous runs
fix.st.cfg.setup.clear();
fix.st.cfg.setup.push("HELLO", 3);
@@ -670,147 +533,9 @@ 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)"},
"establishment contains an error. [boost.redis:23] (ERR: wrong command)"}
});
}
@@ -822,13 +547,8 @@ 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();
@@ -848,10 +568,5 @@ 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

@@ -1,682 +0,0 @@
//
// 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();
}

View File

@@ -1,193 +0,0 @@
//
// 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/request.hpp>
#include <boost/redis/resp3/serialization.hpp>
#include <boost/core/lightweight_test.hpp>
#include <cstdint>
#include <limits>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
using boost::redis::request;
namespace other {
struct my_struct {
int value;
};
void boost_redis_to_bulk(std::string& to, my_struct value)
{
boost::redis::resp3::boost_redis_to_bulk(to, value.value);
}
} // namespace other
namespace {
// --- Strings ---
void test_string_view()
{
request req;
req.push("GET", std::string_view("key"));
BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n");
}
void test_string()
{
std::string s{"k1"};
const std::string s2{"k2"};
request req;
req.push("GET", s, s2, std::string("k3"));
BOOST_TEST_EQ(req.payload(), "*4\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\nk2\r\n$2\r\nk3\r\n");
}
void test_c_string()
{
request req;
req.push("GET", "k1", static_cast<const char*>("k2"));
BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\nk2\r\n");
}
void test_string_empty()
{
request req;
req.push("GET", std::string_view{});
BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$0\r\n\r\n");
}
// --- Integers ---
void test_signed_ints()
{
request req;
req.push("GET", static_cast<signed char>(20), static_cast<short>(-42), -1, 80l, 200ll);
BOOST_TEST_EQ(
req.payload(),
"*6\r\n$3\r\nGET\r\n$2\r\n20\r\n$3\r\n-42\r\n$2\r\n-1\r\n$2\r\n80\r\n$3\r\n200\r\n");
}
void test_unsigned_ints()
{
request req;
req.push(
"GET",
static_cast<unsigned char>(20),
static_cast<unsigned short>(42),
50u,
80ul,
200ull);
BOOST_TEST_EQ(
req.payload(),
"*6\r\n$3\r\nGET\r\n$2\r\n20\r\n$2\r\n42\r\n$2\r\n50\r\n$2\r\n80\r\n$3\r\n200\r\n");
}
// We don't overflow for big ints
void test_signed_ints_minmax()
{
using lims = std::numeric_limits<std::int64_t>;
request req;
req.push("GET", (lims::min)(), (lims::max)());
BOOST_TEST_EQ(
req.payload(),
"*3\r\n$3\r\nGET\r\n$20\r\n-9223372036854775808\r\n$19\r\n9223372036854775807\r\n");
}
void test_unsigned_ints_max()
{
request req;
req.push("GET", (std::numeric_limits<std::uint64_t>::max)());
BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$20\r\n18446744073709551615\r\n");
}
// Custom type
void test_custom()
{
request req;
req.push("GET", other::my_struct{42});
BOOST_TEST_EQ(req.payload(), "*2\r\n$3\r\nGET\r\n$2\r\n42\r\n");
}
// --- Pairs and tuples (only supported in the range versions) ---
// Nested structures are not supported (compile time error)
void test_pair()
{
std::vector<std::pair<std::string_view, int>> vec{
{"k1", 42}
};
request req;
req.push_range("GET", vec);
BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n");
}
void test_pair_custom()
{
std::vector<std::pair<std::string_view, other::my_struct>> vec{
{"k1", {42}}
};
request req;
req.push_range("GET", vec);
BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n");
}
void test_tuple()
{
std::vector<std::tuple<std::string_view, int, unsigned char>> vec{
{"k1", 42, 1}
};
request req;
req.push_range("GET", vec);
BOOST_TEST_EQ(req.payload(), "*4\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n$1\r\n1\r\n");
}
void test_tuple_custom()
{
std::vector<std::tuple<std::string_view, other::my_struct>> vec{
{"k1", {42}}
};
request req;
req.push_range("GET", vec);
BOOST_TEST_EQ(req.payload(), "*3\r\n$3\r\nGET\r\n$2\r\nk1\r\n$2\r\n42\r\n");
}
void test_tuple_empty()
{
std::vector<std::tuple<>> vec{{}};
request req;
req.push_range("GET", vec);
BOOST_TEST_EQ(req.payload(), "*1\r\n$3\r\nGET\r\n");
}
} // namespace
int main()
{
test_string_view();
test_string();
test_c_string();
test_string_empty();
test_signed_ints();
test_unsigned_ints();
test_signed_ints_minmax();
test_unsigned_ints_max();
test_custom();
test_pair();
test_pair_custom();
test_tuple();
test_tuple_custom();
test_tuple_empty();
return boost::report_errors();
}

View File

@@ -1,349 +0,0 @@
//
// 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

@@ -25,7 +25,7 @@ using boost::system::error_code;
namespace {
void test_hello()
void test_compose_setup()
{
redis::config cfg;
cfg.clientname = "";
@@ -39,7 +39,7 @@ void test_hello()
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_select()
void test_compose_setup_select()
{
redis::config cfg;
cfg.clientname = "";
@@ -56,7 +56,7 @@ void test_select()
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_clientname()
void test_compose_setup_clientname()
{
redis::config cfg;
@@ -70,7 +70,7 @@ void test_clientname()
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_auth()
void test_compose_setup_auth()
{
redis::config cfg;
cfg.clientname = "";
@@ -87,7 +87,7 @@ void test_auth()
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_auth_empty_password()
void test_compose_setup_auth_empty_password()
{
redis::config cfg;
cfg.clientname = "";
@@ -103,7 +103,7 @@ void test_auth_empty_password()
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_auth_setname()
void test_compose_setup_auth_setname()
{
redis::config cfg;
cfg.clientname = "mytest";
@@ -121,7 +121,7 @@ void test_auth_setname()
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
}
void test_use_setup()
void test_compose_setup_use_setup()
{
redis::config cfg;
cfg.clientname = "mytest";
@@ -143,7 +143,7 @@ void test_use_setup()
}
// Regression check: we set the priority flag
void test_use_setup_no_hello()
void test_compose_setup_use_setup_no_hello()
{
redis::config cfg;
cfg.use_setup = true;
@@ -160,7 +160,7 @@ void test_use_setup_no_hello()
}
// Regression check: we set the relevant cancellation flags in the request
void test_use_setup_flags()
void test_compose_setup_use_setup_flags()
{
redis::config cfg;
cfg.use_setup = true;
@@ -178,65 +178,19 @@ void test_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_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_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()
{
test_hello();
test_select();
test_clientname();
test_auth();
test_auth_empty_password();
test_auth_setname();
test_use_setup();
test_use_setup_no_hello();
test_use_setup_flags();
test_sentinel_auth();
test_sentinel_use_setup();
test_compose_setup();
test_compose_setup_select();
test_compose_setup_clientname();
test_compose_setup_auth();
test_compose_setup_auth_empty_password();
test_compose_setup_auth_setname();
test_compose_setup_use_setup();
test_compose_setup_use_setup_no_hello();
test_compose_setup_use_setup_flags();
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 = "16380";
tcp_tls_cfg.addr.port = "6380";
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 = "16380";
cfg.addr.port = "6380";
cfg.unix_socket = unix_socket_path;
bool finished = false;

View File

@@ -1,212 +0,0 @@
//
// 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,140 +1,19 @@
services:
redis-master:
container_name: redis-master
redis:
image: ${SERVER_IMAGE}
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'
entrypoint: "/docker/entrypoint.sh"
volumes:
- ./docker:/docker
- /tmp/redis-socks:/tmp/redis-socks
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
ports:
- 6379:6379
- 6380:6380
builder:
container_name: builder
image: ${BUILDER_IMAGE}
network_mode: host
container_name: builder
tty: true
environment:
- BOOST_REDIS_TEST_SERVER=redis
volumes:
- ../:/boost-redis
- /tmp/redis-socks:/tmp/redis-socks

16
tools/docker/entrypoint.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/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