Skip to content

Commit 3c88c2a

Browse files
authored
feat: add internal BigSegmentsBuilder + config struct (#541)
## Summary Internal-only plumbing for Big Segments configuration: a private `BigSegmentsBuilder` (under `src/config/builders/`) that produces a public `built::BigSegmentsConfig`. No `ConfigBuilder` method yet, so this isn't reachable from customer code. ## Test plan - [x] 9/9 new `BigSegmentsBuilderTest` cases pass <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > New internal config types and builder only; no client wiring or evaluation behavior changes yet, with behavior locked by unit tests. > > **Overview** > Adds **internal** Big Segments configuration plumbing: a public `built::BigSegmentsConfig` value type and a private `BigSegmentsBuilder` under `src/config/builders/` that resolves store pointer, LRU cache size/TTL, metadata poll interval, and stale-after threshold. > > `Build()` applies spec defaults (e.g. 1000 cache entries, 5s cache/poll, 2min stale), coerces zero/negative durations back to defaults, and **clamps** `status_poll_interval` to `stale_after` when poll would be slower than staleness detection. A null store is allowed for “not configured.” CMake picks up the new built header glob and `big_segments_builder.cpp`; **nine** unit tests cover defaults, setters, coercion, clamping, and repeatability. There is still **no** `ConfigBuilder` hook—customer-facing wiring is not in this diff. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 915ddb1. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent ce27c1a commit 3c88c2a

5 files changed

Lines changed: 310 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
3+
#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>
4+
5+
#include <chrono>
6+
#include <cstddef>
7+
#include <memory>
8+
9+
namespace launchdarkly::server_side::config::built {
10+
11+
struct BigSegmentsConfig {
12+
std::shared_ptr<integrations::IBigSegmentStore> store;
13+
std::size_t context_cache_size;
14+
std::chrono::milliseconds context_cache_time;
15+
std::chrono::milliseconds status_poll_interval;
16+
std::chrono::milliseconds stale_after;
17+
};
18+
19+
} // namespace launchdarkly::server_side::config::built

libs/server-sdk/src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS
44
"${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp"
55
"${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/big_segments/*.hpp"
66
"${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/hooks/*.hpp"
7+
"${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/config/built/*.hpp"
78
)
89

910
if (LD_BUILD_SHARED_LIBS)
@@ -30,6 +31,7 @@ target_sources(${LIBNAME}
3031
config/builders/data_system/data_system_builder.cpp
3132
config/builders/data_system/lazy_load_builder.cpp
3233
config/builders/data_system/data_destination_builder.cpp
34+
config/builders/big_segments_builder.cpp
3335
all_flags_state/all_flags_state.cpp
3436
all_flags_state/json_all_flags_state.cpp
3537
all_flags_state/all_flags_state_builder.cpp
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#include "big_segments_builder.hpp"
2+
3+
#include <algorithm>
4+
#include <chrono>
5+
#include <utility>
6+
7+
namespace launchdarkly::server_side::config::builders {
8+
9+
namespace {
10+
11+
using namespace std::chrono_literals;
12+
13+
constexpr std::size_t kDefaultContextCacheSize = 1000;
14+
constexpr std::chrono::milliseconds kDefaultContextCacheTime = 5s;
15+
constexpr std::chrono::milliseconds kDefaultStatusPollInterval = 5s;
16+
constexpr std::chrono::milliseconds kDefaultStaleAfter = 2min;
17+
18+
} // namespace
19+
20+
BigSegmentsBuilder::BigSegmentsBuilder(
21+
std::shared_ptr<integrations::IBigSegmentStore> store)
22+
: store_(std::move(store)),
23+
context_cache_size_(kDefaultContextCacheSize),
24+
context_cache_time_(kDefaultContextCacheTime),
25+
status_poll_interval_(kDefaultStatusPollInterval),
26+
stale_after_(kDefaultStaleAfter) {}
27+
28+
BigSegmentsBuilder& BigSegmentsBuilder::ContextCacheSize(
29+
std::size_t const size) {
30+
context_cache_size_ = size;
31+
return *this;
32+
}
33+
34+
BigSegmentsBuilder& BigSegmentsBuilder::ContextCacheTime(
35+
std::chrono::milliseconds const ttl) {
36+
context_cache_time_ = ttl > std::chrono::milliseconds::zero()
37+
? ttl
38+
: kDefaultContextCacheTime;
39+
return *this;
40+
}
41+
42+
BigSegmentsBuilder& BigSegmentsBuilder::StatusPollInterval(
43+
std::chrono::milliseconds const interval) {
44+
status_poll_interval_ = interval > std::chrono::milliseconds::zero()
45+
? interval
46+
: kDefaultStatusPollInterval;
47+
return *this;
48+
}
49+
50+
BigSegmentsBuilder& BigSegmentsBuilder::StaleAfter(
51+
std::chrono::milliseconds const threshold) {
52+
stale_after_ = threshold > std::chrono::milliseconds::zero()
53+
? threshold
54+
: kDefaultStaleAfter;
55+
return *this;
56+
}
57+
58+
built::BigSegmentsConfig BigSegmentsBuilder::Build() const {
59+
auto const poll = std::min(status_poll_interval_, stale_after_);
60+
return built::BigSegmentsConfig{store_, context_cache_size_,
61+
context_cache_time_, poll, stale_after_};
62+
}
63+
64+
} // namespace launchdarkly::server_side::config::builders
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#pragma once
2+
3+
#include <launchdarkly/server_side/config/built/big_segments_config.hpp>
4+
#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>
5+
6+
#include <chrono>
7+
#include <cstddef>
8+
#include <memory>
9+
10+
namespace launchdarkly::server_side::config::builders {
11+
12+
/**
13+
* @brief Configures the SDK's Big Segments behavior.
14+
*
15+
* Not thread-safe. Construct, configure, and call @ref Build on a single
16+
* thread; the resulting @ref built::BigSegmentsConfig is safe to share.
17+
*/
18+
class BigSegmentsBuilder {
19+
public:
20+
/**
21+
* @brief Constructs a builder for the given Big Segments store.
22+
*
23+
* @param store The Big Segments store implementation. Shared ownership;
24+
* the SDK retains a reference for the lifetime of the client.
25+
*/
26+
explicit BigSegmentsBuilder(
27+
std::shared_ptr<integrations::IBigSegmentStore> store);
28+
29+
/**
30+
* @brief Sets the maximum number of context membership lookups cached
31+
* by the SDK. Defaults to 1000.
32+
*
33+
* To reduce store traffic, the SDK maintains an LRU cache keyed by
34+
* context key. A higher value reduces store queries for
35+
* recently-referenced contexts at the cost of memory.
36+
*/
37+
BigSegmentsBuilder& ContextCacheSize(std::size_t size);
38+
39+
/**
40+
* @brief Sets the time-to-live for cached membership lookups. Defaults
41+
* to 5 seconds.
42+
*
43+
* A higher value reduces store queries for any given context, but
44+
* delays the SDK noticing membership changes. Zero or negative
45+
* durations are coerced to the default.
46+
*/
47+
BigSegmentsBuilder& ContextCacheTime(std::chrono::milliseconds ttl);
48+
49+
/**
50+
* @brief Sets the interval at which the SDK polls the store's metadata
51+
* to determine availability and staleness. Defaults to 5 seconds.
52+
*
53+
* Zero or negative durations are coerced to the default.
54+
*/
55+
BigSegmentsBuilder& StatusPollInterval(std::chrono::milliseconds interval);
56+
57+
/**
58+
* @brief Sets how long the SDK waits before treating store data as
59+
* stale. Defaults to 2 minutes.
60+
*
61+
* If the store's last-updated timestamp falls behind the current time
62+
* by more than this duration, evaluations report a big segments status
63+
* of `STALE` and the status provider reports the store as stale. Zero
64+
* or negative durations are coerced to the default.
65+
*/
66+
BigSegmentsBuilder& StaleAfter(std::chrono::milliseconds threshold);
67+
68+
/**
69+
* @brief Resolves the configuration.
70+
*
71+
* If the configured @ref StatusPollInterval exceeds @ref StaleAfter,
72+
* the poll interval in the returned config is clamped to the
73+
* stale-after value so the SDK can detect staleness within one poll
74+
* cycle.
75+
*/
76+
[[nodiscard]] built::BigSegmentsConfig Build() const;
77+
78+
private:
79+
std::shared_ptr<integrations::IBigSegmentStore> store_;
80+
std::size_t context_cache_size_;
81+
std::chrono::milliseconds context_cache_time_;
82+
std::chrono::milliseconds status_poll_interval_;
83+
std::chrono::milliseconds stale_after_;
84+
};
85+
86+
} // namespace launchdarkly::server_side::config::builders
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#include <gtest/gtest.h>
2+
3+
#include <launchdarkly/server_side/integrations/big_segments/big_segment_store_types.hpp>
4+
#include <launchdarkly/server_side/integrations/big_segments/ibig_segment_store.hpp>
5+
6+
#include "config/builders/big_segments_builder.hpp"
7+
8+
#include <chrono>
9+
#include <memory>
10+
11+
using launchdarkly::server_side::config::builders::BigSegmentsBuilder;
12+
using launchdarkly::server_side::integrations::IBigSegmentStore;
13+
using launchdarkly::server_side::integrations::Membership;
14+
using launchdarkly::server_side::integrations::StoreMetadata;
15+
16+
namespace {
17+
18+
using namespace std::chrono_literals;
19+
20+
// Minimal stub used only to obtain a shared_ptr<IBigSegmentStore>. The builder
21+
// never invokes the store; it only stores the pointer for later use by the
22+
// wrapper, so the methods here are unreachable in these tests.
23+
class StubStore final : public IBigSegmentStore {
24+
public:
25+
GetMembershipResult GetMembership(
26+
std::string const& /*context_hash*/) const override {
27+
return Membership::FromSegmentRefs({}, {});
28+
}
29+
GetMetadataResult GetMetadata() const override {
30+
return std::optional<StoreMetadata>{};
31+
}
32+
};
33+
34+
std::shared_ptr<IBigSegmentStore> MakeStubStore() {
35+
return std::make_shared<StubStore>();
36+
}
37+
38+
} // namespace
39+
40+
TEST(BigSegmentsBuilderTest, DefaultsMatchSpec) {
41+
auto store = MakeStubStore();
42+
auto const cfg = BigSegmentsBuilder(store).Build();
43+
44+
EXPECT_EQ(cfg.context_cache_size, 1000u);
45+
EXPECT_EQ(cfg.context_cache_time, 5s);
46+
EXPECT_EQ(cfg.status_poll_interval, 5s);
47+
EXPECT_EQ(cfg.stale_after, 2min);
48+
}
49+
50+
TEST(BigSegmentsBuilderTest, BuildPreservesStoreIdentity) {
51+
auto store = MakeStubStore();
52+
auto const cfg = BigSegmentsBuilder(store).Build();
53+
EXPECT_EQ(cfg.store.get(), store.get());
54+
}
55+
56+
TEST(BigSegmentsBuilderTest, AcceptsNullStore) {
57+
// The builder doesn't validate the store; downstream components treat a
58+
// null store as "Big Segments not configured".
59+
auto const cfg = BigSegmentsBuilder(nullptr).Build();
60+
EXPECT_EQ(cfg.store, nullptr);
61+
}
62+
63+
TEST(BigSegmentsBuilderTest, SettersOverrideEachField) {
64+
auto store = MakeStubStore();
65+
auto const cfg = BigSegmentsBuilder(store)
66+
.ContextCacheSize(7)
67+
.ContextCacheTime(11s)
68+
.StatusPollInterval(13s)
69+
.StaleAfter(60s)
70+
.Build();
71+
72+
EXPECT_EQ(cfg.context_cache_size, 7u);
73+
EXPECT_EQ(cfg.context_cache_time, 11s);
74+
EXPECT_EQ(cfg.status_poll_interval, 13s);
75+
EXPECT_EQ(cfg.stale_after, 60s);
76+
}
77+
78+
TEST(BigSegmentsBuilderTest, ZeroDurationsAreCoercedToDefaults) {
79+
auto store = MakeStubStore();
80+
auto const cfg = BigSegmentsBuilder(store)
81+
.ContextCacheTime(0ms)
82+
.StatusPollInterval(0ms)
83+
.StaleAfter(0ms)
84+
.Build();
85+
86+
EXPECT_EQ(cfg.context_cache_time, 5s);
87+
EXPECT_EQ(cfg.status_poll_interval, 5s);
88+
EXPECT_EQ(cfg.stale_after, 2min);
89+
}
90+
91+
TEST(BigSegmentsBuilderTest, NegativeDurationsAreCoercedToDefaults) {
92+
auto store = MakeStubStore();
93+
auto const cfg = BigSegmentsBuilder(store)
94+
.ContextCacheTime(-1ms)
95+
.StatusPollInterval(-1ms)
96+
.StaleAfter(-1ms)
97+
.Build();
98+
99+
EXPECT_EQ(cfg.context_cache_time, 5s);
100+
EXPECT_EQ(cfg.status_poll_interval, 5s);
101+
EXPECT_EQ(cfg.stale_after, 2min);
102+
}
103+
104+
TEST(BigSegmentsBuilderTest, BuildClampsPollIntervalToStaleAfter) {
105+
// When poll interval > stale-after, clamp poll to stale-after so the
106+
// SDK detects staleness within one poll cycle.
107+
auto store = MakeStubStore();
108+
auto const cfg = BigSegmentsBuilder(store)
109+
.StatusPollInterval(10s)
110+
.StaleAfter(3s)
111+
.Build();
112+
113+
EXPECT_EQ(cfg.status_poll_interval, 3s);
114+
EXPECT_EQ(cfg.stale_after, 3s);
115+
}
116+
117+
TEST(BigSegmentsBuilderTest, BuildPreservesPollIntervalWhenWithinStaleAfter) {
118+
auto store = MakeStubStore();
119+
auto const cfg = BigSegmentsBuilder(store)
120+
.StatusPollInterval(3s)
121+
.StaleAfter(10s)
122+
.Build();
123+
124+
EXPECT_EQ(cfg.status_poll_interval, 3s);
125+
EXPECT_EQ(cfg.stale_after, 10s);
126+
}
127+
128+
TEST(BigSegmentsBuilderTest, BuildIsRepeatable) {
129+
auto store = MakeStubStore();
130+
BigSegmentsBuilder builder(store);
131+
builder.ContextCacheSize(42).ContextCacheTime(2s);
132+
133+
auto const cfg1 = builder.Build();
134+
auto const cfg2 = builder.Build();
135+
136+
EXPECT_EQ(cfg1.context_cache_size, cfg2.context_cache_size);
137+
EXPECT_EQ(cfg1.context_cache_time, cfg2.context_cache_time);
138+
EXPECT_EQ(cfg1.store.get(), cfg2.store.get());
139+
}

0 commit comments

Comments
 (0)