Skip to content

Commit 56de5f5

Browse files
authored
feat: expose prerequisite relations in AllFlags API (#463)
Updates the `AllFlags()` API to gather and expose prerequisite information for flags. The prereqs are available via `State::Prerequisites()`, and are more importantly serialized in the JSON representation of the bootstrap payload. The implementation strategy was to create a custom `IEventProcessor` specifically for `AllFlags` usage. Previously, `AllFlags` invoked the evaluator with a no-op `EventScope`, which made event creation a no-op within the eval algorithm. This change now passes in a `PrereqEventRecorder` which tracks the top-level prerequisites of each flag.
1 parent 922d479 commit 56de5f5

File tree

14 files changed

+377
-21
lines changed

14 files changed

+377
-21
lines changed

contract-tests/server-contract-tests/src/main.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ int main(int argc, char* argv[]) {
4747
srv.add_capability("tls:custom-ca");
4848
srv.add_capability("filtering");
4949
srv.add_capability("filtering-strict");
50+
srv.add_capability("client-prereq-events");
51+
5052
net::signal_set signals{ioc, SIGINT, SIGTERM};
5153

5254
boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable {

libs/internal/include/launchdarkly/events/data/common_events.hpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
#include <chrono>
1010
#include <cstdint>
11-
#include <variant>
1211

1312
namespace launchdarkly::events {
1413

libs/internal/include/launchdarkly/events/data/events.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
#include <launchdarkly/events/data/common_events.hpp>
44
#include <launchdarkly/events/data/server_events.hpp>
55

6+
#include <variant>
7+
68
namespace launchdarkly::events {
79

810
using InputEvent = std::variant<FeatureEventParams,

libs/internal/include/launchdarkly/events/event_processor_interface.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class IEventProcessor {
1212
* capacity.
1313
* @param event InputEvent to deliver.
1414
*/
15-
virtual void SendAsync(events::InputEvent event) = 0;
15+
virtual void SendAsync(InputEvent event) = 0;
1616
/**
1717
* Asynchronously flush's the processor's events, returning as soon as
1818
* possible. Flushing may be a no-op if a flush is ongoing.

libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <optional>
88
#include <string>
99
#include <unordered_map>
10+
#include <vector>
1011

1112
namespace launchdarkly::server_side {
1213

@@ -62,6 +63,14 @@ class AllFlagsState {
6263
bool track_reason,
6364
std::optional<std::uint64_t> debug_events_until_date);
6465

66+
State(std::uint64_t version,
67+
std::optional<std::int64_t> variation,
68+
std::optional<EvaluationReason> reason,
69+
bool track_events,
70+
bool track_reason,
71+
std::optional<std::uint64_t> debug_events_until_date,
72+
std::vector<std::string> prerequisites);
73+
6574
/**
6675
* @return The flag's version number when it was evaluated.
6776
*/
@@ -110,6 +119,12 @@ class AllFlagsState {
110119
*/
111120
[[nodiscard]] bool OmitDetails() const;
112121

122+
/**
123+
* @return The list of prerequisites for this flag in the order they
124+
* were evaluated.
125+
*/
126+
[[nodiscard]] std::vector<std::string> const& Prerequisites() const;
127+
113128
friend class AllFlagsStateBuilder;
114129

115130
private:
@@ -120,6 +135,7 @@ class AllFlagsState {
120135
bool track_reason_;
121136
std::optional<std::uint64_t> debug_events_until_date_;
122137
bool omit_details_;
138+
std::vector<std::string> prerequisites_;
123139
};
124140

125141
/**

libs/server-sdk/src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ target_sources(${LIBNAME}
3030
all_flags_state/json_all_flags_state.cpp
3131
all_flags_state/all_flags_state_builder.cpp
3232
integrations/data_reader/kinds.cpp
33+
prereq_event_recorder/prereq_event_recorder.cpp
34+
prereq_event_recorder/prereq_event_recorder.hpp
3335
data_components/change_notifier/change_notifier.hpp
3436
data_components/change_notifier/change_notifier.cpp
3537
data_components/dependency_tracker/dependency_tracker.hpp

libs/server-sdk/src/all_flags_state/all_flags_state.cpp

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,36 @@
33
namespace launchdarkly::server_side {
44

55
AllFlagsState::State::State(
6-
std::uint64_t version,
7-
std::optional<std::int64_t> variation,
6+
std::uint64_t const version,
7+
std::optional<std::int64_t> const variation,
88
std::optional<EvaluationReason> reason,
9-
bool track_events,
10-
bool track_reason,
11-
std::optional<std::uint64_t> debug_events_until_date)
9+
bool const track_events,
10+
bool const track_reason,
11+
std::optional<std::uint64_t> const debug_events_until_date)
12+
: State(version,
13+
variation,
14+
std::move(reason),
15+
track_events,
16+
track_reason,
17+
debug_events_until_date,
18+
std::vector<std::string>{}) {}
19+
20+
AllFlagsState::State::State(
21+
std::uint64_t const version,
22+
std::optional<std::int64_t> const variation,
23+
std::optional<EvaluationReason> reason,
24+
bool const track_events,
25+
bool const track_reason,
26+
std::optional<std::uint64_t> const debug_events_until_date,
27+
std::vector<std::string> prerequisites)
1228
: version_(version),
1329
variation_(variation),
14-
reason_(reason),
30+
reason_(std::move(reason)),
1531
track_events_(track_events),
1632
track_reason_(track_reason),
1733
debug_events_until_date_(debug_events_until_date),
18-
omit_details_(false) {}
34+
omit_details_(false),
35+
prerequisites_(std::move(prerequisites)) {}
1936

2037
std::uint64_t AllFlagsState::State::Version() const {
2138
return version_;
@@ -37,6 +54,10 @@ bool AllFlagsState::State::TrackReason() const {
3754
return track_reason_;
3855
}
3956

57+
std::vector<std::string> const& AllFlagsState::State::Prerequisites() const {
58+
return prerequisites_;
59+
}
60+
4061
std::optional<std::uint64_t> const& AllFlagsState::State::DebugEventsUntilDate()
4162
const {
4263
return debug_events_until_date_;
@@ -80,7 +101,8 @@ bool operator==(AllFlagsState::State const& lhs,
80101
lhs.TrackEvents() == rhs.TrackEvents() &&
81102
lhs.TrackReason() == rhs.TrackReason() &&
82103
lhs.DebugEventsUntilDate() == rhs.DebugEventsUntilDate() &&
83-
lhs.OmitDetails() == rhs.OmitDetails();
104+
lhs.OmitDetails() == rhs.OmitDetails() &&
105+
lhs.Prerequisites() == rhs.Prerequisites();
84106
}
85107

86108
} // namespace launchdarkly::server_side

libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ void tag_invoke(boost::json::value_from_tag const& unused,
3131
obj.emplace("debugEventsUntilDate", boost::json::value_from(*date));
3232
}
3333
}
34+
35+
if (auto const& prerequisites = state.Prerequisites();
36+
!prerequisites.empty()) {
37+
obj.emplace("prerequisites", boost::json::value_from(prerequisites));
38+
}
3439
}
3540

3641
void tag_invoke(boost::json::value_from_tag const& unused,

libs/server-sdk/src/client_impl.cpp

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "data_systems/lazy_load/lazy_load_system.hpp"
66
#include "data_systems/offline.hpp"
77
#include "evaluation/evaluation_stack.hpp"
8+
#include "prereq_event_recorder/prereq_event_recorder.hpp"
89

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

@@ -181,8 +182,6 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context,
181182

182183
AllFlagsStateBuilder builder{options};
183184

184-
EventScope no_events;
185-
186185
auto all_flags = data_system_->AllFlags();
187186

188187
// Because evaluating the flags may access many segments, tell the data
@@ -191,7 +190,7 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context,
191190
// memory.)
192191
auto _ = data_system_->AllSegments();
193192

194-
for (auto const& [k, v] : all_flags) {
193+
for (auto const& [key, v] : all_flags) {
195194
if (!v || !v->item) {
196195
continue;
197196
}
@@ -203,15 +202,20 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context,
203202
continue;
204203
}
205204

206-
EvaluationDetail<Value> detail =
207-
evaluator_.Evaluate(flag, context, no_events);
205+
PrereqEventRecorder recorder{key};
206+
207+
EvaluationDetail<Value> detail = evaluator_.Evaluate(
208+
flag, context,
209+
EventScope{&recorder, EventFactory::WithoutReasons()});
208210

209211
bool in_experiment = flag.IsExperimentationEnabled(detail.Reason());
210-
builder.AddFlag(k, detail.Value(),
212+
213+
builder.AddFlag(key, detail.Value(),
211214
AllFlagsState::State{
212215
flag.Version(), detail.VariationIndex(),
213216
detail.Reason(), flag.trackEvents || in_experiment,
214-
in_experiment, flag.debugEventsUntilDate});
217+
in_experiment, flag.debugEventsUntilDate,
218+
std::move(recorder).TakePrerequisites()});
215219
}
216220

217221
return builder.Build();

libs/server-sdk/src/events/event_factory.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
#include <chrono>
44
namespace launchdarkly::server_side {
55

6-
EventFactory::EventFactory(
7-
launchdarkly::server_side::EventFactory::ReasonPolicy reason_policy)
6+
EventFactory::EventFactory(ReasonPolicy const reason_policy)
87
: reason_policy_(reason_policy),
98
now_([]() { return events::Date{std::chrono::system_clock::now()}; }) {}
109

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#include "prereq_event_recorder.hpp"
2+
3+
namespace launchdarkly::server_side {
4+
5+
PrereqEventRecorder::PrereqEventRecorder(std::string flag_key)
6+
: flag_key_(std::move(flag_key)) {}
7+
8+
void PrereqEventRecorder::SendAsync(events::InputEvent const event) {
9+
if (auto const* feat = std::get_if<events::FeatureEventParams>(&event)) {
10+
if (auto const prereq_of = feat->prereq_of) {
11+
if (*prereq_of == flag_key_) {
12+
prereqs_.push_back(feat->key);
13+
}
14+
}
15+
}
16+
}
17+
18+
void PrereqEventRecorder::FlushAsync() {}
19+
20+
void PrereqEventRecorder::ShutdownAsync() {}
21+
22+
std::vector<std::string> const& PrereqEventRecorder::Prerequisites() const {
23+
return prereqs_;
24+
}
25+
26+
std::vector<std::string>&& PrereqEventRecorder::TakePrerequisites() && {
27+
return std::move(prereqs_);
28+
}
29+
30+
} // namespace launchdarkly::server_side
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#pragma once
2+
3+
#include <launchdarkly/events/event_processor_interface.hpp>
4+
5+
#include <string>
6+
#include <vector>
7+
8+
namespace launchdarkly::server_side {
9+
10+
/**
11+
* This class is meant only to record direct prerequisites of a flag. That is,
12+
* although it will be passed events for all prerequisites seen during an
13+
* evaluation via SendAsync, it will only store those that are a direct
14+
* prerequisite of the parent flag passed in the constructor.
15+
*
16+
* As a future improvement, it would be possible to unify the EventScope
17+
* mechanism currently used by the Evaluator to send events with a class
18+
* similar to this one, or to refactor the Evaluator to include prerequisite
19+
* information in the returned EvaluationDetail (or a new Result class, which
20+
* would be a composite of the EvaluationDetail and a vector of prerequisites.)
21+
*/
22+
class PrereqEventRecorder final : public events::IEventProcessor {
23+
public:
24+
explicit PrereqEventRecorder(std::string flag_key);
25+
26+
void SendAsync(events::InputEvent event) override;
27+
28+
/* No-op */
29+
void FlushAsync() override;
30+
31+
/* No-op */
32+
void ShutdownAsync() override;
33+
34+
std::vector<std::string> const& Prerequisites() const;
35+
36+
std::vector<std::string>&& TakePrerequisites() &&;
37+
38+
private:
39+
std::string const flag_key_;
40+
std::vector<std::string> prereqs_;
41+
};
42+
43+
} // namespace launchdarkly::server_side

0 commit comments

Comments
 (0)