diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp index 795d79114..61afb9ef4 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -13,6 +14,7 @@ class DataSystemBuilder { DataSystemBuilder(); using BackgroundSync = BackgroundSyncBuilder; using LazyLoad = LazyLoadBuilder; + using FDv2 = FDv2Builder; /** * @brief Alias for Enabled(false). @@ -46,10 +48,19 @@ class DataSystemBuilder { */ DataSystemBuilder& Method(LazyLoad lazy_load); + /** + * @brief Configures the FDv2 data system, which receives flag delivery + * updates over the new changeset-based protocol with built-in fallback + * and recovery semantics. + * @param fdv2 FDv2 configuration. + * @return Reference to this. + */ + DataSystemBuilder& Method(FDv2 fdv2); + [[nodiscard]] tl::expected Build() const; private: - std::optional> method_builder_; + std::optional> method_builder_; built::DataSystemConfig config_; }; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/fdv2_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/fdv2_builder.hpp new file mode 100644 index 000000000..8d85d9ab4 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/fdv2_builder.hpp @@ -0,0 +1,142 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace launchdarkly::server_side::config::builders { + +class FDv2Builder { + public: + class Streaming { + public: + Streaming& InitialReconnectDelay(std::chrono::milliseconds delay); + Streaming& Filter(std::string filter_key); + Streaming& BaseUrl(std::string base_url); + [[nodiscard]] built::FDv2Config::StreamingConfig Build() const; + + private: + std::chrono::milliseconds initial_reconnect_delay_{1000}; + std::optional filter_key_; + std::optional base_url_override_; + }; + + class Polling { + public: + Polling& PollInterval(std::chrono::seconds interval); + Polling& Filter(std::string filter_key); + Polling& BaseUrl(std::string base_url); + [[nodiscard]] built::FDv2Config::PollingConfig Build() const; + + private: + std::chrono::seconds poll_interval_{30}; + std::optional filter_key_; + std::optional base_url_override_; + }; + + /** + * Constructs a builder with no initializers, no synchronizers, and no + * FDv1 fallback. Use Default() for the spec-recommended configuration. + */ + FDv2Builder(); + + /** + * @return A builder pre-populated with the spec-recommended initializers, + * synchronizers, and FDv1 fallback. Equivalent to calling + * Initializer(), Synchronizer(), and FDv1Fallback() with the + * standard sources. + */ + static FDv2Builder Default(); + + /** + * @brief Appends a polling initializer to the initializers list. + * @param source Polling source configuration for the initializer. + * @return Reference to this. + */ + FDv2Builder& Initializer(Polling source); + + /** + * @brief Appends a streaming synchronizer to the synchronizers list. + * Order in the list determines preference: the first entry is the + * primary synchronizer, subsequent entries are fallbacks. + * @param source Streaming source configuration. + * @return Reference to this. + */ + FDv2Builder& Synchronizer(Streaming source); + + /** + * @brief Appends a polling synchronizer to the synchronizers list. See + * Synchronizer(Streaming) for ordering semantics. + * @param source Polling source configuration. + * @return Reference to this. + */ + FDv2Builder& Synchronizer(Polling source); + + using FDv1Streaming = + launchdarkly::config::shared::builders::StreamingBuilder< + launchdarkly::config::shared::ServerSDK>; + using FDv1Polling = launchdarkly::config::shared::builders::PollingBuilder< + launchdarkly::config::shared::ServerSDK>; + + /** + * @brief Configures the FDv1 streaming source used as a last-resort + * fallback when the LaunchDarkly service signals (via the + * X-LD-FD-Fallback header) that the SDK should switch to FDv1. The + * fallback reads its endpoint from the top-level ServiceEndpoints; to + * point the fallback at a custom URL, configure ServiceEndpoints + * accordingly. + * @param source FDv1 streaming source configuration. + * @return Reference to this. + */ + FDv2Builder& FDv1Fallback(FDv1Streaming source); + + /** + * @brief Configures the FDv1 polling source used as a last-resort + * fallback when the LaunchDarkly service signals (via the + * X-LD-FD-Fallback header) that the SDK should switch to FDv1. The + * fallback reads its endpoint from the top-level ServiceEndpoints; to + * point the fallback at a custom URL, configure ServiceEndpoints + * accordingly. + * @param source FDv1 polling source configuration. + * @return Reference to this. + */ + FDv2Builder& FDv1Fallback(FDv1Polling source); + + /** + * @brief Disables the FDv1 fallback. After this call, an FDv1 + * fallback directive from the service transitions the SDK to + * OFFLINE rather than reconnecting via FDv1. + * @return Reference to this. + */ + FDv2Builder& DisableFDv1Fallback(); + + /** + * @brief Sets how long the active synchronizer may remain interrupted + * before the orchestrator falls back to the next-preferred synchronizer. + * @param timeout Duration the synchronizer must be continuously + * interrupted for before fallback fires. + * @return Reference to this. + */ + FDv2Builder& FallbackTimeout(std::chrono::milliseconds timeout); + + /** + * @brief Sets how long a fallback synchronizer must run successfully + * before the orchestrator attempts to recover to the primary + * synchronizer. + * @param timeout Duration the fallback synchronizer must run before a + * recovery attempt is made. + * @return Reference to this. + */ + FDv2Builder& RecoveryTimeout(std::chrono::milliseconds timeout); + + [[nodiscard]] built::FDv2Config Build() const; + + private: + built::FDv2Config config_; +}; + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp index 76dd478ed..d3697a479 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -9,7 +10,7 @@ namespace launchdarkly::server_side::config::built { struct DataSystemConfig { bool disabled; - std::variant system_; + std::variant system_; }; } // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/fdv2_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/fdv2_config.hpp new file mode 100644 index 000000000..f0771e2f6 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/fdv2_config.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::config::built { + +struct FDv2Config { + struct StreamingConfig { + std::chrono::milliseconds initial_reconnect_delay; + std::optional filter_key; + std::optional base_url_override; + + friend bool operator==(StreamingConfig const& lhs, + StreamingConfig const& rhs) { + return lhs.initial_reconnect_delay == rhs.initial_reconnect_delay && + lhs.filter_key == rhs.filter_key && + lhs.base_url_override == rhs.base_url_override; + } + }; + + struct PollingConfig { + std::chrono::seconds poll_interval; + std::optional filter_key; + std::optional base_url_override; + + friend bool operator==(PollingConfig const& lhs, + PollingConfig const& rhs) { + return lhs.poll_interval == rhs.poll_interval && + lhs.filter_key == rhs.filter_key && + lhs.base_url_override == rhs.base_url_override; + } + }; + + using FDv1StreamingConfig = + launchdarkly::config::shared::built::StreamingConfig< + launchdarkly::config::shared::ServerSDK>; + using FDv1PollingConfig = + launchdarkly::config::shared::built::PollingConfig< + launchdarkly::config::shared::ServerSDK>; + + std::vector initializers; + std::vector> synchronizers; + std::optional> + fdv1_fallback; + std::chrono::milliseconds fallback_timeout; + std::chrono::milliseconds recovery_timeout; +}; + +} // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 08e4e0396..1090eebac 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -29,6 +29,7 @@ target_sources(${LIBNAME} config/builders/data_system/background_sync_builder.cpp config/builders/data_system/bootstrap_builder.cpp config/builders/data_system/data_system_builder.cpp + config/builders/data_system/fdv2_builder.cpp config/builders/data_system/lazy_load_builder.cpp config/builders/data_system/data_destination_builder.cpp config/builders/big_segments_builder.cpp @@ -73,6 +74,10 @@ target_sources(${LIBNAME} data_systems/fdv2/fdv2_data_system.cpp data_systems/fdv2/fdv1_adapter_synchronizer.hpp data_systems/fdv2/fdv1_adapter_synchronizer.cpp + data_systems/fdv2/synchronizer_factories.hpp + data_systems/fdv2/synchronizer_factories.cpp + data_systems/fdv2/initializer_factories.hpp + data_systems/fdv2/initializer_factories.cpp data_systems/background_sync/sources/streaming/streaming_data_source.hpp data_systems/background_sync/sources/streaming/streaming_data_source.cpp data_systems/background_sync/sources/streaming/event_handler.hpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 9910dd23c..8bc91b61f 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -2,6 +2,10 @@ #include "all_flags_state/all_flags_state_builder.hpp" #include "data_systems/background_sync/background_sync_system.hpp" +#include "data_systems/fdv2/conditions.hpp" +#include "data_systems/fdv2/fdv2_data_system.hpp" +#include "data_systems/fdv2/initializer_factories.hpp" +#include "data_systems/fdv2/synchronizer_factories.hpp" #include "data_systems/lazy_load/lazy_load_system.hpp" #include "data_systems/offline.hpp" #include "evaluation/evaluation_stack.hpp" @@ -39,16 +43,119 @@ auto const kDataSourceShutdownWait = std::chrono::milliseconds(100); // Hook method names // Method names for hooks -static const std::string kMethodBoolVariation = "BoolVariation"; -static const std::string kMethodBoolVariationDetail = "BoolVariationDetail"; -static const std::string kMethodStringVariation = "StringVariation"; -static const std::string kMethodStringVariationDetail = "StringVariationDetail"; -static const std::string kMethodDoubleVariation = "DoubleVariation"; -static const std::string kMethodDoubleVariationDetail = "DoubleVariationDetail"; -static const std::string kMethodIntVariation = "IntVariation"; -static const std::string kMethodIntVariationDetail = "IntVariationDetail"; -static const std::string kMethodJsonVariation = "JsonVariation"; -static const std::string kMethodJsonVariationDetail = "JsonVariationDetail"; +static std::string const kMethodBoolVariation = "BoolVariation"; +static std::string const kMethodBoolVariationDetail = "BoolVariationDetail"; +static std::string const kMethodStringVariation = "StringVariation"; +static std::string const kMethodStringVariationDetail = "StringVariationDetail"; +static std::string const kMethodDoubleVariation = "DoubleVariation"; +static std::string const kMethodDoubleVariationDetail = "DoubleVariationDetail"; +static std::string const kMethodIntVariation = "IntVariation"; +static std::string const kMethodIntVariationDetail = "IntVariationDetail"; +static std::string const kMethodJsonVariation = "JsonVariation"; +static std::string const kMethodJsonVariationDetail = "JsonVariationDetail"; + +namespace { + +template +struct overloaded : Ts... { + using Ts::operator()...; +}; +template +overloaded(Ts...) -> overloaded; + +} // namespace + +static std::unique_ptr MakeBackgroundSyncSystem( + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig const& cfg, + config::built::HttpProperties const& http_properties, + boost::asio::any_io_executor const& executor, + data_components::DataSourceStatusManager& status_manager, + Logger& logger) { + return std::make_unique( + endpoints, cfg, http_properties, executor, status_manager, logger); +} + +static std::unique_ptr MakeLazyLoadSystem( + config::built::LazyLoadConfig const& cfg, + data_components::DataSourceStatusManager& status_manager, + Logger& logger) { + return std::make_unique(logger, cfg, + status_manager); +} + +static std::unique_ptr MakeFDv2System( + config::built::ServiceEndpoints const& endpoints, + config::built::FDv2Config const& cfg, + config::built::HttpProperties const& http_properties, + boost::asio::any_io_executor const& executor, + data_components::DataSourceStatusManager& status_manager, + Logger const& logger) { + std::vector> + initializer_factories; + for (auto const& initializer : cfg.initializers) { + initializer_factories.push_back( + std::make_unique( + executor, logger, endpoints, http_properties, initializer)); + } + + std::vector> + synchronizer_factories; + for (auto const& sync : cfg.synchronizers) { + std::visit( + overloaded{ + [&](config::built::FDv2Config::StreamingConfig const& + streaming) { + synchronizer_factories.push_back( + std::make_unique< + data_systems::FDv2StreamingSynchronizerFactory>( + executor, logger, endpoints, http_properties, + streaming)); + }, + [&](config::built::FDv2Config::PollingConfig const& polling) { + synchronizer_factories.push_back( + std::make_unique< + data_systems::FDv2PollingSynchronizerFactory>( + executor, logger, endpoints, http_properties, + polling)); + }, + }, + sync); + } + if (cfg.fdv1_fallback) { + std::visit(overloaded{ + [&](config::built::FDv2Config::FDv1StreamingConfig const& + streaming) { + synchronizer_factories.push_back( + std::make_unique< + data_systems::FDv1StreamingAdapterFactory>( + executor, logger, &status_manager, endpoints, + streaming, http_properties)); + }, + [&](config::built::FDv2Config::FDv1PollingConfig const& + polling) { + synchronizer_factories.push_back( + std::make_unique< + data_systems::FDv1PollingAdapterFactory>( + executor, logger, &status_manager, endpoints, + polling, http_properties)); + }, + }, + *cfg.fdv1_fallback); + } + + auto fallback_cond_factory = + std::make_unique( + executor, cfg.fallback_timeout); + auto recovery_cond_factory = + std::make_unique( + executor, cfg.recovery_timeout); + + return std::make_unique( + std::move(initializer_factories), std::move(synchronizer_factories), + std::move(fallback_cond_factory), std::move(recovery_cond_factory), + executor, &status_manager, logger); +} static std::unique_ptr MakeDataSystem( config::built::HttpProperties const& http_properties, @@ -60,24 +167,24 @@ static std::unique_ptr MakeDataSystem( return std::make_unique(status_manager); } - auto const builder = - config::builders::HttpPropertiesBuilder(http_properties); - - auto data_source_properties = builder.Build(); + auto data_source_properties = + config::builders::HttpPropertiesBuilder(http_properties).Build(); return std::visit( - [&](auto&& arg) -> std::unique_ptr { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return std::make_unique( - config.ServiceEndpoints(), arg, data_source_properties, + overloaded{ + [&](config::built::BackgroundSyncConfig const& cfg) { + return MakeBackgroundSyncSystem( + config.ServiceEndpoints(), cfg, data_source_properties, executor, status_manager, logger); - } else if constexpr (std::is_same_v< - T, config::built::LazyLoadConfig>) { - return std::make_unique(logger, arg, - status_manager); - } + }, + [&](config::built::LazyLoadConfig const& cfg) { + return MakeLazyLoadSystem(cfg, status_manager, logger); + }, + [&](config::built::FDv2Config const& cfg) { + return MakeFDv2System(config.ServiceEndpoints(), cfg, + data_source_properties, executor, + status_manager, logger); + }, }, config.DataSystemConfig().system_); } @@ -247,9 +354,9 @@ void ClientImpl::TrackInternal(Context const& ctx, std::optional data, std::optional metric_value, hooks::HookContext const& hook_context) { - if (!ctx.Valid()) { - LD_LOG(logger_, LogLevel::kWarn) << "Track method called with an invalid context"; + LD_LOG(logger_, LogLevel::kWarn) + << "Track method called with an invalid context"; return; } // Execute afterTrack hooks before moving the data @@ -259,8 +366,8 @@ void ClientImpl::TrackInternal(Context const& ctx, // In this SDK the data is type-safe, and will be enqueued, so it makes // minimal functional difference. if (!config_.Hooks().empty()) { - hooks::TrackSeriesContext series_context(ctx, event_name, metric_value, - data, hook_context, std::nullopt); + hooks::TrackSeriesContext series_context( + ctx, event_name, metric_value, data, hook_context, std::nullopt); hooks::ExecuteAfterTrack(config_.Hooks(), series_context, logger_); } @@ -367,7 +474,8 @@ EvaluationDetail ClientImpl::VariationInternal( std::optional executor; if (!config_.Hooks().empty()) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); // Executor only created if there are hooks. executor.emplace(config_.Hooks(), logger_); executor->BeforeEvaluation(series_context); @@ -380,7 +488,8 @@ EvaluationDetail ClientImpl::VariationInternal( // Execute afterEvaluation hooks if (executor) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); executor->AfterEvaluation(series_context, detail); } @@ -401,7 +510,8 @@ EvaluationDetail ClientImpl::VariationInternal( // Execute afterEvaluation hooks if (executor) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); executor->AfterEvaluation(series_context, detail); } @@ -416,7 +526,8 @@ EvaluationDetail ClientImpl::VariationInternal( // Execute afterEvaluation hooks if (executor) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); executor->AfterEvaluation(series_context, detail); } @@ -484,7 +595,8 @@ EvaluationDetail ClientImpl::BoolVariationDetail( bool default_value) { static hooks::HookContext empty_hook_context; return VariationDetail(ctx, Value::Type::kBool, key, default_value, - empty_hook_context, kMethodBoolVariationDetail); + empty_hook_context, + kMethodBoolVariationDetail); } EvaluationDetail ClientImpl::BoolVariationDetail( @@ -508,8 +620,8 @@ bool ClientImpl::BoolVariation(Context const& ctx, IClient::FlagKey const& key, bool default_value, hooks::HookContext const& hook_context) { - return Variation(ctx, Value::Type::kBool, key, default_value, - hook_context, kMethodBoolVariation); + return Variation(ctx, Value::Type::kBool, key, default_value, hook_context, + kMethodBoolVariation); } EvaluationDetail ClientImpl::StringVariationDetail( @@ -540,10 +652,11 @@ std::string ClientImpl::StringVariation(Context const& ctx, empty_hook_context, kMethodStringVariation); } -std::string ClientImpl::StringVariation(Context const& ctx, - IClient::FlagKey const& key, - std::string default_value, - hooks::HookContext const& hook_context) { +std::string ClientImpl::StringVariation( + Context const& ctx, + IClient::FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) { return Variation(ctx, Value::Type::kString, key, default_value, hook_context, kMethodStringVariation); } diff --git a/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp b/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp index fbf29b846..74e7d6ba5 100644 --- a/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp +++ b/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp @@ -16,6 +16,11 @@ DataSystemBuilder& DataSystemBuilder::Method(LazyLoad lazy_load) { return *this; } +DataSystemBuilder& DataSystemBuilder::Method(FDv2 fdv2) { + method_builder_ = std::move(fdv2); + return *this; +} + DataSystemBuilder& DataSystemBuilder::Enabled(bool const enabled) { config_.disabled = !enabled; return *this; @@ -27,10 +32,11 @@ DataSystemBuilder& DataSystemBuilder::Disable() { tl::expected DataSystemBuilder::Build() const { if (method_builder_) { - auto lazy_or_background_cfg = std::visit( + auto system_cfg = std::visit( [](auto&& arg) -> tl::expected, + built::BackgroundSyncConfig, + built::FDv2Config>, Error> { using T = std::decay_t; if constexpr (std::is_same_v) { @@ -39,14 +45,16 @@ tl::expected DataSystemBuilder::Build() const { return arg .Build(); // -> tl::expected + } else if constexpr (std::is_same_v) { + return arg.Build(); // -> built::FDv2Config } }, *method_builder_); - if (!lazy_or_background_cfg) { - return tl::make_unexpected(lazy_or_background_cfg.error()); + if (!system_cfg) { + return tl::make_unexpected(system_cfg.error()); } return built::DataSystemConfig{config_.disabled, - std::move(*lazy_or_background_cfg)}; + std::move(*system_cfg)}; } return config_; } diff --git a/libs/server-sdk/src/config/builders/data_system/defaults.hpp b/libs/server-sdk/src/config/builders/data_system/defaults.hpp index c8a6c5112..2dbaa7487 100644 --- a/libs/server-sdk/src/config/builders/data_system/defaults.hpp +++ b/libs/server-sdk/src/config/builders/data_system/defaults.hpp @@ -1,7 +1,9 @@ #pragma once +#include #include #include #include +#include #include namespace launchdarkly::server_side::config { @@ -34,6 +36,31 @@ struct Defaults { std::chrono::minutes{5}, nullptr}; } + static auto FDv2StreamingConfig() -> built::FDv2Config::StreamingConfig { + return {std::chrono::seconds{1}}; + } + + static auto FDv2PollingConfig() -> built::FDv2Config::PollingConfig { + return {std::chrono::seconds{30}}; + } + + static auto FDv2Config() -> built::FDv2Config { + using StreamingConfig = built::FDv2Config::StreamingConfig; + using PollingConfig = built::FDv2Config::PollingConfig; + using SyncVariant = std::variant; + using FallbackVariant = + std::variant; + return { + {FDv2PollingConfig()}, + std::vector{FDv2StreamingConfig(), + FDv2PollingConfig()}, + FallbackVariant{launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ServerSDK>::StreamingConfig()}, + std::chrono::minutes{2}, + std::chrono::minutes{5}}; + } + static auto DataSystemConfig() -> built::DataSystemConfig { return {false, BackgroundSyncConfig()}; } diff --git a/libs/server-sdk/src/config/builders/data_system/fdv2_builder.cpp b/libs/server-sdk/src/config/builders/data_system/fdv2_builder.cpp new file mode 100644 index 000000000..430e9d596 --- /dev/null +++ b/libs/server-sdk/src/config/builders/data_system/fdv2_builder.cpp @@ -0,0 +1,106 @@ +#include + +#include "defaults.hpp" + +namespace launchdarkly::server_side::config::builders { + +FDv2Builder::Streaming& FDv2Builder::Streaming::InitialReconnectDelay( + std::chrono::milliseconds delay) { + initial_reconnect_delay_ = delay; + return *this; +} + +FDv2Builder::Streaming& FDv2Builder::Streaming::Filter(std::string filter_key) { + filter_key_ = std::move(filter_key); + return *this; +} + +FDv2Builder::Streaming& FDv2Builder::Streaming::BaseUrl(std::string base_url) { + base_url_override_ = std::move(base_url); + return *this; +} + +built::FDv2Config::StreamingConfig FDv2Builder::Streaming::Build() const { + return {initial_reconnect_delay_, filter_key_, base_url_override_}; +} + +FDv2Builder::Polling& FDv2Builder::Polling::PollInterval( + std::chrono::seconds interval) { + poll_interval_ = interval; + return *this; +} + +FDv2Builder::Polling& FDv2Builder::Polling::Filter(std::string filter_key) { + filter_key_ = std::move(filter_key); + return *this; +} + +FDv2Builder::Polling& FDv2Builder::Polling::BaseUrl(std::string base_url) { + base_url_override_ = std::move(base_url); + return *this; +} + +built::FDv2Config::PollingConfig FDv2Builder::Polling::Build() const { + return {poll_interval_, filter_key_, base_url_override_}; +} + +FDv2Builder::FDv2Builder() + : config_{{}, + {}, + std::nullopt, + std::chrono::minutes{2}, + std::chrono::minutes{5}} {} + +FDv2Builder FDv2Builder::Default() { + return FDv2Builder() + .Initializer(Polling{}) + .Synchronizer(Streaming{}) + .Synchronizer(Polling{}) + .FDv1Fallback(FDv1Streaming{}); +} + +FDv2Builder& FDv2Builder::Initializer(Polling source) { + config_.initializers.push_back(source.Build()); + return *this; +} + +FDv2Builder& FDv2Builder::Synchronizer(Streaming source) { + config_.synchronizers.push_back(source.Build()); + return *this; +} + +FDv2Builder& FDv2Builder::Synchronizer(Polling source) { + config_.synchronizers.push_back(source.Build()); + return *this; +} + +FDv2Builder& FDv2Builder::FDv1Fallback(FDv1Streaming source) { + config_.fdv1_fallback = source.Build(); + return *this; +} + +FDv2Builder& FDv2Builder::FDv1Fallback(FDv1Polling source) { + config_.fdv1_fallback = source.Build(); + return *this; +} + +FDv2Builder& FDv2Builder::DisableFDv1Fallback() { + config_.fdv1_fallback = std::nullopt; + return *this; +} + +FDv2Builder& FDv2Builder::FallbackTimeout(std::chrono::milliseconds timeout) { + config_.fallback_timeout = timeout; + return *this; +} + +FDv2Builder& FDv2Builder::RecoveryTimeout(std::chrono::milliseconds timeout) { + config_.recovery_timeout = timeout; + return *this; +} + +built::FDv2Config FDv2Builder::Build() const { + return config_; +} + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp b/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp index 5a98a02b2..f86fb7f57 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.cpp @@ -41,13 +41,13 @@ static bool ReadFDv1FallbackDirective( } network::HttpRequest MakeFDv2PollRequest( - config::built::ServiceEndpoints const& endpoints, + std::string const& polling_base_url, config::built::HttpProperties const& http_properties, data_model::Selector const& selector, std::optional const& filter_key) { config::builders::HttpPropertiesBuilder const builder(http_properties); - auto parsed = boost::urls::parse_uri(endpoints.PollingBaseUrl()); + auto parsed = boost::urls::parse_uri(polling_base_url); if (!parsed) { return {"", network::HttpMethod::kGet, builder.Build(), network::HttpRequest::BodyType{}}; diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.hpp b/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.hpp index 086d3c8db..4e06d0a6a 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv2_polling_impl.hpp @@ -16,7 +16,7 @@ namespace launchdarkly::server_side::data_systems { // Build a polling HTTP GET request for the FDv2 endpoint. network::HttpRequest MakeFDv2PollRequest( - config::built::ServiceEndpoints const& endpoints, + std::string const& polling_base_url, config::built::HttpProperties const& http_properties, data_model::Selector const& selector, std::optional const& filter_key); diff --git a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp new file mode 100644 index 000000000..316f4c615 --- /dev/null +++ b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp @@ -0,0 +1,31 @@ +#include "initializer_factories.hpp" + +#include "polling_initializer.hpp" + +#include + +#include + +namespace launchdarkly::server_side::data_systems { + +FDv2PollingInitializerFactory::FDv2PollingInitializerFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::built::ServiceEndpoints endpoints, + config::built::HttpProperties http_properties, + config::built::FDv2Config::PollingConfig polling) + : executor_(std::move(executor)), + logger_(std::move(logger)), + polling_base_url_( + polling.base_url_override.value_or(endpoints.PollingBaseUrl())), + http_properties_(std::move(http_properties)), + polling_(std::move(polling)) {} + +std::unique_ptr +FDv2PollingInitializerFactory::Build() { + return std::make_unique( + executor_, logger_, polling_base_url_, http_properties_, + data_model::Selector{}, polling_.filter_key); +} + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.hpp b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.hpp new file mode 100644 index 000000000..ea97dc374 --- /dev/null +++ b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "../../data_interfaces/source/ifdv2_initializer_factory.hpp" + +#include +#include +#include + +#include + +namespace launchdarkly::server_side::data_systems { + +/** + * Builds fresh FDv2PollingInitializer instances on demand. + */ +class FDv2PollingInitializerFactory final + : public data_interfaces::IFDv2InitializerFactory { + public: + FDv2PollingInitializerFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::built::ServiceEndpoints endpoints, + config::built::HttpProperties http_properties, + config::built::FDv2Config::PollingConfig polling); + + std::unique_ptr Build() override; + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + std::string const polling_base_url_; + config::built::HttpProperties const http_properties_; + config::built::FDv2Config::PollingConfig const polling_; +}; + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp index 05620161d..f70e28008 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp @@ -13,11 +13,11 @@ using data_interfaces::FDv2SourceResult; FDv2PollingInitializer::FDv2PollingInitializer( boost::asio::any_io_executor const& executor, Logger const& logger, - config::built::ServiceEndpoints const& endpoints, + std::string const& polling_base_url, config::built::HttpProperties const& http_properties, data_model::Selector selector, std::optional filter_key) - : request_(MakeFDv2PollRequest(endpoints, + : request_(MakeFDv2PollRequest(polling_base_url, http_properties, std::move(selector), std::move(filter_key))), diff --git a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp index 0fb87c4d0..8d27a549f 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp @@ -35,7 +35,7 @@ class FDv2PollingInitializer final : public data_interfaces::IFDv2Initializer { */ FDv2PollingInitializer(boost::asio::any_io_executor const& executor, Logger const& logger, - config::built::ServiceEndpoints const& endpoints, + std::string const& polling_base_url, config::built::HttpProperties const& http_properties, data_model::Selector selector, std::optional filter_key); diff --git a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp index 84df5e0f3..1f32f3d1e 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp @@ -20,12 +20,12 @@ FDv2PollingSynchronizer::State::State( Logger logger, boost::asio::any_io_executor const& executor, std::chrono::seconds poll_interval, - config::built::ServiceEndpoints const& endpoints, + std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key) : logger_(std::move(logger)), poll_interval_(std::max(poll_interval, kMinPollInterval)), - endpoints_(endpoints), + polling_base_url_(std::move(polling_base_url)), http_properties_(http_properties), filter_key_(std::move(filter_key)), requester_(executor, http_properties.Tls()), @@ -33,8 +33,8 @@ FDv2PollingSynchronizer::State::State( async::Future FDv2PollingSynchronizer::State::Request( data_model::Selector const& selector) const { - auto request = MakeFDv2PollRequest(endpoints_, http_properties_, selector, - filter_key_); + auto request = MakeFDv2PollRequest(polling_base_url_, http_properties_, + selector, filter_key_); // Promise must be in a shared_ptr because Requester requires callbacks // to be copy-constructible (stored in std::function). @@ -81,14 +81,14 @@ void FDv2PollingSynchronizer::State::RecordPollStarted() { FDv2PollingSynchronizer::FDv2PollingSynchronizer( boost::asio::any_io_executor const& executor, Logger const& logger, - config::built::ServiceEndpoints const& endpoints, + std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, std::chrono::seconds poll_interval) : state_(std::make_shared(logger, executor, poll_interval, - endpoints, + std::move(polling_base_url), http_properties, std::move(filter_key))) { if (poll_interval < kMinPollInterval) { diff --git a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp index 954acbc9d..3b03511ce 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp @@ -39,7 +39,7 @@ class FDv2PollingSynchronizer final FDv2PollingSynchronizer( boost::asio::any_io_executor const& executor, Logger const& logger, - config::built::ServiceEndpoints const& endpoints, + std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, std::chrono::seconds poll_interval); @@ -62,7 +62,7 @@ class FDv2PollingSynchronizer final State(Logger logger, boost::asio::any_io_executor const& executor, std::chrono::seconds poll_interval, - config::built::ServiceEndpoints const& endpoints, + std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key); @@ -96,7 +96,7 @@ class FDv2PollingSynchronizer final // Immutable state std::chrono::seconds const poll_interval_; - config::built::ServiceEndpoints const endpoints_; + std::string const polling_base_url_; config::built::HttpProperties const http_properties_; std::optional const filter_key_; network::Requester const requester_; diff --git a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp index e6bc0dceb..7a4068cec 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp @@ -37,12 +37,12 @@ inline constexpr bool always_false_v = false; FDv2StreamingSynchronizer::State::State( Logger logger, boost::asio::any_io_executor const& executor, - config::built::ServiceEndpoints const& endpoints, + std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, std::chrono::milliseconds initial_reconnect_delay) : logger_(std::move(logger)), - endpoints_(endpoints), + streaming_base_url_(std::move(streaming_base_url)), http_properties_(http_properties), filter_key_(std::move(filter_key)), initial_reconnect_delay_(initial_reconnect_delay), @@ -60,7 +60,7 @@ void FDv2StreamingSynchronizer::State::EnsureStarted( started_ = true; } - auto parsed = boost::urls::parse_uri(endpoints_.StreamingBaseUrl()); + auto parsed = boost::urls::parse_uri(streaming_base_url_); if (!parsed) { // started_ intentionally left true: a bad endpoint URL is a // configuration error that won't fix itself. The TerminalError @@ -353,13 +353,13 @@ void FDv2StreamingSynchronizer::State::Shutdown() { FDv2StreamingSynchronizer::FDv2StreamingSynchronizer( boost::asio::any_io_executor const& executor, Logger const& logger, - config::built::ServiceEndpoints const& endpoints, + std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, std::chrono::milliseconds initial_reconnect_delay) : state_(std::make_shared(logger, executor, - endpoints, + std::move(streaming_base_url), http_properties, std::move(filter_key), initial_reconnect_delay)) {} diff --git a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp index 1abed1f64..7a0f12418 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp @@ -46,7 +46,7 @@ class FDv2StreamingSynchronizer final FDv2StreamingSynchronizer( boost::asio::any_io_executor const& executor, Logger const& logger, - config::built::ServiceEndpoints const& endpoints, + std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, std::chrono::milliseconds initial_reconnect_delay); @@ -70,7 +70,7 @@ class FDv2StreamingSynchronizer final public: State(Logger logger, boost::asio::any_io_executor const& executor, - config::built::ServiceEndpoints const& endpoints, + std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, std::chrono::milliseconds initial_reconnect_delay); @@ -132,7 +132,7 @@ class FDv2StreamingSynchronizer final Logger logger_; // Immutable state - config::built::ServiceEndpoints const endpoints_; + std::string const streaming_base_url_; config::built::HttpProperties const http_properties_; std::optional const filter_key_; std::chrono::milliseconds const initial_reconnect_delay_; diff --git a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp new file mode 100644 index 000000000..30036b56c --- /dev/null +++ b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp @@ -0,0 +1,99 @@ +#include "synchronizer_factories.hpp" + +#include "../background_sync/sources/polling/polling_data_source.hpp" +#include "../background_sync/sources/streaming/streaming_data_source.hpp" +#include "fdv1_adapter_synchronizer.hpp" +#include "polling_synchronizer.hpp" +#include "streaming_synchronizer.hpp" + +#include + +#include + +namespace launchdarkly::server_side::data_systems { + +FDv2StreamingSynchronizerFactory::FDv2StreamingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::built::ServiceEndpoints endpoints, + config::built::HttpProperties http_properties, + config::built::FDv2Config::StreamingConfig streaming) + : executor_(std::move(executor)), + logger_(std::move(logger)), + streaming_base_url_( + streaming.base_url_override.value_or(endpoints.StreamingBaseUrl())), + http_properties_(std::move(http_properties)), + streaming_(std::move(streaming)) {} + +std::unique_ptr +FDv2StreamingSynchronizerFactory::Build() { + return std::make_unique( + executor_, logger_, streaming_base_url_, http_properties_, + streaming_.filter_key, streaming_.initial_reconnect_delay); +} + +FDv2PollingSynchronizerFactory::FDv2PollingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::built::ServiceEndpoints endpoints, + config::built::HttpProperties http_properties, + config::built::FDv2Config::PollingConfig polling) + : executor_(std::move(executor)), + logger_(std::move(logger)), + polling_base_url_( + polling.base_url_override.value_or(endpoints.PollingBaseUrl())), + http_properties_(std::move(http_properties)), + polling_(std::move(polling)) {} + +std::unique_ptr +FDv2PollingSynchronizerFactory::Build() { + return std::make_unique( + executor_, logger_, polling_base_url_, http_properties_, + polling_.filter_key, polling_.poll_interval); +} + +FDv1StreamingAdapterFactory::FDv1StreamingAdapterFactory( + boost::asio::any_io_executor executor, + Logger logger, + data_components::DataSourceStatusManager* status_manager, + config::built::ServiceEndpoints endpoints, + config::built::FDv2Config::FDv1StreamingConfig streaming, + config::built::HttpProperties http_properties) + : executor_(std::move(executor)), + logger_(std::move(logger)), + status_manager_(status_manager), + endpoints_(std::move(endpoints)), + streaming_(std::move(streaming)), + http_properties_(std::move(http_properties)) {} + +std::unique_ptr +FDv1StreamingAdapterFactory::Build() { + auto fdv1_source = std::make_unique( + executor_, logger_, *status_manager_, endpoints_, streaming_, + http_properties_); + return std::make_unique(std::move(fdv1_source)); +} + +FDv1PollingAdapterFactory::FDv1PollingAdapterFactory( + boost::asio::any_io_executor executor, + Logger logger, + data_components::DataSourceStatusManager* status_manager, + config::built::ServiceEndpoints endpoints, + config::built::FDv2Config::FDv1PollingConfig polling, + config::built::HttpProperties http_properties) + : executor_(std::move(executor)), + logger_(std::move(logger)), + status_manager_(status_manager), + endpoints_(std::move(endpoints)), + polling_(std::move(polling)), + http_properties_(std::move(http_properties)) {} + +std::unique_ptr +FDv1PollingAdapterFactory::Build() { + auto fdv1_source = std::make_unique( + executor_, logger_, *status_manager_, endpoints_, polling_, + http_properties_); + return std::make_unique(std::move(fdv1_source)); +} + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp new file mode 100644 index 000000000..c459733f9 --- /dev/null +++ b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp @@ -0,0 +1,118 @@ +#pragma once + +#include "../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../data_interfaces/source/ifdv2_synchronizer_factory.hpp" + +#include +#include +#include + +#include + +namespace launchdarkly::server_side::data_systems { + +/** + * Builds fresh FDv2StreamingSynchronizer instances on demand. + */ +class FDv2StreamingSynchronizerFactory final + : public data_interfaces::IFDv2SynchronizerFactory { + public: + FDv2StreamingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::built::ServiceEndpoints endpoints, + config::built::HttpProperties http_properties, + config::built::FDv2Config::StreamingConfig streaming); + + std::unique_ptr Build() override; + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + std::string const streaming_base_url_; + config::built::HttpProperties const http_properties_; + config::built::FDv2Config::StreamingConfig const streaming_; +}; + +/** + * Builds fresh FDv2PollingSynchronizer instances on demand. + */ +class FDv2PollingSynchronizerFactory final + : public data_interfaces::IFDv2SynchronizerFactory { + public: + FDv2PollingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::built::ServiceEndpoints endpoints, + config::built::HttpProperties http_properties, + config::built::FDv2Config::PollingConfig polling); + + std::unique_ptr Build() override; + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + std::string const polling_base_url_; + config::built::HttpProperties const http_properties_; + config::built::FDv2Config::PollingConfig const polling_; +}; + +/** + * Builds fresh FDv1AdapterSynchronizer instances wrapping a freshly-built + * FDv1 StreamingDataSource. Reports IsFDv1Fallback() = true. + */ +class FDv1StreamingAdapterFactory final + : public data_interfaces::IFDv2SynchronizerFactory { + public: + FDv1StreamingAdapterFactory( + boost::asio::any_io_executor executor, + Logger logger, + data_components::DataSourceStatusManager* status_manager, + config::built::ServiceEndpoints endpoints, + config::built::FDv2Config::FDv1StreamingConfig streaming, + config::built::HttpProperties http_properties); + + std::unique_ptr Build() override; + + [[nodiscard]] bool IsFDv1Fallback() const override { return true; } + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + // Non-owning. Provided by the orchestrator; must outlive this factory. + data_components::DataSourceStatusManager* const status_manager_; + config::built::ServiceEndpoints const endpoints_; + config::built::FDv2Config::FDv1StreamingConfig const streaming_; + config::built::HttpProperties const http_properties_; +}; + +/** + * Builds fresh FDv1AdapterSynchronizer instances wrapping a freshly-built + * FDv1 PollingDataSource. Reports IsFDv1Fallback() = true. + */ +class FDv1PollingAdapterFactory final + : public data_interfaces::IFDv2SynchronizerFactory { + public: + FDv1PollingAdapterFactory( + boost::asio::any_io_executor executor, + Logger logger, + data_components::DataSourceStatusManager* status_manager, + config::built::ServiceEndpoints endpoints, + config::built::FDv2Config::FDv1PollingConfig polling, + config::built::HttpProperties http_properties); + + std::unique_ptr Build() override; + + [[nodiscard]] bool IsFDv1Fallback() const override { return true; } + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + // Non-owning. Provided by the orchestrator; must outlive this factory. + data_components::DataSourceStatusManager* const status_manager_; + config::built::ServiceEndpoints const endpoints_; + config::built::FDv2Config::FDv1PollingConfig const polling_; + config::built::HttpProperties const http_properties_; +}; + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 7e4bb0631..236898140 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -102,6 +102,141 @@ TEST_F(ConfigBuilderTest, CanSetPollingPayloadFilterKey) { EXPECT_EQ(polling_config.filter_key, "foo"); } +TEST_F(ConfigBuilderTest, FDv2_DefaultsAreUsed) { + ConfigBuilder builder("sdk-123"); + builder.DataSystem().Method(builders::DataSystemBuilder::FDv2::Default()); + + auto cfg = builder.Build(); + + ASSERT_TRUE(std::holds_alternative( + cfg->DataSystemConfig().system_)); + auto const fdv2_config = + std::get(cfg->DataSystemConfig().system_); + + ASSERT_EQ(fdv2_config.initializers.size(), 1u); + EXPECT_EQ(fdv2_config.initializers[0].poll_interval, + Defaults::FDv2PollingConfig().poll_interval); + + ASSERT_EQ(fdv2_config.synchronizers.size(), 2u); + ASSERT_TRUE(std::holds_alternative( + fdv2_config.synchronizers[0])); + EXPECT_EQ(std::get( + fdv2_config.synchronizers[0]), + Defaults::FDv2StreamingConfig()); + ASSERT_TRUE(std::holds_alternative( + fdv2_config.synchronizers[1])); + + ASSERT_TRUE(fdv2_config.fdv1_fallback.has_value()); + ASSERT_TRUE(std::holds_alternative( + *fdv2_config.fdv1_fallback)); + EXPECT_EQ(std::get( + *fdv2_config.fdv1_fallback), + launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ServerSDK>::StreamingConfig()); + EXPECT_EQ(fdv2_config.fallback_timeout, std::chrono::minutes{2}); + EXPECT_EQ(fdv2_config.recovery_timeout, std::chrono::minutes{5}); +} + +TEST_F(ConfigBuilderTest, FDv2_FDv1FallbackPolling) { + ConfigBuilder builder("sdk-123"); + builder.DataSystem().Method( + builders::DataSystemBuilder::FDv2().FDv1Fallback( + builders::FDv2Builder::FDv1Polling().PollInterval( + std::chrono::seconds{45}))); + + auto cfg = builder.Build(); + auto const fdv2_config = + std::get(cfg->DataSystemConfig().system_); + + ASSERT_TRUE(fdv2_config.fdv1_fallback.has_value()); + ASSERT_TRUE(std::holds_alternative( + *fdv2_config.fdv1_fallback)); + EXPECT_EQ(std::get( + *fdv2_config.fdv1_fallback) + .poll_interval, + std::chrono::seconds{45}); +} + +TEST_F(ConfigBuilderTest, FDv2_MultipleSynchronizers) { + ConfigBuilder builder("sdk-123"); + builder.DataSystem().Method( + builders::DataSystemBuilder::FDv2() + .Synchronizer(builders::FDv2Builder::Polling().PollInterval( + std::chrono::seconds{45})) + .Synchronizer(builders::FDv2Builder::Streaming().Filter("filt"))); + + auto cfg = builder.Build(); + auto const fdv2_config = + std::get(cfg->DataSystemConfig().system_); + + ASSERT_EQ(fdv2_config.synchronizers.size(), 2u); + ASSERT_TRUE(std::holds_alternative( + fdv2_config.synchronizers[0])); + ASSERT_TRUE(std::holds_alternative( + fdv2_config.synchronizers[1])); + EXPECT_EQ(std::get( + fdv2_config.synchronizers[1]) + .filter_key, + "filt"); +} + +TEST_F(ConfigBuilderTest, FDv2_AddingInitializerClearsDefaults) { + ConfigBuilder builder("sdk-123"); + builder.DataSystem().Method(builders::DataSystemBuilder::FDv2().Initializer( + builders::FDv2Builder::Polling().Filter("flag-subset"))); + + auto cfg = builder.Build(); + auto const fdv2_config = + std::get(cfg->DataSystemConfig().system_); + + ASSERT_EQ(fdv2_config.initializers.size(), 1u); + EXPECT_EQ(fdv2_config.initializers[0].filter_key, "flag-subset"); +} + +TEST_F(ConfigBuilderTest, FDv2_PerSourceBaseUrlOverride) { + ConfigBuilder builder("sdk-123"); + builder.DataSystem().Method( + builders::DataSystemBuilder::FDv2().Synchronizer( + builders::FDv2Builder::Streaming().BaseUrl( + "https://example.test"))); + + auto cfg = builder.Build(); + auto const fdv2_config = + std::get(cfg->DataSystemConfig().system_); + + ASSERT_EQ(fdv2_config.synchronizers.size(), 1u); + auto const& sync = std::get( + fdv2_config.synchronizers[0]); + ASSERT_TRUE(sync.base_url_override.has_value()); + EXPECT_EQ(*sync.base_url_override, "https://example.test"); +} + +TEST_F(ConfigBuilderTest, FDv2_DisableFDv1FallbackClearsIt) { + ConfigBuilder builder("sdk-123"); + builder.DataSystem().Method( + builders::DataSystemBuilder::FDv2().DisableFDv1Fallback()); + + auto cfg = builder.Build(); + auto const fdv2_config = + std::get(cfg->DataSystemConfig().system_); + + EXPECT_FALSE(fdv2_config.fdv1_fallback.has_value()); +} + +TEST_F(ConfigBuilderTest, FDv2_FallbackAndRecoveryTimeouts) { + ConfigBuilder builder("sdk-123"); + builder.DataSystem().Method(builders::DataSystemBuilder::FDv2() + .FallbackTimeout(std::chrono::seconds{30}) + .RecoveryTimeout(std::chrono::seconds{90})); + + auto cfg = builder.Build(); + auto const fdv2_config = + std::get(cfg->DataSystemConfig().system_); + + EXPECT_EQ(fdv2_config.fallback_timeout, std::chrono::seconds{30}); + EXPECT_EQ(fdv2_config.recovery_timeout, std::chrono::seconds{90}); +} + TEST_F(ConfigBuilderTest, DefaultConstruction_HttpPropertyDefaultsAreUsed) { ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); diff --git a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp index 99cd536f9..0dc93034b 100644 --- a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp +++ b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp @@ -110,41 +110,35 @@ TEST(HandleFDv2PollResponseTest, NetworkErrorDoesNotSetFlag) { } TEST(MakeFDv2PollRequestTest, BaseWithTrailingSlashDoesNotProduceDoubleSlash) { - config::shared::built::ServiceEndpoints endpoints{"http://example.com/", "", - ""}; auto props = config::shared::Defaults::HttpProperties(); - auto req = MakeFDv2PollRequest(endpoints, props, data_model::Selector{}, - std::nullopt); + auto req = MakeFDv2PollRequest("http://example.com/", props, + data_model::Selector{}, std::nullopt); EXPECT_EQ(req.Url(), "http://example.com/sdk/poll"); } TEST(MakeFDv2PollRequestTest, BaseWithSubpathTrailingSlashJoinsCleanly) { - config::shared::built::ServiceEndpoints endpoints{ - "http://example.com/relay/", "", ""}; auto props = config::shared::Defaults::HttpProperties(); - auto req = MakeFDv2PollRequest(endpoints, props, data_model::Selector{}, - std::nullopt); + auto req = MakeFDv2PollRequest("http://example.com/relay/", props, + data_model::Selector{}, std::nullopt); EXPECT_EQ(req.Url(), "http://example.com/relay/sdk/poll"); } TEST(MakeFDv2PollRequestTest, ValidFilterKeyIsIncluded) { - config::shared::built::ServiceEndpoints endpoints{"http://example.com", "", - ""}; auto props = config::shared::Defaults::HttpProperties(); - auto req = MakeFDv2PollRequest(endpoints, props, data_model::Selector{}, - std::string{"my-filter_1.0"}); + auto req = + MakeFDv2PollRequest("http://example.com", props, data_model::Selector{}, + std::string{"my-filter_1.0"}); EXPECT_EQ(req.Url(), "http://example.com/sdk/poll?filter=my-filter_1.0"); } TEST(MakeFDv2PollRequestTest, InvalidFilterKeyIsDropped) { - config::shared::built::ServiceEndpoints endpoints{"http://example.com", "", - ""}; auto props = config::shared::Defaults::HttpProperties(); - auto req = MakeFDv2PollRequest(endpoints, props, data_model::Selector{}, - std::string{"has spaces"}); + auto req = + MakeFDv2PollRequest("http://example.com", props, data_model::Selector{}, + std::string{"has spaces"}); EXPECT_EQ(req.Url(), "http://example.com/sdk/poll"); } diff --git a/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp b/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp index 68687adb5..9b9c16285 100644 --- a/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp +++ b/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp @@ -105,11 +105,6 @@ class IoContextRunner { std::thread thread_; }; -config::shared::built::ServiceEndpoints MakeEndpoints(std::string streaming) { - return config::shared::built::ServiceEndpoints( - "polling", std::move(streaming), "events"); -} - config::shared::built::HttpProperties MakeHttpProperties() { return launchdarkly::server_side::config::builders::HttpPropertiesBuilder() .Build(); @@ -149,7 +144,7 @@ TEST(FDv2StreamingSynchronizerTest, NextBadEndpointUrlReturnsTerminalError) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, MakeEndpoints("not a url"), + runner.context().get_executor(), logger, "not a url", MakeHttpProperties(), std::nullopt, 1s); // Act: trigger setup with a malformed streaming URL. URL parsing happens @@ -172,9 +167,8 @@ TEST(FDv2StreamingSynchronizerTest, CloseBeforeNextReturnsShutdown) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); synchronizer.Close(); // Act: call Next on an already-closed synchronizer. @@ -193,9 +187,8 @@ TEST(FDv2StreamingSynchronizerTest, CloseDuringPendingNextResolvesShutdown) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); // Skip the SSE setup; we want Next to be pending purely on the // close/timeout race, not on real network activity. @@ -222,9 +215,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectEmptySelectorNoBasisParam) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("https://stream.example.com"), MakeHttpProperties(), - std::nullopt, 1s); + runner.context().get_executor(), logger, "https://stream.example.com", + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -247,9 +239,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectWithSelectorAppendsBasis) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("https://stream.example.com"), MakeHttpProperties(), - std::nullopt, 1s); + runner.context().get_executor(), logger, "https://stream.example.com", + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -274,9 +265,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectWithFilterKeyAppendsFilter) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("https://stream.example.com"), MakeHttpProperties(), - std::string("my-filter"), 1s); + runner.context().get_executor(), logger, "https://stream.example.com", + MakeHttpProperties(), std::string("my-filter"), 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -299,9 +289,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectInvalidFilterKeyIsDropped) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("https://stream.example.com"), MakeHttpProperties(), - std::string("has spaces"), 1s); + runner.context().get_executor(), logger, "https://stream.example.com", + MakeHttpProperties(), std::string("has spaces"), 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -322,9 +311,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectReconnectUsesLatestSelector) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("https://stream.example.com"), MakeHttpProperties(), - std::nullopt, 1s); + runner.context().get_executor(), logger, "https://stream.example.com", + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -360,9 +348,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectSelectorStateIsPercentEncoded) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("https://stream.example.com"), MakeHttpProperties(), - std::nullopt, 1s); + runner.context().get_executor(), logger, "https://stream.example.com", + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -392,9 +379,8 @@ TEST(FDv2StreamingSynchronizerTest, FullChangesetEventsReturnsChangeSet) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event server_intent("server-intent", @@ -430,9 +416,8 @@ TEST(FDv2StreamingSynchronizerTest, GoodbyeEventReturnsGoodbye) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event goodbye("goodbye", R"({"reason":"bye"})"); @@ -456,9 +441,8 @@ TEST(FDv2StreamingSynchronizerTest, GoodbyeEventTriggersAsyncRestart) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -486,9 +470,8 @@ TEST(FDv2StreamingSynchronizerTest, IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // Begin accumulating a payload that we'll abandon mid-flight via Goodbye. @@ -547,9 +530,8 @@ TEST(FDv2StreamingSynchronizerTest, ServerErrorEventReturnsInterrupted) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event server_error("error", R"({"id":"abc","reason":"oops"})"); @@ -579,9 +561,8 @@ TEST(FDv2StreamingSynchronizerTest, UnknownEventWithGarbageBodyIsIgnored) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -599,9 +580,8 @@ TEST(FDv2StreamingSynchronizerTest, MalformedJsonEventReturnsInterrupted) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -632,9 +612,8 @@ TEST(FDv2StreamingSynchronizerTest, IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -659,9 +638,8 @@ TEST(FDv2StreamingSynchronizerTest, TranslationFailureReturnsInterrupted) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // A non-empty segment object missing required fields triggers a schema @@ -705,9 +683,8 @@ TEST(FDv2StreamingSynchronizerTest, IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Error error{sse::errors::UnrecoverableClientError{ @@ -734,9 +711,8 @@ TEST(FDv2StreamingSynchronizerTest, RecoverableReadTimeoutReturnsInterrupted) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Error error{sse::errors::ReadTimeout{std::chrono::milliseconds(100)}}; @@ -766,9 +742,8 @@ TEST(FDv2StreamingSynchronizerTest, OnResponseDirectivePropagatesToChangeSet) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> headers; @@ -804,9 +779,8 @@ TEST(FDv2StreamingSynchronizerTest, SecondResponseWithoutDirectiveClearsFlag) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> first; @@ -833,9 +807,8 @@ TEST(FDv2StreamingSynchronizerTest, ErrorAfterDirectiveCarriesFlag) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - MakeEndpoints("http://localhost"), MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> headers;