Skip to content

Commit 796d25c

Browse files
committed
merge: pick up #550 from main
2 parents 2f68933 + 505ee46 commit 796d25c

6 files changed

Lines changed: 616 additions & 17 deletions

File tree

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
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::kNone:
13+
return 0;
14+
case EvaluationReason::BigSegmentsStatus::kHealthy:
15+
return 1;
16+
case EvaluationReason::BigSegmentsStatus::kStale:
17+
return 2;
18+
case EvaluationReason::BigSegmentsStatus::kStoreError:
19+
return 3;
20+
case EvaluationReason::BigSegmentsStatus::kNotConfigured:
21+
return 4;
22+
}
23+
return 0;
24+
}
25+
} // namespace
26+
27+
EvaluationStack::EvaluationStack(
28+
data_components::BigSegmentStoreWrapper* big_segment_store)
29+
: big_segment_store_(big_segment_store) {}
30+
531
Guard::Guard(std::unordered_set<std::string>& set, std::string key)
632
: set_(set), key_(std::move(key)) {
733
set_.insert(key_);
@@ -27,4 +53,43 @@ std::optional<Guard> EvaluationStack::NoticeSegment(std::string segment_key) {
2753
return std::make_optional<Guard>(segments_seen_, std::move(segment_key));
2854
}
2955

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

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

Lines changed: 75 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,63 @@ 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 kNone if no Big Segment
88+
* was queried during this evaluation.
89+
*/
90+
[[nodiscard]] enum EvaluationReason::BigSegmentsStatus BigSegmentsStatus()
91+
const;
92+
93+
/**
94+
* Returns the cached membership for a context key looked up earlier in this
95+
* evaluation, or nullptr if that key has not been queried yet.
96+
*/
97+
[[nodiscard]] integrations::Membership const* FindMembership(
98+
std::string const& context_key) const;
99+
100+
/**
101+
* Caches a context key's membership so later Big Segment lookups for the
102+
* same key in this evaluation reuse it instead of re-querying the store.
103+
*/
104+
void StoreMembership(std::string context_key,
105+
integrations::Membership membership);
106+
107+
/**
108+
* Records that the Big Segment store returned an error for the given
109+
* context key during this evaluation. Subsequent Big Segment lookups for
110+
* the same key must be treated as non-matches without re-querying.
111+
*/
112+
void RecordStoreError(std::string context_key);
113+
114+
/**
115+
* @return True if a Big Segment store lookup for the given context key has
116+
* already errored during this evaluation.
117+
*/
118+
[[nodiscard]] bool DidStoreError(std::string const& context_key) const;
119+
55120
private:
56121
std::unordered_set<std::string> prerequisites_seen_;
57122
std::unordered_set<std::string> segments_seen_;
123+
124+
data_components::BigSegmentStoreWrapper* big_segment_store_;
125+
enum EvaluationReason::BigSegmentsStatus big_segments_status_ =
126+
EvaluationReason::BigSegmentsStatus::kNone;
127+
// Keyed by unhashed context key. Empty until the first Big Segment lookup.
128+
std::unordered_map<std::string, integrations::Membership> memberships_;
129+
std::unordered_set<std::string> store_error_keys_;
58130
};
59131

60132
} // namespace launchdarkly::server_side::evaluation

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

Lines changed: 27 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,13 @@ 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+
auto status = stack.BigSegmentsStatus();
57+
if (status != EvaluationReason::BigSegmentsStatus::kNone) {
58+
return WithBigSegmentsStatus(std::move(detail), status);
59+
}
60+
return detail;
3861
}
3962

4063
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: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,90 @@
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(
46+
EvaluationReason::BigSegmentsStatus::kNotConfigured);
47+
return false;
48+
}
49+
50+
// An absent or empty unboundedContextKind defaults to "user".
51+
ContextKind const kind = (segment.unboundedContextKind &&
52+
!segment.unboundedContextKind->t.empty())
53+
? *segment.unboundedContextKind
54+
: ContextKind{"user"};
55+
Value const& context_key = context.Get(kind, "key");
56+
if (!context_key.IsString()) {
57+
return false;
58+
}
59+
std::string const& key = context_key.AsString();
60+
61+
if (stack.DidStoreError(key)) {
62+
return false;
63+
}
64+
65+
integrations::Membership const* membership = stack.FindMembership(key);
66+
if (!membership) {
67+
auto* store = stack.BigSegmentStore();
68+
if (!store) {
69+
stack.RecordBigSegmentsStatus(
70+
EvaluationReason::BigSegmentsStatus::kNotConfigured);
71+
return false;
72+
}
73+
auto result = store->GetMembership(key);
74+
auto const status = ToBigSegmentsStatus(result.status);
75+
stack.RecordBigSegmentsStatus(status);
76+
if (status == EvaluationReason::BigSegmentsStatus::kStoreError) {
77+
stack.RecordStoreError(key);
78+
return false;
79+
}
80+
stack.StoreMembership(key, std::move(result.membership));
81+
membership = stack.FindMembership(key);
82+
}
83+
84+
return membership->CheckMembership(MakeBigSegmentRef(segment));
85+
}
86+
87+
} // namespace
88+
989
bool MaybeNegate(Clause const& clause, bool value) {
1090
if (clause.negate) {
1191
return !value;
@@ -161,16 +241,19 @@ tl::expected<bool, Error> Contains(Segment const& segment,
161241
}
162242

163243
if (segment.unbounded) {
164-
// TODO(sc209881): set big segment status to NOT_CONFIGURED.
165-
return false;
166-
}
167-
168-
if (IsTargeted(context, segment.included, segment.includedContexts)) {
169-
return true;
170-
}
244+
if (auto match = MatchBigSegment(segment, context, stack)) {
245+
return *match;
246+
}
247+
// Big segments don't use the regular include/exclude target lists; a
248+
// membership miss falls through directly to the segment's rules.
249+
} else {
250+
if (IsTargeted(context, segment.included, segment.includedContexts)) {
251+
return true;
252+
}
171253

172-
if (IsTargeted(context, segment.excluded, segment.excludedContexts)) {
173-
return false;
254+
if (IsTargeted(context, segment.excluded, segment.excludedContexts)) {
255+
return false;
256+
}
174257
}
175258

176259
for (auto const& rule : segment.rules) {

0 commit comments

Comments
 (0)