Skip to content

Commit ef97966

Browse files
committed
feat: evaluate big segments
1 parent 84a60e4 commit ef97966

6 files changed

Lines changed: 575 additions & 10 deletions

File tree

libs/server-sdk/src/evaluation/evaluation_stack.cpp

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22

33
namespace launchdarkly::server_side::evaluation {
44

5+
namespace {
6+
// Ranks statuses by how little they can be trusted, so the least-trustworthy
7+
// status wins when several Big Segments are queried in one evaluation
8+
// (NOT_CONFIGURED > STORE_ERROR > STALE > HEALTHY). Note this ordering is
9+
// independent of the enum's underlying values.
10+
int Precedence(enum EvaluationReason::BigSegmentsStatus status) {
11+
switch (status) {
12+
case EvaluationReason::BigSegmentsStatus::kHealthy:
13+
return 0;
14+
case EvaluationReason::BigSegmentsStatus::kStale:
15+
return 1;
16+
case EvaluationReason::BigSegmentsStatus::kStoreError:
17+
return 2;
18+
case EvaluationReason::BigSegmentsStatus::kNotConfigured:
19+
return 3;
20+
}
21+
return 0;
22+
}
23+
} // namespace
24+
25+
EvaluationStack::EvaluationStack(
26+
data_components::BigSegmentStoreWrapper* big_segment_store)
27+
: big_segment_store_(big_segment_store) {}
28+
529
Guard::Guard(std::unordered_set<std::string>& set, std::string key)
630
: set_(set), key_(std::move(key)) {
731
set_.insert(key_);
@@ -27,4 +51,43 @@ std::optional<Guard> EvaluationStack::NoticeSegment(std::string segment_key) {
2751
return std::make_optional<Guard>(segments_seen_, std::move(segment_key));
2852
}
2953

54+
data_components::BigSegmentStoreWrapper* EvaluationStack::BigSegmentStore()
55+
const {
56+
return big_segment_store_;
57+
}
58+
59+
void EvaluationStack::RecordBigSegmentsStatus(enum EvaluationReason::BigSegmentsStatus status) {
60+
if (!big_segments_status_ ||
61+
Precedence(status) > Precedence(*big_segments_status_)) {
62+
big_segments_status_ = status;
63+
}
64+
}
65+
66+
std::optional<enum EvaluationReason::BigSegmentsStatus>
67+
EvaluationStack::BigSegmentsStatus() const {
68+
return big_segments_status_;
69+
}
70+
71+
integrations::Membership const* EvaluationStack::FindMembership(
72+
std::string const& context_key) const {
73+
auto const it = memberships_.find(context_key);
74+
if (it == memberships_.end()) {
75+
return nullptr;
76+
}
77+
return &it->second;
78+
}
79+
80+
void EvaluationStack::StoreMembership(std::string context_key,
81+
integrations::Membership membership) {
82+
memberships_.emplace(std::move(context_key), std::move(membership));
83+
}
84+
85+
void EvaluationStack::RecordStoreError(std::string context_key) {
86+
store_error_keys_.insert(std::move(context_key));
87+
}
88+
89+
bool EvaluationStack::DidStoreError(std::string const& context_key) const {
90+
return store_error_keys_.find(context_key) != store_error_keys_.end();
91+
}
92+
3093
} // namespace launchdarkly::server_side::evaluation

libs/server-sdk/src/evaluation/evaluation_stack.hpp

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
#pragma once
22

3+
#include <launchdarkly/data/evaluation_reason.hpp>
4+
#include <launchdarkly/server_side/integrations/big_segments/big_segment_store_types.hpp>
5+
36
#include <optional>
47
#include <string>
8+
#include <unordered_map>
59
#include <unordered_set>
610

11+
namespace launchdarkly::server_side::data_components {
12+
class BigSegmentStoreWrapper;
13+
} // namespace launchdarkly::server_side::data_components
14+
715
namespace launchdarkly::server_side::evaluation {
816

917
/**
@@ -26,12 +34,22 @@ struct Guard {
2634
};
2735

2836
/**
29-
* EvaluationStack is used to track which segments and flags have been noticed
30-
* during evaluation in order to detect circular references.
37+
* EvaluationStack holds the per-evaluation state for a single top-level flag
38+
* evaluation: the prerequisite/segment chains used for circular-reference
39+
* detection, plus the Big Segments status and membership cache that a Big
40+
* Segment lookup populates.
41+
*
42+
* Not thread-safe: a fresh instance is created per top-level evaluation and is
43+
* never shared across threads.
3144
*/
3245
class EvaluationStack {
3346
public:
34-
EvaluationStack() = default;
47+
/**
48+
* @param big_segment_store Non-owning pointer to the Big Segment store
49+
* wrapper, or nullptr if no store is configured. Must outlive the stack.
50+
*/
51+
explicit EvaluationStack(
52+
data_components::BigSegmentStoreWrapper* big_segment_store = nullptr);
3553

3654
/**
3755
* If the given prerequisite key has not been seen, marks it as seen
@@ -52,9 +70,61 @@ class EvaluationStack {
5270
*/
5371
[[nodiscard]] std::optional<Guard> NoticeSegment(std::string segment_key);
5472

73+
/**
74+
* @return The Big Segment store wrapper, or nullptr if none is configured.
75+
*/
76+
[[nodiscard]] data_components::BigSegmentStoreWrapper* BigSegmentStore()
77+
const;
78+
79+
/**
80+
* Records the status of a Big Segment lookup. If multiple lookups occur in
81+
* one evaluation, the least-trustworthy status wins (NOT_CONFIGURED >
82+
* STORE_ERROR > STALE > HEALTHY).
83+
*/
84+
void RecordBigSegmentsStatus(enum EvaluationReason::BigSegmentsStatus status);
85+
86+
/**
87+
* @return The aggregated Big Segments status, or std::nullopt if no Big
88+
* Segment was queried during this evaluation.
89+
*/
90+
[[nodiscard]] std::optional<enum EvaluationReason::BigSegmentsStatus> BigSegmentsStatus() const;
91+
92+
/**
93+
* Returns the cached membership for a context key looked up earlier in this
94+
* evaluation, or nullptr if that key has not been queried yet.
95+
*/
96+
[[nodiscard]] integrations::Membership const* FindMembership(
97+
std::string const& context_key) const;
98+
99+
/**
100+
* Caches a context key's membership so later Big Segment lookups for the
101+
* same key in this evaluation reuse it instead of re-querying the store.
102+
*/
103+
void StoreMembership(std::string context_key,
104+
integrations::Membership membership);
105+
106+
/**
107+
* Records that the Big Segment store returned an error for the given
108+
* context key during this evaluation. Subsequent Big Segment lookups for
109+
* the same key must be treated as non-matches without re-querying.
110+
*/
111+
void RecordStoreError(std::string context_key);
112+
113+
/**
114+
* @return True if a Big Segment store lookup for the given context key has
115+
* already errored during this evaluation.
116+
*/
117+
[[nodiscard]] bool DidStoreError(std::string const& context_key) const;
118+
55119
private:
56120
std::unordered_set<std::string> prerequisites_seen_;
57121
std::unordered_set<std::string> segments_seen_;
122+
123+
data_components::BigSegmentStoreWrapper* big_segment_store_;
124+
std::optional<enum EvaluationReason::BigSegmentsStatus> big_segments_status_;
125+
// Keyed by unhashed context key. Empty until the first Big Segment lookup.
126+
std::unordered_map<std::string, integrations::Membership> memberships_;
127+
std::unordered_set<std::string> store_error_keys_;
58128
};
59129

60130
} // namespace launchdarkly::server_side::evaluation

libs/server-sdk/src/evaluation/evaluator.cpp

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,26 @@ std::optional<std::size_t> TargetMatchVariation(
2020
launchdarkly::Context const& context,
2121
Flag::Target const& target);
2222

23-
Evaluator::Evaluator(Logger& logger, data_interfaces::IStore const& source)
24-
: logger_(logger), source_(source) {}
23+
namespace {
24+
EvaluationDetail<Value> WithBigSegmentsStatus(EvaluationDetail<Value> detail,
25+
enum EvaluationReason::BigSegmentsStatus status) {
26+
auto const& maybe_reason = detail.Reason();
27+
if (!maybe_reason) {
28+
return detail;
29+
}
30+
EvaluationReason const& reason = *maybe_reason;
31+
EvaluationReason updated(
32+
reason.Kind(), reason.ErrorKind(), reason.RuleIndex(), reason.RuleId(),
33+
reason.PrerequisiteKey(), reason.InExperiment(), status);
34+
return EvaluationDetail<Value>(detail.Value(), detail.VariationIndex(),
35+
std::move(updated));
36+
}
37+
} // namespace
38+
39+
Evaluator::Evaluator(Logger& logger,
40+
data_interfaces::IStore const& source,
41+
data_components::BigSegmentStoreWrapper* big_segment_store)
42+
: logger_(logger), source_(source), big_segment_store_(big_segment_store) {}
2543

2644
EvaluationDetail<Value> Evaluator::Evaluate(
2745
data_model::Flag const& flag,
@@ -33,8 +51,12 @@ EvaluationDetail<Value> Evaluator::Evaluate(
3351
Flag const& flag,
3452
launchdarkly::Context const& context,
3553
EventScope const& event_scope) {
36-
EvaluationStack stack;
37-
return Evaluate(std::nullopt, flag, context, stack, event_scope);
54+
EvaluationStack stack{big_segment_store_};
55+
auto detail = Evaluate(std::nullopt, flag, context, stack, event_scope);
56+
if (auto status = stack.BigSegmentsStatus()) {
57+
return WithBigSegmentsStatus(std::move(detail), *status);
58+
}
59+
return detail;
3860
}
3961

4062
EvaluationDetail<Value> Evaluator::Evaluate(

libs/server-sdk/src/evaluation/evaluator.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ class Evaluator {
2121
* threads in parallel, the given logger and IStore must be thread safe.
2222
* @param logger A logger for recording errors or warnings.
2323
* @param source The flag/segment source.
24+
* @param big_segment_store Non-owning pointer to the Big Segment store
25+
* wrapper, or nullptr if Big Segments are not configured. If non-null it
26+
* must outlive the Evaluator and be safe to call from multiple threads.
2427
*/
25-
Evaluator(Logger& logger, data_interfaces::IStore const& source);
28+
Evaluator(
29+
Logger& logger,
30+
data_interfaces::IStore const& source,
31+
data_components::BigSegmentStoreWrapper* big_segment_store = nullptr);
2632

2733
/**
2834
* Evaluates a flag for a given context.
@@ -69,5 +75,6 @@ class Evaluator {
6975

7076
Logger& logger_;
7177
data_interfaces::IStore const& source_;
78+
data_components::BigSegmentStoreWrapper* big_segment_store_;
7279
};
7380
} // namespace launchdarkly::server_side::evaluation

libs/server-sdk/src/evaluation/rules.cpp

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,88 @@
22
#include "bucketing.hpp"
33
#include "operators.hpp"
44

5+
#include "../data_components/big_segments/big_segment_store_wrapper.hpp"
6+
7+
#include <optional>
8+
#include <string>
9+
#include <utility>
10+
511
namespace launchdarkly::server_side::evaluation {
612

713
using namespace data_model;
814

15+
namespace {
16+
17+
// Maps the wrapper's internal status to the public reason status.
18+
enum EvaluationReason::BigSegmentsStatus ToBigSegmentsStatus(
19+
data_components::BigSegmentsStatus status) {
20+
switch (status) {
21+
case data_components::BigSegmentsStatus::kHealthy:
22+
return EvaluationReason::BigSegmentsStatus::kHealthy;
23+
case data_components::BigSegmentsStatus::kStale:
24+
return EvaluationReason::BigSegmentsStatus::kStale;
25+
case data_components::BigSegmentsStatus::kStoreError:
26+
return EvaluationReason::BigSegmentsStatus::kStoreError;
27+
case data_components::BigSegmentsStatus::kNotConfigured:
28+
return EvaluationReason::BigSegmentsStatus::kNotConfigured;
29+
}
30+
return EvaluationReason::BigSegmentsStatus::kHealthy;
31+
}
32+
33+
std::string MakeBigSegmentRef(Segment const& segment) {
34+
return segment.key + ".g" + std::to_string(*segment.generation);
35+
}
36+
37+
// Evaluates membership in an unbounded (Big) segment. Returns true/false for a
38+
// definite match/non-match, or std::nullopt when the membership has no entry
39+
// for this segment and evaluation should fall through to the segment's rules.
40+
std::optional<bool> MatchBigSegment(Segment const& segment,
41+
Context const& context,
42+
EvaluationStack& stack) {
43+
if (!segment.generation) {
44+
// Without a generation the segment ref can't be formed.
45+
stack.RecordBigSegmentsStatus(EvaluationReason::BigSegmentsStatus::kNotConfigured);
46+
return false;
47+
}
48+
49+
// An absent or empty unboundedContextKind defaults to "user".
50+
ContextKind const kind =
51+
(segment.unboundedContextKind && !segment.unboundedContextKind->t.empty())
52+
? *segment.unboundedContextKind
53+
: ContextKind{"user"};
54+
Value const& context_key = context.Get(kind, "key");
55+
if (!context_key.IsString()) {
56+
return false;
57+
}
58+
std::string const& key = context_key.AsString();
59+
60+
if (stack.DidStoreError(key)) {
61+
return false;
62+
}
63+
64+
integrations::Membership const* membership = stack.FindMembership(key);
65+
if (!membership) {
66+
auto* store = stack.BigSegmentStore();
67+
if (!store) {
68+
stack.RecordBigSegmentsStatus(EvaluationReason::BigSegmentsStatus::kNotConfigured);
69+
return false;
70+
}
71+
auto result = store->GetMembership(key);
72+
auto const status = ToBigSegmentsStatus(result.status);
73+
stack.RecordBigSegmentsStatus(status);
74+
if (status == EvaluationReason::BigSegmentsStatus::kStoreError) {
75+
stack.RecordStoreError(key);
76+
return false;
77+
}
78+
stack.StoreMembership(key, std::move(result.membership));
79+
membership = stack.FindMembership(key);
80+
}
81+
82+
return membership->CheckMembership(MakeBigSegmentRef(segment));
83+
}
84+
85+
} // namespace
86+
987
bool MaybeNegate(Clause const& clause, bool value) {
1088
if (clause.negate) {
1189
return !value;
@@ -161,8 +239,11 @@ tl::expected<bool, Error> Contains(Segment const& segment,
161239
}
162240

163241
if (segment.unbounded) {
164-
// TODO(sc209881): set big segment status to NOT_CONFIGURED.
165-
return false;
242+
if (auto match = MatchBigSegment(segment, context, stack)) {
243+
return *match;
244+
}
245+
// The membership had no entry for this segment; fall through to its
246+
// include/exclude lists and rules.
166247
}
167248

168249
if (IsTargeted(context, segment.included, segment.includedContexts)) {

0 commit comments

Comments
 (0)