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
65 changes: 65 additions & 0 deletions libs/server-sdk/src/evaluation/evaluation_stack.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
#include "evaluation_stack.hpp"

namespace launchdarkly::server_side::evaluation {

namespace {
// Ranks statuses by how little they can be trusted, so the least-trustworthy
// status wins when several Big Segments are queried in one evaluation
// (NOT_CONFIGURED > STORE_ERROR > STALE > HEALTHY). Note this ordering is
// independent of the enum's underlying values.
int Precedence(enum EvaluationReason::BigSegmentsStatus status) {
switch (status) {
case EvaluationReason::BigSegmentsStatus::kNone:
return 0;
case EvaluationReason::BigSegmentsStatus::kHealthy:
return 1;
case EvaluationReason::BigSegmentsStatus::kStale:
return 2;
case EvaluationReason::BigSegmentsStatus::kStoreError:
return 3;
case EvaluationReason::BigSegmentsStatus::kNotConfigured:
return 4;
}
return 0;
}
} // namespace

EvaluationStack::EvaluationStack(
data_components::BigSegmentStoreWrapper* big_segment_store)
: big_segment_store_(big_segment_store) {}

Guard::Guard(std::unordered_set<std::string>& set, std::string key)
: set_(set), key_(std::move(key)) {
set_.insert(key_);
Expand All @@ -27,4 +53,43 @@
return std::make_optional<Guard>(segments_seen_, std::move(segment_key));
}

data_components::BigSegmentStoreWrapper* EvaluationStack::BigSegmentStore()
const {
return big_segment_store_;
}

void EvaluationStack::RecordBigSegmentsStatus(
enum EvaluationReason::BigSegmentsStatus status) {
if (Precedence(status) > Precedence(big_segments_status_)) {
big_segments_status_ = status;
}
}

enum EvaluationReason::BigSegmentsStatus
EvaluationStack::BigSegmentsStatus() const {
return big_segments_status_;
}

integrations::Membership const* EvaluationStack::FindMembership(
std::string const& context_key) const {
auto const it = memberships_.find(context_key);
if (it == memberships_.end()) {
return nullptr;
}
return &it->second;
}

void EvaluationStack::StoreMembership(std::string context_key,
integrations::Membership membership) {
memberships_.emplace(std::move(context_key), std::move(membership));
}

void EvaluationStack::RecordStoreError(std::string context_key) {
store_error_keys_.insert(std::move(context_key));
}

bool EvaluationStack::DidStoreError(std::string const& context_key) const {
return store_error_keys_.find(context_key) != store_error_keys_.end();
}

} // namespace launchdarkly::server_side::evaluation
78 changes: 75 additions & 3 deletions libs/server-sdk/src/evaluation/evaluation_stack.hpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
#pragma once

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

#include <optional>
#include <string>
#include <unordered_map>
#include <unordered_set>

namespace launchdarkly::server_side::data_components {
class BigSegmentStoreWrapper;
} // namespace launchdarkly::server_side::data_components

namespace launchdarkly::server_side::evaluation {

/**
Expand All @@ -26,12 +34,22 @@
};

/**
* EvaluationStack is used to track which segments and flags have been noticed
* during evaluation in order to detect circular references.
* EvaluationStack holds the per-evaluation state for a single top-level flag
* evaluation: the prerequisite/segment chains used for circular-reference
* detection, plus the Big Segments status and membership cache that a Big
* Segment lookup populates.
*
* Not thread-safe: a fresh instance is created per top-level evaluation and is
* never shared across threads.
*/
class EvaluationStack {
public:
EvaluationStack() = default;
/**
* @param big_segment_store Non-owning pointer to the Big Segment store
* wrapper, or nullptr if no store is configured. Must outlive the stack.
*/
explicit EvaluationStack(
data_components::BigSegmentStoreWrapper* big_segment_store = nullptr);

/**
* If the given prerequisite key has not been seen, marks it as seen
Expand All @@ -52,9 +70,63 @@
*/
[[nodiscard]] std::optional<Guard> NoticeSegment(std::string segment_key);

/**
* @return The Big Segment store wrapper, or nullptr if none is configured.
*/
[[nodiscard]] data_components::BigSegmentStoreWrapper* BigSegmentStore()
const;

/**
* Records the status of a Big Segment lookup. If multiple lookups occur in
* one evaluation, the least-trustworthy status wins (NOT_CONFIGURED >
* STORE_ERROR > STALE > HEALTHY).
*/
void RecordBigSegmentsStatus(enum EvaluationReason::BigSegmentsStatus status);

/**
* @return The aggregated Big Segments status, or kNone if no Big Segment
* was queried during this evaluation.
*/
[[nodiscard]] enum EvaluationReason::BigSegmentsStatus BigSegmentsStatus()
const;

/**
* Returns the cached membership for a context key looked up earlier in this
* evaluation, or nullptr if that key has not been queried yet.
*/
[[nodiscard]] integrations::Membership const* FindMembership(
std::string const& context_key) const;

/**
* Caches a context key's membership so later Big Segment lookups for the
* same key in this evaluation reuse it instead of re-querying the store.
*/
void StoreMembership(std::string context_key,
integrations::Membership membership);

/**
* Records that the Big Segment store returned an error for the given
* context key during this evaluation. Subsequent Big Segment lookups for
* the same key must be treated as non-matches without re-querying.
*/
void RecordStoreError(std::string context_key);

/**
* @return True if a Big Segment store lookup for the given context key has
* already errored during this evaluation.
*/
[[nodiscard]] bool DidStoreError(std::string const& context_key) const;

private:
std::unordered_set<std::string> prerequisites_seen_;
std::unordered_set<std::string> segments_seen_;

data_components::BigSegmentStoreWrapper* big_segment_store_;
enum EvaluationReason::BigSegmentsStatus big_segments_status_ =
EvaluationReason::BigSegmentsStatus::kNone;
// Keyed by unhashed context key. Empty until the first Big Segment lookup.
std::unordered_map<std::string, integrations::Membership> memberships_;
std::unordered_set<std::string> store_error_keys_;
};

} // namespace launchdarkly::server_side::evaluation
31 changes: 27 additions & 4 deletions libs/server-sdk/src/evaluation/evaluator.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#include "evaluator.hpp"
#include "bucketing.hpp"
#include "rules.hpp"
Expand All @@ -20,8 +20,26 @@
launchdarkly::Context const& context,
Flag::Target const& target);

Evaluator::Evaluator(Logger& logger, data_interfaces::IStore const& source)
: logger_(logger), source_(source) {}
namespace {
EvaluationDetail<Value> WithBigSegmentsStatus(EvaluationDetail<Value> detail,
enum EvaluationReason::BigSegmentsStatus status) {
auto const& maybe_reason = detail.Reason();
if (!maybe_reason) {
return detail;
}
EvaluationReason const& reason = *maybe_reason;
EvaluationReason updated(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Part of me wants this to be more colocated with the evaluation. But I suppose a real constructor change would be noticed.

reason.Kind(), reason.ErrorKind(), reason.RuleIndex(), reason.RuleId(),
reason.PrerequisiteKey(), reason.InExperiment(), status);
return EvaluationDetail<Value>(detail.Value(), detail.VariationIndex(),
std::move(updated));
}
} // namespace

Evaluator::Evaluator(Logger& logger,
data_interfaces::IStore const& source,
data_components::BigSegmentStoreWrapper* big_segment_store)
: logger_(logger), source_(source), big_segment_store_(big_segment_store) {}

EvaluationDetail<Value> Evaluator::Evaluate(
data_model::Flag const& flag,
Expand All @@ -33,8 +51,13 @@
Flag const& flag,
launchdarkly::Context const& context,
EventScope const& event_scope) {
EvaluationStack stack;
return Evaluate(std::nullopt, flag, context, stack, event_scope);
EvaluationStack stack{big_segment_store_};
auto detail = Evaluate(std::nullopt, flag, context, stack, event_scope);
auto status = stack.BigSegmentsStatus();
if (status != EvaluationReason::BigSegmentsStatus::kNone) {
return WithBigSegmentsStatus(std::move(detail), status);
}
return detail;
}

EvaluationDetail<Value> Evaluator::Evaluate(
Expand Down
9 changes: 8 additions & 1 deletion libs/server-sdk/src/evaluation/evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ class Evaluator {
* threads in parallel, the given logger and IStore must be thread safe.
* @param logger A logger for recording errors or warnings.
* @param source The flag/segment source.
* @param big_segment_store Non-owning pointer to the Big Segment store
* wrapper, or nullptr if Big Segments are not configured. If non-null it
* must outlive the Evaluator and be safe to call from multiple threads.
*/
Evaluator(Logger& logger, data_interfaces::IStore const& source);
Evaluator(
Logger& logger,
data_interfaces::IStore const& source,
data_components::BigSegmentStoreWrapper* big_segment_store = nullptr);

/**
* Evaluates a flag for a given context.
Expand Down Expand Up @@ -69,5 +75,6 @@ class Evaluator {

Logger& logger_;
data_interfaces::IStore const& source_;
data_components::BigSegmentStoreWrapper* big_segment_store_;
};
} // namespace launchdarkly::server_side::evaluation
101 changes: 92 additions & 9 deletions libs/server-sdk/src/evaluation/rules.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,90 @@
#include "bucketing.hpp"
#include "operators.hpp"

#include "../data_components/big_segments/big_segment_store_wrapper.hpp"

#include <optional>
#include <string>
#include <utility>

namespace launchdarkly::server_side::evaluation {

using namespace data_model;

namespace {

// Maps the wrapper's internal status to the public reason status.
enum EvaluationReason::BigSegmentsStatus ToBigSegmentsStatus(
data_components::BigSegmentsStatus status) {
switch (status) {
case data_components::BigSegmentsStatus::kHealthy:
return EvaluationReason::BigSegmentsStatus::kHealthy;
case data_components::BigSegmentsStatus::kStale:
return EvaluationReason::BigSegmentsStatus::kStale;
case data_components::BigSegmentsStatus::kStoreError:
return EvaluationReason::BigSegmentsStatus::kStoreError;
case data_components::BigSegmentsStatus::kNotConfigured:
return EvaluationReason::BigSegmentsStatus::kNotConfigured;
}
return EvaluationReason::BigSegmentsStatus::kHealthy;
}

std::string MakeBigSegmentRef(Segment const& segment) {
return segment.key + ".g" + std::to_string(*segment.generation);
}

// Evaluates membership in an unbounded (Big) segment. Returns true/false for a
// definite match/non-match, or std::nullopt when the membership has no entry
// for this segment and evaluation should fall through to the segment's rules.
std::optional<bool> MatchBigSegment(Segment const& segment,
Context const& context,
EvaluationStack& stack) {
if (!segment.generation) {
// Without a generation the segment ref can't be formed.
stack.RecordBigSegmentsStatus(
EvaluationReason::BigSegmentsStatus::kNotConfigured);
return false;
}

// An absent or empty unboundedContextKind defaults to "user".
ContextKind const kind = (segment.unboundedContextKind &&
!segment.unboundedContextKind->t.empty())
? *segment.unboundedContextKind
: ContextKind{"user"};
Value const& context_key = context.Get(kind, "key");
if (!context_key.IsString()) {
return false;
}
std::string const& key = context_key.AsString();

if (stack.DidStoreError(key)) {
return false;
}

integrations::Membership const* membership = stack.FindMembership(key);
if (!membership) {
auto* store = stack.BigSegmentStore();
if (!store) {
stack.RecordBigSegmentsStatus(
EvaluationReason::BigSegmentsStatus::kNotConfigured);
return false;
}
auto result = store->GetMembership(key);
auto const status = ToBigSegmentsStatus(result.status);
stack.RecordBigSegmentsStatus(status);
if (status == EvaluationReason::BigSegmentsStatus::kStoreError) {
stack.RecordStoreError(key);
return false;
}
stack.StoreMembership(key, std::move(result.membership));
membership = stack.FindMembership(key);
}

return membership->CheckMembership(MakeBigSegmentRef(segment));
}

} // namespace

bool MaybeNegate(Clause const& clause, bool value) {
if (clause.negate) {
return !value;
Expand Down Expand Up @@ -161,16 +241,19 @@ tl::expected<bool, Error> Contains(Segment const& segment,
}

if (segment.unbounded) {
// TODO(sc209881): set big segment status to NOT_CONFIGURED.
return false;
}

if (IsTargeted(context, segment.included, segment.includedContexts)) {
return true;
}
if (auto match = MatchBigSegment(segment, context, stack)) {
return *match;
}
// Big segments don't use the regular include/exclude target lists; a
// membership miss falls through directly to the segment's rules.
} else {
if (IsTargeted(context, segment.included, segment.includedContexts)) {
return true;
}

if (IsTargeted(context, segment.excluded, segment.excludedContexts)) {
return false;
if (IsTargeted(context, segment.excluded, segment.excludedContexts)) {
return false;
}
}

for (auto const& rule : segment.rules) {
Expand Down
Loading
Loading