From 89e79686d628c07a8f47f23597faf6c803ce5460 Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 11 Jun 2026 13:39:59 -0700 Subject: [PATCH 1/5] fix: reject null store in BigSegmentsBuilder::Build --- libs/common/include/launchdarkly/error.hpp | 1 + libs/common/src/error.cpp | 2 + .../config/builders/big_segments_builder.hpp | 8 ++- .../config/builders/big_segments_builder.cpp | 7 ++- libs/server-sdk/src/config/config_builder.cpp | 16 +++++ .../tests/big_segments_builder_test.cpp | 62 +++++++++++-------- libs/server-sdk/tests/config_builder_test.cpp | 55 ++++++++++++++++ 7 files changed, 121 insertions(+), 30 deletions(-) rename libs/server-sdk/{src => include/launchdarkly/server_side}/config/builders/big_segments_builder.hpp (93%) diff --git a/libs/common/include/launchdarkly/error.hpp b/libs/common/include/launchdarkly/error.hpp index baa35bfd4..935a05152 100644 --- a/libs/common/include/launchdarkly/error.hpp +++ b/libs/common/include/launchdarkly/error.hpp @@ -24,6 +24,7 @@ enum class Error : std::uint32_t { /* Client-side errors: 10000-19999 */ /* Server-side errors: 20000-29999 */ kConfig_DataSystem_LazyLoad_MissingSource = 20000, + kConfig_BigSegments_NullStore = 20001, kMax = std::numeric_limits::max() }; diff --git a/libs/common/src/error.cpp b/libs/common/src/error.cpp index 2f2eef527..934f17627 100644 --- a/libs/common/src/error.cpp +++ b/libs/common/src/error.cpp @@ -32,6 +32,8 @@ char const* ErrorToString(Error err) { return "sdk key: cannot be empty"; case Error::kConfig_DataSystem_LazyLoad_MissingSource: return "data system: lazy load config requires a source"; + case Error::kConfig_BigSegments_NullStore: + return "big segments: store must not be null"; case Error::kMax: break; } diff --git a/libs/server-sdk/src/config/builders/big_segments_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/big_segments_builder.hpp similarity index 93% rename from libs/server-sdk/src/config/builders/big_segments_builder.hpp rename to libs/server-sdk/include/launchdarkly/server_side/config/builders/big_segments_builder.hpp index 9713292e3..4dffb71ca 100644 --- a/libs/server-sdk/src/config/builders/big_segments_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/big_segments_builder.hpp @@ -3,6 +3,10 @@ #include #include +#include + +#include + #include #include #include @@ -68,12 +72,14 @@ class BigSegmentsBuilder { /** * @brief Resolves the configuration. * + * Returns an error if the store passed to the constructor was null. + * * If the configured @ref StatusPollInterval exceeds @ref StaleAfter, * the poll interval in the returned config is clamped to the * stale-after value so the SDK can detect staleness within one poll * cycle. */ - [[nodiscard]] built::BigSegmentsConfig Build() const; + [[nodiscard]] tl::expected Build() const; private: std::shared_ptr store_; diff --git a/libs/server-sdk/src/config/builders/big_segments_builder.cpp b/libs/server-sdk/src/config/builders/big_segments_builder.cpp index 5bad1b673..3ea336afb 100644 --- a/libs/server-sdk/src/config/builders/big_segments_builder.cpp +++ b/libs/server-sdk/src/config/builders/big_segments_builder.cpp @@ -1,4 +1,4 @@ -#include "big_segments_builder.hpp" +#include #include #include @@ -55,7 +55,10 @@ BigSegmentsBuilder& BigSegmentsBuilder::StaleAfter( return *this; } -built::BigSegmentsConfig BigSegmentsBuilder::Build() const { +tl::expected BigSegmentsBuilder::Build() const { + if (!store_) { + return tl::make_unexpected(Error::kConfig_BigSegments_NullStore); + } auto const poll = std::min(status_poll_interval_, stale_after_); return built::BigSegmentsConfig{store_, context_cache_size_, context_cache_time_, poll, stale_after_}; diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 26efa2deb..87ebb8afd 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -21,6 +21,12 @@ config::builders::DataSystemBuilder& ConfigBuilder::DataSystem() { return data_system_builder_; } +ConfigBuilder& ConfigBuilder::BigSegments( + config::builders::BigSegmentsBuilder builder) { + big_segments_builder_ = std::move(builder); + return *this; +} + config::builders::HttpPropertiesBuilder& ConfigBuilder::HttpProperties() { return http_properties_builder_; } @@ -76,6 +82,15 @@ tl::expected ConfigBuilder::Build() const { auto logging = logging_config_builder_.Build(); + std::optional big_segments_config; + if (big_segments_builder_) { + auto built = big_segments_builder_->Build(); + if (!built) { + return tl::make_unexpected(built.error()); + } + big_segments_config = std::move(*built); + } + return {tl::in_place, sdk_key, logging, @@ -83,6 +98,7 @@ tl::expected ConfigBuilder::Build() const { *events_config, app_tag, std::move(*data_system_config), + std::move(big_segments_config), std::move(http_properties), hooks_}; } diff --git a/libs/server-sdk/tests/big_segments_builder_test.cpp b/libs/server-sdk/tests/big_segments_builder_test.cpp index c3734e401..8073cc1a7 100644 --- a/libs/server-sdk/tests/big_segments_builder_test.cpp +++ b/libs/server-sdk/tests/big_segments_builder_test.cpp @@ -3,7 +3,7 @@ #include #include -#include "config/builders/big_segments_builder.hpp" +#include #include #include @@ -40,24 +40,25 @@ std::shared_ptr MakeStubStore() { TEST(BigSegmentsBuilderTest, DefaultsMatchSpec) { auto store = MakeStubStore(); auto const cfg = BigSegmentsBuilder(store).Build(); + ASSERT_TRUE(cfg); - EXPECT_EQ(cfg.context_cache_size, 1000u); - EXPECT_EQ(cfg.context_cache_time, 5s); - EXPECT_EQ(cfg.status_poll_interval, 5s); - EXPECT_EQ(cfg.stale_after, 2min); + EXPECT_EQ(cfg->context_cache_size, 1000u); + EXPECT_EQ(cfg->context_cache_time, 5s); + EXPECT_EQ(cfg->status_poll_interval, 5s); + EXPECT_EQ(cfg->stale_after, 2min); } TEST(BigSegmentsBuilderTest, BuildPreservesStoreIdentity) { auto store = MakeStubStore(); auto const cfg = BigSegmentsBuilder(store).Build(); - EXPECT_EQ(cfg.store.get(), store.get()); + ASSERT_TRUE(cfg); + EXPECT_EQ(cfg->store.get(), store.get()); } -TEST(BigSegmentsBuilderTest, AcceptsNullStore) { - // The builder doesn't validate the store; downstream components treat a - // null store as "Big Segments not configured". +TEST(BigSegmentsBuilderTest, NullStoreIsRejected) { auto const cfg = BigSegmentsBuilder(nullptr).Build(); - EXPECT_EQ(cfg.store, nullptr); + ASSERT_FALSE(cfg); + EXPECT_EQ(cfg.error(), launchdarkly::Error::kConfig_BigSegments_NullStore); } TEST(BigSegmentsBuilderTest, SettersOverrideEachField) { @@ -68,11 +69,12 @@ TEST(BigSegmentsBuilderTest, SettersOverrideEachField) { .StatusPollInterval(13s) .StaleAfter(60s) .Build(); + ASSERT_TRUE(cfg); - EXPECT_EQ(cfg.context_cache_size, 7u); - EXPECT_EQ(cfg.context_cache_time, 11s); - EXPECT_EQ(cfg.status_poll_interval, 13s); - EXPECT_EQ(cfg.stale_after, 60s); + EXPECT_EQ(cfg->context_cache_size, 7u); + EXPECT_EQ(cfg->context_cache_time, 11s); + EXPECT_EQ(cfg->status_poll_interval, 13s); + EXPECT_EQ(cfg->stale_after, 60s); } TEST(BigSegmentsBuilderTest, ZeroDurationsAreCoercedToDefaults) { @@ -82,10 +84,11 @@ TEST(BigSegmentsBuilderTest, ZeroDurationsAreCoercedToDefaults) { .StatusPollInterval(0ms) .StaleAfter(0ms) .Build(); + ASSERT_TRUE(cfg); - EXPECT_EQ(cfg.context_cache_time, 5s); - EXPECT_EQ(cfg.status_poll_interval, 5s); - EXPECT_EQ(cfg.stale_after, 2min); + EXPECT_EQ(cfg->context_cache_time, 5s); + EXPECT_EQ(cfg->status_poll_interval, 5s); + EXPECT_EQ(cfg->stale_after, 2min); } TEST(BigSegmentsBuilderTest, NegativeDurationsAreCoercedToDefaults) { @@ -95,10 +98,11 @@ TEST(BigSegmentsBuilderTest, NegativeDurationsAreCoercedToDefaults) { .StatusPollInterval(-1ms) .StaleAfter(-1ms) .Build(); + ASSERT_TRUE(cfg); - EXPECT_EQ(cfg.context_cache_time, 5s); - EXPECT_EQ(cfg.status_poll_interval, 5s); - EXPECT_EQ(cfg.stale_after, 2min); + EXPECT_EQ(cfg->context_cache_time, 5s); + EXPECT_EQ(cfg->status_poll_interval, 5s); + EXPECT_EQ(cfg->stale_after, 2min); } TEST(BigSegmentsBuilderTest, BuildClampsPollIntervalToStaleAfter) { @@ -109,9 +113,10 @@ TEST(BigSegmentsBuilderTest, BuildClampsPollIntervalToStaleAfter) { .StatusPollInterval(10s) .StaleAfter(3s) .Build(); + ASSERT_TRUE(cfg); - EXPECT_EQ(cfg.status_poll_interval, 3s); - EXPECT_EQ(cfg.stale_after, 3s); + EXPECT_EQ(cfg->status_poll_interval, 3s); + EXPECT_EQ(cfg->stale_after, 3s); } TEST(BigSegmentsBuilderTest, BuildPreservesPollIntervalWhenWithinStaleAfter) { @@ -120,9 +125,10 @@ TEST(BigSegmentsBuilderTest, BuildPreservesPollIntervalWhenWithinStaleAfter) { .StatusPollInterval(3s) .StaleAfter(10s) .Build(); + ASSERT_TRUE(cfg); - EXPECT_EQ(cfg.status_poll_interval, 3s); - EXPECT_EQ(cfg.stale_after, 10s); + EXPECT_EQ(cfg->status_poll_interval, 3s); + EXPECT_EQ(cfg->stale_after, 10s); } TEST(BigSegmentsBuilderTest, BuildIsRepeatable) { @@ -132,8 +138,10 @@ TEST(BigSegmentsBuilderTest, BuildIsRepeatable) { auto const cfg1 = builder.Build(); auto const cfg2 = builder.Build(); + ASSERT_TRUE(cfg1); + ASSERT_TRUE(cfg2); - EXPECT_EQ(cfg1.context_cache_size, cfg2.context_cache_size); - EXPECT_EQ(cfg1.context_cache_time, cfg2.context_cache_time); - EXPECT_EQ(cfg1.store.get(), cfg2.store.get()); + EXPECT_EQ(cfg1->context_cache_size, cfg2->context_cache_size); + EXPECT_EQ(cfg1->context_cache_time, cfg2->context_cache_time); + EXPECT_EQ(cfg1->store.get(), cfg2->store.get()); } diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 7e4bb0631..a86e53c61 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -3,14 +3,32 @@ #include #include #include +#include #include "config/builders/data_system/defaults.hpp" #include "data_systems/background_sync/sources/streaming/streaming_data_source.hpp" +#include + using namespace launchdarkly; using namespace launchdarkly::server_side; using namespace launchdarkly::server_side::config; +namespace { +// Minimal store so a non-null pointer can be threaded through the config. +class StubBigSegmentStore : public server_side::integrations::IBigSegmentStore { + public: + GetMembershipResult GetMembership( + std::string const&) const noexcept override { + return server_side::integrations::Membership::FromSegmentRefs({}, {}); + } + GetMetadataResult GetMetadata() const noexcept override { + return std::optional{ + std::nullopt}; + } +}; +} // namespace + class ConfigBuilderTest : public ::testing::Test { // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) protected: @@ -128,6 +146,43 @@ TEST_F(ConfigBuilderTest, DefaultConstruction_EventDefaultsAreUsed) { launchdarkly::config::shared::ServerSDK>::Defaults::Events()); } +TEST_F(ConfigBuilderTest, BigSegmentsAbsentByDefault) { + ConfigBuilder builder("sdk-123"); + auto cfg = builder.Build(); + ASSERT_TRUE(cfg); + EXPECT_FALSE(cfg->BigSegments().has_value()); +} + +TEST_F(ConfigBuilderTest, BigSegmentsRoundTripsBuilderTunables) { + using namespace std::chrono_literals; + auto store = std::make_shared(); + + ConfigBuilder builder("sdk-123"); + builder.BigSegments(builders::BigSegmentsBuilder(store) + .ContextCacheSize(50) + .ContextCacheTime(10s) + .StatusPollInterval(20s) + .StaleAfter(3min)); + + auto cfg = builder.Build(); + ASSERT_TRUE(cfg); + ASSERT_TRUE(cfg->BigSegments().has_value()); + EXPECT_EQ(cfg->BigSegments()->store, store); + EXPECT_EQ(cfg->BigSegments()->context_cache_size, 50u); + EXPECT_EQ(cfg->BigSegments()->context_cache_time, 10s); + EXPECT_EQ(cfg->BigSegments()->status_poll_interval, 20s); + EXPECT_EQ(cfg->BigSegments()->stale_after, 3min); +} + +TEST_F(ConfigBuilderTest, BigSegmentsWithNullStoreFailsBuild) { + ConfigBuilder builder("sdk-123"); + builder.BigSegments(builders::BigSegmentsBuilder(nullptr)); + + auto cfg = builder.Build(); + ASSERT_FALSE(cfg); + EXPECT_EQ(cfg.error(), Error::kConfig_BigSegments_NullStore); +} + TEST_F(ConfigBuilderTest, CanDisableDataSystem) { ConfigBuilder builder("sdk-123"); From 024784eac3915206e81eb8b014e48bb29cfcca7d Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 11 Jun 2026 14:00:30 -0700 Subject: [PATCH 2/5] feat: expose Big Segments via public Client and Config APIs --- .../server_side/big_segment_store_status.hpp | 77 ++++++++ .../launchdarkly/server_side/client.hpp | 29 ++- .../config/builders/all_builders.hpp | 2 + .../server_side/config/config.hpp | 11 ++ .../server_side/config/config_builder.hpp | 12 ++ libs/server-sdk/src/CMakeLists.txt | 3 + .../src/big_segment_store_status.cpp | 34 ++++ libs/server-sdk/src/client.cpp | 9 +- libs/server-sdk/src/client_impl.cpp | 74 +++++--- libs/server-sdk/src/client_impl.hpp | 19 +- libs/server-sdk/src/config/config.cpp | 7 + .../big_segment_store_status_provider.cpp | 50 ++++++ .../big_segment_store_status_provider.hpp | 46 +++++ ...big_segment_store_status_provider_test.cpp | 166 ++++++++++++++++++ 14 files changed, 498 insertions(+), 41 deletions(-) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/big_segment_store_status.hpp create mode 100644 libs/server-sdk/src/big_segment_store_status.cpp create mode 100644 libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.cpp create mode 100644 libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp create mode 100644 libs/server-sdk/tests/big_segment_store_status_provider_test.cpp diff --git a/libs/server-sdk/include/launchdarkly/server_side/big_segment_store_status.hpp b/libs/server-sdk/include/launchdarkly/server_side/big_segment_store_status.hpp new file mode 100644 index 000000000..789fe76a5 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/big_segment_store_status.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include + +#include +#include +#include + +namespace launchdarkly::server_side { + +/** + * The current health of a Big Segments store, independent of any single + * context's membership. + */ +class BigSegmentStoreStatus { + public: + BigSegmentStoreStatus(bool available, bool stale); + + /** + * True if the most recent store query or metadata poll succeeded. If false, + * Big Segments membership cannot currently be evaluated reliably. + */ + [[nodiscard]] bool IsAvailable() const; + + /** + * True if the store's data has not been updated within the configured + * stale-after threshold. The data may still be queried; it is just older + * than desired. + */ + [[nodiscard]] bool IsStale() const; + + private: + bool available_; + bool stale_; +}; + +bool operator==(BigSegmentStoreStatus const& a, BigSegmentStoreStatus const& b); +bool operator!=(BigSegmentStoreStatus const& a, BigSegmentStoreStatus const& b); + +/** + * Interface for accessing and listening to the Big Segments store status. + */ +class IBigSegmentStoreStatusProvider { + public: + /** + * The current status of the Big Segments store. If no store is configured, + * reports unavailable and not stale. + */ + [[nodiscard]] virtual BigSegmentStoreStatus Status() const = 0; + + /** + * Listen to changes to the Big Segments store status. The handler is + * invoked only when the status changes, not on every metadata poll. + * + * @param handler Function which will be called with the new status. + * @return A IConnection which can be used to stop listening to the status. + */ + virtual std::unique_ptr OnBigSegmentStoreStatusChange( + std::function handler) = 0; + + virtual ~IBigSegmentStoreStatusProvider() = default; + IBigSegmentStoreStatusProvider(IBigSegmentStoreStatusProvider const&) = + delete; + IBigSegmentStoreStatusProvider(IBigSegmentStoreStatusProvider&&) = delete; + IBigSegmentStoreStatusProvider& operator=( + IBigSegmentStoreStatusProvider const&) = delete; + IBigSegmentStoreStatusProvider& operator=( + IBigSegmentStoreStatusProvider&&) = delete; + + protected: + IBigSegmentStoreStatusProvider() = default; +}; + +std::ostream& operator<<(std::ostream& out, + BigSegmentStoreStatus const& status); + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index 9107d562e..75c0aa4c9 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -277,10 +278,11 @@ class IClient { * @return The variation for the selected context, or default_value if the * flag is disabled in the LaunchDarkly control panel */ - virtual std::string StringVariation(Context const& ctx, - FlagKey const& key, - std::string default_value, - hooks::HookContext const& hook_context) = 0; + virtual std::string StringVariation( + Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) = 0; /** * Returns the string value of a feature flag for a given flag key, in an @@ -499,6 +501,14 @@ class IClient { */ virtual IDataSourceStatusProvider& DataSourceStatus() = 0; + /** + * Returns an interface for querying the status of a Big Segment store and + * subscribing to status changes. If Big Segments are not configured, the + * provider reports the store as unavailable. + * @return A Big Segment store status provider. + */ + virtual IBigSegmentStoreStatusProvider& BigSegmentStoreStatus() = 0; + virtual ~IClient() = default; IClient(IClient const& item) = delete; IClient(IClient&& item) = delete; @@ -574,10 +584,11 @@ class Client : public IClient { FlagKey const& key, std::string default_value) override; - std::string StringVariation(Context const& ctx, - FlagKey const& key, - std::string default_value, - hooks::HookContext const& hook_context) override; + std::string StringVariation( + Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) override; EvaluationDetail StringVariationDetail( Context const& ctx, @@ -650,6 +661,8 @@ class Client : public IClient { IDataSourceStatusProvider& DataSourceStatus() override; + IBigSegmentStoreStatusProvider& BigSegmentStoreStatus() override; + /** * Returns the version of the SDK. * @return String representing version of the SDK. diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp index f49254431..12fcc820d 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp @@ -12,6 +12,8 @@ #include #include +#include + namespace launchdarkly::server_side::config::builders { using SDK = launchdarkly::config::shared::ServerSDK; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp index ff5156255..63c424624 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -1,10 +1,12 @@ #pragma once #include +#include #include #include #include +#include #include namespace launchdarkly::server_side { @@ -17,6 +19,7 @@ struct Config { config::built::Events events, std::optional application_tag, config::built::DataSystemConfig data_system_config, + std::optional big_segments, config::built::HttpProperties http_properties, std::vector> hooks); @@ -31,6 +34,13 @@ struct Config { config::built::DataSystemConfig const& DataSystemConfig() const; + /** + * The Big Segments configuration, or nullopt if Big Segments were not + * enabled via ConfigBuilder::BigSegments. + */ + [[nodiscard]] std::optional const& + BigSegments() const; + [[nodiscard]] config::built::HttpProperties const& HttpProperties() const; [[nodiscard]] config::built::Logging const& Logging() const; @@ -46,6 +56,7 @@ struct Config { std::optional application_tag_; config::built::Events events_; config::built::DataSystemConfig data_system_config_; + std::optional big_segments_; config::built::HttpProperties http_properties_; std::vector> hooks_; }; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 2f768c1f6..c4a8b7a44 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -5,6 +5,7 @@ #include #include +#include #include namespace launchdarkly::server_side { @@ -50,6 +51,16 @@ class ConfigBuilder { */ config::builders::DataSystemBuilder& DataSystem(); + /** + * Configures the SDK's Big Segments behavior. Pass a BigSegmentsBuilder + * constructed with the Big Segments store to use. If never called, Big + * Segments are not enabled and flags referencing them evaluate as if the + * context were not a member. + * @param builder A configured BigSegmentsBuilder. + * @return Reference to this. + */ + ConfigBuilder& BigSegments(config::builders::BigSegmentsBuilder builder); + /** * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. * The builder has methods for setting individual HTTP-related properties. @@ -99,6 +110,7 @@ class ConfigBuilder { config::builders::AppInfoBuilder app_info_builder_; config::builders::EventsBuilder events_builder_; config::builders::DataSystemBuilder data_system_builder_; + std::optional big_segments_builder_; config::builders::HttpPropertiesBuilder http_properties_builder_; config::builders::LoggingBuilder logging_config_builder_; std::vector> hooks_; diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 94e12f113..f2348bc12 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources(${LIBNAME} client.cpp client_impl.cpp data_source_status.cpp + big_segment_store_status.cpp instance_id.hpp instance_id.cpp config/config.cpp @@ -49,6 +50,8 @@ target_sources(${LIBNAME} data_components/big_segments/membership_cache.cpp data_components/big_segments/big_segment_store_wrapper.hpp data_components/big_segments/big_segment_store_wrapper.cpp + data_components/big_segments/big_segment_store_status_provider.hpp + data_components/big_segments/big_segment_store_status_provider.cpp data_interfaces/destination/itransactional_destination.hpp data_components/memory_store/memory_store.hpp data_components/memory_store/memory_store.cpp diff --git a/libs/server-sdk/src/big_segment_store_status.cpp b/libs/server-sdk/src/big_segment_store_status.cpp new file mode 100644 index 000000000..b79a8f371 --- /dev/null +++ b/libs/server-sdk/src/big_segment_store_status.cpp @@ -0,0 +1,34 @@ +#include + +namespace launchdarkly::server_side { + +BigSegmentStoreStatus::BigSegmentStoreStatus(bool const available, + bool const stale) + : available_(available), stale_(stale) {} + +bool BigSegmentStoreStatus::IsAvailable() const { + return available_; +} + +bool BigSegmentStoreStatus::IsStale() const { + return stale_; +} + +bool operator==(BigSegmentStoreStatus const& a, + BigSegmentStoreStatus const& b) { + return a.IsAvailable() == b.IsAvailable() && a.IsStale() == b.IsStale(); +} + +bool operator!=(BigSegmentStoreStatus const& a, + BigSegmentStoreStatus const& b) { + return !(a == b); +} + +std::ostream& operator<<(std::ostream& out, + BigSegmentStoreStatus const& status) { + out << "BigSegmentStoreStatus(available=" << std::boolalpha + << status.IsAvailable() << ", stale=" << status.IsStale() << ")"; + return out; +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client.cpp b/libs/server-sdk/src/client.cpp index 711b439e1..6920f11d6 100644 --- a/libs/server-sdk/src/client.cpp +++ b/libs/server-sdk/src/client.cpp @@ -123,7 +123,7 @@ std::string Client::StringVariation(Context const& ctx, std::string default_value, hooks::HookContext const& hook_context) { return client->StringVariation(ctx, key, std::move(default_value), - hook_context); + hook_context); } EvaluationDetail Client::StringVariationDetail( @@ -166,8 +166,7 @@ EvaluationDetail Client::DoubleVariationDetail( FlagKey const& key, double default_value, hooks::HookContext const& hook_context) { - return client->DoubleVariationDetail(ctx, key, default_value, - hook_context); + return client->DoubleVariationDetail(ctx, key, default_value, hook_context); } int Client::IntVariation(Context const& ctx, @@ -230,6 +229,10 @@ IDataSourceStatusProvider& Client::DataSourceStatus() { return client->DataSourceStatus(); } +IBigSegmentStoreStatusProvider& Client::BigSegmentStoreStatus() { + return client->BigSegmentStoreStatus(); +} + char const* Client::Version() { return kVersion; } diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 9910dd23c..332237565 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -39,16 +39,16 @@ auto const kDataSourceShutdownWait = std::chrono::milliseconds(100); // Hook method names // Method names for hooks -static const std::string kMethodBoolVariation = "BoolVariation"; -static const std::string kMethodBoolVariationDetail = "BoolVariationDetail"; -static const std::string kMethodStringVariation = "StringVariation"; -static const std::string kMethodStringVariationDetail = "StringVariationDetail"; -static const std::string kMethodDoubleVariation = "DoubleVariation"; -static const std::string kMethodDoubleVariationDetail = "DoubleVariationDetail"; -static const std::string kMethodIntVariation = "IntVariation"; -static const std::string kMethodIntVariationDetail = "IntVariationDetail"; -static const std::string kMethodJsonVariation = "JsonVariation"; -static const std::string kMethodJsonVariationDetail = "JsonVariationDetail"; +static std::string const kMethodBoolVariation = "BoolVariation"; +static std::string const kMethodBoolVariationDetail = "BoolVariationDetail"; +static std::string const kMethodStringVariation = "StringVariation"; +static std::string const kMethodStringVariationDetail = "StringVariationDetail"; +static std::string const kMethodDoubleVariation = "DoubleVariation"; +static std::string const kMethodDoubleVariationDetail = "DoubleVariationDetail"; +static std::string const kMethodIntVariation = "IntVariation"; +static std::string const kMethodIntVariationDetail = "IntVariationDetail"; +static std::string const kMethodJsonVariation = "JsonVariation"; +static std::string const kMethodJsonVariationDetail = "JsonVariationDetail"; static std::unique_ptr MakeDataSystem( config::built::HttpProperties const& http_properties, @@ -141,7 +141,15 @@ ClientImpl::ClientImpl(Config config, std::string const& version) ioc_.get_executor(), http_properties_, logger_)), - evaluator_(logger_, *data_system_), + big_segment_store_( + config_.BigSegments() + ? std::make_shared( + *config_.BigSegments(), + ioc_.get_executor(), + logger_) + : nullptr), + big_segment_status_provider_(big_segment_store_), + evaluator_(logger_, *data_system_, big_segment_store_.get()), events_default_(event_processor_.get(), EventFactory::WithoutReasons()), events_with_reasons_(event_processor_.get(), EventFactory::WithReasons()) { @@ -158,6 +166,10 @@ ClientImpl::ClientImpl(Config config, std::string const& version) LD_LOG(logger_, LogLevel::kInfo) << "TLS peer verification disabled"; } + if (big_segment_store_) { + big_segment_store_->Start(); + } + run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); } @@ -247,9 +259,9 @@ void ClientImpl::TrackInternal(Context const& ctx, std::optional data, std::optional metric_value, hooks::HookContext const& hook_context) { - if (!ctx.Valid()) { - LD_LOG(logger_, LogLevel::kWarn) << "Track method called with an invalid context"; + LD_LOG(logger_, LogLevel::kWarn) + << "Track method called with an invalid context"; return; } // Execute afterTrack hooks before moving the data @@ -259,8 +271,8 @@ void ClientImpl::TrackInternal(Context const& ctx, // In this SDK the data is type-safe, and will be enqueued, so it makes // minimal functional difference. if (!config_.Hooks().empty()) { - hooks::TrackSeriesContext series_context(ctx, event_name, metric_value, - data, hook_context, std::nullopt); + hooks::TrackSeriesContext series_context( + ctx, event_name, metric_value, data, hook_context, std::nullopt); hooks::ExecuteAfterTrack(config_.Hooks(), series_context, logger_); } @@ -367,7 +379,8 @@ EvaluationDetail ClientImpl::VariationInternal( std::optional executor; if (!config_.Hooks().empty()) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); // Executor only created if there are hooks. executor.emplace(config_.Hooks(), logger_); executor->BeforeEvaluation(series_context); @@ -380,7 +393,8 @@ EvaluationDetail ClientImpl::VariationInternal( // Execute afterEvaluation hooks if (executor) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); executor->AfterEvaluation(series_context, detail); } @@ -401,7 +415,8 @@ EvaluationDetail ClientImpl::VariationInternal( // Execute afterEvaluation hooks if (executor) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); executor->AfterEvaluation(series_context, detail); } @@ -416,7 +431,8 @@ EvaluationDetail ClientImpl::VariationInternal( // Execute afterEvaluation hooks if (executor) { hooks::EvaluationSeriesContext series_context( - key, context, default_value, method_name, hook_context, std::nullopt); + key, context, default_value, method_name, hook_context, + std::nullopt); executor->AfterEvaluation(series_context, detail); } @@ -484,7 +500,8 @@ EvaluationDetail ClientImpl::BoolVariationDetail( bool default_value) { static hooks::HookContext empty_hook_context; return VariationDetail(ctx, Value::Type::kBool, key, default_value, - empty_hook_context, kMethodBoolVariationDetail); + empty_hook_context, + kMethodBoolVariationDetail); } EvaluationDetail ClientImpl::BoolVariationDetail( @@ -508,8 +525,8 @@ bool ClientImpl::BoolVariation(Context const& ctx, IClient::FlagKey const& key, bool default_value, hooks::HookContext const& hook_context) { - return Variation(ctx, Value::Type::kBool, key, default_value, - hook_context, kMethodBoolVariation); + return Variation(ctx, Value::Type::kBool, key, default_value, hook_context, + kMethodBoolVariation); } EvaluationDetail ClientImpl::StringVariationDetail( @@ -540,10 +557,11 @@ std::string ClientImpl::StringVariation(Context const& ctx, empty_hook_context, kMethodStringVariation); } -std::string ClientImpl::StringVariation(Context const& ctx, - IClient::FlagKey const& key, - std::string default_value, - hooks::HookContext const& hook_context) { +std::string ClientImpl::StringVariation( + Context const& ctx, + IClient::FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) { return Variation(ctx, Value::Type::kString, key, default_value, hook_context, kMethodStringVariation); } @@ -656,6 +674,10 @@ IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { return status_manager_; } +IBigSegmentStoreStatusProvider& ClientImpl::BigSegmentStoreStatus() { + return big_segment_status_provider_; +} + ClientImpl::~ClientImpl() { ioc_.stop(); // TODO(SC-219101) diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 2c29cf0c6..8466a682f 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -1,5 +1,7 @@ #pragma once +#include "data_components/big_segments/big_segment_store_status_provider.hpp" +#include "data_components/big_segments/big_segment_store_wrapper.hpp" #include "data_components/status_notifications/data_source_status_manager.hpp" #include "data_interfaces/system/idata_system.hpp" #include "evaluation/evaluator.hpp" @@ -98,10 +100,11 @@ class ClientImpl : public IClient { FlagKey const& key, std::string default_value) override; - std::string StringVariation(Context const& ctx, - FlagKey const& key, - std::string default_value, - hooks::HookContext const& hook_context) override; + std::string StringVariation( + Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) override; EvaluationDetail StringVariationDetail( Context const& ctx, @@ -174,6 +177,8 @@ class ClientImpl : public IClient { IDataSourceStatusProvider& DataSourceStatus() override; + IBigSegmentStoreStatusProvider& BigSegmentStoreStatus() override; + ~ClientImpl(); std::future StartAsync() override; @@ -254,6 +259,12 @@ class ClientImpl : public IClient { std::unique_ptr event_processor_; + // Null when Big Segments are not configured. Declared before evaluator_ so + // its pointer can be handed to the evaluator, and before + // big_segment_status_provider_ which shares ownership. + std::shared_ptr big_segment_store_; + data_components::BigSegmentStoreStatusProvider big_segment_status_provider_; + mutable std::mutex init_mutex_; std::condition_variable init_waiter_; diff --git a/libs/server-sdk/src/config/config.cpp b/libs/server-sdk/src/config/config.cpp index 31d326366..ac65ff8df 100644 --- a/libs/server-sdk/src/config/config.cpp +++ b/libs/server-sdk/src/config/config.cpp @@ -10,6 +10,7 @@ Config::Config(std::string sdk_key, built::Events events, std::optional application_tag, config::built::DataSystemConfig data_system_config, + std::optional big_segments, built::HttpProperties http_properties, std::vector> hooks) : sdk_key_(std::move(sdk_key)), @@ -18,6 +19,7 @@ Config::Config(std::string sdk_key, events_(std::move(events)), application_tag_(std::move(application_tag)), data_system_config_(std::move(data_system_config)), + big_segments_(std::move(big_segments)), http_properties_(std::move(http_properties)), hooks_(std::move(hooks)) {} @@ -41,6 +43,11 @@ config::built::DataSystemConfig const& Config::DataSystemConfig() const { return data_system_config_; } +std::optional const& Config::BigSegments() + const { + return big_segments_; +} + built::HttpProperties const& Config::HttpProperties() const { return http_properties_; } diff --git a/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.cpp b/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.cpp new file mode 100644 index 000000000..c3bde771d --- /dev/null +++ b/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.cpp @@ -0,0 +1,50 @@ +#include "big_segment_store_status_provider.hpp" + +#include + +namespace launchdarkly::server_side::data_components { + +namespace { + +// Returned when no store is configured: nothing to disconnect. +class NoopConnection : public IConnection { + public: + void Disconnect() override {} +}; + +// Converts the internal status struct to the public status type. Both are named +// BigSegmentStoreStatus, in this namespace and in server_side respectively. +server_side::BigSegmentStoreStatus ToPublic( + data_components::BigSegmentStoreStatus const& status) { + return server_side::BigSegmentStoreStatus{status.available, status.stale}; +} + +} // namespace + +BigSegmentStoreStatusProvider::BigSegmentStoreStatusProvider( + std::shared_ptr wrapper) + : wrapper_(std::move(wrapper)) {} + +server_side::BigSegmentStoreStatus BigSegmentStoreStatusProvider::Status() + const { + if (!wrapper_) { + return server_side::BigSegmentStoreStatus{/* available= */ false, + /* stale= */ false}; + } + return ToPublic(wrapper_->GetStatus()); +} + +std::unique_ptr +BigSegmentStoreStatusProvider::OnBigSegmentStoreStatusChange( + std::function handler) { + if (!wrapper_) { + return std::make_unique(); + } + return wrapper_->OnStatusChange( + [handler = std::move(handler)]( + data_components::BigSegmentStoreStatus status) { + handler(ToPublic(status)); + }); +} + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp b/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp new file mode 100644 index 000000000..b98510605 --- /dev/null +++ b/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "big_segment_store_wrapper.hpp" + +#include + +#include +#include + +namespace launchdarkly::server_side::data_components { + +/** + * @brief Adapts a BigSegmentStoreWrapper to the public + * IBigSegmentStoreStatusProvider, converting the internal status struct to the + * public type. + * + * Holds a null wrapper when Big Segments are not configured; in that case it + * reports the store as unavailable and not stale, and registered listeners + * never fire. + * + * Thread-safe: delegates to the wrapper, which is itself thread-safe. + * + * Note: the public status type server_side::BigSegmentStoreStatus is qualified + * throughout, since this namespace also has an internal struct of the same + * name. + */ +class BigSegmentStoreStatusProvider : public IBigSegmentStoreStatusProvider { + public: + /** + * @param wrapper The wrapper to delegate to, or nullptr if Big Segments are + * not configured. Must outlive this provider. + */ + explicit BigSegmentStoreStatusProvider( + std::shared_ptr wrapper); + + [[nodiscard]] server_side::BigSegmentStoreStatus Status() const override; + + std::unique_ptr OnBigSegmentStoreStatusChange( + std::function handler) + override; + + private: + std::shared_ptr wrapper_; +}; + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/tests/big_segment_store_status_provider_test.cpp b/libs/server-sdk/tests/big_segment_store_status_provider_test.cpp new file mode 100644 index 000000000..d57017177 --- /dev/null +++ b/libs/server-sdk/tests/big_segment_store_status_provider_test.cpp @@ -0,0 +1,166 @@ +#include + +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +using launchdarkly::server_side::BigSegmentStoreStatus; +using launchdarkly::server_side::data_components::BigSegmentStoreStatusProvider; +using launchdarkly::server_side::data_components::BigSegmentStoreWrapper; +namespace integrations = launchdarkly::server_side::integrations; +namespace built = launchdarkly::server_side::config::built; +using namespace std::chrono_literals; + +namespace { + +// In-memory store whose metadata response the test controls. +class FakeBigSegmentStore : public integrations::IBigSegmentStore { + public: + GetMembershipResult GetMembership( + std::string const&) const noexcept override { + return integrations::Membership::FromSegmentRefs({}, {}); + } + + GetMetadataResult GetMetadata() const noexcept override { + std::lock_guard lock(mutex_); + return metadata_; + } + + void SetMetadata(GetMetadataResult metadata) { + std::lock_guard lock(mutex_); + metadata_ = std::move(metadata); + } + + private: + mutable std::mutex mutex_; + GetMetadataResult metadata_ = + std::optional{std::nullopt}; +}; + +built::BigSegmentsConfig MakeConfig( + std::shared_ptr store, + std::chrono::milliseconds poll_interval) { + built::BigSegmentsConfig config; + config.store = std::move(store); + config.context_cache_size = 1000; + config.context_cache_time = 5s; + config.status_poll_interval = poll_interval; + config.stale_after = 2min; + return config; +} + +} // namespace + +TEST(BigSegmentStoreStatusProviderTest, UnconfiguredReportsUnavailable) { + BigSegmentStoreStatusProvider provider(nullptr); + + auto const status = provider.Status(); + EXPECT_FALSE(status.IsAvailable()); + EXPECT_FALSE(status.IsStale()); + + // A listener can still be registered; it simply never fires. + bool fired = false; + auto connection = provider.OnBigSegmentStoreStatusChange( + [&fired](BigSegmentStoreStatus) { fired = true; }); + ASSERT_NE(connection, nullptr); + connection->Disconnect(); + EXPECT_FALSE(fired); +} + +TEST(BigSegmentStoreStatusProviderTest, DelegatesStatusToWrapper) { + auto store = std::make_shared(); + store->SetMetadata( + integrations::StoreMetadata{std::chrono::system_clock::now()}); + + auto logger = launchdarkly::logging::NullLogger(); + boost::asio::io_context ioc; + auto wrapper = std::make_shared( + MakeConfig(store, /*poll_interval=*/5s), ioc.get_executor(), logger); + + BigSegmentStoreStatusProvider provider(wrapper); + + // The wrapper polls inline on the first GetStatus, so fresh metadata is + // reported as available and not stale. + auto const status = provider.Status(); + EXPECT_TRUE(status.IsAvailable()); + EXPECT_FALSE(status.IsStale()); +} + +TEST(BigSegmentStoreStatusProviderTest, StaleMetadataReportedAsStale) { + auto store = std::make_shared(); + store->SetMetadata( + integrations::StoreMetadata{std::chrono::system_clock::now() - 5min}); + + auto logger = launchdarkly::logging::NullLogger(); + boost::asio::io_context ioc; + auto wrapper = std::make_shared( + MakeConfig(store, /*poll_interval=*/5s), ioc.get_executor(), logger); + + BigSegmentStoreStatusProvider provider(wrapper); + + auto const status = provider.Status(); + EXPECT_TRUE(status.IsAvailable()); + EXPECT_TRUE(status.IsStale()); +} + +TEST(BigSegmentStoreStatusProviderTest, ListenerReceivesConvertedPublicStatus) { + auto store = std::make_shared(); + store->SetMetadata( + integrations::StoreMetadata{std::chrono::system_clock::now()}); + + auto logger = launchdarkly::logging::NullLogger(); + boost::asio::io_context ioc; + auto work_guard = boost::asio::make_work_guard(ioc); + std::thread io_thread([&ioc] { ioc.run(); }); + + auto wrapper = std::make_shared( + MakeConfig(store, /*poll_interval=*/5ms), ioc.get_executor(), logger); + + BigSegmentStoreStatusProvider provider(wrapper); + + std::mutex mutex; + std::condition_variable cv; + std::optional last; + auto connection = provider.OnBigSegmentStoreStatusChange( + [&](BigSegmentStoreStatus status) { + std::lock_guard lock(mutex); + last = status; + cv.notify_all(); + }); + + wrapper->Start(); + + // First poll broadcasts the initial healthy status through the adapter. + { + std::unique_lock lock(mutex); + ASSERT_TRUE(cv.wait_for( + lock, 1s, [&] { return last.has_value() && last->IsAvailable(); })); + EXPECT_FALSE(last->IsStale()); + } + + // A store error flips availability, which the listener observes. + store->SetMetadata(tl::make_unexpected("boom")); + { + std::unique_lock lock(mutex); + ASSERT_TRUE(cv.wait_for(lock, 1s, [&] { + return last.has_value() && !last->IsAvailable(); + })); + } + + connection->Disconnect(); + + work_guard.reset(); + ioc.stop(); + io_thread.join(); +} From e5e52aab6f2f97368315350e45d4c44f7cb5d92d Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 11 Jun 2026 23:39:37 -0700 Subject: [PATCH 3/5] test: drive Big Segments status provider tests through real polling --- ...big_segment_store_status_provider_test.cpp | 62 ++++++------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/libs/server-sdk/tests/big_segment_store_status_provider_test.cpp b/libs/server-sdk/tests/big_segment_store_status_provider_test.cpp index d57017177..137d99b86 100644 --- a/libs/server-sdk/tests/big_segment_store_status_provider_test.cpp +++ b/libs/server-sdk/tests/big_segment_store_status_provider_test.cpp @@ -44,8 +44,7 @@ class FakeBigSegmentStore : public integrations::IBigSegmentStore { private: mutable std::mutex mutex_; - GetMetadataResult metadata_ = - std::optional{std::nullopt}; + GetMetadataResult metadata_; }; built::BigSegmentsConfig MakeConfig( @@ -78,43 +77,7 @@ TEST(BigSegmentStoreStatusProviderTest, UnconfiguredReportsUnavailable) { EXPECT_FALSE(fired); } -TEST(BigSegmentStoreStatusProviderTest, DelegatesStatusToWrapper) { - auto store = std::make_shared(); - store->SetMetadata( - integrations::StoreMetadata{std::chrono::system_clock::now()}); - - auto logger = launchdarkly::logging::NullLogger(); - boost::asio::io_context ioc; - auto wrapper = std::make_shared( - MakeConfig(store, /*poll_interval=*/5s), ioc.get_executor(), logger); - - BigSegmentStoreStatusProvider provider(wrapper); - - // The wrapper polls inline on the first GetStatus, so fresh metadata is - // reported as available and not stale. - auto const status = provider.Status(); - EXPECT_TRUE(status.IsAvailable()); - EXPECT_FALSE(status.IsStale()); -} - -TEST(BigSegmentStoreStatusProviderTest, StaleMetadataReportedAsStale) { - auto store = std::make_shared(); - store->SetMetadata( - integrations::StoreMetadata{std::chrono::system_clock::now() - 5min}); - - auto logger = launchdarkly::logging::NullLogger(); - boost::asio::io_context ioc; - auto wrapper = std::make_shared( - MakeConfig(store, /*poll_interval=*/5s), ioc.get_executor(), logger); - - BigSegmentStoreStatusProvider provider(wrapper); - - auto const status = provider.Status(); - EXPECT_TRUE(status.IsAvailable()); - EXPECT_TRUE(status.IsStale()); -} - -TEST(BigSegmentStoreStatusProviderTest, ListenerReceivesConvertedPublicStatus) { +TEST(BigSegmentStoreStatusProviderTest, StatusAndListenerReflectStoreTransitions) { auto store = std::make_shared(); store->SetMetadata( integrations::StoreMetadata{std::chrono::system_clock::now()}); @@ -141,21 +104,34 @@ TEST(BigSegmentStoreStatusProviderTest, ListenerReceivesConvertedPublicStatus) { wrapper->Start(); - // First poll broadcasts the initial healthy status through the adapter. + // Fresh metadata is reported as available and not stale. + { + std::unique_lock lock(mutex); + ASSERT_TRUE(cv.wait_for(lock, 1s, [&] { + return last.has_value() && last->IsAvailable() && !last->IsStale(); + })); + EXPECT_EQ(provider.Status(), *last); + } + + // Old metadata is reported as available but stale. + store->SetMetadata( + integrations::StoreMetadata{std::chrono::system_clock::now() - 5min}); { std::unique_lock lock(mutex); ASSERT_TRUE(cv.wait_for( - lock, 1s, [&] { return last.has_value() && last->IsAvailable(); })); - EXPECT_FALSE(last->IsStale()); + lock, 1s, [&] { return last.has_value() && last->IsStale(); })); + EXPECT_TRUE(last->IsAvailable()); + EXPECT_EQ(provider.Status(), *last); } - // A store error flips availability, which the listener observes. + // A store error flips availability. store->SetMetadata(tl::make_unexpected("boom")); { std::unique_lock lock(mutex); ASSERT_TRUE(cv.wait_for(lock, 1s, [&] { return last.has_value() && !last->IsAvailable(); })); + EXPECT_EQ(provider.Status(), *last); } connection->Disconnect(); From 43af9e5faaa31b90f211c856b387245cacb94e4c Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 11 Jun 2026 23:39:46 -0700 Subject: [PATCH 4/5] refactor: drop incorrect lifetime claim on BigSegmentStoreStatusProvider --- .../big_segments/big_segment_store_status_provider.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp b/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp index b98510605..b0be7dfaa 100644 --- a/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp +++ b/libs/server-sdk/src/data_components/big_segments/big_segment_store_status_provider.hpp @@ -28,7 +28,7 @@ class BigSegmentStoreStatusProvider : public IBigSegmentStoreStatusProvider { public: /** * @param wrapper The wrapper to delegate to, or nullptr if Big Segments are - * not configured. Must outlive this provider. + * not configured. */ explicit BigSegmentStoreStatusProvider( std::shared_ptr wrapper); From 7153392e500fc12a011051b667460dac0614e2df Mon Sep 17 00:00:00 2001 From: Bee Klimt Date: Thu, 11 Jun 2026 23:40:19 -0700 Subject: [PATCH 5/5] refactor: rename local to avoid shadowing built namespace --- libs/server-sdk/src/config/config_builder.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 87ebb8afd..72089cd09 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -84,11 +84,11 @@ tl::expected ConfigBuilder::Build() const { std::optional big_segments_config; if (big_segments_builder_) { - auto built = big_segments_builder_->Build(); - if (!built) { - return tl::make_unexpected(built.error()); + auto big_segments = big_segments_builder_->Build(); + if (!big_segments) { + return tl::make_unexpected(big_segments.error()); } - big_segments_config = std::move(*built); + big_segments_config = std::move(*big_segments); } return {tl::in_place,