Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expose prerequisite relations in AllFlags API #463

Merged
merged 5 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions contract-tests/server-contract-tests/src/main.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "server.hpp"

Check failure on line 1 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/server-contract-tests/src/main.cpp:1:10 [clang-diagnostic-error]

'server.hpp' file not found

#include <launchdarkly/logging/console_backend.hpp>

Expand All @@ -18,7 +18,7 @@
using launchdarkly::LogLevel;

int main(int argc, char* argv[]) {
launchdarkly::Logger logger{

Check warning on line 21 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/server-contract-tests/src/main.cpp:21:26 [cppcoreguidelines-init-variables]

variable 'logger' is not initialized
std::make_unique<ConsoleBackend>("server-contract-tests")};

std::string const default_port = "8123";
Expand All @@ -31,8 +31,8 @@
try {
net::io_context ioc{1};

auto const p = boost::lexical_cast<unsigned short>(port);

Check warning on line 34 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/server-contract-tests/src/main.cpp:34:20 [readability-identifier-length]

variable name 'p' is too short, expected at least 3 characters
server srv{ioc, "0.0.0.0", p, logger};

Check warning on line 35 in contract-tests/server-contract-tests/src/main.cpp

View workflow job for this annotation

GitHub Actions / cpp-linter

/contract-tests/server-contract-tests/src/main.cpp:35:16 [cppcoreguidelines-init-variables]

variable 'srv' is not initialized

srv.add_capability("server-side");
srv.add_capability("strongly-typed");
Expand All @@ -47,6 +47,8 @@
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
@@ -1,8 +1,10 @@
#pragma once

#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
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 @@ -98,7 +99,7 @@
bool IsFlagPresent(
std::shared_ptr<data_model::FlagDescriptor> const& flag_desc);

ClientImpl::ClientImpl(Config config, std::string const& version)

Check warning on line 102 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:102:31 [performance-unnecessary-value-param]

the parameter 'config' is copied for each invocation but only used as a const reference; consider making it a const reference
: config_(config),
http_properties_(
config::builders::HttpPropertiesBuilder(config.HttpProperties())
Expand All @@ -109,7 +110,7 @@
logger_(MakeLogger(config.Logging())),
ioc_(kAsioConcurrencyHint),
work_(boost::asio::make_work_guard(ioc_)),
status_manager_(),

Check warning on line 113 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:113:7 [readability-redundant-member-init]

initializer for member 'status_manager_' is redundant
data_system_(MakeDataSystem(http_properties_,
config_,
ioc_.get_executor(),
Expand Down Expand Up @@ -146,10 +147,10 @@
}

std::future<bool> ClientImpl::StartAsync() {
auto pr = std::make_shared<std::promise<bool>>();

Check warning on line 150 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:150:10 [readability-identifier-length]

variable name 'pr' is too short, expected at least 3 characters
auto fut = pr->get_future();

status_manager_.OnDataSourceStatusChangeEx([this, pr](auto _) {

Check warning on line 153 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:153:64 [readability-identifier-length]

parameter name '_' is too short, expected at least 3 characters
if (data_system_->Initialized()) {
pr->set_value(true);
return true; /* delete this change listener since the
Expand Down Expand Up @@ -181,17 +182,15 @@

AllFlagsStateBuilder builder{options};

EventScope no_events;

auto all_flags = data_system_->AllFlags();

// Because evaluating the flags may access many segments, tell the data
// system to fetch them all at once up-front. This may be a no-op
// depending on the data system (e.g. if the segments are all already in
// 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 @@
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
Loading