// // 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 #include #include #include #include #include #include #include #include #include #include #include #include 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 } // 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) { 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; ++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) 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 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. 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 = value_node.value; } else if (key == "port") { port_seen = true; out[i].port = value_node.value; } ++index; } // Check that the response actually contained the fields we wanted if (!ip_seen || !port_seen) return error::empty_field; } // Done 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 address master_addr; // Always populated std::vector
replicas; // Populated only when connecting to replicas std::vector
sentinels; }; // Parses an array of nodes into a sentinel_response. // The request originating this response should be: // // 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. // 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, sentinel_response& out) { // Clear the output out.diagnostic.clear(); 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; 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 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 if (auto ec = parse_get_master_addr_by_name(tree, index, out.diagnostic, out.master_addr)) return ec; // SENTINEL REPLICAS if (server_role == role::replica) { 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; // Done return system::error_code(); } // Updates the internal Sentinel list. // to should never be empty inline void update_sentinel_list( std::vector
& to, std::size_t current_index, // the one to maintain and place first span gossip_sentinels, // the ones that SENTINEL SENTINELS returned span 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