From e9d1d489d042971ac5174c8800a9ad2c2b24f6e9 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Wed, 2 Sep 2026 03:30:25 +0000 Subject: [PATCH 1/5] feat: Represent the client FDv2 connection modes and assemble their sources --- libs/client-sdk/src/CMakeLists.txt | 4 + .../src/data_sources/fdv2/mode_sources.cpp | 132 +++++++++++++++ .../src/data_sources/fdv2/mode_sources.hpp | 60 +++++++ .../data_sources/fdv2/source_factories.cpp | 59 +++++++ .../data_sources/fdv2/source_factories.hpp | 96 +++++++++++ .../tests/fdv2_mode_sources_test.cpp | 160 ++++++++++++++++++ .../config/shared/built/fdv2_config.hpp | 97 +++++++++++ .../config/shared/connection_mode.hpp | 23 +++ .../launchdarkly/config/shared/defaults.hpp | 47 +++++ libs/common/src/CMakeLists.txt | 1 + libs/common/src/config/connection_mode.cpp | 17 ++ 11 files changed, 696 insertions(+) create mode 100644 libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp create mode 100644 libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp create mode 100644 libs/client-sdk/src/data_sources/fdv2/source_factories.cpp create mode 100644 libs/client-sdk/src/data_sources/fdv2/source_factories.hpp create mode 100644 libs/client-sdk/tests/fdv2_mode_sources_test.cpp create mode 100644 libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/connection_mode.hpp create mode 100644 libs/common/src/config/connection_mode.cpp 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..2d3bfa98c --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp @@ -0,0 +1,132 @@ +#include "mode_sources.hpp" + +#include "cache_initializer.hpp" +#include "source_factories.hpp" + +#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}; +} + +// The poll interval is measured on the monotonic clock, but the last poll was +// recorded on the wall clock so that it could be persisted. An instant in the +// future means the wall clock moved backwards since it was written, which +// says nothing usable about when the last poll happened. +std::optional ToSteadyClock( + std::optional instant) { + if (!instant) { + return std::nullopt; + } + auto const now = std::chrono::system_clock::now(); + if (*instant > now) { + return std::nullopt; + } + return std::chrono::steady_clock::now() - (now - *instant); +} + +} // 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)); + + auto polling_config = [&](FDv2Config::PollingConfig const& polling) { + return MakeRequestConfig( + polling.base_url_override.value_or(params.polling_base_url), params, + serialized_context, config.use_post); + }; + auto streaming_config = [&](FDv2Config::StreamingConfig const& streaming) { + return MakeRequestConfig( + streaming.base_url_override.value_or(params.streaming_base_url), + params, serialized_context, config.use_post); + }; + + 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, + polling_config(polling))); + }, + }, + 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. + auto const last_poll = + ToSteadyClock(params.cache->FreshnessFor(params.context)); + + 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, + polling_config(polling), polling.poll_interval, + last_poll)); + }, + [&](FDv2Config::StreamingConfig const& streaming) { + sources.synchronizers.push_back( + std::make_unique( + params.executor, params.logger, + streaming_config(streaming), + polling_config(FDv2Config::PollingConfig{ + std::chrono::seconds::zero(), std::nullopt}), + streaming.initial_reconnect_delay)); + }, + }, + entry); + } + + 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..b22837ec0 --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp @@ -0,0 +1,60 @@ +#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 + +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; + /** Used by any source that does not configure a URL of its own. */ + std::string polling_base_url; + std::string streaming_base_url; + config::shared::built::HttpProperties http_properties; + Context context; + /** Whether the application asked for evaluation reasons. */ + bool with_reasons; + /** + * The local cache, read by the cache initializer and for the last time + * this context was polled. Non-owning. Must outlive the 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. + */ +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..593350a1b --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp @@ -0,0 +1,59 @@ +#include "source_factories.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_); +} + +} // 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..f5cc48afe --- /dev/null +++ b/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include "fdv2_request_config.hpp" +#include "ifdv2_initializer_factory.hpp" +#include "ifdv2_synchronizer_factory.hpp" + +#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_; +}; + +} // 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..8a0780052 --- /dev/null +++ b/libs/client-sdk/tests/fdv2_mode_sources_test.cpp @@ -0,0 +1,160 @@ +#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(), + 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()); + + ASSERT_EQ(2u, 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(1u, 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()); +} 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..eb55c52f2 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp @@ -0,0 +1,97 @@ +#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; + + /** + * Where a source sends its requests when it does not override the URL + * itself. FDv2's endpoints are not the ones FDv1 uses, so these hold + * FDv2's own defaults. When the application configures its own endpoints, + * these are resolved to those instead. + */ + std::string polling_base_url; + 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..6aa5feda2 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/connection_mode.hpp @@ -0,0 +1,23 @@ +#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..ab3c6eda8 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,52 @@ 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; + + // Both timeouts are chosen for consistency with the other + // LaunchDarkly SDKs. + 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 From 797d51255b4f1869ca85b16b71f2d6725586634b Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Mon, 14 Sep 2026 15:19:45 -0700 Subject: [PATCH 2/5] refactor: Call ReadFreshness from the FDv2 connection-mode sources --- libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp index 2d3bfa98c..f159096c2 100644 --- a/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp @@ -101,7 +101,7 @@ ModeSources BuildModeSources(FDv2Config const& config, // time this context was polled, which survives restarts so that repeated // launches cannot produce a burst of requests. auto const last_poll = - ToSteadyClock(params.cache->FreshnessFor(params.context)); + ToSteadyClock(params.cache->ReadFreshness(params.context)); for (auto const& entry : definition->second.synchronizers) { std::visit( From 459cab82bc268f086e2298b550d1427e7011df7d Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Fri, 25 Sep 2026 18:05:11 -0700 Subject: [PATCH 3/5] refactor: Simplify the FDv2 connection-mode source assembly --- .../src/data_sources/fdv2/mode_sources.cpp | 59 +++++++++---------- .../config/shared/built/fdv2_config.hpp | 8 +-- 2 files changed, 29 insertions(+), 38 deletions(-) diff --git a/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp index f159096c2..1647db20e 100644 --- a/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp @@ -33,20 +33,14 @@ FDv2RequestConfig MakeRequestConfig(std::string base_url, params.with_reasons}; } -// The poll interval is measured on the monotonic clock, but the last poll was -// recorded on the wall clock so that it could be persisted. An instant in the -// future means the wall clock moved backwards since it was written, which -// says nothing usable about when the last poll happened. -std::optional ToSteadyClock( - std::optional instant) { - if (!instant) { - return std::nullopt; - } +// 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(); - if (*instant > now) { - return std::nullopt; - } - return std::chrono::steady_clock::now() - (now - *instant); + return std::chrono::steady_clock::now() - (now - instant); } } // namespace @@ -68,17 +62,6 @@ ModeSources BuildModeSources(FDv2Config const& config, auto const serialized_context = boost::json::serialize(boost::json::value_from(params.context)); - auto polling_config = [&](FDv2Config::PollingConfig const& polling) { - return MakeRequestConfig( - polling.base_url_override.value_or(params.polling_base_url), params, - serialized_context, config.use_post); - }; - auto streaming_config = [&](FDv2Config::StreamingConfig const& streaming) { - return MakeRequestConfig( - streaming.base_url_override.value_or(params.streaming_base_url), - params, serialized_context, config.use_post); - }; - for (auto const& entry : definition->second.initializers) { std::visit( overloaded{ @@ -91,7 +74,10 @@ ModeSources BuildModeSources(FDv2Config const& config, sources.initializers.push_back( std::make_unique( params.executor, params.logger, - polling_config(polling))); + MakeRequestConfig( + polling.base_url_override.value_or( + params.polling_base_url), + params, serialized_context, config.use_post))); }, }, entry); @@ -100,8 +86,10 @@ ModeSources BuildModeSources(FDv2Config const& config, // 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. - auto const last_poll = - ToSteadyClock(params.cache->ReadFreshness(params.context)); + 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( @@ -110,16 +98,23 @@ ModeSources BuildModeSources(FDv2Config const& config, sources.synchronizers.push_back( std::make_unique( params.executor, params.logger, - polling_config(polling), polling.poll_interval, - last_poll)); + 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, - streaming_config(streaming), - polling_config(FDv2Config::PollingConfig{ - std::chrono::seconds::zero(), std::nullopt}), + 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)); }, }, diff --git a/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp b/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp index eb55c52f2..b0df26091 100644 --- a/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/fdv2_config.hpp @@ -63,13 +63,9 @@ struct FDv2Config { /** The mode the SDK starts in. */ ConnectionMode initial_mode; - /** - * Where a source sends its requests when it does not override the URL - * itself. FDv2's endpoints are not the ones FDv1 uses, so these hold - * FDv2's own defaults. When the application configures its own endpoints, - * these are resolved to those instead. - */ + /** 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. */ From 4882e49c489540a200cbf997d9e4b5aa8d103413 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Sat, 26 Sep 2026 00:31:50 -0700 Subject: [PATCH 4/5] feat: Wire the FDv1 fallback tier into the connection-mode assembly --- .../src/data_sources/fdv2/mode_sources.cpp | 36 +++++++++++++++++ .../src/data_sources/fdv2/mode_sources.hpp | 11 +++++ .../data_sources/fdv2/source_factories.cpp | 27 +++++++++++++ .../data_sources/fdv2/source_factories.hpp | 40 +++++++++++++++++++ .../tests/fdv2_mode_sources_test.cpp | 25 +++++++++++- 5 files changed, 137 insertions(+), 2 deletions(-) diff --git a/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp index 1647db20e..acc2d45bc 100644 --- a/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.cpp @@ -3,6 +3,7 @@ #include "cache_initializer.hpp" #include "source_factories.hpp" +#include #include #include @@ -43,6 +44,37 @@ std::chrono::steady_clock::time_point ToSteadyClock( 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, @@ -121,6 +153,10 @@ ModeSources BuildModeSources(FDv2Config const& config, entry); } + if (auto const& fallback = definition->second.fdv1_fallback) { + sources.synchronizers.push_back(MakeFDv1Fallback(*fallback, params)); + } + return 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 index b22837ec0..a66837ccc 100644 --- a/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -38,6 +39,11 @@ struct ModeSourceParams { 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; @@ -52,6 +58,11 @@ struct ModeSourceParams { /** * 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 list. The orchestrator holds that tier in reserve rather than using it + * in rotation. It is started only while the service has directed the SDK away + * from FDv2. */ ModeSources BuildModeSources(FDv2Config const& config, ConnectionMode mode, diff --git a/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp b/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp index 593350a1b..18690808a 100644 --- a/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp +++ b/libs/client-sdk/src/data_sources/fdv2/source_factories.cpp @@ -1,5 +1,7 @@ #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" @@ -56,4 +58,29 @@ std::unique_ptr FDv2StreamingSynchronizerFactory::Build() { 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 index f5cc48afe..59a6b0690 100644 --- a/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp +++ b/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp @@ -4,6 +4,10 @@ #include "ifdv2_initializer_factory.hpp" #include "ifdv2_synchronizer_factory.hpp" +#include +#include +#include +#include #include #include @@ -93,4 +97,40 @@ class FDv2StreamingSynchronizerFactory final : public IFDv2SynchronizerFactory { 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, which the + * orchestrator keeps in reserve until the service directs the SDK away from + * FDv2. + * + * 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 index 8a0780052..250565ff8 100644 --- a/libs/client-sdk/tests/fdv2_mode_sources_test.cpp +++ b/libs/client-sdk/tests/fdv2_mode_sources_test.cpp @@ -36,6 +36,8 @@ class ModeSourcesFixture : public ::testing::Test { "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()}; @@ -64,7 +66,7 @@ TEST_F(ModeSourcesFixture, StreamingModeFallsBackToPolling) { auto sources = BuildModeSources(Defaults(), ConnectionMode::kStreaming, Params()); - ASSERT_EQ(2u, sources.synchronizers.size()); + ASSERT_EQ(3u, sources.synchronizers.size()); EXPECT_EQ("FDv2 streaming synchronizer", sources.synchronizers[0]->Build()->Identity()); EXPECT_EQ("FDv2 polling synchronizer", @@ -77,7 +79,7 @@ TEST_F(ModeSourcesFixture, PollingModeInitializesFromCacheOnly) { ASSERT_EQ(1u, sources.initializers.size()); EXPECT_TRUE(sources.initializers[0]->IsFromCache()); - ASSERT_EQ(1u, sources.synchronizers.size()); + ASSERT_EQ(2u, sources.synchronizers.size()); EXPECT_EQ("FDv2 polling synchronizer", sources.synchronizers[0]->Build()->Identity()); } @@ -158,3 +160,22 @@ TEST(FDv2ConfigTest, ModesThatMakeRequestsConfigureAnFDv1Fallback) { EXPECT_FALSE( config.modes.at(ConnectionMode::kOffline).fdv1_fallback.has_value()); } + +// The FDv1 tier is appended last, and the orchestrator keeps it blocked until +// the service directs the SDK away from FDv2. +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()); +} From d849b821cf7155a26b11eba9714efca17f0774c2 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Tue, 6 Oct 2026 10:21:54 -0700 Subject: [PATCH 5/5] chore: Tighten the connection mode comments --- .../client-sdk/src/data_sources/fdv2/mode_sources.hpp | 11 ++--------- .../src/data_sources/fdv2/source_factories.hpp | 4 +--- libs/client-sdk/tests/fdv2_mode_sources_test.cpp | 4 ++-- .../launchdarkly/config/shared/connection_mode.hpp | 4 +--- .../include/launchdarkly/config/shared/defaults.hpp | 2 -- 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp b/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp index a66837ccc..e7a262507 100644 --- a/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp +++ b/libs/client-sdk/src/data_sources/fdv2/mode_sources.hpp @@ -35,7 +35,6 @@ struct ModeSources { struct ModeSourceParams { boost::asio::any_io_executor executor; Logger logger; - /** Used by any source that does not configure a URL of its own. */ std::string polling_base_url; std::string streaming_base_url; config::shared::built::HttpProperties http_properties; @@ -47,11 +46,7 @@ struct ModeSourceParams { Context context; /** Whether the application asked for evaluation reasons. */ bool with_reasons; - /** - * The local cache, read by the cache initializer and for the last time - * this context was polled. Non-owning. Must outlive the sources built - * from these params. - */ + /** Non-owning. Must outlive sources built from these params. */ flag_manager::FlagPersistence* cache; }; @@ -60,9 +55,7 @@ struct ModeSourceParams { * the configuration does not define the mode. * * A mode that configures an FDv1 fallback gets its synchronizer appended to - * the list. The orchestrator holds that tier in reserve rather than using it - * in rotation. It is started only while the service has directed the SDK away - * from FDv2. + * the synchronizer list. */ ModeSources BuildModeSources(FDv2Config const& config, ConnectionMode mode, diff --git a/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp b/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp index 59a6b0690..0cb522362 100644 --- a/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp +++ b/libs/client-sdk/src/data_sources/fdv2/source_factories.hpp @@ -101,9 +101,7 @@ class FDv2StreamingSynchronizerFactory final : public IFDv2SynchronizerFactory { * Builds fresh FDv1AdapterSynchronizer instances wrapping a freshly-built * FDv1 polling source. * - * The synchronizers it builds report themselves as the FDv1 tier, which the - * orchestrator keeps in reserve until the service directs the SDK away from - * FDv2. + * 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. diff --git a/libs/client-sdk/tests/fdv2_mode_sources_test.cpp b/libs/client-sdk/tests/fdv2_mode_sources_test.cpp index 250565ff8..e9248963d 100644 --- a/libs/client-sdk/tests/fdv2_mode_sources_test.cpp +++ b/libs/client-sdk/tests/fdv2_mode_sources_test.cpp @@ -66,6 +66,7 @@ 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()); @@ -161,8 +162,7 @@ TEST(FDv2ConfigTest, ModesThatMakeRequestsConfigureAnFDv1Fallback) { config.modes.at(ConnectionMode::kOffline).fdv1_fallback.has_value()); } -// The FDv1 tier is appended last, and the orchestrator keeps it blocked until -// the service directs the SDK away from FDv2. +// The FDv1 tier is appended after the mode's own synchronizers. TEST_F(ModeSourcesFixture, ModesWithAFallbackAppendTheFDv1Tier) { auto sources = BuildModeSources(Defaults(), ConnectionMode::kStreaming, Params()); diff --git a/libs/common/include/launchdarkly/config/shared/connection_mode.hpp b/libs/common/include/launchdarkly/config/shared/connection_mode.hpp index 6aa5feda2..858859a69 100644 --- a/libs/common/include/launchdarkly/config/shared/connection_mode.hpp +++ b/libs/common/include/launchdarkly/config/shared/connection_mode.hpp @@ -15,9 +15,7 @@ enum class ConnectionMode { kOffline, }; -/** - * The mode's name as the configuration API spells it. - */ +/** 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 ab3c6eda8..5c8e4f8ad 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -83,8 +83,6 @@ struct Defaults { static auto FDv2Config() -> shared::built::FDv2Config { using Config = shared::built::FDv2Config; - // Both timeouts are chosen for consistency with the other - // LaunchDarkly SDKs. auto const fallback_timeout = std::chrono::seconds(120); auto const recovery_timeout = std::chrono::seconds(300);