Skip to content

Commit 7fd64ef

Browse files
authored
fix: Evaluate should not share EvaluationStack between calls (#374)
Addresses #373.
1 parent bf01313 commit 7fd64ef

File tree

11 files changed

+69
-32
lines changed

11 files changed

+69
-32
lines changed

libs/server-sdk/src/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ target_sources(${LIBNAME}
6060
evaluation/bucketing.cpp
6161
evaluation/operators.cpp
6262
evaluation/evaluation_error.cpp
63-
evaluation/detail/evaluation_stack.cpp
63+
evaluation/evaluation_stack.cpp
6464
evaluation/detail/semver_operations.cpp
6565
evaluation/detail/timestamp_operations.cpp
6666
events/event_factory.cpp

libs/server-sdk/src/client_impl.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "data_systems/background_sync/background_sync_system.hpp"
55
#include "data_systems/lazy_load/lazy_load_system.hpp"
66
#include "data_systems/offline.hpp"
7+
#include "evaluation/evaluation_stack.hpp"
78

89
#include "data_interfaces/system/idata_system.hpp"
910

libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp renamed to libs/server-sdk/src/evaluation/evaluation_stack.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#include "evaluation_stack.hpp"
22

3-
namespace launchdarkly::server_side::evaluation::detail {
3+
namespace launchdarkly::server_side::evaluation {
44

55
Guard::Guard(std::unordered_set<std::string>& set, std::string key)
66
: set_(set), key_(std::move(key)) {
@@ -27,4 +27,4 @@ std::optional<Guard> EvaluationStack::NoticeSegment(std::string segment_key) {
2727
return std::make_optional<Guard>(segments_seen_, std::move(segment_key));
2828
}
2929

30-
} // namespace launchdarkly::server_side::evaluation::detail
30+
} // namespace launchdarkly::server_side::evaluation

libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp renamed to libs/server-sdk/src/evaluation/evaluation_stack.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#include <string>
55
#include <unordered_set>
66

7-
namespace launchdarkly::server_side::evaluation::detail {
7+
namespace launchdarkly::server_side::evaluation {
88

99
/**
1010
* Guard is an object used to track that a segment or flag key has been noticed.
@@ -57,4 +57,4 @@ class EvaluationStack {
5757
std::unordered_set<std::string> segments_seen_;
5858
};
5959

60-
} // namespace launchdarkly::server_side::evaluation::detail
60+
} // namespace launchdarkly::server_side::evaluation

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "evaluator.hpp"
2+
#include "bucketing.hpp"
23
#include "rules.hpp"
34

45
#include <boost/core/ignore_unused.hpp>
@@ -20,7 +21,7 @@ std::optional<std::size_t> TargetMatchVariation(
2021
Flag::Target const& target);
2122

2223
Evaluator::Evaluator(Logger& logger, data_interfaces::IStore const& source)
23-
: logger_(logger), source_(source), stack_() {}
24+
: logger_(logger), source_(source) {}
2425

2526
EvaluationDetail<Value> Evaluator::Evaluate(
2627
data_model::Flag const& flag,
@@ -32,15 +33,17 @@ EvaluationDetail<Value> Evaluator::Evaluate(
3233
Flag const& flag,
3334
launchdarkly::Context const& context,
3435
EventScope const& event_scope) {
35-
return Evaluate(std::nullopt, flag, context, event_scope);
36+
EvaluationStack stack;
37+
return Evaluate(std::nullopt, flag, context, stack, event_scope);
3638
}
3739

3840
EvaluationDetail<Value> Evaluator::Evaluate(
3941
std::optional<std::string> parent_key,
4042
Flag const& flag,
4143
launchdarkly::Context const& context,
44+
EvaluationStack& stack,
4245
EventScope const& event_scope) {
43-
if (auto guard = stack_.NoticePrerequisite(flag.key)) {
46+
if (auto guard = stack.NoticePrerequisite(flag.key)) {
4447
if (!flag.on) {
4548
return OffValue(flag, EvaluationReason::Off());
4649
}
@@ -63,8 +66,8 @@ EvaluationDetail<Value> Evaluator::Evaluate(
6366
}
6467

6568
// Recursive call; cycles are detected by the guard.
66-
EvaluationDetail<Value> detailed_evaluation =
67-
Evaluate(flag.key, *descriptor.item, context, event_scope);
69+
EvaluationDetail<Value> detailed_evaluation = Evaluate(
70+
flag.key, *descriptor.item, context, stack, event_scope);
6871

6972
if (detailed_evaluation.IsError()) {
7073
return detailed_evaluation;
@@ -106,7 +109,7 @@ EvaluationDetail<Value> Evaluator::Evaluate(
106109
auto const& rule = flag.rules[rule_index];
107110

108111
tl::expected<bool, Error> rule_match =
109-
Match(rule, context, source_, stack_);
112+
Match(rule, context, source_, stack);
110113

111114
if (!rule_match) {
112115
LogError(flag.key, rule_match.error());

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@
66
#include <launchdarkly/logging/logger.hpp>
77
#include <launchdarkly/value.hpp>
88

9-
#include "bucketing.hpp"
10-
#include "detail/evaluation_stack.hpp"
119
#include "evaluation_error.hpp"
10+
#include "evaluation_stack.hpp"
1211

1312
#include "../data_interfaces/store/istore.hpp"
1413
#include "../events/event_scope.hpp"
1514

16-
#include <tl/expected.hpp>
17-
1815
namespace launchdarkly::server_side::evaluation {
1916

2017
class Evaluator {
2118
public:
19+
/**
20+
* Constructs a new Evaluator. Since the Evaluator may be used by multiple
21+
* threads in parallel, the given logger and IStore must be thread safe.
22+
* @param logger A logger for recording errors or warnings.
23+
* @param source The flag/segment source.
24+
*/
2225
Evaluator(Logger& logger, data_interfaces::IStore const& source);
2326

2427
/**
2528
* Evaluates a flag for a given context.
26-
* Warning: not thread safe.
2729
*
2830
* @param flag The flag to evaluate.
2931
* @param context The context to evaluate the flag against.
32+
* @param stack The evaluation stack used for detecting circular references.
3033
* @param event_scope The event scope used for recording prerequisite
3134
* events.
3235
*/
@@ -37,7 +40,7 @@ class Evaluator {
3740

3841
/**
3942
* Evaluates a flag for a given context. Does not record prerequisite
40-
* events. Warning: not thread safe.
43+
* events.
4144
*
4245
* @param flag The flag to evaluate.
4346
* @param context The context to evaluate the flag against.
@@ -50,6 +53,7 @@ class Evaluator {
5053
std::optional<std::string> parent_key,
5154
data_model::Flag const& flag,
5255
Context const& context,
56+
EvaluationStack& stack,
5357
EventScope const& event_scope);
5458

5559
[[nodiscard]] EvaluationDetail<Value> FlagVariation(
@@ -65,6 +69,5 @@ class Evaluator {
6569

6670
Logger& logger_;
6771
data_interfaces::IStore const& source_;
68-
detail::EvaluationStack stack_;
6972
};
7073
} // namespace launchdarkly::server_side::evaluation

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ bool MaybeNegate(Clause const& clause, bool value) {
1616
tl::expected<bool, Error> Match(Flag::Rule const& rule,
1717
launchdarkly::Context const& context,
1818
data_interfaces::IStore const& store,
19-
detail::EvaluationStack& stack) {
19+
EvaluationStack& stack) {
2020
for (Clause const& clause : rule.clauses) {
2121
tl::expected<bool, Error> result = Match(clause, context, store, stack);
2222
if (!result) {
@@ -32,7 +32,7 @@ tl::expected<bool, Error> Match(Flag::Rule const& rule,
3232
tl::expected<bool, Error> Match(Segment::Rule const& rule,
3333
Context const& context,
3434
data_interfaces::IStore const& store,
35-
detail::EvaluationStack& stack,
35+
EvaluationStack& stack,
3636
std::string const& key,
3737
std::string const& salt) {
3838
for (Clause const& clause : rule.clauses) {
@@ -62,7 +62,7 @@ tl::expected<bool, Error> Match(Segment::Rule const& rule,
6262
tl::expected<bool, Error> Match(Clause const& clause,
6363
launchdarkly::Context const& context,
6464
data_interfaces::IStore const& store,
65-
detail::EvaluationStack& stack) {
65+
EvaluationStack& stack) {
6666
if (clause.op == Clause::Op::kSegmentMatch) {
6767
return MatchSegment(clause, context, store, stack);
6868
}
@@ -72,7 +72,7 @@ tl::expected<bool, Error> Match(Clause const& clause,
7272
tl::expected<bool, Error> MatchSegment(Clause const& clause,
7373
launchdarkly::Context const& context,
7474
data_interfaces::IStore const& store,
75-
detail::EvaluationStack& stack) {
75+
EvaluationStack& stack) {
7676
for (Value const& value : clause.values) {
7777
// A segment key represented as a Value is a string; non-strings are
7878
// ignored.
@@ -154,7 +154,7 @@ tl::expected<bool, Error> MatchNonSegment(
154154
tl::expected<bool, Error> Contains(Segment const& segment,
155155
Context const& context,
156156
data_interfaces::IStore const& store,
157-
detail::EvaluationStack& stack) {
157+
EvaluationStack& stack) {
158158
auto guard = stack.NoticeSegment(segment.key);
159159
if (!guard) {
160160
return tl::make_unexpected(Error::CyclicSegmentReference(segment.key));

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#pragma once
22

33
#include "../data_interfaces/store/istore.hpp"
4-
#include "detail/evaluation_stack.hpp"
54
#include "evaluation_error.hpp"
5+
#include "evaluation_stack.hpp"
66

77
#include <launchdarkly/context.hpp>
88
#include <launchdarkly/data_model/flag.hpp>
@@ -18,26 +18,26 @@ namespace launchdarkly::server_side::evaluation {
1818
data_model::Flag::Rule const&,
1919
Context const&,
2020
data_interfaces::IStore const& store,
21-
detail::EvaluationStack& stack);
21+
EvaluationStack& stack);
2222

2323
[[nodiscard]] tl::expected<bool, Error> Match(data_model::Clause const&,
2424
Context const&,
2525
data_interfaces::IStore const&,
26-
detail::EvaluationStack&);
26+
EvaluationStack&);
2727

2828
[[nodiscard]] tl::expected<bool, Error> Match(
2929
data_model::Segment::Rule const& rule,
3030
Context const& context,
3131
data_interfaces::IStore const& store,
32-
detail::EvaluationStack& stack,
32+
EvaluationStack& stack,
3333
std::string const& key,
3434
std::string const& salt);
3535

3636
[[nodiscard]] tl::expected<bool, Error> MatchSegment(
3737
data_model::Clause const&,
3838
Context const&,
3939
data_interfaces::IStore const&,
40-
detail::EvaluationStack& stack);
40+
EvaluationStack& stack);
4141

4242
[[nodiscard]] tl::expected<bool, Error> MatchNonSegment(
4343
data_model::Clause const&,
@@ -47,7 +47,7 @@ namespace launchdarkly::server_side::evaluation {
4747
data_model::Segment const&,
4848
Context const&,
4949
data_interfaces::IStore const& store,
50-
detail::EvaluationStack& stack);
50+
EvaluationStack& stack);
5151

5252
[[nodiscard]] bool MaybeNegate(data_model::Clause const& clause, bool value);
5353

libs/server-sdk/tests/evaluation_stack_test.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#include <gtest/gtest.h>
22

3-
#include "evaluation/detail/evaluation_stack.hpp"
3+
#include "evaluation/evaluation_stack.hpp"
44

5-
using namespace launchdarkly::server_side::evaluation::detail;
5+
using namespace launchdarkly::server_side::evaluation;
66

77
TEST(EvalStackTests, SegmentIsNoticed) {
88
EvaluationStack stack;

libs/server-sdk/tests/evaluator_tests.cpp

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
#include <gtest/gtest.h>
1010

11+
#include <random>
12+
#include <thread>
13+
1114
using namespace launchdarkly;
1215
using namespace launchdarkly::server_side;
1316

@@ -246,3 +249,30 @@ TEST_F(EvaluatorTests, FlagWithExperimentTargetingMissingContext) {
246249
ASSERT_EQ(*detail, Value(false));
247250
ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false));
248251
}
252+
253+
TEST_F(EvaluatorTests, ThreadSafeEvaluation) {
254+
auto flag = store_->GetFlag("flagWithTarget")->item.value();
255+
256+
constexpr std::size_t kNumThreads = 20;
257+
258+
std::vector<std::thread> threads;
259+
260+
for (std::size_t i = 0; i < kNumThreads; i++) {
261+
constexpr std::size_t kNumIterations = 1000;
262+
threads.emplace_back([this, &flag, kNumIterations] {
263+
std::mt19937 generator(
264+
std::chrono::system_clock::now().time_since_epoch().count());
265+
std::uniform_int_distribution distribution(1, 3);
266+
for (std::size_t j = 0; j < kNumIterations; j++) {
267+
auto user_a = ContextBuilder().Kind("user", "userKeyA").Build();
268+
auto _ = eval_.Evaluate(flag, user_a);
269+
std::this_thread::sleep_for(
270+
std::chrono::milliseconds(distribution(generator)));
271+
}
272+
});
273+
}
274+
275+
for (auto& t : threads) {
276+
t.join();
277+
}
278+
}

libs/server-sdk/tests/rule_tests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ int const AllOperatorsTest::DATE_MS_NEGATIVE = -10000;
3636
const std::string AllOperatorsTest::INVALID_DATE = "hey what's this?";
3737

3838
TEST_P(AllOperatorsTest, Matches) {
39-
using namespace launchdarkly::server_side::evaluation::detail;
39+
using namespace launchdarkly::server_side::evaluation;
4040
using namespace launchdarkly;
4141

4242
auto const& param = GetParam();

0 commit comments

Comments
 (0)