diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/big_segments_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/big_segments_config.hpp new file mode 100644 index 000000000..f2c0f9139 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/big_segments_config.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::config::built { + +struct BigSegmentsConfig { + std::shared_ptr store; + std::size_t context_cache_size; + std::chrono::milliseconds context_cache_time; + std::chrono::milliseconds status_poll_interval; + std::chrono::milliseconds stale_after; +}; + +} // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index e3a338608..29da3c307 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -4,6 +4,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/big_segments/*.hpp" "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/hooks/*.hpp" + "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/config/built/*.hpp" ) if (LD_BUILD_SHARED_LIBS) @@ -30,6 +31,7 @@ target_sources(${LIBNAME} config/builders/data_system/data_system_builder.cpp config/builders/data_system/lazy_load_builder.cpp config/builders/data_system/data_destination_builder.cpp + config/builders/big_segments_builder.cpp all_flags_state/all_flags_state.cpp all_flags_state/json_all_flags_state.cpp all_flags_state/all_flags_state_builder.cpp diff --git a/libs/server-sdk/src/config/builders/big_segments_builder.cpp b/libs/server-sdk/src/config/builders/big_segments_builder.cpp new file mode 100644 index 000000000..5bad1b673 --- /dev/null +++ b/libs/server-sdk/src/config/builders/big_segments_builder.cpp @@ -0,0 +1,64 @@ +#include "big_segments_builder.hpp" + +#include +#include +#include + +namespace launchdarkly::server_side::config::builders { + +namespace { + +using namespace std::chrono_literals; + +constexpr std::size_t kDefaultContextCacheSize = 1000; +constexpr std::chrono::milliseconds kDefaultContextCacheTime = 5s; +constexpr std::chrono::milliseconds kDefaultStatusPollInterval = 5s; +constexpr std::chrono::milliseconds kDefaultStaleAfter = 2min; + +} // namespace + +BigSegmentsBuilder::BigSegmentsBuilder( + std::shared_ptr store) + : store_(std::move(store)), + context_cache_size_(kDefaultContextCacheSize), + context_cache_time_(kDefaultContextCacheTime), + status_poll_interval_(kDefaultStatusPollInterval), + stale_after_(kDefaultStaleAfter) {} + +BigSegmentsBuilder& BigSegmentsBuilder::ContextCacheSize( + std::size_t const size) { + context_cache_size_ = size; + return *this; +} + +BigSegmentsBuilder& BigSegmentsBuilder::ContextCacheTime( + std::chrono::milliseconds const ttl) { + context_cache_time_ = ttl > std::chrono::milliseconds::zero() + ? ttl + : kDefaultContextCacheTime; + return *this; +} + +BigSegmentsBuilder& BigSegmentsBuilder::StatusPollInterval( + std::chrono::milliseconds const interval) { + status_poll_interval_ = interval > std::chrono::milliseconds::zero() + ? interval + : kDefaultStatusPollInterval; + return *this; +} + +BigSegmentsBuilder& BigSegmentsBuilder::StaleAfter( + std::chrono::milliseconds const threshold) { + stale_after_ = threshold > std::chrono::milliseconds::zero() + ? threshold + : kDefaultStaleAfter; + return *this; +} + +built::BigSegmentsConfig BigSegmentsBuilder::Build() const { + auto const poll = std::min(status_poll_interval_, stale_after_); + return built::BigSegmentsConfig{store_, context_cache_size_, + context_cache_time_, poll, stale_after_}; +} + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/config/builders/big_segments_builder.hpp b/libs/server-sdk/src/config/builders/big_segments_builder.hpp new file mode 100644 index 000000000..9713292e3 --- /dev/null +++ b/libs/server-sdk/src/config/builders/big_segments_builder.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace launchdarkly::server_side::config::builders { + +/** + * @brief Configures the SDK's Big Segments behavior. + * + * Not thread-safe. Construct, configure, and call @ref Build on a single + * thread; the resulting @ref built::BigSegmentsConfig is safe to share. + */ +class BigSegmentsBuilder { + public: + /** + * @brief Constructs a builder for the given Big Segments store. + * + * @param store The Big Segments store implementation. Shared ownership; + * the SDK retains a reference for the lifetime of the client. + */ + explicit BigSegmentsBuilder( + std::shared_ptr store); + + /** + * @brief Sets the maximum number of context membership lookups cached + * by the SDK. Defaults to 1000. + * + * To reduce store traffic, the SDK maintains an LRU cache keyed by + * context key. A higher value reduces store queries for + * recently-referenced contexts at the cost of memory. + */ + BigSegmentsBuilder& ContextCacheSize(std::size_t size); + + /** + * @brief Sets the time-to-live for cached membership lookups. Defaults + * to 5 seconds. + * + * A higher value reduces store queries for any given context, but + * delays the SDK noticing membership changes. Zero or negative + * durations are coerced to the default. + */ + BigSegmentsBuilder& ContextCacheTime(std::chrono::milliseconds ttl); + + /** + * @brief Sets the interval at which the SDK polls the store's metadata + * to determine availability and staleness. Defaults to 5 seconds. + * + * Zero or negative durations are coerced to the default. + */ + BigSegmentsBuilder& StatusPollInterval(std::chrono::milliseconds interval); + + /** + * @brief Sets how long the SDK waits before treating store data as + * stale. Defaults to 2 minutes. + * + * If the store's last-updated timestamp falls behind the current time + * by more than this duration, evaluations report a big segments status + * of `STALE` and the status provider reports the store as stale. Zero + * or negative durations are coerced to the default. + */ + BigSegmentsBuilder& StaleAfter(std::chrono::milliseconds threshold); + + /** + * @brief Resolves the configuration. + * + * 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; + + private: + std::shared_ptr store_; + std::size_t context_cache_size_; + std::chrono::milliseconds context_cache_time_; + std::chrono::milliseconds status_poll_interval_; + std::chrono::milliseconds stale_after_; +}; + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/tests/big_segments_builder_test.cpp b/libs/server-sdk/tests/big_segments_builder_test.cpp new file mode 100644 index 000000000..8b1312416 --- /dev/null +++ b/libs/server-sdk/tests/big_segments_builder_test.cpp @@ -0,0 +1,139 @@ +#include + +#include +#include + +#include "config/builders/big_segments_builder.hpp" + +#include +#include + +using launchdarkly::server_side::config::builders::BigSegmentsBuilder; +using launchdarkly::server_side::integrations::IBigSegmentStore; +using launchdarkly::server_side::integrations::Membership; +using launchdarkly::server_side::integrations::StoreMetadata; + +namespace { + +using namespace std::chrono_literals; + +// Minimal stub used only to obtain a shared_ptr. The builder +// never invokes the store; it only stores the pointer for later use by the +// wrapper, so the methods here are unreachable in these tests. +class StubStore final : public IBigSegmentStore { + public: + GetMembershipResult GetMembership( + std::string const& /*context_hash*/) const override { + return Membership::FromSegmentRefs({}, {}); + } + GetMetadataResult GetMetadata() const override { + return std::optional{}; + } +}; + +std::shared_ptr MakeStubStore() { + return std::make_shared(); +} + +} // namespace + +TEST(BigSegmentsBuilderTest, DefaultsMatchSpec) { + auto store = MakeStubStore(); + auto const cfg = BigSegmentsBuilder(store).Build(); + + 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()); +} + +TEST(BigSegmentsBuilderTest, AcceptsNullStore) { + // The builder doesn't validate the store; downstream components treat a + // null store as "Big Segments not configured". + auto const cfg = BigSegmentsBuilder(nullptr).Build(); + EXPECT_EQ(cfg.store, nullptr); +} + +TEST(BigSegmentsBuilderTest, SettersOverrideEachField) { + auto store = MakeStubStore(); + auto const cfg = BigSegmentsBuilder(store) + .ContextCacheSize(7) + .ContextCacheTime(11s) + .StatusPollInterval(13s) + .StaleAfter(60s) + .Build(); + + 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) { + auto store = MakeStubStore(); + auto const cfg = BigSegmentsBuilder(store) + .ContextCacheTime(0ms) + .StatusPollInterval(0ms) + .StaleAfter(0ms) + .Build(); + + EXPECT_EQ(cfg.context_cache_time, 5s); + EXPECT_EQ(cfg.status_poll_interval, 5s); + EXPECT_EQ(cfg.stale_after, 2min); +} + +TEST(BigSegmentsBuilderTest, NegativeDurationsAreCoercedToDefaults) { + auto store = MakeStubStore(); + auto const cfg = BigSegmentsBuilder(store) + .ContextCacheTime(-1ms) + .StatusPollInterval(-1ms) + .StaleAfter(-1ms) + .Build(); + + EXPECT_EQ(cfg.context_cache_time, 5s); + EXPECT_EQ(cfg.status_poll_interval, 5s); + EXPECT_EQ(cfg.stale_after, 2min); +} + +TEST(BigSegmentsBuilderTest, BuildClampsPollIntervalToStaleAfter) { + // When poll interval > stale-after, clamp poll to stale-after so the + // SDK detects staleness within one poll cycle. + auto store = MakeStubStore(); + auto const cfg = BigSegmentsBuilder(store) + .StatusPollInterval(10s) + .StaleAfter(3s) + .Build(); + + EXPECT_EQ(cfg.status_poll_interval, 3s); + EXPECT_EQ(cfg.stale_after, 3s); +} + +TEST(BigSegmentsBuilderTest, BuildPreservesPollIntervalWhenWithinStaleAfter) { + auto store = MakeStubStore(); + auto const cfg = BigSegmentsBuilder(store) + .StatusPollInterval(3s) + .StaleAfter(10s) + .Build(); + + EXPECT_EQ(cfg.status_poll_interval, 3s); + EXPECT_EQ(cfg.stale_after, 10s); +} + +TEST(BigSegmentsBuilderTest, BuildIsRepeatable) { + auto store = MakeStubStore(); + BigSegmentsBuilder builder(store); + builder.ContextCacheSize(42).ContextCacheTime(2s); + + auto const cfg1 = builder.Build(); + auto const cfg2 = builder.Build(); + + 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()); +}