From bfdd8d56281b8729c77aeff871e3ee1516c2fec4 Mon Sep 17 00:00:00 2001 From: Ilya Repin Date: Mon, 2 Feb 2026 13:49:44 +0000 Subject: [PATCH] add get hub reports handler --- configs/static_config.yaml | 5 + src/api/http/v1/admin/get_hub_reports.cpp | 50 +++ src/api/http/v1/admin/get_hub_reports.hpp | 34 ++ src/app/dto/admin/get_hub_reports.hpp | 16 + src/app/services/admin/admin_service.cpp | 10 +- src/app/services/admin/admin_service.hpp | 7 +- .../admin/get_hub_reports/get_hub_reports.cpp | 40 +++ .../admin/get_hub_reports/get_hub_reports.hpp | 34 ++ .../admin/admin_service_component.cpp | 5 +- src/infra/components/components.cpp | 2 + src/infra/serializer/serializer.cpp | 42 ++- src/infra/serializer/serializer.hpp | 2 + src/infra/serializer/serializer_ut.cpp | 318 ++++++++++++++---- tests/test_get_hub_reports.py | 7 + 14 files changed, 489 insertions(+), 83 deletions(-) create mode 100644 src/api/http/v1/admin/get_hub_reports.cpp create mode 100644 src/api/http/v1/admin/get_hub_reports.hpp create mode 100644 src/app/dto/admin/get_hub_reports.hpp create mode 100644 src/app/use_cases/admin/get_hub_reports/get_hub_reports.cpp create mode 100644 src/app/use_cases/admin/get_hub_reports/get_hub_reports.hpp create mode 100644 tests/test_get_hub_reports.py diff --git a/configs/static_config.yaml b/configs/static_config.yaml index 5751b09..63959cd 100644 --- a/configs/static_config.yaml +++ b/configs/static_config.yaml @@ -119,3 +119,8 @@ components_manager: method: GET task_processor: main-task-processor + handler-get-hub-reports: + load-enabled: true + path: /admin/hub_reports + method: GET + task_processor: main-task-processor diff --git a/src/api/http/v1/admin/get_hub_reports.cpp b/src/api/http/v1/admin/get_hub_reports.cpp new file mode 100644 index 0000000..7dae504 --- /dev/null +++ b/src/api/http/v1/admin/get_hub_reports.cpp @@ -0,0 +1,50 @@ +#include "get_hub_reports.hpp" + +#include + +#include +#include + +#include + +#include + +namespace NCoordinator::NApi::NHandlers { + +//////////////////////////////////////////////////////////////////////////////// + +TGetHubReportsHandler::TGetHubReportsHandler( + const userver::components::ComponentConfig& config, + const userver::components::ComponentContext& context) + : HttpHandlerJsonBase(config, context) + , AdminService_(context.FindComponent().GetService()) +{ } + +userver::formats::json::Value TGetHubReportsHandler::HandleRequestJsonThrow( + const userver::server::http::HttpRequest& /*request*/, + const userver::formats::json::Value& /*request_json*/, + userver::server::request::RequestContext& /*request_context*/) const +{ + NApp::NDto::TGetHubReportsResponse result; + + try { + result = AdminService_.GetHubReports(); + } catch (const NApp::NUseCase::TGetPartitionMapTemporaryUnavailable& ex) { + LOG_ERROR() << "Get hub reports unavailable: " << ex.what(); + throw TServerException("Get hub reports temporary unavailable"); + } + + userver::formats::json::ValueBuilder builder; + builder["hub_reports"] = userver::formats::common::Type::kArray; + + for (const auto& hubReport : result.HubReports) { + auto serializedReport = NInfra::SerializeHubReport(hubReport); + builder["hub_reports"].PushBack(serializedReport); + } + + return builder.ExtractValue(); +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NChat::NApi::NHandlers diff --git a/src/api/http/v1/admin/get_hub_reports.hpp b/src/api/http/v1/admin/get_hub_reports.hpp new file mode 100644 index 0000000..90912fd --- /dev/null +++ b/src/api/http/v1/admin/get_hub_reports.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include +#include +#include + +namespace NCoordinator::NApi::NHandlers { + +//////////////////////////////////////////////////////////////////////////////// + +class TGetHubReportsHandler final + : public userver::server::handlers::HttpHandlerJsonBase +{ +public: + static constexpr std::string_view kName = "handler-get-hub-reports"; + + TGetHubReportsHandler( + const userver::components::ComponentConfig&, + const userver::components::ComponentContext&); + + userver::formats::json::Value HandleRequestJsonThrow( + const userver::server::http::HttpRequest& request, + const userver::formats::json::Value& request_json, + userver::server::request::RequestContext& context) const override; + +private: + NApp::NService::TAdminService& AdminService_; +}; + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NChat::NApi::NHandlers diff --git a/src/app/dto/admin/get_hub_reports.hpp b/src/app/dto/admin/get_hub_reports.hpp new file mode 100644 index 0000000..4ca7bf6 --- /dev/null +++ b/src/app/dto/admin/get_hub_reports.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +namespace NCoordinator::NApp::NDto { + +//////////////////////////////////////////////////////////////////////////////// + +struct TGetHubReportsResponse { + std::vector HubReports; +}; + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NCoordinator::NApp::NDto diff --git a/src/app/services/admin/admin_service.cpp b/src/app/services/admin/admin_service.cpp index 3a59154..c6fae23 100644 --- a/src/app/services/admin/admin_service.cpp +++ b/src/app/services/admin/admin_service.cpp @@ -6,9 +6,11 @@ namespace NCoordinator::NApp::NService { TAdminService::TAdminService( NCore::NDomain::ICoordinationRepository& coordinationRepository, - NCore::NDomain::ICoordinationGateway& coordinationGateway) + NCore::NDomain::ICoordinationGateway& coordinationGateway, + NCore::NDomain::IHubGateway& hubGateway) : GetContextUseCase_(coordinationRepository) , GetPartitionMapUseCase_(coordinationGateway) + , GetHubReportsUseCase_(coordinationGateway, hubGateway) { } NDto::TGetContextResponse TAdminService::GetCoordinationContext() const @@ -21,6 +23,12 @@ NDto::TGetPartitionMapResponse TAdminService::GetPartitionMap() const return GetPartitionMapUseCase_.Execute(); } +NDto::TGetHubReportsResponse TAdminService::GetHubReports() const +{ + return GetHubReportsUseCase_.Execute(); +} + + //////////////////////////////////////////////////////////////////////////////// } // namespace NCoordinator::NApp::NService diff --git a/src/app/services/admin/admin_service.hpp b/src/app/services/admin/admin_service.hpp index 28f4a38..0cd7831 100644 --- a/src/app/services/admin/admin_service.hpp +++ b/src/app/services/admin/admin_service.hpp @@ -1,9 +1,11 @@ #pragma once #include +#include #include #include #include +#include #include #include #include @@ -17,14 +19,17 @@ class TAdminService final public: TAdminService( NCore::NDomain::ICoordinationRepository& coordinationRepository, - NCore::NDomain::ICoordinationGateway& coordinationGateway); + NCore::NDomain::ICoordinationGateway& coordinationGateway, + NCore::NDomain::IHubGateway& hubGateway); NDto::TGetContextResponse GetCoordinationContext() const; NDto::TGetPartitionMapResponse GetPartitionMap() const; + NDto::TGetHubReportsResponse GetHubReports() const; private: NUseCase::TGetContextUseCase GetContextUseCase_; NUseCase::TGetPartitionMapUseCase GetPartitionMapUseCase_; + NUseCase::TGetHubReportsUseCase GetHubReportsUseCase_; }; //////////////////////////////////////////////////////////////////////////////// diff --git a/src/app/use_cases/admin/get_hub_reports/get_hub_reports.cpp b/src/app/use_cases/admin/get_hub_reports/get_hub_reports.cpp new file mode 100644 index 0000000..b4a265f --- /dev/null +++ b/src/app/use_cases/admin/get_hub_reports/get_hub_reports.cpp @@ -0,0 +1,40 @@ +#include "get_hub_reports.hpp" + +#include + +#include + +#include + +namespace NCoordinator::NApp::NUseCase { + +//////////////////////////////////////////////////////////////////////////////// + +TGetHubReportsUseCase::TGetHubReportsUseCase( + NCore::NDomain::ICoordinationGateway& coordinationGateway, + NCore::NDomain::IHubGateway& hubGateway) + : CoordinationGateway_(coordinationGateway) + , HubGateway_(hubGateway) +{ } + +NDto::TGetHubReportsResponse TGetHubReportsUseCase::Execute() const +{ + std::vector hubReports; + + try { + auto hubDiscovery = CoordinationGateway_.GetHubDiscovery(); + hubReports = HubGateway_.GetHubReports(hubDiscovery); + } catch (std::exception& ex) { + throw TGetHubReportsTemporaryUnavailable(fmt::format("Failed to get hub reports: {}", ex.what())); + } + + NDto::TGetHubReportsResponse response{ + .HubReports = std::move(hubReports), + }; + + return response; +} + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NCoordinator::NApp::NUseCase diff --git a/src/app/use_cases/admin/get_hub_reports/get_hub_reports.hpp b/src/app/use_cases/admin/get_hub_reports/get_hub_reports.hpp new file mode 100644 index 0000000..5f067b8 --- /dev/null +++ b/src/app/use_cases/admin/get_hub_reports/get_hub_reports.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +namespace NCoordinator::NApp::NUseCase { + +//////////////////////////////////////////////////////////////////////////////// + +class TGetHubReportsTemporaryUnavailable + : public TApplicationException +{ + using TApplicationException::TApplicationException; +}; + +class TGetHubReportsUseCase final +{ +public: + TGetHubReportsUseCase( + NCore::NDomain::ICoordinationGateway& coordinationGateway, + NCore::NDomain::IHubGateway& hubGateway); + + NDto::TGetHubReportsResponse Execute() const; + +private: + NCore::NDomain::ICoordinationGateway& CoordinationGateway_; + NCore::NDomain::IHubGateway& HubGateway_; +}; + +//////////////////////////////////////////////////////////////////////////////// + +} // namespace NCoordinator::NApp::NUseCase diff --git a/src/infra/components/admin/admin_service_component.cpp b/src/infra/components/admin/admin_service_component.cpp index 8e10a77..56b7533 100644 --- a/src/infra/components/admin/admin_service_component.cpp +++ b/src/infra/components/admin/admin_service_component.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -19,9 +20,11 @@ TAdminServiceComponent::TAdminServiceComponent( context.FindComponent().GetRepository(); auto& coordinationGateway = context.FindComponent().GetGateway(); + auto& hubGateway = + context.FindComponent().GetGateway(); - Service_ = std::make_unique(coordinationRepository, coordinationGateway); + Service_ = std::make_unique(coordinationRepository, coordinationGateway,hubGateway); } NApp::NService::TAdminService& TAdminServiceComponent::GetService() diff --git a/src/infra/components/components.cpp b/src/infra/components/components.cpp index 98202e6..f3e67f6 100644 --- a/src/infra/components/components.cpp +++ b/src/infra/components/components.cpp @@ -1,6 +1,7 @@ #include "components.hpp" #include +#include #include #include #include @@ -65,6 +66,7 @@ void RegisterServices(userver::components::ComponentList& list) void RegisterHandlers(userver::components::ComponentList& list) { list.Append() + .Append() .Append(); } diff --git a/src/infra/serializer/serializer.cpp b/src/infra/serializer/serializer.cpp index fe663ab..2e4142e 100644 --- a/src/infra/serializer/serializer.cpp +++ b/src/infra/serializer/serializer.cpp @@ -13,11 +13,14 @@ namespace { //////////////////////////////////////////////////////////////////////////////// constexpr inline std::string_view EPOCH_KEY = "epoch"; +constexpr inline std::string_view DC_KEY = "dc"; +constexpr inline std::string_view LOAD_FACTOR_KEY = "load_factor"; constexpr inline std::string_view PARTITIONS_KEY = "partitions"; constexpr inline std::string_view PARTITION_ID_KEY = "id"; constexpr inline std::string_view PARTITION_WEIGHT_KEY = "partition_weight"; constexpr inline std::string_view PARTITIONS_CONTEXT_KEY = "partitions_context"; -constexpr inline std::string_view HUB_ENDPOINT_KEY = "hub"; +constexpr inline std::string_view HUB_KEY = "hub"; +constexpr inline std::string_view ENDPOINT_KEY = "endpoint"; constexpr inline std::string_view COOLDOWN_EPOCH_KEY = "cooldown_epoch"; //////////////////////////////////////////////////////////////////////////////// @@ -38,7 +41,7 @@ userver::formats::json::Value SerializePartitionMap( for (const auto& [partition, hub] : partitionMap.Partitions) { userver::formats::json::ValueBuilder pair; pair[PARTITION_ID_KEY.data()] = partition.GetUnderlying(); - pair[HUB_ENDPOINT_KEY.data()] = hub.GetUnderlying(); + pair[HUB_KEY.data()] = hub.GetUnderlying(); result[PARTITIONS_KEY.data()].PushBack(pair.ExtractValue()); } @@ -77,6 +80,29 @@ userver::formats::json::Value SerializeCoordinationContext(const NCore::NDomain: return result.ExtractValue(); } +userver::formats::json::Value SerializeHubReport(const NCore::NDomain::THubReport& report) +{ + userver::formats::json::ValueBuilder builder; + + builder[EPOCH_KEY.data()] = std::to_string(report.Epoch.GetUnderlying()); + + builder[ENDPOINT_KEY.data()] = report.Endpoint.GetUnderlying(); + builder[DC_KEY.data()] = report.DC.GetUnderlying(); + + builder[LOAD_FACTOR_KEY.data()] = report.LoadFactor.GetUnderlying(); + + userver::formats::json::ValueBuilder partitionsBuilder(userver::formats::common::Type::kObject); + + for (const auto& [partitionId, partitionWeight] : report.PartitionWeights) { + partitionsBuilder[std::to_string(partitionId.GetUnderlying())] = + std::to_string(partitionWeight.GetUnderlying()); + } + + builder[PARTITIONS_KEY.data()] = partitionsBuilder; + + return builder.ExtractValue(); +} + NCore::NDomain::TPartitionMap DeserializePartitionMap(const userver::formats::json::Value& jsonValue) { NCore::NDomain::TPartitionMap result; @@ -89,7 +115,7 @@ NCore::NDomain::TPartitionMap DeserializePartitionMap(const userver::formats::js pair[PARTITION_ID_KEY.data()].As() }; NCore::NDomain::THubEndpoint hubEndpoint{ - pair[HUB_ENDPOINT_KEY.data()].As() + pair[HUB_KEY.data()].As() }; result.Partitions.emplace_back(std::move(partitionId), std::move(hubEndpoint)); @@ -124,19 +150,19 @@ NCore::NDomain::THubReport DeserializeHubReport(const userver::formats::json::Va NCore::NDomain::THubReport report; report.Epoch = NCore::NDomain::TEpoch{ - std::stoull(jsonValue["epoch"].As()) + std::stoull(jsonValue[EPOCH_KEY.data()].As()) }; report.Endpoint = NCore::NDomain::THubEndpoint{ - jsonValue["endpoint"].As() + jsonValue[ENDPOINT_KEY.data()].As() }; report.DC = NCore::NDomain::THubDC{ - jsonValue["dc"].As() + jsonValue[DC_KEY.data()].As() }; report.LoadFactor = NCore::NDomain::TLoadFactor{ - jsonValue["load_factor"].As() + jsonValue[LOAD_FACTOR_KEY.data()].As() }; - const auto& partitionsJson = jsonValue["partitions"]; + const auto& partitionsJson = jsonValue[PARTITIONS_KEY]; for (auto it = partitionsJson.begin(); it != partitionsJson.end(); ++it) { NCore::NDomain::TPartitionId partitionId{ std::stoull(it.GetName()) diff --git a/src/infra/serializer/serializer.hpp b/src/infra/serializer/serializer.hpp index 1977474..1e8ebce 100644 --- a/src/infra/serializer/serializer.hpp +++ b/src/infra/serializer/serializer.hpp @@ -17,6 +17,8 @@ userver::formats::json::Value SerializePartitionMap(const NCore::NDomain::TParti userver::formats::json::Value SerializeCoordinationContext(const NCore::NDomain::TCoordinationContext& context); +userver::formats::json::Value SerializeHubReport(const NCore::NDomain::THubReport& report); + NCore::NDomain::TPartitionMap DeserializePartitionMap(const userver::formats::json::Value& jsonValue); NCore::NDomain::TCoordinationContext DeserializeCoordinationContext(const userver::formats::json::Value& json); diff --git a/src/infra/serializer/serializer_ut.cpp b/src/infra/serializer/serializer_ut.cpp index 014d7d9..1e28cd4 100644 --- a/src/infra/serializer/serializer_ut.cpp +++ b/src/infra/serializer/serializer_ut.cpp @@ -5,46 +5,79 @@ #include #include +#include +#include + namespace { +//////////////////////////////////////////////////////////////////////////////// + using namespace NCoordinator::NCore::NDomain; using namespace NCoordinator::NInfra; -//////////////////////////////////////////////////////////////////////////////// - TPartitionMap MakePartitionMap() { TPartitionMap map; map.Epoch = TEpoch{42}; - map.Partitions.emplace_back( - TPartitionId{1}, - THubEndpoint{"hub-1"} - ); - map.Partitions.emplace_back( - TPartitionId{2}, - THubEndpoint{"hub-2"} - ); + map.Partitions.emplace_back(TPartitionId{1}, THubEndpoint{"hub-1"}); + map.Partitions.emplace_back(TPartitionId{2}, THubEndpoint{"hub-2"}); + map.Partitions.emplace_back(TPartitionId{3}, THubEndpoint{"hub-3"}); return map; } -TCoordinationContext MakeTestContext() { +TPartitionMap MakeEmptyPartitionMap() +{ + TPartitionMap map; + map.Epoch = TEpoch{100}; + return map; +} + +TCoordinationContext MakeCoordinationContext() +{ TCoordinationContext context; - auto idBoth = TPartitionId{10}; - context.PartitionCooldowns[idBoth] = TEpoch{100}; - context.PartitionWeights[idBoth] = TPartitionWeight{50}; + context.PartitionCooldowns[TPartitionId{10}] = TEpoch{100}; + context.PartitionWeights[TPartitionId{10}] = TPartitionWeight{50}; - auto idOnlyCooldown = TPartitionId{20}; - context.PartitionCooldowns[idOnlyCooldown] = TEpoch{200}; + context.PartitionCooldowns[TPartitionId{20}] = TEpoch{200}; - auto idOnlyWeight = TPartitionId{30}; - context.PartitionWeights[idOnlyWeight] = TPartitionWeight{300}; + context.PartitionWeights[TPartitionId{30}] = TPartitionWeight{300}; return context; } +TCoordinationContext MakeEmptyCoordinationContext() +{ + return TCoordinationContext{}; +} + +THubReport MakeHubReport() +{ + THubReport report; + report.Epoch = TEpoch{123}; + report.Endpoint = THubEndpoint{"hub.example.com"}; + report.DC = THubDC{"myt"}; + report.LoadFactor = TLoadFactor{85}; + + report.PartitionWeights.emplace(TPartitionId{10}, TPartitionWeight{500}); + report.PartitionWeights.emplace(TPartitionId{20}, TPartitionWeight{600}); + report.PartitionWeights.emplace(TPartitionId{30}, TPartitionWeight{700}); + + return report; +} + +THubReport MakeEmptyHubReport() +{ + THubReport report; + report.Epoch = TEpoch{1}; + report.Endpoint = THubEndpoint{"empty-hub"}; + report.DC = THubDC{"vla"}; + report.LoadFactor = TLoadFactor{0}; + return report; +} + //////////////////////////////////////////////////////////////////////////////// } // anonymous namespace @@ -53,29 +86,30 @@ namespace NCoordinator::NInfra { //////////////////////////////////////////////////////////////////////////////// -TEST(PartitionMapSerializer, SerializeProducesValidJson) -{ +TEST(PartitionMapSerializerTest, SerializeProducesValidJson) { const auto map = MakePartitionMap(); - const auto json = SerializePartitionMap(map); ASSERT_TRUE(json.HasMember("epoch")); ASSERT_TRUE(json.HasMember("partitions")); - EXPECT_EQ(json["epoch"].As(), 42); + EXPECT_EQ(json["epoch"].As(), 42); const auto& partitions = json["partitions"]; - ASSERT_EQ(partitions.GetSize(), 2); + ASSERT_TRUE(partitions.IsArray()); + ASSERT_EQ(partitions.GetSize(), 3); - EXPECT_EQ(partitions[0]["id"].As(), 1); + EXPECT_EQ(partitions[0]["id"].As(), 1); EXPECT_EQ(partitions[0]["hub"].As(), "hub-1"); - EXPECT_EQ(partitions[1]["id"].As(), 2); + EXPECT_EQ(partitions[1]["id"].As(), 2); EXPECT_EQ(partitions[1]["hub"].As(), "hub-2"); + + EXPECT_EQ(partitions[2]["id"].As(), 3); + EXPECT_EQ(partitions[2]["hub"].As(), "hub-3"); } -TEST(PartitionMapSerializer, DeserializeRestoresOriginalData) -{ +TEST(PartitionMapSerializerTest, RoundTripPreservesData) { const auto original = MakePartitionMap(); const auto json = SerializePartitionMap(original); @@ -85,53 +119,148 @@ TEST(PartitionMapSerializer, DeserializeRestoresOriginalData) ASSERT_EQ(restored.Partitions.size(), original.Partitions.size()); for (size_t i = 0; i < original.Partitions.size(); ++i) { - EXPECT_EQ(restored.Partitions[i].first, original.Partitions[i].first); - EXPECT_EQ(restored.Partitions[i].second, original.Partitions[i].second); + EXPECT_EQ(restored.Partitions[i].first, original.Partitions[i].first) + << "Partition ID mismatch at index " << i; + EXPECT_EQ(restored.Partitions[i].second, original.Partitions[i].second) + << "Hub endpoint mismatch at index " << i; } } -TEST(PartitionMapSerializer, HandlesEmptyPartitions) -{ - TPartitionMap map; - map.Epoch = TEpoch{123}; +TEST(PartitionMapSerializerTest, HandlesEmptyPartitions) { + const auto map = MakeEmptyPartitionMap(); const auto json = SerializePartitionMap(map); const auto restored = DeserializePartitionMap(json); - EXPECT_EQ(restored.Epoch, TEpoch{123}); + EXPECT_EQ(restored.Epoch, TEpoch{100}); EXPECT_TRUE(restored.Partitions.empty()); + + const auto& partitions = json["partitions"]; + EXPECT_TRUE(partitions.IsArray()); + EXPECT_EQ(partitions.GetSize(), 0); +} + +TEST(PartitionMapSerializerTest, DeserializeParsesValidJson) { + userver::formats::json::ValueBuilder builder; + builder["epoch"] = 999; + + userver::formats::json::ValueBuilder partitions(userver::formats::common::Type::kArray); + + userver::formats::json::ValueBuilder p1; + p1["id"] = 5; + p1["hub"] = "test-hub-5"; + partitions.PushBack(p1.ExtractValue()); + + userver::formats::json::ValueBuilder p2; + p2["id"] = 7; + p2["hub"] = "test-hub-7"; + partitions.PushBack(p2.ExtractValue()); + + builder["partitions"] = partitions.ExtractValue(); + + const auto restored = DeserializePartitionMap(builder.ExtractValue()); + + EXPECT_EQ(restored.Epoch, TEpoch{999}); + ASSERT_EQ(restored.Partitions.size(), 2); + EXPECT_EQ(restored.Partitions[0].first, TPartitionId{5}); + EXPECT_EQ(restored.Partitions[0].second, THubEndpoint{"test-hub-5"}); + EXPECT_EQ(restored.Partitions[1].first, TPartitionId{7}); + EXPECT_EQ(restored.Partitions[1].second, THubEndpoint{"test-hub-7"}); +} + +//////////////////////////////////////////////////////////////////////////////// + +TEST(CoordinationContextSerializerTest, SerializeProducesValidJson) { + const auto context = MakeCoordinationContext(); + const auto json = SerializeCoordinationContext(context); + + ASSERT_TRUE(json.HasMember("partitions_context")); + ASSERT_TRUE(json["partitions_context"].IsArray()); + EXPECT_EQ(json["partitions_context"].GetSize(), 3); + + std::unordered_set foundIds; + for (const auto& item : json["partitions_context"]) { + ASSERT_TRUE(item.HasMember("id")); + foundIds.insert(item["id"].As()); + } + + EXPECT_TRUE(foundIds.count(10)); + EXPECT_TRUE(foundIds.count(20)); + EXPECT_TRUE(foundIds.count(30)); } -TEST(CoordinationContextSerializer, RoundTripRestoresAllCombinations) { - const auto original = MakeTestContext(); +TEST(CoordinationContextSerializerTest, RoundTripPreservesData) { + const auto original = MakeCoordinationContext(); const auto json = SerializeCoordinationContext(original); const auto restored = DeserializeCoordinationContext(json); ASSERT_EQ(restored.PartitionCooldowns.size(), original.PartitionCooldowns.size()); for (const auto& [id, epoch] : original.PartitionCooldowns) { - ASSERT_TRUE(restored.PartitionCooldowns.count(id)) << "Missing ID in cooldowns: " << id.GetUnderlying(); + ASSERT_TRUE(restored.PartitionCooldowns.count(id)) + << "Missing partition " << id.GetUnderlying() << " in cooldowns"; EXPECT_EQ(restored.PartitionCooldowns.at(id), epoch); } ASSERT_EQ(restored.PartitionWeights.size(), original.PartitionWeights.size()); for (const auto& [id, weight] : original.PartitionWeights) { - ASSERT_TRUE(restored.PartitionWeights.count(id)) << "Missing ID in weights: " << id.GetUnderlying(); + ASSERT_TRUE(restored.PartitionWeights.count(id)) + << "Missing partition " << id.GetUnderlying() << " in weights"; EXPECT_EQ(restored.PartitionWeights.at(id), weight); } } -TEST(CoordinationContextSerializer, JsonStructureCheck) { - const auto context = MakeTestContext(); +TEST(CoordinationContextSerializerTest, HandlesEmptyContext) { + const auto context = MakeEmptyCoordinationContext(); + const auto json = SerializeCoordinationContext(context); + const auto restored = DeserializeCoordinationContext(json); + EXPECT_TRUE(restored.PartitionCooldowns.empty()); + EXPECT_TRUE(restored.PartitionWeights.empty()); + ASSERT_TRUE(json["partitions_context"].IsArray()); - EXPECT_EQ(json["partitions_context"].GetSize(), 3); + EXPECT_EQ(json["partitions_context"].GetSize(), 0); +} + +TEST(CoordinationContextSerializerTest, DeserializeParsesValidJson) { + userver::formats::json::ValueBuilder builder; + userver::formats::json::ValueBuilder items(userver::formats::common::Type::kArray); + + userver::formats::json::ValueBuilder item1; + item1["id"] = 100; + item1["cooldown_epoch"] = 500; + item1["partition_weight"] = 250; + items.PushBack(item1.ExtractValue()); + + userver::formats::json::ValueBuilder item2; + item2["id"] = 200; + item2["cooldown_epoch"] = 600; + items.PushBack(item2.ExtractValue()); + + builder["partitions_context"] = items.ExtractValue(); + + const auto context = DeserializeCoordinationContext(builder.ExtractValue()); + + EXPECT_EQ(context.PartitionCooldowns.size(), 2); + EXPECT_EQ(context.PartitionWeights.size(), 1); + + EXPECT_EQ(context.PartitionCooldowns.at(TPartitionId{100}), TEpoch{500}); + EXPECT_EQ(context.PartitionWeights.at(TPartitionId{100}), TPartitionWeight{250}); + EXPECT_EQ(context.PartitionCooldowns.at(TPartitionId{200}), TEpoch{600}); +} + +TEST(CoordinationContextSerializerTest, JsonStructureValidation) { + const auto context = MakeCoordinationContext(); + const auto json = SerializeCoordinationContext(context); for (const auto& item : json["partitions_context"]) { - uint64_t id = item["id"].As(); + std::uint64_t id = item["id"].As(); - if (id == 20) { + if (id == 10) { + EXPECT_TRUE(item.HasMember("cooldown_epoch")); + EXPECT_TRUE(item.HasMember("partition_weight")); + } else if (id == 20) { EXPECT_TRUE(item.HasMember("cooldown_epoch")); EXPECT_FALSE(item.HasMember("partition_weight")); } else if (id == 30) { @@ -141,47 +270,92 @@ TEST(CoordinationContextSerializer, JsonStructureCheck) { } } -TEST(HubReportDeserializer, ParsesValidReport) -{ - userver::formats::json::ValueBuilder json; +//////////////////////////////////////////////////////////////////////////////// - json["epoch"] = std::to_string(100); - json["endpoint"] = "hub-42"; - json["dc"] = "eu-west"; - json["load_factor"] = 73; +TEST(HubReportSerializerTest, SerializeProducesValidJson) { + const auto report = MakeHubReport(); + const auto json = SerializeHubReport(report); - userver::formats::json::ValueBuilder partitions; - partitions["1"] = std::to_string(10); - partitions["2"] = std::to_string(20); + ASSERT_TRUE(json.HasMember("epoch")); + ASSERT_TRUE(json.HasMember("endpoint")); + ASSERT_TRUE(json.HasMember("dc")); + ASSERT_TRUE(json.HasMember("load_factor")); + ASSERT_TRUE(json.HasMember("partitions")); - json["partitions"] = partitions.ExtractValue(); + EXPECT_EQ(json["epoch"].As(), "123"); + EXPECT_EQ(json["endpoint"].As(), "hub.example.com"); + EXPECT_EQ(json["dc"].As(), "myt"); + EXPECT_EQ(json["load_factor"].As(), 85); - const auto report = DeserializeHubReport(json.ExtractValue()); + const auto& partitions = json["partitions"]; + ASSERT_TRUE(partitions.IsObject()); + EXPECT_EQ(partitions.GetSize(), 3); - EXPECT_EQ(report.Epoch, TEpoch{100}); - EXPECT_EQ(report.Endpoint, THubEndpoint{"hub-42"}); - EXPECT_EQ(report.DC, THubDC{"eu-west"}); - EXPECT_EQ(report.LoadFactor, TLoadFactor{73}); + EXPECT_EQ(partitions["10"].As(), "500"); + EXPECT_EQ(partitions["20"].As(), "600"); + EXPECT_EQ(partitions["30"].As(), "700"); +} + +TEST(HubReportSerializerTest, RoundTripPreservesData) { + const auto original = MakeHubReport(); + + const auto json = SerializeHubReport(original); + const auto restored = DeserializeHubReport(json); + + EXPECT_EQ(restored.Epoch, original.Epoch); + EXPECT_EQ(restored.Endpoint, original.Endpoint); + EXPECT_EQ(restored.DC, original.DC); + EXPECT_EQ(restored.LoadFactor, original.LoadFactor); + + ASSERT_EQ(restored.PartitionWeights.size(), original.PartitionWeights.size()); + for (const auto& [id, weight] : original.PartitionWeights) { + ASSERT_TRUE(restored.PartitionWeights.count(id)) + << "Missing partition " << id.GetUnderlying() << " in restored report"; + EXPECT_EQ(restored.PartitionWeights.at(id), weight); + } +} + +TEST(HubReportSerializerTest, HandlesEmptyPartitions) { + const auto report = MakeEmptyHubReport(); + + const auto json = SerializeHubReport(report); + const auto restored = DeserializeHubReport(json); - ASSERT_EQ(report.PartitionWeights.size(), 2); + EXPECT_EQ(restored.Epoch, TEpoch{1}); + EXPECT_EQ(restored.Endpoint, THubEndpoint{"empty-hub"}); + EXPECT_EQ(restored.DC, THubDC{"vla"}); + EXPECT_EQ(restored.LoadFactor, TLoadFactor{0}); + EXPECT_TRUE(restored.PartitionWeights.empty()); - EXPECT_EQ(report.PartitionWeights.at(TPartitionId{1}), TPartitionWeight{10}); - EXPECT_EQ(report.PartitionWeights.at(TPartitionId{2}), TPartitionWeight{20}); + ASSERT_TRUE(json.HasMember("partitions")); + EXPECT_TRUE(json["partitions"].IsObject()); + EXPECT_EQ(json["partitions"].GetSize(), 0); } -TEST(HubReportDeserializer, HandlesEmptyPartitions) -{ - userver::formats::json::ValueBuilder json; +TEST(HubReportSerializerTest, DeserializeParsesValidJson) { + userver::formats::json::ValueBuilder builder; + builder["epoch"] = "999"; + builder["endpoint"] = "test-hub.com"; + builder["dc"] = "eu-west"; + builder["load_factor"] = 42; - json["epoch"] = "1"; - json["endpoint"] = "hub"; - json["dc"] = "dc"; - json["load_factor"] = 0; - json["partitions"] = userver::formats::json::ValueBuilder{}.ExtractValue(); + userver::formats::json::ValueBuilder partitions; + partitions["1"] = "100"; + partitions["2"] = "200"; + partitions["3"] = "300"; + builder["partitions"] = partitions.ExtractValue(); - const auto report = DeserializeHubReport(json.ExtractValue()); + const auto report = DeserializeHubReport(builder.ExtractValue()); + + EXPECT_EQ(report.Epoch, TEpoch{999}); + EXPECT_EQ(report.Endpoint, THubEndpoint{"test-hub.com"}); + EXPECT_EQ(report.DC, THubDC{"eu-west"}); + EXPECT_EQ(report.LoadFactor, TLoadFactor{42}); - EXPECT_TRUE(report.PartitionWeights.empty()); + ASSERT_EQ(report.PartitionWeights.size(), 3); + EXPECT_EQ(report.PartitionWeights.at(TPartitionId{1}), TPartitionWeight{100}); + EXPECT_EQ(report.PartitionWeights.at(TPartitionId{2}), TPartitionWeight{200}); + EXPECT_EQ(report.PartitionWeights.at(TPartitionId{3}), TPartitionWeight{300}); } //////////////////////////////////////////////////////////////////////////////// diff --git a/tests/test_get_hub_reports.py b/tests/test_get_hub_reports.py new file mode 100644 index 0000000..f5e406f --- /dev/null +++ b/tests/test_get_hub_reports.py @@ -0,0 +1,7 @@ +import pytest + +async def test_get_hub_reports(service_client): + response = await service_client.get('/admin/hub_reports') + assert response.status == 200 + assert 'application/json' in response.headers['Content-Type'] + assert response.text == '{"hub_reports":[]}'