Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>

#include <chrono>
#include <cstddef>
#include <memory>

namespace launchdarkly::server_side::config::built {

struct BigSegmentsConfig {
std::shared_ptr<integrations::IBigSegmentStore> 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
2 changes: 2 additions & 0 deletions libs/server-sdk/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
64 changes: 64 additions & 0 deletions libs/server-sdk/src/config/builders/big_segments_builder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "big_segments_builder.hpp"

#include <algorithm>
#include <chrono>
#include <utility>

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<integrations::IBigSegmentStore> 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
86 changes: 86 additions & 0 deletions libs/server-sdk/src/config/builders/big_segments_builder.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#pragma once

#include <launchdarkly/server_side/config/built/big_segments_config.hpp>
#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>

#include <chrono>
#include <cstddef>
#include <memory>

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<integrations::IBigSegmentStore> 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<integrations::IBigSegmentStore> 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
139 changes: 139 additions & 0 deletions libs/server-sdk/tests/big_segments_builder_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#include <gtest/gtest.h>

#include <launchdarkly/server_side/integrations/big_segments/big_segment_store_types.hpp>
#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>

#include "config/builders/big_segments_builder.hpp"

#include <chrono>
#include <memory>

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<IBigSegmentStore>. 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<StoreMetadata>{};
}
};

std::shared_ptr<IBigSegmentStore> MakeStubStore() {
return std::make_shared<StubStore>();
}

} // 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());
}
Loading