mirror of
https://github.com/boostorg/redis.git
synced 2026-01-19 16:52:08 +00:00
Compare commits
34 Commits
better_sub
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18ee72830b | ||
|
|
89e44dc017 | ||
|
|
002b616dd9 | ||
|
|
c11a5194d8 | ||
|
|
bea547481a | ||
|
|
3b07119e54 | ||
|
|
7750a6b126 | ||
|
|
2bbf0090b5 | ||
|
|
02632b31c6 | ||
|
|
6005ebd04a | ||
|
|
755d14a10d | ||
|
|
d9e4b2c720 | ||
|
|
91afb4a279 | ||
|
|
bdd9c327c1 | ||
|
|
00f3ec9b78 | ||
|
|
b365b96228 | ||
|
|
2c1f1c4c50 | ||
|
|
6ff474008f | ||
|
|
bd799aff96 | ||
|
|
c284960549 | ||
|
|
b1420d3d1d | ||
|
|
84fa39918f | ||
|
|
019a080b65 | ||
|
|
53e5ae0cd4 | ||
|
|
1d9f9ab0e3 | ||
|
|
5444e077f9 | ||
|
|
ecd1573257 | ||
|
|
9419c857fd | ||
|
|
b78cf818e0 | ||
|
|
7d959c1039 | ||
|
|
ccb17f89cd | ||
|
|
c9375a44eb | ||
|
|
c88f9f4666 | ||
|
|
682bec618d |
@@ -77,6 +77,7 @@ if (BOOST_REDIS_MAIN_PROJECT)
|
||||
test
|
||||
json
|
||||
endian
|
||||
compat
|
||||
)
|
||||
|
||||
foreach(dep IN LISTS deps)
|
||||
|
||||
53
README.md
53
README.md
@@ -87,38 +87,47 @@ 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_receive` function, which can be
|
||||
`connection::async_receive2` 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
|
||||
auto
|
||||
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
// Loop while reconnection is enabled
|
||||
// Subscribe to the channel 'mychannel'. You can add any number of channels here.
|
||||
request req;
|
||||
req.subscribe({"mychannel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored
|
||||
// in resp. If the connection encounters a network error and reconnects to the server,
|
||||
// it will automatically subscribe to 'mychannel' again. This is transparent to the user.
|
||||
// You need to use specialized request::subscribe() function (instead of request::push)
|
||||
// to enable this behavior.
|
||||
|
||||
// Loop to read Redis push messages.
|
||||
while (conn->will_reconnect()) {
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Reconnect to channels.
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// Loop reading Redis pushes.
|
||||
for (error_code ec;;) {
|
||||
co_await conn->async_receive2(resp, redirect_error(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.value().clear();
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* xref:cancellation.adoc[]
|
||||
* xref:serialization.adoc[]
|
||||
* xref:logging.adoc[]
|
||||
* xref:sentinel.adoc[]
|
||||
* xref:benchmarks.adoc[]
|
||||
* xref:comparison.adoc[]
|
||||
* xref:examples.adoc[]
|
||||
|
||||
@@ -7,6 +7,146 @@
|
||||
|
||||
= Changelog
|
||||
|
||||
== Boost 1.90
|
||||
|
||||
|
||||
* (Pull request https://github.com/boostorg/redis/pull/310[310])
|
||||
Improves the per-operation support in `async_exec()`. Requests can now
|
||||
be cancelled at any point, and cancellations don't interfere with other
|
||||
requests anyhow. In previous versions, a cancellation could cause
|
||||
`async_run()` to be cancelled, making cancellations unpredictable.
|
||||
* (Issue https://github.com/boostorg/redis/issues/226[226])
|
||||
Added support for the `asio::cancel_after` and `asio::cancel_at`
|
||||
completion tokens in `async_exec()` and `async_run()`.
|
||||
* (Issue https://github.com/boostorg/redis/issues/319[319])
|
||||
Added support for per-operation cancellation in `async_run()`.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/329[329]
|
||||
and https://github.com/boostorg/redis/pull/334[334])
|
||||
The `cancel_on_connection_lost` and `cancel_if_not_connected`
|
||||
flags in `request::config` have been deprecated, and will be removed
|
||||
in subsequent releases. To limit the time span that `async_exec`
|
||||
might take, use `asio::cancel_after`, instead.
|
||||
`cancel_on_connection_lost` default has been changed to `false`.
|
||||
This shouldn't cause much impact, since `cancel_on_connection_lost`
|
||||
is not a reliable way to limit the time span that `async_exec` might take.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/321[321])
|
||||
Calling `cancel` with `operation::resolve`, `operation::connect`,
|
||||
`operation::ssl_handshake`, `operation::reconnection` and
|
||||
`operation::health_check` is now deprecated.
|
||||
These enumerators will be removed in subsequent releases.
|
||||
Users should employ `cancel(operation::run)`, instead.
|
||||
* (Issue github.com/boostorg/redis/issues/302[302] and
|
||||
pull request https://github.com/boostorg/redis/pull/303[303])
|
||||
Added support for custom setup requests using `config::setup`
|
||||
and `config::use_setup`. When setting these fields, users can
|
||||
replace the library-generated `HELLO` request by any other arbitrary request.
|
||||
This request is executed every time a new physical connection with the server
|
||||
is established. This feature can be used to interact with systems that don't
|
||||
support `HELLO`, to handle authentication and to connect to replicas.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/305[305])
|
||||
`request::config::hello_with_priority` and `request::has_hello_priority()` have
|
||||
been deprecated and will be removed in subsequent releases.
|
||||
This flag is not well specified and should only be used by the library.
|
||||
If you need to execute a request before any other, use `config::setup`, instead.
|
||||
* (Issue https://github.com/boostorg/redis/issues/296[296])
|
||||
Valkey long-term support: we guarantee Valkey compatibility
|
||||
starting with this release. Previous releases may also work,
|
||||
but have not been tested with this database system.
|
||||
* (Issue https://github.com/boostorg/redis/issues/341[341])
|
||||
Adds a `request::append()` function, to concatenate request objects.
|
||||
* (Issue https://github.com/boostorg/redis/issues/104[104])
|
||||
The health checker algorithm has been redesigned to avoid
|
||||
false positives under heavy loads. `PING` commands are now
|
||||
only issued when the connection is idle, instead of periodically.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/283[283])
|
||||
Added `config::read_buffer_append_size`, which allows to control
|
||||
the expansion of the connection's read buffer.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/311[311])
|
||||
Added `usage::bytes_rotated`, which measures data copying when
|
||||
reading and parsing data from the server.
|
||||
* (Issue https://github.com/boostorg/redis/issues/298[298])
|
||||
Added support for authenticating users with an empty password
|
||||
but a non-default username.
|
||||
* (Issue https://github.com/boostorg/redis/issues/318[318])
|
||||
Fixed a number of race conditions in the `cancel()` function
|
||||
of `connection` and `basic_connection` that could cause
|
||||
cancellations to be ignored.
|
||||
* (Issue https://github.com/boostorg/redis/issues/290[290])
|
||||
Fixed a problem that could cause an error during `HELLO`
|
||||
to make subsequent `HELLO` attempts during reconnection to fail.
|
||||
* (Issue https://github.com/boostorg/redis/issues/297[297])
|
||||
Errors during `HELLO` are now correctly logged.
|
||||
* (Issue https://github.com/boostorg/redis/issues/287[287])
|
||||
Fixed a bug causing an exception to be thrown when parsing
|
||||
a response that contains an intermediate error into a `generic_response`.
|
||||
|
||||
|
||||
== Boost 1.89
|
||||
|
||||
* (Pull request https://github.com/boostorg/redis/pull/256[256],
|
||||
https://github.com/boostorg/redis/pull/266[266] and
|
||||
https://github.com/boostorg/redis/pull/273[273])
|
||||
The following members in `connection` and `basic_connection` are now deprecated
|
||||
and will be removed in subsequent releases:
|
||||
* `next_layer()` and `next_layer_type`: there is no reason to access the underlying stream object directly.
|
||||
Connection member functions should be used, instead.
|
||||
* `get_ssl_context()`: SSL contexts should never be modified after an `asio::ssl::stream`
|
||||
object has been created from them. Properties should be set before passing the context
|
||||
to the constructor. There is no reason to access the SSL context after that.
|
||||
* `reset_stream()`: connection internals have been refactored to reset the SSL stream
|
||||
automatically when required. This function is now a no-op.
|
||||
* The `async_run()` overload taking no parameters: use the `async_run`
|
||||
overload taking a `config` object explicitly, instead.
|
||||
* (Issue https://github.com/boostorg/redis/issues/213[213])
|
||||
The logging interface has been re-written:
|
||||
* Logging can now be customized by passing a function object
|
||||
to the `logger` constructor. This allows integration with
|
||||
third-party logging libraries, like spdlog.
|
||||
This new logging interface is public and will be maintained long-term.
|
||||
* The old, unstable interface consisting of `logger::on_xxx` functions has been removed.
|
||||
* `connection` and `basic_connection` constructors now accept a `logger` object.
|
||||
This is now the preferred way to configure logging.
|
||||
The `async_run()` overload taking a `logger` object is now deprecated, and will
|
||||
be removed in subsequent releases.
|
||||
* `config::log_prefix` is now deprecated, and will
|
||||
be removed in subsequent releases. Users can achieve the same effect
|
||||
by passing a custom logging function to the `logger` constructor.
|
||||
* The default logging function, which prints to `stderr`,
|
||||
is now based on `printf` and is thus thread-safe.
|
||||
The old function used `std::cerr` and could result to interleaved
|
||||
output in multi-threaded programs.
|
||||
* The default log level is now `logger::level::info`,
|
||||
down from `logger::level::debug`. This results in less verbose output by default.
|
||||
* (Issue https://github.com/boostorg/redis/issues/272[272])
|
||||
Added support for connecting to Redis using UNIX domain sockets.
|
||||
This feature can be accessed using `config::unix_socket`.
|
||||
* (Issue https://github.com/boostorg/redis/issues/255[255])
|
||||
Fixed an issue that caused `async_run` to complete when a connection
|
||||
establishment error is encountered, even if `config::reconnect_wait_interval`
|
||||
specified reconnection.
|
||||
* (Issue https://github.com/boostorg/redis/issues/265[265])
|
||||
`connection::async_exec` now uses `ignore` as the default response,
|
||||
rather than having no default response. This matches
|
||||
`basic_connection::async_exec` behavior.
|
||||
* (Issue https://github.com/boostorg/redis/issues/260[260])
|
||||
Fixed a memory corruption affecting the logger's prefix
|
||||
configured in `config::log_prefix`.
|
||||
* (Pull request https://github.com/boostorg/redis/pull/254[254])
|
||||
Fixed some warnings regarding unused variables, name shadowing
|
||||
and narrowing conversions.
|
||||
* (Issue https://github.com/boostorg/redis/issues/252[252])
|
||||
Fixed a bug that causes reconnection to ignore the reconnection
|
||||
wait time configured in `config::reconnect_wait_interval`.
|
||||
* (Issue https://github.com/boostorg/redis/issues/238[238])
|
||||
Fixed a bug introduced in Boost 1.88 that caused `response<T>` to
|
||||
not compile for some integral types.
|
||||
* (Issue https://github.com/boostorg/redis/issues/247[247])
|
||||
The documentation has been modernized, and a more complete
|
||||
reference section has been added.
|
||||
* Part of the internals have been refactored to allow for better
|
||||
testing and reduce compile times.
|
||||
|
||||
|
||||
== Boost 1.88
|
||||
|
||||
* (Issue https://github.com/boostorg/redis/issues/233[233])
|
||||
|
||||
@@ -15,7 +15,7 @@ The examples below show how to use the features discussed throughout this docume
|
||||
* {site-url}/example/cpp20_containers.cpp[cpp20_containers.cpp]: Shows how to send and receive STL containers and how to use transactions.
|
||||
* {site-url}/example/cpp20_json.cpp[cpp20_json.cpp]: Shows how to serialize types using Boost.Json.
|
||||
* {site-url}/example/cpp20_protobuf.cpp[cpp20_protobuf.cpp]: Shows how to serialize types using protobuf.
|
||||
* {site-url}/example/cpp20_resolve_with_sentinel.cpp[cpp20_resolve_with_sentinel.cpp]: Shows how to resolve a master address using sentinels.
|
||||
* {site-url}/example/cpp20_sentinel.cpp[cpp20_sentinel.cpp]: Shows how to use the library with a Sentinel deployment.
|
||||
* {site-url}/example/cpp20_subscriber.cpp[cpp20_subscriber.cpp]: Shows how to implement pubsub with reconnection re-subscription.
|
||||
* {site-url}/example/cpp20_echo_server.cpp[cpp20_echo_server.cpp]: A simple TCP echo server.
|
||||
* {site-url}/example/cpp20_chat_room.cpp[cpp20_chat_room.cpp]: A command line chat built on Redis pubsub.
|
||||
|
||||
@@ -97,39 +97,48 @@ 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_receive`] function, which can be
|
||||
xref:reference:boost/redis/basic_connection/async_receive.adoc[`connection::async_receive2`] 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]
|
||||
----
|
||||
auto
|
||||
receiver(std::shared_ptr<connection> conn) -> net::awaitable<void>
|
||||
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
// Loop while reconnection is enabled
|
||||
// Subscribe to the channel 'mychannel'. You can add any number of channels here.
|
||||
request req;
|
||||
req.subscribe({"mychannel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored
|
||||
// in resp. If the connection encounters a network error and reconnects to the server,
|
||||
// it will automatically subscribe to 'mychannel' again. This is transparent to the user.
|
||||
// You need to use specialized request::subscribe() function (instead of request::push)
|
||||
// to enable this behavior.
|
||||
|
||||
// Loop to read Redis push messages.
|
||||
while (conn->will_reconnect()) {
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Reconnect to channels.
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// Loop reading Redis pushes.
|
||||
for (error_code ec;;) {
|
||||
co_await conn->async_receive2(resp, redirect_error(ec));
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
|
||||
// Use the response here and then clear it.
|
||||
...
|
||||
|
||||
resp.value().clear();
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -25,8 +25,12 @@ xref:reference:boost/redis/basic_connection.adoc[`basic_connection`]
|
||||
|
||||
xref:reference:boost/redis/address.adoc[`address`]
|
||||
|
||||
xref:reference:boost/redis/role.adoc[`role`]
|
||||
|
||||
xref:reference:boost/redis/config.adoc[`config`]
|
||||
|
||||
xref:reference:boost/redis/sentinel_config.adoc[`sentinel_config`]
|
||||
|
||||
xref:reference:boost/redis/error.adoc[`error`]
|
||||
|
||||
xref:reference:boost/redis/logger.adoc[`logger`]
|
||||
@@ -51,6 +55,8 @@ 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`]
|
||||
|
||||
|
||||
@@ -66,25 +72,33 @@ 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[`basic_node`]
|
||||
xref:reference:boost/redis/resp3/basic_node.adoc[`resp3::basic_node`]
|
||||
|
||||
xref:reference:boost/redis/resp3/node.adoc[`node`]
|
||||
xref:reference:boost/redis/resp3/node.adoc[`resp3::node`]
|
||||
|
||||
xref:reference:boost/redis/resp3/node_view.adoc[`node_view`]
|
||||
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/boost_redis_to_bulk-08.adoc[`boost_redis_to_bulk`]
|
||||
|
||||
xref:reference:boost/redis/resp3/type.adoc[`type`]
|
||||
xref:reference:boost/redis/resp3/type.adoc[`resp3::type`]
|
||||
|
||||
xref:reference:boost/redis/resp3/is_aggregate.adoc[`is_aggregate`]
|
||||
xref:reference:boost/redis/resp3/is_aggregate.adoc[`resp3::is_aggregate`]
|
||||
|
||||
|
||||
|
|
||||
|
||||
xref:reference:boost/redis/adapter/adapt2.adoc[`adapter::adapt2`]
|
||||
|
||||
xref:reference:boost/redis/resp3/parser.adoc[`parser`]
|
||||
xref:reference:boost/redis/resp3/parser.adoc[`resp3::parser`]
|
||||
|
||||
xref:reference:boost/redis/resp3/parse.adoc[`parse`]
|
||||
xref:reference:boost/redis/resp3/parse.adoc[`resp3::parse`]
|
||||
|
||||
|===
|
||||
@@ -184,7 +184,7 @@ must **NOT** be included in the response tuple. For example, the following reque
|
||||
----
|
||||
request req;
|
||||
req.push("PING");
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
req.subscribe({"channel"});
|
||||
req.push("QUIT");
|
||||
----
|
||||
|
||||
@@ -278,7 +278,8 @@ 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].
|
||||
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].
|
||||
The vector can be seen as a pre-order view of the response tree.
|
||||
Using it is not different than using other types:
|
||||
|
||||
@@ -292,7 +293,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`: always works.
|
||||
* `boost::redis::generic_response` and `boost::redis::generic_flat_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`.
|
||||
|
||||
152
doc/modules/ROOT/pages/sentinel.adoc
Normal file
152
doc/modules/ROOT/pages/sentinel.adoc
Normal file
@@ -0,0 +1,152 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
= Sentinel
|
||||
|
||||
Boost.Redis supports Redis Sentinel deployments. Sentinel handling
|
||||
in `connection` is built-in: xref:reference:boost/redis/basic_connection/async_run-04.adoc[`async_run`]
|
||||
automatically connects to Sentinels, resolves the master's address, and connects to the master.
|
||||
|
||||
Configuration is done using xref:reference:boost/redis/sentinel_config.adoc[`config::sentinel`]:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
config cfg;
|
||||
|
||||
// To enable Sentinel, set this field to a non-empty list
|
||||
// of (hostname, port) pairs where Sentinels are listening
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
|
||||
// Set master_name to the identifier that you configured
|
||||
// in the "sentinel monitor" statement of your sentinel.conf file
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
----
|
||||
|
||||
Once set, the connection object can be used normally. See our
|
||||
our {site-url}/example/cpp20_sentinel.cpp[Sentinel example]
|
||||
for a full program.
|
||||
|
||||
== Connecting to replicas
|
||||
|
||||
By default, the library connects to the Redis master.
|
||||
You can connect to one of its replicas by using
|
||||
xref:reference:boost/redis/sentinel_config/server_role.adoc[`config::sentinel::server_role`].
|
||||
This can be used to balance load, if all your commands read data from
|
||||
the server and never write to it. The particular replica will be chosen randomly.
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
config cfg;
|
||||
|
||||
// Set up Sentinel
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Ask the library to connect to a random replica of 'mymaster', rather than the master node
|
||||
cfg.sentinel.server_role = role::replica;
|
||||
----
|
||||
|
||||
|
||||
== Sentinel authentication
|
||||
|
||||
If your Sentinels require authentication,
|
||||
you can use xref:reference:boost/redis/sentinel_config/setup.adoc[`config::sentinel::setup`]
|
||||
to provide credentials.
|
||||
This request is executed immediately after connecting to Sentinels, and
|
||||
before any other command:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Set up Sentinel
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// By default, setup contains a 'HELLO 3' command.
|
||||
// Override it to add an AUTH clause to it with out credentials.
|
||||
cfg.sentinel.setup.clear();
|
||||
cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_password");
|
||||
|
||||
// cfg.sentinel.setup applies to Sentinels, only.
|
||||
// Use cfg.setup to authenticate to masters/replicas.
|
||||
cfg.use_setup = true; // Required for cfg.setup to be used, for historic reasons
|
||||
cfg.setup.clear();
|
||||
cfg.setup.push("HELLO", 3, "AUTH", "master_user", "master_password");
|
||||
----
|
||||
|
||||
== Using TLS with Sentinels
|
||||
|
||||
You might use TLS with Sentinels only, masters/replicas only, or both by adjusting
|
||||
xref:reference:boost/redis/sentinel_config/use_ssl.adoc[`config::sentinel::use_ssl`]
|
||||
and xref:reference:boost/redis/config/use_ssl.adoc[`config::use_ssl`]:
|
||||
|
||||
[source,cpp]
|
||||
----
|
||||
// Set up Sentinel
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"sentinel1.example.com", "26379"},
|
||||
{"sentinel2.example.com", "26379"},
|
||||
{"sentinel3.example.com", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Adjust these switches to enable/disable TLS
|
||||
cfg.use_ssl = true; // Applies to masters and replicas
|
||||
cfg.sentinel.use_ssl = true; // Applies to Sentinels
|
||||
----
|
||||
|
||||
== Sentinel algorithm
|
||||
|
||||
This section details how `async_run` interacts with Sentinel.
|
||||
Most of the algorithm follows
|
||||
https://redis.io/docs/latest/develop/reference/sentinel-clients/[the official Sentinel client guidelines].
|
||||
Some of these details may vary between library versions.
|
||||
|
||||
* Connections maintain an internal list of Sentinels, bootstrapped from
|
||||
xref:reference:boost/redis/sentinel_config/addresses.adoc[`config::sentinel::addresses`].
|
||||
* The first Sentinel in the list is contacted by performing the following:
|
||||
** A physical connection is established.
|
||||
** The setup request is executed.
|
||||
** The master's address is resolved using
|
||||
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL GET-MASTER-NAME-BY-ADDR`].
|
||||
** If `config::sentinel::server_role` is `role::replica`, replica addresses are obtained using
|
||||
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL REPLICAS`].
|
||||
One replica is chosen randomly.
|
||||
** The address of other Sentinels also monitoring this master are retrieved using
|
||||
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL SENTINELS`].
|
||||
* If a Sentinel is unreachable, doesn't know about the configured master,
|
||||
or returns an error while executing the above requests, the next Sentinel in the list is tried.
|
||||
* If all Sentinels have been tried without success, `config::reconnect_wait_interval`
|
||||
is waited, and the process starts again.
|
||||
* After a successful Sentinel response, the internal Sentinel list is updated
|
||||
with any newly discovered Sentinels.
|
||||
Sentinels in `config::sentinel::addresses` are always kept in the list,
|
||||
even if they weren't present in the output of `SENTINEL SENTINELS`.
|
||||
* The retrieved address is used
|
||||
to establish a connection with the master or replica.
|
||||
A `ROLE` command is added at the end of the setup request.
|
||||
This is used to detect situations where a Sentinel returns outdated
|
||||
information due to a failover in process. If `ROLE` doesn't output
|
||||
the expected role (`"master"` or `"slave"`, depending on `config::sentinel::server_role`)
|
||||
`config::reconnect_wait_interval` is waited and Sentinel is contacted again.
|
||||
* The connection to the master/replica is run like any other connection.
|
||||
If network errors or timeouts happen, `config::reconnect_wait_interval`
|
||||
is waited and Sentinel is contacted again.
|
||||
@@ -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.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -6,14 +6,15 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/as_tuple.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/posix/stream_descriptor.hpp>
|
||||
#include <boost/asio/read_until.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -29,10 +30,9 @@ using boost::asio::co_spawn;
|
||||
using boost::asio::consign;
|
||||
using boost::asio::detached;
|
||||
using boost::asio::dynamic_buffer;
|
||||
using boost::asio::redirect_error;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::generic_flat_response;
|
||||
using boost::redis::request;
|
||||
using boost::system::error_code;
|
||||
using namespace std::chrono_literals;
|
||||
@@ -40,27 +40,44 @@ using namespace std::chrono_literals;
|
||||
// Chat over Redis pubsub. To test, run this program from multiple
|
||||
// terminals and type messages to stdin.
|
||||
|
||||
namespace {
|
||||
|
||||
auto rethrow_on_error = [](std::exception_ptr exc) {
|
||||
if (exc)
|
||||
std::rethrow_exception(exc);
|
||||
};
|
||||
|
||||
auto receiver(std::shared_ptr<connection> conn) -> awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
// Set the receive response, so pushes are stored in resp
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
while (conn->will_reconnect()) {
|
||||
// Subscribe to channels.
|
||||
co_await conn->async_exec(req);
|
||||
// Subscribe to the channel 'channel'. Using request::subscribe()
|
||||
// (instead of request::push()) makes the connection re-subscribe
|
||||
// to 'channel' whenever it re-connects to the server.
|
||||
request req;
|
||||
req.subscribe({"channel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// Loop reading Redis push messages.
|
||||
for (error_code ec;;) {
|
||||
co_await conn->async_receive2(redirect_error(ec));
|
||||
if (ec)
|
||||
break; // Connection lost, break so we can reconnect to channels.
|
||||
std::cout << resp.value().at(1).value << " " << resp.value().at(2).value << " "
|
||||
<< resp.value().at(3).value << std::endl;
|
||||
resp.value().clear();
|
||||
while (conn->will_reconnect()) {
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +94,8 @@ auto publisher(std::shared_ptr<stream_descriptor> in, std::shared_ptr<connection
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
auto co_main(config cfg) -> awaitable<void>
|
||||
{
|
||||
@@ -84,8 +103,8 @@ auto co_main(config cfg) -> awaitable<void>
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
auto stream = std::make_shared<stream_descriptor>(ex, ::dup(STDIN_FILENO));
|
||||
|
||||
co_spawn(ex, receiver(conn), detached);
|
||||
co_spawn(ex, publisher(stream, conn), detached);
|
||||
co_spawn(ex, receiver(conn), rethrow_on_error);
|
||||
co_spawn(ex, publisher(stream, conn), rethrow_on_error);
|
||||
conn->async_run(cfg, consign(detached, conn));
|
||||
|
||||
signal_set sig_set{ex, SIGINT, SIGTERM};
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace asio = boost::asio;
|
||||
using endpoints = asio::ip::tcp::resolver::results_type;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::config;
|
||||
using boost::redis::address;
|
||||
using boost::redis::connection;
|
||||
|
||||
auto redir(boost::system::error_code& ec) { return asio::redirect_error(asio::use_awaitable, ec); }
|
||||
|
||||
// For more info see
|
||||
// - https://redis.io/docs/manual/sentinel.
|
||||
// - https://redis.io/docs/reference/sentinel-clients.
|
||||
auto resolve_master_address(std::vector<address> const& addresses) -> asio::awaitable<address>
|
||||
{
|
||||
request req;
|
||||
req.push("SENTINEL", "get-master-addr-by-name", "mymaster");
|
||||
req.push("QUIT");
|
||||
|
||||
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
|
||||
|
||||
response<std::optional<std::array<std::string, 2>>, ignore_t> resp;
|
||||
for (auto addr : addresses) {
|
||||
boost::system::error_code ec;
|
||||
config cfg;
|
||||
cfg.addr = addr;
|
||||
// TODO: async_run and async_exec should be lauched in
|
||||
// parallel here so we can wait for async_run completion
|
||||
// before eventually calling it again.
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
co_await conn->async_exec(req, resp, redir(ec));
|
||||
conn->cancel();
|
||||
if (!ec && std::get<0>(resp))
|
||||
co_return address{
|
||||
std::get<0>(resp).value().value().at(0),
|
||||
std::get<0>(resp).value().value().at(1)};
|
||||
}
|
||||
|
||||
co_return address{};
|
||||
}
|
||||
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
// A list of sentinel addresses from which only one is responsive.
|
||||
// This simulates sentinels that are down.
|
||||
std::vector<address> const addresses{
|
||||
address{"foo", "26379"},
|
||||
address{"bar", "26379"},
|
||||
cfg.addr
|
||||
};
|
||||
|
||||
auto const ep = co_await resolve_master_address(addresses);
|
||||
|
||||
std::clog << "Host: " << ep.host << "\n"
|
||||
<< "Port: " << ep.port << "\n"
|
||||
<< std::flush;
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
60
example/cpp20_sentinel.cpp
Normal file
60
example/cpp20_sentinel.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
|
||||
namespace asio = boost::asio;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::config;
|
||||
using boost::redis::connection;
|
||||
|
||||
// Called from the main function (see main.cpp)
|
||||
auto co_main(config cfg) -> asio::awaitable<void>
|
||||
{
|
||||
// Boost.Redis has built-in support for Sentinel deployments.
|
||||
// To enable it, set the fields in config shown here.
|
||||
// sentinel.addresses should contain a list of (hostname, port) pairs
|
||||
// where Sentinels are listening. IPs can also be used.
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
|
||||
// Set master_name to the identifier that you configured
|
||||
// in the "sentinel monitor" statement of your sentinel.conf file
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// async_run will contact the Sentinels, obtain the master address,
|
||||
// connect to it and keep the connection healthy. If a failover happens,
|
||||
// the address will be resolved again and the new elected master will be contacted.
|
||||
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
|
||||
conn->async_run(cfg, asio::consign(asio::detached, conn));
|
||||
|
||||
// You can now use the connection normally, as you would use a connection to a single master.
|
||||
request req;
|
||||
req.push("PING", "Hello world");
|
||||
response<std::string> resp;
|
||||
|
||||
// Execute the request.
|
||||
co_await conn->async_exec(req, resp);
|
||||
conn->cancel();
|
||||
|
||||
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
|
||||
}
|
||||
|
||||
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)
|
||||
@@ -6,13 +6,12 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
|
||||
#include <boost/asio/as_tuple.hpp>
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
#include <boost/asio/co_spawn.hpp>
|
||||
#include <boost/asio/consign.hpp>
|
||||
#include <boost/asio/detached.hpp>
|
||||
#include <boost/asio/redirect_error.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
#include <boost/asio/use_awaitable.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
@@ -21,7 +20,7 @@
|
||||
namespace asio = boost::asio;
|
||||
using namespace std::chrono_literals;
|
||||
using boost::redis::request;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::generic_flat_response;
|
||||
using boost::redis::config;
|
||||
using boost::system::error_code;
|
||||
using boost::redis::connection;
|
||||
@@ -32,7 +31,7 @@ using asio::signal_set;
|
||||
* To test send messages with redis-cli
|
||||
*
|
||||
* $ redis-cli -3
|
||||
* 127.0.0.1:6379> PUBLISH channel some-message
|
||||
* 127.0.0.1:6379> PUBLISH mychannel some-message
|
||||
* (integer) 3
|
||||
* 127.0.0.1:6379>
|
||||
*
|
||||
@@ -46,33 +45,39 @@ using asio::signal_set;
|
||||
// Receives server pushes.
|
||||
auto receiver(std::shared_ptr<connection> conn) -> asio::awaitable<void>
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
|
||||
generic_response resp;
|
||||
generic_flat_response resp;
|
||||
conn->set_receive_response(resp);
|
||||
|
||||
// Loop while reconnection is enabled
|
||||
// Subscribe to the channel 'mychannel'. You can add any number of channels here.
|
||||
request req;
|
||||
req.subscribe({"mychannel"});
|
||||
co_await conn->async_exec(req);
|
||||
|
||||
// You're now subscribed to 'mychannel'. Pushes sent over this channel will be stored
|
||||
// in resp. If the connection encounters a network error and reconnects to the server,
|
||||
// it will automatically subscribe to 'mychannel' again. This is transparent to the user.
|
||||
// You need to use specialized request::subscribe() function (instead of request::push)
|
||||
// to enable this behavior.
|
||||
|
||||
// Loop to read Redis push messages.
|
||||
while (conn->will_reconnect()) {
|
||||
// Reconnect to the channels.
|
||||
co_await conn->async_exec(req);
|
||||
// Wait for pushes
|
||||
auto [ec] = co_await conn->async_receive2(asio::as_tuple);
|
||||
|
||||
// Loop to read Redis push messages.
|
||||
for (error_code ec;;) {
|
||||
// Wait for pushes
|
||||
co_await conn->async_receive2(asio::redirect_error(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.data << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
// Check for errors and cancellations
|
||||
if (ec) {
|
||||
std::cerr << "Error during receive: " << ec << std::endl;
|
||||
break;
|
||||
}
|
||||
|
||||
// The response must be consumed without suspending the
|
||||
// coroutine i.e. without the use of async operations.
|
||||
for (auto const& elem : resp.value())
|
||||
std::cout << elem.value << "\n";
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
resp.value().clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace boost::redis {
|
||||
@@ -50,20 +48,7 @@ public:
|
||||
using impl_t = std::function<void(parse_event, resp3::node_view const&, system::error_code&)>;
|
||||
|
||||
template <class T>
|
||||
static auto create_impl(T& resp) -> impl_t
|
||||
{
|
||||
using namespace boost::redis::adapter;
|
||||
return [adapter2 = boost_redis_adapt(resp)](
|
||||
any_adapter::parse_event ev,
|
||||
resp3::node_view const& nd,
|
||||
system::error_code& ec) mutable {
|
||||
switch (ev) {
|
||||
case parse_event::init: adapter2.on_init(); break;
|
||||
case parse_event::node: adapter2.on_node(nd, ec); break;
|
||||
case parse_event::done: adapter2.on_done(); break;
|
||||
}
|
||||
};
|
||||
}
|
||||
static auto create_impl(T& resp) -> impl_t;
|
||||
|
||||
/// Contructs from a type erased adaper
|
||||
any_adapter(impl_t fn = [](parse_event, resp3::node_view const&, system::error_code&) { })
|
||||
@@ -109,6 +94,32 @@ private:
|
||||
impl_t impl_;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
|
||||
template <class Adapter>
|
||||
any_adapter::impl_t make_any_adapter_impl(Adapter&& value)
|
||||
{
|
||||
return [adapter = std::move(value)](
|
||||
any_adapter::parse_event ev,
|
||||
resp3::node_view const& nd,
|
||||
system::error_code& ec) mutable {
|
||||
switch (ev) {
|
||||
case any_adapter::parse_event::init: adapter.on_init(); break;
|
||||
case any_adapter::parse_event::node: adapter.on_node(nd, ec); break;
|
||||
case any_adapter::parse_event::done: adapter.on_done(); break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
template <class T>
|
||||
auto boost::redis::any_adapter::create_impl(T& resp) -> impl_t
|
||||
{
|
||||
using adapter::boost_redis_adapt;
|
||||
return detail::make_any_adapter_impl(boost_redis_adapt(resp));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -9,9 +9,11 @@
|
||||
|
||||
#include <boost/redis/adapter/result.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/flat_tree.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
@@ -176,6 +178,99 @@ 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()
|
||||
{
|
||||
if (tree_->has_value()) {
|
||||
tree_->value().notify_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() { tree_->notify_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:
|
||||
|
||||
@@ -92,8 +92,32 @@ struct response_traits<result<ignore_t>> {
|
||||
};
|
||||
|
||||
template <class String, class Allocator>
|
||||
struct response_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
|
||||
using response_type = result<std::vector<resp3::basic_node<String>, 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;
|
||||
using adapter_type = general_aggregate<response_type>;
|
||||
|
||||
static auto adapt(response_type& v) noexcept { return adapter_type{&v}; }
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#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>
|
||||
|
||||
@@ -56,12 +58,33 @@ struct result_traits<result<resp3::basic_node<T>>> {
|
||||
};
|
||||
|
||||
template <class String, class Allocator>
|
||||
struct result_traits<result<std::vector<resp3::basic_node<String>, Allocator>>> {
|
||||
struct result_traits<result<resp3::basic_tree<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;
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
@@ -24,12 +25,130 @@ struct address {
|
||||
std::string port = "6379";
|
||||
};
|
||||
|
||||
/// Configure parameters used by the connection classes.
|
||||
struct config {
|
||||
/// Uses SSL instead of a plain connection.
|
||||
/** @brief Compares two addresses for equality.
|
||||
* @relates address
|
||||
*
|
||||
* @param a Left hand side address.
|
||||
* @param b Right hand side address.
|
||||
*/
|
||||
inline bool operator==(address const& a, address const& b)
|
||||
{
|
||||
return a.host == b.host && a.port == b.port;
|
||||
}
|
||||
|
||||
/** @brief Compares two addresses for inequality.
|
||||
* @relates address
|
||||
*
|
||||
* @param a Left hand side address.
|
||||
* @param b Right hand side address.
|
||||
*/
|
||||
inline bool operator!=(address const& a, address const& b) { return !(a == b); }
|
||||
|
||||
/// Identifies the possible roles of a Redis server.
|
||||
enum class role
|
||||
{
|
||||
/// The server is a master.
|
||||
master,
|
||||
|
||||
/// The server is a replica.
|
||||
replica,
|
||||
};
|
||||
|
||||
/// Configuration values to use when using Sentinel.
|
||||
struct sentinel_config {
|
||||
/**
|
||||
* @brief A list of (hostname, port) pairs where the Sentinels are listening.
|
||||
*
|
||||
* Sentinels in this list will be contacted in order, until a successful
|
||||
* connection is made. At this point, the `SENTINEL SENTINELS` command
|
||||
* will be used to retrieve any additional Sentinels monitoring the configured master.
|
||||
* Thus, it is not required to keep this list comprehensive - if Sentinels are added
|
||||
* later, they will be detected at runtime.
|
||||
*
|
||||
* Sentinel will only be used if this value is not empty.
|
||||
*
|
||||
* Numeric IP addresses are also allowed as hostnames.
|
||||
*/
|
||||
std::vector<address> addresses{};
|
||||
|
||||
/**
|
||||
* @brief The name of the master to connect to, as configured in the
|
||||
* `sentinel monitor` statement in `sentinel.conf`.
|
||||
*
|
||||
* This field is required even when connecting to replicas.
|
||||
*/
|
||||
std::string master_name{};
|
||||
|
||||
/**
|
||||
* @brief Whether connections to Sentinels should use TLS or not.
|
||||
* Does not affect connections to masters.
|
||||
*
|
||||
* When set to `true`, physical connections to Sentinels will be established
|
||||
* using TLS. This setting does *not* influence how masters and replicas are contacted.
|
||||
* To use TLS when connecting to these, set @ref config::use_ssl to `true`.
|
||||
*/
|
||||
bool use_ssl = false;
|
||||
|
||||
/// For TCP connections, hostname and port of the Redis server.
|
||||
/**
|
||||
* @brief A request to be sent to Sentinels upon connection establishment.
|
||||
*
|
||||
* This request is executed every time a Sentinel is contacted, and before
|
||||
* commands like `SENTINEL GET-MASTER-NAME-BY-ADDR` are run.
|
||||
* By default, this field contains a `HELLO 3` command.
|
||||
* You can use this request to set up any authorization required by Sentinels.
|
||||
*
|
||||
* This request should ensure that the connection is upgraded to RESP3
|
||||
* by executing `HELLO 3` or similar. RESP2 is not supported yet.
|
||||
*/
|
||||
request setup = detail::make_hello_request();
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel resolve operation is allowed to elapse.
|
||||
* Does not affect connections to masters and replicas, controlled by @ref config::resolve_timeout.
|
||||
*/
|
||||
std::chrono::steady_clock::duration resolve_timeout = std::chrono::milliseconds{500};
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel connect operation is allowed to elapse.
|
||||
* Does not affect connections to masters and replicas, controlled by @ref config::connect_timeout.
|
||||
*/
|
||||
std::chrono::steady_clock::duration connect_timeout = std::chrono::milliseconds{500};
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel TLS handshake operation is allowed to elapse.
|
||||
* Does not affect connections to masters and replicas, controlled by @ref config::ssl_handshake_timeout.
|
||||
*/
|
||||
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{5};
|
||||
|
||||
/**
|
||||
* @brief Time span that the Sentinel request/response exchange is allowed to elapse.
|
||||
* Includes executing the commands in @ref setup and the commands required to
|
||||
* resolve the server's address.
|
||||
*/
|
||||
std::chrono::steady_clock::duration request_timeout = std::chrono::seconds{5};
|
||||
|
||||
/**
|
||||
* @brief Whether to connect to a Redis master or to a replica.
|
||||
*
|
||||
* The library resolves and connects to the Redis master, by default.
|
||||
* Set this value to @ref role::replica to connect to one of the replicas
|
||||
* of the master identified by @ref master_name.
|
||||
* The particular replica will be chosen randomly.
|
||||
*/
|
||||
role server_role = role::master;
|
||||
};
|
||||
|
||||
/// Configure parameters used by the connection classes.
|
||||
struct config {
|
||||
/**
|
||||
* @brief Whether to use TLS instead of plaintext connections.
|
||||
*
|
||||
* When using Sentinel, configures whether to use TLS when connecting to masters and replicas.
|
||||
* Use @ref sentinel_config::use_ssl to control TLS for Sentinels.
|
||||
*/
|
||||
bool use_ssl = false;
|
||||
|
||||
/// For TCP connections, hostname and port of the Redis server. Ignored when using Sentinel.
|
||||
address addr = address{"127.0.0.1", "6379"};
|
||||
|
||||
/**
|
||||
@@ -37,8 +156,11 @@ struct config {
|
||||
*
|
||||
* If non-empty, communication with the server will happen using
|
||||
* UNIX domain sockets, and @ref addr will be ignored.
|
||||
*
|
||||
* UNIX domain sockets can't be used with SSL: if `unix_socket` is non-empty,
|
||||
* @ref use_ssl must be `false`.
|
||||
* @ref use_ssl must be `false`. UNIX domain sockets can't be used with Sentinel, either.
|
||||
*
|
||||
* UNIX domain sockets can't be used with Sentinel.
|
||||
*/
|
||||
std::string unix_socket;
|
||||
|
||||
@@ -51,6 +173,9 @@ struct config {
|
||||
* If the username equals the literal `"default"` (the default)
|
||||
* and no password is specified, the `HELLO` command is sent
|
||||
* without authentication parameters.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
|
||||
*/
|
||||
std::string username = "default";
|
||||
|
||||
@@ -63,6 +188,9 @@ struct config {
|
||||
* If the username equals the literal `"default"` (the default)
|
||||
* and no password is specified, the `HELLO` command is sent
|
||||
* without authentication parameters.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup to configure authorization for Sentinels.
|
||||
*/
|
||||
std::string password;
|
||||
|
||||
@@ -71,6 +199,9 @@ struct config {
|
||||
* If @ref use_setup is false (the default), during connection establishment,
|
||||
* a `HELLO` command is sent. If this field is not empty, the `HELLO` command
|
||||
* will contain a `SETNAME` subcommand containing this value.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup to configure this value for Sentinels.
|
||||
*/
|
||||
std::string clientname = "Boost.Redis";
|
||||
|
||||
@@ -80,6 +211,8 @@ struct config {
|
||||
* non-empty optional, and its value is different than zero,
|
||||
* a `SELECT` command will be issued during connection establishment to set the logical
|
||||
* database index. By default, no `SELECT` command is sent.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
std::optional<int> database_index = 0;
|
||||
|
||||
@@ -95,13 +228,22 @@ struct config {
|
||||
*/
|
||||
std::string log_prefix = "(Boost.Redis) ";
|
||||
|
||||
/// Time span that the resolve operation is allowed to elapse.
|
||||
/**
|
||||
* @brief Time span that the resolve operation is allowed to elapse.
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
std::chrono::steady_clock::duration resolve_timeout = std::chrono::seconds{10};
|
||||
|
||||
/// Time span that the connect operation is allowed to elapse.
|
||||
/**
|
||||
* @brief Time span that the connect operation is allowed to elapse.
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
std::chrono::steady_clock::duration connect_timeout = std::chrono::seconds{10};
|
||||
|
||||
/// Time span that the SSL handshake operation is allowed to elapse.
|
||||
/**
|
||||
* @brief Time span that the SSL handshake operation is allowed to elapse.
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
*/
|
||||
std::chrono::steady_clock::duration ssl_handshake_timeout = std::chrono::seconds{10};
|
||||
|
||||
/** @brief Time span between successive health checks.
|
||||
@@ -123,18 +265,28 @@ 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.
|
||||
* Set to zero to disable reconnection.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
|
||||
* If none of the configured Sentinels can be contacted, this time span will
|
||||
* be waited before trying again. After a connection error with a master or replica
|
||||
* is encountered, this time span will be waited before contacting Sentinels again.
|
||||
*/
|
||||
std::chrono::steady_clock::duration reconnect_wait_interval = std::chrono::seconds{1};
|
||||
|
||||
/** @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.
|
||||
* Sets a limit on how much data is allowed to be read into the
|
||||
* read buffer. It can be used to prevent DDOS.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
|
||||
*/
|
||||
std::size_t max_read_size = (std::numeric_limits<std::size_t>::max)();
|
||||
|
||||
@@ -144,6 +296,8 @@ struct config {
|
||||
* needed. This can help avoiding some memory allocations. Once the
|
||||
* maximum size is reached no more memory allocations are made
|
||||
* since the buffer is reused.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters, replicas and Sentinels.
|
||||
*/
|
||||
std::size_t read_buffer_append_size = 4096;
|
||||
|
||||
@@ -168,6 +322,9 @@ struct config {
|
||||
* systems that don't support `HELLO`.
|
||||
*
|
||||
* By default, this field is false, and @ref setup will not be used.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup for Sentinels.
|
||||
*/
|
||||
bool use_setup = false;
|
||||
|
||||
@@ -177,8 +334,17 @@ struct config {
|
||||
* @ref use_setup docs for more info.
|
||||
*
|
||||
* By default, `setup` contains a `"HELLO 3"` command.
|
||||
*
|
||||
* When using Sentinel, this setting applies to masters and replicas.
|
||||
* Use @ref sentinel_config::setup for Sentinels.
|
||||
*/
|
||||
request setup = detail::make_hello_request();
|
||||
|
||||
/**
|
||||
* @brief Configuration values for Sentinel. Sentinel is enabled only if
|
||||
* @ref sentinel_config::addresses is not empty.
|
||||
*/
|
||||
sentinel_config sentinel{};
|
||||
};
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
#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/receive_fsm.hpp>
|
||||
#include <boost/redis/detail/redis_stream.hpp>
|
||||
#include <boost/redis/detail/run_fsm.hpp>
|
||||
#include <boost/redis/detail/sentinel_resolve_fsm.hpp>
|
||||
#include <boost/redis/detail/writer_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
@@ -32,9 +35,11 @@
|
||||
#include <boost/asio/bind_cancellation_slot.hpp>
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <boost/asio/buffer.hpp>
|
||||
#include <boost/asio/cancel_after.hpp>
|
||||
#include <boost/asio/cancel_at.hpp>
|
||||
#include <boost/asio/cancellation_signal.hpp>
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/compose.hpp>
|
||||
#include <boost/asio/deferred.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/experimental/cancellation_condition.hpp>
|
||||
@@ -45,6 +50,7 @@
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/asio/write.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/config.hpp>
|
||||
|
||||
@@ -103,7 +109,10 @@ struct connection_impl {
|
||||
{
|
||||
while (true) {
|
||||
// Invoke the state machine
|
||||
auto act = fsm_.resume(obj_->is_open(), self.get_cancellation_state().cancelled());
|
||||
auto act = fsm_.resume(
|
||||
obj_->is_open(),
|
||||
obj_->st_,
|
||||
self.get_cancellation_state().cancelled());
|
||||
|
||||
// Do what the FSM said
|
||||
switch (act.type()) {
|
||||
@@ -146,7 +155,7 @@ struct connection_impl {
|
||||
{
|
||||
switch (op) {
|
||||
case operation::exec: st_.mpx.cancel_waiting(); break;
|
||||
case operation::receive: receive_channel_.cancel(); break;
|
||||
case operation::receive: cancel_receive_v2(); break;
|
||||
case operation::reconnection:
|
||||
st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero();
|
||||
break;
|
||||
@@ -157,7 +166,7 @@ struct connection_impl {
|
||||
case operation::health_check: cancel_run(); break;
|
||||
case operation::all:
|
||||
st_.mpx.cancel_waiting(); // exec
|
||||
receive_channel_.cancel(); // receive
|
||||
cancel_receive_v2(); // receive
|
||||
st_.cfg.reconnect_wait_interval = std::chrono::seconds::zero(); // reconnect
|
||||
cancel_run(); // run
|
||||
break;
|
||||
@@ -165,6 +174,14 @@ struct connection_impl {
|
||||
}
|
||||
}
|
||||
|
||||
void cancel_receive_v1() { receive_channel_.cancel(); }
|
||||
|
||||
void cancel_receive_v2()
|
||||
{
|
||||
st_.receive2_cancelled = true;
|
||||
cancel_receive_v1();
|
||||
}
|
||||
|
||||
void cancel_run()
|
||||
{
|
||||
// Individual operations should see a terminal cancellation, regardless
|
||||
@@ -176,8 +193,8 @@ struct connection_impl {
|
||||
stream_.cancel_resolve();
|
||||
|
||||
// Receive is technically not part of run, but we also cancel it for
|
||||
// backwards compatibility.
|
||||
receive_channel_.cancel();
|
||||
// backwards compatibility. Note that this intentionally affects v1 receive, only.
|
||||
cancel_receive_v1();
|
||||
}
|
||||
|
||||
bool is_open() const noexcept { return stream_.is_open(); }
|
||||
@@ -198,7 +215,7 @@ struct connection_impl {
|
||||
});
|
||||
|
||||
return asio::async_compose<CompletionToken, void(system::error_code, std::size_t)>(
|
||||
exec_op{this, notifier, exec_fsm(st_.mpx, std::move(info))},
|
||||
exec_op{this, notifier, exec_fsm(std::move(info))},
|
||||
token,
|
||||
writer_cv_);
|
||||
}
|
||||
@@ -207,8 +224,161 @@ 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 Executor>
|
||||
struct receive2_op {
|
||||
connection_impl<Executor>* conn_;
|
||||
receive_fsm fsm_{};
|
||||
|
||||
void drain_receive_channel()
|
||||
{
|
||||
// We don't expect any errors here. The only errors
|
||||
// that might appear in the channel are due to cancellations,
|
||||
// and these don't make sense with try_receive
|
||||
auto f = [](system::error_code, std::size_t) { };
|
||||
while (conn_->receive_channel_.try_receive(f))
|
||||
;
|
||||
}
|
||||
|
||||
template <class Self>
|
||||
void operator()(Self& self, system::error_code ec = {}, std::size_t /* push_bytes */ = 0u)
|
||||
{
|
||||
receive_action act = fsm_.resume(conn_->st_, ec, self.get_cancellation_state().cancelled());
|
||||
|
||||
switch (act.type) {
|
||||
case receive_action::action_type::setup_cancellation:
|
||||
self.reset_cancellation_state(asio::enable_total_cancellation());
|
||||
(*this)(self); // this action does not require yielding
|
||||
return;
|
||||
case receive_action::action_type::wait:
|
||||
conn_->receive_channel_.async_receive(std::move(self));
|
||||
return;
|
||||
case receive_action::action_type::drain_channel:
|
||||
drain_receive_channel();
|
||||
(*this)(self); // this action does not require yielding
|
||||
return;
|
||||
case receive_action::action_type::immediate:
|
||||
asio::async_immediate(self.get_io_executor(), std::move(self));
|
||||
return;
|
||||
case receive_action::action_type::done: self.complete(act.ec); return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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_;
|
||||
@@ -335,8 +505,14 @@ public:
|
||||
case run_action_type::immediate:
|
||||
asio::async_immediate(self.get_io_executor(), std::move(self));
|
||||
return;
|
||||
case run_action_type::sentinel_resolve:
|
||||
async_sentinel_resolve(*conn_, std::move(self));
|
||||
return;
|
||||
case run_action_type::connect:
|
||||
conn_->stream_.async_connect(conn_->st_.cfg, conn_->st_.logger, std::move(self));
|
||||
conn_->stream_.async_connect(
|
||||
make_run_connect_params(conn_->st_),
|
||||
conn_->st_.logger,
|
||||
std::move(self));
|
||||
return;
|
||||
case run_action_type::parallel_group:
|
||||
asio::experimental::make_parallel_group(
|
||||
@@ -487,6 +663,8 @@ public:
|
||||
* This function establishes a connection to the Redis server and keeps
|
||||
* it healthy by performing the following operations:
|
||||
*
|
||||
* @li For Sentinel deployments (`config::sentinel::addresses` is not empty),
|
||||
* contacts Sentinels to obtain the address of the configured master.
|
||||
* @li For TCP connections, resolves the server hostname passed in
|
||||
* @ref boost::redis::config::addr.
|
||||
* @li Establishes a physical connection to the server. For TCP connections,
|
||||
@@ -629,27 +807,68 @@ public:
|
||||
return impl_->receive_channel_.async_receive(std::forward<CompletionToken>(token));
|
||||
}
|
||||
|
||||
/** @brief Wait for server pushes asynchronously
|
||||
/** @brief Wait for server pushes asynchronously.
|
||||
*
|
||||
* This function suspends until a server push is received by the
|
||||
* This function suspends until at least one 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.
|
||||
* set_receive_response. Use the functions in the response object
|
||||
* to know how many messages they were received and consume them.
|
||||
*
|
||||
* 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 ongoing `async_exec`s and
|
||||
* health-checks won't make any progress and the connection will
|
||||
* eventually timeout. To avoid that Apps should call
|
||||
* `async_receive2` continuously in a loop.
|
||||
* 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 this, apps that expect server pushes
|
||||
* should call this function continuously in a loop.
|
||||
*
|
||||
* @Note To avoid deadlocks the task (e.g. coroutine) calling
|
||||
* This function should be used instead of the deprecated @ref async_receive.
|
||||
* It differs from `async_receive` in the following:
|
||||
*
|
||||
* @li `async_receive` is designed to consume a single push message at a time.
|
||||
* This can be inefficient when receiving lots of server pushes.
|
||||
* `async_receive2` is batch-oriented. All pushes that are available
|
||||
* when `async_receive2` is called will be marked as consumed.
|
||||
* @li `async_receive` is cancelled when a reconnection happens (e.g. because
|
||||
* of a network error). This enabled the user to re-establish subscriptions
|
||||
* using @ref async_exec before waiting for pushes again. With the introduction of
|
||||
* functions like @ref request::subscribe, subscriptions are automatically
|
||||
* re-established on reconnection. Thus, `async_receive2` is not cancelled
|
||||
* on reconnection.
|
||||
* @li `async_receive` passes the number of bytes that each received
|
||||
* push message contains. This information is unreliable and not very useful.
|
||||
* Equivalent information is available using functions in the response object.
|
||||
* @li `async_receive` might get cancelled if `async_run` is cancelled.
|
||||
* This doesn't happen with `async_receive2`.
|
||||
*
|
||||
* This function does *not* remove messages from the response object
|
||||
* passed to @ref set_receive_response - use the functions in the response
|
||||
* object to achieve this.
|
||||
*
|
||||
* Only a single instance of `async_receive2` may be outstanding
|
||||
* for a given connection at any time. Trying to start a second one
|
||||
* will fail with @ref error::already_running.
|
||||
*
|
||||
* @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.
|
||||
* they could block each other. This is, avoid the following pattern:
|
||||
*
|
||||
* For an example see cpp20_subscriber.cpp. The completion token
|
||||
* must have the following signature
|
||||
* @code
|
||||
* asio::awaitable<void> receiver()
|
||||
* {
|
||||
* // Do NOT do this!!! The receive buffer might get full while
|
||||
* // async_exec runs, which will block all read operations until async_receive2
|
||||
* // is called. The two operations end up waiting each other, making the connection unresponsive.
|
||||
* // If you need to do this, use two connections, instead.
|
||||
* co_await conn.async_receive2();
|
||||
* co_await conn.async_exec(req, resp);
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* For an example see cpp20_subscriber.cpp.
|
||||
*
|
||||
* The completion token must have the following signature:
|
||||
*
|
||||
* @code
|
||||
* void f(system::error_code);
|
||||
@@ -662,34 +881,15 @@ public:
|
||||
* @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_->receive_channel_.async_receive(
|
||||
asio::deferred(
|
||||
[&conn = *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 (conn.impl_->receive_channel_.try_receive(f));
|
||||
}
|
||||
|
||||
return asio::deferred.values(ec);
|
||||
}
|
||||
)
|
||||
)(std::forward<CompletionToken>(token));
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
detail::receive2_op<Executor>{impl_.get()},
|
||||
token,
|
||||
*impl_);
|
||||
}
|
||||
|
||||
/** @brief (Deprecated) Receives server pushes synchronously without blocking.
|
||||
@@ -703,24 +903,7 @@ public:
|
||||
* @returns The number of bytes read from the socket.
|
||||
*/
|
||||
BOOST_DEPRECATED("Please, use async_receive2 instead.")
|
||||
std::size_t receive(system::error_code& ec)
|
||||
{
|
||||
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;
|
||||
}
|
||||
std::size_t receive(system::error_code& ec) { return impl_->receive(ec); }
|
||||
|
||||
/** @brief Executes commands on the Redis server asynchronously.
|
||||
*
|
||||
@@ -1103,12 +1286,14 @@ public:
|
||||
template <class CompletionToken = asio::deferred_t>
|
||||
auto async_receive2(CompletionToken&& token = {})
|
||||
{
|
||||
return impl_.async_receive2(std::forward<CompletionToken>(token));
|
||||
return asio::async_initiate<CompletionToken, void(boost::system::error_code)>(
|
||||
initiation{this},
|
||||
token);
|
||||
}
|
||||
|
||||
/// @copydoc basic_connection::receive
|
||||
BOOST_DEPRECATED("Please use async_receive2 instead.")
|
||||
std::size_t receive(system::error_code& ec) { return impl_.receive(ec); }
|
||||
std::size_t receive(system::error_code& ec) { return impl_.impl_->receive(ec); }
|
||||
|
||||
/**
|
||||
* @brief Calls @ref boost::redis::basic_connection::async_exec.
|
||||
@@ -1216,6 +1401,12 @@ private:
|
||||
{
|
||||
self->async_exec_impl(*req, std::move(adapter), std::forward<Handler>(handler));
|
||||
}
|
||||
|
||||
template <class Handler>
|
||||
void operator()(Handler&& handler)
|
||||
{
|
||||
self->async_receive2_impl(std::forward<Handler>(handler));
|
||||
}
|
||||
};
|
||||
|
||||
void async_run_impl(
|
||||
@@ -1232,6 +1423,8 @@ private:
|
||||
any_adapter&& adapter,
|
||||
asio::any_completion_handler<void(boost::system::error_code, std::size_t)> token);
|
||||
|
||||
void async_receive2_impl(asio::any_completion_handler<void(boost::system::error_code)> token);
|
||||
|
||||
basic_connection<executor_type> impl_;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
#ifndef BOOST_REDIS_CONNECT_FSM_HPP
|
||||
#define BOOST_REDIS_CONNECT_FSM_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
@@ -62,17 +60,13 @@ struct connect_action {
|
||||
|
||||
class connect_fsm {
|
||||
int resume_point_{0};
|
||||
const config* cfg_{nullptr};
|
||||
buffered_logger* lgr_{nullptr};
|
||||
|
||||
public:
|
||||
connect_fsm(const config& cfg, buffered_logger& lgr) noexcept
|
||||
: cfg_(&cfg)
|
||||
, lgr_(&lgr)
|
||||
connect_fsm(buffered_logger& lgr) noexcept
|
||||
: lgr_(&lgr)
|
||||
{ }
|
||||
|
||||
const config& get_config() const { return *cfg_; }
|
||||
|
||||
connect_action resume(
|
||||
system::error_code ec,
|
||||
const asio::ip::tcp::resolver::results_type& resolver_results,
|
||||
|
||||
65
include/boost/redis/detail/connect_params.hpp
Normal file
65
include/boost/redis/detail/connect_params.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_CONNECT_PARAMS_HPP
|
||||
#define BOOST_REDIS_CONNECT_PARAMS_HPP
|
||||
|
||||
// Parameters used by redis_stream::async_connect
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_fsm.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Fully identifies where a server is listening. Reference type.
|
||||
class any_address_view {
|
||||
transport_type type_;
|
||||
union {
|
||||
const address* tcp_;
|
||||
std::string_view unix_;
|
||||
};
|
||||
|
||||
public:
|
||||
any_address_view(const address& addr, bool use_ssl) noexcept
|
||||
: type_(use_ssl ? transport_type::tcp_tls : transport_type::tcp)
|
||||
, tcp_(&addr)
|
||||
{ }
|
||||
|
||||
explicit any_address_view(std::string_view unix_socket) noexcept
|
||||
: type_(transport_type::unix_socket)
|
||||
, unix_(unix_socket)
|
||||
{ }
|
||||
|
||||
transport_type type() const { return type_; }
|
||||
|
||||
const address& tcp_address() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == transport_type::tcp || type_ == transport_type::tcp_tls);
|
||||
return *tcp_;
|
||||
}
|
||||
|
||||
std::string_view unix_socket() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == transport_type::unix_socket);
|
||||
return unix_;
|
||||
}
|
||||
};
|
||||
|
||||
struct connect_params {
|
||||
any_address_view addr;
|
||||
std::chrono::steady_clock::duration resolve_timeout;
|
||||
std::chrono::steady_clock::duration connect_timeout;
|
||||
std::chrono::steady_clock::duration ssl_handshake_timeout;
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
@@ -11,22 +11,52 @@
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
#include <boost/redis/detail/subscription_tracker.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// A random engine that gets seeded lazily.
|
||||
// Seeding with std::random_device is not trivial and might fail.
|
||||
class lazy_random_engine {
|
||||
bool seeded_{};
|
||||
std::minstd_rand eng_;
|
||||
|
||||
public:
|
||||
lazy_random_engine() = default;
|
||||
std::minstd_rand& get()
|
||||
{
|
||||
if (!seeded_) {
|
||||
eng_.seed(static_cast<std::minstd_rand::result_type>(std::random_device{}()));
|
||||
seeded_ = true;
|
||||
}
|
||||
return eng_;
|
||||
}
|
||||
};
|
||||
|
||||
// Contains all the members in connection that don't depend on the Executor.
|
||||
// Makes implementing sans-io algorithms easier
|
||||
struct connection_state {
|
||||
buffered_logger logger;
|
||||
config cfg{};
|
||||
multiplexer mpx{};
|
||||
std::string setup_diagnostic{};
|
||||
std::string diagnostic{}; // Used by the setup request and Sentinel
|
||||
request setup_req{};
|
||||
request ping_req{};
|
||||
subscription_tracker tracker{};
|
||||
bool receive2_running{false}, receive2_cancelled{false};
|
||||
|
||||
// Sentinel stuff
|
||||
lazy_random_engine eng{};
|
||||
std::vector<address> sentinels{};
|
||||
std::vector<resp3::node> sentinel_resp_nodes{}; // for parsing
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
struct connection_state;
|
||||
|
||||
// What should we do next?
|
||||
enum class exec_action_type
|
||||
{
|
||||
@@ -54,16 +56,17 @@ public:
|
||||
|
||||
class exec_fsm {
|
||||
int resume_point_{0};
|
||||
multiplexer* mpx_{nullptr};
|
||||
std::shared_ptr<multiplexer::elem> elem_;
|
||||
|
||||
public:
|
||||
exec_fsm(multiplexer& mpx, std::shared_ptr<multiplexer::elem> elem) noexcept
|
||||
: mpx_(&mpx)
|
||||
, elem_(std::move(elem))
|
||||
exec_fsm(std::shared_ptr<multiplexer::elem> elem) noexcept
|
||||
: elem_(std::move(elem))
|
||||
{ }
|
||||
|
||||
exec_action resume(bool connection_is_open, asio::cancellation_type_t cancel_state);
|
||||
exec_action resume(
|
||||
bool connection_is_open,
|
||||
connection_state& st,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
69
include/boost/redis/detail/exec_one_fsm.hpp
Normal file
69
include/boost/redis/detail/exec_one_fsm.hpp
Normal file
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_EXEC_ONE_FSM_HPP
|
||||
#define BOOST_REDIS_EXEC_ONE_FSM_HPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
// Sans-io algorithm for async_exec_one, as a finite state machine
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
class read_buffer;
|
||||
|
||||
// What should we do next?
|
||||
enum class exec_one_action_type
|
||||
{
|
||||
done, // Call the final handler
|
||||
write, // Write the request
|
||||
read_some, // Read into the read buffer
|
||||
};
|
||||
|
||||
struct exec_one_action {
|
||||
exec_one_action_type type;
|
||||
system::error_code ec;
|
||||
|
||||
exec_one_action(exec_one_action_type type) noexcept
|
||||
: type{type}
|
||||
{ }
|
||||
|
||||
exec_one_action(system::error_code ec) noexcept
|
||||
: type{exec_one_action_type::done}
|
||||
, ec{ec}
|
||||
{ }
|
||||
};
|
||||
|
||||
class exec_one_fsm {
|
||||
int resume_point_{0};
|
||||
any_adapter adapter_;
|
||||
std::size_t remaining_responses_;
|
||||
resp3::parser parser_;
|
||||
|
||||
public:
|
||||
exec_one_fsm(any_adapter resp, std::size_t expected_responses)
|
||||
: adapter_(std::move(resp))
|
||||
, remaining_responses_(expected_responses)
|
||||
{ }
|
||||
|
||||
exec_one_action resume(
|
||||
read_buffer& buffer,
|
||||
system::error_code ec,
|
||||
std::size_t bytes_transferred,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
@@ -181,6 +181,12 @@ public:
|
||||
return std::string_view{write_buffer_}.substr(write_offset_);
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_read_buffer() noexcept -> read_buffer&
|
||||
{
|
||||
return read_buffer_;
|
||||
}
|
||||
|
||||
[[nodiscard]]
|
||||
auto get_prepared_read_buffer() noexcept -> read_buffer::span_type;
|
||||
|
||||
|
||||
58
include/boost/redis/detail/receive_fsm.hpp
Normal file
58
include/boost/redis/detail/receive_fsm.hpp
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// Copyright (c) 2018-2026 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_RECEIVE_FSM_HPP
|
||||
#define BOOST_REDIS_RECEIVE_FSM_HPP
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
// Sans-io algorithm for async_receive2, as a finite state machine
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
struct connection_state;
|
||||
|
||||
struct receive_action {
|
||||
enum class action_type
|
||||
{
|
||||
setup_cancellation, // Set up the cancellation types supported by the composed operation
|
||||
wait, // Wait for a message to appear in the receive channel
|
||||
drain_channel, // Empty the receive channel
|
||||
immediate, // Call async_immediate
|
||||
done, // Complete
|
||||
};
|
||||
|
||||
action_type type;
|
||||
system::error_code ec;
|
||||
|
||||
receive_action(action_type type) noexcept
|
||||
: type{type}
|
||||
{ }
|
||||
|
||||
receive_action(system::error_code ec) noexcept
|
||||
: type{action_type::done}
|
||||
, ec{ec}
|
||||
{ }
|
||||
};
|
||||
|
||||
class receive_fsm {
|
||||
int resume_point_{0};
|
||||
|
||||
public:
|
||||
receive_fsm() = default;
|
||||
|
||||
receive_action resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_fsm.hpp>
|
||||
#include <boost/redis/detail/connect_params.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
|
||||
@@ -24,6 +25,7 @@
|
||||
#include <boost/asio/ssl/stream.hpp>
|
||||
#include <boost/asio/ssl/stream_base.hpp>
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <utility>
|
||||
@@ -46,13 +48,16 @@ class redis_stream {
|
||||
void reset_stream() { stream_ = {resolv_.get_executor(), ssl_ctx_}; }
|
||||
|
||||
struct connect_op {
|
||||
redis_stream& obj;
|
||||
redis_stream& obj_;
|
||||
connect_fsm fsm_;
|
||||
connect_params params_;
|
||||
|
||||
template <class Self>
|
||||
void execute_action(Self& self, connect_action act)
|
||||
{
|
||||
const auto& cfg = fsm_.get_config();
|
||||
// Prevent use-after-move errors
|
||||
auto& obj = this->obj_;
|
||||
auto params = this->params_;
|
||||
|
||||
switch (act.type) {
|
||||
case connect_action_type::unix_socket_close:
|
||||
@@ -69,8 +74,8 @@ class redis_stream {
|
||||
case connect_action_type::unix_socket_connect:
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
obj.unix_socket_.async_connect(
|
||||
cfg.unix_socket,
|
||||
asio::cancel_after(obj.timer_, cfg.connect_timeout, std::move(self)));
|
||||
params.addr.unix_socket(),
|
||||
asio::cancel_after(obj.timer_, params.connect_timeout, std::move(self)));
|
||||
#else
|
||||
BOOST_ASSERT(false);
|
||||
#endif
|
||||
@@ -78,9 +83,9 @@ class redis_stream {
|
||||
|
||||
case connect_action_type::tcp_resolve:
|
||||
obj.resolv_.async_resolve(
|
||||
cfg.addr.host,
|
||||
cfg.addr.port,
|
||||
asio::cancel_after(obj.timer_, cfg.resolve_timeout, std::move(self)));
|
||||
params.addr.tcp_address().host,
|
||||
params.addr.tcp_address().port,
|
||||
asio::cancel_after(obj.timer_, params.resolve_timeout, std::move(self)));
|
||||
return;
|
||||
case connect_action_type::ssl_stream_reset:
|
||||
obj.reset_stream();
|
||||
@@ -90,7 +95,7 @@ class redis_stream {
|
||||
case connect_action_type::ssl_handshake:
|
||||
obj.stream_.async_handshake(
|
||||
asio::ssl::stream_base::client,
|
||||
asio::cancel_after(obj.timer_, cfg.ssl_handshake_timeout, std::move(self)));
|
||||
asio::cancel_after(obj.timer_, params.ssl_handshake_timeout, std::move(self)));
|
||||
return;
|
||||
case connect_action_type::done: self.complete(act.ec); break;
|
||||
// Connect should use the specialized handler, where resolver results are available
|
||||
@@ -109,7 +114,7 @@ class redis_stream {
|
||||
auto act = fsm_.resume(
|
||||
ec,
|
||||
selected_endpoint,
|
||||
obj.st_,
|
||||
obj_.st_,
|
||||
self.get_cancellation_state().cancelled());
|
||||
execute_action(self, act);
|
||||
}
|
||||
@@ -121,12 +126,13 @@ class redis_stream {
|
||||
system::error_code ec,
|
||||
asio::ip::tcp::resolver::results_type endpoints)
|
||||
{
|
||||
auto act = fsm_.resume(ec, endpoints, obj.st_, self.get_cancellation_state().cancelled());
|
||||
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
|
||||
asio::async_connect(
|
||||
obj.stream_.next_layer(),
|
||||
std::move(endpoints),
|
||||
asio::cancel_after(obj.timer_, fsm_.get_config().connect_timeout, std::move(self)));
|
||||
asio::cancel_after(obj.timer_, params_.connect_timeout, std::move(self)));
|
||||
} else {
|
||||
execute_action(self, act);
|
||||
}
|
||||
@@ -135,7 +141,7 @@ class redis_stream {
|
||||
template <class Self>
|
||||
void operator()(Self& self, system::error_code ec = {})
|
||||
{
|
||||
auto act = fsm_.resume(ec, obj.st_, self.get_cancellation_state().cancelled());
|
||||
auto act = fsm_.resume(ec, obj_.st_, self.get_cancellation_state().cancelled());
|
||||
execute_action(self, act);
|
||||
}
|
||||
};
|
||||
@@ -170,10 +176,11 @@ public:
|
||||
|
||||
// I/O
|
||||
template <class CompletionToken>
|
||||
auto async_connect(const config& cfg, buffered_logger& l, CompletionToken&& token)
|
||||
auto async_connect(const connect_params& params, buffered_logger& l, CompletionToken&& token)
|
||||
{
|
||||
this->st_.type = params.addr.type();
|
||||
return asio::async_compose<CompletionToken, void(system::error_code)>(
|
||||
connect_op{*this, connect_fsm(cfg, l)},
|
||||
connect_op{*this, connect_fsm{l}, params},
|
||||
token);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#ifndef BOOST_REDIS_RUN_FSM_HPP
|
||||
#define BOOST_REDIS_RUN_FSM_HPP
|
||||
|
||||
#include <boost/redis/detail/connect_params.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
@@ -25,6 +27,7 @@ enum class run_action_type
|
||||
done, // Call the final handler
|
||||
immediate, // Call asio::async_immediate
|
||||
connect, // Transport connection establishment
|
||||
sentinel_resolve, // Contact Sentinels to resolve the master's address
|
||||
parallel_group, // Run the reader, writer and friends
|
||||
cancel_receive, // Cancel the receiver channel
|
||||
wait_for_reconnection, // Sleep for the reconnection period
|
||||
@@ -57,6 +60,8 @@ public:
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
connect_params make_run_connect_params(const connection_state& st);
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
|
||||
93
include/boost/redis/detail/sentinel_resolve_fsm.hpp
Normal file
93
include/boost/redis/detail/sentinel_resolve_fsm.hpp
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_SENTINEL_RESOLVE_FSM_HPP
|
||||
#define BOOST_REDIS_SENTINEL_RESOLVE_FSM_HPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_params.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
// Sans-io algorithm for async_sentinel_resolve, as a finite state machine
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Forward decls
|
||||
struct connection_state;
|
||||
|
||||
class sentinel_action {
|
||||
public:
|
||||
enum class type
|
||||
{
|
||||
done, // Call the final handler
|
||||
connect, // Transport connection establishment
|
||||
request, // Send the Sentinel request
|
||||
};
|
||||
|
||||
sentinel_action(system::error_code ec) noexcept
|
||||
: type_(type::done)
|
||||
, ec_(ec)
|
||||
{ }
|
||||
|
||||
sentinel_action(const address& addr) noexcept
|
||||
: type_(type::connect)
|
||||
, connect_(&addr)
|
||||
{ }
|
||||
|
||||
static sentinel_action request() { return {type::request}; }
|
||||
|
||||
type get_type() const { return type_; }
|
||||
|
||||
[[nodiscard]]
|
||||
system::error_code error() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == type::done);
|
||||
return ec_;
|
||||
}
|
||||
|
||||
const address& connect_addr() const
|
||||
{
|
||||
BOOST_ASSERT(type_ == type::connect);
|
||||
return *connect_;
|
||||
}
|
||||
|
||||
private:
|
||||
type type_;
|
||||
union {
|
||||
system::error_code ec_;
|
||||
const address* connect_;
|
||||
};
|
||||
|
||||
sentinel_action(type type) noexcept
|
||||
: type_(type)
|
||||
{ }
|
||||
};
|
||||
|
||||
class sentinel_resolve_fsm {
|
||||
int resume_point_{0};
|
||||
std::size_t idx_{0u};
|
||||
|
||||
public:
|
||||
sentinel_resolve_fsm() = default;
|
||||
|
||||
sentinel_action resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state);
|
||||
};
|
||||
|
||||
connect_params make_sentinel_connect_params(const config& cfg, const address& sentinel_addr);
|
||||
any_adapter make_sentinel_adapter(connection_state& st);
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_CONNECTOR_HPP
|
||||
35
include/boost/redis/detail/subscription_tracker.hpp
Normal file
35
include/boost/redis/detail/subscription_tracker.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// 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_SUBSCRIPTION_TRACKER_HPP
|
||||
#define BOOST_REDIS_SUBSCRIPTION_TRACKER_HPP
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
class request;
|
||||
|
||||
namespace detail {
|
||||
|
||||
class subscription_tracker {
|
||||
std::set<std::string> channels_;
|
||||
std::set<std::string> pchannels_;
|
||||
|
||||
public:
|
||||
subscription_tracker() = default;
|
||||
void clear();
|
||||
void commit_changes(const request& req);
|
||||
void compose_subscribe_request(request& to) const;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
} // namespace boost::redis
|
||||
|
||||
#endif
|
||||
@@ -94,6 +94,28 @@ 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,
|
||||
|
||||
/// A @ref basic_connection::async_receive2 operation is already running.
|
||||
/// Only one of such operations might be running at any point in time.
|
||||
already_running,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,20 +61,6 @@ struct log_traits<asio::ip::tcp::resolver::results_type> {
|
||||
}
|
||||
};
|
||||
|
||||
inline transport_type transport_from_config(const config& cfg)
|
||||
{
|
||||
if (cfg.unix_socket.empty()) {
|
||||
if (cfg.use_ssl) {
|
||||
return transport_type::tcp_tls;
|
||||
} else {
|
||||
return transport_type::tcp;
|
||||
}
|
||||
} else {
|
||||
BOOST_ASSERT(!cfg.use_ssl);
|
||||
return transport_type::unix_socket;
|
||||
}
|
||||
}
|
||||
|
||||
inline system::error_code translate_timeout_error(
|
||||
system::error_code io_ec,
|
||||
asio::cancellation_type_t cancel_state,
|
||||
@@ -105,9 +91,9 @@ connect_action connect_fsm::resume(
|
||||
|
||||
// Log it
|
||||
if (ec) {
|
||||
log_info(*lgr_, "Error resolving the server hostname: ", ec);
|
||||
log_info(*lgr_, "Connect: hostname resolution failed: ", ec);
|
||||
} else {
|
||||
log_info(*lgr_, "Resolve results: ", resolver_results);
|
||||
log_debug(*lgr_, "Connect: hostname resolution results: ", resolver_results);
|
||||
}
|
||||
|
||||
// Delegate to the regular resume function
|
||||
@@ -125,9 +111,9 @@ connect_action connect_fsm::resume(
|
||||
|
||||
// Log it
|
||||
if (ec) {
|
||||
log_info(*lgr_, "Failed to connect to the server: ", ec);
|
||||
log_info(*lgr_, "Connect: TCP connect failed: ", ec);
|
||||
} else {
|
||||
log_info(*lgr_, "Connected to ", selected_endpoint);
|
||||
log_debug(*lgr_, "Connect: TCP connect succeeded. Selected endpoint: ", selected_endpoint);
|
||||
}
|
||||
|
||||
// Delegate to the regular resume function
|
||||
@@ -142,9 +128,6 @@ connect_action connect_fsm::resume(
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
// Record the transport that we will be using
|
||||
st.type = transport_from_config(*cfg_);
|
||||
|
||||
if (st.type == transport_type::unix_socket) {
|
||||
// Reset the socket, to discard any previous state. Ignore any errors
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, connect_action_type::unix_socket_close)
|
||||
@@ -160,9 +143,9 @@ connect_action connect_fsm::resume(
|
||||
|
||||
// Log it
|
||||
if (ec) {
|
||||
log_info(*lgr_, "Failed to connect to the server: ", ec);
|
||||
log_info(*lgr_, "Connect: UNIX socket connect failed: ", ec);
|
||||
} else {
|
||||
log_info(*lgr_, "Connected to ", cfg_->unix_socket);
|
||||
log_debug(*lgr_, "Connect: UNIX socket connect succeeded");
|
||||
}
|
||||
|
||||
// If this failed, we can't continue
|
||||
@@ -178,7 +161,7 @@ connect_action connect_fsm::resume(
|
||||
// Must be done before anything else is done on the stream.
|
||||
// We don't need to close the TCP socket if using plaintext TCP
|
||||
// because range-connect closes open sockets, while individual connect doesn't
|
||||
if (cfg_->use_ssl && st.ssl_stream_used) {
|
||||
if (st.type == transport_type::tcp_tls && st.ssl_stream_used) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 3, connect_action_type::ssl_stream_reset)
|
||||
}
|
||||
|
||||
@@ -200,7 +183,7 @@ connect_action connect_fsm::resume(
|
||||
return ec;
|
||||
}
|
||||
|
||||
if (cfg_->use_ssl) {
|
||||
if (st.type == transport_type::tcp_tls) {
|
||||
// Mark the SSL stream as used
|
||||
st.ssl_stream_used = true;
|
||||
|
||||
@@ -212,9 +195,9 @@ connect_action connect_fsm::resume(
|
||||
|
||||
// Log it
|
||||
if (ec) {
|
||||
log_info(*lgr_, "Failed to perform SSL handshake: ", ec);
|
||||
log_info(*lgr_, "Connect: SSL handshake failed: ", ec);
|
||||
} else {
|
||||
log_info(*lgr_, "Successfully performed SSL handshake");
|
||||
log_debug(*lgr_, "Connect: SSL handshake succeeded");
|
||||
}
|
||||
|
||||
// If this failed, we can't continue
|
||||
|
||||
@@ -51,6 +51,12 @@ void connection::async_exec_impl(
|
||||
impl_.async_exec(req, std::move(adapter), std::move(token));
|
||||
}
|
||||
|
||||
void connection::async_receive2_impl(
|
||||
asio::any_completion_handler<void(boost::system::error_code)> token)
|
||||
{
|
||||
impl_.async_receive2(std::move(token));
|
||||
}
|
||||
|
||||
void connection::cancel(operation op) { impl_.cancel(op); }
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
@@ -55,8 +55,22 @@ struct error_category_impl : system::error_category {
|
||||
case error::exceeds_maximum_read_buffer_size:
|
||||
return "Reading data from the socket would exceed the maximum size allowed of the read "
|
||||
"buffer.";
|
||||
case error::write_timeout:
|
||||
return "Timeout while writing data to the server.";
|
||||
case error::write_timeout: return "Timeout while writing data to the server.";
|
||||
case error::sentinel_unix_sockets_unsupported:
|
||||
return "The configuration specified UNIX sockets with Sentinel, which is not "
|
||||
"supported.";
|
||||
case error::sentinel_resolve_failed:
|
||||
return "No Sentinel could be used to obtain the address of the Redis server.";
|
||||
case error::role_check_failed:
|
||||
return "The contacted server does not have the expected role. "
|
||||
"This is likely a transient failure caused by a Sentinel failover in progress.";
|
||||
case error::expects_resp3_string:
|
||||
return "Expects a RESP3 string, but got a different data type.";
|
||||
case error::expects_resp3_array:
|
||||
return "Expects a RESP3 array, but got a different data type.";
|
||||
case error::already_running:
|
||||
return "An async_receive2 operation is already running. Only one of such operations "
|
||||
"might be running at any point in time.";
|
||||
default: BOOST_ASSERT(false); return "Boost.Redis error.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#ifndef BOOST_REDIS_EXEC_FSM_IPP
|
||||
#define BOOST_REDIS_EXEC_FSM_IPP
|
||||
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/exec_fsm.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
@@ -28,7 +29,10 @@ inline bool is_total_cancel(asio::cancellation_type_t type)
|
||||
return !!(type & asio::cancellation_type_t::total);
|
||||
}
|
||||
|
||||
exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t cancel_state)
|
||||
exec_action exec_fsm::resume(
|
||||
bool connection_is_open,
|
||||
connection_state& st,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
@@ -47,7 +51,7 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, exec_action_type::setup_cancellation)
|
||||
|
||||
// Add the request to the multiplexer
|
||||
mpx_->add(elem_);
|
||||
st.mpx.add(elem_);
|
||||
|
||||
// Notify the writer task that there is work to do. If the task is not
|
||||
// listening (e.g. it's already writing or the connection is not healthy),
|
||||
@@ -61,8 +65,14 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t
|
||||
|
||||
// If the request has completed (with error or not), we're done
|
||||
if (elem_->is_done()) {
|
||||
// If the request completed successfully and we were configured to do so,
|
||||
// record the changes applied to the pubsub state
|
||||
if (!elem_->get_error())
|
||||
st.tracker.commit_changes(elem_->get_request());
|
||||
|
||||
// Deallocate memory before finalizing
|
||||
exec_action act{elem_->get_error(), elem_->get_read_size()};
|
||||
elem_.reset(); // Deallocate memory before finalizing
|
||||
elem_.reset();
|
||||
return act;
|
||||
}
|
||||
|
||||
@@ -71,7 +81,7 @@ exec_action exec_fsm::resume(bool connection_is_open, asio::cancellation_type_t
|
||||
if (
|
||||
(is_total_cancel(cancel_state) && elem_->is_waiting()) ||
|
||||
is_partial_or_terminal_cancel(cancel_state)) {
|
||||
mpx_->cancel(elem_);
|
||||
st.mpx.cancel(elem_);
|
||||
elem_.reset(); // Deallocate memory before finalizing
|
||||
return exec_action{asio::error::operation_aborted};
|
||||
}
|
||||
|
||||
95
include/boost/redis/impl/exec_one_fsm.ipp
Normal file
95
include/boost/redis/impl/exec_one_fsm.ipp
Normal file
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_EXEC_ONE_FSM_IPP
|
||||
#define BOOST_REDIS_EXEC_ONE_FSM_IPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/exec_one_fsm.hpp>
|
||||
#include <boost/redis/detail/read_buffer.hpp>
|
||||
#include <boost/redis/impl/is_terminal_cancel.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
exec_one_action exec_one_fsm::resume(
|
||||
read_buffer& buffer,
|
||||
system::error_code ec,
|
||||
std::size_t bytes_transferred,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
// Send the request to the server
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, exec_one_action_type::write)
|
||||
|
||||
// Errors and cancellations
|
||||
if (is_terminal_cancel(cancel_state))
|
||||
return system::error_code{asio::error::operation_aborted};
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// If the request didn't expect any response, we're done
|
||||
if (remaining_responses_ == 0u)
|
||||
return system::error_code{};
|
||||
|
||||
// Read responses until we're done
|
||||
buffer.clear();
|
||||
while (true) {
|
||||
// Prepare the buffer to read some data
|
||||
ec = buffer.prepare();
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Read data
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, exec_one_action_type::read_some)
|
||||
|
||||
// Errors and cancellations
|
||||
if (is_terminal_cancel(cancel_state))
|
||||
return system::error_code{asio::error::operation_aborted};
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// Commit the data into the buffer
|
||||
buffer.commit(bytes_transferred);
|
||||
|
||||
// Consume the data until we run out or all the responses have been read
|
||||
while (resp3::parse(parser_, buffer.get_commited(), adapter_, ec)) {
|
||||
// Check for errors
|
||||
if (ec)
|
||||
return ec;
|
||||
|
||||
// We've finished parsing a response
|
||||
buffer.consume(parser_.get_consumed());
|
||||
parser_.reset();
|
||||
|
||||
// When no more responses remain, we're done.
|
||||
// Don't read ahead, even if more data is available
|
||||
if (--remaining_responses_ == 0u)
|
||||
return system::error_code{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BOOST_ASSERT(false);
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
248
include/boost/redis/impl/flat_tree.ipp
Normal file
248
include/boost/redis/impl/flat_tree.ipp
Normal file
@@ -0,0 +1,248 @@
|
||||
//
|
||||
// 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 <boost/throw_exception.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#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;
|
||||
}
|
||||
|
||||
// Erases the first num_bytes bytes from the buffer by moving
|
||||
// the remaining bytes forward. Rebases the strings in nodes as required.
|
||||
inline void erase_first(flat_buffer& buff, std::size_t num_bytes, view_tree& nodes)
|
||||
{
|
||||
BOOST_ASSERT(num_bytes <= buff.size);
|
||||
if (num_bytes > 0u) {
|
||||
// If we have any data to move, we should always have a buffer
|
||||
BOOST_ASSERT(buff.data.get() != nullptr);
|
||||
|
||||
// Record the old base
|
||||
const char* old_base = buff.data.get() + num_bytes;
|
||||
|
||||
// Move all that we're gonna keep to the start of the buffer
|
||||
auto bytes_left = buff.size - num_bytes;
|
||||
std::memmove(buff.data.get(), old_base, bytes_left);
|
||||
|
||||
// Rebase strings
|
||||
rebase_strings(nodes, old_base, buff.data.get());
|
||||
}
|
||||
}
|
||||
|
||||
// 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_}
|
||||
, node_tmp_offset_{other.node_tmp_offset_}
|
||||
, data_tmp_offset_{other.data_tmp_offset_}
|
||||
{
|
||||
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_;
|
||||
node_tmp_offset_ = other.node_tmp_offset_;
|
||||
data_tmp_offset_ = other.data_tmp_offset_;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
// Discard everything except for the tmp area
|
||||
view_tree_.erase(view_tree_.begin(), view_tree_.begin() + node_tmp_offset_);
|
||||
node_tmp_offset_ = 0u;
|
||||
|
||||
// Do the same for the data area
|
||||
detail::erase_first(data_, data_tmp_offset_, view_tree_);
|
||||
data_tmp_offset_ = 0u;
|
||||
|
||||
// We now have no messages
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
void flat_tree::notify_init()
|
||||
{
|
||||
// Discard any data in the tmp area, as it belongs to an operation that never finished
|
||||
BOOST_ASSERT(node_tmp_offset_ <= view_tree_.size());
|
||||
BOOST_ASSERT(data_tmp_offset_ <= data_.size);
|
||||
view_tree_.resize(node_tmp_offset_);
|
||||
data_.size = data_tmp_offset_;
|
||||
}
|
||||
|
||||
void flat_tree::notify_done()
|
||||
{
|
||||
++total_msgs_;
|
||||
node_tmp_offset_ = view_tree_.size();
|
||||
data_tmp_offset_ = data_.size;
|
||||
}
|
||||
|
||||
const node_view& flat_tree::at(std::size_t i) const
|
||||
{
|
||||
if (i >= size())
|
||||
BOOST_THROW_EXCEPTION(std::out_of_range("flat_tree::at"));
|
||||
return view_tree_[i];
|
||||
}
|
||||
|
||||
bool operator==(flat_tree const& a, flat_tree const& b)
|
||||
{
|
||||
// data is already taken into account by comparing the nodes.
|
||||
// Only committed nodes should be taken into account.
|
||||
return a.size() == b.size() && std::equal(a.begin(), a.end(), b.begin()) &&
|
||||
a.get_total_msgs() == b.get_total_msgs();
|
||||
}
|
||||
|
||||
} // namespace boost::redis::resp3
|
||||
@@ -7,6 +7,7 @@
|
||||
#ifndef BOOST_REDIS_LOG_UTILS_HPP
|
||||
#define BOOST_REDIS_LOG_UTILS_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
@@ -48,6 +49,16 @@ struct log_traits<system::error_code> {
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct log_traits<address> {
|
||||
static inline void log(std::string& to, const address& value)
|
||||
{
|
||||
to += value.host;
|
||||
to += ':';
|
||||
to += value.port;
|
||||
}
|
||||
};
|
||||
|
||||
template <class... Args>
|
||||
void format_log_args(std::string& to, const Args&... args)
|
||||
{
|
||||
|
||||
85
include/boost/redis/impl/receive_fsm.ipp
Normal file
85
include/boost/redis/impl/receive_fsm.ipp
Normal file
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Copyright (c) 2018-2026 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/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/receive_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/experimental/channel_error.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
constexpr bool is_any_cancel(asio::cancellation_type_t type)
|
||||
{
|
||||
return !!(
|
||||
type & (asio::cancellation_type_t::terminal | asio::cancellation_type_t::partial |
|
||||
asio::cancellation_type_t::total));
|
||||
}
|
||||
|
||||
// We use the receive2_cancelled flag rather than will_reconnect() to
|
||||
// avoid entanglement between async_run and async_receive2 cancellations.
|
||||
// If we had used will_reconnect(), async_receive2 would be cancelled
|
||||
// when disabling reconnection and async_run exits, and in an unpredictable fashion.
|
||||
receive_action receive_fsm::resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
// Parallel async_receive2 operations not supported
|
||||
if (st.receive2_running) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, receive_action::action_type::immediate)
|
||||
return system::error_code(error::already_running);
|
||||
}
|
||||
|
||||
// We're now running. Discard any previous cancellation state
|
||||
st.receive2_running = true;
|
||||
st.receive2_cancelled = false;
|
||||
|
||||
// This operation supports total cancellation. Set it up
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, receive_action::action_type::setup_cancellation)
|
||||
|
||||
while (true) {
|
||||
// Wait at least once for a notification to arrive
|
||||
BOOST_REDIS_YIELD(resume_point_, 3, receive_action::action_type::wait)
|
||||
|
||||
// If the wait completed successfully, we have pushes. Drain the channel and exit
|
||||
if (!ec) {
|
||||
BOOST_REDIS_YIELD(resume_point_, 4, receive_action::action_type::drain_channel)
|
||||
st.receive2_running = false;
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// Check for cancellations
|
||||
if (is_any_cancel(cancel_state) || st.receive2_cancelled) {
|
||||
st.receive2_running = false;
|
||||
return system::error_code(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
// If we get any unknown errors, propagate them (shouldn't happen, but just in case)
|
||||
if (ec != asio::experimental::channel_errc::channel_cancelled) {
|
||||
st.receive2_running = false;
|
||||
return ec;
|
||||
}
|
||||
|
||||
// The channel was cancelled and no cancellation state is set.
|
||||
// This is due to a reconnection. Ignore the notification
|
||||
}
|
||||
}
|
||||
|
||||
// We should never get here
|
||||
BOOST_ASSERT(false);
|
||||
return receive_action{system::error_code()};
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
@@ -5,7 +5,9 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
@@ -31,3 +33,35 @@ request make_hello_request()
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
void boost::redis::request::append(const request& other)
|
||||
{
|
||||
// Remember the old payload size, to update offsets
|
||||
std::size_t old_offset = payload_.size();
|
||||
|
||||
// Add the payload
|
||||
payload_ += other.payload_;
|
||||
commands_ += other.commands_;
|
||||
expected_responses_ += other.expected_responses_;
|
||||
|
||||
// Add the pubsub changes. Offsets need to be updated
|
||||
pubsub_changes_.reserve(pubsub_changes_.size() + other.pubsub_changes_.size());
|
||||
for (const auto& change : other.pubsub_changes_) {
|
||||
pubsub_changes_.push_back({
|
||||
change.type,
|
||||
change.channel_offset + old_offset,
|
||||
change.channel_size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void boost::redis::request::add_pubsub_arg(detail::pubsub_change_type type, std::string_view value)
|
||||
{
|
||||
// Add the argument
|
||||
resp3::add_bulk(payload_, value);
|
||||
|
||||
// Track the change.
|
||||
// The final \r\n adds 2 bytes
|
||||
std::size_t offset = payload_.size() - value.size() - 2u;
|
||||
pubsub_changes_.push_back({type, offset, value.size()});
|
||||
}
|
||||
|
||||
@@ -9,9 +9,13 @@
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
void consume_one(generic_response& r, system::error_code& ec)
|
||||
namespace detail {
|
||||
|
||||
inline void consume_one_impl(generic_response& r, system::error_code& ec)
|
||||
{
|
||||
if (r.has_error())
|
||||
return; // Nothing to consume.
|
||||
@@ -38,10 +42,14 @@ void consume_one(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;
|
||||
consume_one(r, ec);
|
||||
detail::consume_one_impl(r, ec);
|
||||
if (ec)
|
||||
throw system::system_error(ec);
|
||||
}
|
||||
|
||||
@@ -8,12 +8,15 @@
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_params.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
#include <boost/redis/detail/run_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/impl/is_terminal_cancel.hpp>
|
||||
#include <boost/redis/impl/log_utils.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp>
|
||||
#include <boost/redis/impl/setup_request_utils.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
@@ -28,6 +31,8 @@ inline system::error_code check_config(const config& cfg)
|
||||
if (!cfg.unix_socket.empty()) {
|
||||
if (cfg.use_ssl)
|
||||
return error::unix_sockets_ssl_unsupported;
|
||||
if (use_sentinel(cfg))
|
||||
return error::sentinel_unix_sockets_unsupported;
|
||||
#ifndef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
return error::unix_sockets_unsupported;
|
||||
#endif
|
||||
@@ -41,45 +46,44 @@ inline void compose_ping_request(const config& cfg, request& to)
|
||||
to.push("PING", cfg.health_check_id);
|
||||
}
|
||||
|
||||
inline void process_setup_node(
|
||||
connection_state& st,
|
||||
resp3::basic_node<std::string_view> const& nd,
|
||||
system::error_code& ec)
|
||||
{
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::simple_error:
|
||||
case resp3::type::blob_error:
|
||||
case resp3::type::null:
|
||||
ec = redis::error::resp3_hello;
|
||||
st.setup_diagnostic = nd.value;
|
||||
break;
|
||||
default:;
|
||||
}
|
||||
}
|
||||
|
||||
inline any_adapter make_setup_adapter(connection_state& st)
|
||||
{
|
||||
return any_adapter{
|
||||
[&st](any_adapter::parse_event evt, resp3::node_view const& nd, system::error_code& ec) {
|
||||
if (evt == any_adapter::parse_event::node)
|
||||
process_setup_node(st, nd, ec);
|
||||
}};
|
||||
}
|
||||
|
||||
inline void on_setup_done(const multiplexer::elem& elm, connection_state& st)
|
||||
{
|
||||
const auto ec = elm.get_error();
|
||||
if (ec) {
|
||||
if (st.setup_diagnostic.empty()) {
|
||||
if (st.diagnostic.empty()) {
|
||||
log_info(st.logger, "Setup request execution: ", ec);
|
||||
} else {
|
||||
log_info(st.logger, "Setup request execution: ", ec, " (", st.setup_diagnostic, ")");
|
||||
log_info(st.logger, "Setup request execution: ", ec, " (", st.diagnostic, ")");
|
||||
}
|
||||
} else {
|
||||
log_info(st.logger, "Setup request execution: success");
|
||||
}
|
||||
}
|
||||
|
||||
inline any_address_view get_server_address(const connection_state& st)
|
||||
{
|
||||
if (st.cfg.unix_socket.empty()) {
|
||||
return {st.cfg.addr, st.cfg.use_ssl};
|
||||
} else {
|
||||
return any_address_view{st.cfg.unix_socket};
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
struct log_traits<any_address_view> {
|
||||
static inline void log(std::string& to, any_address_view value)
|
||||
{
|
||||
if (value.type() == transport_type::unix_socket) {
|
||||
to += '\'';
|
||||
to += value.unix_socket();
|
||||
to += '\'';
|
||||
} else {
|
||||
log_traits<address>::log(to, value.tcp_address());
|
||||
to += value.type() == transport_type::tcp_tls ? " (TLS enabled)" : " (TLS disabled)";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
run_action run_fsm::resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
@@ -97,15 +101,40 @@ run_action run_fsm::resume(
|
||||
return stored_ec_;
|
||||
}
|
||||
|
||||
// Compose the setup request. This only depends on the config, so it can be done just once
|
||||
compose_setup_request(st.cfg);
|
||||
// Clear any remainder from previous runs
|
||||
st.tracker.clear();
|
||||
|
||||
// Compose the PING request. Same as above
|
||||
// Compose the PING request. This only depends on the config, so it can be done just once
|
||||
compose_ping_request(st.cfg, st.ping_req);
|
||||
|
||||
if (use_sentinel(st.cfg)) {
|
||||
// Sentinel request. Same as above
|
||||
compose_sentinel_request(st.cfg);
|
||||
|
||||
// Bootstrap the sentinel list with the ones configured by the user
|
||||
st.sentinels = st.cfg.sentinel.addresses;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
// Sentinel resolve, if required. This leaves the address in st.cfg.address
|
||||
if (use_sentinel(st.cfg)) {
|
||||
// This operation does the logging for us.
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, run_action_type::sentinel_resolve)
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
log_debug(st.logger, "Run: cancelled (4)");
|
||||
return {asio::error::operation_aborted};
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (ec)
|
||||
goto sleep_and_reconnect;
|
||||
}
|
||||
|
||||
// Try to connect
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, run_action_type::connect)
|
||||
log_info(st.logger, "Trying to connect to Redis server at ", get_server_address(st));
|
||||
BOOST_REDIS_YIELD(resume_point_, 4, run_action_type::connect)
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
@@ -113,53 +142,67 @@ run_action run_fsm::resume(
|
||||
return system::error_code(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
// 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_;
|
||||
if (ec) {
|
||||
// There was an error. Skip to the reconnection loop
|
||||
log_info(
|
||||
st.logger,
|
||||
"Failed to connect to Redis server at ",
|
||||
get_server_address(st),
|
||||
": ",
|
||||
ec);
|
||||
goto sleep_and_reconnect;
|
||||
}
|
||||
|
||||
// We were successful
|
||||
log_info(st.logger, "Connected to Redis server at ", get_server_address(st));
|
||||
|
||||
// Initialization
|
||||
st.mpx.reset();
|
||||
st.diagnostic.clear();
|
||||
compose_setup_request(st.cfg, st.tracker, st.setup_req);
|
||||
|
||||
// Add the setup request to the multiplexer
|
||||
if (st.setup_req.get_commands() != 0u) {
|
||||
auto elm = make_elem(st.setup_req, 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_, 5, run_action_type::wait_for_reconnection)
|
||||
BOOST_REDIS_YIELD(resume_point_, 7, run_action_type::wait_for_reconnection)
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
@@ -174,4 +217,14 @@ run_action run_fsm::resume(
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
connect_params make_run_connect_params(const connection_state& st)
|
||||
{
|
||||
return {
|
||||
get_server_address(st),
|
||||
st.cfg.resolve_timeout,
|
||||
st.cfg.connect_timeout,
|
||||
st.cfg.ssl_handshake_timeout,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
182
include/boost/redis/impl/sentinel_resolve_fsm.ipp
Normal file
182
include/boost/redis/impl/sentinel_resolve_fsm.ipp
Normal file
@@ -0,0 +1,182 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_SENTINEL_RESOLVE_FSM_IPP
|
||||
#define BOOST_REDIS_SENTINEL_RESOLVE_FSM_IPP
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_params.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/coroutine.hpp>
|
||||
#include <boost/redis/detail/sentinel_resolve_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/impl/is_terminal_cancel.hpp>
|
||||
#include <boost/redis/impl/log_utils.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <random>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Logs an error at info level, and also stores it in the state,
|
||||
// so it can be logged at error level if all Sentinels fail.
|
||||
template <class... Args>
|
||||
void log_sentinel_error(connection_state& st, std::size_t current_idx, const Args&... args)
|
||||
{
|
||||
st.diagnostic += "\n ";
|
||||
std::size_t size_before = st.diagnostic.size();
|
||||
format_log_args(st.diagnostic, "Sentinel at ", st.sentinels[current_idx], ": ", args...);
|
||||
log_info(st.logger, std::string_view{st.diagnostic}.substr(size_before));
|
||||
}
|
||||
|
||||
sentinel_action sentinel_resolve_fsm::resume(
|
||||
connection_state& st,
|
||||
system::error_code ec,
|
||||
asio::cancellation_type_t cancel_state)
|
||||
{
|
||||
switch (resume_point_) {
|
||||
BOOST_REDIS_CORO_INITIAL
|
||||
|
||||
st.diagnostic.clear();
|
||||
|
||||
log_info(
|
||||
st.logger,
|
||||
"Trying to resolve the address of ",
|
||||
st.cfg.sentinel.server_role == role::master ? "master" : "a replica of master",
|
||||
" '",
|
||||
st.cfg.sentinel.master_name,
|
||||
"' using Sentinel");
|
||||
|
||||
// Try all Sentinels in order. Upon any errors, save the diagnostic and try with the next one.
|
||||
// If none of them are available, print an error diagnostic and fail.
|
||||
for (idx_ = 0u; idx_ < st.sentinels.size(); ++idx_) {
|
||||
log_debug(st.logger, "Trying to contact Sentinel at ", st.sentinels[idx_]);
|
||||
|
||||
// Try to connect
|
||||
BOOST_REDIS_YIELD(resume_point_, 1, st.sentinels[idx_])
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
log_debug(st.logger, "Sentinel resolve: cancelled (1)");
|
||||
return system::error_code(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (ec) {
|
||||
log_sentinel_error(st, idx_, "connection establishment error: ", ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Execute the Sentinel request
|
||||
log_debug(st.logger, "Executing Sentinel request at ", st.sentinels[idx_]);
|
||||
st.sentinel_resp_nodes.clear();
|
||||
BOOST_REDIS_YIELD(resume_point_, 2, sentinel_action::request())
|
||||
|
||||
// Check for cancellations
|
||||
if (is_terminal_cancel(cancel_state)) {
|
||||
log_debug(st.logger, "Sentinel resolve: cancelled (2)");
|
||||
return system::error_code(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if (ec) {
|
||||
log_sentinel_error(st, idx_, "error while executing request: ", ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
sentinel_response resp;
|
||||
ec = parse_sentinel_response(st.sentinel_resp_nodes, st.cfg.sentinel.server_role, resp);
|
||||
|
||||
if (ec) {
|
||||
if (ec == error::resp3_simple_error || ec == error::resp3_blob_error) {
|
||||
log_sentinel_error(st, idx_, "responded with an error: ", resp.diagnostic);
|
||||
} else if (ec == error::resp3_null) {
|
||||
log_sentinel_error(st, idx_, "doesn't know about the configured master");
|
||||
} else {
|
||||
log_sentinel_error(
|
||||
st,
|
||||
idx_,
|
||||
"error parsing response (maybe forgot to upgrade to RESP3?): ",
|
||||
ec);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// When asking for replicas, we might get no replicas
|
||||
if (st.cfg.sentinel.server_role == role::replica && resp.replicas.empty()) {
|
||||
log_sentinel_error(st, idx_, "the configured master has no replicas");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Store the resulting address in a well-known place
|
||||
if (st.cfg.sentinel.server_role == role::master) {
|
||||
st.cfg.addr = resp.master_addr;
|
||||
} else {
|
||||
// Choose a random replica
|
||||
std::uniform_int_distribution<std::size_t> dist{0u, resp.replicas.size() - 1u};
|
||||
const auto idx = dist(st.eng.get());
|
||||
st.cfg.addr = resp.replicas[idx];
|
||||
}
|
||||
|
||||
// Sentinel knows about this master. Log and update our config
|
||||
log_info(
|
||||
st.logger,
|
||||
"Sentinel at ",
|
||||
st.sentinels[idx_],
|
||||
" resolved the server address to ",
|
||||
st.cfg.addr);
|
||||
|
||||
update_sentinel_list(st.sentinels, idx_, resp.sentinels, st.cfg.sentinel.addresses);
|
||||
|
||||
st.sentinel_resp_nodes.clear(); // reduce memory consumption
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// No Sentinel resolved our address
|
||||
log_err(
|
||||
st.logger,
|
||||
"Failed to resolve the address of ",
|
||||
st.cfg.sentinel.server_role == role::master ? "master" : "a replica of master",
|
||||
" '",
|
||||
st.cfg.sentinel.master_name,
|
||||
"'. Tried the following Sentinels:",
|
||||
st.diagnostic);
|
||||
return {error::sentinel_resolve_failed};
|
||||
}
|
||||
|
||||
// We should never get here
|
||||
BOOST_ASSERT(false);
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
connect_params make_sentinel_connect_params(const config& cfg, const address& addr)
|
||||
{
|
||||
return {
|
||||
any_address_view{addr, cfg.sentinel.use_ssl},
|
||||
cfg.sentinel.resolve_timeout,
|
||||
cfg.sentinel.connect_timeout,
|
||||
cfg.sentinel.ssl_handshake_timeout,
|
||||
};
|
||||
}
|
||||
|
||||
any_adapter make_sentinel_adapter(connection_state& st)
|
||||
{
|
||||
return any_adapter(st.sentinel_resp_nodes);
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
277
include/boost/redis/impl/sentinel_utils.hpp
Normal file
277
include/boost/redis/impl/sentinel_utils.hpp
Normal file
@@ -0,0 +1,277 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#ifndef BOOST_REDIS_SENTINEL_UTILS_HPP
|
||||
#define BOOST_REDIS_SENTINEL_UTILS_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/core/span.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
// Returns true if Sentinel should be used
|
||||
inline bool use_sentinel(const config& cfg) { return !cfg.sentinel.addresses.empty(); }
|
||||
|
||||
// Composes the request to send to Sentinel modifying cfg.sentinel.setup
|
||||
inline void compose_sentinel_request(config& cfg)
|
||||
{
|
||||
// These commands should go after the user-supplied setup, as this might involve authentication.
|
||||
// We ask for the master even when connecting to replicas to correctly detect when the master doesn't exist
|
||||
cfg.sentinel.setup.push("SENTINEL", "GET-MASTER-ADDR-BY-NAME", cfg.sentinel.master_name);
|
||||
if (cfg.sentinel.server_role == role::replica)
|
||||
cfg.sentinel.setup.push("SENTINEL", "REPLICAS", cfg.sentinel.master_name);
|
||||
cfg.sentinel.setup.push("SENTINEL", "SENTINELS", cfg.sentinel.master_name);
|
||||
|
||||
// Note that we don't care about request flags because this is a one-time request
|
||||
}
|
||||
|
||||
// Parses a list of replicas or sentinels
|
||||
inline system::error_code parse_server_list(
|
||||
const resp3::node*& first,
|
||||
const resp3::node* last,
|
||||
std::vector<address>& out)
|
||||
{
|
||||
const auto* it = first;
|
||||
ignore_unused(last);
|
||||
|
||||
// The root node must be an array
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 0u);
|
||||
if (it->data_type != resp3::type::array)
|
||||
return {error::expects_resp3_array};
|
||||
const std::size_t num_servers = it->aggregate_size;
|
||||
++it;
|
||||
|
||||
// Each element in the array represents a server
|
||||
out.resize(num_servers);
|
||||
for (std::size_t i = 0u; i < num_servers; ++i) {
|
||||
// A server is a map (resp3) or array (resp2, currently unsupported)
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 1u);
|
||||
if (it->data_type != resp3::type::map)
|
||||
return {error::expects_resp3_map};
|
||||
const std::size_t num_key_values = it->aggregate_size;
|
||||
++it;
|
||||
|
||||
// The server object is composed by a set of key/value pairs.
|
||||
// Skip everything except for the ones we care for.
|
||||
bool ip_seen = false, port_seen = false;
|
||||
for (std::size_t j = 0; j < num_key_values; ++j) {
|
||||
// Key. It should be a string
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 2u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
const std::string_view key = it->value;
|
||||
++it;
|
||||
|
||||
// Value. All values seem to be strings, too.
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 2u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
|
||||
// Record it
|
||||
if (key == "ip") {
|
||||
ip_seen = true;
|
||||
out[i].host = it->value;
|
||||
} else if (key == "port") {
|
||||
port_seen = true;
|
||||
out[i].port = it->value;
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
|
||||
// Check that the response actually contained the fields we wanted
|
||||
if (!ip_seen || !port_seen)
|
||||
return {error::empty_field};
|
||||
}
|
||||
|
||||
// Done
|
||||
first = it;
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// The output type of parse_sentinel_response
|
||||
struct sentinel_response {
|
||||
std::string diagnostic; // In case the server returned an error
|
||||
address master_addr; // Always populated
|
||||
std::vector<address> replicas; // Populated only when connecting to replicas
|
||||
std::vector<address> sentinels;
|
||||
};
|
||||
|
||||
// Parses an array of nodes into a sentinel_response.
|
||||
// The request originating this response should be:
|
||||
// <user-supplied commands, as per sentinel_config::setup>
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME
|
||||
// SENTINEL REPLICAS (only if server_role is replica)
|
||||
// SENTINEL SENTINELS
|
||||
// SENTINEL SENTINELS and SENTINEL REPLICAS error when the master name is unknown. Error nodes
|
||||
// should be allowed in the node array.
|
||||
// This means that we can't use generic_response, since its adapter errors on error nodes.
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME is sent even when connecting to replicas
|
||||
// for better diagnostics when the master name is unknown.
|
||||
// Preconditions:
|
||||
// * There are at least 2 (master)/3 (replica) root nodes.
|
||||
// * The node array originates from parsing a valid RESP3 message.
|
||||
// E.g. we won't check that the first node has depth 0.
|
||||
inline system::error_code parse_sentinel_response(
|
||||
span<const resp3::node> nodes,
|
||||
role server_role,
|
||||
sentinel_response& out)
|
||||
{
|
||||
auto check_errors = [&out](const resp3::node& nd) {
|
||||
switch (nd.data_type) {
|
||||
case resp3::type::simple_error:
|
||||
out.diagnostic = nd.value;
|
||||
return system::error_code(error::resp3_simple_error);
|
||||
case resp3::type::blob_error:
|
||||
out.diagnostic = nd.value;
|
||||
return system::error_code(error::resp3_blob_error);
|
||||
default: return system::error_code();
|
||||
}
|
||||
};
|
||||
|
||||
// Clear the output
|
||||
out.diagnostic.clear();
|
||||
out.sentinels.clear();
|
||||
out.replicas.clear();
|
||||
|
||||
// Find the first root node of interest. It's the 2nd or 3rd, starting with the end
|
||||
auto find_first = [nodes, server_role] {
|
||||
const std::size_t expected_roots = server_role == role::master ? 2u : 3u;
|
||||
std::size_t roots_seen = 0u;
|
||||
for (auto it = nodes.rbegin();; ++it) {
|
||||
BOOST_ASSERT(it != nodes.rend());
|
||||
if (it->depth == 0u && ++roots_seen == expected_roots)
|
||||
return &*it;
|
||||
}
|
||||
};
|
||||
const resp3::node* lib_first = find_first();
|
||||
|
||||
// Iterators
|
||||
const resp3::node* it = nodes.begin();
|
||||
const resp3::node* last = nodes.end();
|
||||
ignore_unused(last);
|
||||
|
||||
// Go through all the responses to user-supplied requests checking for errors
|
||||
for (; it != lib_first; ++it) {
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
}
|
||||
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME
|
||||
|
||||
// Check for errors
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
|
||||
// If the root node is NULL, Sentinel doesn't know about this master.
|
||||
// We use resp3_null to signal this fact. This doesn't reach the end user.
|
||||
if (it->data_type == resp3::type::null) {
|
||||
return {error::resp3_null};
|
||||
}
|
||||
|
||||
// If the root node is an array, an IP and port follow
|
||||
if (it->data_type != resp3::type::array)
|
||||
return {error::expects_resp3_array};
|
||||
if (it->aggregate_size != 2u)
|
||||
return {error::incompatible_size};
|
||||
++it;
|
||||
|
||||
// IP
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 1u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
out.master_addr.host = it->value;
|
||||
++it;
|
||||
|
||||
// Port
|
||||
BOOST_ASSERT(it != last);
|
||||
BOOST_ASSERT(it->depth == 1u);
|
||||
if (it->data_type != resp3::type::blob_string)
|
||||
return {error::expects_resp3_string};
|
||||
out.master_addr.port = it->value;
|
||||
++it;
|
||||
|
||||
if (server_role == role::replica) {
|
||||
// SENTINEL REPLICAS
|
||||
|
||||
// This request fails if Sentinel doesn't know about this master.
|
||||
// However, that's not the case if we got here.
|
||||
// Check for other errors.
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
|
||||
// Actual parsing
|
||||
if (auto ec = parse_server_list(it, last, out.replicas))
|
||||
return ec;
|
||||
}
|
||||
|
||||
// SENTINEL SENTINELS
|
||||
|
||||
// This request fails if Sentinel doesn't know about this master.
|
||||
// However, that's not the case if we got here.
|
||||
// Check for other errors.
|
||||
if (auto ec = check_errors(*it))
|
||||
return ec;
|
||||
|
||||
// Actual parsing
|
||||
if (auto ec = parse_server_list(it, last, out.sentinels))
|
||||
return ec;
|
||||
|
||||
// Done
|
||||
return system::error_code();
|
||||
}
|
||||
|
||||
// Updates the internal Sentinel list.
|
||||
// to should never be empty
|
||||
inline void update_sentinel_list(
|
||||
std::vector<address>& to,
|
||||
std::size_t current_index, // the one to maintain and place first
|
||||
span<const address> gossip_sentinels, // the ones that SENTINEL SENTINELS returned
|
||||
span<const address> bootstrap_sentinels // the ones the user supplied
|
||||
)
|
||||
{
|
||||
BOOST_ASSERT(!to.empty());
|
||||
|
||||
// Remove everything, except the Sentinel that succeeded
|
||||
if (current_index != 0u)
|
||||
std::swap(to.front(), to[current_index]);
|
||||
to.resize(1u);
|
||||
|
||||
// Add one group. These Sentinels are always unique and don't include the one we're currently connected to.
|
||||
to.insert(to.end(), gossip_sentinels.begin(), gossip_sentinels.end());
|
||||
|
||||
// Insert any user-supplied sentinels, if not already present.
|
||||
// This is O(n^2), but is okay because n will be small.
|
||||
// The list can't be sorted, anyway
|
||||
for (const auto& sentinel : bootstrap_sentinels) {
|
||||
if (std::find(to.begin(), to.end(), sentinel) == to.end())
|
||||
to.push_back(sentinel);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif
|
||||
@@ -8,20 +8,40 @@
|
||||
#define BOOST_REDIS_SETUP_REQUEST_UTILS_HPP
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/subscription_tracker.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)
|
||||
inline void compose_setup_request(
|
||||
const config& cfg,
|
||||
const subscription_tracker& pubsub_st,
|
||||
request& req)
|
||||
{
|
||||
if (!cfg.use_setup) {
|
||||
// Clear any previous contents
|
||||
req.clear();
|
||||
|
||||
// Set the appropriate flags
|
||||
request_access::set_priority(req, true);
|
||||
req.get_config().cancel_if_unresponded = true;
|
||||
req.get_config().cancel_on_connection_lost = true;
|
||||
|
||||
if (cfg.use_setup) {
|
||||
// We should use the provided request as-is
|
||||
req.append(cfg.setup);
|
||||
} else {
|
||||
// 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?
|
||||
// Don't send AUTH if the user is the default and the password is empty.
|
||||
@@ -46,14 +66,68 @@ inline void compose_setup_request(config& cfg)
|
||||
req.push("SELECT", cfg.database_index.value());
|
||||
}
|
||||
|
||||
// In any case, the setup request should have the priority
|
||||
// flag set so it's executed before any other request.
|
||||
// The setup request should never be retried.
|
||||
request_access::set_priority(cfg.setup, true);
|
||||
cfg.setup.get_config().cancel_if_unresponded = true;
|
||||
cfg.setup.get_config().cancel_on_connection_lost = true;
|
||||
// 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");
|
||||
|
||||
// Add any subscription commands require to restore the PubSub state
|
||||
pubsub_st.compose_subscribe_request(req);
|
||||
}
|
||||
|
||||
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.
|
||||
// ROLE may be followed by subscribe requests, but these don't expect any response.
|
||||
if (use_sentinel(st_->cfg) && response_idx_ == st_->setup_req.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
|
||||
|
||||
44
include/boost/redis/impl/subscription_tracker.ipp
Normal file
44
include/boost/redis/impl/subscription_tracker.ipp
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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/subscription_tracker.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
void subscription_tracker::clear()
|
||||
{
|
||||
channels_.clear();
|
||||
pchannels_.clear();
|
||||
}
|
||||
|
||||
void subscription_tracker::commit_changes(const request& req)
|
||||
{
|
||||
for (const auto& ch : request_access::pubsub_changes(req)) {
|
||||
std::string channel{req.payload().substr(ch.channel_offset, ch.channel_size)};
|
||||
switch (ch.type) {
|
||||
case pubsub_change_type::subscribe: channels_.insert(std::move(channel)); break;
|
||||
case pubsub_change_type::unsubscribe: channels_.erase(std::move(channel)); break;
|
||||
case pubsub_change_type::psubscribe: pchannels_.insert(std::move(channel)); break;
|
||||
case pubsub_change_type::punsubscribe: pchannels_.erase(std::move(channel)); break;
|
||||
default: BOOST_ASSERT(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subscription_tracker::compose_subscribe_request(request& to) const
|
||||
{
|
||||
to.push_range("SUBSCRIBE", channels_);
|
||||
to.push_range("PSUBSCRIBE", pchannels_);
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
@@ -52,7 +52,12 @@ enum class operation
|
||||
/// Refers to `connection::async_run` operations.
|
||||
run,
|
||||
|
||||
/// Refers to `connection::async_receive` operations.
|
||||
/**
|
||||
* @brief (Deprecated) Refers to `async_receive` and `async_receive2` operations.
|
||||
*
|
||||
* To cancel `async_receive2`, use either @ref basic_connection::cancel with no arguments
|
||||
* or per-operation cancellation.
|
||||
*/
|
||||
receive,
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
// NOTE: For some commands like hset it would be a good idea to assert
|
||||
// the value type is a pair.
|
||||
@@ -22,6 +25,21 @@ namespace boost::redis {
|
||||
namespace detail {
|
||||
auto has_response(std::string_view cmd) -> bool;
|
||||
struct request_access;
|
||||
|
||||
enum class pubsub_change_type
|
||||
{
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
psubscribe,
|
||||
punsubscribe,
|
||||
};
|
||||
|
||||
struct pubsub_change {
|
||||
pubsub_change_type type;
|
||||
std::size_t channel_offset;
|
||||
std::size_t channel_size;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/** @brief Represents a Redis request.
|
||||
@@ -34,11 +52,9 @@ struct request_access;
|
||||
*
|
||||
* @code
|
||||
* request r;
|
||||
* r.push("HELLO", 3);
|
||||
* r.push("FLUSHALL");
|
||||
* r.push("PING");
|
||||
* r.push("PING", "key");
|
||||
* r.push("QUIT");
|
||||
* r.push("SET", "k1", "some_value");
|
||||
* r.push("SET", "k2", "other_value");
|
||||
* r.push("GET", "k3");
|
||||
* @endcode
|
||||
*
|
||||
* Uses a `std::string` for internal storage.
|
||||
@@ -126,6 +142,7 @@ public:
|
||||
void clear()
|
||||
{
|
||||
payload_.clear();
|
||||
pubsub_changes_.clear();
|
||||
commands_ = 0;
|
||||
expected_responses_ = 0;
|
||||
has_hello_priority_ = false;
|
||||
@@ -146,14 +163,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`
|
||||
* or support the `boost_redis_to_bulk` function.
|
||||
* Command arguments should either be convertible to `std::string_view`,
|
||||
* integral types, 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:
|
||||
*
|
||||
@@ -165,7 +182,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. Non-string types will be converted to string by calling `boost_redis_to_bulk` on each argument.
|
||||
* @param args Command arguments. `args` is allowed to be empty.
|
||||
* @tparam Ts Types of the command arguments.
|
||||
*
|
||||
*/
|
||||
@@ -196,21 +213,36 @@ public:
|
||||
* req.push_range("HSET", "key", map.cbegin(), map.cend());
|
||||
* @endcode
|
||||
*
|
||||
* 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:
|
||||
* This will generate the following command:
|
||||
*
|
||||
* @code
|
||||
* void boost_redis_to_bulk(std::string& to, T const& t);
|
||||
* HSET key key1 value1 key2 value2 key3 value3
|
||||
* @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 is convertible to `std::string_view`
|
||||
* or supports `boost_redis_to_bulk`.
|
||||
* @tparam ForwardIterator A forward iterator with an element type that supports one of the points above.
|
||||
*
|
||||
* See cpp20_serialization.cpp
|
||||
*/
|
||||
@@ -245,27 +277,42 @@ public:
|
||||
* of arguments and don't have a key. For example:
|
||||
*
|
||||
* @code
|
||||
* std::set<std::string> channels
|
||||
* { "channel1" , "channel2" , "channel3" };
|
||||
* std::set<std::string> keys
|
||||
* { "key1" , "key2" , "key3" };
|
||||
*
|
||||
* request req;
|
||||
* req.push("SUBSCRIBE", std::cbegin(channels), std::cend(channels));
|
||||
* req.push("MGET", keys.begin(), keys.end());
|
||||
* @endcode
|
||||
*
|
||||
* 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:
|
||||
* This will generate the following command:
|
||||
*
|
||||
* @code
|
||||
* void boost_redis_to_bulk(std::string& to, T const& t);
|
||||
* MGET key1 key2 key3
|
||||
* @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 is convertible to `std::string_view`
|
||||
* or supports `boost_redis_to_bulk`.
|
||||
* @tparam ForwardIterator A forward iterator with an element type that supports one of the points above.
|
||||
*
|
||||
* See cpp20_serialization.cpp
|
||||
*/
|
||||
@@ -296,13 +343,31 @@ 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. The range elements should be convertible to `std::string_view`
|
||||
* or support `boost_redis_to_bulk`.
|
||||
* iterators.
|
||||
*/
|
||||
template <class Range>
|
||||
void push_range(
|
||||
@@ -320,12 +385,30 @@ 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. The range elements should be convertible to `std::string_view`
|
||||
* or support `boost_redis_to_bulk`.
|
||||
* iterators.
|
||||
*/
|
||||
template <class Range>
|
||||
void push_range(
|
||||
@@ -338,6 +421,309 @@ public:
|
||||
push_range(cmd, cbegin(range), cend(range));
|
||||
}
|
||||
|
||||
/** @brief Appends the commands in another request to the end of the request.
|
||||
*
|
||||
* Appends all the commands contained in `other` to the end of
|
||||
* this request. Configuration flags in `*this`,
|
||||
* like @ref config::cancel_if_unresponded, are *not* modified,
|
||||
* even if `other` has a different config than `*this`.
|
||||
*
|
||||
* @param other The request containing the commands to append.
|
||||
*/
|
||||
void append(const request& other);
|
||||
|
||||
/**
|
||||
* @brief Appends a SUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `{"ch1", "ch2"}`, the resulting command
|
||||
* is `SUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void subscribe(std::initializer_list<std::string_view> channels)
|
||||
{
|
||||
subscribe(channels.begin(), channels.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a SUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `SUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void subscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr)
|
||||
{
|
||||
subscribe(std::cbegin(channels), std::cend(channels));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a SUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`channels_begin`, `channels_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `SUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void subscribe(ForwardIt channels_begin, ForwardIt channels_end)
|
||||
{
|
||||
push_pubsub("SUBSCRIBE", detail::pubsub_change_type::subscribe, channels_begin, channels_end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends an UNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `{"ch1", "ch2"}`, the resulting command
|
||||
* is `UNSUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void unsubscribe(std::initializer_list<std::string_view> channels)
|
||||
{
|
||||
unsubscribe(channels.begin(), channels.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends an UNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `channels` contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `UNSUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void unsubscribe(Range&& channels, decltype(std::cbegin(channels))* = nullptr)
|
||||
{
|
||||
unsubscribe(std::cbegin(channels), std::cend(channels));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends an UNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`channels_begin`, `channels_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["ch1", "ch2"]`, the resulting command
|
||||
* is `UNSUBSCRIBE ch1 ch2`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void unsubscribe(ForwardIt channels_begin, ForwardIt channels_end)
|
||||
{
|
||||
push_pubsub(
|
||||
"UNSUBSCRIBE",
|
||||
detail::pubsub_change_type::unsubscribe,
|
||||
channels_begin,
|
||||
channels_end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `{"news.*", "events.*"}`, the resulting command
|
||||
* is `PSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void psubscribe(std::initializer_list<std::string_view> patterns)
|
||||
{
|
||||
psubscribe(patterns.begin(), patterns.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void psubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr)
|
||||
{
|
||||
psubscribe(std::cbegin(patterns), std::cend(patterns));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`patterns_begin`, `patterns_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions created using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void psubscribe(ForwardIt patterns_begin, ForwardIt patterns_end)
|
||||
{
|
||||
push_pubsub(
|
||||
"PSUBSCRIBE",
|
||||
detail::pubsub_change_type::psubscribe,
|
||||
patterns_begin,
|
||||
patterns_end);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PUNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `{"news.*", "events.*"}`, the resulting command
|
||||
* is `PUNSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
void punsubscribe(std::initializer_list<std::string_view> patterns)
|
||||
{
|
||||
punsubscribe(patterns.begin(), patterns.end());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PUNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* If `patterns` contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PUNSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class Range>
|
||||
void punsubscribe(Range&& patterns, decltype(std::cbegin(patterns))* = nullptr)
|
||||
{
|
||||
punsubscribe(std::cbegin(patterns), std::cend(patterns));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Appends a PUNSUBSCRIBE command to the end of the request.
|
||||
*
|
||||
* [`patterns_begin`, `patterns_end`) should point to a valid
|
||||
* range of elements convertible to `std::string_view`.
|
||||
* If the range contains `["news.*", "events.*"]`, the resulting command
|
||||
* is `PUNSUBSCRIBE news.* events.*`.
|
||||
*
|
||||
* Subscriptions removed using this function are tracked
|
||||
* to enable PubSub state restoration. After successfully executing
|
||||
* the request, the connection will store any newly subscribed channels and patterns.
|
||||
* Every time a reconnection happens,
|
||||
* a suitable `SUBSCRIBE`/`PSUBSCRIBE` command is issued automatically,
|
||||
* to restore the subscriptions that were active before the reconnection.
|
||||
*
|
||||
* PubSub store restoration only happens when using @ref subscribe,
|
||||
* @ref unsubscribe, @ref psubscribe or @ref punsubscribe.
|
||||
* Subscription commands added by @ref push or @ref push_range are not tracked.
|
||||
*/
|
||||
template <class ForwardIt>
|
||||
void punsubscribe(ForwardIt patterns_begin, ForwardIt patterns_end)
|
||||
{
|
||||
push_pubsub(
|
||||
"PUNSUBSCRIBE",
|
||||
detail::pubsub_change_type::punsubscribe,
|
||||
patterns_begin,
|
||||
patterns_end);
|
||||
}
|
||||
|
||||
private:
|
||||
void check_cmd(std::string_view cmd)
|
||||
{
|
||||
@@ -355,6 +741,35 @@ private:
|
||||
std::size_t commands_ = 0;
|
||||
std::size_t expected_responses_ = 0;
|
||||
bool has_hello_priority_ = false;
|
||||
std::vector<detail::pubsub_change> pubsub_changes_{};
|
||||
|
||||
void add_pubsub_arg(detail::pubsub_change_type type, std::string_view value);
|
||||
|
||||
template <class ForwardIt>
|
||||
void push_pubsub(
|
||||
std::string_view cmd,
|
||||
detail::pubsub_change_type type,
|
||||
ForwardIt channels_begin,
|
||||
ForwardIt channels_end)
|
||||
{
|
||||
static_assert(
|
||||
std::is_convertible_v<
|
||||
typename std::iterator_traits<ForwardIt>::value_type,
|
||||
std::string_view>,
|
||||
"subscribe, psubscribe, unsubscribe and punsubscribe should be passed ranges of elements "
|
||||
"convertible to std::string_view");
|
||||
if (channels_begin == channels_end)
|
||||
return;
|
||||
|
||||
auto const distance = std::distance(channels_begin, channels_end);
|
||||
resp3::add_header(payload_, resp3::type::array, 1 + distance);
|
||||
resp3::add_bulk(payload_, cmd);
|
||||
|
||||
for (; channels_begin != channels_end; ++channels_begin)
|
||||
add_pubsub_arg(type, *channels_begin);
|
||||
|
||||
++commands_; // these commands don't have a response
|
||||
}
|
||||
|
||||
friend struct detail::request_access;
|
||||
};
|
||||
@@ -364,6 +779,10 @@ namespace detail {
|
||||
struct request_access {
|
||||
inline static void set_priority(request& r, bool value) { r.has_hello_priority_ = value; }
|
||||
inline static bool has_priority(const request& r) { return r.has_hello_priority_; }
|
||||
inline static const std::vector<detail::pubsub_change>& pubsub_changes(const request& r)
|
||||
{
|
||||
return r.pubsub_changes_;
|
||||
}
|
||||
};
|
||||
|
||||
// Creates a HELLO 3 request
|
||||
|
||||
403
include/boost/redis/resp3/flat_tree.hpp
Normal file
403
include/boost/redis/resp3/flat_tree.hpp
Normal file
@@ -0,0 +1,403 @@
|
||||
//
|
||||
// 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 <boost/core/span.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <iterator>
|
||||
#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. Once populated, they can be used as a const range
|
||||
* of @ref resp3::node_view objects. The usual random access range methods (like @ref at, @ref size or
|
||||
* @ref front) are provided. Once populated, `flat_tree` can't be modified,
|
||||
* except for @ref clear and assignment.
|
||||
*
|
||||
* `flat_tree` models `std::ranges::contiguous_range`.
|
||||
*
|
||||
* 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 The type of the iterators returned by @ref begin and @ref end.
|
||||
*
|
||||
* It is guaranteed to be a contiguous iterator. While this is currently a pointer,
|
||||
* users shouldn't rely on this fact, as the exact implementation may change between releases.
|
||||
*/
|
||||
using iterator = const node_view*;
|
||||
|
||||
/**
|
||||
* @brief The type of the iterators returned by @ref rbegin and @ref rend.
|
||||
*
|
||||
* As with @ref iterator, users should treat this type as an unspecified
|
||||
* contiguous iterator type rather than assuming a specific type.
|
||||
*/
|
||||
using reverse_iterator = std::reverse_iterator<iterator>;
|
||||
|
||||
/**
|
||||
* @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
|
||||
* Iterators, pointers and 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
|
||||
* Iterators, pointers and references to the nodes and strings in `other` remain valid.
|
||||
* Iterators, pointers and 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
|
||||
* Iterators, pointers and 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);
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator to the first element of the node range.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator to the first node.
|
||||
*/
|
||||
iterator begin() const noexcept { return data(); }
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator past the last element in the node range.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator past the last element in the node range.
|
||||
*/
|
||||
iterator end() const noexcept { return data() + size(); }
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator to the first element of the reversed node range.
|
||||
*
|
||||
* Allows iterating the range of nodes in reverse order.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator to the first node of the reversed range.
|
||||
*/
|
||||
reverse_iterator rbegin() const noexcept { return reverse_iterator{end()}; }
|
||||
|
||||
/**
|
||||
* @brief Returns an iterator past the last element of the reversed node range.
|
||||
*
|
||||
* Allows iterating the range of nodes in reverse order.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns An iterator past the last element of the reversed node range.
|
||||
*/
|
||||
reverse_iterator rend() const noexcept { return reverse_iterator{begin()}; }
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the node at the specified position (checked access).
|
||||
*
|
||||
* @par Exception safety
|
||||
* Strong guarantee. Throws `std::out_of_range` if `i >= size()`.
|
||||
*
|
||||
* @param i Position of the node to return.
|
||||
* @returns A reference to the node at position `i`.
|
||||
*/
|
||||
const node_view& at(std::size_t i) const;
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the node at the specified position (unchecked access).
|
||||
*
|
||||
* @par Precondition
|
||||
* `i < size()`.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @param i Position of the node to return.
|
||||
* @returns A reference to the node at position `i`.
|
||||
*/
|
||||
const node_view& operator[](std::size_t i) const noexcept { return get_view()[i]; }
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the first node.
|
||||
*
|
||||
* @par Precondition
|
||||
* `!empty()`.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns A reference to the first node.
|
||||
*/
|
||||
const node_view& front() const noexcept { return get_view().front(); }
|
||||
|
||||
/**
|
||||
* @brief Returns a reference to the last node.
|
||||
*
|
||||
* @par Precondition
|
||||
* `!empty()`.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns A reference to the last node.
|
||||
*/
|
||||
const node_view& back() const noexcept { return get_view().back(); }
|
||||
|
||||
/**
|
||||
* @brief Returns a pointer to the underlying node storage.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns A pointer to the underlying node array.
|
||||
*/
|
||||
const node_view* data() const noexcept { return view_tree_.data(); }
|
||||
|
||||
/**
|
||||
* @brief Checks whether the tree is empty.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns `true` if the tree contains no nodes, `false` otherwise.
|
||||
*/
|
||||
bool empty() const noexcept { return size() == 0u; }
|
||||
|
||||
/**
|
||||
* @brief Returns the number of nodes in the tree.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The number of nodes.
|
||||
*/
|
||||
std::size_t size() const noexcept { return node_tmp_offset_; }
|
||||
|
||||
/** @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
|
||||
* the range contain no nodes, 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_tmp_offset_; }
|
||||
|
||||
/** @brief Returns the capacity of the node container.
|
||||
*
|
||||
* @par Exception safety
|
||||
* No-throw guarantee.
|
||||
*
|
||||
* @returns The capacity of the object, in number of nodes.
|
||||
*/
|
||||
auto capacity() const noexcept -> std::size_t { return view_tree_.capacity(); }
|
||||
|
||||
/** @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 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;
|
||||
|
||||
span<const node_view> get_view() const noexcept { return {data(), size()}; }
|
||||
void notify_init();
|
||||
void notify_done();
|
||||
|
||||
// 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;
|
||||
|
||||
// flat_tree supports a "temporary working area" for incrementally reading messages.
|
||||
// Nodes in the tmp area are not part of the object representation until they
|
||||
// are committed with notify_done().
|
||||
// These offsets delimit this area.
|
||||
std::size_t node_tmp_offset_ = 0u;
|
||||
std::size_t data_tmp_offset_ = 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
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <ostream>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
auto to_string(type t) noexcept -> char const*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
/** @brief A node in the response tree.
|
||||
@@ -43,7 +47,7 @@ struct basic_node {
|
||||
* @param b Right hand side node object.
|
||||
*/
|
||||
template <class String>
|
||||
auto operator==(basic_node<String> const& a, basic_node<String> const& b)
|
||||
bool operator==(basic_node<String> const& a, basic_node<String> const& b)
|
||||
{
|
||||
// clang-format off
|
||||
return a.aggregate_size == b.aggregate_size
|
||||
@@ -53,6 +57,18 @@ auto 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>;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
#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.
|
||||
@@ -35,6 +32,10 @@ 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`.
|
||||
*/
|
||||
@@ -100,6 +101,11 @@ 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);
|
||||
|
||||
|
||||
29
include/boost/redis/resp3/tree.hpp
Normal file
29
include/boost/redis/resp3/tree.hpp
Normal file
@@ -0,0 +1,29 @@
|
||||
/* 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
|
||||
@@ -9,9 +9,8 @@
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <iosfwd>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
|
||||
@@ -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,9 +29,12 @@ 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<std::vector<resp3::node>>;
|
||||
using generic_response = adapter::result<resp3::tree>;
|
||||
|
||||
/** @brief Consume on response from a generic response
|
||||
/// 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
|
||||
*
|
||||
* This function rotates the elements so that the start of the next
|
||||
* response becomes the new front element. For example the output of
|
||||
@@ -73,7 +76,11 @@ using generic_response = adapter::result<std::vector<resp3::node>>;
|
||||
BOOST_DEPRECATED("This function is not needed anymore to consume server pushes.")
|
||||
void consume_one(generic_response& r, system::error_code& ec);
|
||||
|
||||
/// @copydoc consume_one
|
||||
/**
|
||||
* @brief (Deprecated) 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);
|
||||
|
||||
|
||||
@@ -8,14 +8,19 @@
|
||||
#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/flat_tree.ipp>
|
||||
#include <boost/redis/impl/ignore.ipp>
|
||||
#include <boost/redis/impl/logger.ipp>
|
||||
#include <boost/redis/impl/multiplexer.ipp>
|
||||
#include <boost/redis/impl/read_buffer.ipp>
|
||||
#include <boost/redis/impl/reader_fsm.ipp>
|
||||
#include <boost/redis/impl/receive_fsm.ipp>
|
||||
#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/subscription_tracker.ipp>
|
||||
#include <boost/redis/impl/writer_fsm.ipp>
|
||||
#include <boost/redis/resp3/impl/parser.ipp>
|
||||
#include <boost/redis/resp3/impl/serialization.ipp>
|
||||
|
||||
@@ -7,7 +7,7 @@ if (MSVC)
|
||||
target_compile_options(boost_redis_project_options INTERFACE /bigobj /W4 /WX /wd4459)
|
||||
target_compile_definitions(boost_redis_project_options INTERFACE _WIN32_WINNT=0x0601 _CRT_SECURE_NO_WARNINGS=1)
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
target_compile_options(boost_redis_project_options INTERFACE -Wall -Wextra -Werror -Wno-deprecated-declarations)
|
||||
target_compile_options(boost_redis_project_options INTERFACE -Wall -Wextra -Werror)
|
||||
endif()
|
||||
|
||||
add_library(boost_redis_src STATIC boost_redis.cpp)
|
||||
@@ -35,17 +35,28 @@ 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_receive_fsm)
|
||||
make_test(test_run_fsm)
|
||||
make_test(test_setup_request_utils)
|
||||
make_test(test_compose_setup_request)
|
||||
make_test(test_setup_adapter)
|
||||
make_test(test_multiplexer)
|
||||
make_test(test_parse_sentinel_response)
|
||||
make_test(test_update_sentinel_list)
|
||||
make_test(test_flat_tree)
|
||||
make_test(test_generic_flat_response)
|
||||
make_test(test_read_buffer)
|
||||
make_test(test_subscription_tracker)
|
||||
|
||||
# Tests that require a real Redis server
|
||||
make_test(test_conn_quit)
|
||||
@@ -56,6 +67,7 @@ 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)
|
||||
@@ -67,6 +79,7 @@ make_test(test_conversions)
|
||||
make_test(test_conn_tls)
|
||||
make_test(test_unix_sockets)
|
||||
make_test(test_conn_cancel_after)
|
||||
make_test(test_conn_sentinel)
|
||||
|
||||
# Coverage
|
||||
set(
|
||||
|
||||
15
test/Jamfile
15
test/Jamfile
@@ -52,17 +52,28 @@ 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_receive_fsm
|
||||
test_run_fsm
|
||||
test_connect_fsm
|
||||
test_setup_request_utils
|
||||
test_compose_setup_request
|
||||
test_setup_adapter
|
||||
test_multiplexer
|
||||
test_parse_sentinel_response
|
||||
test_update_sentinel_list
|
||||
test_flat_tree
|
||||
test_generic_flat_response
|
||||
test_read_buffer
|
||||
test_subscription_tracker
|
||||
;
|
||||
|
||||
# Build and run the tests
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
#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 <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
struct run_callback {
|
||||
std::shared_ptr<boost::redis::connection> conn;
|
||||
@@ -50,6 +57,7 @@ boost::redis::config make_test_config()
|
||||
{
|
||||
boost::redis::config cfg;
|
||||
cfg.addr.host = get_server_hostname();
|
||||
cfg.reconnect_wait_interval = 50ms; // make tests involving reconnection faster
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@@ -71,7 +79,6 @@ void run_coroutine_test(net::awaitable<void> op, std::chrono::steady_clock::dura
|
||||
|
||||
// Finds a value in the output of the CLIENT INFO command
|
||||
// format: key1=value1 key2=value2
|
||||
// TODO: duplicated
|
||||
std::string_view find_client_info(std::string_view client_info, std::string_view key)
|
||||
{
|
||||
std::string prefix{key};
|
||||
@@ -84,3 +91,45 @@ std::string_view find_client_info(std::string_view client_info, std::string_view
|
||||
auto const pos_end = client_info.find(' ', pos_begin);
|
||||
return client_info.substr(pos_begin, pos_end - pos_begin);
|
||||
}
|
||||
|
||||
void create_user(std::string_view port, std::string_view username, std::string_view password)
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
boost::redis::connection conn{ioc};
|
||||
|
||||
boost::redis::config cfg;
|
||||
cfg.addr.port = port;
|
||||
|
||||
// Enable the user and grant them permissions on everything
|
||||
boost::redis::request req;
|
||||
req.push("ACL", "SETUSER", username, "on", ">" + std::string(password), "~*", "&*", "+@all");
|
||||
|
||||
bool run_finished = false, exec_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](boost::system::error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
conn.async_exec(req, boost::redis::ignore, [&](boost::system::error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, boost::system::error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
BOOST_TEST(exec_finished);
|
||||
}
|
||||
|
||||
boost::redis::logger make_string_logger(std::string& to)
|
||||
{
|
||||
return {
|
||||
boost::redis::logger::level::info,
|
||||
[&to](boost::redis::logger::level, std::string_view msg) {
|
||||
to += msg;
|
||||
to += '\n';
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/detail/reader_fsm.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/operation.hpp>
|
||||
|
||||
#include <boost/asio/awaitable.hpp>
|
||||
@@ -11,6 +12,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
// The timeout for tests involving communication to a real server.
|
||||
@@ -40,3 +42,8 @@ void run(
|
||||
// Finds a value in the output of the CLIENT INFO command
|
||||
// format: key1=value1 key2=value2
|
||||
std::string_view find_client_info(std::string_view client_info, std::string_view key);
|
||||
|
||||
// Connects to the Redis server at the given port and creates a user
|
||||
void create_user(std::string_view port, std::string_view username, std::string_view password);
|
||||
|
||||
boost::redis::logger make_string_logger(std::string& to);
|
||||
|
||||
28
test/print_node.hpp
Normal file
28
test/print_node.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#ifndef BOOST_REDIS_TEST_PRINT_NODE_HPP
|
||||
#define BOOST_REDIS_TEST_PRINT_NODE_HPP
|
||||
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <ostream>
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
template <class String>
|
||||
std::ostream& operator<<(std::ostream& os, basic_node<String> const& nd)
|
||||
{
|
||||
return os << "node{ .data_type=" << to_string(nd.data_type)
|
||||
<< ", .aggregate_size=" << nd.aggregate_size << ", .depth=" << nd.depth
|
||||
<< ", .value=" << nd.value << "}";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::resp3
|
||||
|
||||
#endif // BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
@@ -4,8 +4,10 @@
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/ignore_unused.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
@@ -72,4 +74,24 @@ logger log_fixture::make_logger()
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<resp3::node> nodes_from_resp3(
|
||||
const std::vector<std::string_view>& msgs,
|
||||
source_location loc)
|
||||
{
|
||||
std::vector<resp3::node> nodes;
|
||||
any_adapter adapter{nodes};
|
||||
|
||||
for (std::string_view resp : msgs) {
|
||||
resp3::parser p;
|
||||
system::error_code ec;
|
||||
bool done = resp3::parse(p, resp, adapter, ec);
|
||||
if (!BOOST_TEST(done))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_EQ(ec, system::error_code()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#define BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
|
||||
#include <boost/assert/source_location.hpp>
|
||||
|
||||
@@ -15,6 +16,7 @@
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
@@ -50,6 +52,13 @@ constexpr auto to_milliseconds(std::chrono::steady_clock::duration d)
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(d).count();
|
||||
}
|
||||
|
||||
// Creates a vector of nodes from a set of RESP3 messages.
|
||||
// Using the raw RESP values ensures that the correct
|
||||
// node tree is built, which is not always obvious
|
||||
std::vector<resp3::node> nodes_from_resp3(
|
||||
const std::vector<std::string_view>& msgs,
|
||||
source_location loc = BOOST_CURRENT_LOCATION);
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
#endif // BOOST_REDIS_TEST_SANSIO_UTILS_HPP
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#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;
|
||||
@@ -24,10 +25,12 @@ 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});
|
||||
}
|
||||
|
||||
|
||||
256
test/test_compose_setup_request.cpp
Normal file
256
test/test_compose_setup_request.cpp
Normal file
@@ -0,0 +1,256 @@
|
||||
//
|
||||
// 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/subscription_tracker.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/impl/setup_request_utils.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
using namespace boost::redis;
|
||||
namespace asio = boost::asio;
|
||||
using detail::compose_setup_request;
|
||||
using detail::subscription_tracker;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
struct fixture {
|
||||
subscription_tracker tracker;
|
||||
request out;
|
||||
config cfg;
|
||||
|
||||
void run(std::string_view expected_payload, boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
out.push("PING", "leftover"); // verify that we clear the request
|
||||
|
||||
compose_setup_request(cfg, tracker, out);
|
||||
|
||||
if (!BOOST_TEST_EQ(out.payload(), expected_payload))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST(out.has_hello_priority()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST(out.get_config().cancel_if_unresponded))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST(out.get_config().cancel_on_connection_lost))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
void test_hello()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
|
||||
fix.run("*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n");
|
||||
}
|
||||
|
||||
void test_select()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.database_index = 10;
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*2\r\n$6\r\nSELECT\r\n$2\r\n10\r\n");
|
||||
}
|
||||
|
||||
void test_clientname()
|
||||
{
|
||||
fixture fix;
|
||||
|
||||
fix.run("*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n");
|
||||
}
|
||||
|
||||
void test_auth()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
|
||||
fix.run("*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");
|
||||
}
|
||||
|
||||
void test_auth_empty_password()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.username = "foo";
|
||||
|
||||
fix.run("*5\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$4\r\nAUTH\r\n$3\r\nfoo\r\n$0\r\n\r\n");
|
||||
}
|
||||
|
||||
void test_auth_setname()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "mytest";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
|
||||
fix.run(
|
||||
"*7\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$7\r\nSETNAME\r\n$"
|
||||
"6\r\nmytest\r\n");
|
||||
}
|
||||
|
||||
void test_use_setup()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "mytest";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
fix.cfg.database_index = 4;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.push("SELECT", 8);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n");
|
||||
}
|
||||
|
||||
// Regression check: we set the priority flag
|
||||
void test_use_setup_no_hello()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.clear();
|
||||
fix.cfg.setup.push("SELECT", 8);
|
||||
|
||||
fix.run("*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n");
|
||||
}
|
||||
|
||||
// Regression check: we set the relevant cancellation flags in the request
|
||||
void test_use_setup_flags()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.clear();
|
||||
fix.cfg.setup.push("SELECT", 8);
|
||||
fix.cfg.setup.get_config().cancel_if_unresponded = false;
|
||||
fix.cfg.setup.get_config().cancel_on_connection_lost = false;
|
||||
|
||||
fix.run("*2\r\n$6\r\nSELECT\r\n$1\r\n8\r\n");
|
||||
}
|
||||
|
||||
// If we have tracked subscriptions, these are added at the end
|
||||
void test_tracked_subscriptions()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
|
||||
// Populate the tracker
|
||||
request sub_req;
|
||||
sub_req.subscribe({"ch1", "ch2"});
|
||||
fix.tracker.commit_changes(sub_req);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n");
|
||||
}
|
||||
|
||||
void test_tracked_subscriptions_use_setup()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.clear();
|
||||
fix.cfg.setup.push("PING", "value");
|
||||
|
||||
// Populate the tracker
|
||||
request sub_req;
|
||||
sub_req.subscribe({"ch1", "ch2"});
|
||||
fix.tracker.commit_changes(sub_req);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$4\r\nPING\r\n$5\r\nvalue\r\n"
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n");
|
||||
}
|
||||
|
||||
// When using Sentinel, a ROLE command is added. This works
|
||||
// both with the old HELLO and new setup strategies, and with tracked subscriptions
|
||||
void test_sentinel_auth()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.username = "foo";
|
||||
fix.cfg.password = "bar";
|
||||
|
||||
fix.run(
|
||||
"*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");
|
||||
}
|
||||
|
||||
void test_sentinel_use_setup()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
fix.cfg.use_setup = true;
|
||||
fix.cfg.setup.push("SELECT", 42);
|
||||
|
||||
fix.run(
|
||||
"*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");
|
||||
}
|
||||
|
||||
void test_sentinel_tracked_subscriptions()
|
||||
{
|
||||
fixture fix;
|
||||
fix.cfg.clientname = "";
|
||||
fix.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Populate the tracker
|
||||
request sub_req;
|
||||
sub_req.subscribe({"ch1", "ch2"});
|
||||
fix.tracker.commit_changes(sub_req);
|
||||
|
||||
fix.run(
|
||||
"*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n"
|
||||
"*1\r\n$4\r\nROLE\r\n"
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n");
|
||||
}
|
||||
|
||||
} // 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_tracked_subscriptions();
|
||||
test_tracked_subscriptions_use_setup();
|
||||
test_sentinel_auth();
|
||||
test_sentinel_use_setup();
|
||||
test_sentinel_tracked_subscriptions();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -72,26 +72,47 @@ void test_exec()
|
||||
BOOST_TEST(exec_finished);
|
||||
}
|
||||
|
||||
//template <class Connection>
|
||||
//void test_receive()
|
||||
//{
|
||||
// // Setup
|
||||
// asio::io_context ioc;
|
||||
// Connection conn{ioc};
|
||||
// bool receive_finished = false;
|
||||
// generic_response resp;
|
||||
// conn.set_receive_response(resp);
|
||||
//
|
||||
// // Call the function with a very short timeout.
|
||||
// conn.async_receive(asio::cancel_after(1ms, [&](error_code ec, std::size_t) {
|
||||
// BOOST_TEST_EQ(ec, asio::experimental::channel_errc::channel_cancelled);
|
||||
// receive_finished = true;
|
||||
// }));
|
||||
//
|
||||
// ioc.run_for(test_timeout);
|
||||
//
|
||||
// BOOST_TEST(receive_finished);
|
||||
//}
|
||||
template <class Connection>
|
||||
void test_receive()
|
||||
{
|
||||
// Setup
|
||||
asio::io_context ioc;
|
||||
Connection conn{ioc};
|
||||
bool receive_finished = false;
|
||||
generic_response resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// Call the function with a very short timeout.
|
||||
conn.async_receive(asio::cancel_after(1ms, [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, asio::experimental::channel_errc::channel_cancelled);
|
||||
receive_finished = true;
|
||||
}));
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(receive_finished);
|
||||
}
|
||||
|
||||
template <class Connection>
|
||||
void test_receive2()
|
||||
{
|
||||
// Setup
|
||||
asio::io_context ioc;
|
||||
Connection conn{ioc};
|
||||
bool receive_finished = false;
|
||||
generic_response resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// Call the function with a very short timeout.
|
||||
conn.async_receive2(asio::cancel_after(1ms, [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
|
||||
receive_finished = true;
|
||||
}));
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(receive_finished);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -103,8 +124,11 @@ int main()
|
||||
test_exec<basic_connection<asio::io_context::executor_type>>();
|
||||
test_exec<connection>();
|
||||
|
||||
//test_receive<basic_connection<asio::io_context::executor_type>>();
|
||||
//test_receive<connection>();
|
||||
test_receive<basic_connection<asio::io_context::executor_type>>();
|
||||
test_receive<connection>();
|
||||
|
||||
test_receive2<basic_connection<asio::io_context::executor_type>>();
|
||||
test_receive2<connection>();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ void test_reconnection()
|
||||
// Make the test run faster
|
||||
auto cfg = make_test_config();
|
||||
cfg.health_check_interval = 500ms;
|
||||
cfg.reconnect_wait_interval = 100ms;
|
||||
|
||||
bool run_finished = false, exec1_finished = false, exec2_finished = false;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2022 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -27,7 +27,7 @@ using error_code = boost::system::error_code;
|
||||
using boost::redis::operation;
|
||||
using boost::redis::request;
|
||||
using boost::redis::response;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::resp3::flat_tree;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::redis::logger;
|
||||
@@ -55,17 +55,13 @@ std::ostream& operator<<(std::ostream& os, usage const& u)
|
||||
|
||||
namespace {
|
||||
|
||||
auto
|
||||
receiver(
|
||||
connection& conn,
|
||||
generic_response& resp,
|
||||
std::size_t expected) -> net::awaitable<void>
|
||||
auto receiver(connection& conn, flat_tree& resp, std::size_t expected) -> net::awaitable<void>
|
||||
{
|
||||
std::size_t push_counter = 0;
|
||||
while (push_counter != expected) {
|
||||
co_await conn.async_receive2();
|
||||
push_counter += resp.value().get_total_msgs();
|
||||
resp.value().clear();
|
||||
push_counter += resp.get_total_msgs();
|
||||
resp.clear();
|
||||
}
|
||||
|
||||
conn.cancel();
|
||||
@@ -116,7 +112,7 @@ BOOST_AUTO_TEST_CASE(echo_stress)
|
||||
// This is the total number of pushes we will receive.
|
||||
constexpr std::size_t total_pushes = sessions * msgs * n_pubs + 1;
|
||||
|
||||
generic_response resp;
|
||||
flat_tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
request const pub_req = make_pub_req(n_pubs);
|
||||
@@ -135,7 +131,7 @@ BOOST_AUTO_TEST_CASE(echo_stress)
|
||||
|
||||
// Subscribe, then launch the coroutines
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
req.subscribe({"channel"});
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
subscribe_finished = true;
|
||||
BOOST_TEST(ec == error_code());
|
||||
@@ -150,13 +146,11 @@ 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.value().get_reallocs()
|
||||
<< std::endl;
|
||||
std::cout << "-------------------\n"
|
||||
<< "Usage data: \n"
|
||||
<< conn.get_usage() << "\n"
|
||||
<< "-------------------\n"
|
||||
<< "Reallocations: " << resp.get_reallocs() << std::endl;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/detached.hpp>
|
||||
|
||||
@@ -180,4 +181,31 @@ BOOST_AUTO_TEST_CASE(exec_any_adapter)
|
||||
BOOST_TEST(std::get<0>(res).value() == "PONG");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(exec_generic_flat_response)
|
||||
{
|
||||
// Executing with a generic_flat_response works
|
||||
request req;
|
||||
req.push("PING", "PONG");
|
||||
boost::redis::generic_flat_response resp;
|
||||
|
||||
net::io_context ioc;
|
||||
|
||||
auto conn = std::make_shared<connection>(ioc);
|
||||
|
||||
bool finished = false;
|
||||
|
||||
conn->async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST(ec == error_code());
|
||||
conn->cancel();
|
||||
finished = true;
|
||||
});
|
||||
|
||||
run(conn);
|
||||
ioc.run_for(test_timeout);
|
||||
BOOST_TEST_REQUIRE(finished);
|
||||
|
||||
BOOST_TEST(resp.has_value());
|
||||
BOOST_TEST(resp.value().front().value == "PONG");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -5,246 +5,304 @@
|
||||
*/
|
||||
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/flat_tree.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/error.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 <boost/asio/io_context.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
|
||||
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::generic_response;
|
||||
using boost::redis::ignore;
|
||||
using boost::redis::ignore_t;
|
||||
using boost::system::error_code;
|
||||
using boost::redis::logger;
|
||||
using namespace boost::redis;
|
||||
using namespace std::chrono_literals;
|
||||
using boost::system::error_code;
|
||||
|
||||
// Focuses on the deprecated async_receive and receive
|
||||
// functions. test_conn_push2 covers the newer receive functionality.
|
||||
|
||||
namespace {
|
||||
|
||||
BOOST_AUTO_TEST_CASE(receives_push_waiting_resps)
|
||||
// async_receive is outstanding when a push is received
|
||||
void test_async_receive_waiting_for_push()
|
||||
{
|
||||
resp3::flat_tree resp;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
request req1;
|
||||
req1.push("HELLO", 3);
|
||||
req1.push("PING", "Message1");
|
||||
req1.push("SUBSCRIBE", "test_async_receive_waiting_for_push");
|
||||
|
||||
request req2;
|
||||
req2.push("SUBSCRIBE", "channel");
|
||||
req2.push("PING", "Message2");
|
||||
|
||||
request req3;
|
||||
req3.push("PING", "Message2");
|
||||
req3.push("QUIT");
|
||||
bool run_finished = false, push_received = false, exec1_finished = false, exec2_finished = false;
|
||||
|
||||
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 on_exec2 = [&](error_code ec2, std::size_t) {
|
||||
BOOST_TEST_EQ(ec2, error_code());
|
||||
exec2_finished = true;
|
||||
conn.cancel();
|
||||
};
|
||||
|
||||
auto c2 = [&, conn](error_code ec, std::size_t) {
|
||||
c2_called = true;
|
||||
BOOST_TEST(ec == error_code());
|
||||
conn->async_exec(req3, ignore, c3);
|
||||
};
|
||||
conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
exec1_finished = true;
|
||||
});
|
||||
|
||||
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());
|
||||
conn.async_receive([&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
push_received = true;
|
||||
conn->cancel();
|
||||
conn.async_exec(req2, ignore, on_exec2);
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
generic_response 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.value().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);
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
run_finished = true;
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(push_received);
|
||||
BOOST_TEST(exec1_finished);
|
||||
BOOST_TEST(exec2_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// A push is already available when async_receive is called
|
||||
void test_async_receive_push_available()
|
||||
{
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// SUBSCRIBE doesn't have a response, but causes a push to be delivered.
|
||||
// Add a PING so the overall request has a response.
|
||||
// This ensures that when async_exec completes, the push has been delivered
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "test_async_receive_push_available");
|
||||
req.push("PING", "message");
|
||||
|
||||
bool push_received = false, exec_finished = false, run_finished = false;
|
||||
|
||||
auto on_receive = [&](error_code ec, std::size_t) {
|
||||
push_received = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
conn.cancel();
|
||||
};
|
||||
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_receive(on_receive);
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&](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(push_received);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// Synchronous receive can be used to try to read a message
|
||||
void test_sync_receive()
|
||||
{
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// Subscribing to 2 channels causes 2 pushes to be delivered.
|
||||
// Adding a PING guarantees that after exec finishes, the push has been read
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "test_sync_receive_channel1");
|
||||
req.push("SUBSCRIBE", "test_sync_receive_channel2");
|
||||
req.push("PING", "message");
|
||||
|
||||
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());
|
||||
|
||||
// At this point, the receive response contains all the pushes
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 2u);
|
||||
|
||||
// Receive the 1st push synchronously
|
||||
std::size_t push_bytes = conn.receive(ec);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_GT(push_bytes, 0u);
|
||||
|
||||
// Receive the 2nd push synchronously
|
||||
push_bytes = conn.receive(ec);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_GT(push_bytes, 0u);
|
||||
|
||||
// There are no more pushes. Trying to receive one more fails
|
||||
push_bytes = conn.receive(ec);
|
||||
BOOST_TEST_EQ(ec, error::sync_receive_push_failed);
|
||||
BOOST_TEST_EQ(push_bytes, 0u);
|
||||
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
run_finished = true;
|
||||
});
|
||||
|
||||
// Trying to receive a push before one is received fails
|
||||
error_code ec;
|
||||
std::size_t push_bytes = conn.receive(ec);
|
||||
BOOST_TEST_EQ(ec, error::sync_receive_push_failed);
|
||||
BOOST_TEST_EQ(push_bytes, 0u);
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
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)
|
||||
// async_receive is cancelled every time a reconnection happens,
|
||||
// so we can re-establish subscriptions
|
||||
struct test_async_receive_cancelled_on_reconnection_impl {
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp{};
|
||||
request req_subscribe{}, req_quit{};
|
||||
bool receive_finished = false, quit_finished = false;
|
||||
|
||||
// Subscribe to a channel. This will cause a push to be received
|
||||
void start_subscribe1()
|
||||
{
|
||||
conn.async_exec(req_subscribe, ignore, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
start_receive1();
|
||||
});
|
||||
}
|
||||
|
||||
// Receive the push triggered by the subscribe
|
||||
void start_receive1()
|
||||
{
|
||||
conn.async_receive([this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
resp.clear();
|
||||
|
||||
// In parallel, trigger a reconnection and start a receive operation
|
||||
start_receive_reconnection();
|
||||
start_quit();
|
||||
});
|
||||
}
|
||||
|
||||
// The next receive operation will be cancelled by the reconnection
|
||||
void start_receive_reconnection()
|
||||
{
|
||||
conn.async_receive([this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, net::experimental::channel_errc::channel_cancelled);
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 0u);
|
||||
start_subscribe2();
|
||||
});
|
||||
}
|
||||
|
||||
// Trigger a reconnection. This is a "leaf" operation
|
||||
void start_quit()
|
||||
{
|
||||
conn.async_exec(req_quit, ignore, [this](error_code, std::size_t) {
|
||||
quit_finished = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Resubscribe after the reconnection
|
||||
void start_subscribe2()
|
||||
{
|
||||
conn.async_exec(req_subscribe, ignore, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
start_receive2();
|
||||
});
|
||||
}
|
||||
|
||||
// Receive the push triggered by the 2nd subscribe
|
||||
void start_receive2()
|
||||
{
|
||||
conn.async_receive([this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
receive_finished = true;
|
||||
conn.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
req_subscribe.push("SUBSCRIBE", "test_async_receive_cancelled_on_reconnection");
|
||||
req_subscribe.push("PING");
|
||||
|
||||
req_quit.push("QUIT");
|
||||
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
bool run_finished = false;
|
||||
|
||||
start_subscribe1();
|
||||
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
BOOST_TEST(receive_finished);
|
||||
BOOST_TEST(quit_finished);
|
||||
}
|
||||
};
|
||||
|
||||
void test_async_receive_cancelled_on_reconnection()
|
||||
{
|
||||
conn->async_receive2([conn](error_code ec) {
|
||||
if (ec) {
|
||||
BOOST_TEST(ec == net::experimental::error::channel_cancelled);
|
||||
return;
|
||||
}
|
||||
launch_push_consumer(conn);
|
||||
});
|
||||
test_async_receive_cancelled_on_reconnection_impl{}.run();
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(many_subscribers)
|
||||
// After an async_receive operation finishes, another one can be issued
|
||||
void test_consecutive_receives()
|
||||
{
|
||||
request req0;
|
||||
req0.get_config().cancel_on_connection_lost = false;
|
||||
req0.push("HELLO", 3);
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp;
|
||||
bool push_consumer_finished{false};
|
||||
|
||||
std::function<void()> launch_push_consumer = [&]() {
|
||||
conn.async_receive([&](error_code ec, std::size_t) {
|
||||
if (ec) {
|
||||
BOOST_TEST_EQ(ec, net::experimental::error::channel_cancelled);
|
||||
push_consumer_finished = true;
|
||||
resp.clear();
|
||||
return;
|
||||
}
|
||||
launch_push_consumer();
|
||||
});
|
||||
};
|
||||
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
request req1;
|
||||
req1.get_config().cancel_on_connection_lost = false;
|
||||
@@ -254,139 +312,74 @@ BOOST_AUTO_TEST_CASE(many_subscribers)
|
||||
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");
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
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");
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
exec_finished = true;
|
||||
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 c9 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c10);
|
||||
};
|
||||
auto c8 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req1, ignore, c9);
|
||||
};
|
||||
auto c7 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c8);
|
||||
};
|
||||
auto c6 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c7);
|
||||
};
|
||||
auto c5 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req1, ignore, c6);
|
||||
};
|
||||
auto c4 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c5);
|
||||
};
|
||||
auto c3 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req1, ignore, c4);
|
||||
};
|
||||
auto c2 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c3);
|
||||
};
|
||||
auto c1 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c2);
|
||||
};
|
||||
|
||||
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(req1, ignore, c1);
|
||||
launch_push_consumer();
|
||||
|
||||
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);
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(subscribe_finished);
|
||||
BOOST_TEST(unsubscribe_finished);
|
||||
BOOST_TEST(ping_finished);
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
BOOST_TEST(push_consumer_finished);
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_async_receive_waiting_for_push();
|
||||
test_async_receive_push_available();
|
||||
test_sync_receive();
|
||||
test_async_receive_cancelled_on_reconnection();
|
||||
test_consecutive_receives();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
|
||||
853
test/test_conn_push2.cpp
Normal file
853
test/test_conn_push2.cpp
Normal file
@@ -0,0 +1,853 @@
|
||||
/* 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/ignore.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
#include <boost/redis/operation.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/flat_tree.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/bind_cancellation_slot.hpp>
|
||||
#include <boost/asio/cancel_after.hpp>
|
||||
#include <boost/asio/cancellation_signal.hpp>
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/experimental/channel_error.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "common.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using namespace boost::redis;
|
||||
using namespace std::chrono_literals;
|
||||
using boost::system::error_code;
|
||||
using resp3::flat_tree;
|
||||
using resp3::node_view;
|
||||
using resp3::type;
|
||||
|
||||
// Covers all receive functionality except for the deprecated
|
||||
// async_receive and receive functions.
|
||||
|
||||
namespace {
|
||||
|
||||
// async_receive2 is outstanding when a push is received
|
||||
void test_async_receive2_waiting_for_push()
|
||||
{
|
||||
resp3::flat_tree resp;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
request req1;
|
||||
req1.push("PING", "Message1");
|
||||
req1.push("SUBSCRIBE", "test_async_receive_waiting_for_push");
|
||||
|
||||
request req2;
|
||||
req2.push("PING", "Message2");
|
||||
|
||||
bool run_finished = false, push_received = false, exec1_finished = false, exec2_finished = false;
|
||||
|
||||
auto on_exec2 = [&](error_code ec2, std::size_t) {
|
||||
BOOST_TEST_EQ(ec2, error_code());
|
||||
exec2_finished = true;
|
||||
conn.cancel();
|
||||
};
|
||||
|
||||
conn.async_exec(req1, ignore, [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
exec1_finished = true;
|
||||
});
|
||||
|
||||
conn.async_receive2([&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
push_received = true;
|
||||
conn.async_exec(req2, ignore, on_exec2);
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
run_finished = true;
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(push_received);
|
||||
BOOST_TEST(exec1_finished);
|
||||
BOOST_TEST(exec2_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// A push is already available when async_receive2 is called
|
||||
void test_async_receive2_push_available()
|
||||
{
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// SUBSCRIBE doesn't have a response, but causes a push to be delivered.
|
||||
// Add a PING so the overall request has a response.
|
||||
// This ensures that when async_exec completes, the push has been delivered
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "test_async_receive_push_available");
|
||||
req.push("PING", "message");
|
||||
|
||||
bool push_received = false, exec_finished = false, run_finished = false;
|
||||
|
||||
auto on_receive = [&](error_code ec, std::size_t) {
|
||||
push_received = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
conn.cancel();
|
||||
};
|
||||
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_receive(on_receive);
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&](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(push_received);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// async_receive2 blocks only once if several messages are received in a batch
|
||||
void test_async_receive2_batch()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// Cause two messages to be delivered. The PING ensures that
|
||||
// the pushes have been read when exec completes
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "test_async_receive2_batch");
|
||||
req.push("SUBSCRIBE", "test_async_receive2_batch");
|
||||
req.push("PING", "message");
|
||||
|
||||
bool receive_finished = false, run_finished = false;
|
||||
|
||||
// 1. Trigger pushes
|
||||
// 2. Receive both of them
|
||||
// 3. Check that receive2 has consumed them by calling it again
|
||||
auto on_receive2 = [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
receive_finished = true;
|
||||
conn.cancel();
|
||||
};
|
||||
|
||||
auto on_receive1 = [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 2u);
|
||||
conn.async_receive2(net::cancel_after(50ms, on_receive2));
|
||||
};
|
||||
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_receive2(on_receive1);
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(receive_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// async_receive2 can be called several times in a row
|
||||
void test_async_receive2_subsequent_calls()
|
||||
{
|
||||
struct impl {
|
||||
net::io_context ioc{};
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp{};
|
||||
request req{};
|
||||
bool receive_finished = false, run_finished = false;
|
||||
|
||||
// Send a SUBSCRIBE, which will trigger a push
|
||||
void start_subscribe1()
|
||||
{
|
||||
conn.async_exec(req, ignore, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
start_receive1();
|
||||
});
|
||||
}
|
||||
|
||||
// Receive the push
|
||||
void start_receive1()
|
||||
{
|
||||
conn.async_receive2([this](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
resp.clear();
|
||||
start_subscribe2();
|
||||
});
|
||||
}
|
||||
|
||||
// Send another SUBSCRIBE, which will trigger another push
|
||||
void start_subscribe2()
|
||||
{
|
||||
conn.async_exec(req, ignore, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
start_receive2();
|
||||
});
|
||||
}
|
||||
|
||||
// End
|
||||
void start_receive2()
|
||||
{
|
||||
conn.async_receive2([this](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(resp.get_total_msgs(), 1u);
|
||||
receive_finished = true;
|
||||
conn.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
// Setup
|
||||
conn.set_receive_response(resp);
|
||||
req.push("SUBSCRIBE", "test_async_receive2_subsequent_calls");
|
||||
|
||||
start_subscribe1();
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(receive_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
};
|
||||
|
||||
impl{}.run();
|
||||
}
|
||||
|
||||
// async_receive2 can be cancelled using per-operation cancellation,
|
||||
// and supports all cancellation types
|
||||
void test_async_receive2_per_operation_cancellation(
|
||||
std::string_view name,
|
||||
net::cancellation_type_t type)
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
net::cancellation_signal sig;
|
||||
bool receive_finished = false;
|
||||
|
||||
conn.async_receive2(net::bind_cancellation_slot(sig.slot(), [&](error_code ec) {
|
||||
if (!BOOST_TEST_EQ(ec, net::error::operation_aborted))
|
||||
std::cerr << "With cancellation type " << name << std::endl;
|
||||
receive_finished = true;
|
||||
}));
|
||||
|
||||
sig.emit(type);
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
if (!BOOST_TEST(receive_finished))
|
||||
std::cerr << "With cancellation type " << name << std::endl;
|
||||
}
|
||||
|
||||
// connection::cancel() cancels async_receive2
|
||||
void test_async_receive2_connection_cancel()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
net::cancellation_signal sig;
|
||||
bool receive_finished = false;
|
||||
|
||||
conn.async_receive2([&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
receive_finished = true;
|
||||
});
|
||||
|
||||
conn.cancel();
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(receive_finished);
|
||||
}
|
||||
|
||||
// Reconnection doesn't cancel async_receive2
|
||||
void test_async_receive2_reconnection()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// Causes the reconnection
|
||||
request req_quit;
|
||||
req_quit.push("QUIT");
|
||||
|
||||
// When this completes, the reconnection has happened
|
||||
request req_ping;
|
||||
req_ping.get_config().cancel_if_unresponded = false;
|
||||
req_ping.push("PING", "test_async_receive2_connection");
|
||||
|
||||
// Generates a push
|
||||
request req_subscribe;
|
||||
req_subscribe.push("SUBSCRIBE", "test_async_receive2_connection");
|
||||
|
||||
bool exec_finished = false, receive_finished = false, run_finished = false;
|
||||
|
||||
// Launch a receive operation, and in parallel
|
||||
// 1. Trigger a reconnection
|
||||
// 2. Wait for the reconnection and check that receive hasn't been cancelled
|
||||
// 3. Trigger a push to make receive complete
|
||||
auto on_subscribe = [&](error_code ec, std::size_t) {
|
||||
// Will finish before receive2 because the command doesn't have a response
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
exec_finished = true;
|
||||
};
|
||||
|
||||
auto on_ping = [&](error_code ec, std::size_t) {
|
||||
// Reconnection has already happened here
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_NOT(receive_finished);
|
||||
conn.async_exec(req_subscribe, ignore, on_subscribe);
|
||||
};
|
||||
|
||||
conn.async_exec(req_quit, ignore, [&](error_code, std::size_t) {
|
||||
conn.async_exec(req_ping, ignore, on_ping);
|
||||
});
|
||||
|
||||
conn.async_receive2([&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
receive_finished = true;
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&](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);
|
||||
}
|
||||
|
||||
// A push may be interleaved between regular responses.
|
||||
// It is handed to the receive adapter (filtered out).
|
||||
void test_exec_push_interleaved()
|
||||
{
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree receive_resp;
|
||||
conn.set_receive_response(receive_resp);
|
||||
|
||||
request req;
|
||||
req.push("PING", "msg1");
|
||||
req.push("SUBSCRIBE", "test_exec_push_interleaved");
|
||||
req.push("PING", "msg2");
|
||||
|
||||
response<std::string, std::string> resp;
|
||||
|
||||
bool exec_finished = false, push_received = 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_EQ(std::get<0>(resp).value(), "msg1");
|
||||
BOOST_TEST_EQ(std::get<1>(resp).value(), "msg2");
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_receive2([&](error_code ec) {
|
||||
push_received = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(receive_resp.get_total_msgs(), 1u);
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&](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(push_received);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// An adapter that always errors
|
||||
struct response_error_tag { };
|
||||
response_error_tag error_tag_obj;
|
||||
|
||||
struct response_error_adapter {
|
||||
void on_init() { }
|
||||
void on_done() { }
|
||||
void on_node(node_view const&, error_code& ec) { ec = error::incompatible_size; }
|
||||
};
|
||||
|
||||
auto boost_redis_adapt(response_error_tag&) { return response_error_adapter{}; }
|
||||
|
||||
// If the push adapter returns an error, the connection is torn down
|
||||
void test_push_adapter_error()
|
||||
{
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
conn.set_receive_response(error_tag_obj);
|
||||
|
||||
request req;
|
||||
req.push("PING");
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
req.push("PING");
|
||||
|
||||
bool receive_finished = false, exec_finished = false, run_finished = false;
|
||||
|
||||
// We cancel receive when run exits
|
||||
conn.async_receive2([&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
receive_finished = true;
|
||||
});
|
||||
|
||||
// The request is cancelled because the PING response isn't processed
|
||||
// by the time the error is generated
|
||||
conn.async_exec(req, ignore, [&exec_finished](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
exec_finished = true;
|
||||
});
|
||||
|
||||
auto cfg = make_test_config();
|
||||
cfg.reconnect_wait_interval = 0s; // so we can validate the generated error
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, error::incompatible_size);
|
||||
run_finished = true;
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
BOOST_TEST(receive_finished);
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// A push response error triggers a reconnection
|
||||
void test_push_adapter_error_reconnection()
|
||||
{
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
conn.set_receive_response(error_tag_obj);
|
||||
|
||||
request req;
|
||||
req.push("PING");
|
||||
req.push("SUBSCRIBE", "channel");
|
||||
req.push("PING");
|
||||
|
||||
request req2;
|
||||
req2.push("PING", "msg2");
|
||||
req2.get_config().cancel_if_unresponded = false;
|
||||
|
||||
response<std::string> resp;
|
||||
|
||||
bool push_received = false, exec_finished = false, run_finished = false;
|
||||
|
||||
// async_receive2 is cancelled every reconnection cycle
|
||||
conn.async_receive2([&](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
push_received = true;
|
||||
});
|
||||
|
||||
auto on_exec2 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(std::get<0>(resp).value(), "msg2");
|
||||
exec_finished = true;
|
||||
conn.cancel();
|
||||
};
|
||||
|
||||
// The request is cancelled because the PING response isn't processed
|
||||
// by the time the error is generated
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
conn.async_exec(req2, resp, on_exec2);
|
||||
});
|
||||
|
||||
conn.async_run(make_test_config(), [&run_finished](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
run_finished = true;
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(push_received);
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// Tests the usual push consumer pattern that we recommend in the examples
|
||||
void test_push_consumer()
|
||||
{
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
resp3::flat_tree resp;
|
||||
bool push_consumer_finished{false};
|
||||
|
||||
std::function<void()> launch_push_consumer = [&]() {
|
||||
conn.async_receive2([&](error_code ec) {
|
||||
if (ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
push_consumer_finished = true;
|
||||
resp.clear();
|
||||
return;
|
||||
}
|
||||
launch_push_consumer();
|
||||
});
|
||||
};
|
||||
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
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");
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
auto c10 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
exec_finished = true;
|
||||
conn.cancel();
|
||||
};
|
||||
auto c9 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c10);
|
||||
};
|
||||
auto c8 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req1, ignore, c9);
|
||||
};
|
||||
auto c7 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c8);
|
||||
};
|
||||
auto c6 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c7);
|
||||
};
|
||||
auto c5 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req1, ignore, c6);
|
||||
};
|
||||
auto c4 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c5);
|
||||
};
|
||||
auto c3 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req1, ignore, c4);
|
||||
};
|
||||
auto c2 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c3);
|
||||
};
|
||||
auto c1 = [&](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.async_exec(req2, ignore, c2);
|
||||
};
|
||||
|
||||
conn.async_exec(req1, ignore, c1);
|
||||
launch_push_consumer();
|
||||
|
||||
conn.async_run(make_test_config(), [&](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);
|
||||
BOOST_TEST(push_consumer_finished);
|
||||
}
|
||||
|
||||
// UNSUBSCRIBE and PUNSUBSCRIBE work
|
||||
void 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_EQ(ec, error_code());
|
||||
ping_finished = true;
|
||||
BOOST_TEST(std::get<0>(resp_ping).has_value());
|
||||
BOOST_TEST_EQ(std::get<0>(resp_ping).value(), "test_unsubscribe");
|
||||
conn.cancel();
|
||||
};
|
||||
|
||||
auto on_unsubscribe = [&](error_code ec, std::size_t) {
|
||||
unsubscribe_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST(std::get<0>(resp_unsubscribe).has_value());
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp_unsubscribe).value(), "sub"), "2");
|
||||
BOOST_TEST_EQ(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_EQ(ec, error_code());
|
||||
BOOST_TEST(std::get<0>(resp_subscribe).has_value());
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp_subscribe).value(), "sub"), "3");
|
||||
BOOST_TEST_EQ(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_EQ(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);
|
||||
}
|
||||
|
||||
struct test_pubsub_state_restoration_impl {
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
request req{};
|
||||
response<std::string> resp_str{};
|
||||
flat_tree resp_push{};
|
||||
bool exec_finished = false;
|
||||
|
||||
void check_subscriptions()
|
||||
{
|
||||
// Checks for the expected subscriptions and patterns after restoration
|
||||
std::set<std::string_view> seen_channels, seen_patterns;
|
||||
for (auto it = resp_push.begin(); it != resp_push.end();) {
|
||||
// The root element should be a push
|
||||
BOOST_TEST_EQ(it->data_type, type::push);
|
||||
BOOST_TEST_GE(it->aggregate_size, 2u);
|
||||
BOOST_TEST(++it != resp_push.end());
|
||||
|
||||
// The next element should be the message type
|
||||
std::string_view msg_type = it->value;
|
||||
BOOST_TEST(++it != resp_push.end());
|
||||
|
||||
// The next element is the channel or pattern
|
||||
if (msg_type == "subscribe")
|
||||
seen_channels.insert(it->value);
|
||||
else if (msg_type == "psubscribe")
|
||||
seen_patterns.insert(it->value);
|
||||
|
||||
// Skip the rest of the nodes
|
||||
while (it != resp_push.end() && it->depth != 0u)
|
||||
++it;
|
||||
}
|
||||
|
||||
const std::string_view expected_channels[] = {"ch1", "ch3", "ch5"};
|
||||
const std::string_view expected_patterns[] = {"ch1*", "ch3*", "ch4*", "ch8*"};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
seen_channels.begin(),
|
||||
seen_channels.end(),
|
||||
std::begin(expected_channels),
|
||||
std::end(expected_channels));
|
||||
BOOST_TEST_ALL_EQ(
|
||||
seen_patterns.begin(),
|
||||
seen_patterns.end(),
|
||||
std::begin(expected_patterns),
|
||||
std::end(expected_patterns));
|
||||
}
|
||||
|
||||
void sub1()
|
||||
{
|
||||
// Subscribe to some channels and patterns
|
||||
req.clear();
|
||||
req.subscribe({"ch1", "ch2", "ch3"}); // active: 1, 2, 3
|
||||
req.psubscribe({"ch1*", "ch2*", "ch3*", "ch4*"}); // active: 1, 2, 3, 4
|
||||
conn.async_exec(req, ignore, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
unsub();
|
||||
});
|
||||
}
|
||||
|
||||
void unsub()
|
||||
{
|
||||
// Unsubscribe from some channels and patterns.
|
||||
// Unsubscribing from a channel/pattern that we weren't subscribed to is OK.
|
||||
req.clear();
|
||||
req.unsubscribe({"ch2", "ch1", "ch5"}); // active: 3
|
||||
req.punsubscribe({"ch2*", "ch4*", "ch9*"}); // active: 1, 3
|
||||
conn.async_exec(req, ignore, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
sub2();
|
||||
});
|
||||
}
|
||||
|
||||
void sub2()
|
||||
{
|
||||
// Subscribe to other channels/patterns.
|
||||
// Re-subscribing to channels/patterns we unsubscribed from is OK.
|
||||
// Subscribing to the same channel/pattern twice is OK.
|
||||
req.clear();
|
||||
req.subscribe({"ch1", "ch3", "ch5"}); // active: 1, 3, 5
|
||||
req.psubscribe({"ch3*", "ch4*", "ch8*"}); // active: 1, 3, 4, 8
|
||||
|
||||
// Subscriptions created by push() don't survive reconnection
|
||||
req.push("SUBSCRIBE", "ch10"); // active: 1, 3, 5, 10
|
||||
req.push("PSUBSCRIBE", "ch10*"); // active: 1, 3, 4, 8, 10
|
||||
|
||||
// Validate that we're subscribed to what we expect
|
||||
req.push("CLIENT", "INFO");
|
||||
|
||||
conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// We are subscribed to 4 channels and 5 patterns
|
||||
BOOST_TEST(std::get<0>(resp_str).has_value());
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "4");
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "5");
|
||||
|
||||
resp_push.clear();
|
||||
|
||||
quit();
|
||||
});
|
||||
}
|
||||
|
||||
void quit()
|
||||
{
|
||||
req.clear();
|
||||
req.push("QUIT");
|
||||
|
||||
conn.async_exec(req, ignore, [this](error_code, std::size_t) {
|
||||
// we don't know if this request will complete successfully or not
|
||||
client_info();
|
||||
});
|
||||
}
|
||||
|
||||
void client_info()
|
||||
{
|
||||
req.clear();
|
||||
req.push("CLIENT", "INFO");
|
||||
req.get_config().cancel_if_unresponded = false;
|
||||
|
||||
conn.async_exec(req, resp_str, [this](error_code ec, std::size_t) {
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// We are subscribed to 3 channels and 4 patterns (1 of each didn't survive reconnection)
|
||||
BOOST_TEST(std::get<0>(resp_str).has_value());
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "sub"), "3");
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp_str).value(), "psub"), "4");
|
||||
|
||||
// We have received pushes confirming it
|
||||
check_subscriptions();
|
||||
|
||||
exec_finished = true;
|
||||
conn.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
void run()
|
||||
{
|
||||
conn.set_receive_response(resp_push);
|
||||
|
||||
// Start the request chain
|
||||
sub1();
|
||||
|
||||
// Start running
|
||||
bool run_finished = false;
|
||||
conn.async_run(make_test_config(), [&run_finished](error_code ec) {
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
run_finished = true;
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
// Done
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
};
|
||||
void test_pubsub_state_restoration() { test_pubsub_state_restoration_impl{}.run(); }
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_async_receive2_waiting_for_push();
|
||||
test_async_receive2_push_available();
|
||||
test_async_receive2_batch();
|
||||
test_async_receive2_subsequent_calls();
|
||||
test_async_receive2_per_operation_cancellation("terminal", net::cancellation_type_t::terminal);
|
||||
test_async_receive2_per_operation_cancellation("partial", net::cancellation_type_t::partial);
|
||||
test_async_receive2_per_operation_cancellation("total", net::cancellation_type_t::total);
|
||||
test_async_receive2_connection_cancel();
|
||||
test_async_receive2_reconnection();
|
||||
test_exec_push_interleaved();
|
||||
test_push_adapter_error();
|
||||
test_push_adapter_error_reconnection();
|
||||
test_push_consumer();
|
||||
test_unsubscribe();
|
||||
test_pubsub_state_restoration();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -47,9 +47,7 @@ net::awaitable<void> test_reconnect_impl()
|
||||
regular_req.get_config().cancel_if_unresponded = false;
|
||||
|
||||
auto conn = std::make_shared<connection>(ex);
|
||||
auto cfg = make_test_config();
|
||||
cfg.reconnect_wait_interval = 100ms; // make the test run faster
|
||||
run(conn, std::move(cfg));
|
||||
run(conn, make_test_config());
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
BOOST_TEST_CONTEXT("i=" << i)
|
||||
|
||||
491
test/test_conn_sentinel.cpp
Normal file
491
test/test_conn_sentinel.cpp
Normal file
@@ -0,0 +1,491 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/connection.hpp>
|
||||
#include <boost/redis/ignore.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/tree.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "common.hpp"
|
||||
#include "print_node.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using namespace boost::redis;
|
||||
using namespace std::chrono_literals;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
// We can execute requests normally when using Sentinel run
|
||||
void test_exec()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Verify that we're connected to the master
|
||||
request req;
|
||||
req.push("ROLE");
|
||||
|
||||
generic_response resp;
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// ROLE outputs an array, 1st element should be 'master'
|
||||
BOOST_TEST(resp.has_value());
|
||||
BOOST_TEST_GE(resp.value().size(), 2u);
|
||||
BOOST_TEST_EQ(resp.value().at(1u).value, "master");
|
||||
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// We can use receive normally when using Sentinel run
|
||||
void test_receive()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
resp3::tree resp;
|
||||
conn.set_receive_response(resp);
|
||||
|
||||
// Subscribe to a channel. This produces a push message on itself
|
||||
request req;
|
||||
req.subscribe({"sentinel_channel"});
|
||||
|
||||
bool exec_finished = false, receive_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
});
|
||||
|
||||
conn.async_receive2([&](error_code ec2) {
|
||||
receive_finished = true;
|
||||
BOOST_TEST_EQ(ec2, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(receive_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
// We subscribed to channel 'sentinel_channel', and have 1 active subscription
|
||||
const resp3::node expected[] = {
|
||||
{resp3::type::push, 3u, 0u, "" },
|
||||
{resp3::type::blob_string, 1u, 1u, "subscribe" },
|
||||
{resp3::type::blob_string, 1u, 1u, "sentinel_channel"},
|
||||
{resp3::type::number, 1u, 1u, "1" },
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(resp.begin(), resp.end(), std::begin(expected), std::end(expected));
|
||||
}
|
||||
|
||||
// If connectivity to the Redis master fails, we can reconnect
|
||||
void test_reconnect()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Will cause the connection to fail
|
||||
request req_quit;
|
||||
req_quit.push("QUIT");
|
||||
|
||||
// Will succeed if the reconnection succeeds
|
||||
request req_ping;
|
||||
req_ping.push("PING", "sentinel_reconnect");
|
||||
req_ping.get_config().cancel_if_unresponded = false;
|
||||
|
||||
bool quit_finished = false, ping_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req_quit, ignore, [&](error_code ec1, std::size_t) {
|
||||
quit_finished = true;
|
||||
BOOST_TEST_EQ(ec1, error_code());
|
||||
conn.async_exec(req_ping, ignore, [&](error_code ec2, std::size_t) {
|
||||
ping_finished = true;
|
||||
BOOST_TEST_EQ(ec2, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(quit_finished);
|
||||
BOOST_TEST(ping_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// If a Sentinel is not reachable, we try the next one
|
||||
void test_sentinel_not_reachable()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "45678"}, // invalid
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
|
||||
// Verify that we're connected to the master, listening at port 6380
|
||||
request req;
|
||||
req.push("PING", "test_sentinel_not_reachable");
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// Both Sentinels and masters may be protected with authorization
|
||||
void test_auth()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_pass");
|
||||
|
||||
cfg.use_setup = true;
|
||||
cfg.setup.clear();
|
||||
cfg.setup.push("HELLO", 3, "AUTH", "redis_user", "redis_pass");
|
||||
|
||||
// Verify that we're authenticated correctly
|
||||
request req;
|
||||
req.push("ACL", "WHOAMI");
|
||||
|
||||
response<std::string> resp;
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST(std::get<0>(resp).has_value());
|
||||
BOOST_TEST_EQ(std::get<0>(resp).value(), "redis_user");
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// TLS might be used with Sentinels. In our setup, nodes don't use TLS,
|
||||
// but this setting is independent from Sentinel.
|
||||
void test_tls()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
net::ssl::context ssl_ctx{net::ssl::context::tlsv13_client};
|
||||
|
||||
// The custom server uses a certificate signed by a CA
|
||||
// that is not trusted by default - skip verification.
|
||||
ssl_ctx.set_verify_mode(net::ssl::verify_none);
|
||||
|
||||
connection conn{ioc, std::move(ssl_ctx)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "36379"},
|
||||
{"localhost", "36380"},
|
||||
{"localhost", "36381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.sentinel.use_ssl = true;
|
||||
|
||||
request req;
|
||||
req.push("PING", "test_sentinel_tls");
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, ignore, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST(ec == error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, {}, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST(ec == net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// We can also connect to replicas
|
||||
void test_replica()
|
||||
{
|
||||
// Setup
|
||||
net::io_context ioc;
|
||||
connection conn{ioc};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"},
|
||||
{"localhost", "26380"},
|
||||
{"localhost", "26381"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.sentinel.server_role = role::replica;
|
||||
|
||||
// Verify that we're connected to a replica
|
||||
request req;
|
||||
req.push("ROLE");
|
||||
|
||||
generic_response resp;
|
||||
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// ROLE outputs an array, 1st element should be 'slave'
|
||||
BOOST_TEST(resp.has_value());
|
||||
BOOST_TEST_GE(resp.value().size(), 2u);
|
||||
BOOST_TEST_EQ(resp.value().at(1u).value, "slave");
|
||||
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, net::error::operation_aborted);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(run_finished);
|
||||
}
|
||||
|
||||
// If no Sentinel is reachable, an error is issued.
|
||||
// This tests disabling reconnection with Sentinel, too.
|
||||
void test_error_no_sentinel_reachable()
|
||||
{
|
||||
// Setup
|
||||
std::string logs;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "43210"},
|
||||
{"localhost", "43211"},
|
||||
};
|
||||
cfg.sentinel.master_name = "mymaster";
|
||||
cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error
|
||||
|
||||
bool run_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, error::sentinel_resolve_failed);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
if (
|
||||
!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:43210: connection establishment error"),
|
||||
std::string::npos) ||
|
||||
!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:43211: connection establishment error"),
|
||||
std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// If Sentinel doesn't know about the configured master,
|
||||
// the appropriate error is returned
|
||||
void test_error_unknown_master()
|
||||
{
|
||||
// Setup
|
||||
std::string logs;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26380"},
|
||||
};
|
||||
cfg.sentinel.master_name = "unknown_master";
|
||||
cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error
|
||||
|
||||
bool run_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, error::sentinel_resolve_failed);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
if (!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:26380: doesn't know about the configured master"),
|
||||
std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
// The same applies when connecting to replicas, too
|
||||
void test_error_unknown_master_replica()
|
||||
{
|
||||
// Setup
|
||||
std::string logs;
|
||||
net::io_context ioc;
|
||||
connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26380"},
|
||||
};
|
||||
cfg.sentinel.master_name = "unknown_master";
|
||||
cfg.reconnect_wait_interval = 0s; // disable reconnection so we can verify the error
|
||||
cfg.sentinel.server_role = role::replica;
|
||||
|
||||
bool run_finished = false;
|
||||
|
||||
conn.async_run(cfg, [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, error::sentinel_resolve_failed);
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
if (!BOOST_TEST_NE(
|
||||
logs.find("Sentinel at localhost:26380: doesn't know about the configured master"),
|
||||
std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
// Create the required users in the master, replicas and sentinels
|
||||
create_user("6379", "redis_user", "redis_pass");
|
||||
create_user("6380", "redis_user", "redis_pass");
|
||||
create_user("6381", "redis_user", "redis_pass");
|
||||
create_user("26379", "sentinel_user", "sentinel_pass");
|
||||
create_user("26380", "sentinel_user", "sentinel_pass");
|
||||
create_user("26381", "sentinel_user", "sentinel_pass");
|
||||
|
||||
// Actual tests
|
||||
test_exec();
|
||||
test_receive();
|
||||
test_reconnect();
|
||||
test_sentinel_not_reachable();
|
||||
test_auth();
|
||||
test_tls();
|
||||
test_replica();
|
||||
|
||||
test_error_no_sentinel_reachable();
|
||||
test_error_unknown_master();
|
||||
test_error_unknown_master_replica();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
#include "common.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -29,37 +28,6 @@ using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
// Creates a user with a known password. Harmless if the user already exists
|
||||
void setup_password()
|
||||
{
|
||||
// Setup
|
||||
asio::io_context ioc;
|
||||
redis::connection conn{ioc};
|
||||
|
||||
// Enable the user and grant them permissions on everything
|
||||
redis::request req;
|
||||
req.push("ACL", "SETUSER", "myuser", "on", ">mypass", "~*", "&*", "+@all");
|
||||
redis::generic_response resp;
|
||||
|
||||
bool run_finished = false, exec_finished = false;
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST_EQ(ec, asio::error::operation_aborted);
|
||||
});
|
||||
|
||||
conn.async_exec(req, resp, [&](error_code ec, std::size_t) {
|
||||
exec_finished = true;
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
conn.cancel();
|
||||
});
|
||||
|
||||
ioc.run_for(test_timeout);
|
||||
|
||||
BOOST_TEST(run_finished);
|
||||
BOOST_TEST(exec_finished);
|
||||
BOOST_TEST(resp.has_value());
|
||||
}
|
||||
|
||||
void test_auth_success()
|
||||
{
|
||||
// Setup
|
||||
@@ -96,17 +64,13 @@ void test_auth_success()
|
||||
BOOST_TEST_EQ(std::get<0>(resp).value(), "myuser");
|
||||
}
|
||||
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
void test_auth_failure()
|
||||
{
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
std::ostringstream oss;
|
||||
redis::logger lgr(redis::logger::level::info, [&](redis::logger::level, std::string_view msg) {
|
||||
oss << msg << '\n';
|
||||
});
|
||||
|
||||
// Setup
|
||||
std::string logs;
|
||||
asio::io_context ioc;
|
||||
redis::connection conn{ioc, std::move(lgr)};
|
||||
redis::connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
// Disable reconnection so the hello error causes the connection to exit
|
||||
auto cfg = make_test_config();
|
||||
@@ -126,9 +90,8 @@ void test_auth_failure()
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
// Check the log
|
||||
auto log = oss.str();
|
||||
if (!BOOST_TEST_NE(log.find("WRONGPASS"), std::string::npos)) {
|
||||
std::cerr << "Log was: " << log << std::endl;
|
||||
if (!BOOST_TEST_NE(logs.find("WRONGPASS"), std::string::npos)) {
|
||||
std::cerr << "Log was: \n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,17 +238,13 @@ void test_setup_no_hello()
|
||||
BOOST_TEST_EQ(find_client_info(std::get<0>(resp).value(), "db"), "8");
|
||||
}
|
||||
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
void test_setup_failure()
|
||||
{
|
||||
// Verify that we log appropriately (see https://github.com/boostorg/redis/issues/297)
|
||||
std::ostringstream oss;
|
||||
redis::logger lgr(redis::logger::level::info, [&](redis::logger::level, std::string_view msg) {
|
||||
oss << msg << '\n';
|
||||
});
|
||||
|
||||
// Setup
|
||||
std::string logs;
|
||||
asio::io_context ioc;
|
||||
redis::connection conn{ioc, std::move(lgr)};
|
||||
redis::connection conn{ioc, make_string_logger(logs)};
|
||||
|
||||
// Disable reconnection so the hello error causes the connection to exit
|
||||
auto cfg = make_test_config();
|
||||
@@ -306,9 +265,8 @@ void test_setup_failure()
|
||||
BOOST_TEST(run_finished);
|
||||
|
||||
// Check the log
|
||||
auto log = oss.str();
|
||||
if (!BOOST_TEST_NE(log.find("wrong number of arguments"), std::string::npos)) {
|
||||
std::cerr << "Log was: " << log << std::endl;
|
||||
if (!BOOST_TEST_NE(logs.find("wrong number of arguments"), std::string::npos)) {
|
||||
std::cerr << "Log was:\n" << logs << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +274,8 @@ void test_setup_failure()
|
||||
|
||||
int main()
|
||||
{
|
||||
setup_password();
|
||||
create_user("6379", "myuser", "mypass");
|
||||
|
||||
test_auth_success();
|
||||
test_auth_failure();
|
||||
test_database_index();
|
||||
|
||||
@@ -55,7 +55,7 @@ static config make_tls_config()
|
||||
config cfg;
|
||||
cfg.use_ssl = true;
|
||||
cfg.addr.host = get_server_hostname();
|
||||
cfg.addr.port = "6380";
|
||||
cfg.addr.port = "16380";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@@ -147,8 +147,6 @@ BOOST_AUTO_TEST_CASE(reconnection)
|
||||
net::io_context ioc;
|
||||
net::steady_timer timer{ioc};
|
||||
connection conn{ioc};
|
||||
auto cfg = make_tls_config();
|
||||
cfg.reconnect_wait_interval = 10ms; // make the test run faster
|
||||
|
||||
request ping_request;
|
||||
ping_request.push("PING", "some_value");
|
||||
@@ -161,7 +159,7 @@ BOOST_AUTO_TEST_CASE(reconnection)
|
||||
bool exec_finished = false, run_finished = false;
|
||||
|
||||
// Run the connection
|
||||
conn.async_run(cfg, {}, [&](error_code ec) {
|
||||
conn.async_run(make_test_config(), [&](error_code ec) {
|
||||
run_finished = true;
|
||||
BOOST_TEST(ec == net::error::operation_aborted);
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connect_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/logger.hpp>
|
||||
@@ -103,30 +102,15 @@ auto resolver_data = [] {
|
||||
|
||||
// Reduce duplication
|
||||
struct fixture : detail::log_fixture {
|
||||
config cfg;
|
||||
buffered_logger lgr{make_logger()};
|
||||
connect_fsm fsm{cfg, lgr};
|
||||
redis_stream_state st{};
|
||||
connect_fsm fsm{lgr};
|
||||
redis_stream_state st;
|
||||
|
||||
fixture(config&& cfg = {})
|
||||
: cfg{std::move(cfg)}
|
||||
fixture(transport_type type = transport_type::tcp)
|
||||
: st{type, false}
|
||||
{ }
|
||||
};
|
||||
|
||||
config make_ssl_config()
|
||||
{
|
||||
config cfg;
|
||||
cfg.use_ssl = true;
|
||||
return cfg;
|
||||
}
|
||||
|
||||
config make_unix_config()
|
||||
{
|
||||
config cfg;
|
||||
cfg.unix_socket = "/run/redis.sock";
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void test_tcp_success()
|
||||
{
|
||||
// Setup
|
||||
@@ -141,20 +125,21 @@ void test_tcp_success()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::tcp);
|
||||
BOOST_TEST_NOT(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
// clang-format off
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
void test_tcp_tls_success()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. No SSL stream reset is performed here
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -167,21 +152,22 @@ void test_tcp_tls_success()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::tcp_tls);
|
||||
BOOST_TEST(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Successfully performed SSL handshake" },
|
||||
// clang-format off
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" },
|
||||
{logger::level::debug, "Connect: SSL handshake succeeded" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
void test_tcp_tls_success_reconnect()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
fix.st.ssl_stream_used = true;
|
||||
|
||||
// Run the algorithm. The stream is used, so it needs to be reset
|
||||
@@ -197,21 +183,22 @@ void test_tcp_tls_success_reconnect()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::tcp_tls);
|
||||
BOOST_TEST(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Successfully performed SSL handshake" },
|
||||
// clang-format off
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234" },
|
||||
{logger::level::debug, "Connect: SSL handshake succeeded" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
void test_unix_success()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -222,12 +209,11 @@ void test_unix_success()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::unix_socket);
|
||||
BOOST_TEST_NOT(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Connected to /run/redis.sock"},
|
||||
{logger::level::debug, "Connect: UNIX socket connect succeeded"},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -235,7 +221,7 @@ void test_unix_success()
|
||||
void test_unix_success_close_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -246,12 +232,11 @@ void test_unix_success_close_error()
|
||||
BOOST_TEST_EQ(act, connect_action_type::done);
|
||||
|
||||
// The transport type was appropriately set
|
||||
BOOST_TEST_EQ(fix.st.type, transport_type::unix_socket);
|
||||
BOOST_TEST_NOT(fix.st.ssl_stream_used);
|
||||
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
{logger::level::info, "Connected to /run/redis.sock"},
|
||||
{logger::level::debug, "Connect: UNIX socket connect succeeded"},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,7 +255,7 @@ void test_tcp_resolve_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Error resolving the server hostname: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::info, "Connect: hostname resolution failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -293,7 +278,7 @@ void test_tcp_resolve_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Error resolving the server hostname: Resolve timeout. [boost.redis:17]"},
|
||||
{logger::level::info, "Connect: hostname resolution failed: Resolve timeout. [boost.redis:17]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -349,8 +334,8 @@ void test_tcp_connect_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Failed to connect to the server: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connect: TCP connect failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -375,8 +360,8 @@ void test_tcp_connect_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Failed to connect to the server: Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connect: TCP connect failed: Connect timeout. [boost.redis:18]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -423,7 +408,7 @@ void test_tcp_connect_cancel_edge()
|
||||
void test_ssl_handshake_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. No SSL stream reset is performed here
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -441,9 +426,9 @@ void test_ssl_handshake_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Failed to perform SSL handshake: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234"},
|
||||
{logger::level::info, "Connect: SSL handshake failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -451,7 +436,7 @@ void test_ssl_handshake_error()
|
||||
void test_ssl_handshake_timeout()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. Timeout = operation_aborted without the cancel type set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -469,9 +454,9 @@ void test_ssl_handshake_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Resolve results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::info, "Connected to 192.168.10.1:1234" },
|
||||
{logger::level::info, "Failed to perform SSL handshake: SSL handshake timeout. [boost.redis:20]"},
|
||||
{logger::level::debug, "Connect: hostname resolution results: 192.168.10.1:1234, 192.168.10.2:1235"},
|
||||
{logger::level::debug, "Connect: TCP connect succeeded. Selected endpoint: 192.168.10.1:1234"},
|
||||
{logger::level::info, "Connect: SSL handshake failed: SSL handshake timeout. [boost.redis:20]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -479,7 +464,7 @@ void test_ssl_handshake_timeout()
|
||||
void test_ssl_handshake_cancel()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. Cancel = operation_aborted with the cancel type set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -501,7 +486,7 @@ void test_ssl_handshake_cancel()
|
||||
void test_ssl_handshake_cancel_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_ssl_config()};
|
||||
fixture fix{transport_type::tcp_tls};
|
||||
|
||||
// Run the algorithm. No error, but the cancel state is set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -524,7 +509,7 @@ void test_ssl_handshake_cancel_edge()
|
||||
void test_unix_connect_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -537,7 +522,7 @@ void test_unix_connect_error()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Failed to connect to the server: Expected field value is empty. [boost.redis:5]"},
|
||||
{logger::level::info, "Connect: UNIX socket connect failed: Expected field value is empty. [boost.redis:5]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -545,7 +530,7 @@ void test_unix_connect_error()
|
||||
void test_unix_connect_timeout()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm. Timeout = operation_aborted without a cancel state
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -558,7 +543,7 @@ void test_unix_connect_timeout()
|
||||
// Check logging
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Failed to connect to the server: Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Connect: UNIX socket connect failed: Connect timeout. [boost.redis:18]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
@@ -566,7 +551,7 @@ void test_unix_connect_timeout()
|
||||
void test_unix_connect_cancel()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm. Cancel = operation_aborted with a cancel state
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
@@ -583,7 +568,7 @@ void test_unix_connect_cancel()
|
||||
void test_unix_connect_cancel_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{make_unix_config()};
|
||||
fixture fix{transport_type::unix_socket};
|
||||
|
||||
// Run the algorithm. No error, but cancel state is set
|
||||
auto act = fix.fsm.resume(error_code(), fix.st, cancellation_type_t::none);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// 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/detail/exec_fsm.hpp>
|
||||
#include <boost/redis/detail/multiplexer.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
@@ -30,6 +31,7 @@ using detail::multiplexer;
|
||||
using detail::exec_action_type;
|
||||
using detail::consume_result;
|
||||
using detail::exec_action;
|
||||
using detail::connection_state;
|
||||
using boost::system::error_code;
|
||||
using boost::asio::cancellation_type_t;
|
||||
|
||||
@@ -102,11 +104,23 @@ struct elem_and_request {
|
||||
std::shared_ptr<multiplexer::elem> elm;
|
||||
std::weak_ptr<multiplexer::elem> weak_elm; // check that we free memory
|
||||
|
||||
elem_and_request(request::config cfg = {})
|
||||
: req(cfg)
|
||||
static request make_request(request::config cfg)
|
||||
{
|
||||
request req{cfg};
|
||||
|
||||
// Empty requests are not valid. The request needs to be populated before creating the element
|
||||
req.push("get", "mykey");
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
elem_and_request(request::config cfg = {})
|
||||
: elem_and_request(make_request(cfg))
|
||||
{ }
|
||||
|
||||
elem_and_request(request input_req)
|
||||
: req(std::move(input_req))
|
||||
{
|
||||
elm = std::make_shared<multiplexer::elem>(req, any_adapter{});
|
||||
|
||||
elm->set_done_callback([this] {
|
||||
@@ -121,35 +135,35 @@ struct elem_and_request {
|
||||
void test_success()
|
||||
{
|
||||
// Setup
|
||||
multiplexer mpx;
|
||||
connection_state st;
|
||||
elem_and_request input;
|
||||
exec_fsm fsm(mpx, std::move(input.elm));
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
error_code ec;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(true, cancellation_type_t::none);
|
||||
auto act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
|
||||
|
||||
// We should now wait for a response
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
|
||||
|
||||
// Simulate a successful write
|
||||
BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
|
||||
BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size()));
|
||||
|
||||
// Simulate a successful read
|
||||
read(mpx, "$5\r\nhello\r\n");
|
||||
auto req_status = mpx.consume(ec);
|
||||
read(st.mpx, "$5\r\nhello\r\n");
|
||||
auto req_status = st.mpx.consume(ec);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(req_status.first, consume_result::got_response);
|
||||
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
|
||||
BOOST_TEST_EQ(input.done_calls, 1u);
|
||||
|
||||
// This will awaken the exec operation, and should complete the operation
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action(error_code(), 11u));
|
||||
|
||||
// All memory should have been freed by now
|
||||
@@ -160,37 +174,37 @@ void test_success()
|
||||
void test_parse_error()
|
||||
{
|
||||
// Setup
|
||||
multiplexer mpx;
|
||||
connection_state st;
|
||||
elem_and_request input;
|
||||
exec_fsm fsm(mpx, std::move(input.elm));
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
error_code ec;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(true, cancellation_type_t::none);
|
||||
auto act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
|
||||
|
||||
// We should now wait for a response
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
|
||||
|
||||
// Simulate a successful write
|
||||
BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
|
||||
BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size()));
|
||||
|
||||
// Simulate a read that will trigger an error.
|
||||
// The second field should be a number (rather than the empty string).
|
||||
// Note that although part of the buffer was consumed, the multiplexer
|
||||
// currently throws this information away.
|
||||
read(mpx, "*2\r\n$5\r\nhello\r\n:\r\n");
|
||||
auto req_status = mpx.consume(ec);
|
||||
read(st.mpx, "*2\r\n$5\r\nhello\r\n:\r\n");
|
||||
auto req_status = st.mpx.consume(ec);
|
||||
BOOST_TEST_EQ(ec, error::empty_field);
|
||||
BOOST_TEST_EQ(req_status.second, 15u);
|
||||
BOOST_TEST_EQ(input.done_calls, 1u);
|
||||
|
||||
// This will awaken the exec operation, and should complete the operation
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action(error::empty_field, 0u));
|
||||
|
||||
// All memory should have been freed by now
|
||||
@@ -201,17 +215,17 @@ void test_parse_error()
|
||||
void test_cancel_if_not_connected()
|
||||
{
|
||||
// Setup
|
||||
multiplexer mpx;
|
||||
connection_state st;
|
||||
request::config cfg;
|
||||
cfg.cancel_if_not_connected = true;
|
||||
elem_and_request input(cfg);
|
||||
exec_fsm fsm(mpx, std::move(input.elm));
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
|
||||
// Initiate. We're not connected, so the request gets cancelled
|
||||
auto act = fsm.resume(false, cancellation_type_t::none);
|
||||
auto act = fsm.resume(false, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::immediate);
|
||||
|
||||
act = fsm.resume(false, cancellation_type_t::none);
|
||||
act = fsm.resume(false, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action(error::not_connected));
|
||||
|
||||
// We didn't leave memory behind
|
||||
@@ -222,35 +236,35 @@ void test_cancel_if_not_connected()
|
||||
void test_not_connected()
|
||||
{
|
||||
// Setup
|
||||
multiplexer mpx;
|
||||
connection_state st;
|
||||
elem_and_request input;
|
||||
exec_fsm fsm(mpx, std::move(input.elm));
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
error_code ec;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(false, cancellation_type_t::none);
|
||||
auto act = fsm.resume(false, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
|
||||
|
||||
// We should now wait for a response
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
|
||||
|
||||
// Simulate a successful write
|
||||
BOOST_TEST_EQ(mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
|
||||
BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size()));
|
||||
|
||||
// Simulate a successful read
|
||||
read(mpx, "$5\r\nhello\r\n");
|
||||
auto req_status = mpx.consume(ec);
|
||||
read(st.mpx, "$5\r\nhello\r\n");
|
||||
auto req_status = st.mpx.consume(ec);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(req_status.first, consume_result::got_response);
|
||||
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
|
||||
BOOST_TEST_EQ(input.done_calls, 1u);
|
||||
|
||||
// This will awaken the exec operation, and should complete the operation
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action(error_code(), 11u));
|
||||
|
||||
// All memory should have been freed by now
|
||||
@@ -277,24 +291,24 @@ void test_cancel_waiting()
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
// Setup
|
||||
multiplexer mpx;
|
||||
connection_state st;
|
||||
elem_and_request input, input2;
|
||||
exec_fsm fsm(mpx, std::move(input.elm));
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
|
||||
// Another request enters the multiplexer, so it's busy when we start
|
||||
mpx.add(input2.elm);
|
||||
BOOST_TEST_EQ_MSG(mpx.prepare_write(), 1u, tc.name);
|
||||
st.mpx.add(input2.elm);
|
||||
BOOST_TEST_EQ_MSG(st.mpx.prepare_write(), 1u, tc.name);
|
||||
|
||||
// Initiate and wait
|
||||
auto act = fsm.resume(true, cancellation_type_t::none);
|
||||
auto act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name);
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name);
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name);
|
||||
|
||||
// We get notified because the request got cancelled
|
||||
act = fsm.resume(true, tc.type);
|
||||
act = fsm.resume(true, st, tc.type);
|
||||
BOOST_TEST_EQ_MSG(act, exec_action(asio::error::operation_aborted), tc.name);
|
||||
BOOST_TEST_EQ_MSG(input.weak_elm.expired(), true, tc.name); // we didn't leave memory behind
|
||||
}
|
||||
@@ -314,32 +328,32 @@ void test_cancel_notwaiting_terminal_partial()
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
// Setup
|
||||
multiplexer mpx;
|
||||
connection_state st;
|
||||
auto input = std::make_unique<elem_and_request>();
|
||||
exec_fsm fsm(mpx, std::move(input->elm));
|
||||
exec_fsm fsm(std::move(input->elm));
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(false, cancellation_type_t::none);
|
||||
auto act = fsm.resume(false, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ_MSG(act, exec_action_type::setup_cancellation, tc.name);
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ_MSG(act, exec_action_type::notify_writer, tc.name);
|
||||
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ_MSG(act, exec_action_type::wait_for_response, tc.name);
|
||||
|
||||
// The multiplexer starts writing the request
|
||||
BOOST_TEST_EQ_MSG(mpx.prepare_write(), 1u, tc.name);
|
||||
BOOST_TEST_EQ_MSG(mpx.commit_write(mpx.get_write_buffer().size()), true, tc.name);
|
||||
BOOST_TEST_EQ_MSG(st.mpx.prepare_write(), 1u, tc.name);
|
||||
BOOST_TEST_EQ_MSG(st.mpx.commit_write(st.mpx.get_write_buffer().size()), true, tc.name);
|
||||
|
||||
// A cancellation arrives
|
||||
act = fsm.resume(true, tc.type);
|
||||
act = fsm.resume(true, st, tc.type);
|
||||
BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted));
|
||||
input.reset(); // Verify we don't access the request or response after completion
|
||||
|
||||
error_code ec;
|
||||
// When the response to this request arrives, it gets ignored
|
||||
read(mpx, "-ERR wrong command\r\n");
|
||||
auto res = mpx.consume(ec);
|
||||
read(st.mpx, "-ERR wrong command\r\n");
|
||||
auto res = st.mpx.consume(ec);
|
||||
BOOST_TEST_EQ_MSG(ec, error_code(), tc.name);
|
||||
BOOST_TEST_EQ_MSG(res.first, consume_result::got_response, tc.name);
|
||||
|
||||
@@ -352,44 +366,122 @@ void test_cancel_notwaiting_terminal_partial()
|
||||
void test_cancel_notwaiting_total()
|
||||
{
|
||||
// Setup
|
||||
multiplexer mpx;
|
||||
connection_state st;
|
||||
elem_and_request input;
|
||||
exec_fsm fsm(mpx, std::move(input.elm));
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
error_code ec;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(true, cancellation_type_t::none);
|
||||
auto act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
|
||||
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
|
||||
|
||||
// Simulate a successful write
|
||||
BOOST_TEST_EQ(mpx.prepare_write(), 1u);
|
||||
BOOST_TEST(mpx.commit_write(mpx.get_write_buffer().size()));
|
||||
BOOST_TEST_EQ(st.mpx.prepare_write(), 1u);
|
||||
BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size()));
|
||||
|
||||
// We got requested a cancellation here, but we can't honor it
|
||||
act = fsm.resume(true, asio::cancellation_type_t::total);
|
||||
act = fsm.resume(true, st, asio::cancellation_type_t::total);
|
||||
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
|
||||
|
||||
// Simulate a successful read
|
||||
read(mpx, "$5\r\nhello\r\n");
|
||||
auto req_status = mpx.consume(ec);
|
||||
read(st.mpx, "$5\r\nhello\r\n");
|
||||
auto req_status = st.mpx.consume(ec);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST_EQ(req_status.first, consume_result::got_response);
|
||||
BOOST_TEST_EQ(req_status.second, 11u); // the entire buffer was consumed
|
||||
BOOST_TEST_EQ(input.done_calls, 1u);
|
||||
|
||||
// This will awaken the exec operation, and should complete the operation
|
||||
act = fsm.resume(true, cancellation_type_t::none);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action(error_code(), 11u));
|
||||
|
||||
// All memory should have been freed by now
|
||||
BOOST_TEST_EQ(input.weak_elm.expired(), true);
|
||||
}
|
||||
|
||||
// If a request completes successfully and contained pubsub changes, these are committed
|
||||
void test_subscription_tracking_success()
|
||||
{
|
||||
// Setup
|
||||
request req;
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
connection_state st;
|
||||
elem_and_request input{std::move(req)};
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
|
||||
|
||||
// We should now wait for a response
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
|
||||
|
||||
// Simulate a successful write
|
||||
BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
BOOST_TEST(st.mpx.commit_write(st.mpx.get_write_buffer().size()));
|
||||
|
||||
// The request doesn't have a response, so this will
|
||||
// awaken the exec operation, and should complete the operation
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action(error_code(), 0u));
|
||||
|
||||
// All memory should have been freed by now
|
||||
BOOST_TEST(input.weak_elm.expired());
|
||||
|
||||
// The subscription has been added to the tracker
|
||||
request tracker_req;
|
||||
st.tracker.compose_subscribe_request(tracker_req);
|
||||
|
||||
request expected_req;
|
||||
expected_req.push("SUBSCRIBE", "ch1", "ch2");
|
||||
BOOST_TEST_EQ(tracker_req.payload(), expected_req.payload());
|
||||
}
|
||||
|
||||
// If the request errors, tracked subscriptions are not committed
|
||||
void test_subscription_tracking_error()
|
||||
{
|
||||
// Setup
|
||||
request req;
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
connection_state st;
|
||||
elem_and_request input{std::move(req)};
|
||||
exec_fsm fsm(std::move(input.elm));
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::setup_cancellation);
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::notify_writer);
|
||||
|
||||
// We should now wait for a response
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action_type::wait_for_response);
|
||||
|
||||
// Simulate a write error, which would trigger a reconnection
|
||||
BOOST_TEST_EQ(st.mpx.prepare_write(), 1u); // one request was placed in the packet to write
|
||||
st.mpx.cancel_on_conn_lost();
|
||||
|
||||
// This awakens the request
|
||||
act = fsm.resume(true, st, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_action(asio::error::operation_aborted, 0u));
|
||||
|
||||
// All memory should have been freed by now
|
||||
BOOST_TEST(input.weak_elm.expired());
|
||||
|
||||
// The subscription has not been added to the tracker
|
||||
request tracker_req;
|
||||
st.tracker.compose_subscribe_request(tracker_req);
|
||||
BOOST_TEST_EQ(tracker_req.payload(), "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
@@ -401,6 +493,8 @@ int main()
|
||||
test_cancel_waiting();
|
||||
test_cancel_notwaiting_terminal_partial();
|
||||
test_cancel_notwaiting_total();
|
||||
test_subscription_tracking_success();
|
||||
test_subscription_tracking_error();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
|
||||
365
test/test_exec_one_fsm.cpp
Normal file
365
test/test_exec_one_fsm.cpp
Normal file
@@ -0,0 +1,365 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/detail/exec_one_fsm.hpp>
|
||||
#include <boost/redis/detail/read_buffer.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include "print_node.hpp"
|
||||
|
||||
#include <iterator>
|
||||
#include <ostream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using namespace boost::redis;
|
||||
namespace asio = boost::asio;
|
||||
using detail::exec_one_fsm;
|
||||
using detail::exec_one_action;
|
||||
using detail::exec_one_action_type;
|
||||
using detail::read_buffer;
|
||||
using boost::system::error_code;
|
||||
using boost::asio::cancellation_type_t;
|
||||
using parse_event = any_adapter::parse_event;
|
||||
using resp3::type;
|
||||
|
||||
// Operators
|
||||
static const char* to_string(exec_one_action_type value)
|
||||
{
|
||||
switch (value) {
|
||||
case exec_one_action_type::done: return "done";
|
||||
case exec_one_action_type::write: return "write";
|
||||
case exec_one_action_type::read_some: return "read_some";
|
||||
default: return "<unknown writer_action_type>";
|
||||
}
|
||||
}
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
bool operator==(const exec_one_action& lhs, const exec_one_action& rhs) noexcept
|
||||
{
|
||||
return lhs.type == rhs.type && lhs.ec == rhs.ec;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const exec_one_action& act)
|
||||
{
|
||||
os << "exec_one_action{ .type=" << to_string(act.type);
|
||||
if (act.type == exec_one_action_type::done)
|
||||
os << ", ec=" << act.ec;
|
||||
return os << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
namespace {
|
||||
|
||||
struct adapter_event {
|
||||
parse_event type;
|
||||
resp3::node node{};
|
||||
|
||||
friend bool operator==(const adapter_event& lhs, const adapter_event& rhs) noexcept
|
||||
{
|
||||
return lhs.type == rhs.type && lhs.node == rhs.node;
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const adapter_event& value)
|
||||
{
|
||||
switch (value.type) {
|
||||
case parse_event::init: return os << "adapter_event{ .type=init }";
|
||||
case parse_event::done: return os << "adapter_event{ .type=done }";
|
||||
case parse_event::node:
|
||||
return os << "adapter_event{ .type=node, .node=" << value.node << " }";
|
||||
default: return os << "adapter_event{ .type=unknown }";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
any_adapter make_snoop_adapter(std::vector<adapter_event>& events)
|
||||
{
|
||||
return any_adapter::impl_t{[&](parse_event ev, resp3::node_view const& nd, error_code&) {
|
||||
events.push_back({
|
||||
ev,
|
||||
{nd.data_type, nd.aggregate_size, nd.depth, std::string(nd.value)}
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
void copy_to(read_buffer& buff, std::string_view data)
|
||||
{
|
||||
auto const buffer = buff.get_prepared();
|
||||
BOOST_TEST_GE(buffer.size(), data.size());
|
||||
std::copy(data.cbegin(), data.cend(), buffer.begin());
|
||||
}
|
||||
|
||||
void test_success()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read the entire response in one go
|
||||
constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n";
|
||||
copy_to(buff, payload);
|
||||
act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::done);
|
||||
|
||||
// Verify the adapter calls
|
||||
const adapter_event expected[] = {
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::blob_string, 1u, 0u, "hello"}},
|
||||
{parse_event::done},
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::array, 1u, 0u, ""}},
|
||||
{parse_event::node, {type::simple_string, 1u, 1u, "goodbye"}},
|
||||
{parse_event::done},
|
||||
};
|
||||
BOOST_TEST_ALL_EQ(events.begin(), events.end(), std::begin(expected), std::end(expected));
|
||||
}
|
||||
|
||||
// The request didn't have any expected response (e.g. SUBSCRIBE)
|
||||
void test_no_expected_response()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 0u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM shouldn't ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// No adapter calls should be done
|
||||
BOOST_TEST_EQ(events.size(), 0u);
|
||||
}
|
||||
|
||||
// The response is scattered in several smaller fragments
|
||||
void test_short_reads()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read fragments
|
||||
constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n";
|
||||
copy_to(buff, payload.substr(0, 6u));
|
||||
act = fsm.resume(buff, error_code(), 6u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
copy_to(buff, payload.substr(6, 10u));
|
||||
act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
copy_to(buff, payload.substr(16));
|
||||
act = fsm.resume(buff, error_code(), payload.substr(16).size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::done);
|
||||
|
||||
// Verify the adapter calls
|
||||
const adapter_event expected[] = {
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::blob_string, 1u, 0u, "hello"}},
|
||||
{parse_event::done},
|
||||
{parse_event::init},
|
||||
{parse_event::node, {type::array, 1u, 0u, ""}},
|
||||
{parse_event::node, {type::simple_string, 1u, 1u, "goodbye"}},
|
||||
{parse_event::done},
|
||||
};
|
||||
BOOST_TEST_ALL_EQ(events.begin(), events.end(), std::begin(expected), std::end(expected));
|
||||
}
|
||||
|
||||
// Errors in write
|
||||
void test_write_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// Write error
|
||||
act = fsm.resume(buff, asio::error::connection_reset, 10u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::connection_reset));
|
||||
}
|
||||
|
||||
void test_write_cancel()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// Edge case where the operation finished successfully but with the cancellation state set
|
||||
act = fsm.resume(buff, error_code(), 10u, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
}
|
||||
|
||||
// Errors in read
|
||||
void test_read_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read error
|
||||
act = fsm.resume(buff, asio::error::network_reset, 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::network_reset));
|
||||
}
|
||||
|
||||
void test_read_cancelled()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Edge case where the operation finished successfully but with the cancellation state set
|
||||
copy_to(buff, "$5\r\n");
|
||||
act = fsm.resume(buff, error_code(), 4u, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
}
|
||||
|
||||
// Buffer too small
|
||||
void test_buffer_prepare_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
buff.set_config({4096u, 8u}); // max size is 8 bytes
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// When preparing the buffer, we encounter an error
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::exceeds_maximum_read_buffer_size));
|
||||
}
|
||||
|
||||
// An invalid RESP3 message
|
||||
void test_parse_error()
|
||||
{
|
||||
// Setup
|
||||
std::vector<adapter_event> events;
|
||||
exec_one_fsm fsm{make_snoop_adapter(events), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// The response contains an invalid message
|
||||
constexpr std::string_view payload = "$bad\r\n";
|
||||
copy_to(buff, payload);
|
||||
act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::not_a_number));
|
||||
}
|
||||
|
||||
// Adapter signals an error
|
||||
void test_adapter_error()
|
||||
{
|
||||
// Setup. The adapter will fail in the 2nd node
|
||||
any_adapter adapter{[](parse_event ev, resp3::node_view const&, error_code& ec) {
|
||||
if (ev == parse_event::node)
|
||||
ec = error::empty_field;
|
||||
}};
|
||||
exec_one_fsm fsm{std::move(adapter), 2u};
|
||||
read_buffer buff;
|
||||
|
||||
// Write the request
|
||||
auto act = fsm.resume(buff, error_code(), 0u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::write);
|
||||
|
||||
// FSM should now ask for data
|
||||
act = fsm.resume(buff, error_code(), 25u, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, exec_one_action_type::read_some);
|
||||
|
||||
// Read the entire response in one go
|
||||
constexpr std::string_view payload = "$5\r\nhello\r\n*1\r\n+goodbye\r\n";
|
||||
copy_to(buff, payload);
|
||||
act = fsm.resume(buff, error_code(), payload.size(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::empty_field));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_success();
|
||||
test_no_expected_response();
|
||||
test_short_reads();
|
||||
|
||||
test_write_error();
|
||||
test_write_cancel();
|
||||
|
||||
test_read_error();
|
||||
test_read_cancelled();
|
||||
|
||||
test_buffer_prepare_error();
|
||||
test_parse_error();
|
||||
test_adapter_error();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
1581
test/test_flat_tree.cpp
Normal file
1581
test/test_flat_tree.cpp
Normal file
File diff suppressed because it is too large
Load Diff
115
test/test_generic_flat_response.cpp
Normal file
115
test/test_generic_flat_response.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
/* Copyright (c) 2018-2026 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
* Ruben Perez Hidalgo (rubenperez038@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/node.hpp>
|
||||
#include <boost/redis/resp3/serialization.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include "print_node.hpp"
|
||||
|
||||
using namespace boost::redis;
|
||||
using boost::system::error_code;
|
||||
using boost::redis::resp3::detail::deserialize;
|
||||
using resp3::node_view;
|
||||
using resp3::type;
|
||||
using adapter::adapt2;
|
||||
|
||||
// Regular nodes are just stored
|
||||
void test_success()
|
||||
{
|
||||
generic_flat_response resp;
|
||||
|
||||
error_code ec;
|
||||
deserialize("+hello\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
BOOST_TEST(resp.has_value());
|
||||
std::vector<node_view> expected_nodes{
|
||||
{type::simple_string, 1u, 0u, "hello"},
|
||||
};
|
||||
BOOST_TEST_ALL_EQ(resp->begin(), resp->end(), expected_nodes.begin(), expected_nodes.end());
|
||||
}
|
||||
|
||||
// If an error of any kind appears, we set the overall result to error
|
||||
void test_simple_error()
|
||||
{
|
||||
generic_flat_response resp;
|
||||
|
||||
error_code ec;
|
||||
deserialize("-Error\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
BOOST_TEST(resp.has_error());
|
||||
auto const error = resp.error();
|
||||
|
||||
BOOST_TEST_EQ(error.data_type, resp3::type::simple_error);
|
||||
BOOST_TEST_EQ(error.diagnostic, "Error");
|
||||
}
|
||||
|
||||
void test_blob_error()
|
||||
{
|
||||
generic_flat_response resp;
|
||||
|
||||
error_code ec;
|
||||
deserialize("!5\r\nError\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
BOOST_TEST(resp.has_error());
|
||||
auto const error = resp.error();
|
||||
|
||||
BOOST_TEST_EQ(error.data_type, resp3::type::blob_error);
|
||||
BOOST_TEST_EQ(error.diagnostic, "Error");
|
||||
}
|
||||
|
||||
// Mixing success and error nodes is safe. Only the last error is stored
|
||||
void test_mix_success_error()
|
||||
{
|
||||
generic_flat_response resp;
|
||||
error_code ec;
|
||||
|
||||
// Success message
|
||||
deserialize("+message\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
// An error
|
||||
deserialize("-Error\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
// Another success message
|
||||
deserialize("+other data\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
// Another error
|
||||
deserialize("-Different err\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
// Final success message
|
||||
deserialize("*1\r\n+last message\r\n", adapt2(resp), ec);
|
||||
BOOST_TEST_EQ(ec, error_code{});
|
||||
|
||||
// Check
|
||||
BOOST_TEST(resp.has_error());
|
||||
auto const error = resp.error();
|
||||
|
||||
BOOST_TEST_EQ(error.data_type, resp3::type::simple_error);
|
||||
BOOST_TEST_EQ(error.diagnostic, "Different err");
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
test_success();
|
||||
test_simple_error();
|
||||
test_blob_error();
|
||||
test_mix_success_error();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -530,6 +530,11 @@ BOOST_AUTO_TEST_CASE(cover_error)
|
||||
check_error("boost.redis", boost::redis::error::resp3_hello);
|
||||
check_error("boost.redis", boost::redis::error::exceeds_maximum_read_buffer_size);
|
||||
check_error("boost.redis", boost::redis::error::write_timeout);
|
||||
check_error("boost.redis", boost::redis::error::sentinel_unix_sockets_unsupported);
|
||||
check_error("boost.redis", boost::redis::error::sentinel_resolve_failed);
|
||||
check_error("boost.redis", boost::redis::error::role_check_failed);
|
||||
check_error("boost.redis", boost::redis::error::expects_resp3_string);
|
||||
check_error("boost.redis", boost::redis::error::expects_resp3_array);
|
||||
}
|
||||
|
||||
std::string get_type_as_str(boost::redis::resp3::type t)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Copyright (c) 2018-2024 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
/* Copyright (c) 2018-2025 Marcelo Zimbres Silva (mzimbres@gmail.com)
|
||||
*
|
||||
* Distributed under the Boost Software License, Version 1.0. (See
|
||||
* accompanying file LICENSE.txt)
|
||||
@@ -22,15 +22,19 @@
|
||||
using boost::redis::request;
|
||||
using boost::redis::adapter::adapt2;
|
||||
using boost::redis::adapter::result;
|
||||
using boost::redis::generic_response;
|
||||
using boost::redis::resp3::tree;
|
||||
using boost::redis::generic_flat_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"
|
||||
@@ -42,7 +46,9 @@ BOOST_AUTO_TEST_CASE(low_level_sync_sans_io)
|
||||
try {
|
||||
result<std::set<std::string>> resp;
|
||||
|
||||
deserialize(resp3_set, adapt2(resp));
|
||||
error_code ec;
|
||||
deserialize(resp3_set, adapt2(resp), ec);
|
||||
BOOST_CHECK_EQUAL(ec, error_code{});
|
||||
|
||||
for (auto const& e : resp.value())
|
||||
std::cout << e << std::endl;
|
||||
@@ -65,7 +71,9 @@ 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";
|
||||
|
||||
deserialize(wire, adapt2(resp));
|
||||
error_code ec;
|
||||
deserialize(wire, adapt2(resp), ec);
|
||||
BOOST_CHECK_EQUAL(ec, error_code{});
|
||||
|
||||
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
|
||||
BOOST_CHECK(std::get<1>(resp.value()).value().empty());
|
||||
@@ -91,7 +99,9 @@ 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";
|
||||
|
||||
deserialize(wire, adapt2(resp));
|
||||
error_code ec;
|
||||
deserialize(wire, adapt2(resp), ec);
|
||||
BOOST_CHECK_EQUAL(ec, error_code{});
|
||||
|
||||
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
|
||||
BOOST_CHECK_EQUAL(std::get<1>(resp.value()).value().size(), 1u);
|
||||
@@ -118,7 +128,9 @@ 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";
|
||||
|
||||
deserialize(wire, adapt2(resp));
|
||||
error_code ec;
|
||||
deserialize(wire, adapt2(resp), ec);
|
||||
BOOST_CHECK_EQUAL(ec, error_code{});
|
||||
|
||||
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
|
||||
BOOST_CHECK_EQUAL(std::get<1>(resp.value()).value().at(0), std::string{"foo"});
|
||||
@@ -140,7 +152,9 @@ 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";
|
||||
|
||||
deserialize(wire, adapt2(resp));
|
||||
error_code ec;
|
||||
deserialize(wire, adapt2(resp), ec);
|
||||
BOOST_CHECK_EQUAL(ec, error_code{});
|
||||
|
||||
BOOST_CHECK_EQUAL(std::get<0>(resp.value()).value(), 1);
|
||||
BOOST_CHECK_EQUAL(std::get<1>(resp.value()).value(), std::string{"foo"});
|
||||
@@ -159,7 +173,10 @@ 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";
|
||||
deserialize(wire, adapt2(resp));
|
||||
|
||||
error_code ec;
|
||||
deserialize(wire, adapt2(resp), ec);
|
||||
BOOST_CHECK_EQUAL(ec, error_code{});
|
||||
|
||||
BOOST_CHECK_EQUAL(resp.value().at(0).value(), "one");
|
||||
BOOST_TEST(!resp.value().at(1).has_value());
|
||||
@@ -177,7 +194,10 @@ 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";
|
||||
deserialize(wire, adapt2(resp));
|
||||
|
||||
error_code ec;
|
||||
deserialize(wire, adapt2(resp), ec);
|
||||
BOOST_CHECK_EQUAL(ec, error_code{});
|
||||
|
||||
BOOST_CHECK_EQUAL(resp.value().value().at(0).value(), "one");
|
||||
BOOST_TEST(!resp.value().value().at(1).has_value());
|
||||
@@ -189,87 +209,6 @@ 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;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
#include "print_node.hpp"
|
||||
#include "sansio_utils.hpp"
|
||||
|
||||
#include <iostream>
|
||||
@@ -33,17 +34,6 @@ using boost::redis::response;
|
||||
using boost::redis::any_adapter;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace boost::redis::resp3 {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, node const& nd)
|
||||
{
|
||||
return os << "node{ .data_type=" << to_string(nd.data_type)
|
||||
<< ", .aggregate_size=" << nd.aggregate_size << ", .depth=" << nd.depth
|
||||
<< ", .value=" << nd.value << "}";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::resp3
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, consume_result v)
|
||||
|
||||
727
test/test_parse_sentinel_response.cpp
Normal file
727
test/test_parse_sentinel_response.cpp
Normal file
@@ -0,0 +1,727 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/adapter/any_adapter.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp>
|
||||
#include <boost/redis/resp3/node.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include "sansio_utils.hpp"
|
||||
|
||||
#include <initializer_list>
|
||||
#include <ostream>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using namespace boost::redis;
|
||||
using detail::nodes_from_resp3;
|
||||
using detail::parse_sentinel_response;
|
||||
using detail::sentinel_response;
|
||||
using boost::system::error_code;
|
||||
|
||||
// Operators
|
||||
namespace boost::redis {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const address& addr)
|
||||
{
|
||||
return os << "address{ .host=" << addr.host << ", .port=" << addr.port << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
namespace {
|
||||
|
||||
struct fixture {
|
||||
sentinel_response resp{
|
||||
"leftover",
|
||||
{"leftover_host", "6543"},
|
||||
{address()},
|
||||
{address()},
|
||||
};
|
||||
|
||||
void check_response(
|
||||
const address& expected_master_addr,
|
||||
boost::span<const address> expected_replicas,
|
||||
boost::span<const address> expected_sentinels,
|
||||
boost::source_location loc = BOOST_CURRENT_LOCATION) const
|
||||
{
|
||||
if (!BOOST_TEST_EQ(resp.diagnostic, ""))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_EQ(resp.master_addr, expected_master_addr))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_ALL_EQ(
|
||||
resp.replicas.begin(),
|
||||
resp.replicas.end(),
|
||||
expected_replicas.begin(),
|
||||
expected_replicas.end()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
if (!BOOST_TEST_ALL_EQ(
|
||||
resp.sentinels.begin(),
|
||||
resp.sentinels.end(),
|
||||
expected_sentinels.begin(),
|
||||
expected_sentinels.end()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
};
|
||||
|
||||
// Usual response when asking for a master
|
||||
void test_master()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n334\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n134\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, {}, expected_sentinels);
|
||||
}
|
||||
|
||||
// Works correctly even if no Sentinels are present
|
||||
void test_master_no_sentinels()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
fix.check_response({"localhost", "6380"}, {}, {});
|
||||
}
|
||||
|
||||
// The responses corresponding to the user-defined setup request are ignored
|
||||
void test_master_setup_request()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"+OK\r\n",
|
||||
"%6\r\n$6\r\nserver\r\n$5\r\nredis\r\n$7\r\nversion\r\n$5\r\n7.4.2\r\n$5\r\nproto\r\n:3\r\n$2\r\nid\r\n:3\r\n$4\r\nmode\r\n$8\r\nsentinel\r\n$7\r\nmodules\r\n*0\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n334\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n134\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, {}, expected_sentinels);
|
||||
}
|
||||
|
||||
// IP and port can be out of order
|
||||
void test_master_ip_port_out_of_order()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$4\r\nport\r\n$5\r\n26380\r\n$2\r\nip\r\n$8\r\nhost.one\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::master, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, {}, expected_sentinels);
|
||||
}
|
||||
|
||||
// Usual response when asking for a replica
|
||||
void test_replica()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%21\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n$4\r\nport\r\n$4\r\n6381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\ncdfa33e2d39958c0b10c0391c0c3d4ab096edfeb\r\n$5\r\nflags\r\n$5\r\nslave\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n134\r\n$15\r\nlast-ping-reply\r\n$3\r\n134\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$12\r\ninfo-refresh\r\n$4\r\n5302\r\n$13\r\nrole-reported\r\n$5\r\nslave\r\n$18\r\nrole-reported-time\r\n$6\r\n442121\r\n"
|
||||
"$21\r\nmaster-link-down-time\r\n$1\r\n0\r\n$18\r\nmaster-link-status\r\n$2\r\nok\r\n$11\r\nmaster-host\r\n$9\r\nlocalhost\r\n"
|
||||
"$11\r\nmaster-port\r\n$4\r\n6380\r\n$14\r\nslave-priority\r\n$3\r\n100\r\n$17\r\nslave-repl-offset\r\n$5\r\n29110\r\n"
|
||||
"$17\r\nreplica-announced\r\n$1\r\n1\r\n"
|
||||
"%21\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6382\r\n$2\r\nip\r\n$9\r\ntest.host\r\n$4\r\nport\r\n$4\r\n6382\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\n11bfea62c25316e211fdf0e1ccd2dbd920e90815\r\n$5\r\nflags\r\n$5\r\nslave\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n134\r\n$15\r\nlast-ping-reply\r\n$3\r\n134\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$12\r\ninfo-refresh\r\n$4\r\n5302\r\n$13\r\nrole-reported\r\n$5\r\nslave\r\n$18\r\nrole-reported-time\r\n$6\r\n442132\r\n"
|
||||
"$21\r\nmaster-link-down-time\r\n$1\r\n0\r\n$18\r\nmaster-link-status\r\n$2\r\nok\r\n$11\r\nmaster-host\r\n$9\r\nlocalhost\r\n"
|
||||
"$11\r\nmaster-port\r\n$4\r\n6380\r\n$14\r\nslave-priority\r\n$3\r\n100\r\n$17\r\nslave-repl-offset\r\n$5\r\n29110\r\n"
|
||||
"$17\r\nreplica-announced\r\n$1\r\n1\r\n",
|
||||
"*2\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n334\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n"
|
||||
"%14\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
"$5\r\nrunid\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$5\r\nflags\r\n$8\r\nsentinel\r\n"
|
||||
"$21\r\nlink-pending-commands\r\n$1\r\n0\r\n$13\r\nlink-refcount\r\n$1\r\n1\r\n$14\r\nlast-ping-sent\r\n$1\r\n0\r\n"
|
||||
"$18\r\nlast-ok-ping-reply\r\n$3\r\n696\r\n$15\r\nlast-ping-reply\r\n$3\r\n696\r\n$23\r\ndown-after-milliseconds\r\n$5\r\n10000\r\n"
|
||||
"$18\r\nlast-hello-message\r\n$3\r\n134\r\n$12\r\nvoted-leader\r\n$1\r\n?\r\n$18\r\nvoted-leader-epoch\r\n$1\r\n0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
{"test.host", "6382"},
|
||||
};
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, expected_replicas, expected_sentinels);
|
||||
}
|
||||
|
||||
// Like the master case
|
||||
void test_replica_no_sentinels()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n$4\r\nport\r\n$4\r\n6381\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6382\r\n$2\r\nip\r\n$9\r\ntest.host\r\n$4\r\nport\r\n$4\r\n6382\r\n",
|
||||
"*0\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
{"test.host", "6382"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, expected_replicas, {});
|
||||
}
|
||||
|
||||
// Asking for replicas, but there is none
|
||||
void test_replica_no_replicas()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
"*0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
fix.check_response({"localhost", "6380"}, {}, {});
|
||||
}
|
||||
|
||||
// Setup requests work with replicas, too
|
||||
void test_replica_setup_request()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n+OK\r\n+OK\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*2\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n$4\r\nport\r\n$4\r\n6381\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$14\r\nlocalhost:6382\r\n$2\r\nip\r\n$9\r\ntest.host\r\n$4\r\nport\r\n$4\r\n6382\r\n",
|
||||
"*2\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf14ef06a8a478cdd66ded467ec18accd2a24b731\r\n$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n"
|
||||
"%3\r\n"
|
||||
"$4\r\nname\r\n$40\r\nf9b54e79e2e7d3f17ad60527504191ec8a861f27\r\n$2\r\nip\r\n$8\r\nhost.two\r\n$4\r\nport\r\n$5\r\n26381\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
{"test.host", "6382"},
|
||||
};
|
||||
const address expected_sentinels[] = {
|
||||
{"host.one", "26380"},
|
||||
{"host.two", "26381"},
|
||||
};
|
||||
fix.check_response({"localhost", "6380"}, expected_replicas, expected_sentinels);
|
||||
}
|
||||
|
||||
// IP and port can be out of order
|
||||
void test_replica_ip_port_out_of_order()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6389\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$4\r\nport\r\n$4\r\n6381\r\n$2\r\nip\r\n$9\r\nsome.host\r\n",
|
||||
"*0\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, role::replica, fix.resp);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
|
||||
// Check
|
||||
const address expected_replicas[] = {
|
||||
{"some.host", "6381"},
|
||||
};
|
||||
fix.check_response({"test.host", "6389"}, expected_replicas, {});
|
||||
}
|
||||
|
||||
void test_errors()
|
||||
{
|
||||
const struct {
|
||||
std::string_view name;
|
||||
role server_role;
|
||||
std::vector<std::string_view> responses;
|
||||
std::string_view expected_diagnostic;
|
||||
error_code expected_ec;
|
||||
} test_cases[]{
|
||||
// clang-format off
|
||||
{
|
||||
// A RESP3 simple error
|
||||
"setup_error_simple",
|
||||
role::master,
|
||||
{
|
||||
"-WRONGPASS invalid username-password pair or user is disabled.\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"WRONGPASS invalid username-password pair or user is disabled.",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// A RESP3 blob error
|
||||
"setup_error_blob",
|
||||
role::master,
|
||||
{
|
||||
"!3\r\nBad\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"Bad",
|
||||
error::resp3_blob_error
|
||||
},
|
||||
{
|
||||
// Errors in intermediate nodes of the user-supplied request
|
||||
"setup_error_intermediate",
|
||||
role::master,
|
||||
{
|
||||
"+OK\r\n",
|
||||
"-Something happened!\r\n",
|
||||
"+OK\r\n",
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"Something happened!",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// Only the first error is processed (e.g. auth failure may cause subsequent cmds to fail)
|
||||
"setup_error_intermediate",
|
||||
role::master,
|
||||
{
|
||||
"-Something happened!\r\n",
|
||||
"-Something worse happened!\r\n",
|
||||
"-Bad\r\n",
|
||||
"-Worse\r\n",
|
||||
},
|
||||
"Something happened!",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// This works for replicas, too
|
||||
"setup_error_replicas",
|
||||
role::replica,
|
||||
{
|
||||
"-Something happened!\r\n",
|
||||
"-Something worse happened!\r\n",
|
||||
"-Bad\r\n",
|
||||
"-Worse\r\n",
|
||||
},
|
||||
"Something happened!",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
|
||||
// SENTINEL GET-MASTER-ADDR-BY-NAME
|
||||
{
|
||||
// Unknown master. This returns NULL and causes SENTINEL SENTINELS to fail
|
||||
"getmasteraddr_unknown_master",
|
||||
role::master,
|
||||
{
|
||||
"_\r\n",
|
||||
"-ERR Unknown master\r\n",
|
||||
},
|
||||
"",
|
||||
error::resp3_null
|
||||
},
|
||||
{
|
||||
// The request errors for any other reason
|
||||
"getmasteraddr_error",
|
||||
role::master,
|
||||
{
|
||||
"-ERR something happened\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"ERR something happened",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// Same, for replicas
|
||||
"getmasteraddr_unknown_master_replica",
|
||||
role::replica,
|
||||
{
|
||||
"_\r\n",
|
||||
"-ERR Unknown master\r\n",
|
||||
"-ERR Unknown master\r\n",
|
||||
},
|
||||
"",
|
||||
error::resp3_null
|
||||
},
|
||||
{
|
||||
// Root node should be a list
|
||||
"getmasteraddr_not_array",
|
||||
role::master,
|
||||
{
|
||||
"+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_array
|
||||
},
|
||||
{
|
||||
// Root node should have exactly 2 elements
|
||||
"getmasteraddr_array_size_1",
|
||||
role::master,
|
||||
{
|
||||
"*1\r\n$5\r\nhello\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::incompatible_size
|
||||
},
|
||||
{
|
||||
// Root node should have exactly 2 elements
|
||||
"getmasteraddr_array_size_3",
|
||||
role::master,
|
||||
{
|
||||
"*3\r\n$5\r\nhello\r\n$3\r\nbye\r\n$3\r\nabc\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::incompatible_size
|
||||
},
|
||||
{
|
||||
// IP should be a string
|
||||
"getmasteraddr_ip_not_string",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n+OK\r\n$5\r\nhello\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
// Port should be a string
|
||||
"getmasteraddr_port_not_string",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$5\r\nhello\r\n+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
|
||||
// SENTINEL SENTINELS
|
||||
{
|
||||
// The request errors
|
||||
"sentinels_error",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"-ERR something went wrong\r\n",
|
||||
},
|
||||
"ERR something went wrong",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// The root node should be an array
|
||||
"sentinels_not_array",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"+OK\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_array
|
||||
},
|
||||
{
|
||||
// Each Sentinel object should be a map
|
||||
"sentinels_subobject_not_map",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n*1\r\n$9\r\nlocalhost\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_map
|
||||
},
|
||||
{
|
||||
// Keys in the Sentinel object should be strings
|
||||
"sentinels_keys_not_strings",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n*0\r\n$5\r\nhello\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
// Values in the Sentinel object should be strings
|
||||
"sentinels_keys_not_strings",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$5\r\nhello\r\n*1\r\n+OK\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
"sentinels_ip_not_found",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$4\r\nport\r\n$4\r\n6380\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
},
|
||||
{
|
||||
"sentinels_port_not_found",
|
||||
role::master,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$2\r\nip\r\n$9\r\nlocalhost\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
},
|
||||
|
||||
// SENTINEL REPLICAS
|
||||
{
|
||||
// The request errors
|
||||
"replicas_error",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"-ERR something went wrong\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"ERR something went wrong",
|
||||
error::resp3_simple_error
|
||||
},
|
||||
{
|
||||
// The root node should be an array
|
||||
"replicas_not_array",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_array
|
||||
},
|
||||
{
|
||||
// Each replica object should be a map
|
||||
"replicas_subobject_not_map",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n*1\r\n$9\r\nlocalhost\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_map
|
||||
},
|
||||
{
|
||||
// Keys in the replica object should be strings
|
||||
"replicas_keys_not_strings",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n*0\r\n$5\r\nhello\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
// Values in the replica object should be strings
|
||||
"replicas_keys_not_strings",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$5\r\nhello\r\n*1\r\n+OK\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::expects_resp3_string
|
||||
},
|
||||
{
|
||||
"replicas_ip_not_found",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$4\r\nport\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
},
|
||||
{
|
||||
"replicas_port_not_found",
|
||||
role::replica,
|
||||
{
|
||||
"*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n%1\r\n$2\r\nip\r\n$9\r\nlocalhost\r\n",
|
||||
"*0\r\n",
|
||||
},
|
||||
"",
|
||||
error::empty_field
|
||||
}
|
||||
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
for (const auto& tc : test_cases) {
|
||||
// Setup
|
||||
std::cerr << "Running error test case: " << tc.name << std::endl;
|
||||
fixture fix;
|
||||
auto nodes = nodes_from_resp3(tc.responses);
|
||||
|
||||
// Call the function
|
||||
auto ec = parse_sentinel_response(nodes, tc.server_role, fix.resp);
|
||||
BOOST_TEST_EQ(ec, tc.expected_ec);
|
||||
BOOST_TEST_EQ(fix.resp.diagnostic, tc.expected_diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_master();
|
||||
test_master_no_sentinels();
|
||||
test_master_setup_request();
|
||||
test_master_ip_port_out_of_order();
|
||||
|
||||
test_replica();
|
||||
test_replica_no_sentinels();
|
||||
test_replica_no_replicas();
|
||||
test_replica_setup_request();
|
||||
test_replica_ip_port_out_of_order();
|
||||
|
||||
test_errors();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
102
test/test_read_buffer.cpp
Normal file
102
test/test_read_buffer.cpp
Normal file
@@ -0,0 +1,102 @@
|
||||
/* 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();
|
||||
}
|
||||
246
test/test_receive_fsm.cpp
Normal file
246
test/test_receive_fsm.cpp
Normal file
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// Copyright (c) 2026 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/detail/receive_fsm.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/asio/experimental/channel_error.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using namespace boost::redis;
|
||||
using net::cancellation_type_t;
|
||||
using boost::system::error_code;
|
||||
using net::cancellation_type_t;
|
||||
using detail::receive_action;
|
||||
using detail::receive_fsm;
|
||||
using detail::connection_state;
|
||||
namespace channel_errc = net::experimental::channel_errc;
|
||||
using action_type = receive_action::action_type;
|
||||
|
||||
// Operators
|
||||
static const char* to_string(action_type type)
|
||||
{
|
||||
switch (type) {
|
||||
case action_type::setup_cancellation: return "setup_cancellation";
|
||||
case action_type::wait: return "wait";
|
||||
case action_type::drain_channel: return "drain_channel";
|
||||
case action_type::immediate: return "immediate";
|
||||
case action_type::done: return "done";
|
||||
default: return "<unknown action::type>";
|
||||
}
|
||||
}
|
||||
|
||||
namespace boost::redis::detail {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, action_type type) { return os << to_string(type); }
|
||||
|
||||
bool operator==(const receive_action& lhs, const receive_action& rhs) noexcept
|
||||
{
|
||||
return lhs.type == rhs.type && lhs.ec == rhs.ec;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const receive_action& act)
|
||||
{
|
||||
os << "action{ .type=" << act.type;
|
||||
if (act.type == action_type::done)
|
||||
os << ", ec=" << act.ec;
|
||||
return os << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
namespace {
|
||||
|
||||
struct fixture {
|
||||
connection_state st;
|
||||
generic_response resp;
|
||||
};
|
||||
|
||||
void test_success()
|
||||
{
|
||||
connection_state st;
|
||||
receive_fsm fsm;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::setup_cancellation);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
|
||||
// At this point, the operation is now running
|
||||
BOOST_TEST(st.receive2_running);
|
||||
|
||||
// The wait finishes successfully (we were notified). Receive exits
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::drain_channel);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The operation is no longer running
|
||||
BOOST_TEST_NOT(st.receive2_running);
|
||||
}
|
||||
|
||||
// We might see spurious cancels during reconnection (v1 compatibility).
|
||||
void test_cancelled_reconnection()
|
||||
{
|
||||
connection_state st;
|
||||
receive_fsm fsm;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::setup_cancellation);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
|
||||
// Reconnection happens
|
||||
act = fsm.resume(st, channel_errc::channel_cancelled, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
BOOST_TEST(st.receive2_running); // still running
|
||||
|
||||
// Another reconnection
|
||||
act = fsm.resume(st, channel_errc::channel_cancelled, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
BOOST_TEST(st.receive2_running); // still running
|
||||
|
||||
// The wait finishes successfully (we were notified). Receive exits
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::drain_channel);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The operation is no longer running
|
||||
BOOST_TEST_NOT(st.receive2_running);
|
||||
}
|
||||
|
||||
// We might get cancellations due to connection::cancel()
|
||||
void test_cancelled_connection_cancel()
|
||||
{
|
||||
connection_state st;
|
||||
receive_fsm fsm;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::setup_cancellation);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
|
||||
// Simulate a connection::cancel()
|
||||
st.receive2_cancelled = true;
|
||||
act = fsm.resume(st, channel_errc::channel_cancelled, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(net::error::operation_aborted));
|
||||
BOOST_TEST_NOT(st.receive2_running);
|
||||
}
|
||||
|
||||
// Operations can still run after connection::cancel()
|
||||
void test_after_connection_cancel()
|
||||
{
|
||||
connection_state st;
|
||||
receive_fsm fsm;
|
||||
st.receive2_cancelled = true;
|
||||
|
||||
// The operation initiates and runs normally
|
||||
auto act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::setup_cancellation);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
BOOST_TEST(st.receive2_running);
|
||||
|
||||
// Reconnection behavior not affected
|
||||
act = fsm.resume(st, channel_errc::channel_cancelled, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
BOOST_TEST(st.receive2_running); // still running
|
||||
|
||||
// Simulate a connection::cancel()
|
||||
st.receive2_cancelled = true;
|
||||
act = fsm.resume(st, channel_errc::channel_cancelled, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(net::error::operation_aborted));
|
||||
BOOST_TEST_NOT(st.receive2_running);
|
||||
}
|
||||
|
||||
// Per-operation cancellation is supported
|
||||
void test_per_operation_cancellation(std::string_view name, cancellation_type_t type)
|
||||
{
|
||||
std::cerr << "Running cancellation case " << name << std::endl;
|
||||
|
||||
connection_state st;
|
||||
receive_fsm fsm;
|
||||
|
||||
// The operation initiates and runs normally
|
||||
auto act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::setup_cancellation);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
BOOST_TEST(st.receive2_running);
|
||||
|
||||
// Cancellation is received
|
||||
act = fsm.resume(st, channel_errc::channel_cancelled, type);
|
||||
BOOST_TEST_EQ(act, error_code(net::error::operation_aborted));
|
||||
BOOST_TEST_NOT(st.receive2_running);
|
||||
}
|
||||
|
||||
// Only a single instance of async_receive2 can be running at the same time
|
||||
void test_error_already_running()
|
||||
{
|
||||
connection_state st;
|
||||
receive_fsm fsm;
|
||||
st.receive2_running = true;
|
||||
|
||||
// The operation fails immediately
|
||||
auto act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::immediate);
|
||||
BOOST_TEST(st.receive2_running); // not affected
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::already_running));
|
||||
BOOST_TEST(st.receive2_running); // not affected
|
||||
}
|
||||
|
||||
// If an unknown error was obtained during channel receive, we propagate it
|
||||
void test_error_unknown()
|
||||
{
|
||||
connection_state st;
|
||||
receive_fsm fsm;
|
||||
|
||||
// Initiate
|
||||
auto act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::setup_cancellation);
|
||||
act = fsm.resume(st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, action_type::wait);
|
||||
BOOST_TEST(st.receive2_running);
|
||||
|
||||
// We have an unknown error
|
||||
act = fsm.resume(st, channel_errc::channel_closed, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(channel_errc::channel_closed));
|
||||
BOOST_TEST_NOT(st.receive2_running);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_success();
|
||||
test_cancelled_reconnection();
|
||||
test_cancelled_connection_cancel();
|
||||
test_after_connection_cancel();
|
||||
|
||||
test_per_operation_cancellation("terminal", cancellation_type_t::terminal);
|
||||
test_per_operation_cancellation("partial", cancellation_type_t::partial);
|
||||
test_per_operation_cancellation("total", cancellation_type_t::total);
|
||||
test_per_operation_cancellation("all", cancellation_type_t::all);
|
||||
|
||||
test_error_already_running();
|
||||
test_error_unknown();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -4,55 +4,716 @@
|
||||
* accompanying file LICENSE.txt)
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#define BOOST_TEST_MODULE request
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <boost/test/included/unit_test.hpp>
|
||||
#include <boost/assert/source_location.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/core/span.hpp>
|
||||
|
||||
using boost::redis::request;
|
||||
#include <array>
|
||||
#include <forward_list>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
// TODO: Serialization.
|
||||
using namespace boost::redis;
|
||||
using detail::pubsub_change;
|
||||
using detail::pubsub_change_type;
|
||||
|
||||
BOOST_AUTO_TEST_CASE(single_arg_allocator)
|
||||
namespace {
|
||||
|
||||
// --- Utilities to check subscription tracking ---
|
||||
const char* to_string(pubsub_change_type type)
|
||||
{
|
||||
switch (type) {
|
||||
case pubsub_change_type::subscribe: return "subscribe";
|
||||
case pubsub_change_type::unsubscribe: return "unsubscribe";
|
||||
case pubsub_change_type::psubscribe: return "psubscribe";
|
||||
case pubsub_change_type::punsubscribe: return "punsubscribe";
|
||||
default: return "<unknown pubsub_change_type>";
|
||||
}
|
||||
}
|
||||
|
||||
// Like pubsub_change, but using a string instead of an offset
|
||||
struct pubsub_change_str {
|
||||
pubsub_change_type type;
|
||||
std::string_view value;
|
||||
|
||||
friend bool operator==(const pubsub_change_str& lhs, const pubsub_change_str& rhs)
|
||||
{
|
||||
return lhs.type == rhs.type && lhs.value == rhs.value;
|
||||
}
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const pubsub_change_str& value)
|
||||
{
|
||||
return os << "{ " << to_string(value.type) << ", " << value.value << " }";
|
||||
}
|
||||
};
|
||||
|
||||
void check_pubsub_changes(
|
||||
const request& req,
|
||||
boost::span<const pubsub_change_str> expected,
|
||||
boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
// Convert from offsets to strings
|
||||
std::vector<pubsub_change_str> actual;
|
||||
for (const auto& change : detail::request_access::pubsub_changes(req)) {
|
||||
actual.push_back(
|
||||
{change.type, req.payload().substr(change.channel_offset, change.channel_size)});
|
||||
}
|
||||
|
||||
// Check
|
||||
if (!BOOST_TEST_ALL_EQ(actual.begin(), actual.end(), expected.begin(), expected.end()))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
}
|
||||
|
||||
// --- Generic functions to add commands ---
|
||||
void test_push_no_args()
|
||||
{
|
||||
request req1;
|
||||
req1.push("PING");
|
||||
BOOST_CHECK_EQUAL(req1.payload(), std::string{"*1\r\n$4\r\nPING\r\n"});
|
||||
BOOST_TEST_EQ(req1.payload(), "*1\r\n$4\r\nPING\r\n");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(arg_int)
|
||||
void test_push_int()
|
||||
{
|
||||
request req;
|
||||
req.push("PING", 42);
|
||||
BOOST_CHECK_EQUAL(req.payload(), std::string{"*2\r\n$4\r\nPING\r\n$2\r\n42\r\n"});
|
||||
BOOST_TEST_EQ(req.payload(), "*2\r\n$4\r\nPING\r\n$2\r\n42\r\n");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(multiple_args)
|
||||
void test_push_multiple_args()
|
||||
{
|
||||
char const* res = "*5\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n$2\r\nEX\r\n$1\r\n2\r\n";
|
||||
request req;
|
||||
req.push("SET", "key", "value", "EX", "2");
|
||||
BOOST_CHECK_EQUAL(req.payload(), std::string{res});
|
||||
BOOST_TEST_EQ(req.payload(), res);
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(container_and_range)
|
||||
// Subscription commands added with push are not tracked
|
||||
void test_push_pubsub()
|
||||
{
|
||||
request req;
|
||||
req.push("SUBSCRIBE", "ch1");
|
||||
req.push("UNSUBSCRIBE", "ch2");
|
||||
req.push("PSUBSCRIBE", "ch3*");
|
||||
req.push("PUNSUBSCRIBE", "ch4*");
|
||||
|
||||
char const* res =
|
||||
"*2\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n"
|
||||
"*2\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch2\r\n"
|
||||
"*2\r\n$10\r\nPSUBSCRIBE\r\n$4\r\nch3*\r\n"
|
||||
"*2\r\n$12\r\nPUNSUBSCRIBE\r\n$4\r\nch4*\r\n";
|
||||
BOOST_TEST_EQ(req.payload(), res);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
// --- push_range ---
|
||||
void test_push_range()
|
||||
{
|
||||
std::map<std::string, std::string> in{
|
||||
{"key1", "value1"},
|
||||
{"key2", "value2"}
|
||||
};
|
||||
|
||||
char const* res =
|
||||
constexpr std::string_view expected =
|
||||
"*6\r\n$4\r\nHSET\r\n$3\r\nkey\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n$4\r\nkey2\r\n$"
|
||||
"6\r\nvalue2\r\n";
|
||||
|
||||
request req1;
|
||||
req1.push_range("HSET", "key", in);
|
||||
BOOST_CHECK_EQUAL(req1.payload(), std::string{res});
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
|
||||
request req2;
|
||||
req2.push_range("HSET", "key", std::cbegin(in), std::cend(in));
|
||||
BOOST_CHECK_EQUAL(req2.payload(), std::string{res});
|
||||
BOOST_TEST_EQ(req2.payload(), expected);
|
||||
}
|
||||
|
||||
// Subscription commands added with push_range are not tracked
|
||||
void test_push_range_pubsub()
|
||||
{
|
||||
const std::vector<std::string_view> channels1{"ch1", "ch2"}, channels2{"ch3"}, patterns1{"ch3*"},
|
||||
patterns2{"ch4*"};
|
||||
request req;
|
||||
req.push_range("SUBSCRIBE", channels1);
|
||||
req.push_range("UNSUBSCRIBE", channels2);
|
||||
req.push_range("PSUBSCRIBE", patterns1);
|
||||
req.push_range("PUNSUBSCRIBE", patterns2);
|
||||
|
||||
char const* res =
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"
|
||||
"*2\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch3\r\n"
|
||||
"*2\r\n$10\r\nPSUBSCRIBE\r\n$4\r\nch3*\r\n"
|
||||
"*2\r\n$12\r\nPUNSUBSCRIBE\r\n$4\r\nch4*\r\n";
|
||||
BOOST_TEST_EQ(req.payload(), res);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
// --- subscribe ---
|
||||
// Most of the tests build the same request using different overloads.
|
||||
// This fixture makes checking easier
|
||||
struct subscribe_fixture {
|
||||
request req;
|
||||
|
||||
void check_impl(
|
||||
std::string_view expected_payload,
|
||||
pubsub_change_type expected_type,
|
||||
boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
if (!BOOST_TEST_EQ(req.payload(), expected_payload))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST_EQ(req.get_commands(), 1u))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
if (!BOOST_TEST_EQ(req.get_expected_responses(), 0u))
|
||||
std::cerr << "Called from " << loc << std::endl;
|
||||
|
||||
const pubsub_change_str expected_changes[] = {
|
||||
{expected_type, "ch1"},
|
||||
{expected_type, "ch2"},
|
||||
};
|
||||
check_pubsub_changes(req, expected_changes, loc);
|
||||
}
|
||||
|
||||
void check_subscribe(boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
check_impl(
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n",
|
||||
pubsub_change_type::subscribe,
|
||||
loc);
|
||||
}
|
||||
|
||||
void check_unsubscribe(boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
check_impl(
|
||||
"*3\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n",
|
||||
pubsub_change_type::unsubscribe,
|
||||
loc);
|
||||
}
|
||||
|
||||
void check_psubscribe(boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
check_impl(
|
||||
"*3\r\n$10\r\nPSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n",
|
||||
pubsub_change_type::psubscribe,
|
||||
loc);
|
||||
}
|
||||
|
||||
void check_punsubscribe(boost::source_location loc = BOOST_CURRENT_LOCATION)
|
||||
{
|
||||
check_impl(
|
||||
"*3\r\n$12\r\nPUNSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n",
|
||||
pubsub_change_type::punsubscribe,
|
||||
loc);
|
||||
}
|
||||
};
|
||||
|
||||
void test_subscribe_iterators()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::forward_list<std::string_view> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.subscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_subscribe();
|
||||
}
|
||||
|
||||
// Like push_range, if the range is empty, this is a no-op
|
||||
void test_subscribe_iterators_empty()
|
||||
{
|
||||
const std::forward_list<std::string_view> channels;
|
||||
request req;
|
||||
|
||||
req.subscribe(channels.begin(), channels.end());
|
||||
|
||||
BOOST_TEST_EQ(req.payload(), "");
|
||||
BOOST_TEST_EQ(req.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
// Iterators whose value_type is convertible to std::string_view work
|
||||
void test_subscribe_iterators_convertible_string_view()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.subscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_subscribe();
|
||||
}
|
||||
|
||||
// The range overload just dispatches to the iterator one
|
||||
void test_subscribe_range()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.subscribe(channels);
|
||||
|
||||
fix.check_subscribe();
|
||||
}
|
||||
|
||||
// The initializer_list overload just dispatches to the iterator one
|
||||
void test_subscribe_initializer_list()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
|
||||
fix.req.subscribe({"ch1", "ch2"});
|
||||
|
||||
fix.check_subscribe();
|
||||
}
|
||||
|
||||
// --- unsubscribe ---
|
||||
void test_unsubscribe_iterators()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::forward_list<std::string_view> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.unsubscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_unsubscribe();
|
||||
}
|
||||
|
||||
// Like push_range, if the range is empty, this is a no-op
|
||||
void test_unsubscribe_iterators_empty()
|
||||
{
|
||||
const std::forward_list<std::string_view> channels;
|
||||
request req;
|
||||
|
||||
req.unsubscribe(channels.begin(), channels.end());
|
||||
|
||||
BOOST_TEST_EQ(req.payload(), "");
|
||||
BOOST_TEST_EQ(req.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
// Iterators whose value_type is convertible to std::string_view work
|
||||
void test_unsubscribe_iterators_convertible_string_view()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.unsubscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_unsubscribe();
|
||||
}
|
||||
|
||||
// The range overload just dispatches to the iterator one
|
||||
void test_unsubscribe_range()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.unsubscribe(channels);
|
||||
|
||||
fix.check_unsubscribe();
|
||||
}
|
||||
|
||||
// The initializer_list overload just dispatches to the iterator one
|
||||
void test_unsubscribe_initializer_list()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
|
||||
fix.req.unsubscribe({"ch1", "ch2"});
|
||||
|
||||
fix.check_unsubscribe();
|
||||
}
|
||||
|
||||
// --- psubscribe ---
|
||||
void test_psubscribe_iterators()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::forward_list<std::string_view> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.psubscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_psubscribe();
|
||||
}
|
||||
|
||||
// Like push_range, if the range is empty, this is a no-op
|
||||
void test_psubscribe_iterators_empty()
|
||||
{
|
||||
const std::forward_list<std::string_view> channels;
|
||||
request req;
|
||||
|
||||
req.psubscribe(channels.begin(), channels.end());
|
||||
|
||||
BOOST_TEST_EQ(req.payload(), "");
|
||||
BOOST_TEST_EQ(req.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
// Iterators whose value_type is convertible to std::string_view work
|
||||
void test_psubscribe_iterators_convertible_string_view()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.psubscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_psubscribe();
|
||||
}
|
||||
|
||||
// The range overload just dispatches to the iterator one
|
||||
void test_psubscribe_range()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.psubscribe(channels);
|
||||
|
||||
fix.check_psubscribe();
|
||||
}
|
||||
|
||||
// The initializer_list overload just dispatches to the iterator one
|
||||
void test_psubscribe_initializer_list()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
|
||||
fix.req.psubscribe({"ch1", "ch2"});
|
||||
|
||||
fix.check_psubscribe();
|
||||
}
|
||||
|
||||
// --- punsubscribe ---
|
||||
void test_punsubscribe_iterators()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::forward_list<std::string_view> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.punsubscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_punsubscribe();
|
||||
}
|
||||
|
||||
// Like push_range, if the range is empty, this is a no-op
|
||||
void test_punsubscribe_iterators_empty()
|
||||
{
|
||||
const std::forward_list<std::string_view> channels;
|
||||
request req;
|
||||
|
||||
req.punsubscribe(channels.begin(), channels.end());
|
||||
|
||||
BOOST_TEST_EQ(req.payload(), "");
|
||||
BOOST_TEST_EQ(req.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
// Iterators whose value_type is convertible to std::string_view work
|
||||
void test_punsubscribe_iterators_convertible_string_view()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.punsubscribe(channels.begin(), channels.end());
|
||||
|
||||
fix.check_punsubscribe();
|
||||
}
|
||||
|
||||
// The range overload just dispatches to the iterator one
|
||||
void test_punsubscribe_range()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
const std::vector<std::string> channels{"ch1", "ch2"};
|
||||
|
||||
fix.req.punsubscribe(channels);
|
||||
|
||||
fix.check_punsubscribe();
|
||||
}
|
||||
|
||||
// The initializer_list overload just dispatches to the iterator one
|
||||
void test_punsubscribe_initializer_list()
|
||||
{
|
||||
subscribe_fixture fix;
|
||||
|
||||
fix.req.punsubscribe({"ch1", "ch2"});
|
||||
|
||||
fix.check_punsubscribe();
|
||||
}
|
||||
|
||||
// Mixing regular commands and pubsub commands is OK
|
||||
void test_mix_pubsub_regular()
|
||||
{
|
||||
request req;
|
||||
req.push("PING");
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
req.push("GET", "key");
|
||||
req.punsubscribe({"ch4*"});
|
||||
|
||||
constexpr std::string_view expected =
|
||||
"*1\r\n$4\r\nPING\r\n"
|
||||
"*3\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n$3\r\nch2\r\n"
|
||||
"*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n"
|
||||
"*2\r\n$12\r\nPUNSUBSCRIBE\r\n$4\r\nch4*\r\n";
|
||||
BOOST_TEST_EQ(req.payload(), expected);
|
||||
BOOST_TEST_EQ(req.get_commands(), 4u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 2u);
|
||||
constexpr pubsub_change_str expected_changes[] = {
|
||||
{pubsub_change_type::subscribe, "ch1" },
|
||||
{pubsub_change_type::subscribe, "ch2" },
|
||||
{pubsub_change_type::punsubscribe, "ch4*"},
|
||||
};
|
||||
check_pubsub_changes(req, expected_changes);
|
||||
}
|
||||
|
||||
// --- append ---
|
||||
void test_append()
|
||||
{
|
||||
request req1;
|
||||
req1.push("PING", "req1");
|
||||
|
||||
request req2;
|
||||
req2.push("GET", "mykey");
|
||||
req2.push("GET", "other");
|
||||
|
||||
req1.append(req2);
|
||||
|
||||
constexpr std::string_view expected =
|
||||
"*2\r\n$4\r\nPING\r\n$4\r\nreq1\r\n"
|
||||
"*2\r\n$3\r\nGET\r\n$5\r\nmykey\r\n"
|
||||
"*2\r\n$3\r\nGET\r\n$5\r\nother\r\n";
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
BOOST_TEST_EQ(req1.get_commands(), 3u);
|
||||
BOOST_TEST_EQ(req1.get_expected_responses(), 3u);
|
||||
check_pubsub_changes(req1, {});
|
||||
}
|
||||
|
||||
// Commands without responses are handled correctly
|
||||
void test_append_no_response()
|
||||
{
|
||||
request req1;
|
||||
req1.push("PING", "req1");
|
||||
|
||||
request req2;
|
||||
req2.push("SUBSCRIBE", "mychannel");
|
||||
req2.push("GET", "other");
|
||||
|
||||
req1.append(req2);
|
||||
|
||||
constexpr std::string_view expected =
|
||||
"*2\r\n$4\r\nPING\r\n$4\r\nreq1\r\n"
|
||||
"*2\r\n$9\r\nSUBSCRIBE\r\n$9\r\nmychannel\r\n"
|
||||
"*2\r\n$3\r\nGET\r\n$5\r\nother\r\n";
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
BOOST_TEST_EQ(req1.get_commands(), 3u);
|
||||
BOOST_TEST_EQ(req1.get_expected_responses(), 2u);
|
||||
check_pubsub_changes(req1, {});
|
||||
}
|
||||
|
||||
// Flags are not modified by append
|
||||
void test_append_flags()
|
||||
{
|
||||
request req1;
|
||||
req1.get_config().cancel_if_not_connected = false;
|
||||
req1.get_config().cancel_if_unresponded = false;
|
||||
req1.get_config().cancel_on_connection_lost = false;
|
||||
req1.push("PING", "req1");
|
||||
|
||||
request req2;
|
||||
req2.get_config().cancel_if_not_connected = true;
|
||||
req2.get_config().cancel_if_unresponded = true;
|
||||
req2.get_config().cancel_on_connection_lost = true;
|
||||
req2.push("GET", "other");
|
||||
|
||||
req1.append(req2);
|
||||
|
||||
constexpr std::string_view expected =
|
||||
"*2\r\n$4\r\nPING\r\n$4\r\nreq1\r\n"
|
||||
"*2\r\n$3\r\nGET\r\n$5\r\nother\r\n";
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
BOOST_TEST_NOT(req1.get_config().cancel_if_not_connected);
|
||||
BOOST_TEST_NOT(req1.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST_NOT(req1.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
// Empty requests don't cause problems with append
|
||||
void test_append_target_empty()
|
||||
{
|
||||
request req1;
|
||||
|
||||
request req2;
|
||||
req2.push("GET", "other");
|
||||
|
||||
req1.append(req2);
|
||||
|
||||
constexpr std::string_view expected = "*2\r\n$3\r\nGET\r\n$5\r\nother\r\n";
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
BOOST_TEST_EQ(req1.get_commands(), 1u);
|
||||
BOOST_TEST_EQ(req1.get_expected_responses(), 1u);
|
||||
check_pubsub_changes(req1, {});
|
||||
}
|
||||
|
||||
void test_append_source_empty()
|
||||
{
|
||||
request req1;
|
||||
req1.push("GET", "other");
|
||||
|
||||
request req2;
|
||||
|
||||
req1.append(req2);
|
||||
|
||||
constexpr std::string_view expected = "*2\r\n$3\r\nGET\r\n$5\r\nother\r\n";
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
BOOST_TEST_EQ(req1.get_commands(), 1u);
|
||||
BOOST_TEST_EQ(req1.get_expected_responses(), 1u);
|
||||
check_pubsub_changes(req1, {});
|
||||
}
|
||||
|
||||
void test_append_both_empty()
|
||||
{
|
||||
request req1;
|
||||
request req2;
|
||||
|
||||
req1.append(req2);
|
||||
|
||||
BOOST_TEST_EQ(req1.payload(), "");
|
||||
BOOST_TEST_EQ(req1.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req1.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req1, {});
|
||||
}
|
||||
|
||||
// Append correctly handles requests with pubsub changes
|
||||
void test_append_pubsub()
|
||||
{
|
||||
request req1;
|
||||
req1.subscribe({"ch1"});
|
||||
|
||||
auto req2 = std::make_unique<request>();
|
||||
req2->unsubscribe({"ch2"});
|
||||
req2->psubscribe({"really_very_long_pattern_name*"});
|
||||
|
||||
req1.append(*req2);
|
||||
req2.reset(); // make sure we don't leave dangling pointers
|
||||
|
||||
constexpr std::string_view expected =
|
||||
"*2\r\n$9\r\nSUBSCRIBE\r\n$3\r\nch1\r\n"
|
||||
"*2\r\n$11\r\nUNSUBSCRIBE\r\n$3\r\nch2\r\n"
|
||||
"*2\r\n$10\r\nPSUBSCRIBE\r\n$30\r\nreally_very_long_pattern_name*\r\n";
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
const pubsub_change_str expected_changes[] = {
|
||||
{pubsub_change_type::subscribe, "ch1" },
|
||||
{pubsub_change_type::unsubscribe, "ch2" },
|
||||
{pubsub_change_type::psubscribe, "really_very_long_pattern_name*"},
|
||||
};
|
||||
check_pubsub_changes(req1, expected_changes);
|
||||
}
|
||||
|
||||
// If the target is empty and the source has pubsub changes, that's OK
|
||||
void test_append_pubsub_target_empty()
|
||||
{
|
||||
request req1;
|
||||
|
||||
request req2;
|
||||
req2.punsubscribe({"ch2"});
|
||||
|
||||
req1.append(req2);
|
||||
|
||||
constexpr std::string_view expected = "*2\r\n$12\r\nPUNSUBSCRIBE\r\n$3\r\nch2\r\n";
|
||||
BOOST_TEST_EQ(req1.payload(), expected);
|
||||
const pubsub_change_str expected_changes[] = {
|
||||
{pubsub_change_type::punsubscribe, "ch2"},
|
||||
};
|
||||
check_pubsub_changes(req1, expected_changes);
|
||||
}
|
||||
|
||||
// --- clear ---
|
||||
void test_clear()
|
||||
{
|
||||
// Create request with some commands and some pubsub changes
|
||||
request req;
|
||||
req.push("PING", "value");
|
||||
req.push("GET", "key");
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
req.punsubscribe({"ch3*"});
|
||||
|
||||
// Clear removes the payload, the commands and the pubsub changes
|
||||
req.clear();
|
||||
BOOST_TEST_EQ(req.payload(), "");
|
||||
BOOST_TEST_EQ(req.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
|
||||
// Clearing again does nothing
|
||||
req.clear();
|
||||
BOOST_TEST_EQ(req.payload(), "");
|
||||
BOOST_TEST_EQ(req.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
// Clearing an empty request doesn't cause trouble
|
||||
void test_clear_empty()
|
||||
{
|
||||
request req;
|
||||
|
||||
req.clear();
|
||||
|
||||
BOOST_TEST_EQ(req.payload(), "");
|
||||
BOOST_TEST_EQ(req.get_commands(), 0u);
|
||||
BOOST_TEST_EQ(req.get_expected_responses(), 0u);
|
||||
check_pubsub_changes(req, {});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_push_no_args();
|
||||
test_push_int();
|
||||
test_push_multiple_args();
|
||||
test_push_pubsub();
|
||||
|
||||
test_push_range();
|
||||
test_push_range_pubsub();
|
||||
|
||||
test_subscribe_iterators();
|
||||
test_subscribe_iterators_empty();
|
||||
test_subscribe_iterators_convertible_string_view();
|
||||
test_subscribe_range();
|
||||
test_subscribe_initializer_list();
|
||||
|
||||
test_unsubscribe_iterators();
|
||||
test_unsubscribe_iterators_empty();
|
||||
test_unsubscribe_iterators_convertible_string_view();
|
||||
test_unsubscribe_range();
|
||||
test_unsubscribe_initializer_list();
|
||||
|
||||
test_psubscribe_iterators();
|
||||
test_psubscribe_iterators_empty();
|
||||
test_psubscribe_iterators_convertible_string_view();
|
||||
test_psubscribe_range();
|
||||
test_psubscribe_initializer_list();
|
||||
|
||||
test_punsubscribe_iterators();
|
||||
test_punsubscribe_iterators_empty();
|
||||
test_punsubscribe_iterators_convertible_string_view();
|
||||
test_punsubscribe_range();
|
||||
test_punsubscribe_initializer_list();
|
||||
|
||||
test_mix_pubsub_regular();
|
||||
|
||||
test_append();
|
||||
test_append_no_response();
|
||||
test_append_flags();
|
||||
test_append_target_empty();
|
||||
test_append_source_empty();
|
||||
test_append_both_empty();
|
||||
test_append_pubsub();
|
||||
test_append_pubsub_target_empty();
|
||||
|
||||
test_clear();
|
||||
test_clear_empty();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ static const char* to_string(run_action_type value)
|
||||
switch (value) {
|
||||
case run_action_type::done: return "run_action_type::done";
|
||||
case run_action_type::immediate: return "run_action_type::immediate";
|
||||
case run_action_type::sentinel_resolve: return "run_action_type::sentinel_resolve";
|
||||
case run_action_type::connect: return "run_action_type::connect";
|
||||
case run_action_type::parallel_group: return "run_action_type::parallel_group";
|
||||
case run_action_type::cancel_receive: return "run_action_type::cancel_receive";
|
||||
@@ -142,6 +143,30 @@ void test_config_error_unix_ssl()
|
||||
});
|
||||
}
|
||||
|
||||
void test_config_error_unix_sentinel()
|
||||
{
|
||||
// Setup
|
||||
config cfg;
|
||||
cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
cfg.unix_socket = "/var/sock";
|
||||
fixture fix{std::move(cfg)};
|
||||
|
||||
// Launching the operation fails immediately
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::immediate);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_unix_sockets_unsupported));
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::err,
|
||||
"Invalid configuration: The configuration specified UNIX sockets with Sentinel, which is "
|
||||
"not supported. [boost.redis:28]"},
|
||||
});
|
||||
}
|
||||
|
||||
// An error in connect with reconnection enabled triggers a reconnection
|
||||
void test_connect_error()
|
||||
{
|
||||
@@ -162,10 +187,83 @@ void test_connect_error()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at 127.0.0.1:6379 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// Check logs for other transport types
|
||||
void test_connect_error_ssl()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.addr = {"my_hostname", "10000"};
|
||||
fix.st.cfg.use_ssl = true;
|
||||
|
||||
// Launch the operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Connect errors. We sleep and try to connect again
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// This time we succeed and we launch the parallel group
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at my_hostname:10000 (TLS enabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at my_hostname:10000 (TLS enabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at my_hostname:10000 (TLS enabled)" },
|
||||
{logger::level::info, "Connected to Redis server at my_hostname:10000 (TLS enabled)" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
void test_connect_error_unix()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.unix_socket = "/tmp/sock";
|
||||
|
||||
// Launch the operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Connect errors. We sleep and try to connect again
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// This time we succeed and we launch the parallel group
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at '/tmp/sock'" },
|
||||
{logger::level::info, "Failed to connect to Redis server at '/tmp/sock': Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at '/tmp/sock'" },
|
||||
{logger::level::info, "Connected to Redis server at '/tmp/sock'" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// An error in connect without reconnection enabled makes the operation finish
|
||||
void test_connect_error_no_reconnect()
|
||||
{
|
||||
@@ -180,8 +278,13 @@ void test_connect_error_no_reconnect()
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::connect_timeout));
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at 127.0.0.1:6379 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// A cancellation in connect makes the operation finish even with reconnection enabled
|
||||
@@ -198,9 +301,10 @@ void test_connect_cancel()
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (1)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::debug, "Run: cancelled (1)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,9 +322,10 @@ void test_connect_cancel_edge()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (1)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::debug, "Run: cancelled (1)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,8 +352,13 @@ void test_parallel_group_error()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
});
|
||||
}
|
||||
|
||||
// An error in the parallel group makes the operation exit if reconnection is disabled
|
||||
@@ -269,8 +379,11 @@ void test_parallel_group_error_no_reconnect()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::empty_field));
|
||||
|
||||
// Run doesn't log, it's the subordinate tasks that do
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
});
|
||||
}
|
||||
|
||||
// A cancellation in the parallel group makes it exit, even if reconnection is enabled.
|
||||
@@ -292,9 +405,11 @@ void test_parallel_group_cancel()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (2)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (2)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,9 +430,11 @@ void test_parallel_group_cancel_no_reconnect()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (2)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (2)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -343,9 +460,11 @@ void test_wait_cancel()
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (3)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (3)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -370,9 +489,11 @@ void test_wait_cancel_edge()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// We log on cancellation only
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (3)"}
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (3)" }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -409,9 +530,16 @@ void test_several_reconnections()
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// The cancellation was logged
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (2)"}
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Failed to connect to Redis server at 127.0.0.1:6379 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::debug, "Run: cancelled (2)" } // clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
@@ -437,7 +565,7 @@ void test_setup_ping_requests()
|
||||
const std::string_view
|
||||
expected_setup = "*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";
|
||||
BOOST_TEST_EQ(fix.st.ping_req.payload(), expected_ping);
|
||||
BOOST_TEST_EQ(fix.st.cfg.setup.payload(), expected_setup);
|
||||
BOOST_TEST_EQ(fix.st.setup_req.payload(), expected_setup);
|
||||
|
||||
// Reconnect
|
||||
act = fix.fsm.resume(fix.st, error::empty_field, cancellation_type_t::none);
|
||||
@@ -451,7 +579,7 @@ void test_setup_ping_requests()
|
||||
|
||||
// The requests haven't been modified
|
||||
BOOST_TEST_EQ(fix.st.ping_req.payload(), expected_ping);
|
||||
BOOST_TEST_EQ(fix.st.cfg.setup.payload(), expected_setup);
|
||||
BOOST_TEST_EQ(fix.st.setup_req.payload(), expected_setup);
|
||||
}
|
||||
|
||||
// We correctly send and log the setup request
|
||||
@@ -481,7 +609,11 @@ void test_setup_request_success()
|
||||
|
||||
// Check log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Setup request execution: success"}
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Setup request execution: success"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
@@ -501,8 +633,13 @@ void test_setup_request_empty()
|
||||
// Nothing was added to the multiplexer
|
||||
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 0u);
|
||||
|
||||
// Check log
|
||||
fix.check_log({});
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// A server error would cause the reader to exit
|
||||
@@ -510,7 +647,7 @@ void test_setup_request_server_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.setup_diagnostic = "leftover"; // simulate a leftover from previous runs
|
||||
fix.st.diagnostic = "leftover"; // simulate a leftover from previous runs
|
||||
fix.st.cfg.setup.clear();
|
||||
fix.st.cfg.setup.push("HELLO", 3);
|
||||
|
||||
@@ -533,9 +670,147 @@ void test_setup_request_server_error()
|
||||
|
||||
// Check log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at 127.0.0.1:6379 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at 127.0.0.1:6379 (TLS disabled)" },
|
||||
{logger::level::info,
|
||||
"Setup request execution: The server response to the setup request sent during connection "
|
||||
"establishment contains an error. [boost.redis:23] (ERR: wrong command)"}
|
||||
"establishment contains an error. [boost.redis:23] (ERR: wrong command)" }
|
||||
});
|
||||
}
|
||||
|
||||
// When using Sentinel, reconnection works normally
|
||||
void test_sentinel_reconnection()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Resolve succeeds, and a connection is attempted
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"host1", "1000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// This errors, so we sleep and resolve again
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"host2", "2000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::parallel_group);
|
||||
|
||||
// Sentinel involves always a setup request containing the role check. Run it.
|
||||
BOOST_TEST_EQ(fix.st.mpx.prepare_write(), 1u);
|
||||
BOOST_TEST(fix.st.mpx.commit_write(fix.st.mpx.get_write_buffer().size()));
|
||||
read(fix.st.mpx, "*1\r\n$6\r\nmaster\r\n");
|
||||
error_code ec;
|
||||
auto res = fix.st.mpx.consume(ec);
|
||||
BOOST_TEST_EQ(ec, error_code());
|
||||
BOOST_TEST(res.first == detail::consume_result::got_response);
|
||||
|
||||
// The parallel group errors, so we sleep and resolve again
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::cancel_receive);
|
||||
act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"host3", "3000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Cancel
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to connect to Redis server at host1:1000 (TLS disabled)"},
|
||||
{logger::level::info, "Failed to connect to Redis server at host1:1000 (TLS disabled): Connect timeout. [boost.redis:18]"},
|
||||
{logger::level::info, "Trying to connect to Redis server at host2:2000 (TLS disabled)"},
|
||||
{logger::level::info, "Connected to Redis server at host2:2000 (TLS disabled)"},
|
||||
{logger::level::info, "Setup request execution: success"},
|
||||
{logger::level::info, "Trying to connect to Redis server at host3:3000 (TLS disabled)"},
|
||||
{logger::level::debug, "Run: cancelled (1)"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// If the Sentinel resolve operation errors, we try again
|
||||
void test_sentinel_resolve_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Start the Sentinel resolve operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
|
||||
// It fails with an error, so we go to sleep
|
||||
act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::wait_for_reconnection);
|
||||
|
||||
// Retrying it succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
fix.st.cfg.addr = {"myhost", "10000"};
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::connect);
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to connect to Redis server at myhost:10000 (TLS disabled)"},
|
||||
});
|
||||
}
|
||||
|
||||
// The reconnection setting affects Sentinel reconnection, too
|
||||
void test_sentinel_resolve_error_no_reconnect()
|
||||
{
|
||||
// Setup
|
||||
fixture fix{config_no_reconnect()};
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Start the Sentinel resolve operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
|
||||
// It fails with an error, so we exit
|
||||
act = fix.fsm.resume(fix.st, error::sentinel_resolve_failed, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed));
|
||||
|
||||
// Log
|
||||
fix.check_log({});
|
||||
}
|
||||
|
||||
void test_sentinel_resolve_cancel()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.addresses = {
|
||||
{"localhost", "26379"}
|
||||
};
|
||||
|
||||
// Start the Sentinel resolve operation
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, run_action_type::sentinel_resolve);
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Log
|
||||
fix.check_log({
|
||||
{logger::level::debug, "Run: cancelled (4)"},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -547,8 +822,13 @@ int main()
|
||||
test_config_error_unix();
|
||||
#endif
|
||||
test_config_error_unix_ssl();
|
||||
test_config_error_unix_sentinel();
|
||||
|
||||
test_connect_error();
|
||||
test_connect_error_ssl();
|
||||
#ifdef BOOST_ASIO_HAS_LOCAL_SOCKETS
|
||||
test_connect_error_unix();
|
||||
#endif
|
||||
test_connect_error_no_reconnect();
|
||||
test_connect_cancel();
|
||||
test_connect_cancel_edge();
|
||||
@@ -568,5 +848,10 @@ int main()
|
||||
test_setup_request_empty();
|
||||
test_setup_request_server_error();
|
||||
|
||||
test_sentinel_reconnection();
|
||||
test_sentinel_resolve_error();
|
||||
test_sentinel_resolve_error_no_reconnect();
|
||||
test_sentinel_resolve_cancel();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
|
||||
682
test/test_sentinel_resolve_fsm.cpp
Normal file
682
test/test_sentinel_resolve_fsm.cpp
Normal file
@@ -0,0 +1,682 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/detail/sentinel_resolve_fsm.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
|
||||
#include <boost/asio/cancellation_type.hpp>
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include "sansio_utils.hpp"
|
||||
|
||||
#include <iterator>
|
||||
|
||||
using namespace boost::redis;
|
||||
namespace asio = boost::asio;
|
||||
using detail::sentinel_resolve_fsm;
|
||||
using detail::sentinel_action;
|
||||
using detail::connection_state;
|
||||
using detail::nodes_from_resp3;
|
||||
using boost::system::error_code;
|
||||
using boost::asio::cancellation_type_t;
|
||||
|
||||
static char const* to_string(sentinel_action::type t)
|
||||
{
|
||||
switch (t) {
|
||||
case sentinel_action::type::done: return "sentinel_action::type::done";
|
||||
case sentinel_action::type::connect: return "sentinel_action::type::connect";
|
||||
case sentinel_action::type::request: return "sentinel_action::type::request";
|
||||
default: return "sentinel_action::type::<invalid type>";
|
||||
}
|
||||
}
|
||||
|
||||
// Operators
|
||||
namespace boost::redis::detail {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, sentinel_action::type type)
|
||||
{
|
||||
os << to_string(type);
|
||||
return os;
|
||||
}
|
||||
|
||||
bool operator==(sentinel_action lhs, sentinel_action rhs) noexcept
|
||||
{
|
||||
if (lhs.get_type() != rhs.get_type())
|
||||
return false;
|
||||
else if (lhs.get_type() == sentinel_action::type::done)
|
||||
return lhs.error() == rhs.error();
|
||||
else if (lhs.get_type() == sentinel_action::type::connect)
|
||||
return lhs.connect_addr() == rhs.connect_addr();
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, sentinel_action act)
|
||||
{
|
||||
os << "exec_action{ .type=" << act.get_type();
|
||||
if (act.get_type() == sentinel_action::type::done)
|
||||
os << ", .error=" << act.error();
|
||||
else if (act.get_type() == sentinel_action::type::connect)
|
||||
os << ", .addr=" << act.connect_addr().host << ":" << act.connect_addr().port;
|
||||
return os << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis::detail
|
||||
|
||||
namespace boost::redis {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const address& addr)
|
||||
{
|
||||
return os << "address{ .host=" << addr.host << ", .port=" << addr.port << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
namespace {
|
||||
|
||||
struct fixture : detail::log_fixture {
|
||||
connection_state st{{make_logger()}};
|
||||
sentinel_resolve_fsm fsm;
|
||||
|
||||
fixture()
|
||||
{
|
||||
st.sentinels = {
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
st.cfg.sentinel.addresses = {
|
||||
{"host1", "1000"},
|
||||
{"host4", "4000"},
|
||||
};
|
||||
st.cfg.sentinel.master_name = "mymaster";
|
||||
}
|
||||
};
|
||||
|
||||
void test_success()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Now send the request
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$8\r\nhost.one\r\n$4\r\nport\r\n$5\r\n26380\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// We received a valid request, so we're done
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// The Sentinel list is updated
|
||||
const address expected_sentinels[] = {
|
||||
{"host1", "1000" },
|
||||
{"host.one", "26380"},
|
||||
{"host4", "4000" },
|
||||
};
|
||||
BOOST_TEST_ALL_EQ(
|
||||
fix.st.sentinels.begin(),
|
||||
fix.st.sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000 resolved the server address to test.host:6380"},
|
||||
});
|
||||
}
|
||||
|
||||
void test_success_replica()
|
||||
{
|
||||
// Setup. Seed the engine so that it returns index 1
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.server_role = role::replica;
|
||||
fix.st.eng.get().seed(static_cast<std::uint_fast32_t>(183984887232u));
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Now send the request
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*3\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.one\r\n$4\r\nport\r\n$4\r\n6379\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.two\r\n$4\r\nport\r\n$4\r\n6379\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.thr\r\n$4\r\nport\r\n$4\r\n6379\r\n",
|
||||
"*0\r\n"
|
||||
// clang-format on
|
||||
});
|
||||
|
||||
// We received a valid request, so we're done
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The address of one of the replicas is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"replica.two", "6379"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of a replica of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000 resolved the server address to replica.two:6379" },
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel fails connection, but subsequent ones succeed
|
||||
void test_one_connect_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// This errors, so we connect to the 2nd sentinel
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
|
||||
// Now send the request
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
|
||||
// We received a valid request, so we're done
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: connection establishment error: Connect timeout. [boost.redis:18]" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel fails while executing the request, but subsequent ones succeed
|
||||
void test_one_request_network_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
|
||||
// It fails, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error::write_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: error while executing request: Timeout while writing data to the server. [boost.redis:27]"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel responds with an invalid message, but subsequent ones succeed
|
||||
void test_one_request_parse_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"+OK\r\n",
|
||||
"+OK\r\n",
|
||||
});
|
||||
|
||||
// This fails parsing, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: error parsing response (maybe forgot to upgrade to RESP3?): "
|
||||
"Expects a RESP3 array, but got a different data type. [boost.redis:32]"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel responds with an error (e.g. failed auth), but subsequent ones succeed
|
||||
void test_one_request_error_node()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"-ERR needs authentication\r\n",
|
||||
"-ERR needs authentication\r\n",
|
||||
});
|
||||
|
||||
// This fails, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: responded with an error: ERR needs authentication"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel doesn't know about the master, but others do
|
||||
void test_one_master_unknown()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"_\r\n",
|
||||
"-ERR unknown master\r\n",
|
||||
});
|
||||
|
||||
// It doesn't know about our master, so we connect to the 2nd sentinel.
|
||||
// This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The master's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"test.host", "6380"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: doesn't know about the configured master" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to test.host:6380"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The first Sentinel thinks there are no replicas (stale data?), but others do
|
||||
void test_one_no_replicas()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.cfg.sentinel.server_role = role::replica;
|
||||
|
||||
// Initiate, connect to the 1st Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
|
||||
// This errors, so we connect to the 2nd sentinel. This one succeeds
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
// clang-format off
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*1\r\n"
|
||||
"%2\r\n"
|
||||
"$2\r\nip\r\n$11\r\nreplica.one\r\n$4\r\nport\r\n$4\r\n6379\r\n",
|
||||
"*0\r\n",
|
||||
// clang-format on
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code());
|
||||
|
||||
// The replica's address is stored
|
||||
BOOST_TEST_EQ(fix.st.cfg.addr, (address{"replica.one", "6379"}));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of a replica of master 'mymaster' using Sentinel" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: the configured master has no replicas" },
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000 resolved the server address to replica.one:6379"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// If no Sentinel is available, the operation fails. A comprehensive error is logged.
|
||||
void test_error()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// 1st Sentinel doesn't know about the master
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"_\r\n",
|
||||
"-ERR unknown master\r\n",
|
||||
});
|
||||
|
||||
// Move to the 2nd Sentinel, which fails to connect
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host2", "2000"}));
|
||||
|
||||
// Move to the 3rd Sentinel, which has authentication misconfigured
|
||||
act = fix.fsm.resume(fix.st, error::connect_timeout, cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host3", "3000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"-ERR unauthorized\r\n",
|
||||
"-ERR unauthorized\r\n",
|
||||
});
|
||||
|
||||
// Sentinel list exhausted
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed));
|
||||
|
||||
// The Sentinel list is not updated
|
||||
BOOST_TEST_EQ(fix.st.sentinels.size(), 3u);
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: doesn't know about the configured master" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host2:2000" },
|
||||
{logger::level::info, "Sentinel at host2:2000: connection establishment error: Connect timeout. [boost.redis:18]" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host3:3000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host3:3000" },
|
||||
{logger::level::info, "Sentinel at host3:3000: responded with an error: ERR unauthorized"},
|
||||
|
||||
{logger::level::err, "Failed to resolve the address of master 'mymaster'. Tried the following Sentinels:"
|
||||
"\n Sentinel at host1:1000: doesn't know about the configured master"
|
||||
"\n Sentinel at host2:2000: connection establishment error: Connect timeout. [boost.redis:18]"
|
||||
"\n Sentinel at host3:3000: responded with an error: ERR unauthorized"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// The replica error text is slightly different
|
||||
void test_error_replica()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
fix.st.sentinels = {
|
||||
{"host1", "1000"}
|
||||
};
|
||||
fix.st.cfg.sentinel.server_role = role::replica;
|
||||
|
||||
// Initiate, connect to the only Sentinel, and send the request
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
fix.st.sentinel_resp_nodes = nodes_from_resp3({
|
||||
"*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n",
|
||||
"*0\r\n",
|
||||
"*0\r\n",
|
||||
});
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, error_code(error::sentinel_resolve_failed));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
// clang-format off
|
||||
{logger::level::info, "Trying to resolve the address of a replica of master 'mymaster' using Sentinel" },
|
||||
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::info, "Sentinel at host1:1000: the configured master has no replicas" },
|
||||
|
||||
{logger::level::err, "Failed to resolve the address of a replica of master 'mymaster'. Tried the following Sentinels:"
|
||||
"\n Sentinel at host1:1000: the configured master has no replicas"},
|
||||
// clang-format on
|
||||
});
|
||||
}
|
||||
|
||||
// Cancellations
|
||||
void test_cancel_connect()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Cancellation
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (1)" },
|
||||
});
|
||||
}
|
||||
|
||||
void test_cancel_connect_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
|
||||
// Cancellation (without error code)
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (1)" },
|
||||
});
|
||||
}
|
||||
|
||||
void test_cancel_request()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
act = fix.fsm.resume(fix.st, asio::error::operation_aborted, cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (2)" },
|
||||
});
|
||||
}
|
||||
|
||||
void test_cancel_request_edge()
|
||||
{
|
||||
// Setup
|
||||
fixture fix;
|
||||
|
||||
// Initiate. We should connect to the 1st sentinel
|
||||
auto act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, (address{"host1", "1000"}));
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::none);
|
||||
BOOST_TEST_EQ(act, sentinel_action::request());
|
||||
act = fix.fsm.resume(fix.st, error_code(), cancellation_type_t::terminal);
|
||||
BOOST_TEST_EQ(act, error_code(asio::error::operation_aborted));
|
||||
|
||||
// Logs
|
||||
fix.check_log({
|
||||
{logger::level::info, "Trying to resolve the address of master 'mymaster' using Sentinel"},
|
||||
{logger::level::debug, "Trying to contact Sentinel at host1:1000" },
|
||||
{logger::level::debug, "Executing Sentinel request at host1:1000" },
|
||||
{logger::level::debug, "Sentinel resolve: cancelled (2)" },
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_success();
|
||||
test_success_replica();
|
||||
|
||||
test_one_connect_error();
|
||||
test_one_request_network_error();
|
||||
test_one_request_parse_error();
|
||||
test_one_request_error_node();
|
||||
test_one_master_unknown();
|
||||
test_one_no_replicas();
|
||||
|
||||
test_error();
|
||||
test_error_replica();
|
||||
|
||||
test_cancel_connect();
|
||||
test_cancel_connect_edge();
|
||||
test_cancel_request();
|
||||
test_cancel_request_edge();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
193
test/test_serialization.cpp
Normal file
193
test/test_serialization.cpp
Normal file
@@ -0,0 +1,193 @@
|
||||
//
|
||||
// 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();
|
||||
}
|
||||
349
test/test_setup_adapter.cpp
Normal file
349
test/test_setup_adapter.cpp
Normal file
@@ -0,0 +1,349 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/detail/connection_state.hpp>
|
||||
#include <boost/redis/impl/setup_request_utils.hpp>
|
||||
#include <boost/redis/resp3/parser.hpp>
|
||||
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/detail/error_code.hpp>
|
||||
#include <boost/system/result.hpp>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
using namespace boost::redis;
|
||||
using detail::setup_adapter;
|
||||
using detail::connection_state;
|
||||
using detail::compose_setup_request;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
void test_success()
|
||||
{
|
||||
// Setup
|
||||
connection_state st;
|
||||
st.cfg.use_setup = true;
|
||||
st.cfg.setup.push("SELECT", 2);
|
||||
compose_setup_request(st.cfg, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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, st.tracker, st.setup_req);
|
||||
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();
|
||||
}
|
||||
@@ -1,196 +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/result.hpp>
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/error.hpp>
|
||||
#include <boost/redis/impl/setup_request_utils.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
#include <boost/redis/resp3/type.hpp>
|
||||
#include <boost/redis/response.hpp>
|
||||
|
||||
#include <boost/asio/error.hpp>
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/result.hpp>
|
||||
|
||||
namespace asio = boost::asio;
|
||||
namespace redis = boost::redis;
|
||||
using redis::detail::compose_setup_request;
|
||||
using boost::system::error_code;
|
||||
|
||||
namespace {
|
||||
|
||||
void test_compose_setup()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.clientname = "";
|
||||
|
||||
compose_setup_request(cfg);
|
||||
|
||||
std::string_view const expected = "*2\r\n$5\r\nHELLO\r\n$1\r\n3\r\n";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
void test_compose_setup_select()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.clientname = "";
|
||||
cfg.database_index = 10;
|
||||
|
||||
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\n10\r\n";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
void test_compose_setup_clientname()
|
||||
{
|
||||
redis::config cfg;
|
||||
|
||||
compose_setup_request(cfg);
|
||||
|
||||
std::string_view const
|
||||
expected = "*4\r\n$5\r\nHELLO\r\n$1\r\n3\r\n$7\r\nSETNAME\r\n$11\r\nBoost.Redis\r\n";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
void test_compose_setup_auth()
|
||||
{
|
||||
redis::config cfg;
|
||||
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";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
void test_compose_setup_auth_empty_password()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.clientname = "";
|
||||
cfg.username = "foo";
|
||||
|
||||
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$0\r\n\r\n";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
void test_compose_setup_auth_setname()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.clientname = "mytest";
|
||||
cfg.username = "foo";
|
||||
cfg.password = "bar";
|
||||
|
||||
compose_setup_request(cfg);
|
||||
|
||||
std::string_view const expected =
|
||||
"*7\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$7\r\nSETNAME\r\n$"
|
||||
"6\r\nmytest\r\n";
|
||||
BOOST_TEST_EQ(cfg.setup.payload(), expected);
|
||||
BOOST_TEST(cfg.setup.has_hello_priority());
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_if_unresponded);
|
||||
BOOST_TEST(cfg.setup.get_config().cancel_on_connection_lost);
|
||||
}
|
||||
|
||||
void test_compose_setup_use_setup()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.clientname = "mytest";
|
||||
cfg.username = "foo";
|
||||
cfg.password = "bar";
|
||||
cfg.database_index = 4;
|
||||
cfg.use_setup = true;
|
||||
cfg.setup.push("SELECT", 8);
|
||||
|
||||
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$1\r\n8\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);
|
||||
}
|
||||
|
||||
// Regression check: we set the priority flag
|
||||
void test_compose_setup_use_setup_no_hello()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.use_setup = true;
|
||||
cfg.setup.clear();
|
||||
cfg.setup.push("SELECT", 8);
|
||||
|
||||
compose_setup_request(cfg);
|
||||
|
||||
std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\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);
|
||||
}
|
||||
|
||||
// Regression check: we set the relevant cancellation flags in the request
|
||||
void test_compose_setup_use_setup_flags()
|
||||
{
|
||||
redis::config cfg;
|
||||
cfg.use_setup = true;
|
||||
cfg.setup.clear();
|
||||
cfg.setup.push("SELECT", 8);
|
||||
cfg.setup.get_config().cancel_if_unresponded = false;
|
||||
cfg.setup.get_config().cancel_on_connection_lost = false;
|
||||
|
||||
compose_setup_request(cfg);
|
||||
|
||||
std::string_view const expected = "*2\r\n$6\r\nSELECT\r\n$1\r\n8\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_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();
|
||||
}
|
||||
276
test/test_subscription_tracker.cpp
Normal file
276
test/test_subscription_tracker.cpp
Normal file
@@ -0,0 +1,276 @@
|
||||
//
|
||||
// Copyright (c) 2025-2026 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/subscription_tracker.hpp>
|
||||
#include <boost/redis/request.hpp>
|
||||
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
|
||||
using namespace boost::redis;
|
||||
using detail::subscription_tracker;
|
||||
|
||||
namespace {
|
||||
|
||||
// State originated by SUBSCRIBE commands, only
|
||||
void test_subscribe()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req1, req2, req_output, req_expected;
|
||||
|
||||
// Add some changes to the tracker
|
||||
req1.subscribe({"channel_a", "channel_b"});
|
||||
tracker.commit_changes(req1);
|
||||
|
||||
req2.subscribe({"channel_c"});
|
||||
tracker.commit_changes(req2);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "channel_a", "channel_b", "channel_c");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// State originated by PSUBSCRIBE commands, only
|
||||
void test_psubscribe()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req1, req2, req_output, req_expected;
|
||||
|
||||
// Add some changes to the tracker
|
||||
req1.psubscribe({"channel_b*", "channel_c*"});
|
||||
tracker.commit_changes(req1);
|
||||
|
||||
req2.psubscribe({"channel_a*"});
|
||||
tracker.commit_changes(req2);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("PSUBSCRIBE", "channel_a*", "channel_b*", "channel_c*"); // we sort them
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// We can mix SUBSCRIBE and PSUBSCRIBE operations
|
||||
void test_subscribe_psubscribe()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req1, req2, req_output, req_expected;
|
||||
|
||||
// Add some changes to the tracker
|
||||
req1.psubscribe({"channel_a*", "channel_b*"});
|
||||
req1.subscribe({"ch1"});
|
||||
tracker.commit_changes(req1);
|
||||
|
||||
req2.subscribe({"ch2"});
|
||||
req2.psubscribe({"channel_c*"});
|
||||
tracker.commit_changes(req2);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "ch1", "ch2");
|
||||
req_expected.push("PSUBSCRIBE", "channel_a*", "channel_b*", "channel_c*");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// We can have subscribe and psubscribe commands with the same argument
|
||||
void test_subscribe_psubscribe_same_arg()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req, req_output, req_expected;
|
||||
|
||||
req.subscribe({"ch1"});
|
||||
req.psubscribe({"ch1"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "ch1");
|
||||
req_expected.push("PSUBSCRIBE", "ch1");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// An unsubscribe/punsubscribe balances a matching subscribe
|
||||
void test_unsubscribe()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req1, req2, req_output, req_expected;
|
||||
|
||||
// Add some changes to the tracker
|
||||
req1.subscribe({"ch1", "ch2"});
|
||||
req1.psubscribe({"ch1*", "ch2*"});
|
||||
tracker.commit_changes(req1);
|
||||
|
||||
// Unsubscribe from some channels
|
||||
req2.punsubscribe({"ch2*"});
|
||||
req2.unsubscribe({"ch1"});
|
||||
tracker.commit_changes(req2);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "ch2");
|
||||
req_expected.push("PSUBSCRIBE", "ch1*");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// After an unsubscribe, we can subscribe again
|
||||
void test_resubscribe()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req, req_output, req_expected;
|
||||
|
||||
// Subscribe to some channels
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
req.psubscribe({"ch1*", "ch2*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Unsubscribe from some channels
|
||||
req.clear();
|
||||
req.punsubscribe({"ch2*"});
|
||||
req.unsubscribe({"ch1"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Subscribe again
|
||||
req.clear();
|
||||
req.subscribe({"ch1"});
|
||||
req.psubscribe({"ch2*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "ch1", "ch2");
|
||||
req_expected.push("PSUBSCRIBE", "ch1*", "ch2*");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// Subscribing twice is not a problem
|
||||
void test_subscribe_twice()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req, req_output, req_expected;
|
||||
|
||||
// Subscribe to some channels
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
req.psubscribe({"ch1*", "ch2*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Subscribe to the same channels again
|
||||
req.clear();
|
||||
req.subscribe({"ch2"});
|
||||
req.psubscribe({"ch1*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "ch1", "ch2");
|
||||
req_expected.push("PSUBSCRIBE", "ch1*", "ch2*");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// Unsubscribing from channels we haven't subscribed to is not a problem
|
||||
void test_lone_unsubscribe()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req, req_output, req_expected;
|
||||
|
||||
// Subscribe to some channels
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
req.psubscribe({"ch1*", "ch2*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Unsubscribe from channels we haven't subscribed to
|
||||
req.clear();
|
||||
req.unsubscribe({"other"});
|
||||
req.punsubscribe({"other*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "ch1", "ch2");
|
||||
req_expected.push("PSUBSCRIBE", "ch1*", "ch2*");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// A state with no changes is not a problem
|
||||
void test_empty()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req_output;
|
||||
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
BOOST_TEST_EQ(req_output.payload(), "");
|
||||
}
|
||||
|
||||
// If the output request is not empty, the commands are added to it, rather than replaced
|
||||
void test_output_request_not_empty()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req, req_output, req_expected;
|
||||
|
||||
// Subscribe to some channels
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
req.psubscribe({"ch1*", "ch2*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Compose the output request
|
||||
req_output.push("PING", "hello");
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
|
||||
// Check that we generate the correct response
|
||||
req_expected.push("PING", "hello");
|
||||
req_expected.push("SUBSCRIBE", "ch1", "ch2");
|
||||
req_expected.push("PSUBSCRIBE", "ch1*", "ch2*");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
// Clear removes everything from the state
|
||||
void test_clear()
|
||||
{
|
||||
subscription_tracker tracker;
|
||||
request req, req_output, req_expected;
|
||||
|
||||
// Subscribe to some channels
|
||||
req.subscribe({"ch1", "ch2"});
|
||||
req.psubscribe({"ch1*", "ch2*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Clear
|
||||
tracker.clear();
|
||||
|
||||
// Nothing should be generated
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
BOOST_TEST_EQ(req_output.payload(), "");
|
||||
|
||||
// We can reuse the tracker by now committing some more changes
|
||||
req.clear();
|
||||
req.subscribe({"ch5"});
|
||||
req.psubscribe({"ch6*"});
|
||||
tracker.commit_changes(req);
|
||||
|
||||
// Check that we generate the correct response
|
||||
tracker.compose_subscribe_request(req_output);
|
||||
req_expected.push("SUBSCRIBE", "ch5");
|
||||
req_expected.push("PSUBSCRIBE", "ch6*");
|
||||
BOOST_TEST_EQ(req_output.payload(), req_expected.payload());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_subscribe();
|
||||
test_psubscribe();
|
||||
test_subscribe_psubscribe();
|
||||
test_subscribe_psubscribe_same_arg();
|
||||
test_unsubscribe();
|
||||
test_resubscribe();
|
||||
test_subscribe_twice();
|
||||
test_lone_unsubscribe();
|
||||
test_empty();
|
||||
test_output_request_not_empty();
|
||||
test_clear();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -78,7 +78,6 @@ void test_reconnection()
|
||||
connection conn{ioc};
|
||||
auto cfg = make_test_config();
|
||||
cfg.unix_socket = unix_socket_path;
|
||||
cfg.reconnect_wait_interval = 10ms; // make the test run faster
|
||||
|
||||
request ping_request;
|
||||
ping_request.get_config().cancel_if_not_connected = false;
|
||||
@@ -131,7 +130,7 @@ void test_switch_between_transports()
|
||||
// Create configurations for TLS and UNIX connections
|
||||
auto tcp_tls_cfg = make_test_config();
|
||||
tcp_tls_cfg.use_ssl = true;
|
||||
tcp_tls_cfg.addr.port = "6380";
|
||||
tcp_tls_cfg.addr.port = "16380";
|
||||
auto unix_cfg = make_test_config();
|
||||
unix_cfg.unix_socket = unix_socket_path;
|
||||
|
||||
@@ -194,7 +193,7 @@ void test_error_unix_tls()
|
||||
connection conn{ioc};
|
||||
auto cfg = make_test_config();
|
||||
cfg.use_ssl = true;
|
||||
cfg.addr.port = "6380";
|
||||
cfg.addr.port = "16380";
|
||||
cfg.unix_socket = unix_socket_path;
|
||||
bool finished = false;
|
||||
|
||||
|
||||
212
test/test_update_sentinel_list.cpp
Normal file
212
test/test_update_sentinel_list.cpp
Normal file
@@ -0,0 +1,212 @@
|
||||
//
|
||||
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
|
||||
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
|
||||
//
|
||||
// Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
//
|
||||
|
||||
#include <boost/redis/config.hpp>
|
||||
#include <boost/redis/impl/sentinel_utils.hpp>
|
||||
|
||||
#include <boost/core/lightweight_test.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
#include <vector>
|
||||
|
||||
using namespace boost::redis;
|
||||
using detail::update_sentinel_list;
|
||||
using boost::system::error_code;
|
||||
|
||||
// Operators
|
||||
namespace boost::redis {
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const address& addr)
|
||||
{
|
||||
return os << "address{ .host=" << addr.host << ", .port=" << addr.port << " }";
|
||||
}
|
||||
|
||||
} // namespace boost::redis
|
||||
|
||||
namespace {
|
||||
|
||||
// The only Sentinel resolved the address successfully, and there's no newly discovered Sentinels
|
||||
void test_single_sentinel()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"}
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, {}, initial_sentinels);
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
initial_sentinels.begin(),
|
||||
initial_sentinels.end());
|
||||
}
|
||||
|
||||
// Some new Sentinels were discovered using SENTINEL SENTINELS
|
||||
void test_new_sentinels()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"}
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// Some of the new Sentinels are already in the list
|
||||
void test_new_sentinels_known()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// The Sentinel that succeeded should be placed first
|
||||
void test_success_sentinel_not_first()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
std::vector<address> sentinels{initial_sentinels};
|
||||
const address new_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 2u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host3", "3000"},
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// If a discovered Sentinel is not returned in subsequent iterations, it's removed from the list
|
||||
void test_new_sentinel_removed()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
};
|
||||
std::vector<address> sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host4", "4000"},
|
||||
};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
// Bootstrap Sentinels are never removed
|
||||
void test_bootstrap_sentinel_removed()
|
||||
{
|
||||
const std::vector<address> initial_sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
};
|
||||
std::vector<address> sentinels{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host3", "3000"},
|
||||
{"host4", "4000"},
|
||||
{"host5", "5000"},
|
||||
};
|
||||
const address new_sentinels[]{
|
||||
{"host2", "2000"},
|
||||
{"host4", "4000"},
|
||||
};
|
||||
|
||||
update_sentinel_list(sentinels, 0u, new_sentinels, initial_sentinels);
|
||||
|
||||
const address expected_sentinels[]{
|
||||
{"host1", "1000"},
|
||||
{"host2", "2000"},
|
||||
{"host4", "4000"},
|
||||
{"host3", "3000"}, // bootstrap Sentinels placed last
|
||||
};
|
||||
|
||||
BOOST_TEST_ALL_EQ(
|
||||
sentinels.begin(),
|
||||
sentinels.end(),
|
||||
std::begin(expected_sentinels),
|
||||
std::end(expected_sentinels));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
test_single_sentinel();
|
||||
test_new_sentinels();
|
||||
test_new_sentinels_known();
|
||||
test_success_sentinel_not_first();
|
||||
test_new_sentinel_removed();
|
||||
test_bootstrap_sentinel_removed();
|
||||
|
||||
return boost::report_errors();
|
||||
}
|
||||
@@ -134,7 +134,7 @@ def _build_b2_distro(
|
||||
_run([
|
||||
_b2_command,
|
||||
'--prefix={}'.format(_b2_distro),
|
||||
'--with-system',
|
||||
'--with-headers',
|
||||
'toolset={}'.format(toolset),
|
||||
'-d0',
|
||||
'install'
|
||||
|
||||
@@ -1,19 +1,140 @@
|
||||
services:
|
||||
redis:
|
||||
redis-master:
|
||||
container_name: redis-master
|
||||
image: ${SERVER_IMAGE}
|
||||
entrypoint: "/docker/entrypoint.sh"
|
||||
network_mode: host
|
||||
command: >
|
||||
sh -c 'chmod 777 /tmp/redis-socks &&
|
||||
redis-server \
|
||||
--replica-announce-ip localhost \
|
||||
--port 6379 \
|
||||
--tls-port 16379 \
|
||||
--tls-cert-file /docker/tls/server.crt \
|
||||
--tls-key-file /docker/tls/server.key \
|
||||
--tls-ca-cert-file /docker/tls/ca.crt \
|
||||
--tls-auth-clients no \
|
||||
--unixsocket /tmp/redis-socks/redis.sock \
|
||||
--unixsocketperm 777'
|
||||
volumes:
|
||||
- ./docker:/docker
|
||||
- /tmp/redis-socks:/tmp/redis-socks
|
||||
ports:
|
||||
- 6379:6379
|
||||
- 6380:6380
|
||||
|
||||
redis-replica-1:
|
||||
container_name: redis-replica-1
|
||||
image: ${SERVER_IMAGE}
|
||||
network_mode: host
|
||||
command:
|
||||
[
|
||||
"redis-server",
|
||||
"--replica-announce-ip", "localhost",
|
||||
"--replicaof", "localhost", "6379",
|
||||
"--port", "6380",
|
||||
"--tls-port", "16380",
|
||||
"--tls-cert-file", "/docker/tls/server.crt",
|
||||
"--tls-key-file", "/docker/tls/server.key",
|
||||
"--tls-ca-cert-file", "/docker/tls/ca.crt",
|
||||
"--tls-auth-clients", "no",
|
||||
]
|
||||
volumes:
|
||||
- ./docker:/docker
|
||||
|
||||
|
||||
redis-replica-2:
|
||||
container_name: redis-replica-2
|
||||
image: ${SERVER_IMAGE}
|
||||
network_mode: host
|
||||
command:
|
||||
[
|
||||
"redis-server",
|
||||
"--replica-announce-ip", "localhost",
|
||||
"--replicaof", "localhost", "6379",
|
||||
"--port", "6381",
|
||||
"--tls-port", "16381",
|
||||
"--tls-cert-file", "/docker/tls/server.crt",
|
||||
"--tls-key-file", "/docker/tls/server.key",
|
||||
"--tls-ca-cert-file", "/docker/tls/ca.crt",
|
||||
"--tls-auth-clients", "no",
|
||||
]
|
||||
volumes:
|
||||
- ./docker:/docker
|
||||
|
||||
|
||||
sentinel-1:
|
||||
container_name: sentinel-1
|
||||
image: ${SERVER_IMAGE}
|
||||
network_mode: host
|
||||
command: >
|
||||
sh -c 'cat << EOF > /etc/sentinel.conf && redis-sentinel /etc/sentinel.conf
|
||||
port 26379
|
||||
tls-port 36379
|
||||
tls-cert-file /docker/tls/server.crt
|
||||
tls-key-file /docker/tls/server.key
|
||||
tls-ca-cert-file /docker/tls/ca.crt
|
||||
tls-auth-clients no
|
||||
sentinel resolve-hostnames yes
|
||||
sentinel announce-hostnames yes
|
||||
sentinel announce-ip localhost
|
||||
sentinel monitor mymaster localhost 6379 2
|
||||
sentinel down-after-milliseconds mymaster 10000
|
||||
sentinel failover-timeout mymaster 10000
|
||||
sentinel parallel-syncs mymaster 1
|
||||
EOF'
|
||||
volumes:
|
||||
- ./docker:/docker
|
||||
|
||||
|
||||
sentinel-2:
|
||||
container_name: sentinel-2
|
||||
image: ${SERVER_IMAGE}
|
||||
network_mode: host
|
||||
command: >
|
||||
sh -c 'cat << EOF > /etc/sentinel.conf && redis-sentinel /etc/sentinel.conf
|
||||
port 26380
|
||||
tls-port 36380
|
||||
tls-cert-file /docker/tls/server.crt
|
||||
tls-key-file /docker/tls/server.key
|
||||
tls-ca-cert-file /docker/tls/ca.crt
|
||||
tls-auth-clients no
|
||||
sentinel resolve-hostnames yes
|
||||
sentinel announce-hostnames yes
|
||||
sentinel announce-ip localhost
|
||||
sentinel monitor mymaster localhost 6379 2
|
||||
sentinel down-after-milliseconds mymaster 10000
|
||||
sentinel failover-timeout mymaster 10000
|
||||
sentinel parallel-syncs mymaster 1
|
||||
EOF'
|
||||
volumes:
|
||||
- ./docker:/docker
|
||||
|
||||
sentinel-3:
|
||||
container_name: sentinel-3
|
||||
image: ${SERVER_IMAGE}
|
||||
network_mode: host
|
||||
command: >
|
||||
sh -c 'cat << EOF > /etc/sentinel.conf && redis-sentinel /etc/sentinel.conf
|
||||
port 26381
|
||||
tls-port 36381
|
||||
tls-cert-file /docker/tls/server.crt
|
||||
tls-key-file /docker/tls/server.key
|
||||
tls-ca-cert-file /docker/tls/ca.crt
|
||||
tls-auth-clients no
|
||||
sentinel resolve-hostnames yes
|
||||
sentinel announce-hostnames yes
|
||||
sentinel announce-ip localhost
|
||||
sentinel monitor mymaster localhost 6379 2
|
||||
sentinel down-after-milliseconds mymaster 10000
|
||||
sentinel failover-timeout mymaster 10000
|
||||
sentinel parallel-syncs mymaster 1
|
||||
EOF'
|
||||
volumes:
|
||||
- ./docker:/docker
|
||||
|
||||
|
||||
builder:
|
||||
image: ${BUILDER_IMAGE}
|
||||
container_name: builder
|
||||
image: ${BUILDER_IMAGE}
|
||||
network_mode: host
|
||||
tty: true
|
||||
environment:
|
||||
- BOOST_REDIS_TEST_SERVER=redis
|
||||
volumes:
|
||||
- ../:/boost-redis
|
||||
- /tmp/redis-socks:/tmp/redis-socks
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/bin/sh
|
||||
# The Redis container entrypoint. Runs the server with the required
|
||||
# flags and makes the socket accessible
|
||||
|
||||
set -e
|
||||
|
||||
chmod 777 /tmp/redis-socks
|
||||
|
||||
redis-server \
|
||||
--tls-port 6380 \
|
||||
--tls-cert-file /docker/tls/server.crt \
|
||||
--tls-key-file /docker/tls/server.key \
|
||||
--tls-ca-cert-file /docker/tls/ca.crt \
|
||||
--tls-auth-clients no \
|
||||
--unixsocket /tmp/redis-socks/redis.sock \
|
||||
--unixsocketperm 777
|
||||
Reference in New Issue
Block a user