From 6762ff2c033d1f444fefb2b7db9b4b9341b8d03f Mon Sep 17 00:00:00 2001 From: Dmitry Kropachev Date: Mon, 20 Jul 2026 10:32:31 -0400 Subject: [PATCH] test: cover DNS entrypoint discovery --- tests/live_nodes_integration_test.cpp | 123 +++++++++++++++++++++++++- tests/live_nodes_test.cpp | 109 +++++++++++++++++++++++ 2 files changed, 231 insertions(+), 1 deletion(-) diff --git a/tests/live_nodes_integration_test.cpp b/tests/live_nodes_integration_test.cpp index 155c688..e09966a 100644 --- a/tests/live_nodes_integration_test.cpp +++ b/tests/live_nodes_integration_test.cpp @@ -1,12 +1,21 @@ +#include #include +#include #include +#include +#include +#include -#include +#include #include +#include #include +#include #include #include +#include +#include #include using namespace scylladb::alternator; @@ -42,6 +51,90 @@ std::vector Hosts(const std::vector& nodes) { return out; } +class LocalDnsEntrypointServer { +public: + explicit LocalDnsEntrypointServer(std::string body) + : body_(std::move(body)) { + fd_ = socket(AF_INET, SOCK_STREAM, 0); + if (fd_ < 0) { + throw std::runtime_error("socket failed"); + } + + int yes = 1; + setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (bind(fd_, reinterpret_cast(&addr), sizeof(addr)) != 0) { + throw std::runtime_error("bind failed"); + } + if (listen(fd_, 1) != 0) { + throw std::runtime_error("listen failed"); + } + + socklen_t len = sizeof(addr); + if (getsockname(fd_, reinterpret_cast(&addr), &len) != 0) { + throw std::runtime_error("getsockname failed"); + } + port_ = ntohs(addr.sin_port); + + worker_ = std::thread([this] { + int client = accept(fd_, nullptr, nullptr); + if (client < 0) { + return; + } + + char buffer[2048]; + const auto n = recv(client, buffer, sizeof(buffer), 0); + if (n > 0) { + request_.assign(buffer, static_cast(n)); + } + + std::ostringstream response; + response << "HTTP/1.1 200 OK\r\n" + << "Content-Type: application/json\r\n" + << "Content-Length: " << body_.size() << "\r\n" + << "Connection: close\r\n" + << "\r\n" + << body_; + const auto response_text = response.str(); + send(client, response_text.data(), response_text.size(), 0); + close(client); + }); + } + + ~LocalDnsEntrypointServer() { + Wait(); + } + + void Wait() { + if (fd_ >= 0) { + close(fd_); + fd_ = -1; + } + if (worker_.joinable()) { + worker_.join(); + } + } + + [[nodiscard]] std::uint16_t Port() const { + return port_; + } + + [[nodiscard]] const std::string& Request() const { + return request_; + } + +private: + int fd_ = -1; + std::uint16_t port_ = 0; + std::string body_; + std::string request_; + std::thread worker_; +}; + Config IntegrationConfig(std::uint16_t port) { Config cfg; cfg.port = port; @@ -51,6 +144,18 @@ Config IntegrationConfig(std::uint16_t port) { return cfg; } +std::string FetchIntegrationLocalNodesBody() { + auto cfg = IntegrationConfig(IntegrationHttpPort()); + auto client = NewDefaultHttpClient(cfg); + auto url = Url::FromHostPort("http", IntegrationNodes()[0], IntegrationHttpPort()) + .WithPathAndQuery("/localnodes"); + auto response = client->Get(url); + if (response.status_code != 200) { + throw std::runtime_error("integration /localnodes returned HTTP " + std::to_string(response.status_code)); + } + return response.body; +} + } // namespace #define REQUIRE_INTEGRATION() \ @@ -89,6 +194,22 @@ TEST(AlternatorLiveNodesIntegration, CompressedHttpDiscoveryWorks) { EXPECT_FALSE(nodes.GetNodes().empty()); } +TEST(AlternatorLiveNodesIntegration, DnsEntrypointDiscoversLiveClusterNodes) { + REQUIRE_INTEGRATION(); + + LocalDnsEntrypointServer server(FetchIntegrationLocalNodesBody()); + auto cfg = IntegrationConfig(server.Port()); + + AlternatorLiveNodes nodes({"localhost"}, cfg); + EXPECT_NO_THROW(nodes.UpdateLiveNodes()); + server.Wait(); + + EXPECT_NE(server.Request().find("GET /localnodes HTTP/1.1"), std::string::npos); + EXPECT_TRUE(server.Request().find("Host: localhost:") != std::string::npos || + server.Request().find("host: localhost:") != std::string::npos); + EXPECT_FALSE(nodes.GetNodes().empty()); +} + TEST(AlternatorLiveNodesIntegration, RejectsWrongDatacenter) { REQUIRE_INTEGRATION(); diff --git a/tests/live_nodes_test.cpp b/tests/live_nodes_test.cpp index cb5f54c..6174917 100644 --- a/tests/live_nodes_test.cpp +++ b/tests/live_nodes_test.cpp @@ -1,13 +1,19 @@ #include +#include #include +#include +#include +#include #include #include #include #include #include +#include #include +#include #include #include @@ -63,6 +69,90 @@ static std::int64_t HashWhereFirstNodeIs(const std::vector& nodes, const Ur throw std::runtime_error("failed to find hash for target node"); } +class LocalDnsEntrypointServer { +public: + explicit LocalDnsEntrypointServer(std::string body) + : body_(std::move(body)) { + fd_ = socket(AF_INET, SOCK_STREAM, 0); + if (fd_ < 0) { + throw std::runtime_error("socket failed"); + } + + int yes = 1; + setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (bind(fd_, reinterpret_cast(&addr), sizeof(addr)) != 0) { + throw std::runtime_error("bind failed"); + } + if (listen(fd_, 1) != 0) { + throw std::runtime_error("listen failed"); + } + + socklen_t len = sizeof(addr); + if (getsockname(fd_, reinterpret_cast(&addr), &len) != 0) { + throw std::runtime_error("getsockname failed"); + } + port_ = ntohs(addr.sin_port); + + worker_ = std::thread([this] { + int client = accept(fd_, nullptr, nullptr); + if (client < 0) { + return; + } + + char buffer[2048]; + const auto n = recv(client, buffer, sizeof(buffer), 0); + if (n > 0) { + request_.assign(buffer, static_cast(n)); + } + + std::ostringstream response; + response << "HTTP/1.1 200 OK\r\n" + << "Content-Type: application/json\r\n" + << "Content-Length: " << body_.size() << "\r\n" + << "Connection: close\r\n" + << "\r\n" + << body_; + const auto response_text = response.str(); + send(client, response_text.data(), response_text.size(), 0); + close(client); + }); + } + + ~LocalDnsEntrypointServer() { + Wait(); + } + + void Wait() { + if (fd_ >= 0) { + close(fd_); + fd_ = -1; + } + if (worker_.joinable()) { + worker_.join(); + } + } + + [[nodiscard]] std::uint16_t Port() const { + return port_; + } + + [[nodiscard]] const std::string& Request() const { + return request_; + } + +private: + int fd_ = -1; + std::uint16_t port_ = 0; + std::string body_; + std::string request_; + std::thread worker_; +}; + TEST(AlternatorLiveNodes, RoutingScopeFallbackRetriesKnownNodes) { Config cfg; cfg.routing_scope = NewDCScope("wrong", NewDCScope("target")); @@ -123,6 +213,25 @@ TEST(AlternatorLiveNodes, ClusterScopeMergesSeedNodes) { EXPECT_GT(dc2_requests.load(), 0); } +TEST(AlternatorLiveNodes, DnsEntrypointDiscoversDnsNodeRecords) { + LocalDnsEntrypointServer server(R"(["localhost","node-a.internal"])"); + Config cfg; + cfg.scheme = "http"; + cfg.port = server.Port(); + cfg.nodes_list_update_period = std::chrono::milliseconds{0}; + cfg.idle_nodes_list_update_period = std::chrono::milliseconds{0}; + cfg.node_health.down_node_probe_period = std::chrono::milliseconds{0}; + + AlternatorLiveNodes nodes({"localhost"}, cfg); + nodes.UpdateLiveNodes(); + server.Wait(); + + EXPECT_NE(server.Request().find("GET /localnodes HTTP/1.1"), std::string::npos); + EXPECT_TRUE(server.Request().find("Host: localhost:") != std::string::npos || + server.Request().find("host: localhost:") != std::string::npos); + EXPECT_EQ(Hosts(nodes.GetNodes()), std::vector({"localhost", "node-a.internal"})); +} + TEST(AlternatorLiveNodes, ClusterScopeRefreshUsesConfiguredSeedNodes) { Config cfg; cfg.routing_scope = NewClusterScope();