diff --git a/apps/hello-cpp/main.cpp b/apps/hello-cpp/main.cpp index 0657b01e4..b72f52eb0 100644 --- a/apps/hello-cpp/main.cpp +++ b/apps/hello-cpp/main.cpp @@ -22,7 +22,7 @@ using launchdarkly::client_side::flag_manager::detail::FlagManager; using launchdarkly::client_side::flag_manager::detail::FlagUpdater; int main() { - Logger logger(std::make_unique(LogLevel::kDebug, "Hello")); + Logger logger(std::make_unique("Hello")); net::io_context ioc; @@ -45,6 +45,8 @@ int main() { std::chrono::seconds{30})) .WithReasons(true) .UseReport(true)) + .Events(launchdarkly::client_side::EventsBuilder().FlushInterval( + std::chrono::seconds(5))) .Build() .value(), ContextBuilder().kind("user", "ryan").build()); @@ -58,8 +60,9 @@ int main() { client.WaitForReadySync(std::chrono::seconds(30)); - auto value = client.BoolVariation("my-boolean-flag", false); - LD_LOG(logger, LogLevel::kInfo) << "Value was: " << value; + auto value = client.BoolVariationDetail("my-bool-flag", false); + LD_LOG(logger, LogLevel::kInfo) << "Value was: " << *value; + LD_LOG(logger, LogLevel::kInfo) << "Reason was: " << value.Reason(); // Sit around. std::cout << "Press enter to exit" << std::endl; diff --git a/libs/client-sdk/include/launchdarkly/client_side/api.hpp b/libs/client-sdk/include/launchdarkly/client_side/api.hpp index 4e9c8291f..e57029889 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/api.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/api.hpp @@ -7,10 +7,11 @@ #include #include #include - #include +#include #include "config/client.hpp" #include "context.hpp" +#include "data/evaluation_detail.hpp" #include "error.hpp" #include "launchdarkly/client_side/data_source.hpp" #include "launchdarkly/client_side/data_sources/detail/data_source_status_manager.hpp" @@ -30,6 +31,8 @@ class Client { Client& operator=(Client) = delete; Client& operator=(Client&& other) = delete; + bool Initialized() const; + using FlagKey = std::string; [[nodiscard]] std::unordered_map AllFlags() const; @@ -45,14 +48,30 @@ class Client { bool BoolVariation(FlagKey const& key, bool default_value); + EvaluationDetail BoolVariationDetail(FlagKey const& key, + bool default_value); + std::string StringVariation(FlagKey const& key, std::string default_value); + EvaluationDetail StringVariationDetail( + FlagKey const& key, + std::string default_value); + double DoubleVariation(FlagKey const& key, double default_value); + EvaluationDetail DoubleVariationDetail(FlagKey const& key, + double default_value); + int IntVariation(FlagKey const& key, int default_value); + EvaluationDetail IntVariationDetail(FlagKey const& key, + int default_value); + Value JsonVariation(FlagKey const& key, Value default_value); + EvaluationDetail JsonVariationDetail(FlagKey const& key, + Value default_value); + data_sources::IDataSourceStatusProvider& DataSourceStatus(); void WaitForReadySync(std::chrono::seconds timeout); @@ -60,13 +79,17 @@ class Client { ~Client(); private: - Value VariationInternal(FlagKey const& key, Value default_value); + template + [[nodiscard]] EvaluationDetail VariationInternal(FlagKey const& key, + Value default_value, + bool check_type, + bool detailed); void TrackInternal(std::string event_name, std::optional data, std::optional metric_value); bool initialized_; - std::mutex init_mutex_; + mutable std::mutex init_mutex_; std::condition_variable init_waiter_; data_sources::detail::DataSourceStatusManager status_manager_; @@ -80,6 +103,8 @@ class Client { std::unique_ptr event_processor_; std::unique_ptr data_source_; std::thread run_thread_; + + bool eval_reasons_available_; }; } // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/api.cpp b/libs/client-sdk/src/api.cpp index 7e331ceeb..ef7133fd9 100644 --- a/libs/client-sdk/src/api.cpp +++ b/libs/client-sdk/src/api.cpp @@ -41,7 +41,8 @@ Client::Client(Config config, Context context) flag_updater_, status_manager_, logger_)), - initialized_(false) { + initialized_(false), + eval_reasons_available_(config.DataSourceConfig().with_reasons) { if (config.Events().Enabled()) { event_processor_ = std::make_unique( ioc_.get_executor(), config, logger_); @@ -67,6 +68,11 @@ Client::Client(Config config, Context context) run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); } +bool Client::Initialized() const { + std::unique_lock lock(init_mutex_); + return initialized_; +} + std::unordered_map Client::AllFlags() const { return {}; } @@ -100,34 +106,150 @@ void Client::AsyncIdentify(Context context) { std::chrono::system_clock::now(), std::move(context)}); } -Value Client::VariationInternal(FlagKey const& key, Value default_value) { - auto res = flag_manager_.Get(key); - if (res && res->flag) { - return res->flag->detail().value(); +// TODO(cwaldren): refactor VariationInternal so it isn't so long and mixing up +// multiple concerns. +template +EvaluationDetail Client::VariationInternal(FlagKey const& key, + Value default_value, + bool check_type, + bool detailed) { + auto desc = flag_manager_.Get(key); + + events::client::FeatureEventParams event = { + std::chrono::system_clock::now(), + key, + context_, + default_value, + default_value, + std::nullopt, + std::nullopt, + std::nullopt, + false, + std::nullopt, + }; + + if (!desc || !desc->flag) { + if (!Initialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "LaunchDarkly client has not yet been initialized. " + "Returning default value"; + + // TODO: SC-199918 + auto error_reason = EvaluationReason("CLIENT_NOT_READY"); + if (eval_reasons_available_) { + event.reason = error_reason; + } + event_processor_->AsyncSend(std::move(event)); + return EvaluationDetail(default_value, std::nullopt, + std::move(error_reason)); + } + + LD_LOG(logger_, LogLevel::kInfo) + << "Unknown feature flag " << key << "; returning default value"; + + auto error_reason = EvaluationReason("FLAG_NOT_FOUND"); + if (eval_reasons_available_) { + event.reason = error_reason; + } + event_processor_->AsyncSend(std::move(event)); + return EvaluationDetail(default_value, std::nullopt, + std::move(error_reason)); + + } else if (!Initialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "LaunchDarkly client has not yet been initialized. " + "Returning cached value"; + } + + assert(desc->flag); + + auto const& flag = *(desc->flag); + auto const& detail = flag.detail(); + + if (check_type && default_value.type() != Value::Type::kNull && + detail.value().type() != default_value.type()) { + auto error_reason = EvaluationReason("WRONG_TYPE"); + if (eval_reasons_available_) { + event.reason = error_reason; + } + event_processor_->AsyncSend(std::move(event)); + return EvaluationDetail(default_value, std::nullopt, error_reason); + } + + event.value = detail.value(); + event.variation = detail.variation_index(); + + if (detailed || flag.track_reason()) { + event.reason = detail.reason(); + } + + event.version = flag.flag_version().value_or(flag.version()); + event.require_full_event = flag.track_events(); + if (auto date = flag.debug_events_until_date()) { + event.debug_events_until_date = events::Date{*date}; } - return default_value; + + event_processor_->AsyncSend(std::move(event)); + + // TODO: this isn't a valid error, figure out how to handle if reason is + // missing. + EvaluationReason returned_reason("UNKNOWN"); + if (detail.reason()) { + returned_reason = detail.reason()->get(); + } + return EvaluationDetail(detail.value(), detail.variation_index(), + returned_reason); +} + +EvaluationDetail Client::BoolVariationDetail(Client::FlagKey const& key, + bool default_value) { + return VariationInternal(key, default_value, true, true); } bool Client::BoolVariation(Client::FlagKey const& key, bool default_value) { - return VariationInternal(key, default_value).as_bool(); + return *VariationInternal(key, default_value, true, false); +} + +EvaluationDetail Client::StringVariationDetail( + Client::FlagKey const& key, + std::string default_value) { + return VariationInternal(key, std::move(default_value), true, + true); } std::string Client::StringVariation(Client::FlagKey const& key, std::string default_value) { - return VariationInternal(key, std::move(default_value)).as_string(); + return *VariationInternal(key, std::move(default_value), true, + false); +} + +EvaluationDetail Client::DoubleVariationDetail( + Client::FlagKey const& key, + double default_value) { + return VariationInternal(key, default_value, true, true); } double Client::DoubleVariation(Client::FlagKey const& key, double default_value) { - return VariationInternal(key, default_value).as_double(); + return *VariationInternal(key, default_value, true, false); } +EvaluationDetail Client::IntVariationDetail(Client::FlagKey const& key, + int default_value) { + return VariationInternal(key, default_value, true, true); +} int Client::IntVariation(Client::FlagKey const& key, int default_value) { - return VariationInternal(key, default_value).as_int(); + return *VariationInternal(key, default_value, true, false); +} + +EvaluationDetail Client::JsonVariationDetail(Client::FlagKey const& key, + Value default_value) { + return VariationInternal(key, std::move(default_value), false, true); } Value Client::JsonVariation(Client::FlagKey const& key, Value default_value) { - return VariationInternal(key, std::move(default_value)); + return *VariationInternal(key, std::move(default_value), false, + false); } data_sources::IDataSourceStatusProvider& Client::DataSourceStatus() { diff --git a/libs/client-sdk/tests/client_test.cpp b/libs/client-sdk/tests/client_test.cpp index 447dab874..853feb95e 100644 --- a/libs/client-sdk/tests/client_test.cpp +++ b/libs/client-sdk/tests/client_test.cpp @@ -1,19 +1,82 @@ #include #include +#include #include "context_builder.hpp" using namespace launchdarkly; +using namespace launchdarkly::client_side; -TEST(ClientTest, ConstructClientWithConfig) { - tl::expected config = - client_side::ConfigBuilder("sdk-123").Build(); - +TEST(ClientTest, ClientConstructedWithMinimalConfigAndContext) { + tl::expected config = ConfigBuilder("sdk-123").Build(); ASSERT_TRUE(config); - auto context = ContextBuilder().kind("cat", "shadow").build(); + Context context = ContextBuilder().kind("cat", "shadow").build(); + + Client client(std::move(*config), context); +} - client_side::Client client(std::move(*config), context); +TEST(ClientTest, AllFlagsIsEmpty) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().kind("cat", "shadow").build()); ASSERT_TRUE(client.AllFlags().empty()); - ASSERT_TRUE(client.BoolVariation("cat-food", true)); +} + +TEST(ClientTest, BoolVariationDefaultPassesThrough) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().kind("cat", "shadow").build()); + + const std::string flag = "extra-cat-food"; + std::vector values = {true, false}; + for (auto const& v : values) { + ASSERT_EQ(client.BoolVariation(flag, v), v); + ASSERT_EQ(*client.BoolVariationDetail(flag, v), v); + } +} + +TEST(ClientTest, StringVariationDefaultPassesThrough) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().kind("cat", "shadow").build()); + const std::string flag = "treat"; + std::vector values = {"chicken", "fish", "cat-grass"}; + for (auto const& v : values) { + ASSERT_EQ(client.StringVariation(flag, v), v); + ASSERT_EQ(*client.StringVariationDetail(flag, v), v); + } +} + +TEST(ClientTest, IntVariationDefaultPassesThrough) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().kind("cat", "shadow").build()); + const std::string flag = "weight"; + std::vector values = {0, 12, 13, 24, 1000}; + for (auto const& v : values) { + ASSERT_EQ(client.IntVariation("weight", v), v); + ASSERT_EQ(*client.IntVariationDetail("weight", v), v); + } +} + +TEST(ClientTest, DoubleVariationDefaultPassesThrough) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().kind("cat", "shadow").build()); + const std::string flag = "weight"; + std::vector values = {0.0, 12.0, 13.0, 24.0, 1000.0}; + for (auto const& v : values) { + ASSERT_EQ(client.DoubleVariation(flag, v), v); + ASSERT_EQ(*client.DoubleVariationDetail(flag, v), v); + } +} + +TEST(ClientTest, JsonVariationDefaultPassesThrough) { + Client client(ConfigBuilder("sdk-123").Build().value(), + ContextBuilder().kind("cat", "shadow").build()); + + const std::string flag = "assorted-values"; + std::vector values = { + Value({"running", "jumping"}), Value(3), Value(1.0), Value(true), + Value(std::map{{"weight", 20}})}; + for (auto const& v : values) { + ASSERT_EQ(client.JsonVariation(flag, v), v); + ASSERT_EQ(*client.JsonVariationDetail(flag, v), v); + } } diff --git a/libs/common/include/data/evaluation_detail.hpp b/libs/common/include/data/evaluation_detail.hpp new file mode 100644 index 000000000..62be5b723 --- /dev/null +++ b/libs/common/include/data/evaluation_detail.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +#include "data/evaluation_reason.hpp" + +namespace launchdarkly { + +/** + * EvaluationDetail contains additional metadata related to a feature flag + * evaluation. To obtain an instance of EvaluationDetail, use a variation method + * suffixed with Detail, such as BoolVariationDetail. + * @tparam T The primitive variation value, which is limited to + * bool, int, double, std::string, and launchdarkly::Value. + */ +template +class EvaluationDetail { + public: + /** + * Constructs an EvaluationDetail from results of an evaluation. + * @param value The variation value. + * @param variation_index The variation index. + * @param reason The reason for the results. + */ + EvaluationDetail(T value, + std::optional variation_index, + EvaluationReason reason); + + /** + * Constructs an EvaluationDetail representing an error and a default + * value. + * @param error_kind Kind of the error. + * @param default_value Default value. + */ + EvaluationDetail(std::string error_kind, T default_value); + + /** + * @return A reference to the variation value. For convenience, the * + * operator may also be used to obtain the value. + */ + [[nodiscard]] T const& Value() const; + + /** + * @return A variation index, if this was a successful evaluation; + * otherwise, std::nullopt. + */ + [[nodiscard]] std::optional VariationIndex() const; + + /** + * @return A reference to the reason for the results. + */ + [[nodiscard]] EvaluationReason const& Reason() const; + + /** + * @return A reference to the variation value. + */ + T const& operator*() const; + + private: + T value_; + std::optional variation_index_; + EvaluationReason reason_; +}; +} // namespace launchdarkly diff --git a/libs/common/include/data/evaluation_reason.hpp b/libs/common/include/data/evaluation_reason.hpp index 9721a5617..e43bd2f1a 100644 --- a/libs/common/include/data/evaluation_reason.hpp +++ b/libs/common/include/data/evaluation_reason.hpp @@ -86,6 +86,8 @@ class EvaluationReason { bool in_experiment, std::optional big_segment_status); + explicit EvaluationReason(std::string error_kind); + friend std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason); diff --git a/libs/common/include/events/client_events.hpp b/libs/common/include/events/client_events.hpp index a4d3ae292..4f3b1b2e9 100644 --- a/libs/common/include/events/client_events.hpp +++ b/libs/common/include/events/client_events.hpp @@ -18,14 +18,19 @@ struct FeatureEventParams { Date creation_date; std::string key; Context context; - EvaluationResult eval_result; + Value value; Value default_; + std::optional version; + std::optional variation; + std::optional reason; + bool require_full_event; + std::optional debug_events_until_date; }; struct FeatureEventBase { Date creation_date; std::string key; - Version version; + std::optional version; std::optional variation; Value value; std::optional reason; diff --git a/libs/common/include/events/detail/request_worker.hpp b/libs/common/include/events/detail/request_worker.hpp index b8c34b344..ebca415bf 100644 --- a/libs/common/include/events/detail/request_worker.hpp +++ b/libs/common/include/events/detail/request_worker.hpp @@ -92,10 +92,12 @@ class RequestWorker { * operations. * @param retry_after How long to wait after a recoverable failure before * trying to deliver events again. + * @param id Unique identifier for the flush worker (used for logging). * @param logger Logger. */ RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, + std::size_t id, Logger& logger); /** @@ -128,7 +130,7 @@ class RequestWorker { batch_ = std::move(batch); LD_LOG(logger_, LogLevel::kDebug) - << "posting " << batch_->Count() << " events(s) to " + << tag_ << "posting " << batch_->Count() << " events(s) to " << batch_->Target() << " with payload: " << batch_->Request().Body().value_or("(no body)"); @@ -158,6 +160,9 @@ class RequestWorker { * request is in-flight or a retry is taking place. */ std::optional batch_; + /* Tag used in logs. */ + std::string tag_; + Logger& logger_; void OnDeliveryAttempt(network::detail::HttpResult request, diff --git a/libs/common/include/events/detail/summarizer.hpp b/libs/common/include/events/detail/summarizer.hpp index 163bf9f73..d1ee27393 100644 --- a/libs/common/include/events/detail/summarizer.hpp +++ b/libs/common/include/events/detail/summarizer.hpp @@ -76,7 +76,8 @@ class Summarizer { std::optional variation; VariationKey(); - VariationKey(Version version, std::optional variation); + VariationKey(std::optional version, + std::optional variation); bool operator==(VariationKey const& k) const { return k.variation == variation && k.version == version; diff --git a/libs/common/include/value.hpp b/libs/common/include/value.hpp index 3d2823491..641b4fc85 100644 --- a/libs/common/include/value.hpp +++ b/libs/common/include/value.hpp @@ -413,6 +413,14 @@ class Value final { return out; } + operator bool() const { return as_bool(); } + + operator std::string() const { return as_string(); } + + operator double() const { return as_double(); } + + operator int() const { return as_int(); } + private: boost::variant storage_; Type type_; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 580948925..a30d091e9 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -26,6 +26,7 @@ add_library(${LIBNAME} config/config.cpp data/evaluation_reason.cpp data/evaluation_detail_internal.cpp + data/evaluation_detail.cpp data/evaluation_result.cpp context_filter.cpp config/app_info_builder.cpp diff --git a/libs/common/src/data/evaluation_detail.cpp b/libs/common/src/data/evaluation_detail.cpp new file mode 100644 index 000000000..f5aafcd30 --- /dev/null +++ b/libs/common/src/data/evaluation_detail.cpp @@ -0,0 +1,47 @@ +#include "data/evaluation_detail.hpp" +#include +#include "value.hpp" + +namespace launchdarkly { + +template +EvaluationDetail::EvaluationDetail( + T value, + std::optional variation_index, + launchdarkly::EvaluationReason reason) + : value_(std::move(value)), + variation_index_(variation_index), + reason_(std::move(reason)) {} + +template +EvaluationDetail::EvaluationDetail(std::string error_kind, T default_value) + : value_(std::move(default_value)), + variation_index_(std::nullopt), + reason_(error_kind) {} + +template +T const& EvaluationDetail::Value() const { + return value_; +} + +template +EvaluationReason const& EvaluationDetail::Reason() const { + return reason_; +} + +template +std::optional EvaluationDetail::VariationIndex() const { + return variation_index_; +} +template +T const& EvaluationDetail::operator*() const { + return value_; +} + +template class EvaluationDetail; +template class EvaluationDetail; +template class EvaluationDetail; +template class EvaluationDetail; +template class EvaluationDetail; + +} // namespace launchdarkly diff --git a/libs/common/src/data/evaluation_reason.cpp b/libs/common/src/data/evaluation_reason.cpp index 3c3c9259c..1d08381c5 100644 --- a/libs/common/src/data/evaluation_reason.cpp +++ b/libs/common/src/data/evaluation_reason.cpp @@ -46,6 +46,15 @@ EvaluationReason::EvaluationReason( in_experiment_(in_experiment), big_segment_status_(std::move(big_segment_status)) {} +EvaluationReason::EvaluationReason(std::string error_kind) + : EvaluationReason("ERROR", + error_kind, + std::nullopt, + std::nullopt, + std::nullopt, + false, + std::nullopt) {} + std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason) { out << "{"; out << " kind: " << reason.kind_; diff --git a/libs/common/src/events/asio_event_processor.cpp b/libs/common/src/events/asio_event_processor.cpp index cf2b30e54..f828cdf93 100644 --- a/libs/common/src/events/asio_event_processor.cpp +++ b/libs/common/src/events/asio_event_processor.cpp @@ -188,14 +188,15 @@ void AsioEventProcessor::AsyncClose() { template std::optional AsioEventProcessor::CreateBatch() { - if (outbox_.Empty()) { - return std::nullopt; - } - auto events = boost::json::value_from(outbox_.Consume()); - if (!summarizer_.Finish(std::chrono::system_clock::now()).Empty()) { + bool has_summary = + !summarizer_.Finish(std::chrono::system_clock::now()).Empty(); + + if (has_summary) { events.as_array().push_back(boost::json::value_from(summarizer_)); + } else if (outbox_.Empty()) { + return std::nullopt; } // TODO(cwaldren): Template the event processor over SDK type? Add it into @@ -219,14 +220,13 @@ std::vector AsioEventProcessor::Process( overloaded{[&](client::FeatureEventParams&& event) { summarizer_.Update(event); - if (!event.eval_result.track_events()) { + if (!event.require_full_event) { return; } client::FeatureEventBase base{event}; - auto debug_until_date = - event.eval_result.debug_events_until_date(); + auto debug_until_date = event.debug_events_until_date; auto max_time = std::max( std::chrono::system_clock::now(), @@ -234,8 +234,7 @@ std::vector AsioEventProcessor::Process( std::chrono::system_clock::time_point{})); bool emit_debug_event = - debug_until_date && - debug_until_date.value() > max_time; + debug_until_date && debug_until_date->t > max_time; if (emit_debug_event) { out.emplace_back(client::DebugEvent{ diff --git a/libs/common/src/events/client_events.cpp b/libs/common/src/events/client_events.cpp index 5b1c4a898..7756e50db 100644 --- a/libs/common/src/events/client_events.cpp +++ b/libs/common/src/events/client_events.cpp @@ -4,15 +4,10 @@ namespace launchdarkly::events::client { FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) : creation_date(params.creation_date), key(params.key), - version(params.eval_result.version()), - variation(params.eval_result.detail().variation_index()), - value(params.eval_result.detail().value()), - default_(params.default_) { - // TODO(cwaldren): should also add the reason if the - // variation method was VariationDetail(). - if (params.eval_result.track_reason()) { - reason = params.eval_result.detail().reason(); - } -} + version(params.version), + variation(params.variation), + value(params.value), + reason(params.reason), + default_(params.default_) {} } // namespace launchdarkly::events::client diff --git a/libs/common/src/events/request_worker.cpp b/libs/common/src/events/request_worker.cpp index 3f5cc63b4..efefbc224 100644 --- a/libs/common/src/events/request_worker.cpp +++ b/libs/common/src/events/request_worker.cpp @@ -5,12 +5,14 @@ namespace launchdarkly::events::detail { RequestWorker::RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, + std::size_t id, Logger& logger) : timer_(io), retry_delay_(retry_after), state_(State::Idle), requester_(timer_.get_executor()), batch_(std::nullopt), + tag_("flush-worker[" + std::to_string(id) + "]: "), logger_(logger) {} bool RequestWorker::Available() const { @@ -49,8 +51,8 @@ void RequestWorker::OnDeliveryAttempt(network::detail::HttpResult result, ResultCallback callback) { auto [next_state, action] = NextState(state_, result); - LD_LOG(logger_, LogLevel::kDebug) << "flush-worker: " << state_ << " -> " - << next_state << ", " << action << ""; + LD_LOG(logger_, LogLevel::kDebug) + << tag_ << state_ << " -> " << next_state << ", " << action << ""; switch (action) { case Action::None: @@ -58,12 +60,12 @@ void RequestWorker::OnDeliveryAttempt(network::detail::HttpResult result, case Action::Reset: if (result.IsError()) { LD_LOG(logger_, LogLevel::kWarn) - << "error posting " << batch_->Count() + << tag_ << "error posting " << batch_->Count() << " event(s) (some events were dropped): " << result.ErrorMessage().value_or("unknown IO error"); } else { LD_LOG(logger_, LogLevel::kWarn) - << "error posting " << batch_->Count() + << tag_ << "error posting " << batch_->Count() << " event(s) (some events were dropped): " "HTTP error " << result.Status(); @@ -72,7 +74,7 @@ void RequestWorker::OnDeliveryAttempt(network::detail::HttpResult result, break; case Action::NotifyPermanentFailure: LD_LOG(logger_, LogLevel::kWarn) - << "error posting " << batch_->Count() + << tag_ << "error posting " << batch_->Count() << " event(s) (giving up permanently): HTTP error " << result.Status(); callback(batch_->Count(), result.Status()); @@ -92,12 +94,12 @@ void RequestWorker::OnDeliveryAttempt(network::detail::HttpResult result, case Action::Retry: if (result.IsError()) { LD_LOG(logger_, LogLevel::kWarn) - << "error posting " << batch_->Count() + << tag_ << "error posting " << batch_->Count() << " event(s) (will retry): " << result.ErrorMessage().value_or("unknown IO error"); } else { LD_LOG(logger_, LogLevel::kWarn) - << "error posting " << batch_->Count() + << tag_ << "error posting " << batch_->Count() << " event(s) (will retry): HTTP error " << result.Status(); } timer_.expires_from_now(retry_delay_); diff --git a/libs/common/src/events/summarizer.cpp b/libs/common/src/events/summarizer.cpp index fe169e311..82f3dc522 100644 --- a/libs/common/src/events/summarizer.cpp +++ b/libs/common/src/events/summarizer.cpp @@ -14,14 +14,6 @@ Summarizer::Features() const { return features_; } -static bool FlagNotFound(client::FeatureEventParams const& event) { - if (auto reason = event.eval_result.detail().reason()) { - return reason->get().kind() == "ERROR" && - reason->get().error_kind() == "FLAG_NOT_FOUND"; - } - return false; -} - void Summarizer::Update(client::FeatureEventParams const& event) { auto const& kinds = event.context.kinds(); @@ -31,24 +23,11 @@ void Summarizer::Update(client::FeatureEventParams const& event) { feature_state_iterator->second.context_kinds.insert(kinds.begin(), kinds.end()); - decltype(std::begin( - feature_state_iterator->second.counters)) summary_counter; - - if (FlagNotFound(event)) { - summary_counter = - feature_state_iterator->second.counters - .try_emplace(VariationKey(), - feature_state_iterator->second.default_) - .first; - - } else { - auto key = VariationKey(event.eval_result.version(), - event.eval_result.detail().variation_index()); - summary_counter = - feature_state_iterator->second.counters - .try_emplace(key, event.eval_result.detail().value()) - .first; - } + auto summary_counter = + feature_state_iterator->second.counters + .try_emplace(VariationKey(event.version, event.variation), + event.value) + .first; summary_counter->second.Increment(); } @@ -66,7 +45,7 @@ Summarizer::Time Summarizer::end_time() const { return end_time_; } -Summarizer::VariationKey::VariationKey(Version version, +Summarizer::VariationKey::VariationKey(std::optional version, std::optional variation) : version(version), variation(variation) {} diff --git a/libs/common/src/events/worker_pool.cpp b/libs/common/src/events/worker_pool.cpp index 55682be71..66506cb52 100644 --- a/libs/common/src/events/worker_pool.cpp +++ b/libs/common/src/events/worker_pool.cpp @@ -10,8 +10,8 @@ WorkerPool::WorkerPool(boost::asio::any_io_executor io, Logger& logger) : io_(io), workers_() { for (std::size_t i = 0; i < pool_size; i++) { - workers_.emplace_back( - std::make_unique(io_, delivery_retry_delay, logger)); + workers_.emplace_back(std::make_unique( + io_, delivery_retry_delay, i, logger)); } } diff --git a/libs/common/src/serialization/events/json_events.cpp b/libs/common/src/serialization/events/json_events.cpp index 50f32350a..2a0470bbe 100644 --- a/libs/common/src/serialization/events/json_events.cpp +++ b/libs/common/src/serialization/events/json_events.cpp @@ -29,7 +29,9 @@ void tag_invoke(boost::json::value_from_tag const& tag, auto& obj = json_value.emplace_object(); obj.emplace("creationDate", boost::json::value_from(event.creation_date)); obj.emplace("key", event.key); - obj.emplace("version", event.version); + if (event.version) { + obj.emplace("version", *event.version); + } if (event.variation) { obj.emplace("variation", *event.variation); } diff --git a/libs/common/tests/event_serialization_test.cpp b/libs/common/tests/event_serialization_test.cpp index 90de09e08..3d6a77695 100644 --- a/libs/common/tests/event_serialization_test.cpp +++ b/libs/common/tests/event_serialization_test.cpp @@ -18,13 +18,14 @@ TEST(EventSerialization, FeatureEvent) { creation_date, "key", ContextBuilder().kind("foo", "bar").build(), - EvaluationResult( - 1, std::nullopt, false, false, std::nullopt, - EvaluationDetailInternal( - Value(42), 2, - Reason("error", std::nullopt, std::nullopt, std::nullopt, - std::nullopt, false, std::nullopt))), - 3, + Value(42), + Value(3), + 1, + 2, + std::nullopt, + false, + std::nullopt, + }), std::map{{"foo", "bar"}}}; @@ -42,18 +43,20 @@ TEST(EventSerialization, DebugEvent) { ContextFilter filter(false, attrs); auto context = ContextBuilder().kind("foo", "bar").build(); auto event = events::client::DebugEvent{ - client::FeatureEventBase(client::FeatureEventParams{ - creation_date, - "key", - context, - EvaluationResult( - 1, std::nullopt, false, false, std::nullopt, - EvaluationDetailInternal( - Value(42), 2, - Reason("error", std::nullopt, std::nullopt, std::nullopt, - std::nullopt, false, std::nullopt))), - 3, - }), + client::FeatureEventBase( + client::FeatureEventBase(client::FeatureEventParams{ + creation_date, + "key", + ContextBuilder().kind("foo", "bar").build(), + Value(42), + Value(3), + 1, + 2, + std::nullopt, + false, + std::nullopt, + + })), filter.filter(context)}; auto event_json = boost::json::value_from(event); diff --git a/libs/common/tests/event_summarizer_test.cpp b/libs/common/tests/event_summarizer_test.cpp index a39acb72c..cf07cd5fb 100644 --- a/libs/common/tests/event_summarizer_test.cpp +++ b/libs/common/tests/event_summarizer_test.cpp @@ -74,14 +74,15 @@ static FeatureEventParams FeatureEventFromParams(EvaluationParams params, TimeZero(), params.feature_key, std::move(context), - launchdarkly::EvaluationResult( - params.feature_version, std::nullopt, false, false, std::nullopt, - launchdarkly::EvaluationDetailInternal( - params.feature_value, params.feature_variation, - launchdarkly::EvaluationReason( - "FALLTHROUGH", std::nullopt, std::nullopt, std::nullopt, - std::nullopt, false, std::nullopt))), + params.feature_value, params.feature_default, + params.feature_version, + params.feature_variation, + launchdarkly::EvaluationReason("FALLTHROUGH", std::nullopt, + std::nullopt, std::nullopt, std::nullopt, + false, std::nullopt), + false, + std::nullopt, }; } @@ -303,14 +304,16 @@ TEST(SummarizerTests, MissingFlagCreatesCounterUsingDefaultValue) { TimeZero(), feature_key, context, - EvaluationResult( - 0, std::nullopt, false, false, std::nullopt, - EvaluationDetailInternal( - Value(), std::nullopt, - EvaluationReason("ERROR", "FLAG_NOT_FOUND", std::nullopt, - std::nullopt, std::nullopt, false, - std::nullopt))), feature_default, + feature_default, + std::nullopt, + std::nullopt, + + EvaluationReason("ERROR", "FLAG_NOT_FOUND", std::nullopt, std::nullopt, + std::nullopt, false, std::nullopt), + + false, + std::nullopt, }; summarizer.Update(event); diff --git a/libs/common/tests/value_test.cpp b/libs/common/tests/value_test.cpp index cba2e6f25..7923d9786 100644 --- a/libs/common/tests/value_test.cpp +++ b/libs/common/tests/value_test.cpp @@ -269,6 +269,14 @@ TEST(ValueTests, ToBoostJson) { EXPECT_EQ("ham", boost_arr.as_array().at(3).as_object().at("string")); } +TEST(ValueTests, ConversionOperators) { + EXPECT_TRUE(Value(true)); + EXPECT_FALSE(Value(false)); + EXPECT_EQ("potato", static_cast(Value("potato"))); + EXPECT_EQ(3.14, static_cast(Value(3.14))); + EXPECT_EQ(1, static_cast(Value(1))); +} + TEST(ValueTests, ArrayEquality) { std::vector arrays = { {"foo", "bar", "baz"}, diff --git a/libs/server-sent-events/CMakeLists.txt b/libs/server-sent-events/CMakeLists.txt index 127de39bd..a7a77548a 100644 --- a/libs/server-sent-events/CMakeLists.txt +++ b/libs/server-sent-events/CMakeLists.txt @@ -29,7 +29,6 @@ include(FetchContent) set(OPENSSL_USE_STATIC_LIBS OFF) -set(OPENSSL_ROOT_DIR "/opt/homebrew/opt/openssl@1.1") find_package(OpenSSL REQUIRED) message(STATUS "LaunchDarkly: using OpenSSL v${OPENSSL_VERSION}") @@ -43,7 +42,6 @@ message(STATUS "LaunchDarkly: using Boost v${Boost_VERSION}") include(${CMAKE_FILES}/certify.cmake) - add_subdirectory(src) if (BUILD_TESTING)