diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index 90e74c4fa..8db6518b4 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -23,6 +23,8 @@ target_sources(${LIBNAME} PRIVATE data_sources/fdv2/streaming_synchronizer.cpp data_sources/fdv2/fdv2_data_source.cpp data_sources/fdv2/cache_initializer.cpp + data_sources/fdv2/source_factories.cpp + data_sources/fdv2/mode_sources.cpp data_sources/fdv2/fdv1_adapter_synchronizer.cpp data_sources/data_source_event_handler.cpp data_sources/polling_data_source.cpp @@ -52,6 +54,8 @@ target_sources(${LIBNAME} PRIVATE data_sources/fdv2/streaming_synchronizer.hpp data_sources/fdv2/fdv2_data_source.hpp data_sources/fdv2/cache_initializer.hpp + data_sources/fdv2/source_factories.hpp + data_sources/fdv2/mode_sources.hpp data_sources/fdv2/fdv1_adapter_synchronizer.hpp flag_manager/flag_store.hpp flag_manager/flag_updater.hpp diff --git a/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp new file mode 100644 index 000000000..acc2d45bc --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp @@ -0,0 +1,163 @@ +#include "mode_sources.hpp" + +#include "cache_initializer.hpp" +#include "source_factories.hpp" + +#include +#include + +#include + +#include +#include + +namespace launchdarkly::client_side::data_sources { + +namespace { + +// Lets std::visit dispatch to a different lambda per variant alternative. +template +struct overloaded : Ts... { + using Ts::operator()...; +}; +template +overloaded(Ts...) -> overloaded; + +FDv2RequestConfig MakeRequestConfig(std::string base_url, + ModeSourceParams const& params, + std::string serialized_context, + bool use_post) { + return FDv2RequestConfig{std::move(base_url), params.http_properties, + std::move(serialized_context), + use_post ? FDv2ContextTransport::kPostBody + : FDv2ContextTransport::kGetPath, + params.with_reasons}; +} + +// Maps a wall-clock instant onto the monotonic clock the poll interval is timed +// against. steady_clock is used while running (for monotonicity) but cannot +// persist across a restart, so the last poll is stored as system_clock and +// mapped back here. +std::chrono::steady_clock::time_point ToSteadyClock( + std::chrono::system_clock::time_point instant) { + auto const now = std::chrono::system_clock::now(); + return std::chrono::steady_clock::now() - (now - instant); +} + +// The FDv1 fallback reuses the client's own FDv1 polling source, which is +// already pointed at the client SDK's FDv1 endpoints. +std::unique_ptr MakeFDv1Fallback( + FDv2Config::FDv1FallbackConfig const& fallback, + ModeSourceParams const& params) { + auto const defaults = + config::shared::Defaults::PollingConfig(); + + config::shared::built::DataSourceConfig const + fdv1_config{ + config::shared::built::PollingConfig{ + fallback.poll_interval, defaults.polling_get_path, + defaults.polling_report_path, defaults.min_polling_interval}, + params.with_reasons, + // FDv2 supersedes the REPORT transport, so the option is + // ignored when both are configured. + /* use_report= */ false}; + + auto endpoints = + fallback.base_url_override + ? config::shared::built:: + ServiceEndpoints{*fallback.base_url_override, + params.endpoints.StreamingBaseUrl(), + params.endpoints.EventsBaseUrl()} + : params.endpoints; + + return std::make_unique( + params.executor, params.logger, std::move(endpoints), fdv1_config, + params.http_properties, params.context); +} + +} // namespace + +ModeSources BuildModeSources(FDv2Config const& config, + ConnectionMode mode, + ModeSourceParams const& params) { + ModeSources sources; + + auto const definition = config.modes.find(mode); + if (definition == config.modes.end()) { + LD_LOG(params.logger, LogLevel::kError) + << "fdv2: connection mode " + << config::shared::GetConnectionModeName(mode) + << " is not configured"; + return sources; + } + + auto const serialized_context = + boost::json::serialize(boost::json::value_from(params.context)); + + for (auto const& entry : definition->second.initializers) { + std::visit( + overloaded{ + [&](FDv2Config::CacheConfig const&) { + sources.initializers.push_back( + std::make_unique( + params.cache, params.context, params.logger)); + }, + [&](FDv2Config::PollingConfig const& polling) { + sources.initializers.push_back( + std::make_unique( + params.executor, params.logger, + MakeRequestConfig( + polling.base_url_override.value_or( + params.polling_base_url), + params, serialized_context, config.use_post))); + }, + }, + entry); + } + + // The interval a poll is rate limited against is measured from the last + // time this context was polled, which survives restarts so that repeated + // launches cannot produce a burst of requests. + std::optional last_poll; + if (auto const freshness = params.cache->ReadFreshness(params.context)) { + last_poll = ToSteadyClock(*freshness); + } + + for (auto const& entry : definition->second.synchronizers) { + std::visit( + overloaded{ + [&](FDv2Config::PollingConfig const& polling) { + sources.synchronizers.push_back( + std::make_unique( + params.executor, params.logger, + MakeRequestConfig( + polling.base_url_override.value_or( + params.polling_base_url), + params, serialized_context, config.use_post), + polling.poll_interval, last_poll)); + }, + [&](FDv2Config::StreamingConfig const& streaming) { + sources.synchronizers.push_back( + std::make_unique( + params.executor, params.logger, + MakeRequestConfig( + streaming.base_url_override.value_or( + params.streaming_base_url), + params, serialized_context, config.use_post), + MakeRequestConfig(params.polling_base_url, params, + serialized_context, + config.use_post), + streaming.initial_reconnect_delay)); + }, + }, + entry); + } + + if (auto const& fallback = definition->second.fdv1_fallback) { + sources.synchronizers.push_back(MakeFDv1Fallback(*fallback, params)); + } + + return sources; +} + +} // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp b/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp new file mode 100644 index 000000000..e7a262507 --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "ifdv2_initializer_factory.hpp" +#include "ifdv2_synchronizer_factory.hpp" + +#include "../../flag_manager/flag_persistence.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace launchdarkly::client_side::data_sources { + +using ConnectionMode = config::shared::ConnectionMode; +using FDv2Config = config::shared::built::FDv2Config; + +/** The factories one connection mode calls for. */ +struct ModeSources { + std::vector> initializers; + std::vector> synchronizers; +}; + +/** + * Everything the sources of a mode need that the mode itself does not say: + * where to send requests, which context to evaluate, and where the cache is. + */ +struct ModeSourceParams { + boost::asio::any_io_executor executor; + Logger logger; + std::string polling_base_url; + std::string streaming_base_url; + config::shared::built::HttpProperties http_properties; + /** + * Where the FDv1 fallback source sends its requests. FDv2 sources use + * the resolved base URLs above instead. + */ + config::shared::built::ServiceEndpoints endpoints; + Context context; + /** Whether the application asked for evaluation reasons. */ + bool with_reasons; + /** Non-owning. Must outlive sources built from these params. */ + flag_manager::FlagPersistence* cache; +}; + +/** + * Assembles the factories the given mode calls for. Returns empty lists if + * the configuration does not define the mode. + * + * A mode that configures an FDv1 fallback gets its synchronizer appended to + * the synchronizer list. + */ +ModeSources BuildModeSources(FDv2Config const& config, + ConnectionMode mode, + ModeSourceParams const& params); + +} // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp b/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp new file mode 100644 index 000000000..18690808a --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp @@ -0,0 +1,86 @@ +#include "source_factories.hpp" + +#include "../polling_data_source.hpp" +#include "fdv1_adapter_synchronizer.hpp" +#include "polling_initializer.hpp" +#include "polling_synchronizer.hpp" +#include "streaming_synchronizer.hpp" + +#include + +namespace launchdarkly::client_side::data_sources { + +FDv2PollingInitializerFactory::FDv2PollingInitializerFactory( + boost::asio::any_io_executor executor, + Logger logger, + FDv2RequestConfig request_config) + : executor_(std::move(executor)), + logger_(std::move(logger)), + request_config_(std::move(request_config)) {} + +std::unique_ptr FDv2PollingInitializerFactory::Build() { + return std::make_unique(executor_, logger_, + request_config_); +} + +FDv2PollingSynchronizerFactory::FDv2PollingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + FDv2RequestConfig request_config, + std::chrono::seconds poll_interval, + std::optional last_poll) + : executor_(std::move(executor)), + logger_(std::move(logger)), + request_config_(std::move(request_config)), + poll_interval_(poll_interval), + last_poll_(last_poll) {} + +std::unique_ptr FDv2PollingSynchronizerFactory::Build() { + return std::make_unique( + executor_, logger_, request_config_, poll_interval_, last_poll_); +} + +FDv2StreamingSynchronizerFactory::FDv2StreamingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + FDv2RequestConfig stream_config, + FDv2RequestConfig poll_config, + std::chrono::milliseconds initial_reconnect_delay) + : executor_(std::move(executor)), + logger_(std::move(logger)), + stream_config_(std::move(stream_config)), + poll_config_(std::move(poll_config)), + initial_reconnect_delay_(initial_reconnect_delay) {} + +std::unique_ptr FDv2StreamingSynchronizerFactory::Build() { + return std::make_unique( + executor_, logger_, stream_config_, poll_config_, + initial_reconnect_delay_); +} + +FDv1PollingAdapterFactory::FDv1PollingAdapterFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::shared::built::ServiceEndpoints endpoints, + config::shared::built::DataSourceConfig + data_source_config, + config::shared::built::HttpProperties http_properties, + Context context) + : executor_(std::move(executor)), + logger_(std::move(logger)), + endpoints_(std::move(endpoints)), + data_source_config_(std::move(data_source_config)), + http_properties_(std::move(http_properties)), + context_(std::move(context)) {} + +std::unique_ptr FDv1PollingAdapterFactory::Build() { + return std::make_unique( + [this](IDataSourceUpdateSink* sink, + DataSourceStatusManager* status_manager) { + return std::make_shared( + endpoints_, data_source_config_, http_properties_, executor_, + context_, *sink, *status_manager, logger_); + }); +} + +} // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp b/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp new file mode 100644 index 000000000..0cb522362 --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include "fdv2_request_config.hpp" +#include "ifdv2_initializer_factory.hpp" +#include "ifdv2_synchronizer_factory.hpp" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace launchdarkly::client_side::data_sources { + +/** + * Builds fresh FDv2PollingInitializer instances on demand. + * + * Thread-safe: Build() may be called from any thread, and the + * configuration it hands to each source is fixed at construction. + */ +class FDv2PollingInitializerFactory final : public IFDv2InitializerFactory { + public: + FDv2PollingInitializerFactory(boost::asio::any_io_executor executor, + Logger logger, + FDv2RequestConfig request_config); + + std::unique_ptr Build() override; + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + FDv2RequestConfig const request_config_; +}; + +/** + * Builds fresh FDv2PollingSynchronizer instances on demand. + * + * Thread-safe: Build() may be called from any thread, and the + * configuration it hands to each source is fixed at construction. + */ +class FDv2PollingSynchronizerFactory final : public IFDv2SynchronizerFactory { + public: + /** + * @param last_poll When this context was last polled, if that is known, + * so that the first poll after the synchronizer starts still respects the + * interval. + */ + FDv2PollingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + FDv2RequestConfig request_config, + std::chrono::seconds poll_interval, + std::optional last_poll); + + std::unique_ptr Build() override; + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + FDv2RequestConfig const request_config_; + std::chrono::seconds const poll_interval_; + std::optional const last_poll_; +}; + +/** + * Builds fresh FDv2StreamingSynchronizer instances on demand. + * + * Thread-safe: Build() may be called from any thread, and the + * configuration it hands to each source is fixed at construction. + */ +class FDv2StreamingSynchronizerFactory final : public IFDv2SynchronizerFactory { + public: + /** + * @param poll_config Where to poll in answer to a `ping` event on the + * stream. Must describe the same context as stream_config. + */ + FDv2StreamingSynchronizerFactory( + boost::asio::any_io_executor executor, + Logger logger, + FDv2RequestConfig stream_config, + FDv2RequestConfig poll_config, + std::chrono::milliseconds initial_reconnect_delay); + + std::unique_ptr Build() override; + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + FDv2RequestConfig const stream_config_; + FDv2RequestConfig const poll_config_; + std::chrono::milliseconds const initial_reconnect_delay_; +}; + +/** + * Builds fresh FDv1AdapterSynchronizer instances wrapping a freshly-built + * FDv1 polling source. + * + * The synchronizers it builds report themselves as the FDv1 tier. + * + * Thread-safe: Build() may be called from any thread, and the + * configuration it hands to each source is fixed at construction. + */ +class FDv1PollingAdapterFactory final : public IFDv2SynchronizerFactory { + public: + FDv1PollingAdapterFactory( + boost::asio::any_io_executor executor, + Logger logger, + config::shared::built::ServiceEndpoints endpoints, + config::shared::built::DataSourceConfig + data_source_config, + config::shared::built::HttpProperties http_properties, + Context context); + + std::unique_ptr Build() override; + + [[nodiscard]] bool IsFDv1Fallback() const override { return true; } + + private: + boost::asio::any_io_executor const executor_; + Logger const logger_; + config::shared::built::ServiceEndpoints const endpoints_; + config::shared::built::DataSourceConfig const + data_source_config_; + config::shared::built::HttpProperties const http_properties_; + Context const context_; +}; + +} // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/tests/fdv2_mode_sources_test.cpp b/libs/client-sdk/tests/fdv2_mode_sources_test.cpp new file mode 100644 index 000000000..e9248963d --- /dev/null +++ b/libs/client-sdk/tests/fdv2_mode_sources_test.cpp @@ -0,0 +1,181 @@ +#include + +#include +#include + +#include +#include +#include + +#include + +using launchdarkly::ContextBuilder; +using launchdarkly::client_side::flag_manager::FlagManager; +using launchdarkly::config::shared::GetConnectionModeName; + +using namespace launchdarkly::client_side::data_sources; + +namespace { + +class ModeSourcesFixture : public ::testing::Test { + public: + ModeSourcesFixture() + : logger_(launchdarkly::logging::NullLogger()), + flag_manager_("sdk-key", logger_, 5, nullptr) {} + + static FDv2Config Defaults() { + return launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::FDv2Config(); + } + + ModeSourceParams Params() { + return ModeSourceParams{ + ioc_.get_executor(), + logger_, + "https://polling.example.com", + "https://streaming.example.com", + launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::HttpProperties(), + launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::ServiceEndpoints(), + ContextBuilder().Kind("user", "user-key").Build(), + /* with_reasons= */ false, + &flag_manager_.Cache()}; + } + + private: + boost::asio::io_context ioc_; + launchdarkly::Logger logger_; + FlagManager flag_manager_; +}; + +} // namespace + +TEST_F(ModeSourcesFixture, StreamingModeInitializesFromCacheThenPolls) { + auto sources = + BuildModeSources(Defaults(), ConnectionMode::kStreaming, Params()); + + ASSERT_EQ(2u, sources.initializers.size()); + EXPECT_TRUE(sources.initializers[0]->IsFromCache()); + EXPECT_FALSE(sources.initializers[1]->IsFromCache()); +} + +// Streaming is the primary tier and polling the fallback, so the SDK keeps +// receiving updates when a stream cannot be maintained. +TEST_F(ModeSourcesFixture, StreamingModeFallsBackToPolling) { + auto sources = + BuildModeSources(Defaults(), ConnectionMode::kStreaming, Params()); + + // Streaming, polling, and FDv1. + ASSERT_EQ(3u, sources.synchronizers.size()); + EXPECT_EQ("FDv2 streaming synchronizer", + sources.synchronizers[0]->Build()->Identity()); + EXPECT_EQ("FDv2 polling synchronizer", + sources.synchronizers[1]->Build()->Identity()); +} + +TEST_F(ModeSourcesFixture, PollingModeInitializesFromCacheOnly) { + auto sources = + BuildModeSources(Defaults(), ConnectionMode::kPolling, Params()); + + ASSERT_EQ(1u, sources.initializers.size()); + EXPECT_TRUE(sources.initializers[0]->IsFromCache()); + ASSERT_EQ(2u, sources.synchronizers.size()); + EXPECT_EQ("FDv2 polling synchronizer", + sources.synchronizers[0]->Build()->Identity()); +} + +// Offline still loads persisted flags, but makes no requests. +TEST_F(ModeSourcesFixture, OfflineModeHasOnlyTheCacheInitializer) { + auto sources = + BuildModeSources(Defaults(), ConnectionMode::kOffline, Params()); + + ASSERT_EQ(1u, sources.initializers.size()); + EXPECT_TRUE(sources.initializers[0]->IsFromCache()); + EXPECT_TRUE(sources.synchronizers.empty()); +} + +TEST_F(ModeSourcesFixture, UnconfiguredModeProducesNoSources) { + auto config = launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::FDv2Config(); + config.modes.erase(ConnectionMode::kPolling); + + auto sources = BuildModeSources(config, ConnectionMode::kPolling, Params()); + + EXPECT_TRUE(sources.initializers.empty()); + EXPECT_TRUE(sources.synchronizers.empty()); +} + +TEST_F(ModeSourcesFixture, AnOverriddenModeReplacesTheBuiltInPipeline) { + auto config = launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::FDv2Config(); + config.modes[ConnectionMode::kStreaming] = FDv2Config::ModeDefinition{ + {FDv2Config::CacheConfig{}}, + {FDv2Config::StreamingConfig{std::chrono::seconds{1}, std::nullopt}}, + std::nullopt}; + + auto sources = + BuildModeSources(config, ConnectionMode::kStreaming, Params()); + + EXPECT_EQ(1u, sources.initializers.size()); + ASSERT_EQ(1u, sources.synchronizers.size()); + EXPECT_EQ("FDv2 streaming synchronizer", + sources.synchronizers[0]->Build()->Identity()); +} + +TEST(ConnectionModeTest, ModesAreNamedAsTheConfigurationSpellsThem) { + EXPECT_STREQ("streaming", + GetConnectionModeName(ConnectionMode::kStreaming)); + EXPECT_STREQ("polling", GetConnectionModeName(ConnectionMode::kPolling)); + EXPECT_STREQ("offline", GetConnectionModeName(ConnectionMode::kOffline)); +} + +// Desktop starts in streaming, and provides no background mode. +TEST(FDv2ConfigTest, DefaultsProvideTheThreeDesktopModes) { + auto const config = launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::FDv2Config(); + + EXPECT_EQ(ConnectionMode::kStreaming, config.initial_mode); + EXPECT_EQ(3u, config.modes.size()); + EXPECT_EQ(1u, config.modes.count(ConnectionMode::kStreaming)); + EXPECT_EQ(1u, config.modes.count(ConnectionMode::kPolling)); + EXPECT_EQ(1u, config.modes.count(ConnectionMode::kOffline)); +} + +TEST(FDv2ConfigTest, DefaultTimeoutsAreTwoAndFiveMinutes) { + auto const config = launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::FDv2Config(); + + EXPECT_EQ(std::chrono::seconds{120}, config.fallback_timeout); + EXPECT_EQ(std::chrono::seconds{300}, config.recovery_timeout); +} + +TEST(FDv2ConfigTest, ModesThatMakeRequestsConfigureAnFDv1Fallback) { + auto const config = launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ClientSDK>::FDv2Config(); + + EXPECT_TRUE( + config.modes.at(ConnectionMode::kStreaming).fdv1_fallback.has_value()); + EXPECT_TRUE( + config.modes.at(ConnectionMode::kPolling).fdv1_fallback.has_value()); + EXPECT_FALSE( + config.modes.at(ConnectionMode::kOffline).fdv1_fallback.has_value()); +} + +// The FDv1 tier is appended after the mode's own synchronizers. +TEST_F(ModeSourcesFixture, ModesWithAFallbackAppendTheFDv1Tier) { + auto sources = + BuildModeSources(Defaults(), ConnectionMode::kStreaming, Params()); + + ASSERT_FALSE(sources.synchronizers.empty()); + auto const& last = sources.synchronizers.back(); + EXPECT_TRUE(last->IsFDv1Fallback()); + EXPECT_EQ("FDv1 fallback adapter", last->Build()->Identity()); +} + +TEST_F(ModeSourcesFixture, ModesWithNoFallbackAppendNothing) { + auto sources = + BuildModeSources(Defaults(), ConnectionMode::kOffline, Params()); + + EXPECT_TRUE(sources.synchronizers.empty()); +} diff --git a/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp b/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp new file mode 100644 index 000000000..b0df26091 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::config::shared::built { + +template +struct FDv2Config; + +/** + * The FDv2 data system configuration: which connection mode the SDK starts + * in, what each mode's sources are, and the settings shared across them. + */ +template <> +struct FDv2Config { + /** Reads flag data the SDK persisted on a previous run. */ + struct CacheConfig {}; + + struct StreamingConfig { + std::chrono::milliseconds initial_reconnect_delay; + /** Overrides the streaming base URL for this source alone. */ + std::optional base_url_override; + }; + + struct PollingConfig { + std::chrono::seconds poll_interval; + /** Overrides the polling base URL for this source alone. */ + std::optional base_url_override; + }; + + /** + * The FDv1 polling source used while the service has directed the SDK + * away from FDv2. + */ + struct FDv1FallbackConfig { + std::chrono::seconds poll_interval; + /** Overrides the polling base URL for the fallback alone. */ + std::optional base_url_override; + }; + + using InitializerEntry = std::variant; + using SynchronizerEntry = std::variant; + + /** + * What one connection mode does. The synchronizer list is ordered by + * preference. The first entry is the primary, and later entries are the + * tiers the SDK falls back to. + */ + struct ModeDefinition { + std::vector initializers; + std::vector synchronizers; + std::optional fdv1_fallback; + }; + + /** The mode the SDK starts in. */ + ConnectionMode initial_mode; + + /** Base URL for polling sources that do not override it. */ + std::string polling_base_url; + /** Base URL for streaming sources that do not override it. */ + std::string streaming_base_url; + + /** What each mode does. Modes absent from the map are unavailable. */ + std::map modes; + + /** + * Whether to send the evaluation context in a request body rather than + * base64url-encoded into the request path. + */ + bool use_post; + + /** + * How long the active synchronizer may remain interrupted before the SDK + * falls back to the next tier. + */ + std::chrono::milliseconds fallback_timeout; + + /** + * How long a fallback tier must run before the SDK attempts to return to + * the preferred one. + */ + std::chrono::milliseconds recovery_timeout; +}; + +} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/connection_mode.hpp b/libs/common/include/launchdarkly/config/shared/connection_mode.hpp new file mode 100644 index 000000000..858859a69 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/connection_mode.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace launchdarkly::config::shared { + +/** + * A named data system configuration: which sources the SDK uses to load flag + * data, and which it uses to keep that data current. + */ +enum class ConnectionMode { + /** Stream updates, falling back to polling. */ + kStreaming, + /** Poll for updates on an interval. */ + kPolling, + /** Evaluate against whatever is cached, and make no requests. */ + kOffline, +}; + +/** The mode's name as the configuration API spells it. */ +char const* GetConnectionModeName(ConnectionMode mode); + +} // namespace launchdarkly::config::shared diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 0fc0cb9d9..5c8e4f8ad 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -75,6 +76,50 @@ struct Defaults { "/msdk/evalx/context", std::chrono::minutes(5)}; } + /** + * The three connection modes a desktop SDK provides, starting in + * streaming, with automatic mode switching off. + */ + static auto FDv2Config() -> shared::built::FDv2Config { + using Config = shared::built::FDv2Config; + + auto const fallback_timeout = std::chrono::seconds(120); + auto const recovery_timeout = std::chrono::seconds(300); + + Config::StreamingConfig const streaming{std::chrono::seconds(1), + std::nullopt}; + Config::PollingConfig const polling{std::chrono::minutes(5), + std::nullopt}; + Config::FDv1FallbackConfig const fdv1_fallback{std::chrono::minutes(5), + std::nullopt}; + + return { + shared::ConnectionMode::kStreaming, + "https://sdk.launchdarkly.com", + "https://clientstream.launchdarkly.com", + { + // Streaming initializes from the cache, then polls for a + // basis so that the stream can deliver only what changed, + // and falls back to polling if the stream cannot be kept up. + {shared::ConnectionMode::kStreaming, + Config::ModeDefinition{{Config::CacheConfig{}, polling}, + {streaming, polling}, + fdv1_fallback}}, + {shared::ConnectionMode::kPolling, + Config::ModeDefinition{ + {Config::CacheConfig{}}, {polling}, fdv1_fallback}}, + // Offline evaluates against the cache and makes no requests, + // so it has nothing to fall back to. + {shared::ConnectionMode::kOffline, + Config::ModeDefinition{ + {Config::CacheConfig{}}, {}, std::nullopt}}, + }, + /* use_post= */ false, + fallback_timeout, + recovery_timeout, + }; + } + static std::size_t MaxCachedContexts() { return 5; } }; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index b0a9446b2..c39644fd0 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -28,6 +28,7 @@ add_library(${LIBNAME} OBJECT attributes_builder.cpp error.cpp config/service_endpoints.cpp + config/connection_mode.cpp config/events.cpp config/endpoints_builder.cpp config/events_builder.cpp diff --git a/libs/common/src/config/connection_mode.cpp b/libs/common/src/config/connection_mode.cpp new file mode 100644 index 000000000..2bd40630b --- /dev/null +++ b/libs/common/src/config/connection_mode.cpp @@ -0,0 +1,17 @@ +#include + +namespace launchdarkly::config::shared { + +char const* GetConnectionModeName(ConnectionMode mode) { + switch (mode) { + case ConnectionMode::kStreaming: + return "streaming"; + case ConnectionMode::kPolling: + return "polling"; + case ConnectionMode::kOffline: + return "offline"; + } + return "unknown"; +} + +} // namespace launchdarkly::config::shared