From 7b1915840b6a4a8b65bc60c9acec6cc6a56004b2 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 17 Jan 2025 01:09:32 +0300 Subject: [PATCH 01/42] init etcd client --- CMakeLists.txt | 6 ++ cmake/install/userver-etcd-config.cmake | 11 +++ etcd/CMakeLists.txt | 8 ++ etcd/include/userver/storages/etcd/client.hpp | 33 +++++++ .../userver/storages/etcd/component.hpp | 30 ++++++ etcd/library.yaml | 9 ++ etcd/src/storages/etcd/client.cpp | 92 +++++++++++++++++++ etcd/src/storages/etcd/component.cpp | 40 ++++++++ .../pytest_userver/plugins/config.py | 2 +- 9 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 cmake/install/userver-etcd-config.cmake create mode 100644 etcd/CMakeLists.txt create mode 100644 etcd/include/userver/storages/etcd/client.hpp create mode 100644 etcd/include/userver/storages/etcd/component.hpp create mode 100644 etcd/library.yaml create mode 100644 etcd/src/storages/etcd/client.cpp create mode 100644 etcd/src/storages/etcd/component.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fe708131cdfd..23cbf07ba037 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,6 +148,7 @@ option(USERVER_FEATURE_YDB "Provide asynchronous driver for YDB" "${USERVER_YDB_ option(USERVER_FEATURE_OTLP "Provide asynchronous OTLP exporters" "${USERVER_LIB_ENABLED_DEFAULT}") option(USERVER_FEATURE_SQLITE "Provide asynchronous driver for SQLite" "${USERVER_LIB_ENABLED_DEFAULT}") option(USERVER_FEATURE_ODBC "Provide asynchronous wrapper around ODBC" "${USERVER_LIB_ENABLED_DEFAULT}") +option(USERVER_FEATURE_ETCD "Provide asynchronous driver for etcd" "${USERVER_LIB_ENABLED_DEFAULT}") set(CMAKE_DEBUG_POSTFIX d) @@ -312,6 +313,11 @@ if (USERVER_FEATURE_ODBC) list(APPEND USERVER_AVAILABLE_COMPONENTS odbc) endif() +if (USERVER_FEATURE_ETCD) + _require_userver_core("USERVER_FEATURE_ETCD") + add_subdirectory(etcd) +endif() + add_subdirectory(libraries) if (USERVER_BUILD_TESTS) diff --git a/cmake/install/userver-etcd-config.cmake b/cmake/install/userver-etcd-config.cmake new file mode 100644 index 000000000000..0d4826b3ab69 --- /dev/null +++ b/cmake/install/userver-etcd-config.cmake @@ -0,0 +1,11 @@ +include_guard(GLOBAL) + +if(userver_etcd_FOUND) + return() +endif() + +find_package(userver REQUIRED COMPONENTS + core +) + +set(userver_etcd_FOUND TRUE) diff --git a/etcd/CMakeLists.txt b/etcd/CMakeLists.txt new file mode 100644 index 000000000000..f37b3bca274d --- /dev/null +++ b/etcd/CMakeLists.txt @@ -0,0 +1,8 @@ +project(userver-etcd CXX) + +# TODO: Add etcd setup + +userver_module(etcd + SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" + UTEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*_test.cpp" +) diff --git a/etcd/include/userver/storages/etcd/client.hpp b/etcd/include/userver/storages/etcd/client.hpp new file mode 100644 index 000000000000..4dde62d73831 --- /dev/null +++ b/etcd/include/userver/storages/etcd/client.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +struct ClientV2Settings final { + std::vector endpoints; +}; + +ClientV2Settings Parse(const yaml_config::YamlConfig& value, formats::parse::To); + +class ClientV2 final { +public: + ClientV2(clients::http::Client& http_client, ClientV2Settings settings); + void Put(const std::string& key, const std::string& value); + [[nodiscard]] std::vector Range(const std::string& key); +private: + clients::http::Client& http_client_; + const ClientV2Settings settings_; +}; + +using ClientV2Ptr = std::shared_ptr; + +} + +USERVER_NAMESPACE_END diff --git a/etcd/include/userver/storages/etcd/component.hpp b/etcd/include/userver/storages/etcd/component.hpp new file mode 100644 index 000000000000..321b8ecf469e --- /dev/null +++ b/etcd/include/userver/storages/etcd/component.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +class Component final : public components::ComponentBase { +public: + static constexpr std::string_view kName = "etcd"; + + Component(const components::ComponentConfig&, const components::ComponentContext&); + + ~Component() = default; + + static yaml_config::Schema GetStaticConfigSchema(); + + ClientV2Ptr GetClientV2(); + +private: + const ClientV2Ptr etcd_client_v2_ptr_; +}; + +} + +USERVER_NAMESPACE_END diff --git a/etcd/library.yaml b/etcd/library.yaml new file mode 100644 index 000000000000..0988e0388455 --- /dev/null +++ b/etcd/library.yaml @@ -0,0 +1,9 @@ +project-name: userver-etcd +project-alt-names: + - yandex-userver-etcd +maintainers: + - Common components +description: Etcd driver + +libraries: + - userver-core diff --git a/etcd/src/storages/etcd/client.cpp b/etcd/src/storages/etcd/client.cpp new file mode 100644 index 000000000000..e1fb41527626 --- /dev/null +++ b/etcd/src/storages/etcd/client.cpp @@ -0,0 +1,92 @@ +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +namespace { + +std::string BuildPutUrl(const std::string& service_url) { + return fmt::format("{}/v3/kv/put", service_url); +} + +std::string BuildPutData(const std::string& key, const std::string& value) { + formats::json::StringBuilder sb; + { + formats::json::StringBuilder::ObjectGuard guard{sb}; + sb.Key("key"); + sb.WriteString(crypto::base64::Base64Encode(key)); + sb.Key("value"); + sb.WriteString(crypto::base64::Base64Encode(value)); + } + return sb.GetString(); +} + +std::string BuildRangeUrl(const std::string& service_url) { + return fmt::format("{}/v3/kv/range", service_url); +} + +std::string BuildRangeData(const std::string& key) { + formats::json::StringBuilder sb; + { + formats::json::StringBuilder::ObjectGuard guard{sb}; + sb.Key("key"); + sb.WriteString(crypto::base64::Base64Encode(key)); + } + return sb.GetString(); +} + +} + +ClientV2Settings Parse(const yaml_config::YamlConfig& value, formats::parse::To) { + ClientV2Settings result; + result.endpoints = value["endpoints"].As>(result.endpoints); + return result; +} + +ClientV2::ClientV2(clients::http::Client& http_client, ClientV2Settings settings) + : http_client_(http_client), + settings_(settings) { +} + +void ClientV2::Put(const std::string& key, const std::string& value) { + const auto service_url = settings_.endpoints.at(0); + auto request = http_client_ + .CreateRequest() + .post(BuildPutUrl(service_url), BuildPutData(key, value)); + auto response = request.perform(); +} + +std::vector ClientV2::Range(const std::string& key) { + const auto service_url = settings_.endpoints.at(0); + auto request = http_client_ + .CreateRequest() + .post(BuildRangeUrl(service_url), BuildRangeData(key)); + auto response = request.perform(); + const auto json_body = formats::json::FromString(response->body()); + const auto& key_value_list = json_body["kvs"]; + std::vector values; + values.reserve(key_value_list.GetSize()); + for (const auto& key_value : key_value_list) { + values.push_back( + crypto::base64::Base64Decode(key_value["value"].As()) + ); + } + LOG_ERROR() << "||| " << values[0]; + return values; +} + +} // namespace storages::etcd + +USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/component.cpp b/etcd/src/storages/etcd/component.cpp new file mode 100644 index 000000000000..c98dc5426384 --- /dev/null +++ b/etcd/src/storages/etcd/component.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include + + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +Component::Component(const components::ComponentConfig& config, const components::ComponentContext& context) + : + ComponentBase(config, context), + etcd_client_v2_ptr_(std::make_shared( + context.FindComponent().GetHttpClient(), + config.As() + )) {} + +yaml_config::Schema Component::GetStaticConfigSchema() { + return yaml_config::MergeSchemas(R"( +type: object +description: Etcd client component +additionalProperties: false +properties: + endpoints: + type: array + description: etcd hosts + items: + type: string + description: host +)"); +} + +ClientV2Ptr Component::GetClientV2() { + return etcd_client_v2_ptr_; +} + +} // namespace storages::etcd + +USERVER_NAMESPACE_END diff --git a/testsuite/pytest_plugins/pytest_userver/plugins/config.py b/testsuite/pytest_plugins/pytest_userver/plugins/config.py index 0d5b1e168fd5..ca1d401442c7 100644 --- a/testsuite/pytest_plugins/pytest_userver/plugins/config.py +++ b/testsuite/pytest_plugins/pytest_userver/plugins/config.py @@ -515,7 +515,7 @@ def allowed_url_prefixes_extra() -> List[str]: @ingroup userver_testsuite_fixtures """ - return [] + return ["http://localhost:2379"] @pytest.fixture(scope='session') From 0de12483d8d51df8b1815e914b84c2b231c6e213 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 17 Jan 2025 01:09:52 +0300 Subject: [PATCH 02/42] minor fixes --- etcd/src/storages/etcd/client.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/etcd/src/storages/etcd/client.cpp b/etcd/src/storages/etcd/client.cpp index e1fb41527626..87e7edd6e313 100644 --- a/etcd/src/storages/etcd/client.cpp +++ b/etcd/src/storages/etcd/client.cpp @@ -83,7 +83,6 @@ std::vector ClientV2::Range(const std::string& key) { crypto::base64::Base64Decode(key_value["value"].As()) ); } - LOG_ERROR() << "||| " << values[0]; return values; } From 5436c1efda508bc70258137c0ff501f0ec57a153 Mon Sep 17 00:00:00 2001 From: eskemer Date: Mon, 20 Jan 2025 11:22:44 +0300 Subject: [PATCH 03/42] iteration --- etcd/include/userver/storages/etcd/client.hpp | 17 ++--- .../userver/storages/etcd/settings.hpp | 27 ++++++++ etcd/src/storages/etcd/client.cpp | 62 ++++++++++++++----- etcd/src/storages/etcd/component.cpp | 14 ++++- etcd/src/storages/etcd/settings.cpp | 32 ++++++++++ 5 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 etcd/include/userver/storages/etcd/settings.hpp create mode 100644 etcd/src/storages/etcd/settings.cpp diff --git a/etcd/include/userver/storages/etcd/client.hpp b/etcd/include/userver/storages/etcd/client.hpp index 4dde62d73831..61f57cae25a8 100644 --- a/etcd/include/userver/storages/etcd/client.hpp +++ b/etcd/include/userver/storages/etcd/client.hpp @@ -1,29 +1,32 @@ #pragma once +#include +#include #include #include #include +#include +#include #include USERVER_NAMESPACE_BEGIN namespace storages::etcd { -struct ClientV2Settings final { - std::vector endpoints; -}; - -ClientV2Settings Parse(const yaml_config::YamlConfig& value, formats::parse::To); - class ClientV2 final { public: ClientV2(clients::http::Client& http_client, ClientV2Settings settings); void Put(const std::string& key, const std::string& value); [[nodiscard]] std::vector Range(const std::string& key); + void DeleteRange(const std::string& key); private: + [[nodiscard]] std::shared_ptr + PerformEtcdRequest(const std::function& url_builder, const std::string& data); + clients::http::Client& http_client_; - const ClientV2Settings settings_; + engine::SharedMutex endpoints_shared_mutex_; + ClientV2Settings settings_; }; using ClientV2Ptr = std::shared_ptr; diff --git a/etcd/include/userver/storages/etcd/settings.hpp b/etcd/include/userver/storages/etcd/settings.hpp new file mode 100644 index 000000000000..4aa5ad2553a3 --- /dev/null +++ b/etcd/include/userver/storages/etcd/settings.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +struct ClientV2Settings final { + std::vector endpoints; + std::uint32_t retries; + std::chrono::microseconds request_timeout_ms; +}; + +} + +namespace formats::parse { + +storages::etcd::ClientV2Settings Parse(const yaml_config::YamlConfig& value, To); + +} + +USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/client.cpp b/etcd/src/storages/etcd/client.cpp index 87e7edd6e313..3bd2148e517e 100644 --- a/etcd/src/storages/etcd/client.cpp +++ b/etcd/src/storages/etcd/client.cpp @@ -1,14 +1,17 @@ #include #include +#include #include #include #include #include +#include #include #include +#include #include USERVER_NAMESPACE_BEGIN @@ -47,12 +50,24 @@ std::string BuildRangeData(const std::string& key) { return sb.GetString(); } +std::string BuildDeleteRangeUrl(const std::string& service_url) { + return fmt::format("{}/v3/kv/deleterange", service_url); +} + +std::string BuildDeleteRangeData(const std::string& key) { + formats::json::StringBuilder sb; + { + formats::json::StringBuilder::ObjectGuard guard{sb}; + sb.Key("key"); + sb.WriteString(crypto::base64::Base64Encode(key)); + } + return sb.GetString(); +} + +bool ShouldRetry(std::shared_ptr response) { + return false; } -ClientV2Settings Parse(const yaml_config::YamlConfig& value, formats::parse::To) { - ClientV2Settings result; - result.endpoints = value["endpoints"].As>(result.endpoints); - return result; } ClientV2::ClientV2(clients::http::Client& http_client, ClientV2Settings settings) @@ -61,19 +76,12 @@ ClientV2::ClientV2(clients::http::Client& http_client, ClientV2Settings settings } void ClientV2::Put(const std::string& key, const std::string& value) { - const auto service_url = settings_.endpoints.at(0); - auto request = http_client_ - .CreateRequest() - .post(BuildPutUrl(service_url), BuildPutData(key, value)); - auto response = request.perform(); + auto response = PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); } std::vector ClientV2::Range(const std::string& key) { - const auto service_url = settings_.endpoints.at(0); - auto request = http_client_ - .CreateRequest() - .post(BuildRangeUrl(service_url), BuildRangeData(key)); - auto response = request.perform(); + auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); + const auto json_body = formats::json::FromString(response->body()); const auto& key_value_list = json_body["kvs"]; std::vector values; @@ -86,6 +94,32 @@ std::vector ClientV2::Range(const std::string& key) { return values; } +void ClientV2::DeleteRange(const std::string& key) { + auto response = PerformEtcdRequest(BuildDeleteRangeUrl, BuildDeleteRangeData(key)); +} + +std::shared_ptr ClientV2::PerformEtcdRequest( + const std::function& url_builder, const std::string& data +) { + endpoints_shared_mutex_.lock_shared(); + auto endpoints = settings_.endpoints; + endpoints_shared_mutex_.unlock_shared(); + utils::Shuffle(endpoints); + + for (const auto& endpoint : endpoints) { + auto response = http_client_ + .CreateRequest() + .post(url_builder(endpoint), data) + .retry(settings_.retries) + .timeout(settings_.request_timeout_ms.count()) + .perform(); + LOG_DEBUG() << "Response: " << formats::json::FromString(response->body()); + if (!ShouldRetry(response)) { + return response; + } + } +} + } // namespace storages::etcd USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/component.cpp b/etcd/src/storages/etcd/component.cpp index c98dc5426384..affede3a6f30 100644 --- a/etcd/src/storages/etcd/component.cpp +++ b/etcd/src/storages/etcd/component.cpp @@ -1,6 +1,7 @@ #include #include +#include #include @@ -19,15 +20,24 @@ Component::Component(const components::ComponentConfig& config, const components yaml_config::Schema Component::GetStaticConfigSchema() { return yaml_config::MergeSchemas(R"( type: object -description: Etcd client component +description: Etcd cluster component additionalProperties: false properties: endpoints: type: array - description: etcd hosts + description: Etcd endpoints items: type: string description: host + retries: + type: integer + description: > + Number of retries per one endpoints, total number of retries is number of endpoints times retries + minimum: 1 + request_timeout_ms: + type: integer + description: Number of miliseconds to timeout request + minimum: 1 )"); } diff --git a/etcd/src/storages/etcd/settings.cpp b/etcd/src/storages/etcd/settings.cpp new file mode 100644 index 000000000000..29b450b6dea7 --- /dev/null +++ b/etcd/src/storages/etcd/settings.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + + +namespace storages::etcd { + +namespace { + +constexpr std::uint32_t kDefaultRetries{3}; +constexpr std::chrono::milliseconds kDefaultRequestTimeout{1'000}; + +} + +} + +namespace formats::parse { + +storages::etcd::ClientV2Settings Parse(const yaml_config::YamlConfig& cofig, To) { + storages::etcd::ClientV2Settings result; + result.endpoints = cofig["endpoints"].As>(result.endpoints); + result.retries = cofig["retries"].As(storages::etcd::kDefaultRetries); + result.request_timeout_ms = cofig["request_timeout_ms"].As(storages::etcd::kDefaultRequestTimeout); + return result; +} + +} // namespace formats::parse + +USERVER_NAMESPACE_END From eb2bc58b4f32c97ce5b595f385f182607e1b4f3e Mon Sep 17 00:00:00 2001 From: eskemer Date: Mon, 20 Jan 2025 11:36:30 +0300 Subject: [PATCH 04/42] rename client --- etcd/include/userver/storages/etcd/client.hpp | 8 ++++---- etcd/include/userver/storages/etcd/component.hpp | 4 ++-- etcd/include/userver/storages/etcd/settings.hpp | 4 ++-- etcd/src/storages/etcd/client.cpp | 10 +++++----- etcd/src/storages/etcd/component.cpp | 8 ++++---- etcd/src/storages/etcd/settings.cpp | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/etcd/include/userver/storages/etcd/client.hpp b/etcd/include/userver/storages/etcd/client.hpp index 61f57cae25a8..2074a6575a3a 100644 --- a/etcd/include/userver/storages/etcd/client.hpp +++ b/etcd/include/userver/storages/etcd/client.hpp @@ -14,9 +14,9 @@ USERVER_NAMESPACE_BEGIN namespace storages::etcd { -class ClientV2 final { +class Client final { public: - ClientV2(clients::http::Client& http_client, ClientV2Settings settings); + Client(clients::http::Client& http_client, ClientSettings settings); void Put(const std::string& key, const std::string& value); [[nodiscard]] std::vector Range(const std::string& key); void DeleteRange(const std::string& key); @@ -26,10 +26,10 @@ class ClientV2 final { clients::http::Client& http_client_; engine::SharedMutex endpoints_shared_mutex_; - ClientV2Settings settings_; + ClientSettings settings_; }; -using ClientV2Ptr = std::shared_ptr; +using ClientPtr = std::shared_ptr; } diff --git a/etcd/include/userver/storages/etcd/component.hpp b/etcd/include/userver/storages/etcd/component.hpp index 321b8ecf469e..170bed6d987e 100644 --- a/etcd/include/userver/storages/etcd/component.hpp +++ b/etcd/include/userver/storages/etcd/component.hpp @@ -19,10 +19,10 @@ class Component final : public components::ComponentBase { static yaml_config::Schema GetStaticConfigSchema(); - ClientV2Ptr GetClientV2(); + ClientPtr GetClient(); private: - const ClientV2Ptr etcd_client_v2_ptr_; + const ClientPtr etcd_client_ptr_; }; } diff --git a/etcd/include/userver/storages/etcd/settings.hpp b/etcd/include/userver/storages/etcd/settings.hpp index 4aa5ad2553a3..d12a0694e0a2 100644 --- a/etcd/include/userver/storages/etcd/settings.hpp +++ b/etcd/include/userver/storages/etcd/settings.hpp @@ -10,7 +10,7 @@ USERVER_NAMESPACE_BEGIN namespace storages::etcd { -struct ClientV2Settings final { +struct ClientSettings final { std::vector endpoints; std::uint32_t retries; std::chrono::microseconds request_timeout_ms; @@ -20,7 +20,7 @@ struct ClientV2Settings final { namespace formats::parse { -storages::etcd::ClientV2Settings Parse(const yaml_config::YamlConfig& value, To); +storages::etcd::ClientSettings Parse(const yaml_config::YamlConfig& value, To); } diff --git a/etcd/src/storages/etcd/client.cpp b/etcd/src/storages/etcd/client.cpp index 3bd2148e517e..c54a544325df 100644 --- a/etcd/src/storages/etcd/client.cpp +++ b/etcd/src/storages/etcd/client.cpp @@ -70,16 +70,16 @@ bool ShouldRetry(std::shared_ptr response) { } -ClientV2::ClientV2(clients::http::Client& http_client, ClientV2Settings settings) +Client::Client(clients::http::Client& http_client, ClientSettings settings) : http_client_(http_client), settings_(settings) { } -void ClientV2::Put(const std::string& key, const std::string& value) { +void Client::Put(const std::string& key, const std::string& value) { auto response = PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); } -std::vector ClientV2::Range(const std::string& key) { +std::vector Client::Range(const std::string& key) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); const auto json_body = formats::json::FromString(response->body()); @@ -94,11 +94,11 @@ std::vector ClientV2::Range(const std::string& key) { return values; } -void ClientV2::DeleteRange(const std::string& key) { +void Client::DeleteRange(const std::string& key) { auto response = PerformEtcdRequest(BuildDeleteRangeUrl, BuildDeleteRangeData(key)); } -std::shared_ptr ClientV2::PerformEtcdRequest( +std::shared_ptr Client::PerformEtcdRequest( const std::function& url_builder, const std::string& data ) { endpoints_shared_mutex_.lock_shared(); diff --git a/etcd/src/storages/etcd/component.cpp b/etcd/src/storages/etcd/component.cpp index affede3a6f30..0712fed90da6 100644 --- a/etcd/src/storages/etcd/component.cpp +++ b/etcd/src/storages/etcd/component.cpp @@ -12,9 +12,9 @@ namespace storages::etcd { Component::Component(const components::ComponentConfig& config, const components::ComponentContext& context) : ComponentBase(config, context), - etcd_client_v2_ptr_(std::make_shared( + etcd_client_ptr_(std::make_shared( context.FindComponent().GetHttpClient(), - config.As() + config.As() )) {} yaml_config::Schema Component::GetStaticConfigSchema() { @@ -41,8 +41,8 @@ additionalProperties: false )"); } -ClientV2Ptr Component::GetClientV2() { - return etcd_client_v2_ptr_; +ClientPtr Component::GetClient() { + return etcd_client_ptr_; } } // namespace storages::etcd diff --git a/etcd/src/storages/etcd/settings.cpp b/etcd/src/storages/etcd/settings.cpp index 29b450b6dea7..c5f81a8a9a75 100644 --- a/etcd/src/storages/etcd/settings.cpp +++ b/etcd/src/storages/etcd/settings.cpp @@ -19,8 +19,8 @@ constexpr std::chrono::milliseconds kDefaultRequestTimeout{1'000}; namespace formats::parse { -storages::etcd::ClientV2Settings Parse(const yaml_config::YamlConfig& cofig, To) { - storages::etcd::ClientV2Settings result; +storages::etcd::ClientSettings Parse(const yaml_config::YamlConfig& cofig, To) { + storages::etcd::ClientSettings result; result.endpoints = cofig["endpoints"].As>(result.endpoints); result.retries = cofig["retries"].As(storages::etcd::kDefaultRetries); result.request_timeout_ms = cofig["request_timeout_ms"].As(storages::etcd::kDefaultRequestTimeout); From 4ff117092becaa6439a898c238386b1ba2f0c8be Mon Sep 17 00:00:00 2001 From: eskemer Date: Sun, 2 Feb 2025 01:28:10 +0300 Subject: [PATCH 05/42] iteration --- etcd/include/userver/storages/etcd/client.hpp | 40 +++++-- .../userver/storages/etcd/settings.hpp | 6 +- etcd/src/storages/etcd/client.cpp | 110 +++++++++++++----- etcd/src/storages/etcd/component.cpp | 2 +- etcd/src/storages/etcd/settings.cpp | 10 +- .../pytest_userver/plugins/config.py | 2 +- 6 files changed, 120 insertions(+), 50 deletions(-) diff --git a/etcd/include/userver/storages/etcd/client.hpp b/etcd/include/userver/storages/etcd/client.hpp index 2074a6575a3a..bf8611ec7a33 100644 --- a/etcd/include/userver/storages/etcd/client.hpp +++ b/etcd/include/userver/storages/etcd/client.hpp @@ -3,10 +3,11 @@ #include #include #include +#include #include #include -#include +#include #include #include @@ -14,21 +15,44 @@ USERVER_NAMESPACE_BEGIN namespace storages::etcd { -class Client final { +namespace impl { + +class ClientImpl; + +} + +class Client { public: - Client(clients::http::Client& http_client, ClientSettings settings); - void Put(const std::string& key, const std::string& value); - [[nodiscard]] std::vector Range(const std::string& key); - void DeleteRange(const std::string& key); + virtual ~Client() = default; + virtual void Put(const std::string& key, const std::string& value) = 0; + [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; + virtual void DeleteRange(const std::string& key) = 0; + virtual void StartWatch(const std::string& key) = 0; +}; + +namespace impl { + +class ClientImpl : public Client { + public: + ~ClientImpl() override = default; + ClientImpl(clients::http::Client& http_client, ClientSettings settings); + void Put(const std::string& key, const std::string& value) override; + [[nodiscard]] std::vector Range(const std::string& key) override; + void DeleteRange(const std::string& key) override; + void StartWatch(const std::string& key) override; private: [[nodiscard]] std::shared_ptr PerformEtcdRequest(const std::function& url_builder, const std::string& data); + [[nodiscard]] clients::http::StreamedResponse PerformStreamEtcdRequest( + const std::function& url_builder, const std::string& data); clients::http::Client& http_client_; - engine::SharedMutex endpoints_shared_mutex_; - ClientSettings settings_; + userver::engine::TaskWithResult watch_task_; + const ClientSettings settings_; }; +} + using ClientPtr = std::shared_ptr; } diff --git a/etcd/include/userver/storages/etcd/settings.hpp b/etcd/include/userver/storages/etcd/settings.hpp index d12a0694e0a2..d05777c21d4b 100644 --- a/etcd/include/userver/storages/etcd/settings.hpp +++ b/etcd/include/userver/storages/etcd/settings.hpp @@ -11,9 +11,9 @@ USERVER_NAMESPACE_BEGIN namespace storages::etcd { struct ClientSettings final { - std::vector endpoints; - std::uint32_t retries; - std::chrono::microseconds request_timeout_ms; + const std::vector endpoints; + const std::uint32_t retries; + const std::chrono::microseconds request_timeout_ms; }; } diff --git a/etcd/src/storages/etcd/client.cpp b/etcd/src/storages/etcd/client.cpp index c54a544325df..313563c8da8e 100644 --- a/etcd/src/storages/etcd/client.cpp +++ b/etcd/src/storages/etcd/client.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include @@ -5,14 +7,19 @@ #include +#include +#include +#include #include #include +#include #include #include #include #include #include #include +#include USERVER_NAMESPACE_BEGIN @@ -25,15 +32,10 @@ std::string BuildPutUrl(const std::string& service_url) { } std::string BuildPutData(const std::string& key, const std::string& value) { - formats::json::StringBuilder sb; - { - formats::json::StringBuilder::ObjectGuard guard{sb}; - sb.Key("key"); - sb.WriteString(crypto::base64::Base64Encode(key)); - sb.Key("value"); - sb.WriteString(crypto::base64::Base64Encode(value)); - } - return sb.GetString(); + formats::json::ValueBuilder builder; + builder["key"] = crypto::base64::Base64Encode(key); + builder["value"] = crypto::base64::Base64Encode(value); + return formats::json::ToString(builder.ExtractValue()); } std::string BuildRangeUrl(const std::string& service_url) { @@ -41,13 +43,9 @@ std::string BuildRangeUrl(const std::string& service_url) { } std::string BuildRangeData(const std::string& key) { - formats::json::StringBuilder sb; - { - formats::json::StringBuilder::ObjectGuard guard{sb}; - sb.Key("key"); - sb.WriteString(crypto::base64::Base64Encode(key)); - } - return sb.GetString(); + formats::json::ValueBuilder builder; + builder["key"] = crypto::base64::Base64Encode(key); + return formats::json::ToString(builder.ExtractValue()); } std::string BuildDeleteRangeUrl(const std::string& service_url) { @@ -55,31 +53,40 @@ std::string BuildDeleteRangeUrl(const std::string& service_url) { } std::string BuildDeleteRangeData(const std::string& key) { - formats::json::StringBuilder sb; - { - formats::json::StringBuilder::ObjectGuard guard{sb}; - sb.Key("key"); - sb.WriteString(crypto::base64::Base64Encode(key)); - } - return sb.GetString(); + formats::json::ValueBuilder builder; + builder["key"] = crypto::base64::Base64Encode(key); + return formats::json::ToString(builder.ExtractValue()); +} + + +std::string BuildWatchUrl(const std::string& service_url) { + return fmt::format("{}/v3/watch", service_url); } -bool ShouldRetry(std::shared_ptr response) { +std::string BuildWatchData(const std::string& key) { + formats::json::ValueBuilder builder; + builder["create_request"]["key"] = crypto::base64::Base64Encode(key); + return formats::json::ToString(builder.ExtractValue()); +} + +bool ShouldRetry(const http::StatusCode status_code) { return false; } } -Client::Client(clients::http::Client& http_client, ClientSettings settings) +namespace impl { + +ClientImpl::ClientImpl(clients::http::Client& http_client, ClientSettings settings) : http_client_(http_client), settings_(settings) { } -void Client::Put(const std::string& key, const std::string& value) { +void ClientImpl::Put(const std::string& key, const std::string& value) { auto response = PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); } -std::vector Client::Range(const std::string& key) { +std::vector ClientImpl::Range(const std::string& key) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); const auto json_body = formats::json::FromString(response->body()); @@ -94,16 +101,53 @@ std::vector Client::Range(const std::string& key) { return values; } -void Client::DeleteRange(const std::string& key) { +void ClientImpl::DeleteRange(const std::string& key) { auto response = PerformEtcdRequest(BuildDeleteRangeUrl, BuildDeleteRangeData(key)); } -std::shared_ptr Client::PerformEtcdRequest( +clients::http::StreamedResponse ClientImpl::PerformStreamEtcdRequest( + const std::function& url_builder, const std::string& data +){ + auto endpoints = settings_.endpoints; + utils::Shuffle(endpoints); + + for (const auto& endpoint : endpoints) { + const auto queue = concurrent::StringStreamQueue::Create(); + auto stream_response = http_client_ + .CreateRequest() + .post(url_builder(endpoint), data) + .retry(settings_.retries) + .timeout(1'000'000'000) + .async_perform_stream_body(queue); + if (!ShouldRetry(stream_response.StatusCode())) { + return stream_response; + } + } +} + +void ClientImpl::StartWatch(const std::string& key) { + + const auto data = BuildWatchData(key); + auto stream_response = PerformStreamEtcdRequest(BuildWatchUrl, BuildWatchData(key)); + + watch_task_ = utils::Async( + "watch task", + [stream_response = std::move(stream_response)] mutable { + std::string body_part; + std::string result; + const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); + while (stream_response.ReadChunk(body_part, deadline)) { + LOG_ERROR() << "Kek " << body_part; + result += body_part; + } + } + ); +} + +std::shared_ptr ClientImpl::PerformEtcdRequest( const std::function& url_builder, const std::string& data ) { - endpoints_shared_mutex_.lock_shared(); auto endpoints = settings_.endpoints; - endpoints_shared_mutex_.unlock_shared(); utils::Shuffle(endpoints); for (const auto& endpoint : endpoints) { @@ -114,12 +158,14 @@ std::shared_ptr Client::PerformEtcdRequest( .timeout(settings_.request_timeout_ms.count()) .perform(); LOG_DEBUG() << "Response: " << formats::json::FromString(response->body()); - if (!ShouldRetry(response)) { + if (!ShouldRetry(response->status_code())) { return response; } } } +} + } // namespace storages::etcd USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/component.cpp b/etcd/src/storages/etcd/component.cpp index 0712fed90da6..bc6d7ab52501 100644 --- a/etcd/src/storages/etcd/component.cpp +++ b/etcd/src/storages/etcd/component.cpp @@ -12,7 +12,7 @@ namespace storages::etcd { Component::Component(const components::ComponentConfig& config, const components::ComponentContext& context) : ComponentBase(config, context), - etcd_client_ptr_(std::make_shared( + etcd_client_ptr_(std::make_shared( context.FindComponent().GetHttpClient(), config.As() )) {} diff --git a/etcd/src/storages/etcd/settings.cpp b/etcd/src/storages/etcd/settings.cpp index c5f81a8a9a75..dd307e87c9f5 100644 --- a/etcd/src/storages/etcd/settings.cpp +++ b/etcd/src/storages/etcd/settings.cpp @@ -20,11 +20,11 @@ constexpr std::chrono::milliseconds kDefaultRequestTimeout{1'000}; namespace formats::parse { storages::etcd::ClientSettings Parse(const yaml_config::YamlConfig& cofig, To) { - storages::etcd::ClientSettings result; - result.endpoints = cofig["endpoints"].As>(result.endpoints); - result.retries = cofig["retries"].As(storages::etcd::kDefaultRetries); - result.request_timeout_ms = cofig["request_timeout_ms"].As(storages::etcd::kDefaultRequestTimeout); - return result; + return storages::etcd::ClientSettings { + .endpoints = cofig["endpoints"].As>(), + .retries = cofig["retries"].As(storages::etcd::kDefaultRetries), + .request_timeout_ms = cofig["request_timeout_ms"].As(storages::etcd::kDefaultRequestTimeout), + }; } } // namespace formats::parse diff --git a/testsuite/pytest_plugins/pytest_userver/plugins/config.py b/testsuite/pytest_plugins/pytest_userver/plugins/config.py index ca1d401442c7..f93e0146267d 100644 --- a/testsuite/pytest_plugins/pytest_userver/plugins/config.py +++ b/testsuite/pytest_plugins/pytest_userver/plugins/config.py @@ -539,7 +539,7 @@ def patch_config(config, config_vars): return http_client = components['http-client'] or {} http_client['testsuite-enabled'] = True - http_client['testsuite-timeout'] = '10s' + # http_client['testsuite-timeout'] = '30s' allowed_urls = [mockserver_info.base_url] if mockserver_ssl_info: From 83b37a513aec8b04a27987231354ffe87e877bbf Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Mon, 3 Feb 2025 02:30:39 +0300 Subject: [PATCH 06/42] iteration --- etcd/CMakeLists.txt | 2 - etcd/include/userver/storages/etcd/client.hpp | 9 ++- .../userver/storages/etcd/exceptions.hpp | 19 ++++++ .../userver/storages/etcd/watch_listener.hpp | 25 ++++++++ etcd/src/storages/etcd/client.cpp | 63 +++++++++++++------ etcd/src/storages/etcd/watch_listener.cpp | 19 ++++++ 6 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 etcd/include/userver/storages/etcd/exceptions.hpp create mode 100644 etcd/include/userver/storages/etcd/watch_listener.hpp create mode 100644 etcd/src/storages/etcd/watch_listener.cpp diff --git a/etcd/CMakeLists.txt b/etcd/CMakeLists.txt index f37b3bca274d..aced3e0e2523 100644 --- a/etcd/CMakeLists.txt +++ b/etcd/CMakeLists.txt @@ -1,7 +1,5 @@ project(userver-etcd CXX) -# TODO: Add etcd setup - userver_module(etcd SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" UTEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*_test.cpp" diff --git a/etcd/include/userver/storages/etcd/client.hpp b/etcd/include/userver/storages/etcd/client.hpp index bf8611ec7a33..66f3864bc821 100644 --- a/etcd/include/userver/storages/etcd/client.hpp +++ b/etcd/include/userver/storages/etcd/client.hpp @@ -7,8 +7,10 @@ #include #include +#include #include #include +#include #include USERVER_NAMESPACE_BEGIN @@ -27,7 +29,7 @@ class Client { virtual void Put(const std::string& key, const std::string& value) = 0; [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; virtual void DeleteRange(const std::string& key) = 0; - virtual void StartWatch(const std::string& key) = 0; + virtual WatchListener StartWatch(const std::string& key) = 0; }; namespace impl { @@ -39,7 +41,7 @@ class ClientImpl : public Client { void Put(const std::string& key, const std::string& value) override; [[nodiscard]] std::vector Range(const std::string& key) override; void DeleteRange(const std::string& key) override; - void StartWatch(const std::string& key) override; + WatchListener StartWatch(const std::string& key) override; private: [[nodiscard]] std::shared_ptr PerformEtcdRequest(const std::function& url_builder, const std::string& data); @@ -47,7 +49,8 @@ class ClientImpl : public Client { const std::function& url_builder, const std::string& data); clients::http::Client& http_client_; - userver::engine::TaskWithResult watch_task_; + std::vector> watch_tasks_; + std::vector>> watch_queues_; const ClientSettings settings_; }; diff --git a/etcd/include/userver/storages/etcd/exceptions.hpp b/etcd/include/userver/storages/etcd/exceptions.hpp new file mode 100644 index 000000000000..8979b6b42896 --- /dev/null +++ b/etcd/include/userver/storages/etcd/exceptions.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +/// @brief Base class for all etcd client exceptions +class EtcdError : public std::runtime_error { +public: + using std::runtime_error::runtime_error; +}; + +} + +USERVER_NAMESPACE_END diff --git a/etcd/include/userver/storages/etcd/watch_listener.hpp b/etcd/include/userver/storages/etcd/watch_listener.hpp new file mode 100644 index 000000000000..6d6a4cd124ba --- /dev/null +++ b/etcd/include/userver/storages/etcd/watch_listener.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +struct KVEvent final { + std::string key; + std::string value; + std::size_t version; +}; + +struct WatchListener final { + concurrent::SpscQueue::Consumer consumer; + + KVEvent GetEvent(); +}; + +} + +USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/client.cpp b/etcd/src/storages/etcd/client.cpp index 313563c8da8e..545b6b350de9 100644 --- a/etcd/src/storages/etcd/client.cpp +++ b/etcd/src/storages/etcd/client.cpp @@ -9,7 +9,6 @@ #include #include -#include #include #include #include @@ -105,6 +104,49 @@ void ClientImpl::DeleteRange(const std::string& key) { auto response = PerformEtcdRequest(BuildDeleteRangeUrl, BuildDeleteRangeData(key)); } +WatchListener ClientImpl::StartWatch(const std::string& key) { + + const auto data = BuildWatchData(key); + auto stream_response = PerformStreamEtcdRequest(BuildWatchUrl, BuildWatchData(key)); + auto queue = concurrent::SpscQueue::Create(); + // TODO: add lock + watch_queues_.push_back(queue); + watch_tasks_.push_back(utils::Async( + "watch task", + [stream_response = std::move(stream_response), produser = queue->GetProducer()] mutable { + std::string body_part; + const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); + while (stream_response.ReadChunk(body_part, deadline)) { + const auto watch_response = formats::json::FromString(body_part); + LOG_ERROR() << watch_response; + if (!watch_response["result"].HasMember("events")) { + LOG_INFO() << "No events in watch part response, skipping"; + continue; + } + for (const auto event : watch_response["result"]["events"]) { + if (!event.HasMember("kv")) { + continue; + } + const auto key = crypto::base64::Base64Decode(event["kv"]["key"].As()); + const auto value = crypto::base64::Base64Decode(event["kv"]["value"].As()); + const auto version = std::stoi(event["kv"]["version"].As()); + if (!produser.PushNoblock({ + .key = key, + .value = value, + .version = version, + })) { + LOG_ERROR() << "PushNoblock failed"; + return; + }; + } + } + } + )); + return WatchListener{ + .consumer = queue->GetConsumer() + }; +} + clients::http::StreamedResponse ClientImpl::PerformStreamEtcdRequest( const std::function& url_builder, const std::string& data ){ @@ -125,25 +167,6 @@ clients::http::StreamedResponse ClientImpl::PerformStreamEtcdRequest( } } -void ClientImpl::StartWatch(const std::string& key) { - - const auto data = BuildWatchData(key); - auto stream_response = PerformStreamEtcdRequest(BuildWatchUrl, BuildWatchData(key)); - - watch_task_ = utils::Async( - "watch task", - [stream_response = std::move(stream_response)] mutable { - std::string body_part; - std::string result; - const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); - while (stream_response.ReadChunk(body_part, deadline)) { - LOG_ERROR() << "Kek " << body_part; - result += body_part; - } - } - ); -} - std::shared_ptr ClientImpl::PerformEtcdRequest( const std::function& url_builder, const std::string& data ) { diff --git a/etcd/src/storages/etcd/watch_listener.cpp b/etcd/src/storages/etcd/watch_listener.cpp new file mode 100644 index 000000000000..375677eb6ccd --- /dev/null +++ b/etcd/src/storages/etcd/watch_listener.cpp @@ -0,0 +1,19 @@ +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace storages::etcd { + +KVEvent WatchListener::GetEvent() { + KVEvent event; + if (!consumer.Pop(event)) { + throw EtcdError("Consumer pop failed"); + } + return event; +} + +} // namespace formats::parse + +USERVER_NAMESPACE_END From bb38f454740443c219c27356c44d48e681feea19 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sun, 16 Feb 2025 22:19:21 +0300 Subject: [PATCH 07/42] iteration --- etcd/include/userver/etcd/client.hpp | 59 ++++++ .../userver/{storages => }/etcd/component.hpp | 10 +- .../{storages => }/etcd/exceptions.hpp | 4 +- .../userver/{storages => }/etcd/settings.hpp | 8 +- etcd/include/userver/etcd/watch_listener.hpp | 32 +++ etcd/include/userver/storages/etcd/client.hpp | 63 ------ .../userver/storages/etcd/watch_listener.hpp | 25 --- etcd/src/etcd/client_impl.cpp | 183 +++++++++++++++++ etcd/src/etcd/client_impl.hpp | 41 ++++ etcd/src/{storages => }/etcd/component.cpp | 31 ++- etcd/src/{storages => }/etcd/settings.cpp | 19 +- etcd/src/etcd/watch_listener.cpp | 32 +++ etcd/src/storages/etcd/client.cpp | 194 ------------------ etcd/src/storages/etcd/watch_listener.cpp | 19 -- 14 files changed, 380 insertions(+), 340 deletions(-) create mode 100644 etcd/include/userver/etcd/client.hpp rename etcd/include/userver/{storages => }/etcd/component.hpp (76%) rename etcd/include/userver/{storages => }/etcd/exceptions.hpp (87%) rename etcd/include/userver/{storages => }/etcd/settings.hpp (65%) create mode 100644 etcd/include/userver/etcd/watch_listener.hpp delete mode 100644 etcd/include/userver/storages/etcd/client.hpp delete mode 100644 etcd/include/userver/storages/etcd/watch_listener.hpp create mode 100644 etcd/src/etcd/client_impl.cpp create mode 100644 etcd/src/etcd/client_impl.hpp rename etcd/src/{storages => }/etcd/component.cpp (54%) rename etcd/src/{storages => }/etcd/settings.cpp (50%) create mode 100644 etcd/src/etcd/watch_listener.cpp delete mode 100644 etcd/src/storages/etcd/client.cpp delete mode 100644 etcd/src/storages/etcd/watch_listener.cpp diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp new file mode 100644 index 000000000000..6f9b7f010986 --- /dev/null +++ b/etcd/include/userver/etcd/client.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace etcd { + +class Client { +public: + virtual ~Client() = default; + virtual void Put(const std::string& key, const std::string& value) = 0; + [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; + virtual void DeleteRange(const std::string& key) = 0; + virtual WatchListener StartWatch(const std::string& key) = 0; +}; + +// namespace impl { + +// class ClientImpl : public Client { +// public: +// ~ClientImpl() override = default; +// ClientImpl(clients::http::Client& http_client, ClientSettings settings); +// void Put(const std::string& key, const std::string& value) override; +// [[nodiscard]] std::vector Range(const std::string& key) override; +// void DeleteRange(const std::string& key) override; +// WatchListener StartWatch(const std::string& key) override; + +// private: +// [[nodiscard]] std::shared_ptr +// PerformEtcdRequest(const std::function& url_builder, const std::string& data); +// [[nodiscard]] clients::http::StreamedResponse PerformStreamEtcdRequest( +// const std::function& url_builder, +// const std::string& data +// ); + +// clients::http::Client& http_client_; +// std::vector>> watch_queues_; +// const ClientSettings settings_; +// }; + +// } // namespace impl + +using ClientPtr = std::shared_ptr; + +} // namespace etcd + +USERVER_NAMESPACE_END diff --git a/etcd/include/userver/storages/etcd/component.hpp b/etcd/include/userver/etcd/component.hpp similarity index 76% rename from etcd/include/userver/storages/etcd/component.hpp rename to etcd/include/userver/etcd/component.hpp index 170bed6d987e..48111eb626a3 100644 --- a/etcd/include/userver/storages/etcd/component.hpp +++ b/etcd/include/userver/etcd/component.hpp @@ -3,20 +3,18 @@ #include #include #include -#include +#include USERVER_NAMESPACE_BEGIN -namespace storages::etcd { +namespace etcd { class Component final : public components::ComponentBase { public: - static constexpr std::string_view kName = "etcd"; + static constexpr std::string_view kName = "etcd-сlient"; Component(const components::ComponentConfig&, const components::ComponentContext&); - ~Component() = default; - static yaml_config::Schema GetStaticConfigSchema(); ClientPtr GetClient(); @@ -25,6 +23,6 @@ class Component final : public components::ComponentBase { const ClientPtr etcd_client_ptr_; }; -} +} // namespace etcd USERVER_NAMESPACE_END diff --git a/etcd/include/userver/storages/etcd/exceptions.hpp b/etcd/include/userver/etcd/exceptions.hpp similarity index 87% rename from etcd/include/userver/storages/etcd/exceptions.hpp rename to etcd/include/userver/etcd/exceptions.hpp index 8979b6b42896..a6215cb5fbf7 100644 --- a/etcd/include/userver/storages/etcd/exceptions.hpp +++ b/etcd/include/userver/etcd/exceptions.hpp @@ -6,7 +6,7 @@ USERVER_NAMESPACE_BEGIN -namespace storages::etcd { +namespace etcd { /// @brief Base class for all etcd client exceptions class EtcdError : public std::runtime_error { @@ -14,6 +14,6 @@ class EtcdError : public std::runtime_error { using std::runtime_error::runtime_error; }; -} +} // namespace etcd USERVER_NAMESPACE_END diff --git a/etcd/include/userver/storages/etcd/settings.hpp b/etcd/include/userver/etcd/settings.hpp similarity index 65% rename from etcd/include/userver/storages/etcd/settings.hpp rename to etcd/include/userver/etcd/settings.hpp index d05777c21d4b..4fbab86254fc 100644 --- a/etcd/include/userver/storages/etcd/settings.hpp +++ b/etcd/include/userver/etcd/settings.hpp @@ -8,19 +8,19 @@ USERVER_NAMESPACE_BEGIN -namespace storages::etcd { +namespace etcd { struct ClientSettings final { const std::vector endpoints; - const std::uint32_t retries; + const std::uint32_t attempts; const std::chrono::microseconds request_timeout_ms; }; -} +} // namespace etcd namespace formats::parse { -storages::etcd::ClientSettings Parse(const yaml_config::YamlConfig& value, To); +etcd::ClientSettings Parse(const yaml_config::YamlConfig& value, To); } diff --git a/etcd/include/userver/etcd/watch_listener.hpp b/etcd/include/userver/etcd/watch_listener.hpp new file mode 100644 index 000000000000..b46a454ceb5a --- /dev/null +++ b/etcd/include/userver/etcd/watch_listener.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace etcd { + +struct KeyValueEvent final { + std::string key; + std::string value; + std::int32_t version; +}; + +struct WatchListener final { + concurrent::SpscQueue::Consumer consumer; + + KeyValueEvent GetEvent(); +}; + +} // namespace etcd + +namespace formats::parse { + +etcd::KeyValueEvent Parse(const formats::json::Value& value, To); + +} + +USERVER_NAMESPACE_END diff --git a/etcd/include/userver/storages/etcd/client.hpp b/etcd/include/userver/storages/etcd/client.hpp deleted file mode 100644 index 66f3864bc821..000000000000 --- a/etcd/include/userver/storages/etcd/client.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -USERVER_NAMESPACE_BEGIN - -namespace storages::etcd { - -namespace impl { - -class ClientImpl; - -} - -class Client { -public: - virtual ~Client() = default; - virtual void Put(const std::string& key, const std::string& value) = 0; - [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; - virtual void DeleteRange(const std::string& key) = 0; - virtual WatchListener StartWatch(const std::string& key) = 0; -}; - -namespace impl { - -class ClientImpl : public Client { - public: - ~ClientImpl() override = default; - ClientImpl(clients::http::Client& http_client, ClientSettings settings); - void Put(const std::string& key, const std::string& value) override; - [[nodiscard]] std::vector Range(const std::string& key) override; - void DeleteRange(const std::string& key) override; - WatchListener StartWatch(const std::string& key) override; -private: - [[nodiscard]] std::shared_ptr - PerformEtcdRequest(const std::function& url_builder, const std::string& data); - [[nodiscard]] clients::http::StreamedResponse PerformStreamEtcdRequest( - const std::function& url_builder, const std::string& data); - - clients::http::Client& http_client_; - std::vector> watch_tasks_; - std::vector>> watch_queues_; - const ClientSettings settings_; -}; - -} - -using ClientPtr = std::shared_ptr; - -} - -USERVER_NAMESPACE_END diff --git a/etcd/include/userver/storages/etcd/watch_listener.hpp b/etcd/include/userver/storages/etcd/watch_listener.hpp deleted file mode 100644 index 6d6a4cd124ba..000000000000 --- a/etcd/include/userver/storages/etcd/watch_listener.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include - -#include - -USERVER_NAMESPACE_BEGIN - -namespace storages::etcd { - -struct KVEvent final { - std::string key; - std::string value; - std::size_t version; -}; - -struct WatchListener final { - concurrent::SpscQueue::Consumer consumer; - - KVEvent GetEvent(); -}; - -} - -USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp new file mode 100644 index 000000000000..86bfdcad1514 --- /dev/null +++ b/etcd/src/etcd/client_impl.cpp @@ -0,0 +1,183 @@ +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace etcd { + +namespace { + +const std::uint32_t kMinRetryStatusCode = 500; +const std::uint32_t kMaxRetryStatusCode = 599; + +const std::uint32_t kMinGoodStatusCode = 200; +const std::uint32_t kMaxGoodStatusCode = 299; + +std::string BuildPutUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/put", service_url); } + +std::string BuildPutData(const std::string& key, const std::string& value) { + return formats::json::ToString(formats::json::MakeObject( + "key", crypto::base64::Base64Encode(key), "value", crypto::base64::Base64Encode(value) + )); +} + +std::string BuildRangeUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/range", service_url); } + +std::string BuildRangeData(const std::string& key) { + return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(key))); +} + +std::string BuildDeleteRangeUrl(const std::string& service_url) { + return fmt::format("{}/v3/kv/deleterange", service_url); +} + +std::string BuildDeleteRangeData(const std::string& key) { + return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(key))); +} + +std::string BuildWatchUrl(const std::string& service_url) { return fmt::format("{}/v3/watch", service_url); } + +std::string BuildWatchData(const std::string& key) { + return formats::json::ToString( + formats::json::MakeObject("create_request", formats::json::MakeObject("key", crypto::base64::Base64Encode(key))) + ); +} + +bool ShouldRetry(const http::StatusCode status_code) { + return kMinRetryStatusCode <= status_code && status_code <= kMaxRetryStatusCode; +} + +void CheckRequestStatusCode(const http::StatusCode status_code) { + if (status_code < kMinGoodStatusCode || kMaxGoodStatusCode < status_code) { + throw EtcdError("Got bad status code from etcd"); + } +} + +} // namespace + +namespace impl { + +ClientImpl::ClientImpl(clients::http::Client& http_client, ClientSettings settings) + : http_client_(http_client), settings_(settings) {} + +void ClientImpl::Put(const std::string& key, const std::string& value) { + auto response = PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); +} + +std::vector ClientImpl::Range(const std::string& key) { + auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); + + const auto json_body = formats::json::FromString(response->body()); + const auto& key_value_list = json_body["kvs"]; + std::vector values; + values.reserve(key_value_list.GetSize()); + for (const auto& key_value : key_value_list) { + values.push_back(crypto::base64::Base64Decode(key_value["value"].As())); + } + return values; +} + +void ClientImpl::DeleteRange(const std::string& key) { + auto response = PerformEtcdRequest(BuildDeleteRangeUrl, BuildDeleteRangeData(key)); +} + +WatchListener ClientImpl::StartWatch(const std::string& key) { + const auto data = BuildWatchData(key); + auto stream_response = PerformStreamEtcdRequest(BuildWatchUrl, BuildWatchData(key)); + auto queue = concurrent::SpscQueue::Create(); + + watch_queues_lock_.lock(); + watch_queues_.push_back(queue); + watch_queues_lock_.unlock(); + + utils::Async("watch task", [stream_response = std::move(stream_response), produser = queue->GetProducer()] mutable { + std::string body_part; + const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); + while (stream_response.ReadChunk(body_part, deadline)) { + const auto watch_response = formats::json::FromString(body_part); + LOG_ERROR() << watch_response; + if (!watch_response["result"].HasMember("events")) { + LOG_INFO() << "No events in watch part response, skipping"; + continue; + } + for (const auto event : watch_response["result"]["events"]) { + if (!event.HasMember("kv")) { + continue; + } + if (!produser.PushNoblock(event["kv"].As())) { + LOG_ERROR() << "PushNoblock failed"; + return; + }; + } + } + }).Detach(); + return WatchListener{.consumer = queue->GetConsumer()}; +} + +clients::http::StreamedResponse ClientImpl::PerformStreamEtcdRequest( + const std::function& url_builder, + const std::string& data +) { + auto endpoints = settings_.endpoints; + utils::Shuffle(endpoints); + + for (const auto& endpoint : endpoints) { + const auto queue = concurrent::StringStreamQueue::Create(); + auto stream_response = http_client_.CreateRequest() + .post(url_builder(endpoint), data) + .retry(settings_.attempts) + .timeout(1'000'000'000) + .async_perform_stream_body(queue); + if (!ShouldRetry(stream_response.StatusCode())) { + CheckRequestStatusCode(stream_response.StatusCode()); + return stream_response; + } + } + throw EtcdError("Failed to get Ok response from etcd"); +} + +std::shared_ptr ClientImpl::PerformEtcdRequest( + const std::function& url_builder, + const std::string& data +) { + auto endpoints = settings_.endpoints; + utils::Shuffle(endpoints); + + for (const auto& endpoint : endpoints) { + auto response = http_client_.CreateRequest() + .post(url_builder(endpoint), data) + .retry(settings_.attempts) + .timeout(settings_.request_timeout_ms.count()) + .perform(); + LOG_DEBUG() << "Response: " << formats::json::FromString(response->body()); + if (!ShouldRetry(response->status_code())) { + CheckRequestStatusCode(response->status_code()); + return response; + } + } + throw EtcdError("Failed to get Ok response from etcd"); +} + +} // namespace impl + +} // namespace etcd + +USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp new file mode 100644 index 000000000000..34136a2d69ac --- /dev/null +++ b/etcd/src/etcd/client_impl.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace etcd { + +namespace impl { + +class ClientImpl : public Client { +public: + ~ClientImpl() override = default; + ClientImpl(clients::http::Client& http_client, ClientSettings settings); + void Put(const std::string& key, const std::string& value) override; + [[nodiscard]] std::vector Range(const std::string& key) override; + void DeleteRange(const std::string& key) override; + WatchListener StartWatch(const std::string& key) override; + +private: + [[nodiscard]] std::shared_ptr + PerformEtcdRequest(const std::function& url_builder, const std::string& data); + [[nodiscard]] clients::http::StreamedResponse PerformStreamEtcdRequest( + const std::function& url_builder, + const std::string& data + ); + + clients::http::Client& http_client_; + engine::Mutex watch_queues_lock_; + std::vector>> watch_queues_; + const ClientSettings settings_; +}; + +} // namespace impl + +} // namespace etcd + +USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/component.cpp b/etcd/src/etcd/component.cpp similarity index 54% rename from etcd/src/storages/etcd/component.cpp rename to etcd/src/etcd/component.cpp index bc6d7ab52501..3f0f4d871601 100644 --- a/etcd/src/storages/etcd/component.cpp +++ b/etcd/src/etcd/component.cpp @@ -1,21 +1,20 @@ -#include +#include +#include #include -#include +#include #include - USERVER_NAMESPACE_BEGIN -namespace storages::etcd { +namespace etcd { Component::Component(const components::ComponentConfig& config, const components::ComponentContext& context) - : - ComponentBase(config, context), - etcd_client_ptr_(std::make_shared( - context.FindComponent().GetHttpClient(), - config.As() - )) {} + : ComponentBase(config, context), + etcd_client_ptr_(std::make_shared( + context.FindComponent().GetHttpClient(), + config.As() + )) {} yaml_config::Schema Component::GetStaticConfigSchema() { return yaml_config::MergeSchemas(R"( @@ -29,22 +28,20 @@ additionalProperties: false items: type: string description: host - retries: + attempts: type: integer description: > - Number of retries per one endpoints, total number of retries is number of endpoints times retries + Number of attempts per one endpoints, total number of attempts is number of endpoints times attempts minimum: 1 request_timeout_ms: type: integer - description: Number of miliseconds to timeout request + description: Number of miliseconds between request attempts minimum: 1 )"); } -ClientPtr Component::GetClient() { - return etcd_client_ptr_; -} +ClientPtr Component::GetClient() { return etcd_client_ptr_; } -} // namespace storages::etcd +} // namespace etcd USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/settings.cpp b/etcd/src/etcd/settings.cpp similarity index 50% rename from etcd/src/storages/etcd/settings.cpp rename to etcd/src/etcd/settings.cpp index dd307e87c9f5..d00e27446821 100644 --- a/etcd/src/storages/etcd/settings.cpp +++ b/etcd/src/etcd/settings.cpp @@ -1,29 +1,28 @@ -#include +#include #include #include USERVER_NAMESPACE_BEGIN - -namespace storages::etcd { +namespace etcd { namespace { -constexpr std::uint32_t kDefaultRetries{3}; +constexpr std::uint32_t kDefaultAttempts{3}; constexpr std::chrono::milliseconds kDefaultRequestTimeout{1'000}; -} +} // namespace -} +} // namespace etcd namespace formats::parse { -storages::etcd::ClientSettings Parse(const yaml_config::YamlConfig& cofig, To) { - return storages::etcd::ClientSettings { +etcd::ClientSettings Parse(const yaml_config::YamlConfig& cofig, To) { + return etcd::ClientSettings{ .endpoints = cofig["endpoints"].As>(), - .retries = cofig["retries"].As(storages::etcd::kDefaultRetries), - .request_timeout_ms = cofig["request_timeout_ms"].As(storages::etcd::kDefaultRequestTimeout), + .attempts = cofig["attempts"].As(etcd::kDefaultAttempts), + .request_timeout_ms = cofig["request_timeout_ms"].As(etcd::kDefaultRequestTimeout), }; } diff --git a/etcd/src/etcd/watch_listener.cpp b/etcd/src/etcd/watch_listener.cpp new file mode 100644 index 000000000000..db1c5ca762e6 --- /dev/null +++ b/etcd/src/etcd/watch_listener.cpp @@ -0,0 +1,32 @@ +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace etcd { + +KeyValueEvent WatchListener::GetEvent() { + KeyValueEvent event; + if (!consumer.Pop(event)) { + throw EtcdError("Consumer pop failed"); + } + return event; +} + +} // namespace etcd + +namespace formats::parse { + +etcd::KeyValueEvent Parse(const formats::json::Value& value, To) { + return etcd::KeyValueEvent{ + .key = crypto::base64::Base64Decode(value["key"].As()), + .value = crypto::base64::Base64Decode(value["value"].As()), + .version = std::stoi(value["version"].As()), + }; +} + +} // namespace formats::parse + +USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/client.cpp b/etcd/src/storages/etcd/client.cpp deleted file mode 100644 index 545b6b350de9..000000000000 --- a/etcd/src/storages/etcd/client.cpp +++ /dev/null @@ -1,194 +0,0 @@ -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -USERVER_NAMESPACE_BEGIN - -namespace storages::etcd { - -namespace { - -std::string BuildPutUrl(const std::string& service_url) { - return fmt::format("{}/v3/kv/put", service_url); -} - -std::string BuildPutData(const std::string& key, const std::string& value) { - formats::json::ValueBuilder builder; - builder["key"] = crypto::base64::Base64Encode(key); - builder["value"] = crypto::base64::Base64Encode(value); - return formats::json::ToString(builder.ExtractValue()); -} - -std::string BuildRangeUrl(const std::string& service_url) { - return fmt::format("{}/v3/kv/range", service_url); -} - -std::string BuildRangeData(const std::string& key) { - formats::json::ValueBuilder builder; - builder["key"] = crypto::base64::Base64Encode(key); - return formats::json::ToString(builder.ExtractValue()); -} - -std::string BuildDeleteRangeUrl(const std::string& service_url) { - return fmt::format("{}/v3/kv/deleterange", service_url); -} - -std::string BuildDeleteRangeData(const std::string& key) { - formats::json::ValueBuilder builder; - builder["key"] = crypto::base64::Base64Encode(key); - return formats::json::ToString(builder.ExtractValue()); -} - - -std::string BuildWatchUrl(const std::string& service_url) { - return fmt::format("{}/v3/watch", service_url); -} - -std::string BuildWatchData(const std::string& key) { - formats::json::ValueBuilder builder; - builder["create_request"]["key"] = crypto::base64::Base64Encode(key); - return formats::json::ToString(builder.ExtractValue()); -} - -bool ShouldRetry(const http::StatusCode status_code) { - return false; -} - -} - -namespace impl { - -ClientImpl::ClientImpl(clients::http::Client& http_client, ClientSettings settings) - : http_client_(http_client), - settings_(settings) { -} - -void ClientImpl::Put(const std::string& key, const std::string& value) { - auto response = PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); -} - -std::vector ClientImpl::Range(const std::string& key) { - auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); - - const auto json_body = formats::json::FromString(response->body()); - const auto& key_value_list = json_body["kvs"]; - std::vector values; - values.reserve(key_value_list.GetSize()); - for (const auto& key_value : key_value_list) { - values.push_back( - crypto::base64::Base64Decode(key_value["value"].As()) - ); - } - return values; -} - -void ClientImpl::DeleteRange(const std::string& key) { - auto response = PerformEtcdRequest(BuildDeleteRangeUrl, BuildDeleteRangeData(key)); -} - -WatchListener ClientImpl::StartWatch(const std::string& key) { - - const auto data = BuildWatchData(key); - auto stream_response = PerformStreamEtcdRequest(BuildWatchUrl, BuildWatchData(key)); - auto queue = concurrent::SpscQueue::Create(); - // TODO: add lock - watch_queues_.push_back(queue); - watch_tasks_.push_back(utils::Async( - "watch task", - [stream_response = std::move(stream_response), produser = queue->GetProducer()] mutable { - std::string body_part; - const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); - while (stream_response.ReadChunk(body_part, deadline)) { - const auto watch_response = formats::json::FromString(body_part); - LOG_ERROR() << watch_response; - if (!watch_response["result"].HasMember("events")) { - LOG_INFO() << "No events in watch part response, skipping"; - continue; - } - for (const auto event : watch_response["result"]["events"]) { - if (!event.HasMember("kv")) { - continue; - } - const auto key = crypto::base64::Base64Decode(event["kv"]["key"].As()); - const auto value = crypto::base64::Base64Decode(event["kv"]["value"].As()); - const auto version = std::stoi(event["kv"]["version"].As()); - if (!produser.PushNoblock({ - .key = key, - .value = value, - .version = version, - })) { - LOG_ERROR() << "PushNoblock failed"; - return; - }; - } - } - } - )); - return WatchListener{ - .consumer = queue->GetConsumer() - }; -} - -clients::http::StreamedResponse ClientImpl::PerformStreamEtcdRequest( - const std::function& url_builder, const std::string& data -){ - auto endpoints = settings_.endpoints; - utils::Shuffle(endpoints); - - for (const auto& endpoint : endpoints) { - const auto queue = concurrent::StringStreamQueue::Create(); - auto stream_response = http_client_ - .CreateRequest() - .post(url_builder(endpoint), data) - .retry(settings_.retries) - .timeout(1'000'000'000) - .async_perform_stream_body(queue); - if (!ShouldRetry(stream_response.StatusCode())) { - return stream_response; - } - } -} - -std::shared_ptr ClientImpl::PerformEtcdRequest( - const std::function& url_builder, const std::string& data -) { - auto endpoints = settings_.endpoints; - utils::Shuffle(endpoints); - - for (const auto& endpoint : endpoints) { - auto response = http_client_ - .CreateRequest() - .post(url_builder(endpoint), data) - .retry(settings_.retries) - .timeout(settings_.request_timeout_ms.count()) - .perform(); - LOG_DEBUG() << "Response: " << formats::json::FromString(response->body()); - if (!ShouldRetry(response->status_code())) { - return response; - } - } -} - -} - -} // namespace storages::etcd - -USERVER_NAMESPACE_END diff --git a/etcd/src/storages/etcd/watch_listener.cpp b/etcd/src/storages/etcd/watch_listener.cpp deleted file mode 100644 index 375677eb6ccd..000000000000 --- a/etcd/src/storages/etcd/watch_listener.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include - -#include - -USERVER_NAMESPACE_BEGIN - -namespace storages::etcd { - -KVEvent WatchListener::GetEvent() { - KVEvent event; - if (!consumer.Pop(event)) { - throw EtcdError("Consumer pop failed"); - } - return event; -} - -} // namespace formats::parse - -USERVER_NAMESPACE_END From 1fe00a21a36017b680d2cbcf2c0e3ea981f639be Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Mon, 17 Feb 2025 01:24:59 +0300 Subject: [PATCH 08/42] iteration --- etcd/include/userver/etcd/client.hpp | 30 ++------------ etcd/src/etcd/client_impl.cpp | 59 ++++++++++++++++------------ etcd/src/etcd/client_impl.hpp | 8 +++- etcd/tests/etcd_client_test.cpp | 0 4 files changed, 44 insertions(+), 53 deletions(-) create mode 100644 etcd/tests/etcd_client_test.cpp diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 6f9b7f010986..911cb0693ca2 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -20,38 +20,16 @@ namespace etcd { class Client { public: virtual ~Client() = default; + virtual void Put(const std::string& key, const std::string& value) = 0; + [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; + virtual void DeleteRange(const std::string& key) = 0; + virtual WatchListener StartWatch(const std::string& key) = 0; }; -// namespace impl { - -// class ClientImpl : public Client { -// public: -// ~ClientImpl() override = default; -// ClientImpl(clients::http::Client& http_client, ClientSettings settings); -// void Put(const std::string& key, const std::string& value) override; -// [[nodiscard]] std::vector Range(const std::string& key) override; -// void DeleteRange(const std::string& key) override; -// WatchListener StartWatch(const std::string& key) override; - -// private: -// [[nodiscard]] std::shared_ptr -// PerformEtcdRequest(const std::function& url_builder, const std::string& data); -// [[nodiscard]] clients::http::StreamedResponse PerformStreamEtcdRequest( -// const std::function& url_builder, -// const std::string& data -// ); - -// clients::http::Client& http_client_; -// std::vector>> watch_queues_; -// const ClientSettings settings_; -// }; - -// } // namespace impl - using ClientPtr = std::shared_ptr; } // namespace etcd diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 86bfdcad1514..0f7a602e9a9c 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -67,7 +67,7 @@ bool ShouldRetry(const http::StatusCode status_code) { void CheckRequestStatusCode(const http::StatusCode status_code) { if (status_code < kMinGoodStatusCode || kMaxGoodStatusCode < status_code) { - throw EtcdError("Got bad status code from etcd"); + throw EtcdError(fmt::format("Got bad status code from etcd: {}", status_code)); } } @@ -101,14 +101,14 @@ void ClientImpl::DeleteRange(const std::string& key) { WatchListener ClientImpl::StartWatch(const std::string& key) { const auto data = BuildWatchData(key); - auto stream_response = PerformStreamEtcdRequest(BuildWatchUrl, BuildWatchData(key)); + auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); auto queue = concurrent::SpscQueue::Create(); watch_queues_lock_.lock(); watch_queues_.push_back(queue); watch_queues_lock_.unlock(); - utils::Async("watch task", [stream_response = std::move(stream_response), produser = queue->GetProducer()] mutable { + utils::Async("watch task", [stream_response = std::move(stream_response), produсer = queue->GetProducer()] mutable { std::string body_part; const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); while (stream_response.ReadChunk(body_part, deadline)) { @@ -118,11 +118,11 @@ WatchListener ClientImpl::StartWatch(const std::string& key) { LOG_INFO() << "No events in watch part response, skipping"; continue; } - for (const auto event : watch_response["result"]["events"]) { + for (const auto& event : watch_response["result"]["events"]) { if (!event.HasMember("kv")) { continue; } - if (!produser.PushNoblock(event["kv"].As())) { + if (!produсer.PushNoblock(event["kv"].As())) { LOG_ERROR() << "PushNoblock failed"; return; }; @@ -132,26 +132,34 @@ WatchListener ClientImpl::StartWatch(const std::string& key) { return WatchListener{.consumer = queue->GetConsumer()}; } -clients::http::StreamedResponse ClientImpl::PerformStreamEtcdRequest( +clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( const std::function& url_builder, const std::string& data ) { auto endpoints = settings_.endpoints; utils::Shuffle(endpoints); + std::optional maybe_streamed_response; for (const auto& endpoint : endpoints) { const auto queue = concurrent::StringStreamQueue::Create(); - auto stream_response = http_client_.CreateRequest() - .post(url_builder(endpoint), data) - .retry(settings_.attempts) - .timeout(1'000'000'000) - .async_perform_stream_body(queue); - if (!ShouldRetry(stream_response.StatusCode())) { - CheckRequestStatusCode(stream_response.StatusCode()); - return stream_response; + maybe_streamed_response = http_client_.CreateRequest() + .post(url_builder(endpoint), data) + .retry(settings_.attempts) + .timeout(1'000'000'000) + .async_perform_stream_body(queue); + auto& streamed_response = maybe_streamed_response.value(); + if (!ShouldRetry(streamed_response.StatusCode())) { + CheckRequestStatusCode(streamed_response.StatusCode()); + return std::move(streamed_response); } } - throw EtcdError("Failed to get Ok response from etcd"); + if (maybe_streamed_response.has_value()) { + throw EtcdError( + "Failed to get Ok response from etcd with status code: " + maybe_streamed_response.value().StatusCode() + ); + } else { + throw EtcdError(fmt::format("Failed to get streamed response, number of etcd endpoints: {}", endpoints.size())); + } } std::shared_ptr ClientImpl::PerformEtcdRequest( @@ -161,19 +169,20 @@ std::shared_ptr ClientImpl::PerformEtcdRequest( auto endpoints = settings_.endpoints; utils::Shuffle(endpoints); + std::shared_ptr response_ptr; for (const auto& endpoint : endpoints) { - auto response = http_client_.CreateRequest() - .post(url_builder(endpoint), data) - .retry(settings_.attempts) - .timeout(settings_.request_timeout_ms.count()) - .perform(); - LOG_DEBUG() << "Response: " << formats::json::FromString(response->body()); - if (!ShouldRetry(response->status_code())) { - CheckRequestStatusCode(response->status_code()); - return response; + response_ptr = http_client_.CreateRequest() + .post(url_builder(endpoint), data) + .retry(settings_.attempts) + .timeout(settings_.request_timeout_ms.count()) + .perform(); + if (!ShouldRetry(response_ptr->status_code())) { + response_ptr->raise_for_status(); + return response_ptr; } } - throw EtcdError("Failed to get Ok response from etcd"); + + throw EtcdError("Failed to get Ok response from etcd with error: " + response_ptr->body()); } } // namespace impl diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index 34136a2d69ac..d6d6c928c3d4 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -13,17 +13,21 @@ namespace impl { class ClientImpl : public Client { public: - ~ClientImpl() override = default; ClientImpl(clients::http::Client& http_client, ClientSettings settings); + void Put(const std::string& key, const std::string& value) override; + [[nodiscard]] std::vector Range(const std::string& key) override; + void DeleteRange(const std::string& key) override; + WatchListener StartWatch(const std::string& key) override; private: [[nodiscard]] std::shared_ptr PerformEtcdRequest(const std::function& url_builder, const std::string& data); - [[nodiscard]] clients::http::StreamedResponse PerformStreamEtcdRequest( + + [[nodiscard]] clients::http::StreamedResponse PerformStreamedEtcdRequest( const std::function& url_builder, const std::string& data ); diff --git a/etcd/tests/etcd_client_test.cpp b/etcd/tests/etcd_client_test.cpp new file mode 100644 index 000000000000..e69de29bb2d1 From 1479ef23b4ac4454770bfeddc5883245169c9ff1 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Mon, 3 Mar 2025 00:48:04 +0300 Subject: [PATCH 09/42] iteration --- etcd/include/userver/etcd/client.hpp | 2 ++ etcd/src/etcd/client_impl.cpp | 43 +++++++++++++++++++++++----- etcd/src/etcd/client_impl.hpp | 2 ++ 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 911cb0693ca2..88b47477effc 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -23,6 +23,8 @@ class Client { virtual void Put(const std::string& key, const std::string& value) = 0; + [[nodiscard]] virtual std::optional Get(const std::string& key) = 0; + [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; virtual void DeleteRange(const std::string& key) = 0; diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 0f7a602e9a9c..4f127606bc30 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -31,18 +31,25 @@ const std::uint32_t kMaxRetryStatusCode = 599; const std::uint32_t kMinGoodStatusCode = 200; const std::uint32_t kMaxGoodStatusCode = 299; +const std::string kKeyPrefix = "/etcd/"; +const std::string kLastPrefix = "/etcd0"; + std::string BuildPutUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/put", service_url); } std::string BuildPutData(const std::string& key, const std::string& value) { + const auto etcd_key = kKeyPrefix + key; return formats::json::ToString(formats::json::MakeObject( - "key", crypto::base64::Base64Encode(key), "value", crypto::base64::Base64Encode(value) + "key", crypto::base64::Base64Encode(etcd_key), "value", crypto::base64::Base64Encode(value) )); } std::string BuildRangeUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/range", service_url); } std::string BuildRangeData(const std::string& key) { - return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(key))); + const auto etcd_key = kKeyPrefix + key; + return formats::json::ToString(formats::json::MakeObject( + "key", crypto::base64::Base64Encode(etcd_key), "range_end", crypto::base64::Base64Encode(kLastPrefix) + )); } std::string BuildDeleteRangeUrl(const std::string& service_url) { @@ -50,15 +57,17 @@ std::string BuildDeleteRangeUrl(const std::string& service_url) { } std::string BuildDeleteRangeData(const std::string& key) { - return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(key))); + const auto etcd_key = kKeyPrefix + key; + return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(etcd_key))); } std::string BuildWatchUrl(const std::string& service_url) { return fmt::format("{}/v3/watch", service_url); } std::string BuildWatchData(const std::string& key) { - return formats::json::ToString( - formats::json::MakeObject("create_request", formats::json::MakeObject("key", crypto::base64::Base64Encode(key))) - ); + const auto etcd_key = kKeyPrefix + key; + return formats::json::ToString(formats::json::MakeObject( + "create_request", formats::json::MakeObject("key", crypto::base64::Base64Encode(etcd_key)) + )); } bool ShouldRetry(const http::StatusCode status_code) { @@ -82,10 +91,30 @@ void ClientImpl::Put(const std::string& key, const std::string& value) { auto response = PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); } +std::optional ClientImpl::Get(const std::string& key) { + auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); + + const auto json_body = formats::json::FromString(response->body()); + if (!json_body.HasMember("kvs")) { + return std::nullopt; + } + const auto& key_value_list = json_body["kvs"]; + const auto etcd_key = kKeyPrefix + key; + for (const auto& key_value : key_value_list) { + if (crypto::base64::Base64Decode(key_value["key"].As()) == etcd_key) { + return crypto::base64::Base64Decode(key_value["value"].As()); + } + } + return std::nullopt; +} + std::vector ClientImpl::Range(const std::string& key) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); const auto json_body = formats::json::FromString(response->body()); + if (!json_body.HasMember("kvs")) { + return {}; + } const auto& key_value_list = json_body["kvs"]; std::vector values; values.reserve(key_value_list.GetSize()); @@ -113,7 +142,7 @@ WatchListener ClientImpl::StartWatch(const std::string& key) { const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); while (stream_response.ReadChunk(body_part, deadline)) { const auto watch_response = formats::json::FromString(body_part); - LOG_ERROR() << watch_response; + LOG_DEBUG() << watch_response; if (!watch_response["result"].HasMember("events")) { LOG_INFO() << "No events in watch part response, skipping"; continue; diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index d6d6c928c3d4..3a787bc5ac23 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -17,6 +17,8 @@ class ClientImpl : public Client { void Put(const std::string& key, const std::string& value) override; + [[nodiscard]] std::optional Get(const std::string& key) override; + [[nodiscard]] std::vector Range(const std::string& key) override; void DeleteRange(const std::string& key) override; From 99aacab71b6b8ab4698555e325ad5d1d1025fd27 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sun, 16 Mar 2025 22:57:39 +0300 Subject: [PATCH 10/42] iteration --- etcd/include/userver/etcd/client.hpp | 2 +- etcd/src/etcd/client_impl.cpp | 75 +++++++++++++++------------- etcd/src/etcd/client_impl.hpp | 9 ++-- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 88b47477effc..3466002494b3 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -27,7 +27,7 @@ class Client { [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; - virtual void DeleteRange(const std::string& key) = 0; + virtual void Delete(const std::string& key) = 0; virtual WatchListener StartWatch(const std::string& key) = 0; }; diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 4f127606bc30..e44eb0c17c79 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -32,7 +33,7 @@ const std::uint32_t kMinGoodStatusCode = 200; const std::uint32_t kMaxGoodStatusCode = 299; const std::string kKeyPrefix = "/etcd/"; -const std::string kLastPrefix = "/etcd0"; +const std::string kLastPossibleKeyPrefix = "/etcd0"; std::string BuildPutUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/put", service_url); } @@ -52,11 +53,9 @@ std::string BuildRangeData(const std::string& key) { )); } -std::string BuildDeleteRangeUrl(const std::string& service_url) { - return fmt::format("{}/v3/kv/deleterange", service_url); -} +std::string BuildDeleteUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/deleterange", service_url); } -std::string BuildDeleteRangeData(const std::string& key) { +std::string BuildDeleteData(const std::string& key) { const auto etcd_key = kKeyPrefix + key; return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(etcd_key))); } @@ -74,7 +73,7 @@ bool ShouldRetry(const http::StatusCode status_code) { return kMinRetryStatusCode <= status_code && status_code <= kMaxRetryStatusCode; } -void CheckRequestStatusCode(const http::StatusCode status_code) { +void CheckResponseStatusCode(const http::StatusCode status_code) { if (status_code < kMinGoodStatusCode || kMaxGoodStatusCode < status_code) { throw EtcdError(fmt::format("Got bad status code from etcd: {}", status_code)); } @@ -124,40 +123,20 @@ std::vector ClientImpl::Range(const std::string& key) { return values; } -void ClientImpl::DeleteRange(const std::string& key) { - auto response = PerformEtcdRequest(BuildDeleteRangeUrl, BuildDeleteRangeData(key)); +void ClientImpl::Delete(const std::string& key) { + auto response = PerformEtcdRequest(BuildDeleteUrl, BuildDeleteData(key)); } WatchListener ClientImpl::StartWatch(const std::string& key) { - const auto data = BuildWatchData(key); - auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); auto queue = concurrent::SpscQueue::Create(); - watch_queues_lock_.lock(); - watch_queues_.push_back(queue); - watch_queues_lock_.unlock(); - - utils::Async("watch task", [stream_response = std::move(stream_response), produсer = queue->GetProducer()] mutable { - std::string body_part; - const auto deadline = engine::Deadline::FromDuration(std::chrono::seconds{100'000'000}); - while (stream_response.ReadChunk(body_part, deadline)) { - const auto watch_response = formats::json::FromString(body_part); - LOG_DEBUG() << watch_response; - if (!watch_response["result"].HasMember("events")) { - LOG_INFO() << "No events in watch part response, skipping"; - continue; - } - for (const auto& event : watch_response["result"]["events"]) { - if (!event.HasMember("kv")) { - continue; - } - if (!produсer.PushNoblock(event["kv"].As())) { - LOG_ERROR() << "PushNoblock failed"; - return; - }; - } - } + auto watch_queues_ptr = watch_queues_.Lock(); + watch_queues_ptr->push_back(queue); + + utils::Async("watch task", [&key, producer = queue->GetProducer(), this] mutable { + this->WatchKeyChanges(key, std::move(producer)); }).Detach(); + return WatchListener{.consumer = queue->GetConsumer()}; } @@ -178,7 +157,7 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( .async_perform_stream_body(queue); auto& streamed_response = maybe_streamed_response.value(); if (!ShouldRetry(streamed_response.StatusCode())) { - CheckRequestStatusCode(streamed_response.StatusCode()); + CheckResponseStatusCode(streamed_response.StatusCode()); return std::move(streamed_response); } } @@ -214,6 +193,32 @@ std::shared_ptr ClientImpl::PerformEtcdRequest( throw EtcdError("Failed to get Ok response from etcd with error: " + response_ptr->body()); } +void ClientImpl::WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer) { + auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); + + std::string body_part; + while (stream_response.ReadChunk( + body_part, engine::Deadline::FromTimePoint(std::chrono::system_clock::time_point::max()) + )) { + const auto watch_response = formats::json::FromString(body_part); + LOG_DEBUG() << watch_response; + if (!watch_response["result"].HasMember("events")) { + LOG_DEBUG() << "No events in watch part response, skipping"; + continue; + } + for (const auto& event : watch_response["result"]["events"]) { + if (!event.HasMember("kv")) { + continue; + } + LOG_DEBUG() << "Got event with kv: " << event["kv"]; + if (!producer.Push(event["kv"].As())) { + LOG_ERROR() << "Could not push to queue, aborting task"; + return; + }; + } + } +} + } // namespace impl } // namespace etcd diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index 3a787bc5ac23..bad3be285cb4 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -21,7 +22,7 @@ class ClientImpl : public Client { [[nodiscard]] std::vector Range(const std::string& key) override; - void DeleteRange(const std::string& key) override; + void Delete(const std::string& key) override; WatchListener StartWatch(const std::string& key) override; @@ -34,9 +35,11 @@ class ClientImpl : public Client { const std::string& data ); + void WatchKeyChanges(const std::string key, concurrent::SpscQueue::Producer producer); + + using WatchQueuePtr = std::shared_ptr>; clients::http::Client& http_client_; - engine::Mutex watch_queues_lock_; - std::vector>> watch_queues_; + concurrent::Variable> watch_queues_; const ClientSettings settings_; }; From 2b41803f4826dab2e252de5048cb6ce56e691337 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sun, 16 Mar 2025 22:58:02 +0300 Subject: [PATCH 11/42] iteration --- etcd/src/etcd/client_impl.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index bad3be285cb4..24cfe9e27d6f 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -35,7 +35,7 @@ class ClientImpl : public Client { const std::string& data ); - void WatchKeyChanges(const std::string key, concurrent::SpscQueue::Producer producer); + void WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer); using WatchQueuePtr = std::shared_ptr>; clients::http::Client& http_client_; From 0c95754aa57ce4e98016bd07bcd6f13ce28b5b88 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sun, 16 Mar 2025 23:01:40 +0300 Subject: [PATCH 12/42] iteration --- etcd/src/etcd/client_impl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index e44eb0c17c79..09f4af79525b 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -49,7 +49,7 @@ std::string BuildRangeUrl(const std::string& service_url) { return fmt::format(" std::string BuildRangeData(const std::string& key) { const auto etcd_key = kKeyPrefix + key; return formats::json::ToString(formats::json::MakeObject( - "key", crypto::base64::Base64Encode(etcd_key), "range_end", crypto::base64::Base64Encode(kLastPrefix) + "key", crypto::base64::Base64Encode(etcd_key), "range_end", crypto::base64::Base64Encode(kLastPossibleKeyPrefix) )); } From 627a9f9a302bee93c81f8d3d52aa5c7fae5c0e2e Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Wed, 16 Apr 2025 23:36:53 +0300 Subject: [PATCH 13/42] iteration --- core/src/clients/http/request_state.cpp | 8 ++ core/src/clients/http/request_state.hpp | 1 + etcd/include/userver/etcd/client.hpp | 22 +++- etcd/include/userver/etcd/component.hpp | 12 ++ .../userver/etcd/dynamic_config.hpp} | 0 etcd/include/userver/etcd/exceptions.hpp | 6 + etcd/include/userver/etcd/settings.hpp | 1 + etcd/include/userver/etcd/watch_listener.hpp | 8 +- etcd/src/etcd/client_impl.cpp | 41 ++++--- etcd/src/etcd/client_impl.hpp | 8 +- etcd/src/etcd/component.cpp | 2 +- etcd/src/etcd/etcd_client_test.cpp | 112 ++++++++++++++++++ etcd/src/etcd/settings.cpp | 10 +- etcd/src/etcd/watch_listener.cpp | 16 +-- samples/etcd_service/CMakeLists.txt | 9 ++ samples/etcd_service/service.cpp | 0 samples/etcd_service/static_config.yaml | 0 17 files changed, 218 insertions(+), 38 deletions(-) rename etcd/{tests/etcd_client_test.cpp => include/userver/etcd/dynamic_config.hpp} (100%) create mode 100644 etcd/src/etcd/etcd_client_test.cpp create mode 100644 samples/etcd_service/CMakeLists.txt create mode 100644 samples/etcd_service/service.cpp create mode 100644 samples/etcd_service/static_config.yaml diff --git a/core/src/clients/http/request_state.cpp b/core/src/clients/http/request_state.cpp index a2b806d26480..41fe80aea8aa 100644 --- a/core/src/clients/http/request_state.cpp +++ b/core/src/clients/http/request_state.cpp @@ -746,6 +746,14 @@ void RequestState::SetEasyTimeout(std::chrono::milliseconds timeout) { easy().set_connect_timeout_ms(timeout.count()); } +void RequestState::SetEasyConnectTimeout(std::chrono::milliseconds timeout) { + UASSERT_MSG( + timeout >= std::chrono::seconds{0}, fmt::format("timeout_ms < 0 ({})), uninitialized variable?", timeout) + ); + easy().set_timeout_ms(timeout.count()); + easy().set_connect_timeout_ms(timeout.count()); +} + engine::Deadline RequestState::GetDeadline() const noexcept { return deadline_; } bool RequestState::IsDeadlineExpired() const noexcept { return deadline_expired_; } diff --git a/core/src/clients/http/request_state.hpp b/core/src/clients/http/request_state.hpp index c0312b473738..150cdc0ee373 100644 --- a/core/src/clients/http/request_state.hpp +++ b/core/src/clients/http/request_state.hpp @@ -130,6 +130,7 @@ class RequestState : public std::enable_shared_from_this { void SetLoggedUrl(std::string url); void SetEasyTimeout(std::chrono::milliseconds timeout); + void SetEasyConnectTimeout(std::chrono::milliseconds timeout); void SetTracingManager(const tracing::TracingManagerBase&); diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 3466002494b3..4e44b4fad919 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -1,5 +1,8 @@ #pragma once +/// @file userver/etcd/client.hpp +/// @brief @copybrief etcd::Client + #include #include #include @@ -17,18 +20,35 @@ USERVER_NAMESPACE_BEGIN namespace etcd { +/// @brief Etcd client that uses http client inside class Client { public: virtual ~Client() = default; + /// @brief Puts a key value pair into etcd cluster. + /// The pair should be retrieve only with current client, + /// because Put can transform the key. + /// virtual void Put(const std::string& key, const std::string& value) = 0; + /// @brief Gets a value from etcd cluster by a key. + /// If there is no value with such key, returns std::nullopt + /// [[nodiscard]] virtual std::optional Get(const std::string& key) = 0; - [[nodiscard]] virtual std::vector Range(const std::string& key) = 0; + /// @brief Retrieves values from the etcd cluster, + /// the keys of which match the passed prefix. + /// If there is no value with such key, returns std::nullopt + /// + [[nodiscard]] virtual std::vector Range(const std::string& key_prefix) = 0; + /// @brief Delete a key value pair with the passed key + /// from the etcd cluster + /// virtual void Delete(const std::string& key) = 0; + /// @brief Start task that produces events when key value pair changes + /// virtual WatchListener StartWatch(const std::string& key) = 0; }; diff --git a/etcd/include/userver/etcd/component.hpp b/etcd/include/userver/etcd/component.hpp index 48111eb626a3..0aebd5175a21 100644 --- a/etcd/include/userver/etcd/component.hpp +++ b/etcd/include/userver/etcd/component.hpp @@ -1,5 +1,8 @@ #pragma once +/// @file userver/etcd/component.hpp +/// @brief @copybrief etcd::Component + #include #include #include @@ -9,6 +12,15 @@ USERVER_NAMESPACE_BEGIN namespace etcd { +// clang-format off +/// @ingroup userver_components +/// +/// @brief Etcd client component +/// +/// Provides access to a etcd cluster. +/// +// clang-format on + class Component final : public components::ComponentBase { public: static constexpr std::string_view kName = "etcd-сlient"; diff --git a/etcd/tests/etcd_client_test.cpp b/etcd/include/userver/etcd/dynamic_config.hpp similarity index 100% rename from etcd/tests/etcd_client_test.cpp rename to etcd/include/userver/etcd/dynamic_config.hpp diff --git a/etcd/include/userver/etcd/exceptions.hpp b/etcd/include/userver/etcd/exceptions.hpp index a6215cb5fbf7..734f3b70abd9 100644 --- a/etcd/include/userver/etcd/exceptions.hpp +++ b/etcd/include/userver/etcd/exceptions.hpp @@ -14,6 +14,12 @@ class EtcdError : public std::runtime_error { using std::runtime_error::runtime_error; }; +/// @brief Etcd request error +class EtcdRequestError : public EtcdError { +public: + using EtcdError::EtcdError; +}; + } // namespace etcd USERVER_NAMESPACE_END diff --git a/etcd/include/userver/etcd/settings.hpp b/etcd/include/userver/etcd/settings.hpp index 4fbab86254fc..1b5637698a4d 100644 --- a/etcd/include/userver/etcd/settings.hpp +++ b/etcd/include/userver/etcd/settings.hpp @@ -14,6 +14,7 @@ struct ClientSettings final { const std::vector endpoints; const std::uint32_t attempts; const std::chrono::microseconds request_timeout_ms; + const std::chrono::microseconds watch_timeout_ms; }; } // namespace etcd diff --git a/etcd/include/userver/etcd/watch_listener.hpp b/etcd/include/userver/etcd/watch_listener.hpp index b46a454ceb5a..29e113562c48 100644 --- a/etcd/include/userver/etcd/watch_listener.hpp +++ b/etcd/include/userver/etcd/watch_listener.hpp @@ -9,23 +9,23 @@ USERVER_NAMESPACE_BEGIN namespace etcd { -struct KeyValueEvent final { +struct KeyValueState final { std::string key; std::string value; std::int32_t version; }; struct WatchListener final { - concurrent::SpscQueue::Consumer consumer; + concurrent::SpscQueue::Consumer consumer; - KeyValueEvent GetEvent(); + KeyValueState GetEvent(); }; } // namespace etcd namespace formats::parse { -etcd::KeyValueEvent Parse(const formats::json::Value& value, To); +etcd::KeyValueState Parse(const formats::json::Value& value, To); } diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 09f4af79525b..6cf673de98a2 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -87,7 +87,19 @@ ClientImpl::ClientImpl(clients::http::Client& http_client, ClientSettings settin : http_client_(http_client), settings_(settings) {} void ClientImpl::Put(const std::string& key, const std::string& value) { - auto response = PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); + try { + PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); + } catch (const clients::http::HttpClientException& exception) { + throw EtcdRequestError(fmt::format("Request to etcd was unsuccessful: {}", exception.what())); + } +} + +void ClientImpl::Delete(const std::string& key) { + try { + PerformEtcdRequest(BuildDeleteUrl, BuildDeleteData(key)); + } catch (const clients::http::HttpClientException& exception) { + throw EtcdRequestError(fmt::format("Request to etcd was unsuccessful: {}", exception.what())); + } } std::optional ClientImpl::Get(const std::string& key) { @@ -107,8 +119,8 @@ std::optional ClientImpl::Get(const std::string& key) { return std::nullopt; } -std::vector ClientImpl::Range(const std::string& key) { - auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); +std::vector ClientImpl::Range(const std::string& key_prefix) { + auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix)); const auto json_body = formats::json::FromString(response->body()); if (!json_body.HasMember("kvs")) { @@ -123,21 +135,17 @@ std::vector ClientImpl::Range(const std::string& key) { return values; } -void ClientImpl::Delete(const std::string& key) { - auto response = PerformEtcdRequest(BuildDeleteUrl, BuildDeleteData(key)); -} - WatchListener ClientImpl::StartWatch(const std::string& key) { - auto queue = concurrent::SpscQueue::Create(); + auto queue = concurrent::SpscQueue::Create(); auto watch_queues_ptr = watch_queues_.Lock(); watch_queues_ptr->push_back(queue); - utils::Async("watch task", [&key, producer = queue->GetProducer(), this] mutable { + utils::Async("watch task", [&key, producer = queue->GetProducer(), this]() mutable { this->WatchKeyChanges(key, std::move(producer)); }).Detach(); - return WatchListener{.consumer = queue->GetConsumer()}; + return WatchListener{queue->GetConsumer()}; } clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( @@ -153,7 +161,7 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( maybe_streamed_response = http_client_.CreateRequest() .post(url_builder(endpoint), data) .retry(settings_.attempts) - .timeout(1'000'000'000) + .timeout(settings_.watch_timeout_ms.count()) .async_perform_stream_body(queue); auto& streamed_response = maybe_streamed_response.value(); if (!ShouldRetry(streamed_response.StatusCode())) { @@ -166,7 +174,7 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( "Failed to get Ok response from etcd with status code: " + maybe_streamed_response.value().StatusCode() ); } else { - throw EtcdError(fmt::format("Failed to get streamed response, number of etcd endpoints: {}", endpoints.size())); + throw EtcdError("Failed to get streamed response, number of etcd endpoints: " + endpoints.size()); } } @@ -193,25 +201,26 @@ std::shared_ptr ClientImpl::PerformEtcdRequest( throw EtcdError("Failed to get Ok response from etcd with error: " + response_ptr->body()); } -void ClientImpl::WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer) { +void ClientImpl::WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer) { auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); std::string body_part; while (stream_response.ReadChunk( - body_part, engine::Deadline::FromTimePoint(std::chrono::system_clock::time_point::max()) + body_part, engine::Deadline() )) { const auto watch_response = formats::json::FromString(body_part); - LOG_DEBUG() << watch_response; + LOG_DEBUG() << "Got folowing chunk from etcd watch handler: " << watch_response; if (!watch_response["result"].HasMember("events")) { LOG_DEBUG() << "No events in watch part response, skipping"; continue; } for (const auto& event : watch_response["result"]["events"]) { if (!event.HasMember("kv")) { + LOG_DEBUG() << "Event is not key value change, skipping"; continue; } LOG_DEBUG() << "Got event with kv: " << event["kv"]; - if (!producer.Push(event["kv"].As())) { + if (!producer.Push(event["kv"].As())) { LOG_ERROR() << "Could not push to queue, aborting task"; return; }; diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index 24cfe9e27d6f..bccd81d7ca52 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -20,14 +20,14 @@ class ClientImpl : public Client { [[nodiscard]] std::optional Get(const std::string& key) override; - [[nodiscard]] std::vector Range(const std::string& key) override; + [[nodiscard]] std::vector Range(const std::string& key_prefix) override; void Delete(const std::string& key) override; WatchListener StartWatch(const std::string& key) override; private: - [[nodiscard]] std::shared_ptr + std::shared_ptr PerformEtcdRequest(const std::function& url_builder, const std::string& data); [[nodiscard]] clients::http::StreamedResponse PerformStreamedEtcdRequest( @@ -35,9 +35,9 @@ class ClientImpl : public Client { const std::string& data ); - void WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer); + void WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer); - using WatchQueuePtr = std::shared_ptr>; + using WatchQueuePtr = std::shared_ptr>; clients::http::Client& http_client_; concurrent::Variable> watch_queues_; const ClientSettings settings_; diff --git a/etcd/src/etcd/component.cpp b/etcd/src/etcd/component.cpp index 3f0f4d871601..1156a99b4a54 100644 --- a/etcd/src/etcd/component.cpp +++ b/etcd/src/etcd/component.cpp @@ -27,7 +27,7 @@ additionalProperties: false description: Etcd endpoints items: type: string - description: host + description: host, e.g. http://localhost:2379 attempts: type: integer description: > diff --git a/etcd/src/etcd/etcd_client_test.cpp b/etcd/src/etcd/etcd_client_test.cpp new file mode 100644 index 000000000000..04e1bc01656f --- /dev/null +++ b/etcd/src/etcd/etcd_client_test.cpp @@ -0,0 +1,112 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace { + +utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServerMock::HttpRequest& request) { + static std::map storage; + + EXPECT_EQ(request.method, clients::http::HttpMethod::kPost); + const auto request_body = formats::json::FromString(request.body); + formats::json::ValueBuilder response_body_value_builder; + + if (request.path == "/v3/kv/put") { + const auto key = crypto::base64::Base64Decode(request_body["key"].As()); + const auto value = crypto::base64::Base64Decode(request_body["value"].As()); + storage[key] = value; + } else if (request.path == "/v3/kv/range") { + const auto key = crypto::base64::Base64Decode(request_body["key"].As()); + const auto range_end = crypto::base64::Base64Decode(request_body["range_end"].As()); + auto first_key = storage.lower_bound(key); + const auto last_key = storage.upper_bound(range_end); + + response_body_value_builder["kvs"] = formats::json::MakeArray(); + while (first_key != last_key) { + response_body_value_builder["kvs"].PushBack( + formats::json::MakeObject( + "key", + crypto::base64::Base64Encode(first_key->first), + "value", + crypto::base64::Base64Encode(first_key->second) + ) + ); + ++first_key; + } + } else if (request.path == "/v3/kv/deleterange") { + const auto key = crypto::base64::Base64Decode(request_body["key"].As()); + storage.erase(key); + } + + return utest::HttpServerMock::HttpResponse{ + 200, + clients::http::Headers{}, + formats::json::ToString(response_body_value_builder.ExtractValue()) + }; +} + +} // namespace + +UTEST(Etcd, TestKeyValueStorage) { + utest::HttpServerMock mock_server(&EtcdRequestProcessor); + auto http_client_ptr = utest::CreateHttpClient(); + auto etcd_client_ptr = std::make_shared( + *http_client_ptr, + etcd::ClientSettings{ + {mock_server.GetBaseUrl()}, + 2, + std::chrono::milliseconds{500}, + } + ); + + const auto empty_value = etcd_client_ptr->Get("key_with_empty_value"); + EXPECT_EQ(empty_value, std::nullopt); + + etcd_client_ptr->Put("some_key", "some_value"); + const auto some_value = etcd_client_ptr->Get("some_key"); + EXPECT_EQ(some_value, "some_value"); + + etcd_client_ptr->Put("some_key", "some_new_value"); + const auto some_new_value = etcd_client_ptr->Get("some_key"); + EXPECT_EQ(some_new_value, "some_new_value"); + + etcd_client_ptr->Delete("some_key"); + const auto deleted_value = etcd_client_ptr->Get("some_key"); + EXPECT_EQ(deleted_value, std::nullopt); +} + +UTEST(Etcd, TestRange) { + utest::HttpServerMock mock_server(&EtcdRequestProcessor); + auto http_client_ptr = utest::CreateHttpClient(); + auto etcd_client_ptr = std::make_shared( + *http_client_ptr, + etcd::ClientSettings{ + {mock_server.GetBaseUrl()}, + 2, + std::chrono::milliseconds{500}, + } + ); + + EXPECT_EQ(etcd_client_ptr->Range("some_key"), (std::vector{})); + + etcd_client_ptr->Put("some_key_1", "some_value_1"); + etcd_client_ptr->Put("some_key_2", "some_value_2"); + etcd_client_ptr->Put("some_key_3", "some_value_3"); + const auto range_result = etcd_client_ptr->Range("some_key"); + EXPECT_EQ(range_result, (std::vector{"some_value_1", "some_value_2", "some_value_3"})); + etcd_client_ptr->Delete("some_key_1"); + etcd_client_ptr->Delete("some_key_2"); + etcd_client_ptr->Delete("some_key_3"); + EXPECT_EQ(etcd_client_ptr->Range("some_key"), (std::vector{})); +} + +USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/settings.cpp b/etcd/src/etcd/settings.cpp index d00e27446821..91e0baafb19f 100644 --- a/etcd/src/etcd/settings.cpp +++ b/etcd/src/etcd/settings.cpp @@ -11,6 +11,7 @@ namespace { constexpr std::uint32_t kDefaultAttempts{3}; constexpr std::chrono::milliseconds kDefaultRequestTimeout{1'000}; +constexpr std::chrono::milliseconds kDefaultWatchTimeout{1'000'000}; } // namespace @@ -18,11 +19,12 @@ constexpr std::chrono::milliseconds kDefaultRequestTimeout{1'000}; namespace formats::parse { -etcd::ClientSettings Parse(const yaml_config::YamlConfig& cofig, To) { +etcd::ClientSettings Parse(const yaml_config::YamlConfig& config, To) { return etcd::ClientSettings{ - .endpoints = cofig["endpoints"].As>(), - .attempts = cofig["attempts"].As(etcd::kDefaultAttempts), - .request_timeout_ms = cofig["request_timeout_ms"].As(etcd::kDefaultRequestTimeout), + .endpoints = config["endpoints"].As>(), + .attempts = config["attempts"].As(etcd::kDefaultAttempts), + .request_timeout_ms = config["request_timeout_ms"].As(etcd::kDefaultRequestTimeout), + .watch_timeout_ms = config["watch_timeout_ms"].As(etcd::kDefaultWatchTimeout), }; } diff --git a/etcd/src/etcd/watch_listener.cpp b/etcd/src/etcd/watch_listener.cpp index db1c5ca762e6..23950592a230 100644 --- a/etcd/src/etcd/watch_listener.cpp +++ b/etcd/src/etcd/watch_listener.cpp @@ -7,10 +7,10 @@ USERVER_NAMESPACE_BEGIN namespace etcd { -KeyValueEvent WatchListener::GetEvent() { - KeyValueEvent event; +KeyValueState WatchListener::GetEvent() { + KeyValueState event; if (!consumer.Pop(event)) { - throw EtcdError("Consumer pop failed"); + throw EtcdError("Consumer pop failed while trying to get etcd key-value event"); } return event; } @@ -19,11 +19,11 @@ KeyValueEvent WatchListener::GetEvent() { namespace formats::parse { -etcd::KeyValueEvent Parse(const formats::json::Value& value, To) { - return etcd::KeyValueEvent{ - .key = crypto::base64::Base64Decode(value["key"].As()), - .value = crypto::base64::Base64Decode(value["value"].As()), - .version = std::stoi(value["version"].As()), +etcd::KeyValueState Parse(const formats::json::Value& value, To) { + return etcd::KeyValueState{ + /* .key = */ crypto::base64::Base64Decode(value["key"].As()), + /* .value = */ crypto::base64::Base64Decode(value["value"].As()), + /* .version = */ std::stoi(value["version"].As()), }; } diff --git a/samples/etcd_service/CMakeLists.txt b/samples/etcd_service/CMakeLists.txt new file mode 100644 index 000000000000..9a890eabc107 --- /dev/null +++ b/samples/etcd_service/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.14) +project(userver-samples-multipart_service CXX) + +find_package(userver COMPONENTS core etcd REQUIRED) + +add_executable(${PROJECT_NAME} service.cpp) +target_link_libraries(${PROJECT_NAME} userver::core userver::etcd) + +userver_testsuite_add_simple() diff --git a/samples/etcd_service/service.cpp b/samples/etcd_service/service.cpp new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/samples/etcd_service/static_config.yaml b/samples/etcd_service/static_config.yaml new file mode 100644 index 000000000000..e69de29bb2d1 From 2c59b2f7175d9259e2e4ddcb661c0882a337af1a Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 2 May 2025 16:48:44 +0300 Subject: [PATCH 14/42] iteration --- etcd/include/userver/etcd/client.hpp | 2 +- etcd/include/userver/etcd/dynamic_config.hpp | 0 etcd/include/userver/etcd/exceptions.hpp | 5 +- etcd/include/userver/etcd/settings.hpp | 8 ++++ etcd/include/userver/etcd/watch_listener.hpp | 5 ++ etcd/src/etcd/client_impl.cpp | 46 +++++++++---------- etcd/src/etcd/component.cpp | 13 ++++-- samples/etcd_service/{ => src}/CMakeLists.txt | 2 +- samples/etcd_service/{ => src}/service.cpp | 0 .../etcd_service/{ => src}/static_config.yaml | 0 10 files changed, 51 insertions(+), 30 deletions(-) delete mode 100644 etcd/include/userver/etcd/dynamic_config.hpp rename samples/etcd_service/{ => src}/CMakeLists.txt (83%) rename samples/etcd_service/{ => src}/service.cpp (100%) rename samples/etcd_service/{ => src}/static_config.yaml (100%) diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 4e44b4fad919..4092e95627da 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -20,7 +20,7 @@ USERVER_NAMESPACE_BEGIN namespace etcd { -/// @brief Etcd client that uses http client inside +/// @brief Etcd client implemented using http client class Client { public: virtual ~Client() = default; diff --git a/etcd/include/userver/etcd/dynamic_config.hpp b/etcd/include/userver/etcd/dynamic_config.hpp deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/etcd/include/userver/etcd/exceptions.hpp b/etcd/include/userver/etcd/exceptions.hpp index 734f3b70abd9..2d4641dd5aac 100644 --- a/etcd/include/userver/etcd/exceptions.hpp +++ b/etcd/include/userver/etcd/exceptions.hpp @@ -1,5 +1,8 @@ #pragma once +/// @file userver/etcd/exceptions.hpp +/// @brief Exceptions thrown by etcd client + #include #include @@ -14,7 +17,7 @@ class EtcdError : public std::runtime_error { using std::runtime_error::runtime_error; }; -/// @brief Etcd request error +/// @brief Error during a request to etcd class EtcdRequestError : public EtcdError { public: using EtcdError::EtcdError; diff --git a/etcd/include/userver/etcd/settings.hpp b/etcd/include/userver/etcd/settings.hpp index 1b5637698a4d..311b70d940e3 100644 --- a/etcd/include/userver/etcd/settings.hpp +++ b/etcd/include/userver/etcd/settings.hpp @@ -1,5 +1,8 @@ #pragma once +/// @file userver/etcd/settings.hpp +/// @brief etcd client settings + #include #include #include @@ -10,10 +13,15 @@ USERVER_NAMESPACE_BEGIN namespace etcd { +/// @brief Etcd client settigs struct struct ClientSettings final { + // Etcd endpoints to which client make HTTP requests const std::vector endpoints; + // Number of attempts to each endpoint, on failed attempts client randomly moves to another endpoint const std::uint32_t attempts; + // Timeout for all HTTP requests to etcd except watch request const std::chrono::microseconds request_timeout_ms; + // Timeout for watch HTTP request. It's a stremed request, so it is used also as a connection timeout, so it should not be too short const std::chrono::microseconds watch_timeout_ms; }; diff --git a/etcd/include/userver/etcd/watch_listener.hpp b/etcd/include/userver/etcd/watch_listener.hpp index 29e113562c48..722d2a44c5f5 100644 --- a/etcd/include/userver/etcd/watch_listener.hpp +++ b/etcd/include/userver/etcd/watch_listener.hpp @@ -1,5 +1,8 @@ #pragma once +/// @file userver/etcd/watch_listener.hpp +/// @brief Queue with value change events in etcd + #include #include @@ -9,12 +12,14 @@ USERVER_NAMESPACE_BEGIN namespace etcd { +/// @brief Struct with key value pair from etcd. It represents current status of key value pair. struct KeyValueState final { std::string key; std::string value; std::int32_t version; }; +/// @brief Struct that return value change events in etcd struct WatchListener final { concurrent::SpscQueue::Consumer consumer; diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 6cf673de98a2..50519ea95360 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -120,6 +120,7 @@ std::optional ClientImpl::Get(const std::string& key) { } std::vector ClientImpl::Range(const std::string& key_prefix) { + auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix)); const auto json_body = formats::json::FromString(response->body()); @@ -148,6 +149,28 @@ WatchListener ClientImpl::StartWatch(const std::string& key) { return WatchListener{queue->GetConsumer()}; } +std::shared_ptr ClientImpl::PerformEtcdRequest( + const std::function& url_builder, + const std::string& data +) { + auto endpoints = settings_.endpoints; + utils::Shuffle(endpoints); + + std::shared_ptr response_ptr; + for (const auto& endpoint : endpoints) { + response_ptr = http_client_.CreateRequest() + .post(url_builder(endpoint), data) + .retry(settings_.attempts) + .timeout(settings_.request_timeout_ms.count()) + .perform(); + if (!ShouldRetry(response_ptr->status_code())) { + CheckResponseStatusCode(response_ptr->status_code()); + return response_ptr; + } + } + throw EtcdError("Failed to get Ok response from etcd with error: " + response_ptr->body()); +} + clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( const std::function& url_builder, const std::string& data @@ -178,29 +201,6 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( } } -std::shared_ptr ClientImpl::PerformEtcdRequest( - const std::function& url_builder, - const std::string& data -) { - auto endpoints = settings_.endpoints; - utils::Shuffle(endpoints); - - std::shared_ptr response_ptr; - for (const auto& endpoint : endpoints) { - response_ptr = http_client_.CreateRequest() - .post(url_builder(endpoint), data) - .retry(settings_.attempts) - .timeout(settings_.request_timeout_ms.count()) - .perform(); - if (!ShouldRetry(response_ptr->status_code())) { - response_ptr->raise_for_status(); - return response_ptr; - } - } - - throw EtcdError("Failed to get Ok response from etcd with error: " + response_ptr->body()); -} - void ClientImpl::WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer) { auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); diff --git a/etcd/src/etcd/component.cpp b/etcd/src/etcd/component.cpp index 1156a99b4a54..dae36c54c6cb 100644 --- a/etcd/src/etcd/component.cpp +++ b/etcd/src/etcd/component.cpp @@ -24,18 +24,23 @@ additionalProperties: false properties: endpoints: type: array - description: Etcd endpoints + description: Etcd endpoints to which client make HTTP requests items: type: string - description: host, e.g. http://localhost:2379 + description: Etcd endpoint, e.g. http://localhost:2379 attempts: type: integer description: > - Number of attempts per one endpoints, total number of attempts is number of endpoints times attempts + Number of attempts to each endpoint, on failed attempts client randomly moves to another endpoint minimum: 1 request_timeout_ms: type: integer - description: Number of miliseconds between request attempts + description: Timeout for all HTTP requests to etcd except watch request + minimum: 1 + watch_timeout_ms: + type: integer + description: > + Timeout for watch HTTP request. It's a stremed request, so it is used also as a connection timeout, so it should not be too short minimum: 1 )"); } diff --git a/samples/etcd_service/CMakeLists.txt b/samples/etcd_service/src/CMakeLists.txt similarity index 83% rename from samples/etcd_service/CMakeLists.txt rename to samples/etcd_service/src/CMakeLists.txt index 9a890eabc107..364978e93f78 100644 --- a/samples/etcd_service/CMakeLists.txt +++ b/samples/etcd_service/src/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.14) -project(userver-samples-multipart_service CXX) +project(userver-samples-etcd_service CXX) find_package(userver COMPONENTS core etcd REQUIRED) diff --git a/samples/etcd_service/service.cpp b/samples/etcd_service/src/service.cpp similarity index 100% rename from samples/etcd_service/service.cpp rename to samples/etcd_service/src/service.cpp diff --git a/samples/etcd_service/static_config.yaml b/samples/etcd_service/src/static_config.yaml similarity index 100% rename from samples/etcd_service/static_config.yaml rename to samples/etcd_service/src/static_config.yaml From 2e5d7f55cbacf4654705e7eb8b7061d35acb882f Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 2 May 2025 16:52:06 +0300 Subject: [PATCH 15/42] remove connection timeout change --- core/src/clients/http/request_state.cpp | 8 -------- core/src/clients/http/request_state.hpp | 1 - 2 files changed, 9 deletions(-) diff --git a/core/src/clients/http/request_state.cpp b/core/src/clients/http/request_state.cpp index 41fe80aea8aa..a2b806d26480 100644 --- a/core/src/clients/http/request_state.cpp +++ b/core/src/clients/http/request_state.cpp @@ -746,14 +746,6 @@ void RequestState::SetEasyTimeout(std::chrono::milliseconds timeout) { easy().set_connect_timeout_ms(timeout.count()); } -void RequestState::SetEasyConnectTimeout(std::chrono::milliseconds timeout) { - UASSERT_MSG( - timeout >= std::chrono::seconds{0}, fmt::format("timeout_ms < 0 ({})), uninitialized variable?", timeout) - ); - easy().set_timeout_ms(timeout.count()); - easy().set_connect_timeout_ms(timeout.count()); -} - engine::Deadline RequestState::GetDeadline() const noexcept { return deadline_; } bool RequestState::IsDeadlineExpired() const noexcept { return deadline_expired_; } diff --git a/core/src/clients/http/request_state.hpp b/core/src/clients/http/request_state.hpp index 150cdc0ee373..c0312b473738 100644 --- a/core/src/clients/http/request_state.hpp +++ b/core/src/clients/http/request_state.hpp @@ -130,7 +130,6 @@ class RequestState : public std::enable_shared_from_this { void SetLoggedUrl(std::string url); void SetEasyTimeout(std::chrono::milliseconds timeout); - void SetEasyConnectTimeout(std::chrono::milliseconds timeout); void SetTracingManager(const tracing::TracingManagerBase&); From 64453634157d39f425cb35579193d6348cbb69e4 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 2 May 2025 18:47:08 +0300 Subject: [PATCH 16/42] remove allowed url prefix --- testsuite/pytest_plugins/pytest_userver/plugins/config.py | 2 +- testsuite/pytest_plugins/pytest_userver/plugins/etcd.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 testsuite/pytest_plugins/pytest_userver/plugins/etcd.py diff --git a/testsuite/pytest_plugins/pytest_userver/plugins/config.py b/testsuite/pytest_plugins/pytest_userver/plugins/config.py index f93e0146267d..4d7f55ae09fb 100644 --- a/testsuite/pytest_plugins/pytest_userver/plugins/config.py +++ b/testsuite/pytest_plugins/pytest_userver/plugins/config.py @@ -515,7 +515,7 @@ def allowed_url_prefixes_extra() -> List[str]: @ingroup userver_testsuite_fixtures """ - return ["http://localhost:2379"] + return [] @pytest.fixture(scope='session') diff --git a/testsuite/pytest_plugins/pytest_userver/plugins/etcd.py b/testsuite/pytest_plugins/pytest_userver/plugins/etcd.py new file mode 100644 index 000000000000..139597f9cb07 --- /dev/null +++ b/testsuite/pytest_plugins/pytest_userver/plugins/etcd.py @@ -0,0 +1,2 @@ + + From 733ad26f1d4ac001102941e633feae125b5c3e38 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 2 May 2025 19:35:33 +0300 Subject: [PATCH 17/42] fix config plugin --- testsuite/pytest_plugins/pytest_userver/plugins/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testsuite/pytest_plugins/pytest_userver/plugins/config.py b/testsuite/pytest_plugins/pytest_userver/plugins/config.py index 4d7f55ae09fb..0d5b1e168fd5 100644 --- a/testsuite/pytest_plugins/pytest_userver/plugins/config.py +++ b/testsuite/pytest_plugins/pytest_userver/plugins/config.py @@ -539,7 +539,7 @@ def patch_config(config, config_vars): return http_client = components['http-client'] or {} http_client['testsuite-enabled'] = True - # http_client['testsuite-timeout'] = '30s' + http_client['testsuite-timeout'] = '10s' allowed_urls = [mockserver_info.base_url] if mockserver_ssl_info: From 063030b2afad9901fca86c5f67291e85d8a053bf Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sat, 3 May 2025 18:54:30 +0300 Subject: [PATCH 18/42] format --- etcd/include/userver/etcd/client.hpp | 4 ++-- etcd/include/userver/etcd/settings.hpp | 5 ++-- etcd/src/etcd/client_impl.cpp | 5 +--- etcd/src/etcd/etcd_client_test.cpp | 33 +++++++++++--------------- 4 files changed, 20 insertions(+), 27 deletions(-) diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 4092e95627da..5d3bc4a0eed8 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -32,13 +32,13 @@ class Client { virtual void Put(const std::string& key, const std::string& value) = 0; /// @brief Gets a value from etcd cluster by a key. - /// If there is no value with such key, returns std::nullopt + /// If there is no value with such key, returns std::nullopt /// [[nodiscard]] virtual std::optional Get(const std::string& key) = 0; /// @brief Retrieves values from the etcd cluster, /// the keys of which match the passed prefix. - /// If there is no value with such key, returns std::nullopt + /// If there is no value with such key, returns std::nullopt /// [[nodiscard]] virtual std::vector Range(const std::string& key_prefix) = 0; diff --git a/etcd/include/userver/etcd/settings.hpp b/etcd/include/userver/etcd/settings.hpp index 311b70d940e3..322ebc7105b4 100644 --- a/etcd/include/userver/etcd/settings.hpp +++ b/etcd/include/userver/etcd/settings.hpp @@ -15,13 +15,14 @@ namespace etcd { /// @brief Etcd client settigs struct struct ClientSettings final { - // Etcd endpoints to which client make HTTP requests + // Etcd endpoints to which client make HTTP requests const std::vector endpoints; // Number of attempts to each endpoint, on failed attempts client randomly moves to another endpoint const std::uint32_t attempts; // Timeout for all HTTP requests to etcd except watch request const std::chrono::microseconds request_timeout_ms; - // Timeout for watch HTTP request. It's a stremed request, so it is used also as a connection timeout, so it should not be too short + // Timeout for watch HTTP request. It's a stremed request, so it is used also as a connection timeout, so it should + // not be too short const std::chrono::microseconds watch_timeout_ms; }; diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 50519ea95360..39e3a6ff5c26 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -120,7 +120,6 @@ std::optional ClientImpl::Get(const std::string& key) { } std::vector ClientImpl::Range(const std::string& key_prefix) { - auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix)); const auto json_body = formats::json::FromString(response->body()); @@ -205,9 +204,7 @@ void ClientImpl::WatchKeyChanges(const std::string& key, concurrent::SpscQueue +#include #include #include -#include -#include -#include -#include #include #include -#include +#include +#include +#include +#include USERVER_NAMESPACE_BEGIN @@ -15,7 +15,7 @@ namespace { utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServerMock::HttpRequest& request) { static std::map storage; - + EXPECT_EQ(request.method, clients::http::HttpMethod::kPost); const auto request_body = formats::json::FromString(request.body); formats::json::ValueBuilder response_body_value_builder; @@ -32,26 +32,21 @@ utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServer response_body_value_builder["kvs"] = formats::json::MakeArray(); while (first_key != last_key) { - response_body_value_builder["kvs"].PushBack( - formats::json::MakeObject( - "key", - crypto::base64::Base64Encode(first_key->first), - "value", - crypto::base64::Base64Encode(first_key->second) - ) - ); + response_body_value_builder["kvs"].PushBack(formats::json::MakeObject( + "key", + crypto::base64::Base64Encode(first_key->first), + "value", + crypto::base64::Base64Encode(first_key->second) + )); ++first_key; } } else if (request.path == "/v3/kv/deleterange") { const auto key = crypto::base64::Base64Decode(request_body["key"].As()); storage.erase(key); } - + return utest::HttpServerMock::HttpResponse{ - 200, - clients::http::Headers{}, - formats::json::ToString(response_body_value_builder.ExtractValue()) - }; + 200, clients::http::Headers{}, formats::json::ToString(response_body_value_builder.ExtractValue())}; } } // namespace From 333cbd45866a794c8acb05fb4e9201c25e53baa9 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sat, 3 May 2025 19:33:48 +0300 Subject: [PATCH 19/42] fix doc --- etcd/include/userver/etcd/component.hpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/etcd/include/userver/etcd/component.hpp b/etcd/include/userver/etcd/component.hpp index 0aebd5175a21..a6dd4003ad02 100644 --- a/etcd/include/userver/etcd/component.hpp +++ b/etcd/include/userver/etcd/component.hpp @@ -19,6 +19,14 @@ namespace etcd { /// /// Provides access to a etcd cluster. /// +/// ## Static options: +/// Name | Description | Default value +/// ---------------------------------- | ---------------------------------------------------------- | --------------- +/// endpoints | Etcd endpoints to which client make HTTP requests | - +/// attempts | Number of attempts to each endpoint, on failed attempts client randomly moves to another endpoint | 3 +/// request_timeout_ms | Timeout for all HTTP requests to etcd except watch request | 1000 +/// watch_timeout_ms | Timeout for watch HTTP request. It's a stremed request, so it is used also as a connection timeout, so it should not be too short | 1000000 +/// // clang-format on class Component final : public components::ComponentBase { From 5377ffbd1973a0094d24ac8b43edea7691fc0703 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sun, 4 May 2025 20:08:15 +0300 Subject: [PATCH 20/42] add tests --- etcd/CMakeLists.txt | 4 + etcd/functional_tests/CMakeLists.txt | 6 + etcd/functional_tests/client/CMakeLists.txt | 6 + etcd/functional_tests/client/etcd_service.cpp | 105 ++++++++++++++++++ .../client/static_config.yaml | 52 +++++++++ .../functional_tests/client/tests/conftest.py | 1 + .../client/tests/test_etcd_client.py | 5 + samples/etcd_service/src/CMakeLists.txt | 9 -- samples/etcd_service/src/service.cpp | 0 samples/etcd_service/src/static_config.yaml | 0 10 files changed, 179 insertions(+), 9 deletions(-) create mode 100644 etcd/functional_tests/CMakeLists.txt create mode 100644 etcd/functional_tests/client/CMakeLists.txt create mode 100644 etcd/functional_tests/client/etcd_service.cpp create mode 100644 etcd/functional_tests/client/static_config.yaml create mode 100644 etcd/functional_tests/client/tests/conftest.py create mode 100644 etcd/functional_tests/client/tests/test_etcd_client.py delete mode 100644 samples/etcd_service/src/CMakeLists.txt delete mode 100644 samples/etcd_service/src/service.cpp delete mode 100644 samples/etcd_service/src/static_config.yaml diff --git a/etcd/CMakeLists.txt b/etcd/CMakeLists.txt index aced3e0e2523..9b14fe8602ba 100644 --- a/etcd/CMakeLists.txt +++ b/etcd/CMakeLists.txt @@ -4,3 +4,7 @@ userver_module(etcd SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" UTEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*_test.cpp" ) + +if (USERVER_BUILD_TESTS) + add_subdirectory(functional_tests) +endif() diff --git a/etcd/functional_tests/CMakeLists.txt b/etcd/functional_tests/CMakeLists.txt new file mode 100644 index 000000000000..4f24d80d6503 --- /dev/null +++ b/etcd/functional_tests/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-etcd-tests CXX) + +add_custom_target(${PROJECT_NAME}) + +add_subdirectory(client) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}-client) diff --git a/etcd/functional_tests/client/CMakeLists.txt b/etcd/functional_tests/client/CMakeLists.txt new file mode 100644 index 000000000000..6d2994bd4429 --- /dev/null +++ b/etcd/functional_tests/client/CMakeLists.txt @@ -0,0 +1,6 @@ +project(userver-etcd-tests-client CXX) + +add_executable(${PROJECT_NAME} "etcd_service.cpp") +target_link_libraries(${PROJECT_NAME} userver-etcd) + +userver_chaos_testsuite_add() diff --git a/etcd/functional_tests/client/etcd_service.cpp b/etcd/functional_tests/client/etcd_service.cpp new file mode 100644 index 000000000000..e8b7113eda39 --- /dev/null +++ b/etcd/functional_tests/client/etcd_service.cpp @@ -0,0 +1,105 @@ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +class HandlerV1Get final : public userver::server::handlers::HttpHandlerBase { +public: + static constexpr std::string_view kName = "handler-v1-put"; + + HandlerV1Get( + const userver::components::ComponentConfig& config, + const userver::components::ComponentContext& component_context + ) + : HttpHandlerBase(config, component_context), + etcd_client_ptr_(component_context.FindComponent("etcd-сlient").GetClient()) {} + + std::string HandleRequestThrow( + const userver::server::http::HttpRequest& request, + userver::server::request::RequestContext& + ) const override { + const auto maybe_value = etcd_client_ptr_->Get(request.GetArg("key")); + return maybe_value.value_or("No value"); + } + +private: + userver::etcd::ClientPtr etcd_client_ptr_; +}; + +class HandlerV1Put final : public userver::server::handlers::HttpHandlerBase { +public: + static constexpr std::string_view kName = "handler-v1-put"; + + HandlerV1Put( + const userver::components::ComponentConfig& config, + const userver::components::ComponentContext& component_context + ) + : HttpHandlerBase(config, component_context), + etcd_client_ptr_(component_context.FindComponent("etcd-сlient").GetClient()) {} + + std::string HandleRequestThrow( + const userver::server::http::HttpRequest& request, + userver::server::request::RequestContext& + ) const override { + etcd_client_ptr_->Put(request.GetArg("key"), request.GetArg("value")); + + return std::string(); + } + +private: + userver::etcd::ClientPtr etcd_client_ptr_; +}; + +class HandlerV1Watch final : public userver::server::handlers::HttpHandlerBase { + public: + static constexpr std::string_view kName = "handler-v1-watch"; + + HandlerV1Watch( + const userver::components::ComponentConfig& config, + const userver::components::ComponentContext& component_context + ) + : HttpHandlerBase(config, component_context), + etcd_client_ptr_(component_context.FindComponent("etcd-сlient").GetClient()) {} + + std::string HandleRequestThrow( + const userver::server::http::HttpRequest& request, + userver::server::request::RequestContext& + ) const override { + const auto key = request.GetArg("key"); + const auto original_value = etcd_client_ptr_->Get(key); + auto watch_listener = etcd_client_ptr_->StartWatch(key); + const auto watch_event = watch_listener.GetEvent(); + const auto new_value = watch_event.value; + return fmt::format("original value: {}, new value: {}", original_value.value_or("No value"), new_value); + } + + private: + userver::etcd::ClientPtr etcd_client_ptr_; + }; + +} // namespace + +int main(int argc, char* argv[]) { + auto component_list = userver::components::MinimalServerComponentList() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append(); + + return userver::utils::DaemonMain(argc, argv, component_list); +} diff --git a/etcd/functional_tests/client/static_config.yaml b/etcd/functional_tests/client/static_config.yaml new file mode 100644 index 000000000000..b57b0ac5bb6c --- /dev/null +++ b/etcd/functional_tests/client/static_config.yaml @@ -0,0 +1,52 @@ +components_manager: + task_processors: # Task processor is an executor for coroutine tasks + main-task-processor: # Make a task processor for CPU-bound coroutine tasks. + worker_threads: $worker-threads # Process tasks in 4 threads. + fs-task-processor: # Make a separate task processor for filesystem bound tasks. + worker_threads: $worker-fs-threads + + default_task_processor: main-task-processor + + components: # Configuring components that were registered via component_list + server: + listener: # configuring the main listening socket... + port: $server-port # ...to listen on this port and... + task_processor: main-task-processor # ...process incoming requests on this task processor. + logging: + fs-task-processor: fs-task-processor + loggers: + default: + file_path: '@stderr' + level: $logger-level + overflow_behavior: discard # Drop logs if the system is too busy to write them down. + + testsuite-support: {} + + http-client: + load-enabled: $is_testing + fs-task-processor: fs-task-processor + + tests-control: + path: /tests/{action} + method: POST + task_processor: main-task-processor + testpoint-timeout: 10s + testpoint-url: mockserver/testpoint + throttling_enabled: false + + etcd-сlient: + endpoints: + - http://localhost:2379 + attempts: 2 + request_timeout_ms: 500 + watch_timeout_ms: 1000000 + + handler-v1-get: + path: /v1/get + method: POST + task_processor: main-task-processor + + handler-v1-put: + path: /v1/put + method: PUT + task_processor: main-task-processor diff --git a/etcd/functional_tests/client/tests/conftest.py b/etcd/functional_tests/client/tests/conftest.py new file mode 100644 index 000000000000..5871ed8eef2f --- /dev/null +++ b/etcd/functional_tests/client/tests/conftest.py @@ -0,0 +1 @@ +import pytest diff --git a/etcd/functional_tests/client/tests/test_etcd_client.py b/etcd/functional_tests/client/tests/test_etcd_client.py new file mode 100644 index 000000000000..8cec27a19b77 --- /dev/null +++ b/etcd/functional_tests/client/tests/test_etcd_client.py @@ -0,0 +1,5 @@ +import pytest + + +async def test_etcd_put_get(service_client): + assert "hello" == "hello" diff --git a/samples/etcd_service/src/CMakeLists.txt b/samples/etcd_service/src/CMakeLists.txt deleted file mode 100644 index 364978e93f78..000000000000 --- a/samples/etcd_service/src/CMakeLists.txt +++ /dev/null @@ -1,9 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(userver-samples-etcd_service CXX) - -find_package(userver COMPONENTS core etcd REQUIRED) - -add_executable(${PROJECT_NAME} service.cpp) -target_link_libraries(${PROJECT_NAME} userver::core userver::etcd) - -userver_testsuite_add_simple() diff --git a/samples/etcd_service/src/service.cpp b/samples/etcd_service/src/service.cpp deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/samples/etcd_service/src/static_config.yaml b/samples/etcd_service/src/static_config.yaml deleted file mode 100644 index e69de29bb2d1..000000000000 From 710ae63b6a951d4db88152428d0c571ad6d42b18 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sun, 4 May 2025 23:27:46 +0300 Subject: [PATCH 21/42] add tests --- etcd/functional_tests/client/CMakeLists.txt | 2 +- etcd/functional_tests/client/etcd_service.cpp | 82 +++++++++---------- .../client/static_config.yaml | 58 +++++++++---- .../functional_tests/client/tests/conftest.py | 65 +++++++++++++++ .../client/tests/test_etcd_client.py | 22 ++++- etcd/include/userver/etcd/component.hpp | 2 +- etcd/src/etcd/client_impl.cpp | 12 ++- etcd/src/etcd/etcd_client_test.cpp | 3 + 8 files changed, 180 insertions(+), 66 deletions(-) diff --git a/etcd/functional_tests/client/CMakeLists.txt b/etcd/functional_tests/client/CMakeLists.txt index 6d2994bd4429..76b956f0c704 100644 --- a/etcd/functional_tests/client/CMakeLists.txt +++ b/etcd/functional_tests/client/CMakeLists.txt @@ -1,6 +1,6 @@ project(userver-etcd-tests-client CXX) add_executable(${PROJECT_NAME} "etcd_service.cpp") -target_link_libraries(${PROJECT_NAME} userver-etcd) +target_link_libraries(${PROJECT_NAME} userver-core userver-etcd) userver_chaos_testsuite_add() diff --git a/etcd/functional_tests/client/etcd_service.cpp b/etcd/functional_tests/client/etcd_service.cpp index e8b7113eda39..737231d5f93a 100644 --- a/etcd/functional_tests/client/etcd_service.cpp +++ b/etcd/functional_tests/client/etcd_service.cpp @@ -1,34 +1,33 @@ #include +#include +#include #include -#include #include #include -#include -#include +#include #include -#include -#include #include +#include +#include namespace { class HandlerV1Get final : public userver::server::handlers::HttpHandlerBase { public: - static constexpr std::string_view kName = "handler-v1-put"; + static constexpr std::string_view kName = "handler-v1-get"; HandlerV1Get( const userver::components::ComponentConfig& config, const userver::components::ComponentContext& component_context ) : HttpHandlerBase(config, component_context), - etcd_client_ptr_(component_context.FindComponent("etcd-сlient").GetClient()) {} + etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} - std::string HandleRequestThrow( - const userver::server::http::HttpRequest& request, - userver::server::request::RequestContext& - ) const override { + std::string + HandleRequestThrow(const userver::server::http::HttpRequest& request, userver::server::request::RequestContext&) + const override { const auto maybe_value = etcd_client_ptr_->Get(request.GetArg("key")); return maybe_value.value_or("No value"); } @@ -46,12 +45,11 @@ class HandlerV1Put final : public userver::server::handlers::HttpHandlerBase { const userver::components::ComponentContext& component_context ) : HttpHandlerBase(config, component_context), - etcd_client_ptr_(component_context.FindComponent("etcd-сlient").GetClient()) {} + etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} - std::string HandleRequestThrow( - const userver::server::http::HttpRequest& request, - userver::server::request::RequestContext& - ) const override { + std::string + HandleRequestThrow(const userver::server::http::HttpRequest& request, userver::server::request::RequestContext&) + const override { etcd_client_ptr_->Put(request.GetArg("key"), request.GetArg("value")); return std::string(); @@ -62,32 +60,31 @@ class HandlerV1Put final : public userver::server::handlers::HttpHandlerBase { }; class HandlerV1Watch final : public userver::server::handlers::HttpHandlerBase { - public: - static constexpr std::string_view kName = "handler-v1-watch"; - - HandlerV1Watch( - const userver::components::ComponentConfig& config, - const userver::components::ComponentContext& component_context - ) - : HttpHandlerBase(config, component_context), - etcd_client_ptr_(component_context.FindComponent("etcd-сlient").GetClient()) {} - - std::string HandleRequestThrow( - const userver::server::http::HttpRequest& request, - userver::server::request::RequestContext& - ) const override { - const auto key = request.GetArg("key"); - const auto original_value = etcd_client_ptr_->Get(key); - auto watch_listener = etcd_client_ptr_->StartWatch(key); - const auto watch_event = watch_listener.GetEvent(); - const auto new_value = watch_event.value; - return fmt::format("original value: {}, new value: {}", original_value.value_or("No value"), new_value); - } - - private: - userver::etcd::ClientPtr etcd_client_ptr_; - }; - +public: + static constexpr std::string_view kName = "handler-v1-watch"; + + HandlerV1Watch( + const userver::components::ComponentConfig& config, + const userver::components::ComponentContext& component_context + ) + : HttpHandlerBase(config, component_context), + etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} + + std::string + HandleRequestThrow(const userver::server::http::HttpRequest& request, userver::server::request::RequestContext&) + const override { + const auto key = request.GetArg("key"); + const auto original_value = etcd_client_ptr_->Get(key); + auto watch_listener = etcd_client_ptr_->StartWatch(key); + const auto watch_event = watch_listener.GetEvent(); + const auto new_value = watch_event.value; + return fmt::format("original value: {}, new value: {}", original_value.value_or("No value"), new_value); + } + +private: + userver::etcd::ClientPtr etcd_client_ptr_; +}; + } // namespace int main(int argc, char* argv[]) { @@ -95,6 +92,7 @@ int main(int argc, char* argv[]) { .Append() .Append() .Append() + .Append() .Append() .Append() .Append() diff --git a/etcd/functional_tests/client/static_config.yaml b/etcd/functional_tests/client/static_config.yaml index b57b0ac5bb6c..6ddd00996d71 100644 --- a/etcd/functional_tests/client/static_config.yaml +++ b/etcd/functional_tests/client/static_config.yaml @@ -1,40 +1,59 @@ +# yaml components_manager: - task_processors: # Task processor is an executor for coroutine tasks - main-task-processor: # Make a task processor for CPU-bound coroutine tasks. - worker_threads: $worker-threads # Process tasks in 4 threads. - fs-task-processor: # Make a separate task processor for filesystem bound tasks. - worker_threads: $worker-fs-threads + task_processors: + main-task-processor: + worker_threads: 4 + + fs-task-processor: + worker_threads: 1 default_task_processor: main-task-processor - components: # Configuring components that were registered via component_list + components: server: - listener: # configuring the main listening socket... - port: $server-port # ...to listen on this port and... - task_processor: main-task-processor # ...process incoming requests on this task processor. + listener: + port: 8080 + task_processor: main-task-processor + connection: + http-version: '2' + http2-session: + max_concurrent_streams: 100 + max_frame_size: 16384 + initial_window_size: 65536 + listener-monitor: + port: 8081 + task_processor: main-task-processor logging: fs-task-processor: fs-task-processor loggers: default: file_path: '@stderr' - level: $logger-level - overflow_behavior: discard # Drop logs if the system is too busy to write them down. - - testsuite-support: {} + level: error + overflow_behavior: discard http-client: - load-enabled: $is_testing fs-task-processor: fs-task-processor - + user-agent: $server-name + user-agent#fallback: 'userver-based-service 1.0' + dns-client: + fs-task-processor: fs-task-processor + testsuite-support: tests-control: path: /tests/{action} method: POST task_processor: main-task-processor testpoint-timeout: 10s - testpoint-url: mockserver/testpoint + testpoint-url: $mockserver/testpoint + throttling_enabled: false + + handler-ping: + path: /ping + method: GET + task_processor: main-task-processor throttling_enabled: false + url_trailing_slash: strict-match - etcd-сlient: + etcd-client: endpoints: - http://localhost:2379 attempts: 2 @@ -50,3 +69,8 @@ components_manager: path: /v1/put method: PUT task_processor: main-task-processor + + handler-v1-watch: + path: /v1/watch + method: POST + task_processor: main-task-processor diff --git a/etcd/functional_tests/client/tests/conftest.py b/etcd/functional_tests/client/tests/conftest.py index 5871ed8eef2f..e57c00de3cea 100644 --- a/etcd/functional_tests/client/tests/conftest.py +++ b/etcd/functional_tests/client/tests/conftest.py @@ -1 +1,66 @@ +import base64 + import pytest + +pytest_plugins = ['pytest_userver.plugins.core'] + +@pytest.fixture(scope='session') +def userver_config_http_client( + mockserver_info, + mockserver_ssl_info, + allowed_url_prefixes_extra, +): + def patch_config(config, config_vars): + components: dict = config['components_manager']['components'] + + http_client = components['http-client'] or {} + http_client['testsuite-enabled'] = False + + etcd_client = components['etcd-client'] or {} + etcd_client['endpoints'] = [mockserver_info.base_url[:-1]] + + allowed_urls = [mockserver_info.base_url] + if mockserver_ssl_info: + allowed_urls.append(mockserver_ssl_info.base_url) + allowed_urls += allowed_url_prefixes_extra + http_client['testsuite-allowed-url-prefixes'] = allowed_urls + + return patch_config + + +@pytest.fixture(name='etcd_mock') +def etcd_mock(mockserver): + etcd_storage = {} + + @mockserver.json_handler('/v3/kv/put') + async def mock(request): + key = base64.b64decode(request.json['key']) + value = base64.b64decode(request.json['value']) + etcd_storage[key] = value + return mockserver.make_response('OK!') + + @mockserver.json_handler('/v3/kv/range') + async def mock(request): + request_key = base64.b64decode(request.json['key']) + if 'range_end' in request.json: + request_range_end = base64.b64decode(request.json['range_end']) + else: + return mockserver.make_response(json={ + 'kvs': [{ + 'key': base64.b64encode(request_key).decode('utf-8'), + 'value': base64.b64encode(etcd_storage[request_key]).decode('utf-8'), + }] + }) + + values = [] + for key, value in etcd_storage.items(): + if key >= request_key and key < request_range_end: + values.append({ + 'key': base64.b64encode(key).decode('utf-8'), + 'value': base64.b64encode(value).decode('utf-8'), + }) + + return mockserver.make_response(json={ + 'kvs': values + }) + diff --git a/etcd/functional_tests/client/tests/test_etcd_client.py b/etcd/functional_tests/client/tests/test_etcd_client.py index 8cec27a19b77..6f42289e1ee0 100644 --- a/etcd/functional_tests/client/tests/test_etcd_client.py +++ b/etcd/functional_tests/client/tests/test_etcd_client.py @@ -1,5 +1,23 @@ import pytest -async def test_etcd_put_get(service_client): - assert "hello" == "hello" +async def test_etcd_put_get(service_client, etcd_mock): + response = await service_client.post( + '/v1/get', + params={'key': 'some_key'} + ) + assert response.status == 200 + assert response.content == b'No value' + + response = await service_client.put( + '/v1/put', + params={'key': 'some_key', 'value': 'some_value'} + ) + assert response.status == 200 + + response = await service_client.post( + '/v1/get', + params={'key': 'some_key'} + ) + assert response.status == 200 + assert response.content == b'some_value' diff --git a/etcd/include/userver/etcd/component.hpp b/etcd/include/userver/etcd/component.hpp index a6dd4003ad02..3ea243235031 100644 --- a/etcd/include/userver/etcd/component.hpp +++ b/etcd/include/userver/etcd/component.hpp @@ -31,7 +31,7 @@ namespace etcd { class Component final : public components::ComponentBase { public: - static constexpr std::string_view kName = "etcd-сlient"; + static constexpr std::string_view kName = "etcd-client"; Component(const components::ComponentConfig&, const components::ComponentContext&); diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 39e3a6ff5c26..b77e6d7024e7 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -46,10 +47,15 @@ std::string BuildPutData(const std::string& key, const std::string& value) { std::string BuildRangeUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/range", service_url); } -std::string BuildRangeData(const std::string& key) { +std::string BuildRangeData(const std::string& key, const std::optional& maybe_range_end = std::nullopt) { const auto etcd_key = kKeyPrefix + key; + if (!maybe_range_end.has_value()) { + return formats::json::ToString(formats::json::MakeObject( + "key", crypto::base64::Base64Encode(etcd_key) + )); + } return formats::json::ToString(formats::json::MakeObject( - "key", crypto::base64::Base64Encode(etcd_key), "range_end", crypto::base64::Base64Encode(kLastPossibleKeyPrefix) + "key", crypto::base64::Base64Encode(etcd_key), "range_end", crypto::base64::Base64Encode(maybe_range_end) )); } @@ -120,7 +126,7 @@ std::optional ClientImpl::Get(const std::string& key) { } std::vector ClientImpl::Range(const std::string& key_prefix) { - auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix)); + auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix, kLastPossibleKeyPrefix)); const auto json_body = formats::json::FromString(response->body()); if (!json_body.HasMember("kvs")) { diff --git a/etcd/src/etcd/etcd_client_test.cpp b/etcd/src/etcd/etcd_client_test.cpp index 4946a83a37ee..c672d214f669 100644 --- a/etcd/src/etcd/etcd_client_test.cpp +++ b/etcd/src/etcd/etcd_client_test.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -60,6 +61,7 @@ UTEST(Etcd, TestKeyValueStorage) { {mock_server.GetBaseUrl()}, 2, std::chrono::milliseconds{500}, + std::chrono::milliseconds{100'000}, } ); @@ -88,6 +90,7 @@ UTEST(Etcd, TestRange) { {mock_server.GetBaseUrl()}, 2, std::chrono::milliseconds{500}, + std::chrono::milliseconds{100'000}, } ); From 95ff228d335438420b5778c838a749bf0032ef5a Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Mon, 5 May 2025 20:02:24 +0300 Subject: [PATCH 22/42] add watch test --- etcd/functional_tests/client/etcd_service.cpp | 4 +- .../functional_tests/client/tests/conftest.py | 42 +++++++++++++++++-- .../client/tests/test_etcd_client.py | 20 +++++++-- etcd/src/etcd/client_impl.cpp | 15 ++++--- 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/etcd/functional_tests/client/etcd_service.cpp b/etcd/functional_tests/client/etcd_service.cpp index 737231d5f93a..5fbed45916e0 100644 --- a/etcd/functional_tests/client/etcd_service.cpp +++ b/etcd/functional_tests/client/etcd_service.cpp @@ -74,11 +74,11 @@ class HandlerV1Watch final : public userver::server::handlers::HttpHandlerBase { HandleRequestThrow(const userver::server::http::HttpRequest& request, userver::server::request::RequestContext&) const override { const auto key = request.GetArg("key"); - const auto original_value = etcd_client_ptr_->Get(key); + const auto maybe_original_value = etcd_client_ptr_->Get(key); auto watch_listener = etcd_client_ptr_->StartWatch(key); const auto watch_event = watch_listener.GetEvent(); const auto new_value = watch_event.value; - return fmt::format("original value: {}, new value: {}", original_value.value_or("No value"), new_value); + return fmt::format("original value: {}, new value: {}", maybe_original_value.value_or("No value"), new_value); } private: diff --git a/etcd/functional_tests/client/tests/conftest.py b/etcd/functional_tests/client/tests/conftest.py index e57c00de3cea..0485261ec44a 100644 --- a/etcd/functional_tests/client/tests/conftest.py +++ b/etcd/functional_tests/client/tests/conftest.py @@ -1,9 +1,14 @@ +import asyncio import base64 +import json + +import aiohttp import pytest pytest_plugins = ['pytest_userver.plugins.core'] + @pytest.fixture(scope='session') def userver_config_http_client( mockserver_info, @@ -12,7 +17,7 @@ def userver_config_http_client( ): def patch_config(config, config_vars): components: dict = config['components_manager']['components'] - + http_client = components['http-client'] or {} http_client['testsuite-enabled'] = False @@ -38,19 +43,21 @@ async def mock(request): value = base64.b64decode(request.json['value']) etcd_storage[key] = value return mockserver.make_response('OK!') - + @mockserver.json_handler('/v3/kv/range') async def mock(request): request_key = base64.b64decode(request.json['key']) if 'range_end' in request.json: request_range_end = base64.b64decode(request.json['range_end']) - else: + elif request_key in etcd_storage: return mockserver.make_response(json={ 'kvs': [{ 'key': base64.b64encode(request_key).decode('utf-8'), 'value': base64.b64encode(etcd_storage[request_key]).decode('utf-8'), }] }) + else: + return mockserver.make_response(json={}) values = [] for key, value in etcd_storage.items(): @@ -63,4 +70,31 @@ async def mock(request): return mockserver.make_response(json={ 'kvs': values }) - + + @mockserver.handler('/v3/watch') + async def mock(request): + key = base64.b64decode(request.json['create_request']['key']) + response = aiohttp.web.StreamResponse( + status=200, + headers={ + 'Content-Type': 'application/json', + } + ) + await response.prepare(request._request) + data = json.dumps({ + 'result': { + 'events': [ + { + 'kv': { + 'key': base64.b64encode(key).decode(), + 'value': base64.b64encode(b'new_value').decode(), + 'version': '2', + } + } + ] + } + }) + await response.write(data.encode()) + await asyncio.sleep(0.01) + + return response diff --git a/etcd/functional_tests/client/tests/test_etcd_client.py b/etcd/functional_tests/client/tests/test_etcd_client.py index 6f42289e1ee0..7105f415b351 100644 --- a/etcd/functional_tests/client/tests/test_etcd_client.py +++ b/etcd/functional_tests/client/tests/test_etcd_client.py @@ -1,6 +1,3 @@ -import pytest - - async def test_etcd_put_get(service_client, etcd_mock): response = await service_client.post( '/v1/get', @@ -8,7 +5,7 @@ async def test_etcd_put_get(service_client, etcd_mock): ) assert response.status == 200 assert response.content == b'No value' - + response = await service_client.put( '/v1/put', params={'key': 'some_key', 'value': 'some_value'} @@ -21,3 +18,18 @@ async def test_etcd_put_get(service_client, etcd_mock): ) assert response.status == 200 assert response.content == b'some_value' + + +async def test_etcd_watch(service_client, etcd_mock): + response = await service_client.put( + '/v1/put', + params={'key': 'some_key', 'value': 'original_value'} + ) + assert response.status == 200 + + response = await service_client.post( + '/v1/watch', + params={'key': 'some_key'} + ) + assert response.status == 200 + assert response.content == b'original value: original_value, new value: new_value' diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index b77e6d7024e7..f08f5f17947f 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -50,12 +50,13 @@ std::string BuildRangeUrl(const std::string& service_url) { return fmt::format(" std::string BuildRangeData(const std::string& key, const std::optional& maybe_range_end = std::nullopt) { const auto etcd_key = kKeyPrefix + key; if (!maybe_range_end.has_value()) { - return formats::json::ToString(formats::json::MakeObject( - "key", crypto::base64::Base64Encode(etcd_key) - )); + return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(etcd_key))); } return formats::json::ToString(formats::json::MakeObject( - "key", crypto::base64::Base64Encode(etcd_key), "range_end", crypto::base64::Base64Encode(maybe_range_end) + "key", + crypto::base64::Base64Encode(etcd_key), + "range_end", + crypto::base64::Base64Encode(maybe_range_end.value()) )); } @@ -207,12 +208,16 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( } void ClientImpl::WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer) { + LOG_DEBUG() << "Start whatching key changes"; auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); - std::string body_part; while (stream_response.ReadChunk(body_part, engine::Deadline())) { const auto watch_response = formats::json::FromString(body_part); LOG_DEBUG() << "Got folowing chunk from etcd watch handler: " << watch_response; + if (!watch_response.HasMember("result")) { + LOG_DEBUG() << "No result in watch part response, skipping"; + continue; + } if (!watch_response["result"].HasMember("events")) { LOG_DEBUG() << "No events in watch part response, skipping"; continue; From dc75cd56527cc01bd13e9816f85b04654aa5c475 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Wed, 7 May 2025 20:40:57 +0300 Subject: [PATCH 23/42] delete file --- testsuite/pytest_plugins/pytest_userver/plugins/etcd.py | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 testsuite/pytest_plugins/pytest_userver/plugins/etcd.py diff --git a/testsuite/pytest_plugins/pytest_userver/plugins/etcd.py b/testsuite/pytest_plugins/pytest_userver/plugins/etcd.py deleted file mode 100644 index 139597f9cb07..000000000000 --- a/testsuite/pytest_plugins/pytest_userver/plugins/etcd.py +++ /dev/null @@ -1,2 +0,0 @@ - - From 5240f6d6e6d70d740e5c3fffefbda50816acf044 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 9 May 2025 22:05:13 +0300 Subject: [PATCH 24/42] iteration --- etcd/include/userver/etcd/client.hpp | 15 +-- etcd/include/userver/etcd/key_value_state.hpp | 30 ++++++ etcd/include/userver/etcd/watch_listener.hpp | 17 +--- etcd/src/etcd/client_impl.cpp | 95 ++++++++----------- etcd/src/etcd/client_impl.hpp | 20 ++-- etcd/src/etcd/etcd_client_test.cpp | 70 ++++++++++---- etcd/src/etcd/etcd_responses.cpp | 15 +++ etcd/src/etcd/etcd_responses.hpp | 26 +++++ etcd/src/etcd/key_value_state.cpp | 20 ++++ etcd/src/etcd/watch_listener.cpp | 12 --- 10 files changed, 205 insertions(+), 115 deletions(-) create mode 100644 etcd/include/userver/etcd/key_value_state.hpp create mode 100644 etcd/src/etcd/etcd_responses.cpp create mode 100644 etcd/src/etcd/etcd_responses.hpp create mode 100644 etcd/src/etcd/key_value_state.cpp diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 5d3bc4a0eed8..1850440c8808 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -29,27 +30,27 @@ class Client { /// The pair should be retrieve only with current client, /// because Put can transform the key. /// - virtual void Put(const std::string& key, const std::string& value) = 0; + virtual void Put(std::string_view key, std::string_view value) = 0; /// @brief Gets a value from etcd cluster by a key. /// If there is no value with such key, returns std::nullopt /// - [[nodiscard]] virtual std::optional Get(const std::string& key) = 0; + [[nodiscard]] virtual std::optional Get(std::string_view key) = 0; - /// @brief Retrieves values from the etcd cluster, + /// @brief Retrieves key values pairs from the etcd cluster, /// the keys of which match the passed prefix. - /// If there is no value with such key, returns std::nullopt + /// If there is no value with such key, returns empty vector /// - [[nodiscard]] virtual std::vector Range(const std::string& key_prefix) = 0; + [[nodiscard]] virtual std::vector Range(std::string_view key_prefix) = 0; /// @brief Delete a key value pair with the passed key /// from the etcd cluster /// - virtual void Delete(const std::string& key) = 0; + virtual void Delete(std::string_view key) = 0; /// @brief Start task that produces events when key value pair changes /// - virtual WatchListener StartWatch(const std::string& key) = 0; + virtual WatchListener StartWatch(std::string_view key) = 0; }; using ClientPtr = std::shared_ptr; diff --git a/etcd/include/userver/etcd/key_value_state.hpp b/etcd/include/userver/etcd/key_value_state.hpp new file mode 100644 index 000000000000..b699cbcada40 --- /dev/null +++ b/etcd/include/userver/etcd/key_value_state.hpp @@ -0,0 +1,30 @@ +#pragma once + +/// @file userver/etcd/key_value_state.hpp +/// @brief @copybrief etcd::KeyValueState + +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace etcd { + +/// @brief Struct with key value pair from etcd. It represents current status of key value pair. +struct KeyValueState final { + std::string key; + std::string value; + std::int32_t version; +}; + +} // namespace etcd + +namespace formats::parse { + +etcd::KeyValueState Parse(const formats::json::Value& value, To); + +} + +USERVER_NAMESPACE_END diff --git a/etcd/include/userver/etcd/watch_listener.hpp b/etcd/include/userver/etcd/watch_listener.hpp index 722d2a44c5f5..8e202bb625a9 100644 --- a/etcd/include/userver/etcd/watch_listener.hpp +++ b/etcd/include/userver/etcd/watch_listener.hpp @@ -6,32 +6,23 @@ #include #include +#include #include USERVER_NAMESPACE_BEGIN namespace etcd { -/// @brief Struct with key value pair from etcd. It represents current status of key value pair. -struct KeyValueState final { - std::string key; - std::string value; - std::int32_t version; -}; - /// @brief Struct that return value change events in etcd struct WatchListener final { concurrent::SpscQueue::Consumer consumer; + /// Get an event from etcd if there was one, otherwise waits asynchronously until a next event occurs. + /// If the coroutine, that was spawned by StartWatch method of etcd client, is finished or failed, GetEvent raises + /// exception Get Event uses Consumer::Pop method for getting the event. KeyValueState GetEvent(); }; } // namespace etcd -namespace formats::parse { - -etcd::KeyValueState Parse(const formats::json::Value& value, To); - -} - USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index f08f5f17947f..fd4313f72151 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -21,6 +21,8 @@ #include #include +#include + USERVER_NAMESPACE_BEGIN namespace etcd { @@ -33,22 +35,22 @@ const std::uint32_t kMaxRetryStatusCode = 599; const std::uint32_t kMinGoodStatusCode = 200; const std::uint32_t kMaxGoodStatusCode = 299; -const std::string kKeyPrefix = "/etcd/"; -const std::string kLastPossibleKeyPrefix = "/etcd0"; +std::string kKeyPrefix = "/etcd/"; +std::string kLastPossibleKeyPrefix = "/etcd0"; -std::string BuildPutUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/put", service_url); } +std::string BuildPutUrl(std::string_view service_url) { return fmt::format("{}/v3/kv/put", service_url); } -std::string BuildPutData(const std::string& key, const std::string& value) { - const auto etcd_key = kKeyPrefix + key; +std::string BuildPutData(std::string_view key, std::string_view value) { + const auto etcd_key = fmt::format("{}{}", kKeyPrefix, key); return formats::json::ToString(formats::json::MakeObject( "key", crypto::base64::Base64Encode(etcd_key), "value", crypto::base64::Base64Encode(value) )); } -std::string BuildRangeUrl(const std::string& service_url) { return fmt::format("{}/v3/kv/range", service_url); } +std::string BuildRangeUrl(std::string_view service_url) { return fmt::format("{}/v3/kv/range", service_url); } -std::string BuildRangeData(const std::string& key, const std::optional& maybe_range_end = std::nullopt) { - const auto etcd_key = kKeyPrefix + key; +std::string BuildRangeData(std::string_view key, const std::optional maybe_range_end = std::nullopt) { + const auto etcd_key = fmt::format("{}{}", kKeyPrefix, key); if (!maybe_range_end.has_value()) { return formats::json::ToString(formats::json::MakeObject("key", crypto::base64::Base64Encode(etcd_key))); } @@ -60,17 +62,17 @@ std::string BuildRangeData(const std::string& key, const std::optional ClientImpl::Get(const std::string& key) { +std::optional ClientImpl::Get(std::string_view key) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); - const auto json_body = formats::json::FromString(response->body()); - if (!json_body.HasMember("kvs")) { - return std::nullopt; - } - const auto& key_value_list = json_body["kvs"]; - const auto etcd_key = kKeyPrefix + key; - for (const auto& key_value : key_value_list) { - if (crypto::base64::Base64Decode(key_value["key"].As()) == etcd_key) { - return crypto::base64::Base64Decode(key_value["value"].As()); + const auto range_response = formats::json::FromString(response->body()).As(); + const auto etcd_key = fmt::format("{}{}", kKeyPrefix, key); + for (const auto& key_value_state : range_response.key_value_states) { + if (key_value_state.key == etcd_key) { + return key_value_state.value; } } return std::nullopt; } -std::vector ClientImpl::Range(const std::string& key_prefix) { +std::vector ClientImpl::Range(std::string_view key_prefix) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix, kLastPossibleKeyPrefix)); - const auto json_body = formats::json::FromString(response->body()); - if (!json_body.HasMember("kvs")) { - return {}; - } - const auto& key_value_list = json_body["kvs"]; - std::vector values; - values.reserve(key_value_list.GetSize()); - for (const auto& key_value : key_value_list) { - values.push_back(crypto::base64::Base64Decode(key_value["value"].As())); - } - return values; + const auto range_response = formats::json::FromString(response->body()).As(); + return range_response.key_value_states; } -WatchListener ClientImpl::StartWatch(const std::string& key) { +WatchListener ClientImpl::StartWatch(std::string_view key) { auto queue = concurrent::SpscQueue::Create(); auto watch_queues_ptr = watch_queues_.Lock(); watch_queues_ptr->push_back(queue); - utils::Async("watch task", [&key, producer = queue->GetProducer(), this]() mutable { - this->WatchKeyChanges(key, std::move(producer)); + utils::Async("watch task", [key, producer = queue->GetProducer(), this]() mutable { + this->WatchKeyChanges(std::string(key), std::move(producer)); }).Detach(); return WatchListener{queue->GetConsumer()}; } -std::shared_ptr ClientImpl::PerformEtcdRequest( - const std::function& url_builder, - const std::string& data -) { +std::shared_ptr +ClientImpl::PerformEtcdRequest(const std::function& url_builder, std::string_view data) { auto endpoints = settings_.endpoints; utils::Shuffle(endpoints); std::shared_ptr response_ptr; for (const auto& endpoint : endpoints) { response_ptr = http_client_.CreateRequest() - .post(url_builder(endpoint), data) + .post(url_builder(endpoint), std::string{data}) .retry(settings_.attempts) .timeout(settings_.request_timeout_ms.count()) .perform(); if (!ShouldRetry(response_ptr->status_code())) { - CheckResponseStatusCode(response_ptr->status_code()); + CheckResponseStatusCode(response_ptr->status_code(), response_ptr->body()); return response_ptr; } } @@ -178,8 +165,8 @@ std::shared_ptr ClientImpl::PerformEtcdRequest( } clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( - const std::function& url_builder, - const std::string& data + const std::function& url_builder, + std::string_view data ) { auto endpoints = settings_.endpoints; utils::Shuffle(endpoints); @@ -188,13 +175,13 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( for (const auto& endpoint : endpoints) { const auto queue = concurrent::StringStreamQueue::Create(); maybe_streamed_response = http_client_.CreateRequest() - .post(url_builder(endpoint), data) + .post(url_builder(endpoint), std::string{data}) .retry(settings_.attempts) .timeout(settings_.watch_timeout_ms.count()) .async_perform_stream_body(queue); auto& streamed_response = maybe_streamed_response.value(); if (!ShouldRetry(streamed_response.StatusCode())) { - CheckResponseStatusCode(streamed_response.StatusCode()); + CheckResponseStatusCode(streamed_response.StatusCode(), "There is no body in stream responses"); return std::move(streamed_response); } } @@ -207,7 +194,7 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( } } -void ClientImpl::WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer) { +void ClientImpl::WatchKeyChanges(const std::string key, concurrent::SpscQueue::Producer producer) { LOG_DEBUG() << "Start whatching key changes"; auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); std::string body_part; diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index bccd81d7ca52..671050a9ec1a 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -16,26 +16,24 @@ class ClientImpl : public Client { public: ClientImpl(clients::http::Client& http_client, ClientSettings settings); - void Put(const std::string& key, const std::string& value) override; + void Put(std::string_view key, std::string_view value) override; - [[nodiscard]] std::optional Get(const std::string& key) override; + [[nodiscard]] std::optional Get(std::string_view key) override; - [[nodiscard]] std::vector Range(const std::string& key_prefix) override; + [[nodiscard]] std::vector Range(std::string_view key_prefix) override; - void Delete(const std::string& key) override; + void Delete(std::string_view key) override; - WatchListener StartWatch(const std::string& key) override; + WatchListener StartWatch(std::string_view key) override; private: std::shared_ptr - PerformEtcdRequest(const std::function& url_builder, const std::string& data); + PerformEtcdRequest(const std::function& url_builder, std::string_view data); - [[nodiscard]] clients::http::StreamedResponse PerformStreamedEtcdRequest( - const std::function& url_builder, - const std::string& data - ); + [[nodiscard]] clients::http::StreamedResponse + PerformStreamedEtcdRequest(const std::function& url_builder, std::string_view data); - void WatchKeyChanges(const std::string& key, concurrent::SpscQueue::Producer producer); + void WatchKeyChanges(const std::string key, concurrent::SpscQueue::Producer producer); using WatchQueuePtr = std::shared_ptr>; clients::http::Client& http_client_; diff --git a/etcd/src/etcd/etcd_client_test.cpp b/etcd/src/etcd/etcd_client_test.cpp index c672d214f669..0d586afcab5d 100644 --- a/etcd/src/etcd/etcd_client_test.cpp +++ b/etcd/src/etcd/etcd_client_test.cpp @@ -1,3 +1,5 @@ +#include + #include #include #include @@ -15,18 +17,39 @@ USERVER_NAMESPACE_BEGIN namespace { utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServerMock::HttpRequest& request) { - static std::map storage; + static std::map storage; EXPECT_EQ(request.method, clients::http::HttpMethod::kPost); const auto request_body = formats::json::FromString(request.body); formats::json::ValueBuilder response_body_value_builder; + const auto key = crypto::base64::Base64Decode(request_body["key"].As()); if (request.path == "/v3/kv/put") { - const auto key = crypto::base64::Base64Decode(request_body["key"].As()); const auto value = crypto::base64::Base64Decode(request_body["value"].As()); - storage[key] = value; + int32_t new_version = 1; + const auto key_value_iterator = storage.find(key); + if (key_value_iterator != storage.end()) { + new_version = (key_value_iterator->second).version + 1; + } + storage[key] = etcd::KeyValueState{ + /* .key = */ key, + /* .value = */ value, + /* .version = */ new_version, + }; + } else if (request.path == "/v3/kv/range" && !request_body.HasMember("range_end")) { + const auto value_iterator = storage.find(key); + response_body_value_builder["kvs"] = formats::json::MakeArray(); + if (value_iterator != storage.end()) { + response_body_value_builder["kvs"].PushBack(formats::json::MakeObject( + "key", + crypto::base64::Base64Encode((value_iterator->second).key), + "value", + crypto::base64::Base64Encode((value_iterator->second).value), + "version", + std::to_string((value_iterator->second).version) + )); + } } else if (request.path == "/v3/kv/range") { - const auto key = crypto::base64::Base64Decode(request_body["key"].As()); const auto range_end = crypto::base64::Base64Decode(request_body["range_end"].As()); auto first_key = storage.lower_bound(key); const auto last_key = storage.upper_bound(range_end); @@ -35,14 +58,15 @@ utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServer while (first_key != last_key) { response_body_value_builder["kvs"].PushBack(formats::json::MakeObject( "key", - crypto::base64::Base64Encode(first_key->first), + crypto::base64::Base64Encode((first_key->second).key), "value", - crypto::base64::Base64Encode(first_key->second) + crypto::base64::Base64Encode((first_key->second).value), + "version", + std::to_string((first_key->second).version) )); ++first_key; } } else if (request.path == "/v3/kv/deleterange") { - const auto key = crypto::base64::Base64Decode(request_body["key"].As()); storage.erase(key); } @@ -93,18 +117,28 @@ UTEST(Etcd, TestRange) { std::chrono::milliseconds{100'000}, } ); + const uint32_t range_size = 3; + + EXPECT_TRUE(etcd_client_ptr->Range("some_key").empty()); + + for (uint32_t i = 1; i <= range_size; ++i) { + etcd_client_ptr->Put(fmt::format("some_key_{}", i), fmt::format("some_value_{}", i)); + } - EXPECT_EQ(etcd_client_ptr->Range("some_key"), (std::vector{})); - - etcd_client_ptr->Put("some_key_1", "some_value_1"); - etcd_client_ptr->Put("some_key_2", "some_value_2"); - etcd_client_ptr->Put("some_key_3", "some_value_3"); - const auto range_result = etcd_client_ptr->Range("some_key"); - EXPECT_EQ(range_result, (std::vector{"some_value_1", "some_value_2", "some_value_3"})); - etcd_client_ptr->Delete("some_key_1"); - etcd_client_ptr->Delete("some_key_2"); - etcd_client_ptr->Delete("some_key_3"); - EXPECT_EQ(etcd_client_ptr->Range("some_key"), (std::vector{})); + auto range_result = etcd_client_ptr->Range("some_key"); + EXPECT_EQ(range_result.size(), range_size); + std::sort(range_result.begin(), range_result.end(), [](const etcd::KeyValueState& l, const etcd::KeyValueState& r) { + return l.value < r.value; + }); + for (uint32_t i = 1; i <= range_size; ++i) { + EXPECT_EQ(range_result[i - 1].value, fmt::format("some_value_{}", i)); + } + + for (uint32_t i = 1; i <= range_size; ++i) { + etcd_client_ptr->Delete(fmt::format("some_key_{}", i)); + } + + EXPECT_TRUE(etcd_client_ptr->Range("some_key").empty()); } USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/etcd_responses.cpp b/etcd/src/etcd/etcd_responses.cpp new file mode 100644 index 000000000000..e5ab92d94dab --- /dev/null +++ b/etcd/src/etcd/etcd_responses.cpp @@ -0,0 +1,15 @@ +#include + +USERVER_NAMESPACE_BEGIN + +namespace formats::parse { + +etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To) { + return etcd::EtcdRangeResponse{ + /* .key_value_states = */ value["kvs"].As>(), + }; +} + +} // namespace formats::parse + +USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/etcd_responses.hpp b/etcd/src/etcd/etcd_responses.hpp new file mode 100644 index 000000000000..db7262b604ae --- /dev/null +++ b/etcd/src/etcd/etcd_responses.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace etcd { + +struct EtcdRangeResponse { + std::vector key_value_states; +}; + +struct EtcdWatchResponse { + /* data */ +}; + +} // namespace etcd + +namespace formats::parse { + +etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To); + +} + +USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/key_value_state.cpp b/etcd/src/etcd/key_value_state.cpp new file mode 100644 index 000000000000..cd449d4cdd45 --- /dev/null +++ b/etcd/src/etcd/key_value_state.cpp @@ -0,0 +1,20 @@ +#include + +#include +#include + +USERVER_NAMESPACE_BEGIN + +namespace formats::parse { + +etcd::KeyValueState Parse(const formats::json::Value& value, To) { + return etcd::KeyValueState{ + /* .key = */ crypto::base64::Base64Decode(value["key"].As()), + /* .value = */ crypto::base64::Base64Decode(value["value"].As()), + /* .version = */ std::stoi(value["version"].As()), + }; +} + +} // namespace formats::parse + +USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/watch_listener.cpp b/etcd/src/etcd/watch_listener.cpp index 23950592a230..57e3631d0fd0 100644 --- a/etcd/src/etcd/watch_listener.cpp +++ b/etcd/src/etcd/watch_listener.cpp @@ -17,16 +17,4 @@ KeyValueState WatchListener::GetEvent() { } // namespace etcd -namespace formats::parse { - -etcd::KeyValueState Parse(const formats::json::Value& value, To) { - return etcd::KeyValueState{ - /* .key = */ crypto::base64::Base64Decode(value["key"].As()), - /* .value = */ crypto::base64::Base64Decode(value["value"].As()), - /* .version = */ std::stoi(value["version"].As()), - }; -} - -} // namespace formats::parse - USERVER_NAMESPACE_END From b8d928e6db6ef07490f77bea44c149e72d0eeb69 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sat, 10 May 2025 00:05:43 +0300 Subject: [PATCH 25/42] format --- etcd/functional_tests/client/etcd_service.cpp | 1 - etcd/src/etcd/etcd_client_test.cpp | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/etcd/functional_tests/client/etcd_service.cpp b/etcd/functional_tests/client/etcd_service.cpp index 5fbed45916e0..6791e29306ec 100644 --- a/etcd/functional_tests/client/etcd_service.cpp +++ b/etcd/functional_tests/client/etcd_service.cpp @@ -1,4 +1,3 @@ - #include #include diff --git a/etcd/src/etcd/etcd_client_test.cpp b/etcd/src/etcd/etcd_client_test.cpp index 0d586afcab5d..312b51708604 100644 --- a/etcd/src/etcd/etcd_client_test.cpp +++ b/etcd/src/etcd/etcd_client_test.cpp @@ -133,11 +133,11 @@ UTEST(Etcd, TestRange) { for (uint32_t i = 1; i <= range_size; ++i) { EXPECT_EQ(range_result[i - 1].value, fmt::format("some_value_{}", i)); } - + for (uint32_t i = 1; i <= range_size; ++i) { etcd_client_ptr->Delete(fmt::format("some_key_{}", i)); } - + EXPECT_TRUE(etcd_client_ptr->Range("some_key").empty()); } From 3f3c400ff639049a60fba6be6c2d923b3d65e0c5 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sat, 10 May 2025 02:07:38 +0300 Subject: [PATCH 26/42] iteration --- .../functional_tests/client/tests/conftest.py | 4 +- etcd/include/userver/etcd/exceptions.hpp | 6 ++ etcd/include/userver/etcd/watch_listener.hpp | 2 +- etcd/library.yaml | 2 +- etcd/src/etcd/client_impl.cpp | 60 +++++++++---------- etcd/src/etcd/client_impl.hpp | 2 +- etcd/src/etcd/etcd_responses.cpp | 23 +++++++ etcd/src/etcd/etcd_responses.hpp | 8 ++- 8 files changed, 67 insertions(+), 40 deletions(-) diff --git a/etcd/functional_tests/client/tests/conftest.py b/etcd/functional_tests/client/tests/conftest.py index 0485261ec44a..0c8418fa9565 100644 --- a/etcd/functional_tests/client/tests/conftest.py +++ b/etcd/functional_tests/client/tests/conftest.py @@ -54,10 +54,11 @@ async def mock(request): 'kvs': [{ 'key': base64.b64encode(request_key).decode('utf-8'), 'value': base64.b64encode(etcd_storage[request_key]).decode('utf-8'), + 'version': '2', }] }) else: - return mockserver.make_response(json={}) + return mockserver.make_response(json={'kvs': []}) values = [] for key, value in etcd_storage.items(): @@ -65,6 +66,7 @@ async def mock(request): values.append({ 'key': base64.b64encode(key).decode('utf-8'), 'value': base64.b64encode(value).decode('utf-8'), + 'version': '2', }) return mockserver.make_response(json={ diff --git a/etcd/include/userver/etcd/exceptions.hpp b/etcd/include/userver/etcd/exceptions.hpp index 2d4641dd5aac..c2fab3a76287 100644 --- a/etcd/include/userver/etcd/exceptions.hpp +++ b/etcd/include/userver/etcd/exceptions.hpp @@ -23,6 +23,12 @@ class EtcdRequestError : public EtcdError { using EtcdError::EtcdError; }; +/// @brief Error during parsing of etcd watch response +class EtcdWatchResponseParseError : public EtcdError { +public: + using EtcdError::EtcdError; +}; + } // namespace etcd USERVER_NAMESPACE_END diff --git a/etcd/include/userver/etcd/watch_listener.hpp b/etcd/include/userver/etcd/watch_listener.hpp index 8e202bb625a9..63f488cb6539 100644 --- a/etcd/include/userver/etcd/watch_listener.hpp +++ b/etcd/include/userver/etcd/watch_listener.hpp @@ -19,7 +19,7 @@ struct WatchListener final { /// Get an event from etcd if there was one, otherwise waits asynchronously until a next event occurs. /// If the coroutine, that was spawned by StartWatch method of etcd client, is finished or failed, GetEvent raises - /// exception Get Event uses Consumer::Pop method for getting the event. + /// EtcdError exception Get Event uses Consumer::Pop method for getting the event. KeyValueState GetEvent(); }; diff --git a/etcd/library.yaml b/etcd/library.yaml index 0988e0388455..0c3a52686ba2 100644 --- a/etcd/library.yaml +++ b/etcd/library.yaml @@ -3,7 +3,7 @@ project-alt-names: - yandex-userver-etcd maintainers: - Common components -description: Etcd driver +description: Etcd client libraries: - userver-core diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index fd4313f72151..e77e8cadf0f6 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -84,7 +84,7 @@ bool ShouldRetry(const http::StatusCode status_code) { void CheckResponseStatusCode(const http::StatusCode status_code, std::string_view body) { if (status_code < kMinGoodStatusCode || kMaxGoodStatusCode < status_code) { - throw EtcdError(fmt::format("Got bad status code from etcd: {}, body: {}", status_code, body)); + throw EtcdRequestError(fmt::format("Got bad status code from etcd: {}, body: {}", status_code, body)); } } @@ -96,25 +96,17 @@ ClientImpl::ClientImpl(clients::http::Client& http_client, ClientSettings settin : http_client_(http_client), settings_(settings) {} void ClientImpl::Put(std::string_view key, std::string_view value) { - try { PerformEtcdRequest(BuildPutUrl, BuildPutData(key, value)); - } catch (const clients::http::HttpClientException& exception) { - throw EtcdRequestError(fmt::format("Request to etcd was unsuccessful: {}", exception.what())); - } } void ClientImpl::Delete(std::string_view key) { - try { PerformEtcdRequest(BuildDeleteUrl, BuildDeleteData(key)); - } catch (const clients::http::HttpClientException& exception) { - throw EtcdRequestError(fmt::format("Request to etcd was unsuccessful: {}", exception.what())); - } } std::optional ClientImpl::Get(std::string_view key) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); - const auto range_response = formats::json::FromString(response->body()).As(); + const auto range_response = formats::json::FromString(response.body()).As(); const auto etcd_key = fmt::format("{}{}", kKeyPrefix, key); for (const auto& key_value_state : range_response.key_value_states) { if (key_value_state.key == etcd_key) { @@ -127,7 +119,7 @@ std::optional ClientImpl::Get(std::string_view key) { std::vector ClientImpl::Range(std::string_view key_prefix) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix, kLastPossibleKeyPrefix)); - const auto range_response = formats::json::FromString(response->body()).As(); + const auto range_response = formats::json::FromString(response.body()).As(); return range_response.key_value_states; } @@ -144,24 +136,34 @@ WatchListener ClientImpl::StartWatch(std::string_view key) { return WatchListener{queue->GetConsumer()}; } -std::shared_ptr +clients::http::Response ClientImpl::PerformEtcdRequest(const std::function& url_builder, std::string_view data) { auto endpoints = settings_.endpoints; utils::Shuffle(endpoints); - std::shared_ptr response_ptr; + std::optional maybe_response; for (const auto& endpoint : endpoints) { - response_ptr = http_client_.CreateRequest() + const auto response_ptr = http_client_.CreateRequest() .post(url_builder(endpoint), std::string{data}) .retry(settings_.attempts) .timeout(settings_.request_timeout_ms.count()) .perform(); - if (!ShouldRetry(response_ptr->status_code())) { - CheckResponseStatusCode(response_ptr->status_code(), response_ptr->body()); - return response_ptr; + if (response_ptr == nullptr) { + LOG_ERROR() << "Perform request returns nullptr"; + continue; + } + maybe_response = *(response_ptr); + const auto& response = maybe_response.value(); + if (!ShouldRetry(response.status_code())) { + CheckResponseStatusCode(response.status_code(), response.body()); + return response; } } - throw EtcdError("Failed to get Ok response from etcd with error: " + response_ptr->body()); + if (maybe_response.has_value()) { + throw EtcdRequestError("Failed to get Ok response from etcd with error: " + maybe_response.value().body()); + } else { + throw EtcdRequestError("Failed to get streamed response, number of etcd endpoints: " + endpoints.size()); + } } clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( @@ -199,23 +201,15 @@ void ClientImpl::WatchKeyChanges(const std::string key, concurrent::SpscQueue(); + } catch (const EtcdWatchResponseParseError& error) { + LOG_DEBUG() << "Couldnot parse etcd response: " << error; continue; } - for (const auto& event : watch_response["result"]["events"]) { - if (!event.HasMember("kv")) { - LOG_DEBUG() << "Event is not key value change, skipping"; - continue; - } - LOG_DEBUG() << "Got event with kv: " << event["kv"]; - if (!producer.Push(event["kv"].As())) { + for (auto event : etcd_watch_response.events) { + if (!producer.Push(std::move(event))) { LOG_ERROR() << "Could not push to queue, aborting task"; return; }; diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index 671050a9ec1a..f95461479695 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -27,7 +27,7 @@ class ClientImpl : public Client { WatchListener StartWatch(std::string_view key) override; private: - std::shared_ptr + clients::http::Response PerformEtcdRequest(const std::function& url_builder, std::string_view data); [[nodiscard]] clients::http::StreamedResponse diff --git a/etcd/src/etcd/etcd_responses.cpp b/etcd/src/etcd/etcd_responses.cpp index e5ab92d94dab..7f862993890a 100644 --- a/etcd/src/etcd/etcd_responses.cpp +++ b/etcd/src/etcd/etcd_responses.cpp @@ -1,5 +1,9 @@ #include +#include +#include +#include + USERVER_NAMESPACE_BEGIN namespace formats::parse { @@ -10,6 +14,25 @@ etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To) { + if (!value.HasMember("result")) { + throw etcd::EtcdWatchResponseParseError(fmt::format("No result in watch response: {}", formats::json::ToString(value))); + } + if (!value["result"].HasMember("events")) { + throw etcd::EtcdWatchResponseParseError(fmt::format("No events in watch response: {}", formats::json::ToString(value))); + } + etcd::EtcdWatchResponse etcd_watch_response; + for (const auto& event : value["result"]["events"]) { + if (!event.HasMember("kv")) { + LOG_DEBUG() << "Event is not key value change, skipping"; + continue; + } + etcd_watch_response.events.push_back(event["kv"].As()); + } + return etcd_watch_response; +} + + } // namespace formats::parse USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/etcd_responses.hpp b/etcd/src/etcd/etcd_responses.hpp index db7262b604ae..66174be704d8 100644 --- a/etcd/src/etcd/etcd_responses.hpp +++ b/etcd/src/etcd/etcd_responses.hpp @@ -7,12 +7,12 @@ USERVER_NAMESPACE_BEGIN namespace etcd { -struct EtcdRangeResponse { +struct EtcdRangeResponse final { std::vector key_value_states; }; -struct EtcdWatchResponse { - /* data */ +struct EtcdWatchResponse final { + std::vector events; }; } // namespace etcd @@ -21,6 +21,8 @@ namespace formats::parse { etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To); +etcd::EtcdWatchResponse Parse(const formats::json::Value& value, To); + } USERVER_NAMESPACE_END From 35b4ca7063f003d2a765d5a3db94cb29ee34ac34 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sat, 10 May 2025 03:52:15 +0300 Subject: [PATCH 27/42] iteration --- etcd/src/etcd/client_impl.cpp | 31 +++++++++++++------------------ etcd/src/etcd/etcd_responses.cpp | 14 ++++++++++---- etcd/src/etcd/etcd_responses.hpp | 2 +- etcd/src/etcd/key_value_state.cpp | 6 +++++- 4 files changed, 29 insertions(+), 24 deletions(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index e77e8cadf0f6..4d0b4787bdc7 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -35,8 +35,7 @@ const std::uint32_t kMaxRetryStatusCode = 599; const std::uint32_t kMinGoodStatusCode = 200; const std::uint32_t kMaxGoodStatusCode = 299; -std::string kKeyPrefix = "/etcd/"; -std::string kLastPossibleKeyPrefix = "/etcd0"; +const std::string kKeyPrefix = "/etcd/"; std::string BuildPutUrl(std::string_view service_url) { return fmt::format("{}/v3/kv/put", service_url); } @@ -54,11 +53,9 @@ std::string BuildRangeData(std::string_view key, const std::optional ClientImpl::Get(std::string_view key) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); const auto range_response = formats::json::FromString(response.body()).As(); - const auto etcd_key = fmt::format("{}{}", kKeyPrefix, key); for (const auto& key_value_state : range_response.key_value_states) { - if (key_value_state.key == etcd_key) { + if (key_value_state.key == key) { return key_value_state.value; } } @@ -117,7 +111,8 @@ std::optional ClientImpl::Get(std::string_view key) { } std::vector ClientImpl::Range(std::string_view key_prefix) { - auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix, kLastPossibleKeyPrefix)); + const auto response = + PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix, fmt::format("{}\xFF", key_prefix))); const auto range_response = formats::json::FromString(response.body()).As(); return range_response.key_value_states; @@ -144,10 +139,10 @@ ClientImpl::PerformEtcdRequest(const std::function maybe_response; for (const auto& endpoint : endpoints) { const auto response_ptr = http_client_.CreateRequest() - .post(url_builder(endpoint), std::string{data}) - .retry(settings_.attempts) - .timeout(settings_.request_timeout_ms.count()) - .perform(); + .post(url_builder(endpoint), std::string{data}) + .retry(settings_.attempts) + .timeout(settings_.request_timeout_ms.count()) + .perform(); if (response_ptr == nullptr) { LOG_ERROR() << "Perform request returns nullptr"; continue; @@ -201,7 +196,7 @@ void ClientImpl::WatchKeyChanges(const std::string key, concurrent::SpscQueue(); } catch (const EtcdWatchResponseParseError& error) { diff --git a/etcd/src/etcd/etcd_responses.cpp b/etcd/src/etcd/etcd_responses.cpp index 7f862993890a..90b69bb9628a 100644 --- a/etcd/src/etcd/etcd_responses.cpp +++ b/etcd/src/etcd/etcd_responses.cpp @@ -1,14 +1,17 @@ #include -#include #include #include +#include USERVER_NAMESPACE_BEGIN namespace formats::parse { etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To) { + if (!value.HasMember("kvs")) { + return etcd::EtcdRangeResponse{}; + } return etcd::EtcdRangeResponse{ /* .key_value_states = */ value["kvs"].As>(), }; @@ -16,10 +19,14 @@ etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To) { if (!value.HasMember("result")) { - throw etcd::EtcdWatchResponseParseError(fmt::format("No result in watch response: {}", formats::json::ToString(value))); + throw etcd::EtcdWatchResponseParseError( + fmt::format("No result in watch response: {}", formats::json::ToString(value)) + ); } if (!value["result"].HasMember("events")) { - throw etcd::EtcdWatchResponseParseError(fmt::format("No events in watch response: {}", formats::json::ToString(value))); + throw etcd::EtcdWatchResponseParseError( + fmt::format("No events in watch response: {}", formats::json::ToString(value)) + ); } etcd::EtcdWatchResponse etcd_watch_response; for (const auto& event : value["result"]["events"]) { @@ -32,7 +39,6 @@ etcd::EtcdWatchResponse Parse(const formats::json::Value& value, To); -} +} // namespace formats::parse USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/key_value_state.cpp b/etcd/src/etcd/key_value_state.cpp index cd449d4cdd45..35a8f60257a3 100644 --- a/etcd/src/etcd/key_value_state.cpp +++ b/etcd/src/etcd/key_value_state.cpp @@ -5,11 +5,15 @@ USERVER_NAMESPACE_BEGIN +namespace { +const std::string kKeyPrefix = "/etcd/"; +} + namespace formats::parse { etcd::KeyValueState Parse(const formats::json::Value& value, To) { return etcd::KeyValueState{ - /* .key = */ crypto::base64::Base64Decode(value["key"].As()), + /* .key = */ crypto::base64::Base64Decode(value["key"].As()).substr(kKeyPrefix.size()), /* .value = */ crypto::base64::Base64Decode(value["value"].As()), /* .version = */ std::stoi(value["version"].As()), }; From e44ad3ff8875cfc4225f09d293bfd618ff88e21f Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sat, 10 May 2025 14:40:15 +0300 Subject: [PATCH 28/42] iteration --- etcd/src/etcd/client_impl.cpp | 8 ++++---- etcd/src/etcd/settings.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 4d0b4787bdc7..c14b9dbfb298 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -155,9 +155,9 @@ ClientImpl::PerformEtcdRequest(const std::function) { return etcd::ClientSettings{ - .endpoints = config["endpoints"].As>(), - .attempts = config["attempts"].As(etcd::kDefaultAttempts), - .request_timeout_ms = config["request_timeout_ms"].As(etcd::kDefaultRequestTimeout), - .watch_timeout_ms = config["watch_timeout_ms"].As(etcd::kDefaultWatchTimeout), + /* .endpoints = */ config["endpoints"].As>(), + /* .attempts = */ config["attempts"].As(etcd::kDefaultAttempts), + /* .request_timeout_ms = */ config["request_timeout_ms"].As(etcd::kDefaultRequestTimeout), + /* .watch_timeout_ms = */ config["watch_timeout_ms"].As(etcd::kDefaultWatchTimeout), }; } From c9b0675e247d5c149c3cacae7dcdf03d25b27a16 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sat, 10 May 2025 17:25:45 +0300 Subject: [PATCH 29/42] fix namespaces --- etcd/functional_tests/client/etcd_service.cpp | 54 ++++++++++--------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/etcd/functional_tests/client/etcd_service.cpp b/etcd/functional_tests/client/etcd_service.cpp index 6791e29306ec..bc2436dbdc70 100644 --- a/etcd/functional_tests/client/etcd_service.cpp +++ b/etcd/functional_tests/client/etcd_service.cpp @@ -1,3 +1,5 @@ +#include + #include #include @@ -13,41 +15,41 @@ namespace { -class HandlerV1Get final : public userver::server::handlers::HttpHandlerBase { +class HandlerV1Get final : public server::handlers::HttpHandlerBase { public: static constexpr std::string_view kName = "handler-v1-get"; HandlerV1Get( - const userver::components::ComponentConfig& config, - const userver::components::ComponentContext& component_context + const components::ComponentConfig& config, + const components::ComponentContext& component_context ) : HttpHandlerBase(config, component_context), - etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} + etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} std::string - HandleRequestThrow(const userver::server::http::HttpRequest& request, userver::server::request::RequestContext&) + HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) const override { const auto maybe_value = etcd_client_ptr_->Get(request.GetArg("key")); return maybe_value.value_or("No value"); } private: - userver::etcd::ClientPtr etcd_client_ptr_; + etcd::ClientPtr etcd_client_ptr_; }; -class HandlerV1Put final : public userver::server::handlers::HttpHandlerBase { +class HandlerV1Put final : public server::handlers::HttpHandlerBase { public: static constexpr std::string_view kName = "handler-v1-put"; HandlerV1Put( - const userver::components::ComponentConfig& config, - const userver::components::ComponentContext& component_context + const components::ComponentConfig& config, + const components::ComponentContext& component_context ) : HttpHandlerBase(config, component_context), - etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} + etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} std::string - HandleRequestThrow(const userver::server::http::HttpRequest& request, userver::server::request::RequestContext&) + HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) const override { etcd_client_ptr_->Put(request.GetArg("key"), request.GetArg("value")); @@ -55,22 +57,22 @@ class HandlerV1Put final : public userver::server::handlers::HttpHandlerBase { } private: - userver::etcd::ClientPtr etcd_client_ptr_; + etcd::ClientPtr etcd_client_ptr_; }; -class HandlerV1Watch final : public userver::server::handlers::HttpHandlerBase { +class HandlerV1Watch final : public server::handlers::HttpHandlerBase { public: static constexpr std::string_view kName = "handler-v1-watch"; HandlerV1Watch( - const userver::components::ComponentConfig& config, - const userver::components::ComponentContext& component_context + const components::ComponentConfig& config, + const components::ComponentContext& component_context ) : HttpHandlerBase(config, component_context), - etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} + etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} std::string - HandleRequestThrow(const userver::server::http::HttpRequest& request, userver::server::request::RequestContext&) + HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) const override { const auto key = request.GetArg("key"); const auto maybe_original_value = etcd_client_ptr_->Get(key); @@ -81,22 +83,22 @@ class HandlerV1Watch final : public userver::server::handlers::HttpHandlerBase { } private: - userver::etcd::ClientPtr etcd_client_ptr_; + etcd::ClientPtr etcd_client_ptr_; }; } // namespace int main(int argc, char* argv[]) { - auto component_list = userver::components::MinimalServerComponentList() - .Append() - .Append() - .Append() - .Append() - .Append() - .Append() + auto component_list = components::MinimalServerComponentList() + .Append() + .Append() + .Append() + .Append() + .Append() + .Append() .Append() .Append() .Append(); - return userver::utils::DaemonMain(argc, argv, component_list); + return utils::DaemonMain(argc, argv, component_list); } From 31e5baf8c57e4a127fee2101e906864cf3ecc5b9 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Sun, 11 May 2025 00:07:23 +0300 Subject: [PATCH 30/42] iteration --- etcd/functional_tests/client/etcd_service.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etcd/functional_tests/client/etcd_service.cpp b/etcd/functional_tests/client/etcd_service.cpp index bc2436dbdc70..c65b8b2e700a 100644 --- a/etcd/functional_tests/client/etcd_service.cpp +++ b/etcd/functional_tests/client/etcd_service.cpp @@ -94,8 +94,8 @@ int main(int argc, char* argv[]) { .Append() .Append() .Append() - .Append() .Append() + .Append() .Append() .Append() .Append(); From 2719aff6e8b6583af088652938ae091f0a8622f6 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Thu, 15 May 2025 00:15:41 +0300 Subject: [PATCH 31/42] format --- etcd/functional_tests/client/etcd_service.cpp | 24 +++++-------------- etcd/src/etcd/client_impl.cpp | 14 +++++++---- etcd/src/etcd/settings.cpp | 4 +++- 3 files changed, 18 insertions(+), 24 deletions(-) diff --git a/etcd/functional_tests/client/etcd_service.cpp b/etcd/functional_tests/client/etcd_service.cpp index c65b8b2e700a..ed402b818da4 100644 --- a/etcd/functional_tests/client/etcd_service.cpp +++ b/etcd/functional_tests/client/etcd_service.cpp @@ -19,15 +19,11 @@ class HandlerV1Get final : public server::handlers::HttpHandlerBase { public: static constexpr std::string_view kName = "handler-v1-get"; - HandlerV1Get( - const components::ComponentConfig& config, - const components::ComponentContext& component_context - ) + HandlerV1Get(const components::ComponentConfig& config, const components::ComponentContext& component_context) : HttpHandlerBase(config, component_context), etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} - std::string - HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) + std::string HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) const override { const auto maybe_value = etcd_client_ptr_->Get(request.GetArg("key")); return maybe_value.value_or("No value"); @@ -41,15 +37,11 @@ class HandlerV1Put final : public server::handlers::HttpHandlerBase { public: static constexpr std::string_view kName = "handler-v1-put"; - HandlerV1Put( - const components::ComponentConfig& config, - const components::ComponentContext& component_context - ) + HandlerV1Put(const components::ComponentConfig& config, const components::ComponentContext& component_context) : HttpHandlerBase(config, component_context), etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} - std::string - HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) + std::string HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) const override { etcd_client_ptr_->Put(request.GetArg("key"), request.GetArg("value")); @@ -64,15 +56,11 @@ class HandlerV1Watch final : public server::handlers::HttpHandlerBase { public: static constexpr std::string_view kName = "handler-v1-watch"; - HandlerV1Watch( - const components::ComponentConfig& config, - const components::ComponentContext& component_context - ) + HandlerV1Watch(const components::ComponentConfig& config, const components::ComponentContext& component_context) : HttpHandlerBase(config, component_context), etcd_client_ptr_(component_context.FindComponent("etcd-client").GetClient()) {} - std::string - HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) + std::string HandleRequestThrow(const server::http::HttpRequest& request, server::request::RequestContext&) const override { const auto key = request.GetArg("key"); const auto maybe_original_value = etcd_client_ptr_->Get(key); diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index c14b9dbfb298..1a3cd23db18e 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -155,9 +155,13 @@ ClientImpl::PerformEtcdRequest(const std::function>(), /* .attempts = */ config["attempts"].As(etcd::kDefaultAttempts), - /* .request_timeout_ms = */ config["request_timeout_ms"].As(etcd::kDefaultRequestTimeout), + /* .request_timeout_ms = */ + config["request_timeout_ms"] + .As(etcd::kDefaultRequestTimeout), /* .watch_timeout_ms = */ config["watch_timeout_ms"].As(etcd::kDefaultWatchTimeout), }; } From 0b7928f7e46e510c3840a5427978f7863be094dc Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Thu, 15 May 2025 23:03:58 +0300 Subject: [PATCH 32/42] Add retries for watch --- etcd/src/etcd/client_impl.cpp | 38 ++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 1a3cd23db18e..22c87afc4d31 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -196,23 +196,29 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( } void ClientImpl::WatchKeyChanges(const std::string key, concurrent::SpscQueue::Producer producer) { - LOG_DEBUG() << "Start whatching key changes"; - auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, BuildWatchData(key)); - std::string body_part; - while (stream_response.ReadChunk(body_part, engine::Deadline())) { - EtcdWatchResponse etcd_watch_response; - try { - etcd_watch_response = formats::json::FromString(body_part).As(); - } catch (const EtcdWatchResponseParseError& error) { - LOG_DEBUG() << "Couldnot parse etcd response: " << error; - continue; - } - for (auto event : etcd_watch_response.events) { - if (!producer.Push(std::move(event))) { - LOG_ERROR() << "Could not push to queue, aborting task"; - return; - }; + const auto watch_data = BuildWatchData(key); + + while (true) + { + LOG_DEBUG() << "Start whatching key changes"; + auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, watch_data); + std::string body_part; + while (stream_response.ReadChunk(body_part, engine::Deadline())) { + EtcdWatchResponse etcd_watch_response; + try { + etcd_watch_response = formats::json::FromString(body_part).As(); + } catch (const EtcdWatchResponseParseError& error) { + LOG_DEBUG() << "Couldnot parse etcd response: " << error; + continue; + } + for (auto event : etcd_watch_response.events) { + if (!producer.Push(std::move(event))) { + LOG_ERROR() << "Could not push to queue, aborting task"; + return; + }; + } } + LOG_ERROR() << "Could not read chunk from stream response"; } } From 95a1928010c6d2927ce0bdd2f2b9a1220df24c37 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Thu, 15 May 2025 23:07:44 +0300 Subject: [PATCH 33/42] fix WatchListener --- etcd/include/userver/etcd/watch_listener.hpp | 13 ++++++++----- etcd/src/etcd/watch_listener.cpp | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/etcd/include/userver/etcd/watch_listener.hpp b/etcd/include/userver/etcd/watch_listener.hpp index 63f488cb6539..6faca72411d3 100644 --- a/etcd/include/userver/etcd/watch_listener.hpp +++ b/etcd/include/userver/etcd/watch_listener.hpp @@ -14,13 +14,16 @@ USERVER_NAMESPACE_BEGIN namespace etcd { /// @brief Struct that return value change events in etcd -struct WatchListener final { - concurrent::SpscQueue::Consumer consumer; +class WatchListener final { +public: + WatchListener(concurrent::SpscQueue::Consumer&& consumer); - /// Get an event from etcd if there was one, otherwise waits asynchronously until a next event occurs. - /// If the coroutine, that was spawned by StartWatch method of etcd client, is finished or failed, GetEvent raises - /// EtcdError exception Get Event uses Consumer::Pop method for getting the event. + /// @brief Get an event from etcd if there was one, otherwise waits asynchronously until a next event occurs. + /// If the event producing coroutine finished or failed, GetEvent raises EtcdError exception KeyValueState GetEvent(); + +private: + concurrent::SpscQueue::Consumer consumer_; }; } // namespace etcd diff --git a/etcd/src/etcd/watch_listener.cpp b/etcd/src/etcd/watch_listener.cpp index 57e3631d0fd0..da5e20774d16 100644 --- a/etcd/src/etcd/watch_listener.cpp +++ b/etcd/src/etcd/watch_listener.cpp @@ -7,9 +7,12 @@ USERVER_NAMESPACE_BEGIN namespace etcd { +WatchListener::WatchListener(concurrent::SpscQueue::Consumer&& consumer) : +consumer_(std::move(consumer)) {} + KeyValueState WatchListener::GetEvent() { KeyValueState event; - if (!consumer.Pop(event)) { + if (!consumer_.Pop(event)) { throw EtcdError("Consumer pop failed while trying to get etcd key-value event"); } return event; From 62acc2974ac2c23fa54ce8921e95d83e80049035 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Thu, 15 May 2025 23:08:04 +0300 Subject: [PATCH 34/42] add chaotic --- etcd/CMakeLists.txt | 14 ++++++++++++++ etcd/include/userver/etcd/client.hpp | 2 ++ etcd/schemas/types.yaml | 16 ++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 etcd/schemas/types.yaml diff --git a/etcd/CMakeLists.txt b/etcd/CMakeLists.txt index 9b14fe8602ba..b94a0168d9b9 100644 --- a/etcd/CMakeLists.txt +++ b/etcd/CMakeLists.txt @@ -1,8 +1,22 @@ project(userver-etcd CXX) +file(GLOB_RECURSE SCHEMAS ${CMAKE_CURRENT_SOURCE_DIR}/schemas/*.yaml) +userver_target_generate_chaotic(${PROJECT_NAME}-chgen + GENERATE_SERIALIZERS + LAYOUT + "/components/schemas/([^/]*)/=etcd_schemas::{0}" + OUTPUT_DIR + ${CMAKE_CURRENT_BINARY_DIR}/src + SCHEMAS + ${SCHEMAS} + RELATIVE_TO + ${CMAKE_CURRENT_SOURCE_DIR} +) + userver_module(etcd SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" UTEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/*_test.cpp" + LINK_LIBRARIES ${PROJECT_NAME}-chgen ) if (USERVER_BUILD_TESTS) diff --git a/etcd/include/userver/etcd/client.hpp b/etcd/include/userver/etcd/client.hpp index 1850440c8808..aa5a1a743c32 100644 --- a/etcd/include/userver/etcd/client.hpp +++ b/etcd/include/userver/etcd/client.hpp @@ -17,6 +17,8 @@ #include #include +#include + USERVER_NAMESPACE_BEGIN namespace etcd { diff --git a/etcd/schemas/types.yaml b/etcd/schemas/types.yaml new file mode 100644 index 000000000000..40cd8ae2022a --- /dev/null +++ b/etcd/schemas/types.yaml @@ -0,0 +1,16 @@ +components: + schemas: + KeyValueState: + type: object + additionalProperties: false + required: + - key + - value + - version + properties: + key: + type: string + value: + type: string + version: + type: string From 968930c81c71cd0c69348363f208c47d4f9c3bb7 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 02:55:40 +0300 Subject: [PATCH 35/42] iteration --- chaotic/chaotic/back/cpp/translator.py | 2 +- .../chaotic/io/crypto/base64/string64.hpp | 4 +- .../integration_tests/tests/render/simple.cpp | 2 +- chaotic/tests/back/cpp/test_tr_string.py | 2 +- etcd/include/userver/etcd/key_value_state.hpp | 9 ---- etcd/include/userver/etcd/settings.hpp | 8 ++-- etcd/schemas/etcd.yaml | 10 +++++ etcd/schemas/types.yaml | 6 ++- etcd/src/etcd/client_impl.cpp | 43 +++++++++++++++---- etcd/src/etcd/etcd_client_test.cpp | 14 +++--- etcd/src/etcd/etcd_responses.cpp | 11 +---- etcd/src/etcd/etcd_responses.hpp | 11 ++--- etcd/src/etcd/key_value_state.cpp | 24 ----------- etcd/src/etcd/settings.cpp | 16 +++---- etcd/src/etcd/watch_listener.cpp | 4 +- 15 files changed, 79 insertions(+), 87 deletions(-) create mode 100644 etcd/schemas/etcd.yaml delete mode 100644 etcd/src/etcd/key_value_state.cpp diff --git a/chaotic/chaotic/back/cpp/translator.py b/chaotic/chaotic/back/cpp/translator.py index 08ab028bb6e9..d637fae4daad 100644 --- a/chaotic/chaotic/back/cpp/translator.py +++ b/chaotic/chaotic/back/cpp/translator.py @@ -444,7 +444,7 @@ def _gen_string( if schema.format and schema.format != types.StringFormat.BINARY: if schema.format == types.StringFormat.BYTE: - format_cpp_type = 'crypto::base64::String64' + format_cpp_type = '::crypto::base64::String64' elif schema.format == types.StringFormat.UUID: format_cpp_type = 'boost::uuids::uuid' elif schema.format == types.StringFormat.DATE: diff --git a/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp b/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp index 7384dbebce64..75ed4e423938 100644 --- a/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp +++ b/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp @@ -18,9 +18,9 @@ USERVER_NAMESPACE_BEGIN namespace chaotic::convert { -crypto::base64::String64 Convert(const std::string& str, chaotic::convert::To); +::crypto::base64::String64 Convert(const std::string& str, chaotic::convert::To<::crypto::base64::String64>); -std::string Convert(const crypto::base64::String64& str64, chaotic::convert::To); +std::string Convert(const ::crypto::base64::String64& str64, chaotic::convert::To); } // namespace chaotic::convert diff --git a/chaotic/integration_tests/tests/render/simple.cpp b/chaotic/integration_tests/tests/render/simple.cpp index 754720c7c8c3..817d660b13a9 100644 --- a/chaotic/integration_tests/tests/render/simple.cpp +++ b/chaotic/integration_tests/tests/render/simple.cpp @@ -369,7 +369,7 @@ TEST(Simple, Uuid) { } TEST(SIMPLE, String64) { - auto str64 = crypto::base64::String64{"hello, userver!"}; + auto str64 = ::crypto::base64::String64{"hello, userver!"}; auto obj = ns::ObjectString64{str64}; auto str = Serialize(obj, formats::serialize::To())["value"].As(); diff --git a/chaotic/tests/back/cpp/test_tr_string.py b/chaotic/tests/back/cpp/test_tr_string.py index 28617124637d..ea86ef085b57 100644 --- a/chaotic/tests/back/cpp/test_tr_string.py +++ b/chaotic/tests/back/cpp/test_tr_string.py @@ -54,7 +54,7 @@ def test_byte(simple_gen): assert types == { '::type': cpp_types.CppStringWithFormat( raw_cpp_type=type_name.TypeName('std::string'), - format_cpp_type='crypto::base64::String64', + format_cpp_type='::crypto::base64::String64', user_cpp_type=None, json_schema=None, nullable=False, diff --git a/etcd/include/userver/etcd/key_value_state.hpp b/etcd/include/userver/etcd/key_value_state.hpp index b699cbcada40..4a9a5fc7d12f 100644 --- a/etcd/include/userver/etcd/key_value_state.hpp +++ b/etcd/include/userver/etcd/key_value_state.hpp @@ -5,9 +5,6 @@ #include -#include -#include - USERVER_NAMESPACE_BEGIN namespace etcd { @@ -21,10 +18,4 @@ struct KeyValueState final { } // namespace etcd -namespace formats::parse { - -etcd::KeyValueState Parse(const formats::json::Value& value, To); - -} - USERVER_NAMESPACE_END diff --git a/etcd/include/userver/etcd/settings.hpp b/etcd/include/userver/etcd/settings.hpp index 322ebc7105b4..7a8b147a2674 100644 --- a/etcd/include/userver/etcd/settings.hpp +++ b/etcd/include/userver/etcd/settings.hpp @@ -16,14 +16,14 @@ namespace etcd { /// @brief Etcd client settigs struct struct ClientSettings final { // Etcd endpoints to which client make HTTP requests - const std::vector endpoints; + std::vector endpoints; // Number of attempts to each endpoint, on failed attempts client randomly moves to another endpoint - const std::uint32_t attempts; + std::uint32_t attempts; // Timeout for all HTTP requests to etcd except watch request - const std::chrono::microseconds request_timeout_ms; + std::chrono::microseconds request_timeout_ms; // Timeout for watch HTTP request. It's a stremed request, so it is used also as a connection timeout, so it should // not be too short - const std::chrono::microseconds watch_timeout_ms; + std::chrono::microseconds watch_timeout_ms; }; } // namespace etcd diff --git a/etcd/schemas/etcd.yaml b/etcd/schemas/etcd.yaml new file mode 100644 index 000000000000..5a04432a1ec4 --- /dev/null +++ b/etcd/schemas/etcd.yaml @@ -0,0 +1,10 @@ +components: + schemas: + EtcdRangeResponse: + type: object + additionalProperties: true + properties: + kvs: + type: array + items: + $ref: 'types.yaml#/components/schemas/RawKeyValueState' diff --git a/etcd/schemas/types.yaml b/etcd/schemas/types.yaml index 40cd8ae2022a..c69dff479c69 100644 --- a/etcd/schemas/types.yaml +++ b/etcd/schemas/types.yaml @@ -1,8 +1,8 @@ components: schemas: - KeyValueState: + RawKeyValueState: type: object - additionalProperties: false + additionalProperties: true required: - key - value @@ -10,7 +10,9 @@ components: properties: key: type: string + format: byte value: type: string + format: byte version: type: string diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 22c87afc4d31..7d3d993d0e6a 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -21,6 +21,8 @@ #include #include +#include + #include USERVER_NAMESPACE_BEGIN @@ -37,6 +39,15 @@ const std::uint32_t kMaxGoodStatusCode = 299; const std::string kKeyPrefix = "/etcd/"; +KeyValueState ConvertRawKeyValueState(const etcd_schemas::RawKeyValueState& raw_key_value_state) { + KeyValueState key_value_state; + key_value_state.key = chaotic::convert::Convert(raw_key_value_state.key, chaotic::convert::To()) + .substr(kKeyPrefix.size()); + key_value_state.value = chaotic::convert::Convert(raw_key_value_state.value, chaotic::convert::To()); + key_value_state.version = std::stoi(raw_key_value_state.version); + return key_value_state; +} + std::string BuildPutUrl(std::string_view service_url) { return fmt::format("{}/v3/kv/put", service_url); } std::string BuildPutData(std::string_view key, std::string_view value) { @@ -101,8 +112,12 @@ void ClientImpl::Delete(std::string_view key) { PerformEtcdRequest(BuildDeleteUr std::optional ClientImpl::Get(std::string_view key) { auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key)); - const auto range_response = formats::json::FromString(response.body()).As(); - for (const auto& key_value_state : range_response.key_value_states) { + const auto maybe_range_response = formats::json::FromString(response.body()).As(); + if (!maybe_range_response.kvs.has_value()) { + return std::nullopt; + } + for (const auto& raw_key_value_state : maybe_range_response.kvs.value()) { + const auto key_value_state = ConvertRawKeyValueState(raw_key_value_state); if (key_value_state.key == key) { return key_value_state.value; } @@ -114,8 +129,18 @@ std::vector ClientImpl::Range(std::string_view key_prefix) { const auto response = PerformEtcdRequest(BuildRangeUrl, BuildRangeData(key_prefix, fmt::format("{}\xFF", key_prefix))); - const auto range_response = formats::json::FromString(response.body()).As(); - return range_response.key_value_states; + const auto maybe_range_response = formats::json::FromString(response.body()).As(); + if (!maybe_range_response.kvs.has_value()) { + return {}; + } + + std::vector range_result; + range_result.reserve(maybe_range_response.kvs.value().size()); + for (const auto& raw_key_value_state : maybe_range_response.kvs.value()) { + const auto key_value_state = ConvertRawKeyValueState(raw_key_value_state); + range_result.push_back(key_value_state); + } + return range_result; } WatchListener ClientImpl::StartWatch(std::string_view key) { @@ -197,9 +222,8 @@ clients::http::StreamedResponse ClientImpl::PerformStreamedEtcdRequest( void ClientImpl::WatchKeyChanges(const std::string key, concurrent::SpscQueue::Producer producer) { const auto watch_data = BuildWatchData(key); - - while (true) - { + + while (true) { LOG_DEBUG() << "Start whatching key changes"; auto stream_response = PerformStreamedEtcdRequest(BuildWatchUrl, watch_data); std::string body_part; @@ -211,8 +235,9 @@ void ClientImpl::WatchKeyChanges(const std::string key, concurrent::SpscQueue storage; + static std::map storage; EXPECT_EQ(request.method, clients::http::HttpMethod::kPost); const auto request_body = formats::json::FromString(request.body); @@ -31,11 +31,12 @@ utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServer if (key_value_iterator != storage.end()) { new_version = (key_value_iterator->second).version + 1; } - storage[key] = etcd::KeyValueState{ - /* .key = */ key, - /* .value = */ value, - /* .version = */ new_version, - }; + KeyValueState key_value_state; + key_value_state.key = key; + key_value_state.value = value; + key_value_state.version = new_version; + storage[key] = key_value_state; + } else if (request.path == "/v3/kv/range" && !request_body.HasMember("range_end")) { const auto value_iterator = storage.find(key); response_body_value_builder["kvs"] = formats::json::MakeArray(); @@ -130,6 +131,7 @@ UTEST(Etcd, TestRange) { std::sort(range_result.begin(), range_result.end(), [](const etcd::KeyValueState& l, const etcd::KeyValueState& r) { return l.value < r.value; }); + for (uint32_t i = 1; i <= range_size; ++i) { EXPECT_EQ(range_result[i - 1].value, fmt::format("some_value_{}", i)); } diff --git a/etcd/src/etcd/etcd_responses.cpp b/etcd/src/etcd/etcd_responses.cpp index 90b69bb9628a..04f2d13b265b 100644 --- a/etcd/src/etcd/etcd_responses.cpp +++ b/etcd/src/etcd/etcd_responses.cpp @@ -8,15 +8,6 @@ USERVER_NAMESPACE_BEGIN namespace formats::parse { -etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To) { - if (!value.HasMember("kvs")) { - return etcd::EtcdRangeResponse{}; - } - return etcd::EtcdRangeResponse{ - /* .key_value_states = */ value["kvs"].As>(), - }; -} - etcd::EtcdWatchResponse Parse(const formats::json::Value& value, To) { if (!value.HasMember("result")) { throw etcd::EtcdWatchResponseParseError( @@ -34,7 +25,7 @@ etcd::EtcdWatchResponse Parse(const formats::json::Value& value, To()); + etcd_watch_response.raw_key_value_states.push_back(event["kv"].As()); } return etcd_watch_response; } diff --git a/etcd/src/etcd/etcd_responses.hpp b/etcd/src/etcd/etcd_responses.hpp index 76af525557c5..b0eae0df83ab 100644 --- a/etcd/src/etcd/etcd_responses.hpp +++ b/etcd/src/etcd/etcd_responses.hpp @@ -1,26 +1,21 @@ #pragma once -#include #include +#include + USERVER_NAMESPACE_BEGIN namespace etcd { -struct EtcdRangeResponse final { - std::vector key_value_states; -}; - struct EtcdWatchResponse final { - std::vector events; + std::vector raw_key_value_states; }; } // namespace etcd namespace formats::parse { -etcd::EtcdRangeResponse Parse(const formats::json::Value& value, To); - etcd::EtcdWatchResponse Parse(const formats::json::Value& value, To); } // namespace formats::parse diff --git a/etcd/src/etcd/key_value_state.cpp b/etcd/src/etcd/key_value_state.cpp deleted file mode 100644 index 35a8f60257a3..000000000000 --- a/etcd/src/etcd/key_value_state.cpp +++ /dev/null @@ -1,24 +0,0 @@ -#include - -#include -#include - -USERVER_NAMESPACE_BEGIN - -namespace { -const std::string kKeyPrefix = "/etcd/"; -} - -namespace formats::parse { - -etcd::KeyValueState Parse(const formats::json::Value& value, To) { - return etcd::KeyValueState{ - /* .key = */ crypto::base64::Base64Decode(value["key"].As()).substr(kKeyPrefix.size()), - /* .value = */ crypto::base64::Base64Decode(value["value"].As()), - /* .version = */ std::stoi(value["version"].As()), - }; -} - -} // namespace formats::parse - -USERVER_NAMESPACE_END diff --git a/etcd/src/etcd/settings.cpp b/etcd/src/etcd/settings.cpp index 372dea04998d..48943493831d 100644 --- a/etcd/src/etcd/settings.cpp +++ b/etcd/src/etcd/settings.cpp @@ -20,14 +20,14 @@ constexpr std::chrono::milliseconds kDefaultWatchTimeout{1'000'000}; namespace formats::parse { etcd::ClientSettings Parse(const yaml_config::YamlConfig& config, To) { - return etcd::ClientSettings{ - /* .endpoints = */ config["endpoints"].As>(), - /* .attempts = */ config["attempts"].As(etcd::kDefaultAttempts), - /* .request_timeout_ms = */ - config["request_timeout_ms"] - .As(etcd::kDefaultRequestTimeout), - /* .watch_timeout_ms = */ config["watch_timeout_ms"].As(etcd::kDefaultWatchTimeout), - }; + etcd::ClientSettings client_settings; + client_settings.endpoints = config["endpoints"].As>(); + client_settings.attempts = config["attempts"].As(etcd::kDefaultAttempts); + client_settings.request_timeout_ms = + config["request_timeout_ms"].As(etcd::kDefaultRequestTimeout); + client_settings.watch_timeout_ms = + config["watch_timeout_ms"].As(etcd::kDefaultWatchTimeout); + return client_settings; } } // namespace formats::parse diff --git a/etcd/src/etcd/watch_listener.cpp b/etcd/src/etcd/watch_listener.cpp index da5e20774d16..926d2e6d9dc1 100644 --- a/etcd/src/etcd/watch_listener.cpp +++ b/etcd/src/etcd/watch_listener.cpp @@ -7,8 +7,8 @@ USERVER_NAMESPACE_BEGIN namespace etcd { -WatchListener::WatchListener(concurrent::SpscQueue::Consumer&& consumer) : -consumer_(std::move(consumer)) {} +WatchListener::WatchListener(concurrent::SpscQueue::Consumer&& consumer) + : consumer_(std::move(consumer)) {} KeyValueState WatchListener::GetEvent() { KeyValueState event; From 07388942efb72c21c21476531b1720f9b8734c13 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 03:20:10 +0300 Subject: [PATCH 36/42] fix convertion --- etcd/src/etcd/client_impl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 7d3d993d0e6a..52bf23de5f53 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -41,9 +41,9 @@ const std::string kKeyPrefix = "/etcd/"; KeyValueState ConvertRawKeyValueState(const etcd_schemas::RawKeyValueState& raw_key_value_state) { KeyValueState key_value_state; - key_value_state.key = chaotic::convert::Convert(raw_key_value_state.key, chaotic::convert::To()) + key_value_state.key = raw_key_value_state.key.GetUnderlying() .substr(kKeyPrefix.size()); - key_value_state.value = chaotic::convert::Convert(raw_key_value_state.value, chaotic::convert::To()); + key_value_state.value = raw_key_value_state.value.GetUnderlying(); key_value_state.version = std::stoi(raw_key_value_state.version); return key_value_state; } From 77662efdc0d6259de15c5d8be417fbedba230888 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 03:20:36 +0300 Subject: [PATCH 37/42] fix convertion --- etcd/src/etcd/client_impl.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 52bf23de5f53..d41b5b05c6df 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -41,8 +41,7 @@ const std::string kKeyPrefix = "/etcd/"; KeyValueState ConvertRawKeyValueState(const etcd_schemas::RawKeyValueState& raw_key_value_state) { KeyValueState key_value_state; - key_value_state.key = raw_key_value_state.key.GetUnderlying() - .substr(kKeyPrefix.size()); + key_value_state.key = raw_key_value_state.key.GetUnderlying().substr(kKeyPrefix.size()); key_value_state.value = raw_key_value_state.value.GetUnderlying(); key_value_state.version = std::stoi(raw_key_value_state.version); return key_value_state; From cca6434bd5e3b9446a23530a8e08967393be1058 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 13:41:21 +0300 Subject: [PATCH 38/42] fix namespace --- etcd/src/etcd/etcd_client_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etcd/src/etcd/etcd_client_test.cpp b/etcd/src/etcd/etcd_client_test.cpp index b39a1a32ef20..779312d63bc6 100644 --- a/etcd/src/etcd/etcd_client_test.cpp +++ b/etcd/src/etcd/etcd_client_test.cpp @@ -17,7 +17,7 @@ USERVER_NAMESPACE_BEGIN namespace { utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServerMock::HttpRequest& request) { - static std::map storage; + static std::map storage; EXPECT_EQ(request.method, clients::http::HttpMethod::kPost); const auto request_body = formats::json::FromString(request.body); @@ -31,7 +31,7 @@ utest::HttpServerMock::HttpResponse EtcdRequestProcessor(const utest::HttpServer if (key_value_iterator != storage.end()) { new_version = (key_value_iterator->second).version + 1; } - KeyValueState key_value_state; + etcd::KeyValueState key_value_state; key_value_state.key = key; key_value_state.value = value; key_value_state.version = new_version; From bc49574a4fa7550520e1e0ed99919a4f05595ece Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 14:04:25 +0300 Subject: [PATCH 39/42] fix namespaces for string64 --- .mapping.json | 2 +- chaotic/chaotic/back/cpp/translator.py | 2 +- .../{ => userver}/crypto/base64/string64.hpp | 8 ++++---- .../integration_tests/tests/render/simple.cpp | 2 +- .../src/chaotic/io/crypto/base64/string64.cpp | 8 ++++---- .../io/userver/crypto/base64/string64.cpp | 19 +++++++++++++++++++ chaotic/tests/back/cpp/test_tr_string.py | 2 +- 7 files changed, 31 insertions(+), 12 deletions(-) rename chaotic/include/userver/chaotic/io/{ => userver}/crypto/base64/string64.hpp (68%) create mode 100644 chaotic/src/chaotic/io/userver/crypto/base64/string64.cpp diff --git a/.mapping.json b/.mapping.json index 19cc4b6b105e..afe241e9c1f9 100644 --- a/.mapping.json +++ b/.mapping.json @@ -217,7 +217,7 @@ "chaotic/include/userver/chaotic/dynamic_config_variable_bundle.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/dynamic_config_variable_bundle.hpp", "chaotic/include/userver/chaotic/exception.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/exception.hpp", "chaotic/include/userver/chaotic/io/boost/uuids/uuid.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/io/boost/uuids/uuid.hpp", - "chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp", + "chaotic/include/userver/chaotic/io/userver/crypto/base64/string64.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp", "chaotic/include/userver/chaotic/io/decimal64/decimal.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/io/decimal64/decimal.hpp", "chaotic/include/userver/chaotic/io/std/chrono/days.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/io/std/chrono/days.hpp", "chaotic/include/userver/chaotic/io/std/chrono/duration.hpp":"taxi/uservices/userver/chaotic/include/userver/chaotic/io/std/chrono/duration.hpp", diff --git a/chaotic/chaotic/back/cpp/translator.py b/chaotic/chaotic/back/cpp/translator.py index d637fae4daad..2e9fca6862d7 100644 --- a/chaotic/chaotic/back/cpp/translator.py +++ b/chaotic/chaotic/back/cpp/translator.py @@ -444,7 +444,7 @@ def _gen_string( if schema.format and schema.format != types.StringFormat.BINARY: if schema.format == types.StringFormat.BYTE: - format_cpp_type = '::crypto::base64::String64' + format_cpp_type = 'userver::crypto::base64::String64' elif schema.format == types.StringFormat.UUID: format_cpp_type = 'boost::uuids::uuid' elif schema.format == types.StringFormat.DATE: diff --git a/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp b/chaotic/include/userver/chaotic/io/userver/crypto/base64/string64.hpp similarity index 68% rename from chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp rename to chaotic/include/userver/chaotic/io/userver/crypto/base64/string64.hpp index 75ed4e423938..cf1dfd27a2ae 100644 --- a/chaotic/include/userver/chaotic/io/crypto/base64/string64.hpp +++ b/chaotic/include/userver/chaotic/io/userver/crypto/base64/string64.hpp @@ -5,6 +5,8 @@ #include +USERVER_NAMESPACE_BEGIN + namespace crypto::base64 { // RFC4648 @@ -14,13 +16,11 @@ class String64 : public USERVER_NAMESPACE::utils::StrongTypedef); +crypto::base64::String64 Convert(const std::string& str, chaotic::convert::To); -std::string Convert(const ::crypto::base64::String64& str64, chaotic::convert::To); +std::string Convert(const crypto::base64::String64& str64, chaotic::convert::To); } // namespace chaotic::convert diff --git a/chaotic/integration_tests/tests/render/simple.cpp b/chaotic/integration_tests/tests/render/simple.cpp index 817d660b13a9..754720c7c8c3 100644 --- a/chaotic/integration_tests/tests/render/simple.cpp +++ b/chaotic/integration_tests/tests/render/simple.cpp @@ -369,7 +369,7 @@ TEST(Simple, Uuid) { } TEST(SIMPLE, String64) { - auto str64 = ::crypto::base64::String64{"hello, userver!"}; + auto str64 = crypto::base64::String64{"hello, userver!"}; auto obj = ns::ObjectString64{str64}; auto str = Serialize(obj, formats::serialize::To())["value"].As(); diff --git a/chaotic/src/chaotic/io/crypto/base64/string64.cpp b/chaotic/src/chaotic/io/crypto/base64/string64.cpp index f05dc10bfe02..1a6b1dc50d5b 100644 --- a/chaotic/src/chaotic/io/crypto/base64/string64.cpp +++ b/chaotic/src/chaotic/io/crypto/base64/string64.cpp @@ -1,4 +1,4 @@ -#include +#include #include @@ -6,11 +6,11 @@ USERVER_NAMESPACE_BEGIN namespace chaotic::convert { -::crypto::base64::String64 Convert(const std::string& str, chaotic::convert::To<::crypto::base64::String64>) { - return ::crypto::base64::String64(crypto::base64::Base64Decode(str)); +crypto::base64::String64 Convert(const std::string& str, chaotic::convert::To) { + return crypto::base64::String64(crypto::base64::Base64Decode(str)); } -std::string Convert(const ::crypto::base64::String64& str64, chaotic::convert::To) { +std::string Convert(const crypto::base64::String64& str64, chaotic::convert::To) { return crypto::base64::Base64Encode(str64.GetUnderlying()); } diff --git a/chaotic/src/chaotic/io/userver/crypto/base64/string64.cpp b/chaotic/src/chaotic/io/userver/crypto/base64/string64.cpp new file mode 100644 index 000000000000..1a6b1dc50d5b --- /dev/null +++ b/chaotic/src/chaotic/io/userver/crypto/base64/string64.cpp @@ -0,0 +1,19 @@ +#include + +#include + +USERVER_NAMESPACE_BEGIN + +namespace chaotic::convert { + +crypto::base64::String64 Convert(const std::string& str, chaotic::convert::To) { + return crypto::base64::String64(crypto::base64::Base64Decode(str)); +} + +std::string Convert(const crypto::base64::String64& str64, chaotic::convert::To) { + return crypto::base64::Base64Encode(str64.GetUnderlying()); +} + +} // namespace chaotic::convert + +USERVER_NAMESPACE_END diff --git a/chaotic/tests/back/cpp/test_tr_string.py b/chaotic/tests/back/cpp/test_tr_string.py index ea86ef085b57..dc019ff617e8 100644 --- a/chaotic/tests/back/cpp/test_tr_string.py +++ b/chaotic/tests/back/cpp/test_tr_string.py @@ -54,7 +54,7 @@ def test_byte(simple_gen): assert types == { '::type': cpp_types.CppStringWithFormat( raw_cpp_type=type_name.TypeName('std::string'), - format_cpp_type='::crypto::base64::String64', + format_cpp_type='userver::crypto::base64::String64', user_cpp_type=None, json_schema=None, nullable=False, From 298d7d3411944f02decbbec957859b7be619a218 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 14:07:36 +0300 Subject: [PATCH 40/42] add throws tag --- etcd/include/userver/etcd/watch_listener.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/etcd/include/userver/etcd/watch_listener.hpp b/etcd/include/userver/etcd/watch_listener.hpp index 6faca72411d3..d0ea1d59ee92 100644 --- a/etcd/include/userver/etcd/watch_listener.hpp +++ b/etcd/include/userver/etcd/watch_listener.hpp @@ -19,7 +19,7 @@ class WatchListener final { WatchListener(concurrent::SpscQueue::Consumer&& consumer); /// @brief Get an event from etcd if there was one, otherwise waits asynchronously until a next event occurs. - /// If the event producing coroutine finished or failed, GetEvent raises EtcdError exception + /// @throws EtcdError if event producing coroutine finished or failed KeyValueState GetEvent(); private: From 38446b4a951df67243370bf809c7de043a984bec Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 14:14:46 +0300 Subject: [PATCH 41/42] switch to BackgroundTaskStorage --- etcd/src/etcd/client_impl.cpp | 4 ++-- etcd/src/etcd/client_impl.hpp | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index d41b5b05c6df..73a7c526140d 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -148,9 +148,9 @@ WatchListener ClientImpl::StartWatch(std::string_view key) { auto watch_queues_ptr = watch_queues_.Lock(); watch_queues_ptr->push_back(queue); - utils::Async("watch task", [key, producer = queue->GetProducer(), this]() mutable { + bts_.AsyncDetach("watch task", [key, producer = queue->GetProducer(), this]() mutable { this->WatchKeyChanges(std::string(key), std::move(producer)); - }).Detach(); + }); return WatchListener{queue->GetConsumer()}; } diff --git a/etcd/src/etcd/client_impl.hpp b/etcd/src/etcd/client_impl.hpp index f95461479695..d54b07ba6d86 100644 --- a/etcd/src/etcd/client_impl.hpp +++ b/etcd/src/etcd/client_impl.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -38,6 +39,7 @@ class ClientImpl : public Client { using WatchQueuePtr = std::shared_ptr>; clients::http::Client& http_client_; concurrent::Variable> watch_queues_; + concurrent::BackgroundTaskStorage bts_; const ClientSettings settings_; }; From cb883c02ccbf655b00c61e60ad56ff28d5457f65 Mon Sep 17 00:00:00 2001 From: ezzkemer Date: Fri, 16 May 2025 14:33:16 +0300 Subject: [PATCH 42/42] fix logs --- etcd/src/etcd/client_impl.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/etcd/src/etcd/client_impl.cpp b/etcd/src/etcd/client_impl.cpp index 73a7c526140d..eb0c3931bfa2 100644 --- a/etcd/src/etcd/client_impl.cpp +++ b/etcd/src/etcd/client_impl.cpp @@ -148,8 +148,8 @@ WatchListener ClientImpl::StartWatch(std::string_view key) { auto watch_queues_ptr = watch_queues_.Lock(); watch_queues_ptr->push_back(queue); - bts_.AsyncDetach("watch task", [key, producer = queue->GetProducer(), this]() mutable { - this->WatchKeyChanges(std::string(key), std::move(producer)); + bts_.AsyncDetach("watch task", [string_key = std::string(key), producer = queue->GetProducer(), this]() mutable { + this->WatchKeyChanges(string_key, std::move(producer)); }); return WatchListener{queue->GetConsumer()}; @@ -168,7 +168,7 @@ ClientImpl::PerformEtcdRequest(const std::function