From 6436533b0a5fc99ccf42e1f4dbbf8311fb73ee15 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 18:33:42 +0000 Subject: [PATCH 1/6] feat: Propagate environment ID to hooks Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- .../server-contract-tests/src/main.cpp | 1 + libs/server-sdk/src/CMakeLists.txt | 2 + libs/server-sdk/src/client_impl.cpp | 40 +++--- libs/server-sdk/src/client_impl.hpp | 6 + .../environment_id/environment_id.cpp | 18 +++ .../environment_id/environment_id.hpp | 38 ++++++ .../background_sync_system.cpp | 7 +- .../background_sync_system.hpp | 4 +- .../sources/polling/polling_data_source.cpp | 15 ++- .../sources/polling/polling_data_source.hpp | 21 ++-- .../streaming/streaming_data_source.cpp | 25 +++- .../streaming/streaming_data_source.hpp | 9 +- .../data_systems/fdv2/fdv2_polling_impl.cpp | 11 +- .../data_systems/fdv2/fdv2_polling_impl.hpp | 8 +- .../fdv2/initializer_factories.cpp | 8 +- .../fdv2/initializer_factories.hpp | 5 +- .../data_systems/fdv2/polling_initializer.cpp | 7 +- .../data_systems/fdv2/polling_initializer.hpp | 23 ++-- .../fdv2/polling_synchronizer.cpp | 15 ++- .../fdv2/polling_synchronizer.hpp | 10 +- .../fdv2/streaming_synchronizer.cpp | 21 +++- .../fdv2/streaming_synchronizer.hpp | 9 +- .../fdv2/synchronizer_factories.cpp | 32 +++-- .../fdv2/synchronizer_factories.hpp | 17 ++- .../tests/fdv2_polling_impl_test.cpp | 41 +++++- .../fdv2_streaming_synchronizer_test.cpp | 119 +++++++++++++----- 26 files changed, 401 insertions(+), 111 deletions(-) create mode 100644 libs/server-sdk/src/data_components/environment_id/environment_id.cpp create mode 100644 libs/server-sdk/src/data_components/environment_id/environment_id.hpp diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp index 2422623a3..57520a2a5 100644 --- a/contract-tests/server-contract-tests/src/main.cpp +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -50,6 +50,7 @@ int main(int argc, char* argv[]) { srv.add_capability("client-prereq-events"); srv.add_capability("evaluation-hooks"); srv.add_capability("track-hooks"); + srv.add_capability("hook-environment-id"); srv.add_capability("wrapper"); srv.add_capability("instance-id"); srv.add_capability("fdv1-fallback"); diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 6be375da6..406a5fb00 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -42,6 +42,8 @@ target_sources(${LIBNAME} prereq_event_recorder/prereq_event_recorder.hpp data_components/change_notifier/change_notifier.hpp data_components/change_notifier/change_notifier.cpp + data_components/environment_id/environment_id.hpp + data_components/environment_id/environment_id.cpp data_components/dependency_tracker/dependency_tracker.hpp data_components/dependency_tracker/dependency_tracker.cpp data_components/expiration_tracker/expiration_tracker.hpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index ba04dda7a..447c556d8 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -71,9 +71,11 @@ static std::unique_ptr MakeBackgroundSyncSystem( config::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, - Logger& logger) { + Logger& logger, + std::shared_ptr environment_id) { return std::make_unique( - endpoints, cfg, http_properties, executor, status_manager, logger); + endpoints, cfg, http_properties, executor, status_manager, logger, + std::move(environment_id)); } static std::unique_ptr MakeLazyLoadSystem( @@ -90,13 +92,15 @@ static std::unique_ptr MakeFDv2System( config::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, - Logger const& logger) { + Logger const& logger, + std::shared_ptr const& environment_id) { std::vector> initializer_factories; for (auto const& initializer : cfg.initializers) { initializer_factories.push_back( std::make_unique( - executor, logger, endpoints, http_properties, initializer)); + executor, logger, endpoints, http_properties, initializer, + environment_id)); } std::vector> @@ -110,14 +114,14 @@ static std::unique_ptr MakeFDv2System( std::make_unique< data_systems::FDv2StreamingSynchronizerFactory>( executor, logger, endpoints, http_properties, - streaming)); + streaming, environment_id)); }, [&](config::built::FDv2Config::PollingConfig const& polling) { synchronizer_factories.push_back( std::make_unique< data_systems::FDv2PollingSynchronizerFactory>( executor, logger, endpoints, http_properties, - polling)); + polling, environment_id)); }, }, sync); @@ -130,7 +134,7 @@ static std::unique_ptr MakeFDv2System( std::make_unique< data_systems::FDv1StreamingAdapterFactory>( executor, logger, endpoints, streaming, - http_properties)); + http_properties, environment_id)); }, [&](config::built::FDv2Config::FDv1PollingConfig const& polling) { @@ -138,7 +142,7 @@ static std::unique_ptr MakeFDv2System( std::make_unique< data_systems::FDv1PollingAdapterFactory>( executor, logger, endpoints, polling, - http_properties)); + http_properties, environment_id)); }, }, *cfg.fdv1_fallback); @@ -162,7 +166,8 @@ static std::unique_ptr MakeDataSystem( Config const& config, boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, - Logger& logger) { + Logger& logger, + std::shared_ptr environment_id) { if (config.DataSystemConfig().disabled) { return std::make_unique(status_manager); } @@ -175,7 +180,7 @@ static std::unique_ptr MakeDataSystem( [&](config::built::BackgroundSyncConfig const& cfg) { return MakeBackgroundSyncSystem( config.ServiceEndpoints(), cfg, data_source_properties, - executor, status_manager, logger); + executor, status_manager, logger, environment_id); }, [&](config::built::LazyLoadConfig const& cfg) { return MakeLazyLoadSystem(cfg, status_manager, logger); @@ -183,7 +188,7 @@ static std::unique_ptr MakeDataSystem( [&](config::built::FDv2Config const& cfg) { return MakeFDv2System(config.ServiceEndpoints(), cfg, data_source_properties, executor, - status_manager, logger); + status_manager, logger, environment_id); }, }, config.DataSystemConfig().system_); @@ -239,11 +244,13 @@ ClientImpl::ClientImpl(Config config, std::string const& version) ioc_(kAsioConcurrencyHint), work_(boost::asio::make_work_guard(ioc_)), status_manager_(), + environment_id_(std::make_shared()), data_system_(MakeDataSystem(http_properties_, config_, ioc_.get_executor(), status_manager_, - logger_)), + logger_, + environment_id_)), event_processor_(MakeEventProcessor(config, ioc_.get_executor(), http_properties_, @@ -378,8 +385,9 @@ 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, + environment_id_->Get()); hooks::ExecuteAfterTrack(config_.Hooks(), series_context, logger_); } @@ -487,7 +495,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (!config_.Hooks().empty()) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + environment_id_->Get()); // Executor only created if there are hooks. executor.emplace(config_.Hooks(), logger_); executor->BeforeEvaluation(series_context); @@ -539,7 +547,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (executor) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + environment_id_->Get()); executor->AfterEvaluation(series_context, detail); } diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 8466a682f..ab41e9284 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -2,6 +2,7 @@ #include "data_components/big_segments/big_segment_store_status_provider.hpp" #include "data_components/big_segments/big_segment_store_wrapper.hpp" +#include "data_components/environment_id/environment_id.hpp" #include "data_components/status_notifications/data_source_status_manager.hpp" #include "data_interfaces/system/idata_system.hpp" #include "evaluation/evaluator.hpp" @@ -252,6 +253,11 @@ class ClientImpl : public IClient { data_components::DataSourceStatusManager status_manager_; + // Populated by the data system's sources with the environment ID reported + // by LaunchDarkly; read when building hook series contexts. Declared + // before data_system_, which shares it with its data sources. + std::shared_ptr const environment_id_; + // This is the main polymorphic component that constitutes the // guts of how data is retrieved (polling, streaming, persistent stores, // etc.) diff --git a/libs/server-sdk/src/data_components/environment_id/environment_id.cpp b/libs/server-sdk/src/data_components/environment_id/environment_id.cpp new file mode 100644 index 000000000..601c46fd1 --- /dev/null +++ b/libs/server-sdk/src/data_components/environment_id/environment_id.cpp @@ -0,0 +1,18 @@ +#include "environment_id.hpp" + +namespace launchdarkly::server_side::data_components { + +std::optional EnvironmentId::Get() const { + std::lock_guard lock(mutex_); + return environment_id_; +} + +void EnvironmentId::Set(std::string_view environment_id) { + if (environment_id.empty()) { + return; + } + std::lock_guard lock(mutex_); + environment_id_ = std::string(environment_id); +} + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/environment_id/environment_id.hpp b/libs/server-sdk/src/data_components/environment_id/environment_id.hpp new file mode 100644 index 000000000..eaf045e19 --- /dev/null +++ b/libs/server-sdk/src/data_components/environment_id/environment_id.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_components { + +/** + * @brief EnvironmentId holds the environment ID reported by LaunchDarkly. + * + * Data sources record the value from the X-LD-EnvID header of successful + * streaming or polling responses; the client reads it when building hook + * series contexts. Instances are shared between the data sources and the + * client, and all methods are thread-safe. + */ +class EnvironmentId { + public: + /** Header carrying the environment ID on streaming/polling responses. */ + static constexpr char const* kHeader = "X-LD-EnvID"; + + /** + * @return The environment ID, if one has been reported. + */ + [[nodiscard]] std::optional Get() const; + + /** + * Records an environment ID. Empty values are ignored. + */ + void Set(std::string_view environment_id); + + private: + mutable std::mutex mutex_; + std::optional environment_id_; +}; + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 1a6de5c0a..05040c810 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -11,7 +11,8 @@ BackgroundSync::BackgroundSync( config::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, - Logger const& logger) + Logger const& logger, + std::shared_ptr environment_id) : store_(), change_notifier_(store_, store_), synchronizer_() { std::visit( [&](auto&& method_config) { @@ -21,13 +22,13 @@ BackgroundSync::BackgroundSync( StreamingConfig>) { synchronizer_ = std::make_shared( ioc, logger, status_manager, endpoints, method_config, - http_properties); + http_properties, environment_id); } else if constexpr (std::is_same_v< T, config::built::BackgroundSyncConfig:: PollingConfig>) { synchronizer_ = std::make_shared( ioc, logger, status_manager, endpoints, method_config, - http_properties); + http_properties, environment_id); } }, background_sync_config.synchronizer_); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index a302b7d0d..4c1240c3e 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -1,6 +1,7 @@ #pragma once #include "../../data_components/change_notifier/change_notifier.hpp" +#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/source/idata_synchronizer.hpp" @@ -32,7 +33,8 @@ class BackgroundSync final : public data_interfaces::IDataSystem { config::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, - Logger const& logger); + Logger const& logger, + std::shared_ptr environment_id); BackgroundSync(BackgroundSync const& item) = delete; BackgroundSync(BackgroundSync&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index abb2a5478..a8fa05c3f 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -3,8 +3,8 @@ #include #include -#include #include +#include #include #include @@ -69,9 +69,11 @@ PollingDataSource::PollingDataSource( config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::PollingConfig const& data_source_config, - config::built::HttpProperties const& http_properties) + config::built::HttpProperties const& http_properties, + std::shared_ptr environment_id) : logger_(logger), status_manager_(status_manager), + environment_id_(std::move(environment_id)), requester_(ioc, http_properties.Tls()), polling_interval_(data_source_config.poll_interval), request_( @@ -102,6 +104,15 @@ void PollingDataSource::DoPoll() { } void PollingDataSource::HandlePollResult(network::HttpResult const& res) { + if (environment_id_ && !res.IsError() && + (res.Status() == 200 || res.Status() == 304)) { + if (auto const it = + res.Headers().find(data_components::EnvironmentId::kHeader); + it != res.Headers().end()) { + environment_id_->Set(it->second); + } + } + auto header_etag = res.Headers().find("etag"); bool has_etag = header_etag != res.Headers().end(); diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp index b9492c755..9f489bb4f 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../../../../data_components/environment_id/environment_id.hpp" #include "../../../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../../../data_interfaces/destination/idestination.hpp" #include "../../../../data_interfaces/source/idata_synchronizer.hpp" @@ -19,13 +20,15 @@ class PollingDataSource : public data_interfaces::IDataSynchronizer, public std::enable_shared_from_this { public: - PollingDataSource(boost::asio::any_io_executor const& ioc, - Logger const& logger, - data_components::DataSourceStatusManager& status_manager, - config::built::ServiceEndpoints const& endpoints, - config::built::BackgroundSyncConfig::PollingConfig const& - data_source_config, - config::built::HttpProperties const& http_properties); + PollingDataSource( + boost::asio::any_io_executor const& ioc, + Logger const& logger, + data_components::DataSourceStatusManager& status_manager, + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig::PollingConfig const& + data_source_config, + config::built::HttpProperties const& http_properties, + std::shared_ptr environment_id); void StartAsync(data_interfaces::IDestination* dest, data_model::SDKDataSet const* bootstrap_data) override; @@ -46,6 +49,10 @@ class PollingDataSource // it has been destroyed. data_components::DataSourceStatusManager& status_manager_; + // Records the environment ID reported by LaunchDarkly. Shared with the + // client, which reads it when building hook contexts. + std::shared_ptr environment_id_; + // Responsible for performing HTTP requests using boost::asio. network::AsioRequester requester_; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index 5f7a27a7b..d7db003fe 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -34,10 +34,12 @@ StreamingDataSource::StreamingDataSource( data_components::DataSourceStatusManager& status_manager, config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::StreamingConfig const& streaming, - config::built::HttpProperties const& http_properties) + config::built::HttpProperties const& http_properties, + std::shared_ptr environment_id) : io_(std::move(io)), logger_(logger), status_manager_(status_manager), + environment_id_(std::move(environment_id)), http_config_(http_properties), streaming_endpoint_(endpoints.StreamingBaseUrl()), streaming_config_(streaming) {} @@ -125,16 +127,33 @@ void StreamingDataSource::StartAsync( auto weak_self = weak_from_this(); + client_builder.on_response( + [weak_self](boost::beast::http::response_header<> const& headers) { + auto self = weak_self.lock(); + if (!self || !self->environment_id_ || + headers.result_int() != 200) { + return; + } + if (auto const it = + headers.find(data_components::EnvironmentId::kHeader); + it != headers.end()) { + self->environment_id_->Set( + std::string_view{it->value().data(), it->value().size()}); + } + }); + client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { if (auto self = weak_self.lock()) { auto status = self->event_handler_->HandleMessage(event.type(), event.data()); - if (status == DataSourceEventHandler::MessageStatus::kInvalidMessage) { + if (status == + DataSourceEventHandler::MessageStatus::kInvalidMessage) { // Invalid data received - restart the connection with backoff // to get a fresh stream. The backoff mechanism prevents rapid // reconnection attempts. LD_LOG(self->logger_, LogLevel::kWarn) - << "Received invalid data from stream, restarting connection"; + << "Received invalid data from stream, restarting " + "connection"; if (self->client_) { self->client_->async_restart("invalid data in stream"); } diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp index f606686c1..faba1c50f 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp @@ -2,6 +2,7 @@ #include "event_handler.hpp" +#include "../../../../data_components/environment_id/environment_id.hpp" #include "../../../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../../../data_interfaces/destination/idestination.hpp" #include "../../../../data_interfaces/source/idata_synchronizer.hpp" @@ -24,7 +25,8 @@ class StreamingDataSource final data_components::DataSourceStatusManager& status_manager, config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::StreamingConfig const& streaming, - config::built::HttpProperties const& http_properties); + config::built::HttpProperties const& http_properties, + std::shared_ptr environment_id); void StartAsync(data_interfaces::IDestination* dest, data_model::SDKDataSet const* bootstrap_data) override; @@ -39,6 +41,11 @@ class StreamingDataSource final Logger const& logger_; data_components::DataSourceStatusManager& status_manager_; + + // Records the environment ID reported by LaunchDarkly. Shared with the + // client, which reads it when building hook contexts. + std::shared_ptr environment_id_; + config::built::HttpProperties http_config_; std::optional event_handler_; 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 0b668851b..8218843b3 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 @@ -192,7 +192,8 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( network::HttpResult const& res, FDv2ProtocolHandler* protocol_handler, Logger const& logger, - std::string_view identity) { + std::string_view identity, + std::shared_ptr const& environment_id) { if (res.IsError()) { auto const& msg = res.ErrorMessage(); std::string error_msg = msg.has_value() ? *msg : "unknown error"; @@ -203,6 +204,14 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( auto fdv1_fallback = ReadFDv1FallbackDirective(res.Headers()); + if (environment_id && (res.Status() == 200 || res.Status() == 304)) { + if (auto const it = + res.Headers().find(data_components::EnvironmentId::kHeader); + it != res.Headers().end()) { + environment_id->Set(it->second); + } + } + if (res.Status() == 304) { return FDv2SourceResult{ FDv2SourceResult::ChangeSet{ 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 94f90f1dc..d2256e417 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 @@ -1,5 +1,6 @@ #pragma once +#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/fdv2_source_result.hpp" #include @@ -8,6 +9,7 @@ #include #include +#include #include #include #include @@ -24,11 +26,13 @@ network::HttpRequest MakeFDv2PollRequest( // Parse an HTTP response from the FDv2 polling endpoint through the protocol // handler and return the appropriate result. identity is used in log messages -// to identify the caller (e.g. "FDv2 polling initializer"). +// to identify the caller (e.g. "FDv2 polling initializer"). If environment_id +// is present, it records the environment ID of a successful response. data_interfaces::FDv2SourceResult HandleFDv2PollResponse( network::HttpResult const& res, FDv2ProtocolHandler* protocol_handler, Logger const& logger, - std::string_view identity); + std::string_view identity, + std::shared_ptr const& environment_id); } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp index cd4808495..c548fe586 100644 --- a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp @@ -13,19 +13,21 @@ FDv2PollingInitializerFactory::FDv2PollingInitializerFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling) + config::built::FDv2Config::PollingConfig polling, + std::shared_ptr environment_id) : 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)) {} + polling_(std::move(polling)), + environment_id_(std::move(environment_id)) {} std::unique_ptr FDv2PollingInitializerFactory::Build() { return std::make_unique( executor_, logger_, polling_base_url_, http_properties_, - data_model::Selector{}, std::nullopt); + data_model::Selector{}, std::nullopt, environment_id_); } } // 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 index ea97dc374..a41ed07ed 100644 --- a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_initializer_factory.hpp" #include @@ -21,7 +22,8 @@ class FDv2PollingInitializerFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling); + config::built::FDv2Config::PollingConfig polling, + std::shared_ptr environment_id); std::unique_ptr Build() override; @@ -31,6 +33,7 @@ class FDv2PollingInitializerFactory final std::string const polling_base_url_; config::built::HttpProperties const http_properties_; config::built::FDv2Config::PollingConfig const polling_; + std::shared_ptr const environment_id_; }; } // 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 b8b47f7b5..85dccb5fe 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp @@ -16,14 +16,15 @@ FDv2PollingInitializer::FDv2PollingInitializer( std::string const& polling_base_url, config::built::HttpProperties const& http_properties, data_model::Selector selector, - std::optional filter_key) + std::optional filter_key, + std::shared_ptr environment_id) : request_(MakeFDv2PollRequest(polling_base_url, http_properties, std::move(selector), std::move(filter_key), logger)), requester_(executor, http_properties.Tls()), - state_(std::make_shared(logger)) {} + state_(std::make_shared(logger, std::move(environment_id))) {} FDv2PollingInitializer::~FDv2PollingInitializer() { close_promise_.Resolve(std::monostate{}); @@ -76,7 +77,7 @@ FDv2SourceResult FDv2PollingInitializer::HandlePollResult( network::HttpResult const& res) { FDv2ProtocolHandler protocol_handler; return HandleFDv2PollResponse(res, &protocol_handler, state->logger, - kIdentity); + kIdentity, state->environment_id); } } // namespace launchdarkly::server_side::data_systems 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 8d27a549f..cb7a57e39 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_initializer.hpp" #include @@ -33,12 +34,14 @@ class FDv2PollingInitializer final : public data_interfaces::IFDv2Initializer { * Constructs an initializer for a single poll request. * If filter_key is present, only the specified payload filter is requested. */ - FDv2PollingInitializer(boost::asio::any_io_executor const& executor, - Logger const& logger, - std::string const& polling_base_url, - config::built::HttpProperties const& http_properties, - data_model::Selector selector, - std::optional filter_key); + FDv2PollingInitializer( + boost::asio::any_io_executor const& executor, + Logger const& logger, + std::string const& polling_base_url, + config::built::HttpProperties const& http_properties, + data_model::Selector selector, + std::optional filter_key, + std::shared_ptr environment_id); ~FDv2PollingInitializer() override; @@ -55,7 +58,13 @@ class FDv2PollingInitializer final : public data_interfaces::IFDv2Initializer { // Logger is itself thread-safe. Logger logger; - explicit State(Logger logger) : logger(std::move(logger)) {} + // EnvironmentId is itself thread-safe. + std::shared_ptr environment_id; + + State(Logger logger, + std::shared_ptr environment_id) + : logger(std::move(logger)), + environment_id(std::move(environment_id)) {} }; /** Interprets an HTTP response as a source result. */ 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 33ea500b8..bdd0fb3c1 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp @@ -22,14 +22,16 @@ FDv2PollingSynchronizer::State::State( std::chrono::seconds poll_interval, std::string polling_base_url, config::built::HttpProperties const& http_properties, - std::optional filter_key) + std::optional filter_key, + std::shared_ptr environment_id) : logger_(std::move(logger)), poll_interval_(std::max(poll_interval, kMinPollInterval)), polling_base_url_(std::move(polling_base_url)), http_properties_(http_properties), filter_key_(std::move(filter_key)), requester_(executor, http_properties.Tls()), - executor_(executor) {} + executor_(executor), + environment_id_(std::move(environment_id)) {} async::Future FDv2PollingSynchronizer::State::Request( data_model::Selector const& selector) const { @@ -50,7 +52,8 @@ async::Future FDv2PollingSynchronizer::State::Request( FDv2SourceResult FDv2PollingSynchronizer::State::HandlePollResult( network::HttpResult const& res) { FDv2ProtocolHandler protocol_handler; - return HandleFDv2PollResponse(res, &protocol_handler, logger_, kIdentity); + return HandleFDv2PollResponse(res, &protocol_handler, logger_, kIdentity, + environment_id_); } async::Future FDv2PollingSynchronizer::State::Delay( @@ -84,13 +87,15 @@ FDv2PollingSynchronizer::FDv2PollingSynchronizer( std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::seconds poll_interval) + std::chrono::seconds poll_interval, + std::shared_ptr environment_id) : state_(std::make_shared(logger, executor, poll_interval, std::move(polling_base_url), http_properties, - std::move(filter_key))) { + std::move(filter_key), + std::move(environment_id))) { if (poll_interval < kMinPollInterval) { LD_LOG(logger, LogLevel::kWarn) << kIdentity << ": polling interval too frequent, defaulting to " 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 3b03511ce..7566c16dc 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_synchronizer.hpp" #include @@ -10,6 +11,7 @@ #include #include +#include #include #include #include @@ -42,7 +44,8 @@ class FDv2PollingSynchronizer final std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::seconds poll_interval); + std::chrono::seconds poll_interval, + std::shared_ptr environment_id); ~FDv2PollingSynchronizer() override; @@ -64,7 +67,8 @@ class FDv2PollingSynchronizer final std::chrono::seconds poll_interval, std::string polling_base_url, config::built::HttpProperties const& http_properties, - std::optional filter_key); + std::optional filter_key, + std::shared_ptr environment_id); /** Issues an async HTTP poll request and returns a Future resolving * with the result. */ @@ -101,6 +105,8 @@ class FDv2PollingSynchronizer final std::optional const filter_key_; network::Requester const requester_; boost::asio::any_io_executor const executor_; + // EnvironmentId is itself thread-safe. + std::shared_ptr const environment_id_; // Mutable state, guarded by mutex_. std::mutex mutex_; 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 0f09ef5cf..8843493bb 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp @@ -40,13 +40,15 @@ FDv2StreamingSynchronizer::State::State( std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay) + std::chrono::milliseconds initial_reconnect_delay, + std::shared_ptr environment_id) : logger_(std::move(logger)), streaming_base_url_(std::move(streaming_base_url)), http_properties_(http_properties), filter_key_(std::move(filter_key)), initial_reconnect_delay_(initial_reconnect_delay), - executor_(executor) {} + executor_(executor), + environment_id_(std::move(environment_id)) {} void FDv2StreamingSynchronizer::State::EnsureStarted( data_model::Selector const& selector, @@ -186,6 +188,15 @@ void FDv2StreamingSynchronizer::State::OnConnect(HttpRequest* req) { void FDv2StreamingSynchronizer::State::OnResponse( HttpResponseHeader const& headers) { + if (environment_id_ && headers.result_int() == 200) { + if (auto const env_it = + headers.find(data_components::EnvironmentId::kHeader); + env_it != headers.end()) { + environment_id_->Set(std::string_view{env_it->value().data(), + env_it->value().size()}); + } + } + auto const it = headers.find("X-LD-FD-Fallback"); if (it == headers.end() || !boost::iequals(it->value(), "true")) { std::lock_guard lock(mutex_); @@ -387,13 +398,15 @@ FDv2StreamingSynchronizer::FDv2StreamingSynchronizer( std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay) + std::chrono::milliseconds initial_reconnect_delay, + std::shared_ptr environment_id) : state_(std::make_shared(logger, executor, std::move(streaming_base_url), http_properties, std::move(filter_key), - initial_reconnect_delay)) {} + initial_reconnect_delay, + std::move(environment_id))) {} FDv2StreamingSynchronizer::~FDv2StreamingSynchronizer() { Close(); 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 f1e69ae8b..a3bd8d5dc 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_synchronizer.hpp" #include @@ -49,7 +50,8 @@ class FDv2StreamingSynchronizer final std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay); + std::chrono::milliseconds initial_reconnect_delay, + std::shared_ptr environment_id); ~FDv2StreamingSynchronizer() override; @@ -73,7 +75,8 @@ class FDv2StreamingSynchronizer final std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay); + std::chrono::milliseconds initial_reconnect_delay, + std::shared_ptr environment_id); /** * Updates the stored selector, starts the SSE client if not already @@ -137,6 +140,8 @@ class FDv2StreamingSynchronizer final std::optional const filter_key_; std::chrono::milliseconds const initial_reconnect_delay_; boost::asio::any_io_executor const executor_; + // EnvironmentId is itself thread-safe. + std::shared_ptr const environment_id_; // Touched only from SSE callbacks, which all run on the same strand. // No lock required. diff --git a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp index 140f64035..611af08ac 100644 --- a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp @@ -17,19 +17,21 @@ FDv2StreamingSynchronizerFactory::FDv2StreamingSynchronizerFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::StreamingConfig streaming) + config::built::FDv2Config::StreamingConfig streaming, + std::shared_ptr environment_id) : 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)) {} + streaming_(std::move(streaming)), + environment_id_(std::move(environment_id)) {} std::unique_ptr FDv2StreamingSynchronizerFactory::Build() { return std::make_unique( executor_, logger_, streaming_base_url_, http_properties_, std::nullopt, - streaming_.initial_reconnect_delay); + streaming_.initial_reconnect_delay, environment_id_); } FDv2PollingSynchronizerFactory::FDv2PollingSynchronizerFactory( @@ -37,19 +39,21 @@ FDv2PollingSynchronizerFactory::FDv2PollingSynchronizerFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling) + config::built::FDv2Config::PollingConfig polling, + std::shared_ptr environment_id) : 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)) {} + polling_(std::move(polling)), + environment_id_(std::move(environment_id)) {} std::unique_ptr FDv2PollingSynchronizerFactory::Build() { return std::make_unique( executor_, logger_, polling_base_url_, http_properties_, std::nullopt, - polling_.poll_interval); + polling_.poll_interval, environment_id_); } FDv1StreamingAdapterFactory::FDv1StreamingAdapterFactory( @@ -57,12 +61,14 @@ FDv1StreamingAdapterFactory::FDv1StreamingAdapterFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1StreamingConfig streaming, - config::built::HttpProperties http_properties) + config::built::HttpProperties http_properties, + std::shared_ptr environment_id) : executor_(std::move(executor)), logger_(std::move(logger)), endpoints_(std::move(endpoints)), streaming_(std::move(streaming)), - http_properties_(std::move(http_properties)) {} + http_properties_(std::move(http_properties)), + environment_id_(std::move(environment_id)) {} std::unique_ptr FDv1StreamingAdapterFactory::Build() { @@ -70,7 +76,7 @@ FDv1StreamingAdapterFactory::Build() { [this](data_components::DataSourceStatusManager& status_manager) { return std::make_shared( executor_, logger_, status_manager, endpoints_, streaming_, - http_properties_); + http_properties_, environment_id_); }); } @@ -79,12 +85,14 @@ FDv1PollingAdapterFactory::FDv1PollingAdapterFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1PollingConfig polling, - config::built::HttpProperties http_properties) + config::built::HttpProperties http_properties, + std::shared_ptr environment_id) : executor_(std::move(executor)), logger_(std::move(logger)), endpoints_(std::move(endpoints)), polling_(std::move(polling)), - http_properties_(std::move(http_properties)) {} + http_properties_(std::move(http_properties)), + environment_id_(std::move(environment_id)) {} std::unique_ptr FDv1PollingAdapterFactory::Build() { @@ -92,7 +100,7 @@ FDv1PollingAdapterFactory::Build() { [this](data_components::DataSourceStatusManager& status_manager) { return std::make_shared( executor_, logger_, status_manager, endpoints_, polling_, - http_properties_); + http_properties_, environment_id_); }); } diff --git a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp index 3602df03f..9010d4f5e 100644 --- a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_synchronizer_factory.hpp" #include @@ -21,7 +22,8 @@ class FDv2StreamingSynchronizerFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::StreamingConfig streaming); + config::built::FDv2Config::StreamingConfig streaming, + std::shared_ptr environment_id); std::unique_ptr Build() override; @@ -31,6 +33,7 @@ class FDv2StreamingSynchronizerFactory final std::string const streaming_base_url_; config::built::HttpProperties const http_properties_; config::built::FDv2Config::StreamingConfig const streaming_; + std::shared_ptr const environment_id_; }; /** @@ -44,7 +47,8 @@ class FDv2PollingSynchronizerFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling); + config::built::FDv2Config::PollingConfig polling, + std::shared_ptr environment_id); std::unique_ptr Build() override; @@ -54,6 +58,7 @@ class FDv2PollingSynchronizerFactory final std::string const polling_base_url_; config::built::HttpProperties const http_properties_; config::built::FDv2Config::PollingConfig const polling_; + std::shared_ptr const environment_id_; }; /** @@ -68,7 +73,8 @@ class FDv1StreamingAdapterFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1StreamingConfig streaming, - config::built::HttpProperties http_properties); + config::built::HttpProperties http_properties, + std::shared_ptr environment_id); std::unique_ptr Build() override; @@ -80,6 +86,7 @@ class FDv1StreamingAdapterFactory final config::built::ServiceEndpoints const endpoints_; config::built::FDv2Config::FDv1StreamingConfig const streaming_; config::built::HttpProperties const http_properties_; + std::shared_ptr const environment_id_; }; /** @@ -94,7 +101,8 @@ class FDv1PollingAdapterFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1PollingConfig polling, - config::built::HttpProperties http_properties); + config::built::HttpProperties http_properties, + std::shared_ptr environment_id); std::unique_ptr Build() override; @@ -106,6 +114,7 @@ class FDv1PollingAdapterFactory final config::built::ServiceEndpoints const endpoints_; config::built::FDv2Config::FDv1PollingConfig const polling_; config::built::HttpProperties const http_properties_; + std::shared_ptr const environment_id_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp index d4ecd9ee6..e6fb04061 100644 --- a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp +++ b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp @@ -22,11 +22,14 @@ static Logger MakeNullLogger() { static FDv2SourceResult HandleResponse( unsigned status, std::optional body, - network::HttpResult::HeadersType headers) { + network::HttpResult::HeadersType headers, + std::shared_ptr + environment_id = nullptr) { auto logger = MakeNullLogger(); FDv2ProtocolHandler handler; network::HttpResult res{status, std::move(body), std::move(headers)}; - return HandleFDv2PollResponse(res, &handler, logger, "test"); + return HandleFDv2PollResponse(res, &handler, logger, "test", + environment_id); } TEST(HandleFDv2PollResponseTest, NotModifiedSetsFdv1FallbackWhenHeaderPresent) { @@ -125,7 +128,8 @@ TEST(HandleFDv2PollResponseTest, NetworkErrorDoesNotSetFlag) { auto logger = MakeNullLogger(); FDv2ProtocolHandler handler; network::HttpResult res{std::optional{"connection refused"}}; - auto result = HandleFDv2PollResponse(res, &handler, logger, "test"); + auto result = HandleFDv2PollResponse(res, &handler, logger, "test", + /* environment_id= */ nullptr); EXPECT_TRUE( std::holds_alternative(result.value)); @@ -171,3 +175,34 @@ TEST(MakeFDv2PollRequestTest, InvalidFilterKeyIsDropped) { std::string{"has spaces"}, logger); EXPECT_EQ(req.Url(), "http://example.com/sdk/poll"); } + +TEST(HandleFDv2PollResponseTest, OkRecordsEnvironmentId) { + auto environment_id = + std::make_shared(); + HandleResponse(200, R"({"events":[]})", {{"X-LD-EnvID", "env-123"}}, + environment_id); + EXPECT_EQ(std::optional{"env-123"}, environment_id->Get()); +} + +TEST(HandleFDv2PollResponseTest, NotModifiedRecordsEnvironmentId) { + auto environment_id = + std::make_shared(); + HandleResponse(304, std::nullopt, {{"X-LD-EnvID", "env-123"}}, + environment_id); + EXPECT_EQ(std::optional{"env-123"}, environment_id->Get()); +} + +TEST(HandleFDv2PollResponseTest, ErrorStatusDoesNotRecordEnvironmentId) { + auto environment_id = + std::make_shared(); + HandleResponse(503, std::nullopt, {{"X-LD-EnvID", "env-123"}}, + environment_id); + EXPECT_FALSE(environment_id->Get()); +} + +TEST(HandleFDv2PollResponseTest, MissingHeaderDoesNotRecordEnvironmentId) { + auto environment_id = + std::make_shared(); + HandleResponse(200, R"({"events":[]})", {}, environment_id); + EXPECT_FALSE(environment_id->Get()); +} diff --git a/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp b/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp index 25bebefc9..59d16c269 100644 --- a/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp +++ b/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp @@ -145,7 +145,8 @@ TEST(FDv2StreamingSynchronizerTest, NextBadEndpointUrlReturnsTerminalError) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "not a url", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); // Act: trigger setup with a malformed streaming URL. URL parsing happens // inside EnsureStarted on the first Next call. @@ -168,7 +169,8 @@ TEST(FDv2StreamingSynchronizerTest, CloseBeforeNextReturnsShutdown) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); synchronizer.Close(); // Act: call Next on an already-closed synchronizer. @@ -188,7 +190,8 @@ TEST(FDv2StreamingSynchronizerTest, CloseDuringPendingNextResolvesShutdown) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); // Skip the SSE setup; we want Next to be pending purely on the // close/timeout race, not on real network activity. @@ -216,7 +219,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectEmptySelectorNoBasisParam) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -240,7 +244,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectWithSelectorAppendsBasis) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -266,7 +271,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectWithFilterKeyAppendsFilter) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::string("my-filter"), 1s); + MakeHttpProperties(), std::string("my-filter"), 1s, + /* environment_id= */ nullptr); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -290,7 +296,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectInvalidFilterKeyIsDropped) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::string("has spaces"), 1s); + MakeHttpProperties(), std::string("has spaces"), 1s, + /* environment_id= */ nullptr); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -312,7 +319,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectReconnectUsesLatestSelector) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -349,7 +357,8 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectSelectorStateIsPercentEncoded) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -380,7 +389,8 @@ TEST(FDv2StreamingSynchronizerTest, FullChangesetEventsReturnsChangeSet) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event server_intent("server-intent", @@ -417,7 +427,8 @@ TEST(FDv2StreamingSynchronizerTest, GoodbyeEventReturnsGoodbye) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event goodbye("goodbye", R"({"reason":"bye"})"); @@ -442,7 +453,8 @@ TEST(FDv2StreamingSynchronizerTest, GoodbyeEventTriggersAsyncRestart) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -471,7 +483,8 @@ TEST(FDv2StreamingSynchronizerTest, FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // Begin accumulating a payload that we'll abandon mid-flight via Goodbye. @@ -531,7 +544,8 @@ TEST(FDv2StreamingSynchronizerTest, ServerErrorEventReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event server_error("error", R"({"id":"abc","reason":"oops"})"); @@ -562,7 +576,8 @@ TEST(FDv2StreamingSynchronizerTest, UnknownEventWithGarbageBodyIsIgnored) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -581,7 +596,8 @@ TEST(FDv2StreamingSynchronizerTest, MalformedJsonEventReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -613,7 +629,8 @@ TEST(FDv2StreamingSynchronizerTest, FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -639,7 +656,8 @@ TEST(FDv2StreamingSynchronizerTest, TranslationFailureReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // A non-empty segment object missing required fields triggers a schema @@ -684,7 +702,8 @@ TEST(FDv2StreamingSynchronizerTest, FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Error error{sse::errors::UnrecoverableClientError{ @@ -712,7 +731,8 @@ TEST(FDv2StreamingSynchronizerTest, RecoverableReadTimeoutReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Error error{sse::errors::ReadTimeout{std::chrono::milliseconds(100)}}; @@ -743,7 +763,8 @@ TEST(FDv2StreamingSynchronizerTest, OnResponseDirectivePropagatesToChangeSet) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> headers; @@ -780,7 +801,8 @@ TEST(FDv2StreamingSynchronizerTest, SecondResponseWithoutDirectiveClearsFlag) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> first; @@ -808,7 +830,8 @@ TEST(FDv2StreamingSynchronizerTest, ErrorAfterDirectiveCarriesFlag) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s); + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> headers; @@ -833,9 +856,9 @@ TEST(FDv2StreamingSynchronizerTest, DirectiveWithTtlHeaderParsesValue) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - "http://localhost", MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // Server sends the directive with an explicit TTL. @@ -862,9 +885,9 @@ TEST(FDv2StreamingSynchronizerTest, DirectiveWithoutTtlHeaderUsesDefault) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, - "http://localhost", MakeHttpProperties(), std::nullopt, - 1s); + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s, + /* environment_id= */ nullptr); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // Server sends the directive with no TTL header. @@ -884,3 +907,41 @@ TEST(FDv2StreamingSynchronizerTest, DirectiveWithoutTtlHeaderUsesDefault) { ASSERT_TRUE(result->fdv1_fallback); EXPECT_EQ(FDv1FallbackDirective::kDefaultTtl, result->fdv1_fallback->ttl); } + +TEST(FDv2StreamingSynchronizerTest, SuccessfulResponseRecordsEnvironmentId) { + auto logger = MakeNullLogger(); + IoContextRunner runner; + + auto environment_id = + std::make_shared(); + FDv2StreamingSynchronizer synchronizer( + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s, environment_id); + FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); + + boost::beast::http::response_header<> headers; + headers.result(200); + headers.set("X-LD-EnvID", "env-123"); + FDv2StreamingSynchronizerTestPeer::OnResponse(synchronizer, headers); + + EXPECT_EQ(std::optional{"env-123"}, environment_id->Get()); +} + +TEST(FDv2StreamingSynchronizerTest, ErrorResponseDoesNotRecordEnvironmentId) { + auto logger = MakeNullLogger(); + IoContextRunner runner; + + auto environment_id = + std::make_shared(); + FDv2StreamingSynchronizer synchronizer( + runner.context().get_executor(), logger, "http://localhost", + MakeHttpProperties(), std::nullopt, 1s, environment_id); + FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); + + boost::beast::http::response_header<> headers; + headers.result(401); + headers.set("X-LD-EnvID", "env-123"); + FDv2StreamingSynchronizerTestPeer::OnResponse(synchronizer, headers); + + EXPECT_FALSE(environment_id->Get()); +} From bdbbb0c324abd2420e905c1f2921691df1bcaeca Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 21:54:55 +0000 Subject: [PATCH 2/6] ci: Re-run contract tests against the released test harness Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> From 9f6f39b20c69edd60bcd26e4217837282faab729 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:21:25 +0000 Subject: [PATCH 3/6] ci: Re-run contract tests against the released test harness Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> From d1cbdd5f825de13753ddbc0e316cf8494cefdec7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 22:46:42 +0000 Subject: [PATCH 4/6] refactor: Carry the environment ID with data payload metadata Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> --- libs/server-sdk/src/CMakeLists.txt | 2 - libs/server-sdk/src/client_impl.cpp | 41 +++--- libs/server-sdk/src/client_impl.hpp | 6 - .../change_notifier/change_notifier.cpp | 4 + .../change_notifier/change_notifier.hpp | 1 + .../environment_id/environment_id.cpp | 18 --- .../environment_id/environment_id.hpp | 38 ------ .../memory_store/memory_store.cpp | 13 ++ .../memory_store/memory_store.hpp | 10 ++ .../json_destination.cpp | 2 + .../json_destination.hpp | 5 + .../destination/idestination.hpp | 7 ++ .../source/fdv2_source_result.hpp | 6 + .../data_interfaces/system/idata_system.hpp | 9 ++ .../background_sync_system.cpp | 11 +- .../background_sync_system.hpp | 6 +- .../sources/polling/polling_data_source.cpp | 14 +-- .../sources/polling/polling_data_source.hpp | 21 ++-- .../streaming/streaming_data_source.cpp | 17 ++- .../streaming/streaming_data_source.hpp | 12 +- .../fdv2/fdv1_adapter_synchronizer.cpp | 14 +++ .../fdv2/fdv1_adapter_synchronizer.hpp | 5 + .../data_systems/fdv2/fdv2_data_system.cpp | 12 ++ .../data_systems/fdv2/fdv2_data_system.hpp | 6 + .../data_systems/fdv2/fdv2_polling_impl.cpp | 24 ++-- .../data_systems/fdv2/fdv2_polling_impl.hpp | 8 +- .../fdv2/initializer_factories.cpp | 8 +- .../fdv2/initializer_factories.hpp | 5 +- .../data_systems/fdv2/polling_initializer.cpp | 7 +- .../data_systems/fdv2/polling_initializer.hpp | 23 ++-- .../fdv2/polling_synchronizer.cpp | 15 +-- .../fdv2/polling_synchronizer.hpp | 10 +- .../fdv2/streaming_synchronizer.cpp | 25 ++-- .../fdv2/streaming_synchronizer.hpp | 11 +- .../fdv2/synchronizer_factories.cpp | 32 ++--- .../fdv2/synchronizer_factories.hpp | 17 +-- .../lazy_load/lazy_load_system.cpp | 4 + .../lazy_load/lazy_load_system.hpp | 2 + libs/server-sdk/src/data_systems/offline.cpp | 4 + libs/server-sdk/src/data_systems/offline.hpp | 1 + .../tests/fdv1_adapter_synchronizer_test.cpp | 14 +++ .../tests/fdv2_polling_impl_test.cpp | 69 +++++----- .../fdv2_streaming_synchronizer_test.cpp | 119 +++++------------- libs/server-sdk/tests/memory_store_test.cpp | 23 ++++ 44 files changed, 319 insertions(+), 382 deletions(-) delete mode 100644 libs/server-sdk/src/data_components/environment_id/environment_id.cpp delete mode 100644 libs/server-sdk/src/data_components/environment_id/environment_id.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 406a5fb00..6be375da6 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -42,8 +42,6 @@ target_sources(${LIBNAME} prereq_event_recorder/prereq_event_recorder.hpp data_components/change_notifier/change_notifier.hpp data_components/change_notifier/change_notifier.cpp - data_components/environment_id/environment_id.hpp - data_components/environment_id/environment_id.cpp data_components/dependency_tracker/dependency_tracker.hpp data_components/dependency_tracker/dependency_tracker.cpp data_components/expiration_tracker/expiration_tracker.hpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 447c556d8..cec42d573 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -71,11 +71,9 @@ static std::unique_ptr MakeBackgroundSyncSystem( config::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, - Logger& logger, - std::shared_ptr environment_id) { + Logger& logger) { return std::make_unique( - endpoints, cfg, http_properties, executor, status_manager, logger, - std::move(environment_id)); + endpoints, cfg, http_properties, executor, status_manager, logger); } static std::unique_ptr MakeLazyLoadSystem( @@ -92,15 +90,13 @@ static std::unique_ptr MakeFDv2System( config::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, - Logger const& logger, - std::shared_ptr const& environment_id) { + 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, - environment_id)); + executor, logger, endpoints, http_properties, initializer)); } std::vector> @@ -114,14 +110,14 @@ static std::unique_ptr MakeFDv2System( std::make_unique< data_systems::FDv2StreamingSynchronizerFactory>( executor, logger, endpoints, http_properties, - streaming, environment_id)); + streaming)); }, [&](config::built::FDv2Config::PollingConfig const& polling) { synchronizer_factories.push_back( std::make_unique< data_systems::FDv2PollingSynchronizerFactory>( executor, logger, endpoints, http_properties, - polling, environment_id)); + polling)); }, }, sync); @@ -134,7 +130,7 @@ static std::unique_ptr MakeFDv2System( std::make_unique< data_systems::FDv1StreamingAdapterFactory>( executor, logger, endpoints, streaming, - http_properties, environment_id)); + http_properties)); }, [&](config::built::FDv2Config::FDv1PollingConfig const& polling) { @@ -142,7 +138,7 @@ static std::unique_ptr MakeFDv2System( std::make_unique< data_systems::FDv1PollingAdapterFactory>( executor, logger, endpoints, polling, - http_properties, environment_id)); + http_properties)); }, }, *cfg.fdv1_fallback); @@ -166,8 +162,7 @@ static std::unique_ptr MakeDataSystem( Config const& config, boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, - Logger& logger, - std::shared_ptr environment_id) { + Logger& logger) { if (config.DataSystemConfig().disabled) { return std::make_unique(status_manager); } @@ -180,7 +175,7 @@ static std::unique_ptr MakeDataSystem( [&](config::built::BackgroundSyncConfig const& cfg) { return MakeBackgroundSyncSystem( config.ServiceEndpoints(), cfg, data_source_properties, - executor, status_manager, logger, environment_id); + executor, status_manager, logger); }, [&](config::built::LazyLoadConfig const& cfg) { return MakeLazyLoadSystem(cfg, status_manager, logger); @@ -188,7 +183,7 @@ static std::unique_ptr MakeDataSystem( [&](config::built::FDv2Config const& cfg) { return MakeFDv2System(config.ServiceEndpoints(), cfg, data_source_properties, executor, - status_manager, logger, environment_id); + status_manager, logger); }, }, config.DataSystemConfig().system_); @@ -244,13 +239,11 @@ ClientImpl::ClientImpl(Config config, std::string const& version) ioc_(kAsioConcurrencyHint), work_(boost::asio::make_work_guard(ioc_)), status_manager_(), - environment_id_(std::make_shared()), data_system_(MakeDataSystem(http_properties_, config_, ioc_.get_executor(), status_manager_, - logger_, - environment_id_)), + logger_)), event_processor_(MakeEventProcessor(config, ioc_.get_executor(), http_properties_, @@ -387,7 +380,7 @@ void ClientImpl::TrackInternal(Context const& ctx, if (!config_.Hooks().empty()) { hooks::TrackSeriesContext series_context(ctx, event_name, metric_value, data, hook_context, - environment_id_->Get()); + data_system_->EnvironmentId()); hooks::ExecuteAfterTrack(config_.Hooks(), series_context, logger_); } @@ -495,7 +488,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (!config_.Hooks().empty()) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - environment_id_->Get()); + data_system_->EnvironmentId()); // Executor only created if there are hooks. executor.emplace(config_.Hooks(), logger_); executor->BeforeEvaluation(series_context); @@ -509,7 +502,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (executor) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + data_system_->EnvironmentId()); executor->AfterEvaluation(series_context, detail); } @@ -531,7 +524,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (executor) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - std::nullopt); + data_system_->EnvironmentId()); executor->AfterEvaluation(series_context, detail); } @@ -547,7 +540,7 @@ EvaluationDetail ClientImpl::VariationInternal( if (executor) { hooks::EvaluationSeriesContext series_context( key, context, default_value, method_name, hook_context, - environment_id_->Get()); + data_system_->EnvironmentId()); executor->AfterEvaluation(series_context, detail); } diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index ab41e9284..8466a682f 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -2,7 +2,6 @@ #include "data_components/big_segments/big_segment_store_status_provider.hpp" #include "data_components/big_segments/big_segment_store_wrapper.hpp" -#include "data_components/environment_id/environment_id.hpp" #include "data_components/status_notifications/data_source_status_manager.hpp" #include "data_interfaces/system/idata_system.hpp" #include "evaluation/evaluator.hpp" @@ -253,11 +252,6 @@ class ClientImpl : public IClient { data_components::DataSourceStatusManager status_manager_; - // Populated by the data system's sources with the environment ID reported - // by LaunchDarkly; read when building hook series contexts. Declared - // before data_system_, which shares it with its data sources. - std::shared_ptr const environment_id_; - // This is the main polymorphic component that constitutes the // guts of how data is retrieved (polling, streaming, persistent stores, // etc.) diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp index 76daefe10..511b0fab3 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp @@ -143,6 +143,10 @@ void ChangeNotifier::Apply( } } +void ChangeNotifier::SetEnvironmentId(std::string environment_id) { + sink_.SetEnvironmentId(std::move(environment_id)); +} + bool ChangeNotifier::HasListeners() const { std::lock_guard lock{signal_mutex_}; return !signals_.empty(); diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp index ed0ca1c64..faf491cbd 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp @@ -38,6 +38,7 @@ class ChangeNotifier final : public data_interfaces::ITransactionalDestination, data_model::SegmentDescriptor segment) override; void Apply(data_model::ChangeSet change_set) override; + void SetEnvironmentId(std::string environment_id) override; [[nodiscard]] std::string const& Identity() const override; diff --git a/libs/server-sdk/src/data_components/environment_id/environment_id.cpp b/libs/server-sdk/src/data_components/environment_id/environment_id.cpp deleted file mode 100644 index 601c46fd1..000000000 --- a/libs/server-sdk/src/data_components/environment_id/environment_id.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "environment_id.hpp" - -namespace launchdarkly::server_side::data_components { - -std::optional EnvironmentId::Get() const { - std::lock_guard lock(mutex_); - return environment_id_; -} - -void EnvironmentId::Set(std::string_view environment_id) { - if (environment_id.empty()) { - return; - } - std::lock_guard lock(mutex_); - environment_id_ = std::string(environment_id); -} - -} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/environment_id/environment_id.hpp b/libs/server-sdk/src/data_components/environment_id/environment_id.hpp deleted file mode 100644 index eaf045e19..000000000 --- a/libs/server-sdk/src/data_components/environment_id/environment_id.hpp +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace launchdarkly::server_side::data_components { - -/** - * @brief EnvironmentId holds the environment ID reported by LaunchDarkly. - * - * Data sources record the value from the X-LD-EnvID header of successful - * streaming or polling responses; the client reads it when building hook - * series contexts. Instances are shared between the data sources and the - * client, and all methods are thread-safe. - */ -class EnvironmentId { - public: - /** Header carrying the environment ID on streaming/polling responses. */ - static constexpr char const* kHeader = "X-LD-EnvID"; - - /** - * @return The environment ID, if one has been reported. - */ - [[nodiscard]] std::optional Get() const; - - /** - * Records an environment ID. Empty values are ignored. - */ - void Set(std::string_view environment_id); - - private: - mutable std::mutex mutex_; - std::optional environment_id_; -}; - -} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 2eee8ff03..23385c309 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -74,6 +74,19 @@ void MemoryStore::Upsert(std::string const& key, std::make_shared(std::move(segment)); } +void MemoryStore::SetEnvironmentId(std::string environment_id) { + std::lock_guard lock{data_mutex_}; + environment_id_ = std::move(environment_id); +} + +std::optional MemoryStore::EnvironmentId() const { + std::lock_guard lock{data_mutex_}; + if (!initialized_) { + return std::nullopt; + } + return environment_id_; +} + bool MemoryStore::RemoveFlag(std::string const& key) { std::lock_guard lock{data_mutex_}; return flags_.erase(key) == 1; diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 3dc73272c..be5e01c51 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -43,6 +44,14 @@ class MemoryStore final : public data_interfaces::IStore, void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + void SetEnvironmentId(std::string environment_id) override; + + /** + * @return The environment ID reported by LaunchDarkly, if any has been + * received. + */ + [[nodiscard]] std::optional EnvironmentId() const; + bool RemoveFlag(std::string const& key); bool RemoveSegment(std::string const& key); @@ -66,6 +75,7 @@ class MemoryStore final : public data_interfaces::IStore, std::shared_ptr> segments_; bool initialized_ = false; + std::optional environment_id_; mutable std::mutex data_mutex_; }; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index b85f2f978..ac121bf71 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -111,6 +111,8 @@ void JsonDestination::Upsert(std::string const& key, dest_.Upsert(Kinds::Segment, key, Serialize(key, segment))); } +void JsonDestination::SetEnvironmentId(std::string) {} + void JsonDestination::LogUpsertResult( std::string const& key, std::string const& data_type, diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 77f4e6a01..fff0c7756 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -70,6 +70,11 @@ class JsonDestination final : public data_interfaces::IDestination { void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + /** + * @brief No-op; serialized destinations do not store the environment ID. + */ + void SetEnvironmentId(std::string environment_id) override; + /** * @return Identity of this destination. Used in logs. */ diff --git a/libs/server-sdk/src/data_interfaces/destination/idestination.hpp b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp index 342acf49c..0bed95354 100644 --- a/libs/server-sdk/src/data_interfaces/destination/idestination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp @@ -35,6 +35,13 @@ class IDestination { virtual void Upsert(std::string const& key, data_model::SegmentDescriptor segment) = 0; + /** + * \brief Record the environment ID that LaunchDarkly reported alongside + * the data. Destinations which do not track it ignore the value. + * \param environment_id The environment ID. + */ + virtual void SetEnvironmentId(std::string environment_id) = 0; + /** * \return Identity of the destination. Used in logs. */ diff --git a/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp b/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp index 53a0cd442..12cb39da7 100644 --- a/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp +++ b/libs/server-sdk/src/data_interfaces/source/fdv2_source_result.hpp @@ -98,6 +98,12 @@ struct FDv2SourceResult { * Set if the underlying transport observed an FDv1 fallback directive. */ std::optional fdv1_fallback; + + /** + * Set if the underlying transport reported the environment ID (e.g. an + * X-LD-EnvID response header). + */ + std::optional environment_id; }; } // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp index 0edf778db..f7aaba819 100644 --- a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp +++ b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp @@ -2,6 +2,9 @@ #include "../store/istore.hpp" +#include +#include + namespace launchdarkly::server_side::data_interfaces { /** @@ -21,6 +24,12 @@ class IDataSystem : public IStore { */ virtual void Initialize() = 0; + /** + * @return The environment ID reported by LaunchDarkly alongside the data, + * if the system has received one. + */ + [[nodiscard]] virtual std::optional EnvironmentId() const = 0; + virtual ~IDataSystem() override = default; IDataSystem(IDataSystem const& item) = delete; IDataSystem(IDataSystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 05040c810..7a1c7d7e9 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -11,8 +11,7 @@ BackgroundSync::BackgroundSync( config::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, - Logger const& logger, - std::shared_ptr environment_id) + Logger const& logger) : store_(), change_notifier_(store_, store_), synchronizer_() { std::visit( [&](auto&& method_config) { @@ -22,13 +21,13 @@ BackgroundSync::BackgroundSync( StreamingConfig>) { synchronizer_ = std::make_shared( ioc, logger, status_manager, endpoints, method_config, - http_properties, environment_id); + http_properties); } else if constexpr (std::is_same_v< T, config::built::BackgroundSyncConfig:: PollingConfig>) { synchronizer_ = std::make_shared( ioc, logger, status_manager, endpoints, method_config, - http_properties, environment_id); + http_properties); } }, background_sync_config.synchronizer_); @@ -48,6 +47,10 @@ std::string const& BackgroundSync::Identity() const { return id; } +std::optional BackgroundSync::EnvironmentId() const { + return store_.EnvironmentId(); +} + std::shared_ptr BackgroundSync::GetFlag( std::string const& key) const { return store_.GetFlag(key); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 4c1240c3e..620eb3c48 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -1,7 +1,6 @@ #pragma once #include "../../data_components/change_notifier/change_notifier.hpp" -#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/source/idata_synchronizer.hpp" @@ -33,8 +32,7 @@ class BackgroundSync final : public data_interfaces::IDataSystem { config::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, - Logger const& logger, - std::shared_ptr environment_id); + Logger const& logger); BackgroundSync(BackgroundSync const& item) = delete; BackgroundSync(BackgroundSync&& item) = delete; @@ -57,6 +55,8 @@ class BackgroundSync final : public data_interfaces::IDataSystem { bool Initialized() const override; + std::optional EnvironmentId() const override; + private: data_components::MemoryStore store_; data_components::ChangeNotifier change_notifier_; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index a8fa05c3f..cdff2b6b1 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -69,11 +69,9 @@ PollingDataSource::PollingDataSource( config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::PollingConfig const& data_source_config, - config::built::HttpProperties const& http_properties, - std::shared_ptr environment_id) + config::built::HttpProperties const& http_properties) : logger_(logger), status_manager_(status_manager), - environment_id_(std::move(environment_id)), requester_(ioc, http_properties.Tls()), polling_interval_(data_source_config.poll_interval), request_( @@ -104,12 +102,10 @@ void PollingDataSource::DoPoll() { } void PollingDataSource::HandlePollResult(network::HttpResult const& res) { - if (environment_id_ && !res.IsError() && - (res.Status() == 200 || res.Status() == 304)) { - if (auto const it = - res.Headers().find(data_components::EnvironmentId::kHeader); - it != res.Headers().end()) { - environment_id_->Set(it->second); + if (!res.IsError() && (res.Status() == 200 || res.Status() == 304)) { + if (auto const it = res.Headers().find("X-LD-EnvID"); + it != res.Headers().end() && !it->second.empty()) { + sink_->SetEnvironmentId(it->second); } } diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp index 9f489bb4f..b9492c755 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp @@ -1,6 +1,5 @@ #pragma once -#include "../../../../data_components/environment_id/environment_id.hpp" #include "../../../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../../../data_interfaces/destination/idestination.hpp" #include "../../../../data_interfaces/source/idata_synchronizer.hpp" @@ -20,15 +19,13 @@ class PollingDataSource : public data_interfaces::IDataSynchronizer, public std::enable_shared_from_this { public: - PollingDataSource( - boost::asio::any_io_executor const& ioc, - Logger const& logger, - data_components::DataSourceStatusManager& status_manager, - config::built::ServiceEndpoints const& endpoints, - config::built::BackgroundSyncConfig::PollingConfig const& - data_source_config, - config::built::HttpProperties const& http_properties, - std::shared_ptr environment_id); + PollingDataSource(boost::asio::any_io_executor const& ioc, + Logger const& logger, + data_components::DataSourceStatusManager& status_manager, + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig::PollingConfig const& + data_source_config, + config::built::HttpProperties const& http_properties); void StartAsync(data_interfaces::IDestination* dest, data_model::SDKDataSet const* bootstrap_data) override; @@ -49,10 +46,6 @@ class PollingDataSource // it has been destroyed. data_components::DataSourceStatusManager& status_manager_; - // Records the environment ID reported by LaunchDarkly. Shared with the - // client, which reads it when building hook contexts. - std::shared_ptr environment_id_; - // Responsible for performing HTTP requests using boost::asio. network::AsioRequester requester_; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index d7db003fe..8da169959 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -34,12 +34,10 @@ StreamingDataSource::StreamingDataSource( data_components::DataSourceStatusManager& status_manager, config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::StreamingConfig const& streaming, - config::built::HttpProperties const& http_properties, - std::shared_ptr environment_id) + config::built::HttpProperties const& http_properties) : io_(std::move(io)), logger_(logger), status_manager_(status_manager), - environment_id_(std::move(environment_id)), http_config_(http_properties), streaming_endpoint_(endpoints.StreamingBaseUrl()), streaming_config_(streaming) {} @@ -49,6 +47,7 @@ void StreamingDataSource::StartAsync( data_model::SDKDataSet const* bootstrap_data) { boost::ignore_unused(bootstrap_data); + sink_ = dest; event_handler_.emplace(*dest, logger_, status_manager_); status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); @@ -130,15 +129,13 @@ void StreamingDataSource::StartAsync( client_builder.on_response( [weak_self](boost::beast::http::response_header<> const& headers) { auto self = weak_self.lock(); - if (!self || !self->environment_id_ || - headers.result_int() != 200) { + if (!self || headers.result_int() != 200) { return; } - if (auto const it = - headers.find(data_components::EnvironmentId::kHeader); - it != headers.end()) { - self->environment_id_->Set( - std::string_view{it->value().data(), it->value().size()}); + if (auto const it = headers.find("X-LD-EnvID"); + it != headers.end() && !it->value().empty()) { + self->sink_->SetEnvironmentId( + std::string(it->value().data(), it->value().size())); } }); diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp index faba1c50f..e51509449 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp @@ -2,7 +2,6 @@ #include "event_handler.hpp" -#include "../../../../data_components/environment_id/environment_id.hpp" #include "../../../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../../../data_interfaces/destination/idestination.hpp" #include "../../../../data_interfaces/source/idata_synchronizer.hpp" @@ -25,8 +24,7 @@ class StreamingDataSource final data_components::DataSourceStatusManager& status_manager, config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::StreamingConfig const& streaming, - config::built::HttpProperties const& http_properties, - std::shared_ptr environment_id); + config::built::HttpProperties const& http_properties); void StartAsync(data_interfaces::IDestination* dest, data_model::SDKDataSet const* bootstrap_data) override; @@ -41,11 +39,6 @@ class StreamingDataSource final Logger const& logger_; data_components::DataSourceStatusManager& status_manager_; - - // Records the environment ID reported by LaunchDarkly. Shared with the - // client, which reads it when building hook contexts. - std::shared_ptr environment_id_; - config::built::HttpProperties http_config_; std::optional event_handler_; @@ -53,6 +46,9 @@ class StreamingDataSource final config::built::BackgroundSyncConfig::StreamingConfig streaming_config_; + // Destination for all data obtained via streaming. Set by StartAsync. + data_interfaces::IDestination* sink_ = nullptr; + std::shared_ptr client_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp index 67bb98766..260e6f60b 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.cpp @@ -44,6 +44,7 @@ void FDv1AdapterSynchronizer::State::Notify(FDv2SourceResult result) { if (closed_future_.IsFinished()) { return; } + result.environment_id = environment_id_; if (pending_promise_) { promise = std::move(pending_promise_); pending_promise_.reset(); @@ -57,6 +58,12 @@ void FDv1AdapterSynchronizer::State::Notify(FDv2SourceResult result) { promise->Resolve(std::move(result)); } +void FDv1AdapterSynchronizer::State::SetEnvironmentId( + std::string environment_id) { + std::lock_guard lock(mutex_); + environment_id_ = std::move(environment_id); +} + // ----- ConvertingDestination ----- FDv1AdapterSynchronizer::ConvertingDestination::ConvertingDestination( @@ -113,6 +120,13 @@ void FDv1AdapterSynchronizer::ConvertingDestination::Upsert( data_model::Selector{}}}}); } +void FDv1AdapterSynchronizer::ConvertingDestination::SetEnvironmentId( + std::string environment_id) { + if (auto state = state_.lock()) { + state->SetEnvironmentId(std::move(environment_id)); + } +} + std::string const& FDv1AdapterSynchronizer::ConvertingDestination::Identity() const { static std::string const identity = "FDv1 adapter destination"; diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp index a382ff90a..2ca25e1c2 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv1_adapter_synchronizer.hpp @@ -73,6 +73,9 @@ class FDv1AdapterSynchronizer final void Notify(data_interfaces::FDv2SourceResult result); + // Stamped onto subsequently notified results. + void SetEnvironmentId(std::string environment_id); + private: // Finished once the owning FDv1AdapterSynchronizer's close_promise_ // is resolved. Read in Notify to drop late results. @@ -83,6 +86,7 @@ class FDv1AdapterSynchronizer final std::optional> pending_promise_; std::deque result_queue_; + std::optional environment_id_; }; /** @@ -97,6 +101,7 @@ class FDv1AdapterSynchronizer final data_model::FlagDescriptor flag) override; void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + void SetEnvironmentId(std::string environment_id) override; [[nodiscard]] std::string const& Identity() const override; private: diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp index 354fb8850..f28465537 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.cpp @@ -147,6 +147,10 @@ void FDv2DataSystem::OnInitializerResult( data_interfaces::FDv2SourceResult result) { using Result = data_interfaces::FDv2SourceResult; + if (result.environment_id) { + change_notifier_.SetEnvironmentId(*result.environment_id); + } + bool got_basis = false; bool got_shutdown = false; bool disconnected = false; @@ -350,6 +354,10 @@ void FDv2DataSystem::OnSynchronizerResult( } } + if (result.environment_id) { + change_notifier_.SetEnvironmentId(*result.environment_id); + } + bool got_shutdown = false; bool advance = false; bool disconnected = false; @@ -487,4 +495,8 @@ bool FDv2DataSystem::Initialized() const { return store_.Initialized(); } +std::optional FDv2DataSystem::EnvironmentId() const { + return store_.EnvironmentId(); +} + } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp index 3c7147ec1..f97259cdb 100644 --- a/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/fdv2_data_system.hpp @@ -229,6 +229,12 @@ class FDv2DataSystem final : public data_interfaces::IDataSystem { */ bool Initialized() const override; + /** + * Returns the environment ID reported by LaunchDarkly alongside the data, + * if data has been received and the transport reported one. + */ + std::optional EnvironmentId() const override; + private: /** * Signals the orchestration loop to stop and closes any active source. 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 8218843b3..f972eb492 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 @@ -12,6 +12,7 @@ namespace launchdarkly::server_side::data_systems { +static char const* const kEnvironmentIdHeader = "X-LD-EnvID"; static char const* const kFDv1FallbackHeader = "X-LD-FD-Fallback"; static char const* const kFDv1FallbackTtlHeader = "X-LD-FD-Fallback-TTL"; @@ -53,6 +54,15 @@ ReadFDv1FallbackDirective(network::HttpResult::HeadersType const& headers) { return directive; } +static std::optional ReadEnvironmentId( + network::HttpResult::HeadersType const& headers) { + auto const it = headers.find(kEnvironmentIdHeader); + if (it == headers.end() || it->second.empty()) { + return std::nullopt; + } + return it->second; +} + network::HttpRequest MakeFDv2PollRequest( std::string const& polling_base_url, config::built::HttpProperties const& http_properties, @@ -192,8 +202,7 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( network::HttpResult const& res, FDv2ProtocolHandler* protocol_handler, Logger const& logger, - std::string_view identity, - std::shared_ptr const& environment_id) { + std::string_view identity) { if (res.IsError()) { auto const& msg = res.ErrorMessage(); std::string error_msg = msg.has_value() ? *msg : "unknown error"; @@ -204,14 +213,6 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( auto fdv1_fallback = ReadFDv1FallbackDirective(res.Headers()); - if (environment_id && (res.Status() == 200 || res.Status() == 304)) { - if (auto const it = - res.Headers().find(data_components::EnvironmentId::kHeader); - it != res.Headers().end()) { - environment_id->Set(it->second); - } - } - if (res.Status() == 304) { return FDv2SourceResult{ FDv2SourceResult::ChangeSet{ @@ -219,7 +220,7 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( data_model::ChangeSetType::kNone, {}, data_model::Selector{}}}, - fdv1_fallback}; + fdv1_fallback, ReadEnvironmentId(res.Headers())}; } if (res.Status() == 200) { @@ -247,6 +248,7 @@ data_interfaces::FDv2SourceResult HandleFDv2PollResponse( if (!result.fdv1_fallback) { result.fdv1_fallback = fdv1_fallback; } + result.environment_id = ReadEnvironmentId(res.Headers()); return result; } 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 d2256e417..94f90f1dc 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 @@ -1,6 +1,5 @@ #pragma once -#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/fdv2_source_result.hpp" #include @@ -9,7 +8,6 @@ #include #include -#include #include #include #include @@ -26,13 +24,11 @@ network::HttpRequest MakeFDv2PollRequest( // Parse an HTTP response from the FDv2 polling endpoint through the protocol // handler and return the appropriate result. identity is used in log messages -// to identify the caller (e.g. "FDv2 polling initializer"). If environment_id -// is present, it records the environment ID of a successful response. +// to identify the caller (e.g. "FDv2 polling initializer"). data_interfaces::FDv2SourceResult HandleFDv2PollResponse( network::HttpResult const& res, FDv2ProtocolHandler* protocol_handler, Logger const& logger, - std::string_view identity, - std::shared_ptr const& environment_id); + std::string_view identity); } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp index c548fe586..cd4808495 100644 --- a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.cpp @@ -13,21 +13,19 @@ FDv2PollingInitializerFactory::FDv2PollingInitializerFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling, - std::shared_ptr environment_id) + 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)), - environment_id_(std::move(environment_id)) {} + polling_(std::move(polling)) {} std::unique_ptr FDv2PollingInitializerFactory::Build() { return std::make_unique( executor_, logger_, polling_base_url_, http_properties_, - data_model::Selector{}, std::nullopt, environment_id_); + data_model::Selector{}, std::nullopt); } } // 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 index a41ed07ed..ea97dc374 100644 --- a/libs/server-sdk/src/data_systems/fdv2/initializer_factories.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/initializer_factories.hpp @@ -1,6 +1,5 @@ #pragma once -#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_initializer_factory.hpp" #include @@ -22,8 +21,7 @@ class FDv2PollingInitializerFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling, - std::shared_ptr environment_id); + config::built::FDv2Config::PollingConfig polling); std::unique_ptr Build() override; @@ -33,7 +31,6 @@ class FDv2PollingInitializerFactory final std::string const polling_base_url_; config::built::HttpProperties const http_properties_; config::built::FDv2Config::PollingConfig const polling_; - std::shared_ptr const environment_id_; }; } // 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 85dccb5fe..b8b47f7b5 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.cpp @@ -16,15 +16,14 @@ FDv2PollingInitializer::FDv2PollingInitializer( std::string const& polling_base_url, config::built::HttpProperties const& http_properties, data_model::Selector selector, - std::optional filter_key, - std::shared_ptr environment_id) + std::optional filter_key) : request_(MakeFDv2PollRequest(polling_base_url, http_properties, std::move(selector), std::move(filter_key), logger)), requester_(executor, http_properties.Tls()), - state_(std::make_shared(logger, std::move(environment_id))) {} + state_(std::make_shared(logger)) {} FDv2PollingInitializer::~FDv2PollingInitializer() { close_promise_.Resolve(std::monostate{}); @@ -77,7 +76,7 @@ FDv2SourceResult FDv2PollingInitializer::HandlePollResult( network::HttpResult const& res) { FDv2ProtocolHandler protocol_handler; return HandleFDv2PollResponse(res, &protocol_handler, state->logger, - kIdentity, state->environment_id); + kIdentity); } } // namespace launchdarkly::server_side::data_systems 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 cb7a57e39..8d27a549f 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_initializer.hpp @@ -1,6 +1,5 @@ #pragma once -#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_initializer.hpp" #include @@ -34,14 +33,12 @@ class FDv2PollingInitializer final : public data_interfaces::IFDv2Initializer { * Constructs an initializer for a single poll request. * If filter_key is present, only the specified payload filter is requested. */ - FDv2PollingInitializer( - boost::asio::any_io_executor const& executor, - Logger const& logger, - std::string const& polling_base_url, - config::built::HttpProperties const& http_properties, - data_model::Selector selector, - std::optional filter_key, - std::shared_ptr environment_id); + FDv2PollingInitializer(boost::asio::any_io_executor const& executor, + Logger const& logger, + std::string const& polling_base_url, + config::built::HttpProperties const& http_properties, + data_model::Selector selector, + std::optional filter_key); ~FDv2PollingInitializer() override; @@ -58,13 +55,7 @@ class FDv2PollingInitializer final : public data_interfaces::IFDv2Initializer { // Logger is itself thread-safe. Logger logger; - // EnvironmentId is itself thread-safe. - std::shared_ptr environment_id; - - State(Logger logger, - std::shared_ptr environment_id) - : logger(std::move(logger)), - environment_id(std::move(environment_id)) {} + explicit State(Logger logger) : logger(std::move(logger)) {} }; /** Interprets an HTTP response as a source result. */ 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 bdd0fb3c1..33ea500b8 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.cpp @@ -22,16 +22,14 @@ FDv2PollingSynchronizer::State::State( std::chrono::seconds poll_interval, std::string polling_base_url, config::built::HttpProperties const& http_properties, - std::optional filter_key, - std::shared_ptr environment_id) + std::optional filter_key) : logger_(std::move(logger)), poll_interval_(std::max(poll_interval, kMinPollInterval)), polling_base_url_(std::move(polling_base_url)), http_properties_(http_properties), filter_key_(std::move(filter_key)), requester_(executor, http_properties.Tls()), - executor_(executor), - environment_id_(std::move(environment_id)) {} + executor_(executor) {} async::Future FDv2PollingSynchronizer::State::Request( data_model::Selector const& selector) const { @@ -52,8 +50,7 @@ async::Future FDv2PollingSynchronizer::State::Request( FDv2SourceResult FDv2PollingSynchronizer::State::HandlePollResult( network::HttpResult const& res) { FDv2ProtocolHandler protocol_handler; - return HandleFDv2PollResponse(res, &protocol_handler, logger_, kIdentity, - environment_id_); + return HandleFDv2PollResponse(res, &protocol_handler, logger_, kIdentity); } async::Future FDv2PollingSynchronizer::State::Delay( @@ -87,15 +84,13 @@ FDv2PollingSynchronizer::FDv2PollingSynchronizer( std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::seconds poll_interval, - std::shared_ptr environment_id) + std::chrono::seconds poll_interval) : state_(std::make_shared(logger, executor, poll_interval, std::move(polling_base_url), http_properties, - std::move(filter_key), - std::move(environment_id))) { + std::move(filter_key))) { if (poll_interval < kMinPollInterval) { LD_LOG(logger, LogLevel::kWarn) << kIdentity << ": polling interval too frequent, defaulting to " 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 7566c16dc..3b03511ce 100644 --- a/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/polling_synchronizer.hpp @@ -1,6 +1,5 @@ #pragma once -#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_synchronizer.hpp" #include @@ -11,7 +10,6 @@ #include #include -#include #include #include #include @@ -44,8 +42,7 @@ class FDv2PollingSynchronizer final std::string polling_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::seconds poll_interval, - std::shared_ptr environment_id); + std::chrono::seconds poll_interval); ~FDv2PollingSynchronizer() override; @@ -67,8 +64,7 @@ class FDv2PollingSynchronizer final std::chrono::seconds poll_interval, std::string polling_base_url, config::built::HttpProperties const& http_properties, - std::optional filter_key, - std::shared_ptr environment_id); + std::optional filter_key); /** Issues an async HTTP poll request and returns a Future resolving * with the result. */ @@ -105,8 +101,6 @@ class FDv2PollingSynchronizer final std::optional const filter_key_; network::Requester const requester_; boost::asio::any_io_executor const executor_; - // EnvironmentId is itself thread-safe. - std::shared_ptr const environment_id_; // Mutable state, guarded by mutex_. std::mutex mutex_; 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 8843493bb..c0d584ccf 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.cpp @@ -40,15 +40,13 @@ FDv2StreamingSynchronizer::State::State( std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay, - std::shared_ptr environment_id) + std::chrono::milliseconds initial_reconnect_delay) : logger_(std::move(logger)), streaming_base_url_(std::move(streaming_base_url)), http_properties_(http_properties), filter_key_(std::move(filter_key)), initial_reconnect_delay_(initial_reconnect_delay), - executor_(executor), - environment_id_(std::move(environment_id)) {} + executor_(executor) {} void FDv2StreamingSynchronizer::State::EnsureStarted( data_model::Selector const& selector, @@ -188,12 +186,12 @@ void FDv2StreamingSynchronizer::State::OnConnect(HttpRequest* req) { void FDv2StreamingSynchronizer::State::OnResponse( HttpResponseHeader const& headers) { - if (environment_id_ && headers.result_int() == 200) { - if (auto const env_it = - headers.find(data_components::EnvironmentId::kHeader); - env_it != headers.end()) { - environment_id_->Set(std::string_view{env_it->value().data(), - env_it->value().size()}); + if (headers.result_int() == 200) { + if (auto const env_it = headers.find("X-LD-EnvID"); + env_it != headers.end() && !env_it->value().empty()) { + std::lock_guard lock(mutex_); + environment_id_ = + std::string(env_it->value().data(), env_it->value().size()); } } @@ -348,6 +346,7 @@ void FDv2StreamingSynchronizer::State::Notify(FDv2SourceResult result) { if (!result.fdv1_fallback) { result.fdv1_fallback = latest_fdv1_fallback_; } + result.environment_id = environment_id_; if (pending_promise_) { promise = std::move(pending_promise_); pending_promise_.reset(); @@ -398,15 +397,13 @@ FDv2StreamingSynchronizer::FDv2StreamingSynchronizer( std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay, - std::shared_ptr environment_id) + std::chrono::milliseconds initial_reconnect_delay) : state_(std::make_shared(logger, executor, std::move(streaming_base_url), http_properties, std::move(filter_key), - initial_reconnect_delay, - std::move(environment_id))) {} + initial_reconnect_delay)) {} FDv2StreamingSynchronizer::~FDv2StreamingSynchronizer() { Close(); 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 a3bd8d5dc..885456e39 100644 --- a/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/streaming_synchronizer.hpp @@ -1,6 +1,5 @@ #pragma once -#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_synchronizer.hpp" #include @@ -50,8 +49,7 @@ class FDv2StreamingSynchronizer final std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay, - std::shared_ptr environment_id); + std::chrono::milliseconds initial_reconnect_delay); ~FDv2StreamingSynchronizer() override; @@ -75,8 +73,7 @@ class FDv2StreamingSynchronizer final std::string streaming_base_url, config::built::HttpProperties const& http_properties, std::optional filter_key, - std::chrono::milliseconds initial_reconnect_delay, - std::shared_ptr environment_id); + std::chrono::milliseconds initial_reconnect_delay); /** * Updates the stored selector, starts the SSE client if not already @@ -140,8 +137,6 @@ class FDv2StreamingSynchronizer final std::optional const filter_key_; std::chrono::milliseconds const initial_reconnect_delay_; boost::asio::any_io_executor const executor_; - // EnvironmentId is itself thread-safe. - std::shared_ptr const environment_id_; // Touched only from SSE callbacks, which all run on the same strand. // No lock required. @@ -154,6 +149,8 @@ class FDv2StreamingSynchronizer final // FDv1 fallback directive from the most recent SSE response. std::optional latest_fdv1_fallback_; + // Environment ID reported by the most recent successful SSE response. + std::optional environment_id_; data_model::Selector latest_selector_; std::optional base_url_; std::shared_ptr sse_client_; diff --git a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp index 611af08ac..140f64035 100644 --- a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp +++ b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.cpp @@ -17,21 +17,19 @@ FDv2StreamingSynchronizerFactory::FDv2StreamingSynchronizerFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::StreamingConfig streaming, - std::shared_ptr environment_id) + 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)), - environment_id_(std::move(environment_id)) {} + streaming_(std::move(streaming)) {} std::unique_ptr FDv2StreamingSynchronizerFactory::Build() { return std::make_unique( executor_, logger_, streaming_base_url_, http_properties_, std::nullopt, - streaming_.initial_reconnect_delay, environment_id_); + streaming_.initial_reconnect_delay); } FDv2PollingSynchronizerFactory::FDv2PollingSynchronizerFactory( @@ -39,21 +37,19 @@ FDv2PollingSynchronizerFactory::FDv2PollingSynchronizerFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling, - std::shared_ptr environment_id) + 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)), - environment_id_(std::move(environment_id)) {} + polling_(std::move(polling)) {} std::unique_ptr FDv2PollingSynchronizerFactory::Build() { return std::make_unique( executor_, logger_, polling_base_url_, http_properties_, std::nullopt, - polling_.poll_interval, environment_id_); + polling_.poll_interval); } FDv1StreamingAdapterFactory::FDv1StreamingAdapterFactory( @@ -61,14 +57,12 @@ FDv1StreamingAdapterFactory::FDv1StreamingAdapterFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1StreamingConfig streaming, - config::built::HttpProperties http_properties, - std::shared_ptr environment_id) + config::built::HttpProperties http_properties) : executor_(std::move(executor)), logger_(std::move(logger)), endpoints_(std::move(endpoints)), streaming_(std::move(streaming)), - http_properties_(std::move(http_properties)), - environment_id_(std::move(environment_id)) {} + http_properties_(std::move(http_properties)) {} std::unique_ptr FDv1StreamingAdapterFactory::Build() { @@ -76,7 +70,7 @@ FDv1StreamingAdapterFactory::Build() { [this](data_components::DataSourceStatusManager& status_manager) { return std::make_shared( executor_, logger_, status_manager, endpoints_, streaming_, - http_properties_, environment_id_); + http_properties_); }); } @@ -85,14 +79,12 @@ FDv1PollingAdapterFactory::FDv1PollingAdapterFactory( Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1PollingConfig polling, - config::built::HttpProperties http_properties, - std::shared_ptr environment_id) + config::built::HttpProperties http_properties) : executor_(std::move(executor)), logger_(std::move(logger)), endpoints_(std::move(endpoints)), polling_(std::move(polling)), - http_properties_(std::move(http_properties)), - environment_id_(std::move(environment_id)) {} + http_properties_(std::move(http_properties)) {} std::unique_ptr FDv1PollingAdapterFactory::Build() { @@ -100,7 +92,7 @@ FDv1PollingAdapterFactory::Build() { [this](data_components::DataSourceStatusManager& status_manager) { return std::make_shared( executor_, logger_, status_manager, endpoints_, polling_, - http_properties_, environment_id_); + http_properties_); }); } diff --git a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp index 9010d4f5e..3602df03f 100644 --- a/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp +++ b/libs/server-sdk/src/data_systems/fdv2/synchronizer_factories.hpp @@ -1,6 +1,5 @@ #pragma once -#include "../../data_components/environment_id/environment_id.hpp" #include "../../data_interfaces/source/ifdv2_synchronizer_factory.hpp" #include @@ -22,8 +21,7 @@ class FDv2StreamingSynchronizerFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::StreamingConfig streaming, - std::shared_ptr environment_id); + config::built::FDv2Config::StreamingConfig streaming); std::unique_ptr Build() override; @@ -33,7 +31,6 @@ class FDv2StreamingSynchronizerFactory final std::string const streaming_base_url_; config::built::HttpProperties const http_properties_; config::built::FDv2Config::StreamingConfig const streaming_; - std::shared_ptr const environment_id_; }; /** @@ -47,8 +44,7 @@ class FDv2PollingSynchronizerFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::HttpProperties http_properties, - config::built::FDv2Config::PollingConfig polling, - std::shared_ptr environment_id); + config::built::FDv2Config::PollingConfig polling); std::unique_ptr Build() override; @@ -58,7 +54,6 @@ class FDv2PollingSynchronizerFactory final std::string const polling_base_url_; config::built::HttpProperties const http_properties_; config::built::FDv2Config::PollingConfig const polling_; - std::shared_ptr const environment_id_; }; /** @@ -73,8 +68,7 @@ class FDv1StreamingAdapterFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1StreamingConfig streaming, - config::built::HttpProperties http_properties, - std::shared_ptr environment_id); + config::built::HttpProperties http_properties); std::unique_ptr Build() override; @@ -86,7 +80,6 @@ class FDv1StreamingAdapterFactory final config::built::ServiceEndpoints const endpoints_; config::built::FDv2Config::FDv1StreamingConfig const streaming_; config::built::HttpProperties const http_properties_; - std::shared_ptr const environment_id_; }; /** @@ -101,8 +94,7 @@ class FDv1PollingAdapterFactory final Logger logger, config::built::ServiceEndpoints endpoints, config::built::FDv2Config::FDv1PollingConfig polling, - config::built::HttpProperties http_properties, - std::shared_ptr environment_id); + config::built::HttpProperties http_properties); std::unique_ptr Build() override; @@ -114,7 +106,6 @@ class FDv1PollingAdapterFactory final config::built::ServiceEndpoints const endpoints_; config::built::FDv2Config::FDv1PollingConfig const polling_; config::built::HttpProperties const http_properties_; - std::shared_ptr const environment_id_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 4235b9676..b9d775e58 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -142,6 +142,10 @@ bool LazyLoad::Initialized() const { return initialized_.value_or(false); } +std::optional LazyLoad::EnvironmentId() const { + return std::nullopt; +} + void LazyLoad::RefreshAllFlags() const { RefreshAll(Keys::kAllFlags, data_components::DataKind::kFlag, diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 9584f60d6..d15c46f01 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -58,6 +58,8 @@ class LazyLoad final : public data_interfaces::IDataSystem { bool Initialized() const override; + std::optional EnvironmentId() const override; + // Public for usage in tests. struct Kinds { static integrations::FlagKind const Flag; diff --git a/libs/server-sdk/src/data_systems/offline.cpp b/libs/server-sdk/src/data_systems/offline.cpp index 148c10a2d..73cbfd926 100644 --- a/libs/server-sdk/src/data_systems/offline.cpp +++ b/libs/server-sdk/src/data_systems/offline.cpp @@ -41,4 +41,8 @@ std::string const& OfflineSystem::Identity() const { return ident; } +std::optional OfflineSystem::EnvironmentId() const { + return std::nullopt; +} + } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/offline.hpp b/libs/server-sdk/src/data_systems/offline.hpp index c9355bd24..ae869de2f 100644 --- a/libs/server-sdk/src/data_systems/offline.hpp +++ b/libs/server-sdk/src/data_systems/offline.hpp @@ -22,6 +22,7 @@ class OfflineSystem final : public data_interfaces::IDataSystem { [[nodiscard]] bool Initialized() const override; [[nodiscard]] std::string const& Identity() const override; void Initialize() override; + [[nodiscard]] std::optional EnvironmentId() const override; private: data_components::DataSourceStatusManager& status_manager_; diff --git a/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp b/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp index 26de674b6..f232bbdc8 100644 --- a/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp +++ b/libs/server-sdk/tests/fdv1_adapter_synchronizer_test.cpp @@ -115,6 +115,20 @@ TEST(FDv1AdapterSynchronizerTest, FDv1InitProducesFullChangeSet) { EXPECT_FALSE(result->fdv1_fallback); } +TEST(FDv1AdapterSynchronizerTest, FDv1EnvironmentIdIsReportedWithChangeSet) { + MockFDv1Source* source = nullptr; + FDv1AdapterSynchronizer adapter(MakeMockBuilder(&source)); + + auto future = adapter.Next(data_model::Selector{}); + + source->destination_->SetEnvironmentId("env-123"); + source->destination_->Init(data_model::SDKDataSet{}); + + auto result = future.WaitForResult(1s); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(std::optional{"env-123"}, result->environment_id); +} + TEST(FDv1AdapterSynchronizerTest, FDv1UpsertProducesPartialChangeSet) { MockFDv1Source* source = nullptr; FDv1AdapterSynchronizer adapter(MakeMockBuilder(&source)); diff --git a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp index e6fb04061..f87e14c1b 100644 --- a/libs/server-sdk/tests/fdv2_polling_impl_test.cpp +++ b/libs/server-sdk/tests/fdv2_polling_impl_test.cpp @@ -22,14 +22,11 @@ static Logger MakeNullLogger() { static FDv2SourceResult HandleResponse( unsigned status, std::optional body, - network::HttpResult::HeadersType headers, - std::shared_ptr - environment_id = nullptr) { + network::HttpResult::HeadersType headers) { auto logger = MakeNullLogger(); FDv2ProtocolHandler handler; network::HttpResult res{status, std::move(body), std::move(headers)}; - return HandleFDv2PollResponse(res, &handler, logger, "test", - environment_id); + return HandleFDv2PollResponse(res, &handler, logger, "test"); } TEST(HandleFDv2PollResponseTest, NotModifiedSetsFdv1FallbackWhenHeaderPresent) { @@ -128,8 +125,7 @@ TEST(HandleFDv2PollResponseTest, NetworkErrorDoesNotSetFlag) { auto logger = MakeNullLogger(); FDv2ProtocolHandler handler; network::HttpResult res{std::optional{"connection refused"}}; - auto result = HandleFDv2PollResponse(res, &handler, logger, "test", - /* environment_id= */ nullptr); + auto result = HandleFDv2PollResponse(res, &handler, logger, "test"); EXPECT_TRUE( std::holds_alternative(result.value)); @@ -166,43 +162,40 @@ TEST(MakeFDv2PollRequestTest, ValidFilterKeyIsIncluded) { EXPECT_EQ(req.Url(), "http://example.com/sdk/poll?filter=my-filter_1.0"); } -TEST(MakeFDv2PollRequestTest, InvalidFilterKeyIsDropped) { - auto logger = MakeNullLogger(); - auto props = - config::shared::Defaults::HttpProperties(); - auto req = - MakeFDv2PollRequest("http://example.com", props, data_model::Selector{}, - std::string{"has spaces"}, logger); - EXPECT_EQ(req.Url(), "http://example.com/sdk/poll"); +TEST(HandleFDv2PollResponseTest, OkReportsEnvironmentId) { + auto result = + HandleResponse(200, R"({"events":[]})", {{"X-LD-EnvID", "env-123"}}); + EXPECT_EQ(std::optional{"env-123"}, result.environment_id); } -TEST(HandleFDv2PollResponseTest, OkRecordsEnvironmentId) { - auto environment_id = - std::make_shared(); - HandleResponse(200, R"({"events":[]})", {{"X-LD-EnvID", "env-123"}}, - environment_id); - EXPECT_EQ(std::optional{"env-123"}, environment_id->Get()); +TEST(HandleFDv2PollResponseTest, NotModifiedReportsEnvironmentId) { + auto result = + HandleResponse(304, std::nullopt, {{"X-LD-EnvID", "env-123"}}); + EXPECT_EQ(std::optional{"env-123"}, result.environment_id); } -TEST(HandleFDv2PollResponseTest, NotModifiedRecordsEnvironmentId) { - auto environment_id = - std::make_shared(); - HandleResponse(304, std::nullopt, {{"X-LD-EnvID", "env-123"}}, - environment_id); - EXPECT_EQ(std::optional{"env-123"}, environment_id->Get()); +TEST(HandleFDv2PollResponseTest, ErrorStatusDoesNotReportEnvironmentId) { + auto result = + HandleResponse(503, std::nullopt, {{"X-LD-EnvID", "env-123"}}); + EXPECT_FALSE(result.environment_id); } -TEST(HandleFDv2PollResponseTest, ErrorStatusDoesNotRecordEnvironmentId) { - auto environment_id = - std::make_shared(); - HandleResponse(503, std::nullopt, {{"X-LD-EnvID", "env-123"}}, - environment_id); - EXPECT_FALSE(environment_id->Get()); +TEST(HandleFDv2PollResponseTest, MissingHeaderDoesNotReportEnvironmentId) { + auto result = HandleResponse(200, R"({"events":[]})", {}); + EXPECT_FALSE(result.environment_id); } -TEST(HandleFDv2PollResponseTest, MissingHeaderDoesNotRecordEnvironmentId) { - auto environment_id = - std::make_shared(); - HandleResponse(200, R"({"events":[]})", {}, environment_id); - EXPECT_FALSE(environment_id->Get()); +TEST(HandleFDv2PollResponseTest, EmptyHeaderDoesNotReportEnvironmentId) { + auto result = HandleResponse(200, R"({"events":[]})", {{"X-LD-EnvID", ""}}); + EXPECT_FALSE(result.environment_id); +} + +TEST(MakeFDv2PollRequestTest, InvalidFilterKeyIsDropped) { + auto logger = MakeNullLogger(); + auto props = + config::shared::Defaults::HttpProperties(); + auto req = + MakeFDv2PollRequest("http://example.com", props, data_model::Selector{}, + std::string{"has spaces"}, logger); + 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 59d16c269..25bebefc9 100644 --- a/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp +++ b/libs/server-sdk/tests/fdv2_streaming_synchronizer_test.cpp @@ -145,8 +145,7 @@ TEST(FDv2StreamingSynchronizerTest, NextBadEndpointUrlReturnsTerminalError) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "not a url", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); // Act: trigger setup with a malformed streaming URL. URL parsing happens // inside EnsureStarted on the first Next call. @@ -169,8 +168,7 @@ TEST(FDv2StreamingSynchronizerTest, CloseBeforeNextReturnsShutdown) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); synchronizer.Close(); // Act: call Next on an already-closed synchronizer. @@ -190,8 +188,7 @@ TEST(FDv2StreamingSynchronizerTest, CloseDuringPendingNextResolvesShutdown) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + 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. @@ -219,8 +216,7 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectEmptySelectorNoBasisParam) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -244,8 +240,7 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectWithSelectorAppendsBasis) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -271,8 +266,7 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectWithFilterKeyAppendsFilter) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::string("my-filter"), 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::string("my-filter"), 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -296,8 +290,7 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectInvalidFilterKeyIsDropped) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::string("has spaces"), 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::string("has spaces"), 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -319,8 +312,7 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectReconnectUsesLatestSelector) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -357,8 +349,7 @@ TEST(FDv2StreamingSynchronizerTest, OnConnectSelectorStateIsPercentEncoded) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "https://stream.example.com", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); boost::urls::url base = boost::urls::parse_uri("https://stream.example.com").value(); @@ -389,8 +380,7 @@ TEST(FDv2StreamingSynchronizerTest, FullChangesetEventsReturnsChangeSet) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event server_intent("server-intent", @@ -427,8 +417,7 @@ TEST(FDv2StreamingSynchronizerTest, GoodbyeEventReturnsGoodbye) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event goodbye("goodbye", R"({"reason":"bye"})"); @@ -453,8 +442,7 @@ TEST(FDv2StreamingSynchronizerTest, GoodbyeEventTriggersAsyncRestart) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -483,8 +471,7 @@ TEST(FDv2StreamingSynchronizerTest, FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // Begin accumulating a payload that we'll abandon mid-flight via Goodbye. @@ -544,8 +531,7 @@ TEST(FDv2StreamingSynchronizerTest, ServerErrorEventReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Event server_error("error", R"({"id":"abc","reason":"oops"})"); @@ -576,8 +562,7 @@ TEST(FDv2StreamingSynchronizerTest, UnknownEventWithGarbageBodyIsIgnored) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -596,8 +581,7 @@ TEST(FDv2StreamingSynchronizerTest, MalformedJsonEventReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -629,8 +613,7 @@ TEST(FDv2StreamingSynchronizerTest, FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); auto mock_client = std::make_shared(); @@ -656,8 +639,7 @@ TEST(FDv2StreamingSynchronizerTest, TranslationFailureReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // A non-empty segment object missing required fields triggers a schema @@ -702,8 +684,7 @@ TEST(FDv2StreamingSynchronizerTest, FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Error error{sse::errors::UnrecoverableClientError{ @@ -731,8 +712,7 @@ TEST(FDv2StreamingSynchronizerTest, RecoverableReadTimeoutReturnsInterrupted) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); sse::Error error{sse::errors::ReadTimeout{std::chrono::milliseconds(100)}}; @@ -763,8 +743,7 @@ TEST(FDv2StreamingSynchronizerTest, OnResponseDirectivePropagatesToChangeSet) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> headers; @@ -801,8 +780,7 @@ TEST(FDv2StreamingSynchronizerTest, SecondResponseWithoutDirectiveClearsFlag) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> first; @@ -830,8 +808,7 @@ TEST(FDv2StreamingSynchronizerTest, ErrorAfterDirectiveCarriesFlag) { FDv2StreamingSynchronizer synchronizer( runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + MakeHttpProperties(), std::nullopt, 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); boost::beast::http::response_header<> headers; @@ -856,9 +833,9 @@ TEST(FDv2StreamingSynchronizerTest, DirectiveWithTtlHeaderParsesValue) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + runner.context().get_executor(), logger, + "http://localhost", MakeHttpProperties(), std::nullopt, + 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // Server sends the directive with an explicit TTL. @@ -885,9 +862,9 @@ TEST(FDv2StreamingSynchronizerTest, DirectiveWithoutTtlHeaderUsesDefault) { IoContextRunner runner; FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, - /* environment_id= */ nullptr); + runner.context().get_executor(), logger, + "http://localhost", MakeHttpProperties(), std::nullopt, + 1s); FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); // Server sends the directive with no TTL header. @@ -907,41 +884,3 @@ TEST(FDv2StreamingSynchronizerTest, DirectiveWithoutTtlHeaderUsesDefault) { ASSERT_TRUE(result->fdv1_fallback); EXPECT_EQ(FDv1FallbackDirective::kDefaultTtl, result->fdv1_fallback->ttl); } - -TEST(FDv2StreamingSynchronizerTest, SuccessfulResponseRecordsEnvironmentId) { - auto logger = MakeNullLogger(); - IoContextRunner runner; - - auto environment_id = - std::make_shared(); - FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, environment_id); - FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); - - boost::beast::http::response_header<> headers; - headers.result(200); - headers.set("X-LD-EnvID", "env-123"); - FDv2StreamingSynchronizerTestPeer::OnResponse(synchronizer, headers); - - EXPECT_EQ(std::optional{"env-123"}, environment_id->Get()); -} - -TEST(FDv2StreamingSynchronizerTest, ErrorResponseDoesNotRecordEnvironmentId) { - auto logger = MakeNullLogger(); - IoContextRunner runner; - - auto environment_id = - std::make_shared(); - FDv2StreamingSynchronizer synchronizer( - runner.context().get_executor(), logger, "http://localhost", - MakeHttpProperties(), std::nullopt, 1s, environment_id); - FDv2StreamingSynchronizerTestPeer::MarkStarted(synchronizer); - - boost::beast::http::response_header<> headers; - headers.result(401); - headers.set("X-LD-EnvID", "env-123"); - FDv2StreamingSynchronizerTestPeer::OnResponse(synchronizer, headers); - - EXPECT_FALSE(environment_id->Get()); -} diff --git a/libs/server-sdk/tests/memory_store_test.cpp b/libs/server-sdk/tests/memory_store_test.cpp index 901282ea2..1ebd30c7e 100644 --- a/libs/server-sdk/tests/memory_store_test.cpp +++ b/libs/server-sdk/tests/memory_store_test.cpp @@ -299,6 +299,29 @@ TEST(MemoryStoreTest, CanDeleteExistingSegment) { ASSERT_FALSE(store.RemoveSegment("segmentA")); } +TEST(MemoryStoreTest, HasNoEnvironmentIdByDefault) { + MemoryStore store; + store.Init(SDKDataSet()); + EXPECT_FALSE(store.EnvironmentId()); +} + +TEST(MemoryStoreTest, EnvironmentIdIsNotVisibleBeforeInit) { + MemoryStore store; + store.SetEnvironmentId("env-123"); + EXPECT_FALSE(store.EnvironmentId()); + + store.Init(SDKDataSet()); + EXPECT_EQ(std::optional{"env-123"}, store.EnvironmentId()); +} + +TEST(MemoryStoreTest, EnvironmentIdIsRetainedAcrossInit) { + MemoryStore store; + store.SetEnvironmentId("env-123"); + store.Init(SDKDataSet()); + store.Init(SDKDataSet()); + EXPECT_EQ(std::optional{"env-123"}, store.EnvironmentId()); +} + TEST(MemoryStoreTest, CanDeleteExistingFlag) { MemoryStore store; From 1ca484266e1f6b655417c71e496145a2046b002b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:16:05 +0000 Subject: [PATCH 5/6] ci: Re-run after a transient dependency fetch failure Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com> From abe15ecd89ace52f22ad6016de2ecf572dbee2e7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:56:27 +0000 Subject: [PATCH 6/6] ci: Re-run after transient runner failures Co-Authored-By: rlamb@launchdarkly.com <4955475+kinyoklion@users.noreply.github.com>