Skip to content

Commit bdd9c32

Browse files
authored
Adds Sentinel support (#345)
close #237 close #269 close #268 close #229
1 parent 00f3ec9 commit bdd9c32

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+5117
-421
lines changed

doc/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* xref:cancellation.adoc[]
44
* xref:serialization.adoc[]
55
* xref:logging.adoc[]
6+
* xref:sentinel.adoc[]
67
* xref:benchmarks.adoc[]
78
* xref:comparison.adoc[]
89
* xref:examples.adoc[]

doc/modules/ROOT/pages/examples.adoc

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

doc/modules/ROOT/pages/reference.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ xref:reference:boost/redis/basic_connection.adoc[`basic_connection`]
2525

2626
xref:reference:boost/redis/address.adoc[`address`]
2727

28+
xref:reference:boost/redis/role.adoc[`role`]
29+
2830
xref:reference:boost/redis/config.adoc[`config`]
2931

32+
xref:reference:boost/redis/sentinel_config.adoc[`sentinel_config`]
33+
3034
xref:reference:boost/redis/error.adoc[`error`]
3135

3236
xref:reference:boost/redis/logger.adoc[`logger`]
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//
2+
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
3+
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
4+
//
5+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
6+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7+
//
8+
9+
= Sentinel
10+
11+
Boost.Redis supports Redis Sentinel deployments. Sentinel handling
12+
in `connection` is built-in: xref:reference:boost/redis/basic_connection/async_run-04.adoc[`async_run`]
13+
automatically connects to Sentinels, resolves the master's address, and connects to the master.
14+
15+
Configuration is done using xref:reference:boost/redis/sentinel_config.adoc[`config::sentinel`]:
16+
17+
[source,cpp]
18+
----
19+
config cfg;
20+
21+
// To enable Sentinel, set this field to a non-empty list
22+
// of (hostname, port) pairs where Sentinels are listening
23+
cfg.sentinel.addresses = {
24+
{"sentinel1.example.com", "26379"},
25+
{"sentinel2.example.com", "26379"},
26+
{"sentinel3.example.com", "26379"},
27+
};
28+
29+
// Set master_name to the identifier that you configured
30+
// in the "sentinel monitor" statement of your sentinel.conf file
31+
cfg.sentinel.master_name = "mymaster";
32+
----
33+
34+
Once set, the connection object can be used normally. See our
35+
our {site-url}/example/cpp20_sentinel.cpp[Sentinel example]
36+
for a full program.
37+
38+
== Connecting to replicas
39+
40+
By default, the library connects to the Redis master.
41+
You can connect to one of its replicas by using
42+
xref:reference:boost/redis/sentinel_config/server_role.adoc[`config::sentinel::server_role`].
43+
This can be used to balance load, if all your commands read data from
44+
the server and never write to it. The particular replica will be chosen randomly.
45+
46+
[source,cpp]
47+
----
48+
config cfg;
49+
50+
// Set up Sentinel
51+
cfg.sentinel.addresses = {
52+
{"sentinel1.example.com", "26379"},
53+
{"sentinel2.example.com", "26379"},
54+
{"sentinel3.example.com", "26379"},
55+
};
56+
cfg.sentinel.master_name = "mymaster";
57+
58+
// Ask the library to connect to a random replica of 'mymaster', rather than the master node
59+
cfg.sentinel.server_role = role::replica;
60+
----
61+
62+
63+
== Sentinel authentication
64+
65+
If your Sentinels require authentication,
66+
you can use xref:reference:boost/redis/sentinel_config/setup.adoc[`config::sentinel::setup`]
67+
to provide credentials.
68+
This request is executed immediately after connecting to Sentinels, and
69+
before any other command:
70+
71+
[source,cpp]
72+
----
73+
// Set up Sentinel
74+
config cfg;
75+
cfg.sentinel.addresses = {
76+
{"sentinel1.example.com", "26379"},
77+
{"sentinel2.example.com", "26379"},
78+
{"sentinel3.example.com", "26379"},
79+
};
80+
cfg.sentinel.master_name = "mymaster";
81+
82+
// By default, setup contains a 'HELLO 3' command.
83+
// Override it to add an AUTH clause to it with out credentials.
84+
cfg.sentinel.setup.clear();
85+
cfg.sentinel.setup.push("HELLO", 3, "AUTH", "sentinel_user", "sentinel_password");
86+
87+
// cfg.sentinel.setup applies to Sentinels, only.
88+
// Use cfg.setup to authenticate to masters/replicas.
89+
cfg.use_setup = true; // Required for cfg.setup to be used, for historic reasons
90+
cfg.setup.clear();
91+
cfg.setup.push("HELLO", 3, "AUTH", "master_user", "master_password");
92+
----
93+
94+
== Using TLS with Sentinels
95+
96+
You might use TLS with Sentinels only, masters/replicas only, or both by adjusting
97+
xref:reference:boost/redis/sentinel_config/use_ssl.adoc[`config::sentinel::use_ssl`]
98+
and xref:reference:boost/redis/config/use_ssl.adoc[`config::use_ssl`]:
99+
100+
[source,cpp]
101+
----
102+
// Set up Sentinel
103+
config cfg;
104+
cfg.sentinel.addresses = {
105+
{"sentinel1.example.com", "26379"},
106+
{"sentinel2.example.com", "26379"},
107+
{"sentinel3.example.com", "26379"},
108+
};
109+
cfg.sentinel.master_name = "mymaster";
110+
111+
// Adjust these switches to enable/disable TLS
112+
cfg.use_ssl = true; // Applies to masters and replicas
113+
cfg.sentinel.use_ssl = true; // Applies to Sentinels
114+
----
115+
116+
== Sentinel algorithm
117+
118+
This section details how `async_run` interacts with Sentinel.
119+
Most of the algorithm follows
120+
https://redis.io/docs/latest/develop/reference/sentinel-clients/[the official Sentinel client guidelines].
121+
Some of these details may vary between library versions.
122+
123+
* Connections maintain an internal list of Sentinels, bootstrapped from
124+
xref:reference:boost/redis/sentinel_config/addresses.adoc[`config::sentinel::addresses`].
125+
* The first Sentinel in the list is contacted by performing the following:
126+
** A physical connection is established.
127+
** The setup request is executed.
128+
** The master's address is resolved using
129+
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL GET-MASTER-NAME-BY-ADDR`].
130+
** If `config::sentinel::server_role` is `role::replica`, replica addresses are obtained using
131+
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL REPLICAS`].
132+
One replica is chosen randomly.
133+
** The address of other Sentinels also monitoring this master are retrieved using
134+
https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/#sentinel-api[`SENTINEL SENTINELS`].
135+
* If a Sentinel is unreachable, doesn't know about the configured master,
136+
or returns an error while executing the above requests, the next Sentinel in the list is tried.
137+
* If all Sentinels have been tried without success, `config::reconnect_wait_interval`
138+
is waited, and the process starts again.
139+
* After a successful Sentinel response, the internal Sentinel list is updated
140+
with any newly discovered Sentinels.
141+
Sentinels in `config::sentinel::addresses` are always kept in the list,
142+
even if they weren't present in the output of `SENTINEL SENTINELS`.
143+
* The retrieved address is used
144+
to establish a connection with the master or replica.
145+
A `ROLE` command is added at the end of the setup request.
146+
This is used to detect situations where a Sentinel returns outdated
147+
information due to a failover in process. If `ROLE` doesn't output
148+
the expected role (`"master"` or `"slave"`, depending on `config::sentinel::server_role`)
149+
`config::reconnect_wait_interval` is waited and Sentinel is contacted again.
150+
* The connection to the master/replica is run like any other connection.
151+
If network errors or timeouts happen, `config::reconnect_wait_interval`
152+
is waited and Sentinel is contacted again.

example/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ make_testable_example(cpp20_containers 20)
2828
make_testable_example(cpp20_json 20)
2929
make_testable_example(cpp20_unix_sockets 20)
3030
make_testable_example(cpp20_timeouts 20)
31+
make_testable_example(cpp20_sentinel 20)
3132

3233
make_example(cpp20_subscriber 20)
3334
make_example(cpp20_streams 20)
3435
make_example(cpp20_echo_server 20)
35-
make_example(cpp20_resolve_with_sentinel 20)
3636
make_example(cpp20_intro_tls 20)
3737

3838
# We test the protobuf example only on gcc.

example/cpp20_resolve_with_sentinel.cpp

Lines changed: 0 additions & 77 deletions
This file was deleted.

example/cpp20_sentinel.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// Copyright (c) 2025 Marcelo Zimbres Silva (mzimbres@gmail.com),
3+
// Ruben Perez Hidalgo (rubenperez038 at gmail dot com)
4+
//
5+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
6+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7+
//
8+
9+
#include <boost/redis/connection.hpp>
10+
11+
#include <boost/asio/co_spawn.hpp>
12+
#include <boost/asio/consign.hpp>
13+
#include <boost/asio/detached.hpp>
14+
15+
#include <iostream>
16+
17+
#if defined(BOOST_ASIO_HAS_CO_AWAIT)
18+
19+
namespace asio = boost::asio;
20+
using boost::redis::request;
21+
using boost::redis::response;
22+
using boost::redis::config;
23+
using boost::redis::connection;
24+
25+
// Called from the main function (see main.cpp)
26+
auto co_main(config cfg) -> asio::awaitable<void>
27+
{
28+
// Boost.Redis has built-in support for Sentinel deployments.
29+
// To enable it, set the fields in config shown here.
30+
// sentinel.addresses should contain a list of (hostname, port) pairs
31+
// where Sentinels are listening. IPs can also be used.
32+
cfg.sentinel.addresses = {
33+
{"localhost", "26379"},
34+
{"localhost", "26380"},
35+
{"localhost", "26381"},
36+
};
37+
38+
// Set master_name to the identifier that you configured
39+
// in the "sentinel monitor" statement of your sentinel.conf file
40+
cfg.sentinel.master_name = "mymaster";
41+
42+
// async_run will contact the Sentinels, obtain the master address,
43+
// connect to it and keep the connection healthy. If a failover happens,
44+
// the address will be resolved again and the new elected master will be contacted.
45+
auto conn = std::make_shared<connection>(co_await asio::this_coro::executor);
46+
conn->async_run(cfg, asio::consign(asio::detached, conn));
47+
48+
// You can now use the connection normally, as you would use a connection to a single master.
49+
request req;
50+
req.push("PING", "Hello world");
51+
response<std::string> resp;
52+
53+
// Execute the request.
54+
co_await conn->async_exec(req, resp);
55+
conn->cancel();
56+
57+
std::cout << "PING: " << std::get<0>(resp).value() << std::endl;
58+
}
59+
60+
#endif // defined(BOOST_ASIO_HAS_CO_AWAIT)

include/boost/redis/adapter/any_adapter.hpp

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@
1212

1313
#include <boost/system/error_code.hpp>
1414

15-
#include <cstddef>
1615
#include <functional>
17-
#include <string_view>
1816
#include <type_traits>
1917

2018
namespace boost::redis {
@@ -50,20 +48,7 @@ class any_adapter {
5048
using impl_t = std::function<void(parse_event, resp3::node_view const&, system::error_code&)>;
5149

5250
template <class T>
53-
static auto create_impl(T& resp) -> impl_t
54-
{
55-
using namespace boost::redis::adapter;
56-
return [adapter2 = boost_redis_adapt(resp)](
57-
any_adapter::parse_event ev,
58-
resp3::node_view const& nd,
59-
system::error_code& ec) mutable {
60-
switch (ev) {
61-
case parse_event::init: adapter2.on_init(); break;
62-
case parse_event::node: adapter2.on_node(nd, ec); break;
63-
case parse_event::done: adapter2.on_done(); break;
64-
}
65-
};
66-
}
51+
static auto create_impl(T& resp) -> impl_t;
6752

6853
/// Contructs from a type erased adaper
6954
any_adapter(impl_t fn = [](parse_event, resp3::node_view const&, system::error_code&) { })
@@ -109,6 +94,32 @@ class any_adapter {
10994
impl_t impl_;
11095
};
11196

97+
namespace detail {
98+
99+
template <class Adapter>
100+
any_adapter::impl_t make_any_adapter_impl(Adapter&& value)
101+
{
102+
return [adapter = std::move(value)](
103+
any_adapter::parse_event ev,
104+
resp3::node_view const& nd,
105+
system::error_code& ec) mutable {
106+
switch (ev) {
107+
case any_adapter::parse_event::init: adapter.on_init(); break;
108+
case any_adapter::parse_event::node: adapter.on_node(nd, ec); break;
109+
case any_adapter::parse_event::done: adapter.on_done(); break;
110+
}
111+
};
112+
}
113+
114+
} // namespace detail
115+
112116
} // namespace boost::redis
113117

118+
template <class T>
119+
auto boost::redis::any_adapter::create_impl(T& resp) -> impl_t
120+
{
121+
using adapter::boost_redis_adapt;
122+
return detail::make_any_adapter_impl(boost_redis_adapt(resp));
123+
}
124+
114125
#endif

0 commit comments

Comments
 (0)