diff --git a/include/boost/redis/detail/connection_state.hpp b/include/boost/redis/detail/connection_state.hpp index e8e09106..4c938a29 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 96cf28f9..c4359e63 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 b3df430b..6b52ce7e 100644 --- a/include/boost/redis/impl/sentinel_utils.hpp +++ b/include/boost/redis/impl/sentinel_utils.hpp @@ -11,11 +11,11 @@ #include #include +#include #include #include #include -#include #include #include @@ -43,71 +43,131 @@ 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::node*& first, - const resp3::node* last, + const resp3::flat_tree& tree, + std::size_t& index, + std::string& diag, std::vector
& out) { - const auto* it = first; - ignore_unused(last); + 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 - 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; + if (root.data_type != resp3::type::array) + return error::expects_resp3_array; + 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) - return {error::expects_resp3_map}; - const std::size_t num_key_values = it->aggregate_size; - ++it; + 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 = 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) - return {error::expects_resp3_string}; - const std::string_view key = it->value; - ++it; + 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 = 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) - return {error::expects_resp3_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 = value_node.value; } else if (key == "port") { port_seen = true; - out[i].port = it->value; + out[i].port = value_node.value; } - ++it; + ++index; } // Check that the response actually contained the fields we wanted if (!ip_seen || !port_seen) - return {error::empty_field}; + return error::empty_field; } // Done - first = it; + 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(); } @@ -130,114 +190,53 @@ 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( - span nodes, + const resp3::flat_tree& tree, 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(); + // Index-based access + std::size_t index = 0; - // Iterators - const resp3::node* it = nodes.begin(); - const resp3::node* last = nodes.end(); - ignore_unused(last); + // 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; + 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; // Go through all the responses to user-supplied requests checking for errors - for (; it != lib_first; ++it) { - if (auto ec = check_errors(*it)) + BOOST_ASSERT(tree.at(index).depth == 0u); + 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 = sentinel_check_errors(node, out.diagnostic)) return ec; } // SENTINEL GET-MASTER-ADDR-BY-NAME - - // Check for errors - if (auto ec = check_errors(*it)) + 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 (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; - + // SENTINEL REPLICAS 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)) + 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(*it)) - return ec; - - // Actual parsing - if (auto ec = parse_server_list(it, last, out.sentinels)) + if (auto ec = parse_server_list(tree, index, out.diagnostic, out.sentinels)) return ec; // Done diff --git a/test/sansio_utils.cpp b/test/sansio_utils.cpp index b3e446ac..90e5529a 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 tree_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 cd4d7ebc..aba0f7f5 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 @@ -52,10 +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 -std::vector 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 00a83d75..b77c6abf 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" @@ -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 }; @@ -697,7 +730,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 4eb847d9..d3046819 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",