Skip to content

Commit

Permalink
feat: expose prerequisite relations in AllFlags API (#463)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cwaldren-ld authored Nov 8, 2024
1 parent 922d479 commit 56de5f5
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 21 deletions.
2 changes: 2 additions & 0 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ int main(int argc, char* argv[]) {
srv.add_capability("tls:custom-ca");
srv.add_capability("filtering");
srv.add_capability("filtering-strict");
srv.add_capability("client-prereq-events");

net::signal_set signals{ioc, SIGINT, SIGTERM};

boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

#include <chrono>
#include <cstdint>
#include <variant>

namespace launchdarkly::events {

Expand Down
2 changes: 2 additions & 0 deletions libs/internal/include/launchdarkly/events/data/events.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <launchdarkly/events/data/common_events.hpp>

Check failure on line 3 in libs/internal/include/launchdarkly/events/data/events.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/internal/include/launchdarkly/events/data/events.hpp:3:10 [clang-diagnostic-error]

'launchdarkly/events/data/common_events.hpp' file not found
#include <launchdarkly/events/data/server_events.hpp>

#include <variant>

namespace launchdarkly::events {

using InputEvent = std::variant<FeatureEventParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class IEventProcessor {
* capacity.
* @param event InputEvent to deliver.
*/
virtual void SendAsync(events::InputEvent event) = 0;
virtual void SendAsync(InputEvent event) = 0;
/**
* Asynchronously flush's the processor's events, returning as soon as
* possible. Flushing may be a no-op if a flush is ongoing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

namespace launchdarkly::server_side {

Expand Down Expand Up @@ -62,6 +63,14 @@ class AllFlagsState {
bool track_reason,
std::optional<std::uint64_t> debug_events_until_date);

State(std::uint64_t version,
std::optional<std::int64_t> variation,
std::optional<EvaluationReason> reason,
bool track_events,
bool track_reason,
std::optional<std::uint64_t> debug_events_until_date,
std::vector<std::string> prerequisites);

/**
* @return The flag's version number when it was evaluated.
*/
Expand Down Expand Up @@ -110,6 +119,12 @@ class AllFlagsState {
*/
[[nodiscard]] bool OmitDetails() const;

/**
* @return The list of prerequisites for this flag in the order they
* were evaluated.
*/
[[nodiscard]] std::vector<std::string> const& Prerequisites() const;

friend class AllFlagsStateBuilder;

private:
Expand All @@ -120,6 +135,7 @@ class AllFlagsState {
bool track_reason_;
std::optional<std::uint64_t> debug_events_until_date_;
bool omit_details_;
std::vector<std::string> prerequisites_;
};

/**
Expand Down
2 changes: 2 additions & 0 deletions libs/server-sdk/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ target_sources(${LIBNAME}
all_flags_state/json_all_flags_state.cpp
all_flags_state/all_flags_state_builder.cpp
integrations/data_reader/kinds.cpp
prereq_event_recorder/prereq_event_recorder.cpp
prereq_event_recorder/prereq_event_recorder.hpp
data_components/change_notifier/change_notifier.hpp
data_components/change_notifier/change_notifier.cpp
data_components/dependency_tracker/dependency_tracker.hpp
Expand Down
38 changes: 30 additions & 8 deletions libs/server-sdk/src/all_flags_state/all_flags_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,36 @@
namespace launchdarkly::server_side {

AllFlagsState::State::State(
std::uint64_t version,
std::optional<std::int64_t> variation,
std::uint64_t const version,
std::optional<std::int64_t> const variation,
std::optional<EvaluationReason> reason,
bool track_events,
bool track_reason,
std::optional<std::uint64_t> debug_events_until_date)
bool const track_events,
bool const track_reason,
std::optional<std::uint64_t> const debug_events_until_date)
: State(version,
variation,
std::move(reason),
track_events,
track_reason,
debug_events_until_date,
std::vector<std::string>{}) {}

AllFlagsState::State::State(
std::uint64_t const version,
std::optional<std::int64_t> const variation,
std::optional<EvaluationReason> reason,
bool const track_events,
bool const track_reason,
std::optional<std::uint64_t> const debug_events_until_date,
std::vector<std::string> prerequisites)
: version_(version),
variation_(variation),
reason_(reason),
reason_(std::move(reason)),
track_events_(track_events),
track_reason_(track_reason),
debug_events_until_date_(debug_events_until_date),
omit_details_(false) {}
omit_details_(false),
prerequisites_(std::move(prerequisites)) {}

std::uint64_t AllFlagsState::State::Version() const {
return version_;
Expand All @@ -37,6 +54,10 @@ bool AllFlagsState::State::TrackReason() const {
return track_reason_;
}

std::vector<std::string> const& AllFlagsState::State::Prerequisites() const {
return prerequisites_;
}

std::optional<std::uint64_t> const& AllFlagsState::State::DebugEventsUntilDate()
const {
return debug_events_until_date_;
Expand Down Expand Up @@ -80,7 +101,8 @@ bool operator==(AllFlagsState::State const& lhs,
lhs.TrackEvents() == rhs.TrackEvents() &&
lhs.TrackReason() == rhs.TrackReason() &&
lhs.DebugEventsUntilDate() == rhs.DebugEventsUntilDate() &&
lhs.OmitDetails() == rhs.OmitDetails();
lhs.OmitDetails() == rhs.OmitDetails() &&
lhs.Prerequisites() == rhs.Prerequisites();
}

} // namespace launchdarkly::server_side
5 changes: 5 additions & 0 deletions libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ void tag_invoke(boost::json::value_from_tag const& unused,
obj.emplace("debugEventsUntilDate", boost::json::value_from(*date));
}
}

if (auto const& prerequisites = state.Prerequisites();
!prerequisites.empty()) {
obj.emplace("prerequisites", boost::json::value_from(prerequisites));
}
}

void tag_invoke(boost::json::value_from_tag const& unused,
Expand Down
18 changes: 11 additions & 7 deletions libs/server-sdk/src/client_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "data_systems/lazy_load/lazy_load_system.hpp"
#include "data_systems/offline.hpp"
#include "evaluation/evaluation_stack.hpp"
#include "prereq_event_recorder/prereq_event_recorder.hpp"

#include "data_interfaces/system/idata_system.hpp"

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

AllFlagsStateBuilder builder{options};

EventScope no_events;

auto all_flags = data_system_->AllFlags();

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

Check warning on line 191 in libs/server-sdk/src/client_impl.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/libs/server-sdk/src/client_impl.cpp:191:10 [readability-identifier-length]

variable name '_' is too short, expected at least 3 characters

for (auto const& [k, v] : all_flags) {
for (auto const& [key, v] : all_flags) {
if (!v || !v->item) {
continue;
}
Expand All @@ -203,15 +202,20 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context,
continue;
}

EvaluationDetail<Value> detail =
evaluator_.Evaluate(flag, context, no_events);
PrereqEventRecorder recorder{key};

EvaluationDetail<Value> detail = evaluator_.Evaluate(
flag, context,
EventScope{&recorder, EventFactory::WithoutReasons()});

bool in_experiment = flag.IsExperimentationEnabled(detail.Reason());
builder.AddFlag(k, detail.Value(),

builder.AddFlag(key, detail.Value(),
AllFlagsState::State{
flag.Version(), detail.VariationIndex(),
detail.Reason(), flag.trackEvents || in_experiment,
in_experiment, flag.debugEventsUntilDate});
in_experiment, flag.debugEventsUntilDate,
std::move(recorder).TakePrerequisites()});
}

return builder.Build();
Expand Down
3 changes: 1 addition & 2 deletions libs/server-sdk/src/events/event_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
#include <chrono>
namespace launchdarkly::server_side {

EventFactory::EventFactory(
launchdarkly::server_side::EventFactory::ReasonPolicy reason_policy)
EventFactory::EventFactory(ReasonPolicy const reason_policy)
: reason_policy_(reason_policy),
now_([]() { return events::Date{std::chrono::system_clock::now()}; }) {}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#include "prereq_event_recorder.hpp"

namespace launchdarkly::server_side {

PrereqEventRecorder::PrereqEventRecorder(std::string flag_key)
: flag_key_(std::move(flag_key)) {}

void PrereqEventRecorder::SendAsync(events::InputEvent const event) {
if (auto const* feat = std::get_if<events::FeatureEventParams>(&event)) {
if (auto const prereq_of = feat->prereq_of) {
if (*prereq_of == flag_key_) {
prereqs_.push_back(feat->key);
}
}
}
}

void PrereqEventRecorder::FlushAsync() {}

void PrereqEventRecorder::ShutdownAsync() {}

std::vector<std::string> const& PrereqEventRecorder::Prerequisites() const {
return prereqs_;
}

std::vector<std::string>&& PrereqEventRecorder::TakePrerequisites() && {
return std::move(prereqs_);
}

} // namespace launchdarkly::server_side
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once

#include <launchdarkly/events/event_processor_interface.hpp>

#include <string>
#include <vector>

namespace launchdarkly::server_side {

/**
* This class is meant only to record direct prerequisites of a flag. That is,
* although it will be passed events for all prerequisites seen during an
* evaluation via SendAsync, it will only store those that are a direct
* prerequisite of the parent flag passed in the constructor.
*
* As a future improvement, it would be possible to unify the EventScope
* mechanism currently used by the Evaluator to send events with a class
* similar to this one, or to refactor the Evaluator to include prerequisite
* information in the returned EvaluationDetail (or a new Result class, which
* would be a composite of the EvaluationDetail and a vector of prerequisites.)
*/
class PrereqEventRecorder final : public events::IEventProcessor {
public:
explicit PrereqEventRecorder(std::string flag_key);

void SendAsync(events::InputEvent event) override;

/* No-op */
void FlushAsync() override;

/* No-op */
void ShutdownAsync() override;

std::vector<std::string> const& Prerequisites() const;

std::vector<std::string>&& TakePrerequisites() &&;

private:
std::string const flag_key_;
std::vector<std::string> prereqs_;
};

} // namespace launchdarkly::server_side
Loading

0 comments on commit 56de5f5

Please sign in to comment.