From 7f6e4ef17ddcbae9791dc2fb40e62a4c4602b8ad Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 20:08:52 +0100 Subject: [PATCH 01/11] Initial impl --- include/boost/redis/detail/connection_state.hpp | 4 ++-- include/boost/redis/impl/sentinel_resolve_fsm.ipp | 1 - include/boost/redis/impl/sentinel_utils.hpp | 14 +++++++------- test/sansio_utils.cpp | 11 +++++------ test/sansio_utils.hpp | 4 +++- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/include/boost/redis/detail/connection_state.hpp b/include/boost/redis/detail/connection_state.hpp index e8e091061..4c938a292 100644 --- a/include/boost/redis/detail/connection_state.hpp +++ b/include/boost/redis/detail/connection_state.hpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include @@ -56,7 +56,7 @@ struct connection_state { // Sentinel stuff lazy_random_engine eng{}; std::vector
sentinels{}; - std::vector sentinel_resp_nodes{}; // for parsing + resp3::flat_tree sentinel_resp_nodes{}; // for parsing }; } // namespace boost::redis::detail diff --git a/include/boost/redis/impl/sentinel_resolve_fsm.ipp b/include/boost/redis/impl/sentinel_resolve_fsm.ipp index 96cf28f92..c4359e63b 100644 --- a/include/boost/redis/impl/sentinel_resolve_fsm.ipp +++ b/include/boost/redis/impl/sentinel_resolve_fsm.ipp @@ -141,7 +141,6 @@ sentinel_action sentinel_resolve_fsm::resume( update_sentinel_list(st.sentinels, idx_, resp.sentinels, st.cfg.sentinel.addresses); - st.sentinel_resp_nodes.clear(); // reduce memory consumption return system::error_code(); } diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index b3df430ba..d8f187535 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -45,8 +45,8 @@ inline void compose_sentinel_request(config& cfg) // Parses a list of replicas or sentinels inline system::error_code parse_server_list( - const resp3::node*& first, - const resp3::node* last, + const resp3::node_view*& first, + const resp3::node_view* last, std::vector
& out) { const auto* it = first; @@ -135,11 +135,11 @@ struct sentinel_response { // * 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 nodes, + span nodes, role server_role, sentinel_response& out) { - auto check_errors = [&out](const resp3::node& nd) { + auto check_errors = [&out](const resp3::node_view& nd) { switch (nd.data_type) { case resp3::type::simple_error: out.diagnostic = nd.value; @@ -166,11 +166,11 @@ inline system::error_code parse_sentinel_response( return &*it; } }; - const resp3::node* lib_first = find_first(); + const resp3::node_view* lib_first = find_first(); // Iterators - const resp3::node* it = nodes.begin(); - const resp3::node* last = nodes.end(); + const resp3::node_view* it = nodes.begin(); + const resp3::node_view* last = nodes.end(); ignore_unused(last); // Go through all the responses to user-supplied requests checking for errors diff --git a/test/sansio_utils.cpp b/test/sansio_utils.cpp index b3e446ac0..09dc9d947 100644 --- a/test/sansio_utils.cpp +++ b/test/sansio_utils.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -74,12 +75,10 @@ logger log_fixture::make_logger() }); } -std::vector nodes_from_resp3( - const std::vector& msgs, - source_location loc) +resp3::flat_tree nodes_from_resp3(const std::vector& msgs, source_location loc) { - std::vector nodes; - any_adapter adapter{nodes}; + resp3::flat_tree res; + any_adapter adapter{res}; for (std::string_view resp : msgs) { resp3::parser p; @@ -91,7 +90,7 @@ std::vector nodes_from_resp3( std::cerr << "Called from " << loc << std::endl; } - return nodes; + return res; } } // namespace boost::redis::detail diff --git a/test/sansio_utils.hpp b/test/sansio_utils.hpp index cd4d7ebc6..8e97e09db 100644 --- a/test/sansio_utils.hpp +++ b/test/sansio_utils.hpp @@ -8,6 +8,7 @@ #define BOOST_REDIS_TEST_SANSIO_UTILS_HPP #include +#include #include #include @@ -55,7 +56,8 @@ constexpr auto to_milliseconds(std::chrono::steady_clock::duration d) // 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 nodes_from_resp3( +// TODO: rename this +resp3::flat_tree nodes_from_resp3( const std::vector& msgs, source_location loc = BOOST_CURRENT_LOCATION); From 9acd67dede397b9ca81f07b0c62a322ef5350f18 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 20:09:30 +0100 Subject: [PATCH 02/11] rename --- test/sansio_utils.cpp | 2 +- test/sansio_utils.hpp | 5 ++--- test/test_parse_sentinel_response.cpp | 22 +++++++++--------- test/test_sentinel_resolve_fsm.cpp | 32 +++++++++++++-------------- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/test/sansio_utils.cpp b/test/sansio_utils.cpp index 09dc9d947..90e5529a9 100644 --- a/test/sansio_utils.cpp +++ b/test/sansio_utils.cpp @@ -75,7 +75,7 @@ logger log_fixture::make_logger() }); } -resp3::flat_tree nodes_from_resp3(const std::vector& msgs, source_location loc) +resp3::flat_tree tree_from_resp3(const std::vector& msgs, source_location loc) { resp3::flat_tree res; any_adapter adapter{res}; diff --git a/test/sansio_utils.hpp b/test/sansio_utils.hpp index 8e97e09db..aba0f7f59 100644 --- a/test/sansio_utils.hpp +++ b/test/sansio_utils.hpp @@ -53,11 +53,10 @@ constexpr auto to_milliseconds(std::chrono::steady_clock::duration d) return std::chrono::duration_cast(d).count(); } -// Creates a vector of nodes from a set of RESP3 messages. +// Creates a collection 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 -// TODO: rename this -resp3::flat_tree nodes_from_resp3( +resp3::flat_tree tree_from_resp3( const std::vector& msgs, source_location loc = BOOST_CURRENT_LOCATION); diff --git a/test/test_parse_sentinel_response.cpp b/test/test_parse_sentinel_response.cpp index 00a83d752..40d78d6fa 100644 --- a/test/test_parse_sentinel_response.cpp +++ b/test/test_parse_sentinel_response.cpp @@ -25,7 +25,7 @@ #include using namespace boost::redis; -using detail::nodes_from_resp3; +using detail::tree_from_resp3; using detail::parse_sentinel_response; using detail::sentinel_response; using boost::system::error_code; @@ -80,7 +80,7 @@ void test_master() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", "*2\r\n" @@ -116,7 +116,7 @@ void test_master_no_sentinels() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_from_resp3({ "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", "*0\r\n", }); @@ -132,7 +132,7 @@ void test_master_setup_request() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_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", @@ -170,7 +170,7 @@ void test_master_ip_port_out_of_order() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", "*1\r\n" @@ -195,7 +195,7 @@ void test_replica() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", "*2\r\n" @@ -254,7 +254,7 @@ void test_replica_no_sentinels() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", "*2\r\n" @@ -283,7 +283,7 @@ void test_replica_no_replicas() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", "*0\r\n", @@ -304,7 +304,7 @@ void test_replica_setup_request() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_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", @@ -342,7 +342,7 @@ void test_replica_ip_port_out_of_order() { // Setup fixture fix; - auto nodes = nodes_from_resp3({ + auto nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\ntest.host\r\n$4\r\n6389\r\n", "*1\r\n" @@ -697,7 +697,7 @@ void test_errors() // Setup std::cerr << "Running error test case: " << tc.name << std::endl; fixture fix; - auto nodes = nodes_from_resp3(tc.responses); + auto nodes = tree_from_resp3(tc.responses); // Call the function auto ec = parse_sentinel_response(nodes, tc.server_role, fix.resp); diff --git a/test/test_sentinel_resolve_fsm.cpp b/test/test_sentinel_resolve_fsm.cpp index 4eb847d93..d3046819a 100644 --- a/test/test_sentinel_resolve_fsm.cpp +++ b/test/test_sentinel_resolve_fsm.cpp @@ -26,7 +26,7 @@ namespace asio = boost::asio; using detail::sentinel_resolve_fsm; using detail::sentinel_action; using detail::connection_state; -using detail::nodes_from_resp3; +using detail::tree_from_resp3; using boost::system::error_code; using boost::asio::cancellation_type_t; @@ -115,7 +115,7 @@ void test_success() // 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*1\r\n" @@ -166,7 +166,7 @@ void test_success_replica() // 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*3\r\n" @@ -215,7 +215,7 @@ void test_one_connect_error() // 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); @@ -257,7 +257,7 @@ void test_one_request_network_error() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); @@ -292,7 +292,7 @@ void test_one_request_parse_error() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "+OK\r\n", "+OK\r\n", }); @@ -302,7 +302,7 @@ void test_one_request_parse_error() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); @@ -338,7 +338,7 @@ void test_one_request_error_node() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "-ERR needs authentication\r\n", "-ERR needs authentication\r\n", }); @@ -348,7 +348,7 @@ void test_one_request_error_node() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); @@ -383,7 +383,7 @@ void test_one_master_unknown() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "_\r\n", "-ERR unknown master\r\n", }); @@ -394,7 +394,7 @@ void test_one_master_unknown() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", }); @@ -430,7 +430,7 @@ void test_one_no_replicas() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", "*0\r\n", @@ -441,7 +441,7 @@ void test_one_no_replicas() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ // clang-format off "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*1\r\n" @@ -481,7 +481,7 @@ void test_error() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "_\r\n", "-ERR unknown master\r\n", }); @@ -495,7 +495,7 @@ void test_error() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "-ERR unauthorized\r\n", "-ERR unauthorized\r\n", }); @@ -546,7 +546,7 @@ void test_error_replica() 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({ + fix.st.sentinel_resp_nodes = tree_from_resp3({ "*2\r\n$9\r\ntest.host\r\n$4\r\n6380\r\n", "*0\r\n", "*0\r\n", From 063c713305100a4e4c01093cd8ab61848b350279 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 20:30:08 +0100 Subject: [PATCH 03/11] use get_num_messages --- include/boost/redis/impl/sentinel_utils.hpp | 37 +++++++++++---------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index d8f187535..e16b184cd 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -135,7 +136,7 @@ struct sentinel_response { // * 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 nodes, + const resp3::flat_tree& tree, role server_role, sentinel_response& out) { @@ -156,27 +157,29 @@ inline system::error_code parse_sentinel_response( 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_view* lib_first = find_first(); + // User-supplied commands are before the ones added by us. + // Find out how many responses should we skip + const std::size_t num_lib_msgs = server_role == role::master ? 2u : 3u; + BOOST_ASSERT(tree.get_total_msgs() >= num_lib_msgs); + const std::size_t num_user_msgs = tree.get_total_msgs() - num_lib_msgs; // Iterators - const resp3::node_view* it = nodes.begin(); - const resp3::node_view* last = nodes.end(); + const resp3::node_view* it = tree.begin(); + const resp3::node_view* last = tree.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; + BOOST_ASSERT(it != last); + BOOST_ASSERT(it->depth == 0u); + for (std::size_t i = 0u; i < num_user_msgs; ++i) { + while (true) { + if (auto ec = check_errors(*it)) + return ec; + ++it; + BOOST_ASSERT(it != last); + if (it->depth == 0u) + break; + } } // SENTINEL GET-MASTER-ADDR-BY-NAME From 17d4132ddd8f9748312451f4d1b1f35912747e74 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 20:43:13 +0100 Subject: [PATCH 04/11] Use indices and at --- include/boost/redis/impl/sentinel_utils.hpp | 109 +++++++++----------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index e16b184cd..c28021f76 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -16,7 +16,6 @@ #include #include -#include #include #include @@ -46,60 +45,57 @@ inline void compose_sentinel_request(config& cfg) // Parses a list of replicas or sentinels inline system::error_code parse_server_list( - const resp3::node_view*& first, - const resp3::node_view* last, + const resp3::flat_tree& tree, + std::size_t& index, std::vector
& 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) + const auto& root = tree.at(index); + BOOST_ASSERT(root.depth == 0u); + if (root.data_type != resp3::type::array) return {error::expects_resp3_array}; - const std::size_t num_servers = it->aggregate_size; - ++it; + const std::size_t num_servers = root.aggregate_size; + ++index; // 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) + const auto& server_node = tree.at(index); + BOOST_ASSERT(server_node.depth == 1u); + if (server_node.data_type != resp3::type::map) return {error::expects_resp3_map}; - const std::size_t num_key_values = it->aggregate_size; - ++it; + const std::size_t num_key_values = server_node.aggregate_size; + ++index; // 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) + const auto& key_node = tree.at(index); + BOOST_ASSERT(key_node.depth == 2u); + if (key_node.data_type != resp3::type::blob_string) return {error::expects_resp3_string}; - const std::string_view key = it->value; - ++it; + const std::string_view key = key_node.value; + ++index; // 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) + const auto& value_node = tree.at(index); + BOOST_ASSERT(value_node.depth == 2u); + if (value_node.data_type != resp3::type::blob_string) return {error::expects_resp3_string}; // Record it if (key == "ip") { ip_seen = true; - out[i].host = it->value; + out[i].host = tree.at(index).value; } else if (key == "port") { port_seen = true; - out[i].port = it->value; + out[i].port = tree.at(index).value; } - ++it; + ++index; } // Check that the response actually contained the fields we wanted @@ -108,7 +104,6 @@ inline system::error_code parse_server_list( } // Done - first = it; return system::error_code(); } @@ -160,62 +155,60 @@ inline system::error_code parse_sentinel_response( // User-supplied commands are before the ones added by us. // Find out how many responses should we skip const std::size_t num_lib_msgs = server_role == role::master ? 2u : 3u; - BOOST_ASSERT(tree.get_total_msgs() >= num_lib_msgs); + BOOST_ASSERT(tree.get_total_msgs() >= num_lib_msgs); // TODO const std::size_t num_user_msgs = tree.get_total_msgs() - num_lib_msgs; - // Iterators - const resp3::node_view* it = tree.begin(); - const resp3::node_view* last = tree.end(); - ignore_unused(last); + // Index-based access + std::size_t index = 0; // Go through all the responses to user-supplied requests checking for errors - BOOST_ASSERT(it != last); - BOOST_ASSERT(it->depth == 0u); + BOOST_ASSERT(tree.at(index).depth == 0u); for (std::size_t i = 0u; i < num_user_msgs; ++i) { while (true) { - if (auto ec = check_errors(*it)) + if (auto ec = check_errors(tree.at(index))) return ec; - ++it; - BOOST_ASSERT(it != last); - if (it->depth == 0u) + ++index; + if (tree.at(index).depth == 0u) break; } } // SENTINEL GET-MASTER-ADDR-BY-NAME + // TODO: move // Check for errors - if (auto ec = check_errors(*it)) + const auto& root_node = tree.at(index); + if (auto ec = check_errors(root_node)) 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) { + if (root_node.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) + if (root_node.data_type != resp3::type::array) return {error::expects_resp3_array}; - if (it->aggregate_size != 2u) + if (root_node.aggregate_size != 2u) return {error::incompatible_size}; - ++it; + ++index; // IP - BOOST_ASSERT(it != last); - BOOST_ASSERT(it->depth == 1u); - if (it->data_type != resp3::type::blob_string) + const auto& ip_node = tree.at(index); + BOOST_ASSERT(ip_node.depth == 1u); + if (ip_node.data_type != resp3::type::blob_string) return {error::expects_resp3_string}; - out.master_addr.host = it->value; - ++it; + out.master_addr.host = ip_node.value; + ++index; // Port - BOOST_ASSERT(it != last); - BOOST_ASSERT(it->depth == 1u); - if (it->data_type != resp3::type::blob_string) + const auto& port_node = tree.at(index); + BOOST_ASSERT(port_node.depth == 1u); + if (port_node.data_type != resp3::type::blob_string) return {error::expects_resp3_string}; - out.master_addr.port = it->value; - ++it; + out.master_addr.port = port_node.value; + ++index; if (server_role == role::replica) { // SENTINEL REPLICAS @@ -223,11 +216,11 @@ inline system::error_code parse_sentinel_response( // 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)) + if (auto ec = check_errors(tree.at(index))) return ec; // Actual parsing - if (auto ec = parse_server_list(it, last, out.replicas)) + if (auto ec = parse_server_list(tree, index, out.replicas)) return ec; } @@ -236,11 +229,11 @@ inline system::error_code parse_sentinel_response( // 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)) + if (auto ec = check_errors(tree.at(index))) return ec; // Actual parsing - if (auto ec = parse_server_list(it, last, out.sentinels)) + if (auto ec = parse_server_list(tree, index, out.sentinels)) return ec; // Done From dbca826c484aa17ef9df728918d8f7b207295d38 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 20:49:04 +0100 Subject: [PATCH 05/11] add a response length check --- include/boost/redis/impl/sentinel_utils.hpp | 29 ++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index c28021f76..05e0d5241 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -53,7 +53,7 @@ inline system::error_code parse_server_list( const auto& root = tree.at(index); BOOST_ASSERT(root.depth == 0u); if (root.data_type != resp3::type::array) - return {error::expects_resp3_array}; + return error::expects_resp3_array; const std::size_t num_servers = root.aggregate_size; ++index; @@ -64,7 +64,7 @@ inline system::error_code parse_server_list( const auto& server_node = tree.at(index); BOOST_ASSERT(server_node.depth == 1u); if (server_node.data_type != resp3::type::map) - return {error::expects_resp3_map}; + return error::expects_resp3_map; const std::size_t num_key_values = server_node.aggregate_size; ++index; @@ -76,7 +76,7 @@ inline system::error_code parse_server_list( const auto& key_node = tree.at(index); BOOST_ASSERT(key_node.depth == 2u); if (key_node.data_type != resp3::type::blob_string) - return {error::expects_resp3_string}; + return error::expects_resp3_string; const std::string_view key = key_node.value; ++index; @@ -84,7 +84,7 @@ inline system::error_code parse_server_list( const auto& value_node = tree.at(index); BOOST_ASSERT(value_node.depth == 2u); if (value_node.data_type != resp3::type::blob_string) - return {error::expects_resp3_string}; + return error::expects_resp3_string; // Record it if (key == "ip") { @@ -100,7 +100,7 @@ inline system::error_code parse_server_list( // Check that the response actually contained the fields we wanted if (!ip_seen || !port_seen) - return {error::empty_field}; + return error::empty_field; } // Done @@ -126,10 +126,8 @@ struct sentinel_response { // 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. +// Note that the tree should originate from a valid RESP3 message +// (i.e. we won't check that the first node has depth 0.) inline system::error_code parse_sentinel_response( const resp3::flat_tree& tree, role server_role, @@ -155,7 +153,8 @@ inline system::error_code parse_sentinel_response( // User-supplied commands are before the ones added by us. // Find out how many responses should we skip const std::size_t num_lib_msgs = server_role == role::master ? 2u : 3u; - BOOST_ASSERT(tree.get_total_msgs() >= num_lib_msgs); // TODO + if (tree.get_total_msgs() < num_lib_msgs) + return error::incompatible_size; const std::size_t num_user_msgs = tree.get_total_msgs() - num_lib_msgs; // Index-based access @@ -184,21 +183,21 @@ inline system::error_code parse_sentinel_response( // 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 (root_node.data_type == resp3::type::null) { - return {error::resp3_null}; + return error::resp3_null; } // If the root node is an array, an IP and port follow if (root_node.data_type != resp3::type::array) - return {error::expects_resp3_array}; + return error::expects_resp3_array; if (root_node.aggregate_size != 2u) - return {error::incompatible_size}; + return error::incompatible_size; ++index; // IP const auto& ip_node = tree.at(index); BOOST_ASSERT(ip_node.depth == 1u); if (ip_node.data_type != resp3::type::blob_string) - return {error::expects_resp3_string}; + return error::expects_resp3_string; out.master_addr.host = ip_node.value; ++index; @@ -206,7 +205,7 @@ inline system::error_code parse_sentinel_response( const auto& port_node = tree.at(index); BOOST_ASSERT(port_node.depth == 1u); if (port_node.data_type != resp3::type::blob_string) - return {error::expects_resp3_string}; + return error::expects_resp3_string; out.master_addr.port = port_node.value; ++index; From 5bb388e59b1cad1bee073aed1fe8db5db651b8b4 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 20:51:43 +0100 Subject: [PATCH 06/11] size check tests --- test/test_parse_sentinel_response.cpp | 35 ++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/test_parse_sentinel_response.cpp b/test/test_parse_sentinel_response.cpp index 40d78d6fa..b77c6abf5 100644 --- a/test/test_parse_sentinel_response.cpp +++ b/test/test_parse_sentinel_response.cpp @@ -688,7 +688,40 @@ void test_errors() }, "", error::empty_field - } + }, + { + "not_enough_messages_1", + role::master, + {}, + "", + error::incompatible_size + }, + { + "not_enough_messages_2", + role::master, + { + "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", + }, + "", + error::incompatible_size + }, + { + "not_enough_messages_replica_1", + role::replica, + {}, + "", + error::incompatible_size + }, + { + "not_enough_messages_replica_2", + role::replica, + { + "*2\r\n$9\r\nlocalhost\r\n$4\r\n6380\r\n", + "*0\r\n", + }, + "", + error::incompatible_size + }, // clang-format on }; From edbca64b33f57bf807bf004437decf99ef71708e Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 21:03:26 +0100 Subject: [PATCH 07/11] simplify skip loop --- include/boost/redis/impl/sentinel_utils.hpp | 23 +++++++++++---------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index 05e0d5241..7fe933efd 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -150,6 +150,9 @@ inline system::error_code parse_sentinel_response( out.sentinels.clear(); out.replicas.clear(); + // Index-based access + std::size_t index = 0; + // User-supplied commands are before the ones added by us. // Find out how many responses should we skip const std::size_t num_lib_msgs = server_role == role::master ? 2u : 3u; @@ -157,19 +160,17 @@ inline system::error_code parse_sentinel_response( return error::incompatible_size; const std::size_t num_user_msgs = tree.get_total_msgs() - num_lib_msgs; - // Index-based access - std::size_t index = 0; - // Go through all the responses to user-supplied requests checking for errors BOOST_ASSERT(tree.at(index).depth == 0u); - for (std::size_t i = 0u; i < num_user_msgs; ++i) { - while (true) { - if (auto ec = check_errors(tree.at(index))) - return ec; - ++index; - if (tree.at(index).depth == 0u) - break; - } + for (std::size_t remaining_roots = num_user_msgs + 1u;; ++index) { + // Exit at node N+1 + const auto& node = tree.at(index); + if (node.depth == 0u && --remaining_roots == 0u) + break; + + // This is a user-supplied message. Check for errors + if (auto ec = check_errors(node)) + return ec; } // SENTINEL GET-MASTER-ADDR-BY-NAME From cea85360231dd65c5962df473cadb79ea6f37a01 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 21:04:53 +0100 Subject: [PATCH 08/11] remove TODO --- include/boost/redis/impl/sentinel_utils.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index 7fe933efd..bbbe93326 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -174,7 +174,6 @@ inline system::error_code parse_sentinel_response( } // SENTINEL GET-MASTER-ADDR-BY-NAME - // TODO: move // Check for errors const auto& root_node = tree.at(index); From d38a4b0577a70fe336fcfae5ced2ec3abfb8dec9 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 21:08:09 +0100 Subject: [PATCH 09/11] don't access value node --- include/boost/redis/impl/sentinel_utils.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index bbbe93326..54083abd3 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -89,10 +89,10 @@ inline system::error_code parse_server_list( // Record it if (key == "ip") { ip_seen = true; - out[i].host = tree.at(index).value; + out[i].host = value_node.value; } else if (key == "port") { port_seen = true; - out[i].port = tree.at(index).value; + out[i].port = value_node.value; } ++index; From 58f790cdd35c4d5a9105b560ee810ec6de4c2932 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 21:20:46 +0100 Subject: [PATCH 10/11] movbe check_errors --- include/boost/redis/impl/sentinel_utils.hpp | 55 +++++++++------------ 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index 54083abd3..bb991fa7f 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -43,15 +43,31 @@ inline void compose_sentinel_request(config& cfg) // Note that we don't care about request flags because this is a one-time request } +// Helper +inline system::error_code sentinel_check_errors(const resp3::node_view& nd, std::string& diag) +{ + switch (nd.data_type) { + case resp3::type::simple_error: diag = nd.value; return error::resp3_simple_error; + case resp3::type::blob_error: diag = nd.value; return error::resp3_blob_error; + default: return system::error_code(); + } +}; + // Parses a list of replicas or sentinels inline system::error_code parse_server_list( const resp3::flat_tree& tree, std::size_t& index, + std::string& diag, std::vector
& out) { - // The root node must be an array const auto& root = tree.at(index); BOOST_ASSERT(root.depth == 0u); + + // If the command failed, this will be an error + if (auto ec = sentinel_check_errors(root, diag)) + return ec; + + // The root node must be an array if (root.data_type != resp3::type::array) return error::expects_resp3_array; const std::size_t num_servers = root.aggregate_size; @@ -133,18 +149,6 @@ inline system::error_code parse_sentinel_response( role server_role, sentinel_response& out) { - auto check_errors = [&out](const resp3::node_view& 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(); @@ -169,7 +173,7 @@ inline system::error_code parse_sentinel_response( break; // This is a user-supplied message. Check for errors - if (auto ec = check_errors(node)) + if (auto ec = sentinel_check_errors(node, out.diagnostic)) return ec; } @@ -177,11 +181,13 @@ inline system::error_code parse_sentinel_response( // Check for errors const auto& root_node = tree.at(index); - if (auto ec = check_errors(root_node)) + if (auto ec = sentinel_check_errors(root_node, out.diagnostic)) 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 this is the case, SENTINEL REPLICAS and SENTINEL SENTINELS will fail. + // We exit here so the diagnostic is clean. if (root_node.data_type == resp3::type::null) { return error::resp3_null; } @@ -211,28 +217,13 @@ inline system::error_code parse_sentinel_response( 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(tree.at(index))) - return ec; - - // Actual parsing - if (auto ec = parse_server_list(tree, index, out.replicas)) + if (auto ec = parse_server_list(tree, index, out.diagnostic, 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(tree.at(index))) - return ec; - - // Actual parsing - if (auto ec = parse_server_list(tree, index, out.sentinels)) + if (auto ec = parse_server_list(tree, index, out.diagnostic, out.sentinels)) return ec; // Done From f7304ae5558e65628ce938c4adebf6951e223901 Mon Sep 17 00:00:00 2001 From: Ruben Perez Date: Tue, 27 Jan 2026 21:24:01 +0100 Subject: [PATCH 11/11] extract get-master-addr --- include/boost/redis/impl/sentinel_utils.hpp | 87 ++++++++++++--------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/include/boost/redis/impl/sentinel_utils.hpp b/include/boost/redis/impl/sentinel_utils.hpp index bb991fa7f..6b52ce7ea 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -123,6 +123,54 @@ inline system::error_code parse_server_list( return system::error_code(); } +// Parses the output of SENTINEL GET-MASTER-ADDR-BY-NAME +inline system::error_code parse_get_master_addr_by_name( + const resp3::flat_tree& tree, + std::size_t& index, + std::string& diag, + address& out) +{ + const auto& root_node = tree.at(index); + BOOST_ASSERT(root_node.depth == 0u); + + // Check for errors + if (auto ec = sentinel_check_errors(root_node, diag)) + 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 this is the case, SENTINEL REPLICAS and SENTINEL SENTINELS will fail. + // We exit here so the diagnostic is clean. + if (root_node.data_type == resp3::type::null) { + return error::resp3_null; + } + + // If the root node is an array, an IP and port follow + if (root_node.data_type != resp3::type::array) + return error::expects_resp3_array; + if (root_node.aggregate_size != 2u) + return error::incompatible_size; + ++index; + + // IP + const auto& ip_node = tree.at(index); + BOOST_ASSERT(ip_node.depth == 1u); + if (ip_node.data_type != resp3::type::blob_string) + return error::expects_resp3_string; + out.host = ip_node.value; + ++index; + + // Port + const auto& port_node = tree.at(index); + BOOST_ASSERT(port_node.depth == 1u); + if (port_node.data_type != resp3::type::blob_string) + return error::expects_resp3_string; + out.port = port_node.value; + ++index; + + return system::error_code(); +} + // The output type of parse_sentinel_response struct sentinel_response { std::string diagnostic; // In case the server returned an error @@ -178,51 +226,16 @@ inline system::error_code parse_sentinel_response( } // SENTINEL GET-MASTER-ADDR-BY-NAME - - // Check for errors - const auto& root_node = tree.at(index); - if (auto ec = sentinel_check_errors(root_node, out.diagnostic)) + if (auto ec = parse_get_master_addr_by_name(tree, index, out.diagnostic, out.master_addr)) 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 this is the case, SENTINEL REPLICAS and SENTINEL SENTINELS will fail. - // We exit here so the diagnostic is clean. - if (root_node.data_type == resp3::type::null) { - return error::resp3_null; - } - - // If the root node is an array, an IP and port follow - if (root_node.data_type != resp3::type::array) - return error::expects_resp3_array; - if (root_node.aggregate_size != 2u) - return error::incompatible_size; - ++index; - - // IP - const auto& ip_node = tree.at(index); - BOOST_ASSERT(ip_node.depth == 1u); - if (ip_node.data_type != resp3::type::blob_string) - return error::expects_resp3_string; - out.master_addr.host = ip_node.value; - ++index; - - // Port - const auto& port_node = tree.at(index); - BOOST_ASSERT(port_node.depth == 1u); - if (port_node.data_type != resp3::type::blob_string) - return error::expects_resp3_string; - out.master_addr.port = port_node.value; - ++index; - + // SENTINEL REPLICAS if (server_role == role::replica) { - // SENTINEL REPLICAS if (auto ec = parse_server_list(tree, index, out.diagnostic, out.replicas)) return ec; } // SENTINEL SENTINELS - if (auto ec = parse_server_list(tree, index, out.diagnostic, out.sentinels)) return ec;