From c417579d3643dbe2f20396bf9d527d83d15fd6eb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 12 Jun 2023 10:54:33 -0700 Subject: [PATCH 01/56] feat: add server-sdk subdirectory --- CMakeLists.txt | 1 + libs/server-sdk/CMakeLists.txt | 35 ++++++++++++++++++++ libs/server-sdk/src/CMakeLists.txt | 48 ++++++++++++++++++++++++++++ libs/server-sdk/src/boost.cpp | 5 +++ libs/server-sdk/tests/CMakeLists.txt | 19 +++++++++++ 5 files changed, 108 insertions(+) create mode 100644 libs/server-sdk/CMakeLists.txt create mode 100644 libs/server-sdk/src/CMakeLists.txt create mode 100644 libs/server-sdk/src/boost.cpp create mode 100644 libs/server-sdk/tests/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f732e7f4..6a58799ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,6 +71,7 @@ find_package(Boost 1.80 REQUIRED COMPONENTS json url coroutine) message(STATUS "LaunchDarkly: using Boost v${Boost_VERSION}") add_subdirectory(libs/client-sdk) +add_subdirectory(libs/server-sdk) set(ORIGINAL_BUILD_SHARED_LIBS "${BUILD_SHARED_LIBS}") set(BUILD_SHARED_LIBS OFF) diff --git a/libs/server-sdk/CMakeLists.txt b/libs/server-sdk/CMakeLists.txt new file mode 100644 index 000000000..36bd7b6d0 --- /dev/null +++ b/libs/server-sdk/CMakeLists.txt @@ -0,0 +1,35 @@ +# This project aims to follow modern cmake guidelines, e.g. +# https://cliutils.gitlab.io/modern-cmake + +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCPPServer + VERSION 0.1 + DESCRIPTION "LaunchDarkly C++ Server SDK" + LANGUAGES CXX C +) + +set(LIBNAME "launchdarkly-cpp-server") + +# If this project is the main CMake project (as opposed to being included via add_subdirectory) +if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + # Disable C++ extensions for portability. + set(CMAKE_CXX_EXTENSIONS OFF) + # Enable folder support in IDEs. + set_property(GLOBAL PROPERTY USE_FOLDERS ON) +endif () + +#set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +#set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_FILES}) + +# Needed to fetch external dependencies. +include(FetchContent) + +# Add main SDK sources. +add_subdirectory(src) + +if (BUILD_TESTING) + add_subdirectory(tests) +endif () diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt new file mode 100644 index 000000000..b229d78a5 --- /dev/null +++ b/libs/server-sdk/src/CMakeLists.txt @@ -0,0 +1,48 @@ + +file(GLOB HEADER_LIST CONFIGURE_DEPENDS + "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" + ) + +# Automatic library: static or dynamic based on user config. + +add_library(${LIBNAME} + ${HEADER_LIST}) + +if (MSVC OR (NOT BUILD_SHARED_LIBS)) + target_link_libraries(${LIBNAME} + PUBLIC launchdarkly::common + PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) +else () + # The default static lib builds, for linux, are positition independent. + # So they do not link into a shared object without issues. So, when + # building shared objects do not link the static libraries and instead + # use the "src.hpp" files for required libraries. + # macOS shares the same path for simplicity. + target_link_libraries(${LIBNAME} + PUBLIC launchdarkly::common + PRIVATE Boost::headers launchdarkly::sse launchdarkly::internal foxy) + + target_sources(${LIBNAME} PRIVATE boost.cpp) +endif () + +add_library(launchdarkly::server ALIAS ${LIBNAME}) + +set_property(TARGET ${LIBNAME} PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + +install(TARGETS ${LIBNAME}) +if (BUILD_SHARED_LIBS AND MSVC) + install(FILES $ DESTINATION bin OPTIONAL) +endif () +# Using PUBLIC_HEADERS would flatten the include. +# This will preserve it, but dependencies must do the same. + +install(DIRECTORY "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly" + DESTINATION "include" + ) + +# Need the public headers to build. +target_include_directories(${LIBNAME} PUBLIC ../include) + +# Minimum C++ standard needed for consuming the public API is C++17. +target_compile_features(${LIBNAME} PUBLIC cxx_std_17) diff --git a/libs/server-sdk/src/boost.cpp b/libs/server-sdk/src/boost.cpp new file mode 100644 index 000000000..5c9ea02b1 --- /dev/null +++ b/libs/server-sdk/src/boost.cpp @@ -0,0 +1,5 @@ +// This file is used to include boost url/json when building a shared library on linux/mac. +// Windows links static libs in this case and does not include these src files, as there +// are issues compiling the value.ipp file from JSON with MSVC. +#include +#include diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt new file mode 100644 index 000000000..d22b1d918 --- /dev/null +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 3.10) +include(GoogleTest) + +include_directories("${PROJECT_SOURCE_DIR}/include") +include_directories("${PROJECT_SOURCE_DIR}/src") + +file(GLOB tests "${PROJECT_SOURCE_DIR}/tests/*.cpp") + +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +# Get things in the same directory on windows. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") + +add_executable(gtest_${LIBNAME} + ${tests}) +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal GTest::gtest_main) + +gtest_discover_tests(gtest_${LIBNAME}) From c985539f541b2527ff73b5722bc91976b0e2e812 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 14 Jun 2023 14:32:26 -0700 Subject: [PATCH 02/56] chore: move ItemDescriptor to internal library (#151) Moves the client's `ItemDescriptor` to internal where it can be shared, and gives it a generic parameter so it can hold flag rules/segments. --- libs/client-sdk/src/CMakeLists.txt | 33 +++--- libs/client-sdk/src/client_impl.cpp | 10 +- .../data_source_event_handler.cpp | 3 +- .../data_sources/data_source_update_sink.cpp | 23 ---- .../data_sources/data_source_update_sink.hpp | 32 +----- .../src/flag_manager/flag_persistence.cpp | 10 +- .../src/flag_manager/flag_store.cpp | 1 - .../src/flag_manager/flag_updater.cpp | 24 ++-- .../src/serialization/json_all_flags.cpp | 54 --------- .../src/serialization/json_all_flags.hpp | 27 ----- .../tests/flag_persistence_test.cpp | 2 +- libs/client-sdk/tests/flag_store_test.cpp | 12 +- libs/client-sdk/tests/flag_updater_test.cpp | 16 +-- .../launchdarkly/data/evaluation_result.hpp | 5 +- libs/common/src/data/evaluation_result.cpp | 12 +- .../data_kinds/item_descriptor.hpp | 108 ++++++++++++++++++ 16 files changed, 172 insertions(+), 200 deletions(-) delete mode 100644 libs/client-sdk/src/data_sources/data_source_update_sink.cpp delete mode 100644 libs/client-sdk/src/serialization/json_all_flags.cpp delete mode 100644 libs/client-sdk/src/serialization/json_all_flags.hpp create mode 100644 libs/internal/include/launchdarkly/data_kinds/item_descriptor.hpp diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index c18258201..8eba21d76 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -9,7 +9,6 @@ add_library(${LIBNAME} ${HEADER_LIST} data_sources/streaming_data_source.cpp data_sources/data_source_event_handler.cpp - data_sources/data_source_update_sink.cpp data_sources/polling_data_source.cpp flag_manager/flag_store.cpp flag_manager/flag_updater.cpp @@ -37,27 +36,25 @@ add_library(${LIBNAME} bindings/c/sdk.cpp data_sources/null_data_source.cpp flag_manager/context_index.cpp - serialization/json_all_flags.hpp - serialization/json_all_flags.cpp flag_manager/flag_manager.cpp flag_manager/flag_persistence.cpp bindings/c/sdk.cpp) -if(MSVC OR (NOT BUILD_SHARED_LIBS)) - target_link_libraries(${LIBNAME} - PUBLIC launchdarkly::common - PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) -else() - # The default static lib builds, for linux, are positition independent. - # So they do not link into a shared object without issues. So, when - # building shared objects do not link the static libraries and instead - # use the "src.hpp" files for required libraries. - # macOS shares the same path for simplicity. - target_link_libraries(${LIBNAME} - PUBLIC launchdarkly::common - PRIVATE Boost::headers launchdarkly::sse launchdarkly::internal foxy) +if (MSVC OR (NOT BUILD_SHARED_LIBS)) + target_link_libraries(${LIBNAME} + PUBLIC launchdarkly::common + PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) +else () + # The default static lib builds, for linux, are positition independent. + # So they do not link into a shared object without issues. So, when + # building shared objects do not link the static libraries and instead + # use the "src.hpp" files for required libraries. + # macOS shares the same path for simplicity. + target_link_libraries(${LIBNAME} + PUBLIC launchdarkly::common + PRIVATE Boost::headers launchdarkly::sse launchdarkly::internal foxy) - target_sources(${LIBNAME} PRIVATE boost.cpp) + target_sources(${LIBNAME} PRIVATE boost.cpp) endif () add_library(launchdarkly::client ALIAS ${LIBNAME}) @@ -67,7 +64,7 @@ set_property(TARGET ${LIBNAME} PROPERTY install(TARGETS ${LIBNAME}) if (BUILD_SHARED_LIBS AND MSVC) - install(FILES $ DESTINATION bin OPTIONAL) + install(FILES $ DESTINATION bin OPTIONAL) endif () # Using PUBLIC_HEADERS would flatten the include. # This will preserve it, but dependencies must do the same. diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index ed16cbe32..224d71555 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -192,8 +192,8 @@ bool ClientImpl::Initialized() const { std::unordered_map ClientImpl::AllFlags() const { std::unordered_map result; for (auto& [key, descriptor] : flag_manager_.Store().GetAll()) { - if (descriptor->flag) { - result.try_emplace(key, descriptor->flag->Detail().Value()); + if (descriptor->item) { + result.try_emplace(key, descriptor->item->Detail().Value()); } } return result; @@ -247,7 +247,7 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, std::nullopt, }; - if (!desc || !desc->flag) { + if (!desc || !desc->item) { if (!Initialized()) { LD_LOG(logger_, LogLevel::kWarn) << "LaunchDarkly client has not yet been initialized. " @@ -282,9 +282,9 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, "Returning cached value"; } - assert(desc->flag); + assert(desc->item); - auto const& flag = *(desc->flag); + auto const& flag = *(desc->item); auto const& detail = flag.Detail(); if (check_type && default_value.Type() != Value::Type::kNull && diff --git a/libs/client-sdk/src/data_sources/data_source_event_handler.cpp b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp index 7710bddeb..f078f2341 100644 --- a/libs/client-sdk/src/data_sources/data_source_event_handler.cpp +++ b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp @@ -1,5 +1,4 @@ #include "data_source_event_handler.hpp" -#include "../serialization/json_all_flags.hpp" #include #include @@ -144,7 +143,7 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( boost::json::parse(data)); if (res.has_value()) { handler_.Upsert(context_, res.value().key, - ItemDescriptor(res.value().version)); + ItemDescriptor(res.value().version)); return DataSourceEventHandler::MessageStatus::kMessageHandled; } LD_LOG(logger_, LogLevel::kError) << kErrorDeleteInvalid; diff --git a/libs/client-sdk/src/data_sources/data_source_update_sink.cpp b/libs/client-sdk/src/data_sources/data_source_update_sink.cpp deleted file mode 100644 index cff337160..000000000 --- a/libs/client-sdk/src/data_sources/data_source_update_sink.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "data_source_update_sink.hpp" - -namespace launchdarkly::client_side { - -bool operator==(ItemDescriptor const& lhs, ItemDescriptor const& rhs) { - return lhs.version == rhs.version && lhs.flag == rhs.flag; -} - -std::ostream& operator<<(std::ostream& out, ItemDescriptor const& descriptor) { - out << "{"; - out << " version: " << descriptor.version; - if (descriptor.flag.has_value()) { - out << " flag: " << descriptor.flag.value(); - } else { - out << " flag: "; - } - return out; -} -ItemDescriptor::ItemDescriptor(uint64_t version) : version(version) {} - -ItemDescriptor::ItemDescriptor(EvaluationResult flag) - : version(flag.Version()), flag(std::move(flag)) {} -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/data_sources/data_source_update_sink.hpp b/libs/client-sdk/src/data_sources/data_source_update_sink.hpp index b08d9daca..09f4891de 100644 --- a/libs/client-sdk/src/data_sources/data_source_update_sink.hpp +++ b/libs/client-sdk/src/data_sources/data_source_update_sink.hpp @@ -9,37 +9,11 @@ #include #include #include +#include namespace launchdarkly::client_side { -/** - * An item descriptor is an abstraction that allows for Flag data to be - * handled using the same type in both a put or a patch. - */ -struct ItemDescriptor { - /** - * The version number of this data, provided by the SDK. - */ - uint64_t version; - - /** - * The data item, or nullopt if this is a deleted item placeholder. - */ - std::optional flag; - - explicit ItemDescriptor(uint64_t version); - - explicit ItemDescriptor(EvaluationResult flag); - - ItemDescriptor(ItemDescriptor const& item) = default; - ItemDescriptor(ItemDescriptor&& item) = default; - ItemDescriptor& operator=(ItemDescriptor const&) = default; - ItemDescriptor& operator=(ItemDescriptor&&) = default; - ~ItemDescriptor() = default; - - friend std::ostream& operator<<(std::ostream& out, - ItemDescriptor const& descriptor); -}; +using ItemDescriptor = data_kinds::ItemDescriptor; /** * Interface for handling updates from LaunchDarkly. @@ -62,6 +36,4 @@ class IDataSourceUpdateSink { IDataSourceUpdateSink() = default; }; -bool operator==(ItemDescriptor const& lhs, ItemDescriptor const& rhs); - } // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/flag_manager/flag_persistence.cpp b/libs/client-sdk/src/flag_manager/flag_persistence.cpp index 2ad273435..0a62cb277 100644 --- a/libs/client-sdk/src/flag_manager/flag_persistence.cpp +++ b/libs/client-sdk/src/flag_manager/flag_persistence.cpp @@ -1,9 +1,10 @@ #include "flag_persistence.hpp" -#include "../serialization/json_all_flags.hpp" #include #include +#include + #include namespace launchdarkly::client_side::flag_manager { @@ -99,9 +100,10 @@ void FlagPersistence::StoreCache(std::string const& context_id) { persistence_->Set(environment_namespace_, index_key_, boost::json::serialize(boost::json::value_from(index))); - persistence_->Set( - environment_namespace_, context_id, - boost::json::serialize(boost::json::value_from(flag_store_.GetAll()))); + boost::json::value v = boost::json::value_from(flag_store_.GetAll()); + + persistence_->Set(environment_namespace_, context_id, + boost::json::serialize(v)); } ContextIndex FlagPersistence::GetIndex() { diff --git a/libs/client-sdk/src/flag_manager/flag_store.cpp b/libs/client-sdk/src/flag_manager/flag_store.cpp index acd7a43e8..dfcdb8da2 100644 --- a/libs/client-sdk/src/flag_manager/flag_store.cpp +++ b/libs/client-sdk/src/flag_manager/flag_store.cpp @@ -1,6 +1,5 @@ #include -#include "../serialization/json_all_flags.hpp" #include "flag_store.hpp" #include diff --git a/libs/client-sdk/src/flag_manager/flag_updater.cpp b/libs/client-sdk/src/flag_manager/flag_updater.cpp index 9e98bd27f..34b4469d5 100644 --- a/libs/client-sdk/src/flag_manager/flag_updater.cpp +++ b/libs/client-sdk/src/flag_manager/flag_updater.cpp @@ -8,10 +8,10 @@ namespace launchdarkly::client_side::flag_manager { FlagUpdater::FlagUpdater(FlagStore& flag_store) : flag_store_(flag_store) {} Value GetValue(ItemDescriptor& descriptor) { - if (descriptor.flag) { + if (descriptor.item) { // `flag->` unwraps the first optional we know is present. // The second `value()` is not an optional. - return descriptor.flag->Detail().Value(); + return descriptor.item->Detail().Value(); } return {}; } @@ -31,7 +31,7 @@ void FlagUpdater::Init(Context const& context, auto existing = old_flags.find(new_pair.first); if (existing != old_flags.end()) { // The flag changed. - auto& evaluation_result = new_pair.second.flag; + auto& evaluation_result = new_pair.second.item; if (evaluation_result) { auto new_value = GetValue(new_pair.second); auto old_value = GetValue(*existing->second); @@ -86,24 +86,24 @@ void FlagUpdater::DispatchEvent(FlagValueChangeEvent event) { void FlagUpdater::Upsert(Context const& context, std::string key, - ItemDescriptor item) { + ItemDescriptor descriptor) { // Check the version. auto existing = flag_store_.Get(key); - if (existing && (existing->version >= item.version)) { + if (existing && (existing->version >= descriptor.version)) { // Out of order update, ignore it. return; } if (HasListeners()) { // Existed and updated. - if (existing && item.flag) { - DispatchEvent( - FlagValueChangeEvent(key, GetValue(item), GetValue(*existing))); - } else if (item.flag) { + if (existing && descriptor.item) { + DispatchEvent(FlagValueChangeEvent(key, GetValue(descriptor), + GetValue(*existing))); + } else if (descriptor.item) { DispatchEvent(FlagValueChangeEvent( - key, item.flag.value().Detail().Value(), Value())); + key, descriptor.item.value().Detail().Value(), Value())); // new flag - } else if (existing && existing->flag.has_value()) { + } else if (existing && existing->item.has_value()) { // Existed and deleted. DispatchEvent(FlagValueChangeEvent(key, GetValue(*existing))); } else { @@ -111,7 +111,7 @@ void FlagUpdater::Upsert(Context const& context, // Do nothing. } } - flag_store_.Upsert(key, item); + flag_store_.Upsert(key, descriptor); } bool FlagUpdater::HasListeners() const { diff --git a/libs/client-sdk/src/serialization/json_all_flags.cpp b/libs/client-sdk/src/serialization/json_all_flags.cpp deleted file mode 100644 index 663b05374..000000000 --- a/libs/client-sdk/src/serialization/json_all_flags.cpp +++ /dev/null @@ -1,54 +0,0 @@ -#include - -#include "json_all_flags.hpp" - -#include - -namespace launchdarkly::client_side { -// This tag_invoke needs to be in the same namespace as the -// ItemDescriptor. - -tl::expected, JsonError> -tag_invoke(boost::json::value_to_tag< - tl::expected, - JsonError>> const& unused, - boost::json::value const& json_value) { - boost::ignore_unused(unused); - - if (!json_value.is_object()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - auto const& obj = json_value.as_object(); - std::unordered_map descriptors; - for (auto const& pair : obj) { - auto eval_result = - boost::json::value_to>( - pair.value()); - if (!eval_result.has_value()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - descriptors.emplace(pair.key(), - ItemDescriptor(std::move(eval_result.value()))); - } - return descriptors; -} - -void tag_invoke( - boost::json::value_from_tag const& unused, - boost::json::value& json_value, - std::unordered_map> const& - all_flags) { - boost::ignore_unused(unused); - - auto& obj = json_value.emplace_object(); - for (auto descriptor : all_flags) { - // Only serialize non-deleted flags. - if (descriptor.second->flag) { - auto eval_result_json = - boost::json::value_from(*descriptor.second->flag); - obj.emplace(descriptor.first, eval_result_json); - } - } -} - -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/serialization/json_all_flags.hpp b/libs/client-sdk/src/serialization/json_all_flags.hpp deleted file mode 100644 index 18150002e..000000000 --- a/libs/client-sdk/src/serialization/json_all_flags.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include - -#include - -#include - -#include "../data_sources/data_source_update_sink.hpp" - -#include - -namespace launchdarkly::client_side { - -tl::expected, JsonError> -tag_invoke(boost::json::value_to_tag< - tl::expected, - JsonError>> const& unused, - boost::json::value const& json_value); - -void tag_invoke( - boost::json::value_from_tag const& unused, - boost::json::value& json_value, - std::unordered_map> const& - evaluation_result); - -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/tests/flag_persistence_test.cpp b/libs/client-sdk/tests/flag_persistence_test.cpp index 4d61b843e..0ff1edb17 100644 --- a/libs/client-sdk/tests/flag_persistence_test.cpp +++ b/libs/client-sdk/tests/flag_persistence_test.cpp @@ -108,7 +108,7 @@ TEST(FlagPersistenceTests, CanLoadCache) { flag_persistence.LoadCached(context); // The store contains the flag loaded from the persistence. - EXPECT_EQ("test", store.Get("flagA")->flag->Detail().Value().AsString()); + EXPECT_EQ("test", store.Get("flagA")->item->Detail().Value().AsString()); } TEST(FlagPersistenceTests, EvictsContextsBeyondMax) { diff --git a/libs/client-sdk/tests/flag_store_test.cpp b/libs/client-sdk/tests/flag_store_test.cpp index 26096fe8c..818c06fe8 100644 --- a/libs/client-sdk/tests/flag_store_test.cpp +++ b/libs/client-sdk/tests/flag_store_test.cpp @@ -32,7 +32,7 @@ TEST(FlagstoreTests, HandlesInitWithData) { std::nullopt}}}}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("test", store.Get("flagA")->flag->Detail().Value()); + EXPECT_EQ("test", store.Get("flagA")->item->Detail().Value()); } TEST(FlagstoreTests, HandlesSecondInit) { @@ -53,7 +53,7 @@ TEST(FlagstoreTests, HandlesSecondInit) { std::nullopt}}}}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("test", store.Get("flagB")->flag->Detail().Value()); + EXPECT_EQ("test", store.Get("flagB")->item->Detail().Value()); EXPECT_FALSE(store.Get("flagA")); } @@ -74,8 +74,8 @@ TEST(FlagstoreTests, HandlePatchNewFlag) { std::nullopt}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("test", store.Get("flagA")->flag->Detail().Value()); - EXPECT_EQ("second", store.Get("flagB")->flag->Detail().Value()); + EXPECT_EQ("test", store.Get("flagA")->item->Detail().Value()); + EXPECT_EQ("second", store.Get("flagB")->item->Detail().Value()); } TEST(FlagstoreTests, HandlePatchUpdateFlag) { @@ -95,7 +95,7 @@ TEST(FlagstoreTests, HandlePatchUpdateFlag) { std::nullopt}}}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_EQ("second", store.Get("flagA")->flag->Detail().Value()); + EXPECT_EQ("second", store.Get("flagA")->item->Detail().Value()); } TEST(FlagstoreTests, HandleDelete) { @@ -111,7 +111,7 @@ TEST(FlagstoreTests, HandleDelete) { store.Upsert("flagA", ItemDescriptor{2}); EXPECT_FALSE(store.GetAll().empty()); - EXPECT_FALSE(store.Get("flagA")->flag.has_value()); + EXPECT_FALSE(store.Get("flagA")->item.has_value()); } TEST(FlagstoreTests, GetItemWhichDoesNotExist) { diff --git a/libs/client-sdk/tests/flag_updater_test.cpp b/libs/client-sdk/tests/flag_updater_test.cpp index eb58ad36a..50b1067c4 100644 --- a/libs/client-sdk/tests/flag_updater_test.cpp +++ b/libs/client-sdk/tests/flag_updater_test.cpp @@ -44,7 +44,7 @@ TEST(FlagUpdaterDataTests, HandlesInitWithData) { std::nullopt}}}}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandlesSecondInit) { @@ -70,7 +70,7 @@ TEST(FlagUpdaterDataTests, HandlesSecondInit) { std::nullopt}}}}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagB")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagB")->item.value().Detail().Value()); EXPECT_FALSE(manager.Get("flagA")); } @@ -94,8 +94,8 @@ TEST(FlagUpdaterDataTests, HandlePatchNewFlag) { std::nullopt}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); - EXPECT_EQ("second", manager.Get("flagB")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); + EXPECT_EQ("second", manager.Get("flagB")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandlePatchUpdateFlag) { @@ -118,7 +118,7 @@ TEST(FlagUpdaterDataTests, HandlePatchUpdateFlag) { std::nullopt}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("second", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("second", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandlePatchOutOfOrder) { @@ -141,7 +141,7 @@ TEST(FlagUpdaterDataTests, HandlePatchOutOfOrder) { std::nullopt}}}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterDataTests, HandleDelete) { @@ -161,7 +161,7 @@ TEST(FlagUpdaterDataTests, HandleDelete) { ItemDescriptor{2}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_FALSE(manager.Get("flagA")->flag.has_value()); + EXPECT_FALSE(manager.Get("flagA")->item.has_value()); } TEST(FlagUpdaterDataTests, HandleDeleteOutOfOrder) { @@ -181,7 +181,7 @@ TEST(FlagUpdaterDataTests, HandleDeleteOutOfOrder) { ItemDescriptor{0}); EXPECT_FALSE(manager.GetAll().empty()); - EXPECT_EQ("test", manager.Get("flagA")->flag.value().Detail().Value()); + EXPECT_EQ("test", manager.Get("flagA")->item.value().Detail().Value()); } TEST(FlagUpdaterEventTests, InitialInitProducesNoEvents) { diff --git a/libs/common/include/launchdarkly/data/evaluation_result.hpp b/libs/common/include/launchdarkly/data/evaluation_result.hpp index 127589c54..52aa598ca 100644 --- a/libs/common/include/launchdarkly/data/evaluation_result.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_result.hpp @@ -57,9 +57,6 @@ class EvaluationResult { debug_events_until_date, EvaluationDetailInternal detail); - friend std::ostream& operator<<(std::ostream& out, - EvaluationResult const& result); - private: uint64_t version_; std::optional flag_version_; @@ -70,6 +67,8 @@ class EvaluationResult { EvaluationDetailInternal detail_; }; +std::ostream& operator<<(std::ostream& out, EvaluationResult const& result); + bool operator==(EvaluationResult const& lhs, EvaluationResult const& rhs); bool operator!=(EvaluationResult const& lhs, EvaluationResult const& rhs); diff --git a/libs/common/src/data/evaluation_result.cpp b/libs/common/src/data/evaluation_result.cpp index a6b1bdbbb..298b49b0b 100644 --- a/libs/common/src/data/evaluation_result.cpp +++ b/libs/common/src/data/evaluation_result.cpp @@ -48,17 +48,17 @@ EvaluationResult::EvaluationResult( std::ostream& operator<<(std::ostream& out, EvaluationResult const& result) { out << "{"; - out << " version: " << result.version_; - out << " trackEvents: " << result.track_events_; - out << " trackReason: " << result.track_reason_; + out << " version: " << result.Version(); + out << " trackEvents: " << result.TrackEvents(); + out << " trackReason: " << result.TrackReason(); - if (result.debug_events_until_date_.has_value()) { + if (result.DebugEventsUntilDate().has_value()) { std::time_t as_time_t = std::chrono::system_clock::to_time_t( - result.debug_events_until_date_.value()); + result.DebugEventsUntilDate().value()); out << " debugEventsUntilDate: " << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S"); } - out << " detail: " << result.detail_; + out << " detail: " << result.Detail(); out << "}"; return out; } diff --git a/libs/internal/include/launchdarkly/data_kinds/item_descriptor.hpp b/libs/internal/include/launchdarkly/data_kinds/item_descriptor.hpp new file mode 100644 index 000000000..b54c0484b --- /dev/null +++ b/libs/internal/include/launchdarkly/data_kinds/item_descriptor.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::data_kinds { +/** + * An item descriptor is an abstraction that allows for Flag data to be + * handled using the same type in both a put or a patch. + */ +template +struct ItemDescriptor { + /** + * The version number of this data, provided by the SDK. + */ + uint64_t version; + + /** + * The data item, or nullopt if this is a deleted item placeholder. + */ + std::optional item; + + explicit ItemDescriptor(uint64_t version); + + explicit ItemDescriptor(T item); + + ItemDescriptor(ItemDescriptor const&) = default; + ItemDescriptor(ItemDescriptor&&) = default; + ItemDescriptor& operator=(ItemDescriptor const&) = default; + ItemDescriptor& operator=(ItemDescriptor&&) = default; + ~ItemDescriptor() = default; +}; + +template +bool operator==(ItemDescriptor const& lhs, ItemDescriptor const& rhs) { + return lhs.version == rhs.version && lhs.item == rhs.item; +} + +template +std::ostream& operator<<(std::ostream& out, + ItemDescriptor const& descriptor) { + out << "{"; + out << " version: " << descriptor.version; + if (descriptor.item.has_value()) { + out << " item: " << descriptor.item.value(); + } else { + out << " item: "; + } + return out; +} + +template +ItemDescriptor::ItemDescriptor(uint64_t version) : version(version) {} + +template +ItemDescriptor::ItemDescriptor(T item) + : version(item.Version()), item(std::move(item)) {} + +template +tl::expected>, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected>, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto const& obj = json_value.as_object(); + std::unordered_map> descriptors; + for (auto const& pair : obj) { + auto eval_result = + boost::json::value_to>(pair.value()); + if (!eval_result.has_value()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + descriptors.emplace(pair.key(), + ItemDescriptor(std::move(eval_result.value()))); + } + return descriptors; +} + +template +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + std::unordered_map>> const& + all_flags) { + boost::ignore_unused(unused); + + auto& obj = json_value.emplace_object(); + for (auto descriptor : all_flags) { + // Only serialize non-deleted items.. + if (descriptor.second->item) { + auto eval_result_json = + boost::json::value_from(*descriptor.second->item); + obj.emplace(descriptor.first, eval_result_json); + } + } +} + +} // namespace launchdarkly::data_kinds From 420d7a2f2e0f74871635c354f6dd088db867ef1d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 14 Jun 2023 15:00:59 -0700 Subject: [PATCH 03/56] feat: update event processor to handle context key deduplication (#150) Augments event processor with context-key deduplication abilities using an LRU cache. --- .../config/shared/built/events.hpp | 14 ++- .../launchdarkly/config/shared/defaults.hpp | 6 +- libs/common/src/config/events.cpp | 13 ++- .../events/asio_event_processor.hpp | 3 + .../include/launchdarkly/events/events.hpp | 3 + .../include/launchdarkly/events/lru_cache.hpp | 41 +++++++ .../launchdarkly/events/server_events.hpp | 12 +++ .../serialization/events/json_events.hpp | 7 ++ libs/internal/src/CMakeLists.txt | 1 + .../src/events/asio_event_processor.cpp | 101 +++++++++++------- libs/internal/src/events/lru_cache.cpp | 32 ++++++ .../src/serialization/events/json_events.cpp | 12 +++ .../tests/event_serialization_test.cpp | 15 +++ libs/internal/tests/lru_cache_test.cpp | 65 +++++++++++ 14 files changed, 278 insertions(+), 47 deletions(-) create mode 100644 libs/internal/include/launchdarkly/events/lru_cache.hpp create mode 100644 libs/internal/include/launchdarkly/events/server_events.hpp create mode 100644 libs/internal/src/events/lru_cache.cpp create mode 100644 libs/internal/tests/lru_cache_test.cpp diff --git a/libs/common/include/launchdarkly/config/shared/built/events.hpp b/libs/common/include/launchdarkly/config/shared/built/events.hpp index 0db3adf6b..f7c59e0ec 100644 --- a/libs/common/include/launchdarkly/config/shared/built/events.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/events.hpp @@ -36,6 +36,9 @@ class Events final { * should be made. * @param flush_workers How many workers to use for concurrent event * delivery. + * @param context_keys_cache_capacity Max number of unique context keys to + * hold in LRU cache used for context deduplication when generating index + * events. */ Events(bool enabled, std::size_t capacity, @@ -44,7 +47,8 @@ class Events final { bool all_attributes_private, AttributeReference::SetType private_attrs, std::chrono::milliseconds delivery_retry_delay, - std::size_t flush_workers); + std::size_t flush_workers, + std::optional context_keys_cache_capacity); /** * Returns true if event-sending is enabled. @@ -87,6 +91,13 @@ class Events final { */ [[nodiscard]] std::size_t FlushWorkers() const; + /** + * Max number of unique context keys to hold in LRU cache used for context + * deduplication when generating index events. + * @return Max, or std::nullopt if not applicable. + */ + [[nodiscard]] std::optional ContextKeysCacheCapacity() const; + private: bool enabled_; std::size_t capacity_; @@ -96,6 +107,7 @@ class Events final { AttributeReference::SetType private_attributes_; std::chrono::milliseconds delivery_retry_delay_; std::size_t flush_workers_; + std::optional context_keys_cache_capacity_; }; bool operator==(Events const& lhs, Events const& rhs); diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 8ab68e73e..bd62ae2c4 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -44,7 +44,8 @@ struct Defaults { false, AttributeReference::SetType(), std::chrono::seconds(1), - 5}; + 5, + std::nullopt}; } static auto HttpProperties() -> shared::built::HttpProperties { @@ -88,7 +89,8 @@ struct Defaults { false, AttributeReference::SetType(), std::chrono::seconds(1), - 5}; + 5, + 1000}; } static auto HttpProperties() -> shared::built::HttpProperties { diff --git a/libs/common/src/config/events.cpp b/libs/common/src/config/events.cpp index 4147639ed..7f24b7c59 100644 --- a/libs/common/src/config/events.cpp +++ b/libs/common/src/config/events.cpp @@ -9,7 +9,8 @@ Events::Events(bool enabled, bool all_attributes_private, AttributeReference::SetType private_attrs, std::chrono::milliseconds delivery_retry_delay, - std::size_t flush_workers) + std::size_t flush_workers, + std::optional context_keys_cache_capacity) : enabled_(enabled), capacity_(capacity), flush_interval_(flush_interval), @@ -17,7 +18,8 @@ Events::Events(bool enabled, all_attributes_private_(all_attributes_private), private_attributes_(std::move(private_attrs)), delivery_retry_delay_(delivery_retry_delay), - flush_workers_(flush_workers) {} + flush_workers_(flush_workers), + context_keys_cache_capacity_(context_keys_cache_capacity) {} bool Events::Enabled() const { return enabled_; @@ -51,6 +53,10 @@ std::size_t Events::FlushWorkers() const { return flush_workers_; } +std::optional Events::ContextKeysCacheCapacity() const { + return context_keys_cache_capacity_; +} + bool operator==(Events const& lhs, Events const& rhs) { return lhs.Path() == rhs.Path() && lhs.FlushInterval() == rhs.FlushInterval() && @@ -58,6 +64,7 @@ bool operator==(Events const& lhs, Events const& rhs) { lhs.AllAttributesPrivate() == rhs.AllAttributesPrivate() && lhs.PrivateAttributes() == rhs.PrivateAttributes() && lhs.DeliveryRetryDelay() == rhs.DeliveryRetryDelay() && - lhs.FlushWorkers() == rhs.FlushWorkers(); + lhs.FlushWorkers() == rhs.FlushWorkers() && + lhs.ContextKeysCacheCapacity() == rhs.ContextKeysCacheCapacity(); } } // namespace launchdarkly::config::shared::built diff --git a/libs/internal/include/launchdarkly/events/asio_event_processor.hpp b/libs/internal/include/launchdarkly/events/asio_event_processor.hpp index 6e8cc23f2..134abf029 100644 --- a/libs/internal/include/launchdarkly/events/asio_event_processor.hpp +++ b/libs/internal/include/launchdarkly/events/asio_event_processor.hpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -74,6 +75,8 @@ class AsioEventProcessor { launchdarkly::ContextFilter filter_; + LRUCache context_key_cache_; + Logger& logger_; void HandleSend(InputEvent event); diff --git a/libs/internal/include/launchdarkly/events/events.hpp b/libs/internal/include/launchdarkly/events/events.hpp index e3770c13d..aeaea92ba 100644 --- a/libs/internal/include/launchdarkly/events/events.hpp +++ b/libs/internal/include/launchdarkly/events/events.hpp @@ -1,6 +1,8 @@ #pragma once #include "client_events.hpp" +#include "server_events.hpp" + namespace launchdarkly::events { using InputEvent = std::variant; } // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/lru_cache.hpp b/libs/internal/include/launchdarkly/events/lru_cache.hpp new file mode 100644 index 000000000..0714507e6 --- /dev/null +++ b/libs/internal/include/launchdarkly/events/lru_cache.hpp @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +namespace launchdarkly::events { + +class LRUCache { + public: + /** + * Constructs a new cache with a given capacity. When capacity is exceeded, + * entries are evicted from the cache in LRU order. + * @param capacity + */ + explicit LRUCache(std::size_t capacity); + + /** + * Adds a value to the cache; returns true if it was already there. + * @param value Value to add. + * @return True if the value was already in the cache. + */ + bool Notice(std::string const& value); + + /** + * Returns the current size of the cache. + * @return Number of unique entries in cache. + */ + std::size_t Size() const; + + /** + * Clears all cache entries. + */ + void Clear(); + + private: + using KeyList = std::list; + std::size_t capacity_; + std::unordered_map map_; + KeyList list_; +}; + +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/server_events.hpp b/libs/internal/include/launchdarkly/events/server_events.hpp new file mode 100644 index 000000000..a7b566a2b --- /dev/null +++ b/libs/internal/include/launchdarkly/events/server_events.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "common_events.hpp" + +namespace launchdarkly::events::server { + +struct IndexEvent { + Date creation_date; + EventContext context; +}; + +} // namespace launchdarkly::events::server diff --git a/libs/internal/include/launchdarkly/serialization/events/json_events.hpp b/libs/internal/include/launchdarkly/serialization/events/json_events.hpp index cdb6acf2a..cc819a0b7 100644 --- a/libs/internal/include/launchdarkly/serialization/events/json_events.hpp +++ b/libs/internal/include/launchdarkly/serialization/events/json_events.hpp @@ -23,6 +23,13 @@ void tag_invoke(boost::json::value_from_tag const&, DebugEvent const& event); } // namespace launchdarkly::events::client +namespace launchdarkly::events::server { + +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + IndexEvent const& event); +} // namespace launchdarkly::events::server + namespace launchdarkly::events { void tag_invoke(boost::json::value_from_tag const&, diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index d8768e0b7..f5a3bb250 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -18,6 +18,7 @@ add_library(${LIBNAME} OBJECT events/request_worker.cpp events/summarizer.cpp events/worker_pool.cpp + events/lru_cache.cpp logging/console_backend.cpp logging/null_logger.cpp logging/logger.cpp diff --git a/libs/internal/src/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index 9cc8e27d6..5c744d9d0 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -10,6 +10,8 @@ #include #include +#include + namespace http = boost::beast::http; namespace launchdarkly::events { @@ -53,6 +55,7 @@ AsioEventProcessor::AsioEventProcessor( last_known_past_time_(std::nullopt), filter_(events_config.AllAttributesPrivate(), events_config.PrivateAttributes()), + context_key_cache_(events_config.ContextKeysCacheCapacity().value_or(0)), logger_(logger) { ScheduleFlush(); } @@ -212,47 +215,63 @@ std::vector AsioEventProcessor::Process( InputEvent input_event) { std::vector out; std::visit( - overloaded{[&](client::FeatureEventParams&& event) { - summarizer_.Update(event); - - client::FeatureEventBase base{event}; - - auto debug_until_date = event.debug_events_until_date; - - // To be conservative, use as the current time the - // maximum of the actual current time and the server's - // time. This way if the local host is running behind, we - // won't accidentally keep emitting events. - - auto conservative_now = std::max( - std::chrono::system_clock::now(), - last_known_past_time_.value_or( - std::chrono::system_clock::from_time_t(0))); - - bool emit_debug_event = - debug_until_date && - conservative_now < debug_until_date->t; - - if (emit_debug_event) { - out.emplace_back(client::DebugEvent{ - base, filter_.filter(event.context)}); - } - - if (event.require_full_event) { - out.emplace_back(client::FeatureEvent{ - std::move(base), event.context.KindsToKeys()}); - } - }, - [&](client::IdentifyEventParams&& event) { - // Contexts should already have been checked for - // validity by this point. - assert(event.context.Valid()); - out.emplace_back(client::IdentifyEvent{ - event.creation_date, filter_.filter(event.context)}); - }, - [&](TrackEventParams&& event) { - out.emplace_back(std::move(event)); - }}, + overloaded{ + [&](client::FeatureEventParams&& event) { + summarizer_.Update(event); + + if constexpr (std::is_same::value) { + if (!context_key_cache_.Notice( + event.context.CanonicalKey())) { + out.emplace_back( + server::IndexEvent{event.creation_date, + filter_.filter(event.context)}); + } + } + + client::FeatureEventBase base{event}; + + auto debug_until_date = event.debug_events_until_date; + + // To be conservative, use as the current time the + // maximum of the actual current time and the server's + // time. This way if the local host is running behind, we + // won't accidentally keep emitting events. + + auto conservative_now = + std::max(std::chrono::system_clock::now(), + last_known_past_time_.value_or( + std::chrono::system_clock::from_time_t(0))); + + bool emit_debug_event = + debug_until_date && conservative_now < debug_until_date->t; + + if (emit_debug_event) { + out.emplace_back(client::DebugEvent{ + base, filter_.filter(event.context)}); + } + + if (event.require_full_event) { + out.emplace_back(client::FeatureEvent{ + std::move(base), event.context.KindsToKeys()}); + } + }, + [&](client::IdentifyEventParams&& event) { + // Contexts should already have been checked for + // validity by this point. + assert(event.context.Valid()); + + if constexpr (std::is_same::value) { + context_key_cache_.Notice(event.context.CanonicalKey()); + } + + out.emplace_back(client::IdentifyEvent{ + event.creation_date, filter_.filter(event.context)}); + }, + [&](TrackEventParams&& event) { + out.emplace_back(std::move(event)); + }}, std::move(input_event)); return out; diff --git a/libs/internal/src/events/lru_cache.cpp b/libs/internal/src/events/lru_cache.cpp new file mode 100644 index 000000000..ad39bd0b4 --- /dev/null +++ b/libs/internal/src/events/lru_cache.cpp @@ -0,0 +1,32 @@ +#include + +namespace launchdarkly::events { +LRUCache::LRUCache(std::size_t capacity) + : capacity_(capacity), map_(), list_() {} + +bool LRUCache::Notice(std::string const& value) { + auto it = map_.find(value); + if (it != map_.end()) { + list_.remove(value); + list_.push_front(value); + return true; + } + while (map_.size() >= capacity_) { + map_.erase(list_.back()); + list_.pop_back(); + } + list_.push_front(value); + map_.emplace(value, list_.front()); + return false; +} + +void LRUCache::Clear() { + map_.clear(); + list_.clear(); +} + +std::size_t LRUCache::Size() const { + return list_.size(); +} + +} // namespace launchdarkly::events diff --git a/libs/internal/src/serialization/events/json_events.cpp b/libs/internal/src/serialization/events/json_events.cpp index c3d821e15..a57aaa155 100644 --- a/libs/internal/src/serialization/events/json_events.cpp +++ b/libs/internal/src/serialization/events/json_events.cpp @@ -51,6 +51,18 @@ void tag_invoke(boost::json::value_from_tag const& tag, } } // namespace launchdarkly::events::client +namespace launchdarkly::events::server { + +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + IndexEvent const& event) { + auto& obj = json_value.emplace_object(); + obj.emplace("kind", "index"); + obj.emplace("creationDate", boost::json::value_from(event.creation_date)); + obj.emplace("context", event.context); +} +} // namespace launchdarkly::events::server + namespace launchdarkly::events { void tag_invoke(boost::json::value_from_tag const& tag, diff --git a/libs/internal/tests/event_serialization_test.cpp b/libs/internal/tests/event_serialization_test.cpp index 00e274946..f34e0ff83 100644 --- a/libs/internal/tests/event_serialization_test.cpp +++ b/libs/internal/tests/event_serialization_test.cpp @@ -82,4 +82,19 @@ TEST(EventSerialization, IdentifyEvent) { ASSERT_EQ(result, event_json); } +TEST(EventSerialization, IndexEvent) { + auto creation_date = std::chrono::system_clock::from_time_t({}); + AttributeReference::SetType attrs; + ContextFilter filter(false, attrs); + auto event = events::server::IndexEvent{ + creation_date, + filter.filter(ContextBuilder().Kind("foo", "bar").Build())}; + + auto event_json = boost::json::value_from(event); + + auto result = boost::json::parse( + R"({"kind":"index","creationDate":0,"context":{"key":"bar","kind":"foo"}})"); + ASSERT_EQ(result, event_json); +} + } // namespace launchdarkly::events diff --git a/libs/internal/tests/lru_cache_test.cpp b/libs/internal/tests/lru_cache_test.cpp new file mode 100644 index 000000000..1dd6a8ea9 --- /dev/null +++ b/libs/internal/tests/lru_cache_test.cpp @@ -0,0 +1,65 @@ +#include +#include + +using namespace launchdarkly::events; + +TEST(ContextKeyCacheTests, CacheSizeOne) { + LRUCache cache(1); + + auto keys = {"foo", "bar", "baz", "qux"}; + for (auto const& k : keys) { + ASSERT_FALSE(cache.Notice(k)); + ASSERT_EQ(cache.Size(), 1); + } +} + +TEST(ContextKeyCacheTests, CacheIsCleared) { + LRUCache cache(3); + auto keys = {"foo", "bar", "baz"}; + for (auto const& k : keys) { + cache.Notice(k); + } + ASSERT_EQ(cache.Size(), 3); + cache.Clear(); + ASSERT_EQ(cache.Size(), 0); +} + +TEST(ContextKeyCacheTests, LRUProperty) { + LRUCache cache(3); + auto keys = {"foo", "bar", "baz"}; + for (auto const& k : keys) { + cache.Notice(k); + } + + for (auto const& k : keys) { + ASSERT_TRUE(cache.Notice(k)); + } + + // Evict foo. + cache.Notice("qux"); + ASSERT_TRUE(cache.Notice("bar")); + ASSERT_TRUE(cache.Notice("baz")); + ASSERT_TRUE(cache.Notice("qux")); + + // Evict bar. + cache.Notice("foo"); + ASSERT_TRUE(cache.Notice("baz")); + ASSERT_TRUE(cache.Notice("qux")); + ASSERT_TRUE(cache.Notice("foo")); +} + +TEST(ContextKeyCacheTests, DoesNotExceedCapacity) { + const std::size_t CAP = 100; + const std::size_t N = 100000; + LRUCache cache(CAP); + + for (int i = 0; i < N; ++i) { + cache.Notice(std::to_string(i)); + } + + for (int i = N - CAP; i < N; ++i) { + ASSERT_TRUE(cache.Notice(std::to_string(i))); + } + + ASSERT_EQ(cache.Size(), CAP); +} From dd174a2e0e086835e793159ca44f24a6deb37d2b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 30 Jun 2023 15:00:19 -0700 Subject: [PATCH 04/56] feat: segment data model (#153) This PR extends our existing deserialization/serialization technique from the client-side SDK to the server-side SDK data model. Specifically, it adds deserialization for the segment structure. I've also introduced some helpers to make the logic less repetitive, and centralize some decisions (like what to do if a JSON field isn't present, or is null.) As background, the `tag_invoke` functions present in this PR are what allows the `boost::json` library to recursively deserialize complex structures. Here, I've added a couple new `tag_invokes` for bool, string, vector, etc. The special part is that they deserialize into `expected` rather than the primitive directly; this allows us to return an error if the deserialization failed. --- .../data_source_event_handler.cpp | 23 +- .../data_sources/data_source_update_sink.hpp | 4 +- .../src/flag_manager/flag_persistence.cpp | 12 +- .../launchdarkly/attribute_reference.hpp | 7 +- libs/common/src/attribute_reference.cpp | 2 + .../item_descriptor.hpp | 48 +---- .../launchdarkly/data_model/sdk_data_set.hpp | 20 ++ .../launchdarkly/data_model/segment.hpp | 74 +++++++ .../serialization/json_evaluation_result.hpp | 12 +- .../serialization/json_item_descriptor.hpp | 58 ++++++ .../serialization/json_primitives.hpp | 115 ++++++++++ .../serialization/json_sdk_data_set.hpp | 11 + .../serialization/json_segment.hpp | 38 ++++ .../launchdarkly/serialization/json_value.hpp | 6 + .../serialization/value_mapping.hpp | 41 +++- libs/internal/src/CMakeLists.txt | 3 + .../serialization/json_evaluation_result.cpp | 151 +++++++------- .../src/serialization/json_primitives.cpp | 52 +++++ .../src/serialization/json_sdk_data_set.cpp | 24 +++ .../src/serialization/json_segment.cpp | 197 ++++++++++++++++++ .../internal/src/serialization/json_value.cpp | 6 + .../src/serialization/value_mapping.cpp | 17 ++ .../tests/data_model_serialization_test.cpp | 176 ++++++++++++++++ .../internal/tests/evaluation_result_test.cpp | 138 ++++++------ 24 files changed, 1018 insertions(+), 217 deletions(-) rename libs/internal/include/launchdarkly/{data_kinds => data_model}/item_descriptor.hpp (50%) create mode 100644 libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp create mode 100644 libs/internal/include/launchdarkly/data_model/segment.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_primitives.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_segment.hpp create mode 100644 libs/internal/src/serialization/json_primitives.cpp create mode 100644 libs/internal/src/serialization/json_sdk_data_set.cpp create mode 100644 libs/internal/src/serialization/json_segment.cpp create mode 100644 libs/internal/tests/data_model_serialization_test.cpp diff --git a/libs/client-sdk/src/data_sources/data_source_event_handler.cpp b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp index 0ee624bf8..fe182d81d 100644 --- a/libs/client-sdk/src/data_sources/data_source_event_handler.cpp +++ b/libs/client-sdk/src/data_sources/data_source_event_handler.cpp @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include @@ -34,13 +36,14 @@ static tl::expected tag_invoke( auto const& obj = json_value.as_object(); auto const* key_iter = obj.find("key"); auto key = ValueAsOpt(key_iter, obj.end()); - auto result = - boost::json::value_to>( - json_value); + auto result = boost::json::value_to< + tl::expected, JsonError>>( + json_value); - if (result.has_value() && key.has_value()) { + if (result.has_value() && result.value().has_value() && + key.has_value()) { return DataSourceEventHandler::PatchData{key.value(), - result.value()}; + result.value().value()}; } } return tl::unexpected(JsonError::kSchemaFailure); @@ -92,11 +95,15 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( return DataSourceEventHandler::MessageStatus::kInvalidMessage; } auto res = boost::json::value_to, JsonError>>( - parsed); + std::optional>, + JsonError>>(parsed); if (res.has_value()) { - handler_.Init(context_, res.value()); + // If the map was null or omitted, treat it like an empty data set. + auto map = res.value().value_or( + std::unordered_map{}); + + handler_.Init(context_, std::move(map)); status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); return DataSourceEventHandler::MessageStatus::kMessageHandled; } diff --git a/libs/client-sdk/src/data_sources/data_source_update_sink.hpp b/libs/client-sdk/src/data_sources/data_source_update_sink.hpp index 09f4891de..1149618c9 100644 --- a/libs/client-sdk/src/data_sources/data_source_update_sink.hpp +++ b/libs/client-sdk/src/data_sources/data_source_update_sink.hpp @@ -9,11 +9,11 @@ #include #include #include -#include +#include namespace launchdarkly::client_side { -using ItemDescriptor = data_kinds::ItemDescriptor; +using ItemDescriptor = data_model::ItemDescriptor; /** * Interface for handling updates from LaunchDarkly. diff --git a/libs/client-sdk/src/flag_manager/flag_persistence.cpp b/libs/client-sdk/src/flag_manager/flag_persistence.cpp index 0a62cb277..5423c7028 100644 --- a/libs/client-sdk/src/flag_manager/flag_persistence.cpp +++ b/libs/client-sdk/src/flag_manager/flag_persistence.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include @@ -73,8 +75,7 @@ void FlagPersistence::LoadCached(Context const& context) { } auto res = boost::json::value_to, + std::optional>, JsonError>>(parsed); if (!res) { LD_LOG(logger_, LogLevel::kError) @@ -82,7 +83,12 @@ void FlagPersistence::LoadCached(Context const& context) { << error_code.message(); return; } - sink_.Init(context, *res); + + // If the map was null or omitted, treat it like an empty data set. + auto map = + res.value().value_or(std::unordered_map{}); + + sink_.Init(context, std::move(map)); } void FlagPersistence::StoreCache(std::string const& context_id) { diff --git a/libs/common/include/launchdarkly/attribute_reference.hpp b/libs/common/include/launchdarkly/attribute_reference.hpp index 6f252f9e9..c1e077e65 100644 --- a/libs/common/include/launchdarkly/attribute_reference.hpp +++ b/libs/common/include/launchdarkly/attribute_reference.hpp @@ -15,7 +15,7 @@ namespace launchdarkly { * launchdarkly::Context::Get, or to identify an attribute or nested value that * should be considered private * with launchdarkly::AttributesBuilder::SetPrivate or - * launchdarkly::AttributesBuilder::AddPrivateAttribute + * launchdarkly::AttributesBuilder::AddPrivateAttribute * (the SDK configuration can also have a list of private attribute references). * * This is represented as a separate type, rather than just a string, so that @@ -123,6 +123,11 @@ class AttributeReference { */ AttributeReference(char const* ref_str); + /** + * Default constructs an invalid attribute reference. + */ + explicit AttributeReference(); + bool operator==(AttributeReference const& other) const { return components_ == other.components_; } diff --git a/libs/common/src/attribute_reference.cpp b/libs/common/src/attribute_reference.cpp index a0dfabef2..150a3f915 100644 --- a/libs/common/src/attribute_reference.cpp +++ b/libs/common/src/attribute_reference.cpp @@ -226,6 +226,8 @@ AttributeReference::AttributeReference(std::string ref_str) AttributeReference::AttributeReference(char const* ref_str) : AttributeReference(std::string(ref_str)) {} +AttributeReference::AttributeReference() : AttributeReference("") {} + std::string AttributeReference::PathToStringReference( std::vector path) { // Approximate size to reduce resizes. diff --git a/libs/internal/include/launchdarkly/data_kinds/item_descriptor.hpp b/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp similarity index 50% rename from libs/internal/include/launchdarkly/data_kinds/item_descriptor.hpp rename to libs/internal/include/launchdarkly/data_model/item_descriptor.hpp index b54c0484b..003f53907 100644 --- a/libs/internal/include/launchdarkly/data_kinds/item_descriptor.hpp +++ b/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp @@ -8,7 +8,7 @@ #include #include -namespace launchdarkly::data_kinds { +namespace launchdarkly::data_model { /** * An item descriptor is an abstraction that allows for Flag data to be * handled using the same type in both a put or a patch. @@ -61,48 +61,4 @@ template ItemDescriptor::ItemDescriptor(T item) : version(item.Version()), item(std::move(item)) {} -template -tl::expected>, JsonError> -tag_invoke(boost::json::value_to_tag< - tl::expected>, - JsonError>> const& unused, - boost::json::value const& json_value) { - boost::ignore_unused(unused); - - if (!json_value.is_object()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - auto const& obj = json_value.as_object(); - std::unordered_map> descriptors; - for (auto const& pair : obj) { - auto eval_result = - boost::json::value_to>(pair.value()); - if (!eval_result.has_value()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - descriptors.emplace(pair.key(), - ItemDescriptor(std::move(eval_result.value()))); - } - return descriptors; -} - -template -void tag_invoke( - boost::json::value_from_tag const& unused, - boost::json::value& json_value, - std::unordered_map>> const& - all_flags) { - boost::ignore_unused(unused); - - auto& obj = json_value.emplace_object(); - for (auto descriptor : all_flags) { - // Only serialize non-deleted items.. - if (descriptor.second->item) { - auto eval_result_json = - boost::json::value_from(*descriptor.second->item); - obj.emplace(descriptor.first, eval_result_json); - } - } -} - -} // namespace launchdarkly::data_kinds +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp new file mode 100644 index 000000000..d1f0c85d4 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::data_model { + +struct SDKDataSet { + using FlagKey = std::string; + using SegmentKey = std::string; + // std::unordered_map> flags; + std::optional>> + segments; +}; + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/segment.hpp b/libs/internal/include/launchdarkly/data_model/segment.hpp new file mode 100644 index 000000000..6794c08a6 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/segment.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::data_model { + +struct Segment { + struct Target { + std::string contextKind; + std::vector values; + }; + + struct Clause { + enum class Op { + kOmitted, /* represents empty string */ + kUnrecognized, /* didn't match any known operators */ + kIn, + kStartsWith, + kEndsWith, + kMatches, + kContains, + kLessThan, + kLessThanOrEqual, + kGreaterThan, + kGreaterThanOrEqual, + kBefore, + kAfter, + kSemVerEqual, + kSemVerLessThan, + kSemVerGreaterThan, + kSegmentMatch + }; + + std::optional attribute; + Op op; + std::vector values; + + std::optional negate; + std::optional contextKind; + }; + + struct Rule { + std::vector clauses; + std::optional id; + std::optional weight; + std::optional bucketBy; + std::optional rolloutContextKind; + }; + + std::string key; + std::uint64_t version; + + std::optional> included; + std::optional> excluded; + std::optional> includedContexts; + std::optional> excludedContexts; + std::optional> rules; + std::optional salt; + std::optional unbounded; + std::optional unboundedContextKind; + std::optional generation; + + [[nodiscard]] inline std::uint64_t Version() const { return version; } +}; +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp b/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp index d03617cfa..4dc5f7331 100644 --- a/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp @@ -8,14 +8,10 @@ #include "json_errors.hpp" namespace launchdarkly { -/** - * Method used by boost::json for converting a boost::json::value into a - * launchdarkly::EvaluationResult. - * @return A EvaluationResult representation of the boost::json::value. - */ -tl::expected tag_invoke( - boost::json::value_to_tag> const& - unused, + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, boost::json::value const& json_value); void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp b/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp new file mode 100644 index 000000000..f96e6afee --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include "json_errors.hpp" + +namespace launchdarkly { + +template +tl::expected>, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected>, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + auto maybe_item = + boost::json::value_to, JsonError>>( + json_value); + + if (!maybe_item) { + return tl::unexpected(maybe_item.error()); + } + + auto const& item = maybe_item.value(); + + if (!item) { + return std::nullopt; + } + + return data_model::ItemDescriptor(std::move(item.value())); +} + +template +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + std::unordered_map>> const& + all_flags) { + boost::ignore_unused(unused); + + auto& obj = json_value.emplace_object(); + for (auto descriptor : all_flags) { + // Only serialize non-deleted items.. + if (descriptor.second->item) { + auto eval_result_json = + boost::json::value_from(*descriptor.second->item); + obj.emplace(descriptor.first, eval_result_json); + } + } +} +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_primitives.hpp b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp new file mode 100644 index 000000000..1c555af12 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include "json_errors.hpp" + +namespace launchdarkly { + +template +tl::expected>, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected>, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (json_value.is_null()) { + return std::nullopt; + } + + if (!json_value.is_array()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + if (json_value.as_array().empty()) { + return std::nullopt; + } + + auto const& arr = json_value.as_array(); + std::vector items; + items.reserve(arr.size()); + for (auto const& item : arr) { + auto eval_result = + boost::json::value_to>(item); + if (!eval_result.has_value()) { + return tl::unexpected(eval_result.error()); + } + items.emplace_back(std::move(eval_result.value())); + } + return items; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +template +tl::expected>, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected>, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + if (json_value.as_object().empty()) { + return std::nullopt; + } + auto const& obj = json_value.as_object(); + std::unordered_map descriptors; + for (auto const& pair : obj) { + auto eval_result = + boost::json::value_to, JsonError>>( + pair.value()); + if (!eval_result) { + return tl::unexpected(eval_result.error()); + } + auto const& maybe_val = eval_result.value(); + if (maybe_val) { + descriptors.emplace(pair.key(), std::move(maybe_val.value())); + } + } + return descriptors; +} + +/** + * Convenience implementation that deserializes a T via the tag_invoke overload + * for std::optional. + * + * If that overload returns std::nullopt, this returns + * a default-constructed T. + * + * Json errors are propagated. + */ +template +tl::expected tag_invoke( + boost::json::value_to_tag> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + auto maybe_val = + boost::json::value_to, JsonError>>( + json_value); + if (!maybe_val.has_value()) { + return tl::unexpected(maybe_val.error()); + } + return maybe_val.value().value_or(T{}); +} + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp b/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp new file mode 100644 index 000000000..41563b497 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace launchdarkly { +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value); + +} diff --git a/libs/internal/include/launchdarkly/serialization/json_segment.hpp b/libs/internal/include/launchdarkly/serialization/json_segment.hpp new file mode 100644 index 000000000..f80728345 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_segment.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value); + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value); + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value); + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_value.hpp b/libs/internal/include/launchdarkly/serialization/json_value.hpp index 5c02047d9..07cb8f0d3 100644 --- a/libs/internal/include/launchdarkly/serialization/json_value.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_value.hpp @@ -2,7 +2,9 @@ #include +#include #include +#include namespace launchdarkly { /** @@ -13,6 +15,10 @@ namespace launchdarkly { Value tag_invoke(boost::json::value_to_tag const&, boost::json::value const&); +tl::expected tag_invoke( + boost::json::value_to_tag> const&, + boost::json::value const&); + /** * Method used by boost::json for converting a launchdarkly::Value into a * boost::json::value. diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index b7cc5aa3d..686ae95a8 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -2,9 +2,48 @@ #include #include +#include +#include +#include -namespace launchdarkly { +#define PARSE_FIELD(field, it) \ + if (auto result = \ + boost::json::value_to>( \ + it->value())) { \ + field = result.value(); \ + } else { \ + return tl::make_unexpected(result.error()); \ + } + +// Attempts to parse a field only if it exists in the data. Propagates an error +// if the field's destination type is not compatible with the data. +#define PARSE_OPTIONAL_FIELD(field, obj, key) \ + do { \ + auto const& it = obj.find(key); \ + if (it != obj.end()) { \ + PARSE_FIELD(field, it); \ + } \ + } while (0) +// Propagates an error upwards if the specified field isn't present in the +// data. +#define PARSE_REQUIRED_FIELD(field, obj, key) \ + do { \ + auto const& it = obj.find(key); \ + if (it == obj.end()) { \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + PARSE_FIELD(field, it); \ + } while (0) + +#define REQUIRE_OBJECT(value) \ + do { \ + if (!json_value.is_object()) { \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + } while (0) + +namespace launchdarkly { template std::optional ValueAsOpt(boost::json::object::const_iterator iterator, boost::json::object::const_iterator end) { diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index f5a3bb250..26e9249ea 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -32,6 +32,9 @@ add_library(${LIBNAME} OBJECT serialization/json_value.cpp serialization/value_mapping.cpp serialization/json_evaluation_result.cpp + serialization/json_sdk_data_set.cpp + serialization/json_segment.cpp + serialization/json_primitives.cpp encoding/base_64.cpp encoding/sha_256.cpp) diff --git a/libs/internal/src/serialization/json_evaluation_result.cpp b/libs/internal/src/serialization/json_evaluation_result.cpp index 00a9e2ed7..14406af75 100644 --- a/libs/internal/src/serialization/json_evaluation_result.cpp +++ b/libs/internal/src/serialization/json_evaluation_result.cpp @@ -6,92 +6,93 @@ #include namespace launchdarkly { -tl::expected tag_invoke( - boost::json::value_to_tag> const& - unused, + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); - if (json_value.is_object()) { - auto& json_obj = json_value.as_object(); - auto* version_iter = json_obj.find("version"); - auto version_opt = ValueAsOpt(version_iter, json_obj.end()); - if (!version_opt.has_value()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - auto version = version_opt.value(); - - auto* flag_version_iter = json_obj.find("flagVersion"); - auto flag_version = - ValueAsOpt(flag_version_iter, json_obj.end()); - - auto* track_events_iter = json_obj.find("trackEvents"); - auto track_events = - ValueOrDefault(track_events_iter, json_obj.end(), false); - - auto* track_reason_iter = json_obj.find("trackReason"); - auto track_reason = - ValueOrDefault(track_reason_iter, json_obj.end(), false); - - auto* debug_events_until_date_iter = - json_obj.find("debugEventsUntilDate"); - - auto debug_events_until_date = - MapOpt, - uint64_t>(ValueAsOpt(debug_events_until_date_iter, - json_obj.end()), - [](auto value) { - return std::chrono::system_clock::time_point{ - std::chrono::milliseconds{value}}; - }); - - // Evaluation detail is directly de-serialized inline here. - // This is because the shape of the evaluation detail is different - // when deserializing FlagMeta. Primarily `variation` not - // `variationIndex`. - - auto* value_iter = json_obj.find("value"); - if (value_iter == json_obj.end()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - auto value = boost::json::value_to(value_iter->value()); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto const& json_obj = json_value.as_object(); + + auto* version_iter = json_obj.find("version"); + auto version_opt = ValueAsOpt(version_iter, json_obj.end()); + if (!version_opt.has_value()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto version = version_opt.value(); + + auto* flag_version_iter = json_obj.find("flagVersion"); + auto flag_version = ValueAsOpt(flag_version_iter, json_obj.end()); - auto* variation_iter = json_obj.find("variation"); - auto variation = ValueAsOpt(variation_iter, json_obj.end()); + auto* track_events_iter = json_obj.find("trackEvents"); + auto track_events = + ValueOrDefault(track_events_iter, json_obj.end(), false); - auto* reason_iter = json_obj.find("reason"); + auto* track_reason_iter = json_obj.find("trackReason"); + auto track_reason = + ValueOrDefault(track_reason_iter, json_obj.end(), false); - // There is a reason. - if (reason_iter != json_obj.end() && !reason_iter->value().is_null()) { - auto reason = boost::json::value_to< - tl::expected>( + auto* debug_events_until_date_iter = json_obj.find("debugEventsUntilDate"); + + auto debug_events_until_date = + MapOpt, uint64_t>( + ValueAsOpt(debug_events_until_date_iter, json_obj.end()), + [](auto value) { + return std::chrono::system_clock::time_point{ + std::chrono::milliseconds{value}}; + }); + + // Evaluation detail is directly de-serialized inline here. + // This is because the shape of the evaluation detail is different + // when deserializing FlagMeta. Primarily `variation` not + // `variationIndex`. + + auto* value_iter = json_obj.find("value"); + if (value_iter == json_obj.end()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto value = boost::json::value_to(value_iter->value()); + + auto* variation_iter = json_obj.find("variation"); + auto variation = ValueAsOpt(variation_iter, json_obj.end()); + + auto* reason_iter = json_obj.find("reason"); + + // There is a reason. + if (reason_iter != json_obj.end() && !reason_iter->value().is_null()) { + auto reason = + boost::json::value_to>( reason_iter->value()); - if (reason.has_value()) { - return EvaluationResult{ - version, - flag_version, - track_events, - track_reason, - debug_events_until_date, - EvaluationDetailInternal( - value, variation, std::make_optional(reason.value()))}; - } - // We could not parse the reason. - return tl::unexpected(JsonError::kSchemaFailure); + if (reason.has_value()) { + return EvaluationResult{ + version, + flag_version, + track_events, + track_reason, + debug_events_until_date, + EvaluationDetailInternal(value, variation, + std::make_optional(reason.value()))}; } - - // There was no reason. - return EvaluationResult{ - version, - flag_version, - track_events, - track_reason, - debug_events_until_date, - EvaluationDetailInternal(value, variation, std::nullopt)}; + // We could not parse the reason. + return tl::unexpected(JsonError::kSchemaFailure); } - return tl::unexpected(JsonError::kSchemaFailure); + // There was no reason. + return EvaluationResult{ + version, + flag_version, + track_events, + track_reason, + debug_events_until_date, + EvaluationDetailInternal(value, variation, std::nullopt)}; } void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/src/serialization/json_primitives.cpp b/libs/internal/src/serialization/json_primitives.cpp new file mode 100644 index 000000000..150d42cbe --- /dev/null +++ b/libs/internal/src/serialization/json_primitives.cpp @@ -0,0 +1,52 @@ +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_bool()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + if (!json_value.as_bool()) { + return std::nullopt; + } + return json_value.as_bool(); +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_number()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return json_value.to_number(); +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_string()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + if (json_value.as_string().empty()) { + return std::nullopt; + } + return std::string(json_value.as_string()); +} + +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_sdk_data_set.cpp b/libs/internal/src/serialization/json_sdk_data_set.cpp new file mode 100644 index 000000000..4b02546bc --- /dev/null +++ b/libs/internal/src/serialization/json_sdk_data_set.cpp @@ -0,0 +1,24 @@ +#include +#include +#include +#include +#include + +namespace launchdarkly { +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + + auto const& obj = json_value.as_object(); + + data_model::SDKDataSet data_set; + + PARSE_OPTIONAL_FIELD(data_set.segments, obj, "segments"); + + return data_set; +} +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp new file mode 100644 index 000000000..57482b7df --- /dev/null +++ b/libs/internal/src/serialization/json_segment.cpp @@ -0,0 +1,197 @@ +#include +#include +#include +#include +#include + +namespace launchdarkly { + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Segment::Target target; + + PARSE_REQUIRED_FIELD(target.contextKind, obj, "contextKind"); + PARSE_REQUIRED_FIELD(target.values, obj, "values"); + + return target; +} + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Segment::Rule rule; + + PARSE_REQUIRED_FIELD(rule.clauses, obj, "clauses"); + + PARSE_OPTIONAL_FIELD(rule.rolloutContextKind, obj, "rolloutContextKind"); + PARSE_OPTIONAL_FIELD(rule.weight, obj, "weight"); + PARSE_OPTIONAL_FIELD(rule.id, obj, "id"); + + std::optional literal_or_ref; + PARSE_OPTIONAL_FIELD(literal_or_ref, obj, "bucketBy"); + + rule.bucketBy = MapOpt( + literal_or_ref, + [has_context = rule.rolloutContextKind.has_value()](auto&& ref) { + if (has_context) { + return AttributeReference::FromReferenceStr(ref); + } else { + return AttributeReference::FromLiteralStr(ref); + } + }); + + return rule; +} + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Segment::Clause clause; + + PARSE_REQUIRED_FIELD(clause.op, obj, "op"); + PARSE_REQUIRED_FIELD(clause.values, obj, "values"); + + PARSE_OPTIONAL_FIELD(clause.negate, obj, "negate"); + PARSE_OPTIONAL_FIELD(clause.contextKind, obj, "contextKind"); + + std::optional literal_or_ref; + PARSE_OPTIONAL_FIELD(literal_or_ref, obj, "attribute"); + + clause.attribute = MapOpt( + literal_or_ref, + [has_context = clause.contextKind.has_value()](auto&& ref) { + if (has_context) { + return AttributeReference::FromReferenceStr(ref); + } else { + return AttributeReference::FromLiteralStr(ref); + } + }); + + return clause; +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_string()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + if (json_value.as_string().empty()) { + return std::nullopt; + } + auto const& str = json_value.as_string(); + + if (str == "in") { + return data_model::Segment::Clause::Op::kIn; + } else if (str == "endsWith") { + return data_model::Segment::Clause::Op::kEndsWith; + } else if (str == "startsWith") { + return data_model::Segment::Clause::Op::kStartsWith; + } else if (str == "matches") { + return data_model::Segment::Clause::Op::kMatches; + } else if (str == "contains") { + return data_model::Segment::Clause::Op::kContains; + } else if (str == "lessThan") { + return data_model::Segment::Clause::Op::kLessThan; + } else if (str == "lessThanOrEqual") { + return data_model::Segment::Clause::Op::kLessThanOrEqual; + } else if (str == "greaterThan") { + return data_model::Segment::Clause::Op::kGreaterThan; + } else if (str == "greaterThanOrEqual") { + return data_model::Segment::Clause::Op::kGreaterThanOrEqual; + } else if (str == "before") { + return data_model::Segment::Clause::Op::kBefore; + } else if (str == "after") { + return data_model::Segment::Clause::Op::kAfter; + } else if (str == "semVerEqual") { + return data_model::Segment::Clause::Op::kSemVerEqual; + } else if (str == "semVerLessThan") { + return data_model::Segment::Clause::Op::kSemVerLessThan; + } else if (str == "semVerGreaterThan") { + return data_model::Segment::Clause::Op::kSemVerGreaterThan; + } else if (str == "segmentMatch") { + return data_model::Segment::Clause::Op::kSegmentMatch; + } else { + return data_model::Segment::Clause::Op::kUnrecognized; + } +} + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + auto maybe_op = boost::json::value_to, JsonError>>(json_value); + if (!maybe_op) { + return tl::unexpected(maybe_op.error()); + } + return maybe_op.value().value_or(data_model::Segment::Clause::Op::kOmitted); +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (json_value.is_null()) { + return std::nullopt; + } + + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + if (json_value.as_object().empty()) { + return std::nullopt; + } + + auto const& obj = json_value.as_object(); + + data_model::Segment segment; + + PARSE_REQUIRED_FIELD(segment.key, obj, "key"); + PARSE_REQUIRED_FIELD(segment.version, obj, "version"); + + PARSE_OPTIONAL_FIELD(segment.excluded, obj, "excluded"); + PARSE_OPTIONAL_FIELD(segment.included, obj, "included"); + + PARSE_OPTIONAL_FIELD(segment.generation, obj, "generation"); + PARSE_OPTIONAL_FIELD(segment.salt, obj, "salt"); + PARSE_OPTIONAL_FIELD(segment.unbounded, obj, "unbounded"); + + PARSE_OPTIONAL_FIELD(segment.includedContexts, obj, "includedContexts"); + PARSE_OPTIONAL_FIELD(segment.excludedContexts, obj, "excludedContexts"); + + PARSE_OPTIONAL_FIELD(segment.rules, obj, "rules"); + + return segment; +} + +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_value.cpp b/libs/internal/src/serialization/json_value.cpp index 551322763..6271463c6 100644 --- a/libs/internal/src/serialization/json_value.cpp +++ b/libs/internal/src/serialization/json_value.cpp @@ -83,5 +83,11 @@ void tag_invoke(boost::json::value_from_tag const&, } } +tl::expected tag_invoke( + boost::json::value_to_tag> const& tag, + boost::json::value const& json_value) { + return boost::json::value_to(json_value); +} + // NOLINTEND modernize-return-braced-init-list } // namespace launchdarkly diff --git a/libs/internal/src/serialization/value_mapping.cpp b/libs/internal/src/serialization/value_mapping.cpp index d8344efdb..db8d75c70 100644 --- a/libs/internal/src/serialization/value_mapping.cpp +++ b/libs/internal/src/serialization/value_mapping.cpp @@ -21,6 +21,23 @@ std::optional ValueAsOpt( return std::nullopt; } +template <> +std::optional> ValueAsOpt( + boost::json::object::const_iterator iterator, + boost::json::object::const_iterator end) { + if (iterator != end && iterator->value().is_array()) { + std::vector result; + for (auto const& item : iterator->value().as_array()) { + if (!item.is_string()) { + return std::nullopt; + } + result.emplace_back(item.as_string()); + } + return result; + } + return std::nullopt; +} + template <> bool ValueOrDefault(boost::json::object::const_iterator iterator, boost::json::object::const_iterator end, diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp new file mode 100644 index 000000000..63a1860ff --- /dev/null +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -0,0 +1,176 @@ +#include + +#include +#include +#include + +using namespace launchdarkly; + +TEST(SDKDataSetTests, DeserializesEmptyDataSet) { + auto result = + boost::json::value_to>( + boost::json::parse("{}")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->segments); +} + +TEST(SDKDataSetTests, ErrorOnInvalidSchema) { + auto result = + boost::json::value_to>( + boost::json::parse("[]")); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), JsonError::kSchemaFailure); +} + +TEST(SDKDataSetTests, DeserializesZeroSegments) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"segments":{}})")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->segments); +} + +TEST(SegmentTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(R"({"key":"foo", "version": 42})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); + + ASSERT_EQ(result.value()->version, 42); + ASSERT_EQ(result.value()->key, "foo"); +} + +TEST(SegmentTests, TolerantOfUnrecognizedFields) { + auto result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse( + R"({"key":"foo", "version": 42, "somethingRandom" : true})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result.value()); +} + +TEST(RuleTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"clauses": [{"attribute": "", "op": "in", "values": ["a"]}]})")); + ASSERT_TRUE(result); + + auto const& clauses = result->clauses; + ASSERT_EQ(clauses.size(), 1); + + auto const& clause = clauses.at(0); + ASSERT_EQ(clause.op, data_model::Segment::Clause::Op::kIn); +} + +TEST(RuleTests, TolerantOfUnrecognizedFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"somethingRandom": true, "clauses": [{"attribute": "", "op": "in", "values": ["a"]}]})")); + + ASSERT_TRUE(result); +} + +TEST(RuleTests, DeserializesSimpleAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "foo", "bucketBy" : "bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->bucketBy, AttributeReference("bar")); +} + +TEST(RuleTests, DeserializesPointerAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "foo", "bucketBy" : "/foo/bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->bucketBy, AttributeReference("/foo/bar")); +} + +TEST(RuleTests, DeserializesEscapedReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "foo", "bucketBy" : "/~1foo~1bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->bucketBy, AttributeReference("/~1foo~1bar")); +} + +TEST(RuleTests, DeserializesLiteralReference) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"bucketBy" : "/~1foo~1bar", "clauses": []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->bucketBy, + AttributeReference::FromLiteralStr("/~1foo~1bar")); +} + +TEST(ClauseTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"op": "segmentMatch", "values": []})")); + ASSERT_TRUE(result); + + ASSERT_EQ(result->op, data_model::Segment::Clause::Op::kSegmentMatch); + ASSERT_TRUE(result->values.empty()); +} + +TEST(ClauseTests, TolerantOfUnrecognizedFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"somethingRandom": true, "attribute": "", "op": "in", "values": ["a"]})")); + ASSERT_TRUE(result); +} + +TEST(ClauseTests, TolerantOfEmptyAttribute) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse( + R"({"attribute": "", "op": "segmentMatch", "values": ["a"]})")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->attribute); +} + +TEST(ClauseTests, TolerantOfUnrecognizedOperator) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "", "op": "notAnActualOperator", "values": ["a"]})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->op, data_model::Segment::Clause::Op::kUnrecognized); +} + +TEST(ClauseTests, DeserializesSimpleAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "foo", "op": "in", "values": ["a"], "contextKind" : "user"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, AttributeReference("foo")); +} + +TEST(ClauseTests, DeserializesPointerAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "/foo/bar", "op": "in", "values": ["a"], "contextKind" : "user"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, AttributeReference("/foo/bar")); +} + +TEST(ClauseTests, DeserializesEscapedReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "/~1foo~1bar", "op": "in", "values": ["a"], "contextKind" : "user"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, AttributeReference("/~1foo~1bar")); +} + +TEST(ClauseTests, DeserializesLiteralAttributeReference) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse( + R"({"attribute": "/foo/bar", "op": "in", "values": ["a"]})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->attribute, + AttributeReference::FromLiteralStr("/foo/bar")); +} diff --git a/libs/internal/tests/evaluation_result_test.cpp b/libs/internal/tests/evaluation_result_test.cpp index 7db22aa92..48956a3af 100644 --- a/libs/internal/tests/evaluation_result_test.cpp +++ b/libs/internal/tests/evaluation_result_test.cpp @@ -15,54 +15,49 @@ using launchdarkly::JsonError; using launchdarkly::Value; TEST(EvaluationResultTests, FromJsonAllFields) { - auto evaluation_result = - boost::json::value_to>( - boost::json::parse("{" - "\"version\": 12," - "\"flagVersion\": 24," - "\"trackEvents\": true," - "\"trackReason\": true," - "\"debugEventsUntilDate\": 1680555761," - "\"value\": {\"item\": 42}," - "\"variation\": 84," - "\"reason\": {" - "\"kind\":\"OFF\"," - "\"errorKind\":\"MALFORMED_FLAG\"," - "\"ruleIndex\":12," - "\"ruleId\":\"RULE_ID\"," - "\"prerequisiteKey\":\"PREREQ_KEY\"," - "\"inExperiment\":true," - "\"bigSegmentStatus\":\"STORE_ERROR\"" - "}" - "}")); + auto evaluation_result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{" + "\"version\": 12," + "\"flagVersion\": 24," + "\"trackEvents\": true," + "\"trackReason\": true," + "\"debugEventsUntilDate\": 1680555761," + "\"value\": {\"item\": 42}," + "\"variation\": 84," + "\"reason\": {" + "\"kind\":\"OFF\"," + "\"errorKind\":\"MALFORMED_FLAG\"," + "\"ruleIndex\":12," + "\"ruleId\":\"RULE_ID\"," + "\"prerequisiteKey\":\"PREREQ_KEY\"," + "\"inExperiment\":true," + "\"bigSegmentStatus\":\"STORE_ERROR\"" + "}" + "}")); EXPECT_TRUE(evaluation_result.has_value()); - EXPECT_EQ(12, evaluation_result.value().Version()); - EXPECT_EQ(24, evaluation_result.value().FlagVersion()); - EXPECT_TRUE(evaluation_result.value().TrackEvents()); - EXPECT_TRUE(evaluation_result.value().TrackReason()); + auto const& val = evaluation_result.value(); + EXPECT_TRUE(val.has_value()); + + EXPECT_EQ(12, val->Version()); + EXPECT_EQ(24, val->FlagVersion()); + EXPECT_TRUE(val->TrackEvents()); + EXPECT_TRUE(val->TrackReason()); EXPECT_EQ(std::chrono::system_clock::time_point{std::chrono::milliseconds{ 1680555761}}, - evaluation_result.value().DebugEventsUntilDate()); - EXPECT_EQ( - 42, - evaluation_result.value().Detail().Value().AsObject()["item"].AsInt()); - EXPECT_EQ(84, evaluation_result.value().Detail().VariationIndex()); + val->DebugEventsUntilDate()); + EXPECT_EQ(42, val->Detail().Value().AsObject()["item"].AsInt()); + EXPECT_EQ(84, val->Detail().VariationIndex()); EXPECT_EQ(EvaluationReason::Kind::kOff, - evaluation_result.value().Detail().Reason()->get().Kind()); + val->Detail().Reason()->get().Kind()); EXPECT_EQ(EvaluationReason::ErrorKind::kMalformedFlag, - evaluation_result.value().Detail().Reason()->get().ErrorKind()); - EXPECT_EQ(12, - evaluation_result.value().Detail().Reason()->get().RuleIndex()); - EXPECT_EQ("RULE_ID", - evaluation_result.value().Detail().Reason()->get().RuleId()); - EXPECT_EQ( - "PREREQ_KEY", - evaluation_result.value().Detail().Reason()->get().PrerequisiteKey()); - EXPECT_EQ("STORE_ERROR", - evaluation_result.value().Detail().Reason()->get().BigSegmentStatus()); - EXPECT_TRUE( - evaluation_result.value().Detail().Reason()->get().InExperiment()); + val->Detail().Reason()->get().ErrorKind()); + EXPECT_EQ(12, val->Detail().Reason()->get().RuleIndex()); + EXPECT_EQ("RULE_ID", val->Detail().Reason()->get().RuleId()); + EXPECT_EQ("PREREQ_KEY", val->Detail().Reason()->get().PrerequisiteKey()); + EXPECT_EQ("STORE_ERROR", val->Detail().Reason()->get().BigSegmentStatus()); + EXPECT_TRUE(val->Detail().Reason()->get().InExperiment()); } TEST(EvaluationResultTests, ToJsonAllFields) { @@ -91,29 +86,27 @@ TEST(EvaluationResultTests, ToJsonAllFields) { } TEST(EvaluationResultTests, FromJsonMinimalFields) { - auto evaluation_result = - boost::json::value_to>( - boost::json::parse("{" - "\"version\": 12," - "\"value\": {\"item\": 42}" - "}")); - - EXPECT_EQ(12, evaluation_result.value().Version()); - EXPECT_EQ(std::nullopt, evaluation_result.value().FlagVersion()); - EXPECT_FALSE(evaluation_result.value().TrackEvents()); - EXPECT_FALSE(evaluation_result.value().TrackReason()); - EXPECT_EQ(std::nullopt, evaluation_result.value().DebugEventsUntilDate()); - EXPECT_EQ( - 42, - evaluation_result.value().Detail().Value().AsObject()["item"].AsInt()); - EXPECT_EQ(std::nullopt, - evaluation_result.value().Detail().VariationIndex()); - EXPECT_EQ(std::nullopt, evaluation_result.value().Detail().Reason()); + auto evaluation_result = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{" + "\"version\": 12," + "\"value\": {\"item\": 42}" + "}")); + + auto const& value = evaluation_result.value(); + EXPECT_EQ(12, value->Version()); + EXPECT_EQ(std::nullopt, value->FlagVersion()); + EXPECT_FALSE(value->TrackEvents()); + EXPECT_FALSE(value->TrackReason()); + EXPECT_EQ(std::nullopt, value->DebugEventsUntilDate()); + EXPECT_EQ(42, value->Detail().Value().AsObject()["item"].AsInt()); + EXPECT_EQ(std::nullopt, value->Detail().VariationIndex()); + EXPECT_EQ(std::nullopt, value->Detail().Reason()); } TEST(EvaluationResultTests, FromMapOfResults) { - auto results = boost::json::value_to< - std::map>>( + auto results = boost::json::value_to, JsonError>>>( boost::json::parse("{" "\"flagA\":{" "\"version\": 12," @@ -124,26 +117,25 @@ TEST(EvaluationResultTests, FromMapOfResults) { "\"value\": false" "}" "}")); - - EXPECT_TRUE(results.at("flagA").value().Detail().Value().AsBool()); - EXPECT_FALSE(results.at("flagB").value().Detail().Value().AsBool()); + EXPECT_TRUE(results.at("flagA").value().value().Detail().Value().AsBool()); + EXPECT_FALSE(results.at("flagB").value().value().Detail().Value().AsBool()); } TEST(EvaluationResultTests, NoResultFieldsJson) { - auto results = - boost::json::value_to>( - boost::json::parse("{}")); + auto results = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{}")); - EXPECT_FALSE(results.has_value()); + EXPECT_FALSE(results); EXPECT_EQ(JsonError::kSchemaFailure, results.error()); } TEST(EvaluationResultTests, VersionWrongTypeJson) { - auto results = - boost::json::value_to>( - boost::json::parse("{\"version\": \"apple\"}")); + auto results = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse("{\"version\": \"apple\"}")); - EXPECT_FALSE(results.has_value()); + EXPECT_FALSE(results); EXPECT_EQ(JsonError::kSchemaFailure, results.error()); } From 4954f88780eddbfeae0e5c6465105b12fb8ea1d3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 30 Jun 2023 15:29:12 -0700 Subject: [PATCH 05/56] feat: flag data model (#156) This PR builds on #153, extending the deserialization to flag data. I've added to the `PARSE_FIELD` macro to handle the following cases: 1. Parse a field where the zero-value is a valid part of the domain: `PARSE_FIELD`. In other words, if the field is omitted or null, it will be filled in with a default value. Most common scenario. 2. Like (1), but supply a default value in case the field is null or omitted: `PARSE_FIELD_DEFAULT`. 3. Parse a field that doesn't have a valid default value, and so is represented as an `optional`: `PARSE_CONDITIONAL_FIELD`. For example, the value `""` wouldn't be a valid default for a unique key. 4. Parse a field that must be present in the JSON data: `PARSE_REQUIRED_FIELD`. Usage of this should be rare; the test would be if the SDK is totally unable to function without this. To achieve this, I've made the `tag_invoke`s operate on `expected, JsonError`'s, instead of `expected #include #include +#include #include #include @@ -65,8 +66,12 @@ static void BuildContextFromParams(launchdarkly::ContextBuilder& builder, if (single.custom) { for (auto const& [key, value] : *single.custom) { - attrs.Set(key, boost::json::value_to( - boost::json::parse(value.dump()))); + auto maybe_attr = boost::json::value_to< + tl::expected>( + boost::json::parse(value.dump())); + if (maybe_attr) { + attrs.Set(key, *maybe_attr); + } } } } @@ -126,9 +131,16 @@ tl::expected ContextConvert( tl::expected ClientEntity::Custom( CustomEventParams const& params) { - auto data = params.data ? boost::json::value_to( - boost::json::parse(params.data->dump())) - : launchdarkly::Value::Null(); + auto data = + params.data + ? boost::json::value_to< + tl::expected>( + boost::json::parse(params.data->dump())) + : launchdarkly::Value::Null(); + + if (!data) { + return tl::make_unexpected("couldn't parse custom event data"); + } if (params.omitNullData.value_or(false) && !params.metricValue && !params.data) { @@ -137,11 +149,11 @@ tl::expected ClientEntity::Custom( } if (!params.metricValue) { - client_->Track(params.eventKey, std::move(data)); + client_->Track(params.eventKey, std::move(*data)); return nlohmann::json{}; } - client_->Track(params.eventKey, std::move(data), *params.metricValue); + client_->Track(params.eventKey, std::move(*data), *params.metricValue); return nlohmann::json{}; } @@ -204,15 +216,19 @@ tl::expected ClientEntity::EvaluateDetail( } case ValueType::Any: case ValueType::Unspecified: { - auto fallback = boost::json::value_to( + auto maybe_fallback = boost::json::value_to< + tl::expected>( boost::json::parse(defaultVal.dump())); + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } /* This switcharoo from nlohmann/json to boost/json to Value, then * back is because we're using nlohmann/json for the test harness * protocol, but boost::json in the SDK. We could swap over to * boost::json entirely here to remove the awkwardness. */ - auto detail = client_->JsonVariationDetail(key, fallback); + auto detail = client_->JsonVariationDetail(key, *maybe_fallback); auto serialized = boost::json::serialize(boost::json::value_from(*detail)); @@ -264,15 +280,18 @@ tl::expected ClientEntity::Evaluate( } case ValueType::Any: case ValueType::Unspecified: { - auto fallback = boost::json::value_to( + auto maybe_fallback = boost::json::value_to< + tl::expected>( boost::json::parse(defaultVal.dump())); - + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } /* This switcharoo from nlohmann/json to boost/json to Value, then * back is because we're using nlohmann/json for the test harness * protocol, but boost::json in the SDK. We could swap over to * boost::json entirely here to remove the awkwardness. */ - auto evaluation = client_->JsonVariation(key, fallback); + auto evaluation = client_->JsonVariation(key, *maybe_fallback); auto serialized = boost::json::serialize(boost::json::value_from(evaluation)); diff --git a/libs/common/src/attribute_reference.cpp b/libs/common/src/attribute_reference.cpp index 150a3f915..c0594a5e3 100644 --- a/libs/common/src/attribute_reference.cpp +++ b/libs/common/src/attribute_reference.cpp @@ -170,6 +170,10 @@ std::string EscapeLiteral(std::string const& literal, } AttributeReference::AttributeReference(std::string str, bool literal) { + if (str.empty()) { + valid_ = false; + return; + } if (literal) { components_.push_back(str); // Literal starting with a '/' needs to be converted to an attribute diff --git a/libs/common/tests/value_test.cpp b/libs/common/tests/value_test.cpp index c4f3255d4..f9ad35fb9 100644 --- a/libs/common/tests/value_test.cpp +++ b/libs/common/tests/value_test.cpp @@ -1,14 +1,14 @@ #include -#include -#include -#include -#include #include -#include #include +#include +#include +#include +#include + // NOLINTBEGIN cppcoreguidelines-avoid-magic-numbers using BoostValue = boost::json::value; diff --git a/libs/internal/include/launchdarkly/context_filter.hpp b/libs/internal/include/launchdarkly/context_filter.hpp index 104e15cce..a4bbddb9a 100644 --- a/libs/internal/include/launchdarkly/context_filter.hpp +++ b/libs/internal/include/launchdarkly/context_filter.hpp @@ -1,13 +1,13 @@ #pragma once -#include -#include -#include +#include +#include #include -#include -#include +#include +#include +#include namespace launchdarkly { diff --git a/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp new file mode 100644 index 000000000..836dc20b4 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include + +namespace launchdarkly::data_model { + +/** + * The JSON data conditionally contains Attribute References (which are capable + * of addressing arbitrarily nested attributes in contexts) or Attribute Names, + * which are names of top-level attributes in contexts. + * + * In order to distinguish these two cases, inspection of a context kind field + * is necessary. The presence or absence of that field determines whether the + * data is an Attribute Reference or Attribute Name. + * + * Because this logic is needed in (3) places, it is factored out into this + * type. To use it, call + * boost::json::value_from, + * JsonError>>(json_value), where T is any type that defines the following: + * - kContextFieldName: name of the field containing the context kind + * - kReferenceFieldName: name of the field containing the attribute reference + * or attribute name' + * + * To ensure the field names don't go out of sync with the declared member + * variables, use the two macros defined below. + * @tparam Fields + */ +template +struct ContextAwareReference { + static_assert( + std::is_same::value && + std::is_same::value, + "T must define kContextFieldName and kReferenceFieldName as constexpr " + "static const char*"); +}; + +template +struct ContextAwareReference< + FieldNames, + typename std::enable_if< + std::is_same::value && + std::is_same::value>::type> { + using fields = FieldNames; + std::string contextKind; + AttributeReference reference; +}; + +#define DEFINE_CONTEXT_KIND_FIELD(name) \ + std::string name; \ + constexpr static const char* kContextFieldName = #name; + +#define DEFINE_ATTRIBUTE_REFERENCE_FIELD(name) \ + AttributeReference name; \ + constexpr static const char* kReferenceFieldName = #name; + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp new file mode 100644 index 000000000..8c3d0bc4e --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::data_model { + +struct Flag { + using ContextKind = std::string; + + struct Rollout { + enum class Kind { + kUnrecognized = 0, + kExperiment = 1, + kRollout = 2, + }; + + struct WeightedVariation { + std::uint64_t variation; + std::uint64_t weight; + bool untracked; + }; + + std::vector variations; + + Kind kind; + std::optional seed; + + DEFINE_ATTRIBUTE_REFERENCE_FIELD(bucketBy) + DEFINE_CONTEXT_KIND_FIELD(contextKind) + }; + + using Variation = std::uint64_t; + using VariationOrRollout = std::variant; + + struct Prerequisite { + std::string key; + std::uint64_t variation; + }; + + struct Target { + std::vector values; + std::uint64_t variation; + ContextKind contextKind; + }; + + struct Rule { + std::vector clauses; + VariationOrRollout variationOrRollout; + + bool trackEvents; + std::optional id; + }; + + struct ClientSideAvailability { + bool usingMobileKey; + bool usingEnvironmentId; + }; + + std::string key; + std::uint64_t version; + bool on; + VariationOrRollout fallthrough; + std::vector variations; + + std::vector prerequisites; + std::vector targets; + std::vector contextTargets; + std::vector rules; + std::optional offVariation; + bool clientSide; + ClientSideAvailability clientSideAvailability; + std::optional salt; + bool trackEvents; + bool trackEventsFallthrough; + std::optional debugEventsUntilDate; +}; +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp b/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp index 003f53907..db6f443bf 100644 --- a/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp +++ b/libs/internal/include/launchdarkly/data_model/item_descriptor.hpp @@ -1,11 +1,13 @@ #pragma once -#include #include + +#include +#include + #include #include #include -#include #include namespace launchdarkly::data_model { diff --git a/libs/internal/include/launchdarkly/data_model/rule_clause.hpp b/libs/internal/include/launchdarkly/data_model/rule_clause.hpp new file mode 100644 index 000000000..751f43fa7 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/rule_clause.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace launchdarkly::data_model { +struct Clause { + enum class Op { + kUnrecognized, /* didn't match any known operators */ + kIn, + kStartsWith, + kEndsWith, + kMatches, + kContains, + kLessThan, + kLessThanOrEqual, + kGreaterThan, + kGreaterThanOrEqual, + kBefore, + kAfter, + kSemVerEqual, + kSemVerLessThan, + kSemVerGreaterThan, + kSegmentMatch + }; + + Op op; + std::vector values; + bool negate; + + DEFINE_CONTEXT_KIND_FIELD(contextKind) + DEFINE_ATTRIBUTE_REFERENCE_FIELD(attribute) +}; +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp index d1f0c85d4..aa0d6be8e 100644 --- a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp +++ b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp @@ -1,10 +1,12 @@ #pragma once -#include #include #include -#include + +#include #include + +#include #include namespace launchdarkly::data_model { diff --git a/libs/internal/include/launchdarkly/data_model/segment.hpp b/libs/internal/include/launchdarkly/data_model/segment.hpp index 6794c08a6..63003b4a4 100644 --- a/libs/internal/include/launchdarkly/data_model/segment.hpp +++ b/libs/internal/include/launchdarkly/data_model/segment.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include #include @@ -14,61 +16,38 @@ namespace launchdarkly::data_model { struct Segment { + using Kind = std::string; struct Target { - std::string contextKind; + Kind contextKind; std::vector values; }; - struct Clause { - enum class Op { - kOmitted, /* represents empty string */ - kUnrecognized, /* didn't match any known operators */ - kIn, - kStartsWith, - kEndsWith, - kMatches, - kContains, - kLessThan, - kLessThanOrEqual, - kGreaterThan, - kGreaterThanOrEqual, - kBefore, - kAfter, - kSemVerEqual, - kSemVerLessThan, - kSemVerGreaterThan, - kSegmentMatch - }; - - std::optional attribute; - Op op; - std::vector values; - - std::optional negate; - std::optional contextKind; - }; - struct Rule { + using ReferenceType = ContextAwareReference; + std::vector clauses; std::optional id; std::optional weight; - std::optional bucketBy; - std::optional rolloutContextKind; + + DEFINE_CONTEXT_KIND_FIELD(rolloutContextKind) + DEFINE_ATTRIBUTE_REFERENCE_FIELD(bucketBy) }; std::string key; std::uint64_t version; - std::optional> included; - std::optional> excluded; - std::optional> includedContexts; - std::optional> excludedContexts; - std::optional> rules; + std::vector included; + std::vector excluded; + std::vector includedContexts; + std::vector excludedContexts; + std::vector rules; std::optional salt; - std::optional unbounded; - std::optional unboundedContextKind; + bool unbounded; + std::optional unboundedContextKind; std::optional generation; + // TODO(cwaldren): make Kind a real type that is deserialized, so we can + // make empty string an error. [[nodiscard]] inline std::uint64_t Version() const { return version; } }; } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/serialization/json_attributes.hpp b/libs/internal/include/launchdarkly/serialization/json_attributes.hpp index 5dbd20c36..c7be6612b 100644 --- a/libs/internal/include/launchdarkly/serialization/json_attributes.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_attributes.hpp @@ -1,9 +1,9 @@ #pragma once -#include - #include +#include + namespace launchdarkly { /** * Method used by boost::json for converting launchdarkly::Attributes into a diff --git a/libs/internal/include/launchdarkly/serialization/json_context.hpp b/libs/internal/include/launchdarkly/serialization/json_context.hpp index 491b7ebde..28c03b51c 100644 --- a/libs/internal/include/launchdarkly/serialization/json_context.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_context.hpp @@ -1,12 +1,11 @@ #pragma once -#include - -#include - #include #include +#include +#include + namespace launchdarkly { /** * Method used by boost::json for converting a launchdarkly::Context into a diff --git a/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp new file mode 100644 index 000000000..77b56af90 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +#include +#include + +namespace launchdarkly { + +template +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + using Type = data_model::ContextAwareReference; + + auto const& obj = json_value.as_object(); + + std::optional kind; + + PARSE_CONDITIONAL_FIELD(kind, obj, Type::fields::kContextFieldName); + + if (kind && *kind == "") { + // Empty string is not a valid kind. + return tl::make_unexpected(JsonError::kSchemaFailure); + } + + std::string attr_ref_or_name; + PARSE_FIELD_DEFAULT(attr_ref_or_name, obj, + Type::fields::kReferenceFieldName, "key"); + + if (kind) { + return Type{*kind, + AttributeReference::FromReferenceStr(attr_ref_or_name)}; + } + + return Type{"user", AttributeReference::FromLiteralStr(attr_ref_or_name)}; +} + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp b/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp index cdb1734a4..deb3d9dfe 100644 --- a/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_evaluation_reason.hpp @@ -1,11 +1,10 @@ #pragma once -#include "tl/expected.hpp" - -#include - #include -#include "json_errors.hpp" +#include + +#include +#include namespace launchdarkly { /** @@ -24,8 +23,8 @@ tl::expected tag_invoke( boost::json::value const& json_value); tl::expected tag_invoke( - boost::json::value_to_tag< - tl::expected> const& unused, + boost::json::value_to_tag> const& unused, boost::json::value const& json_value); void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp b/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp index 4dc5f7331..71f3f37c2 100644 --- a/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_evaluation_result.hpp @@ -1,11 +1,10 @@ #pragma once -#include "tl/expected.hpp" - -#include - #include -#include "json_errors.hpp" +#include + +#include +#include namespace launchdarkly { diff --git a/libs/internal/include/launchdarkly/serialization/json_flag.hpp b/libs/internal/include/launchdarkly/serialization/json_flag.hpp new file mode 100644 index 000000000..88c1edd57 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_flag.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, + JsonError> +tag_invoke(boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected, JsonError> +tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp b/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp index f96e6afee..a04ffe378 100644 --- a/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_item_descriptor.hpp @@ -1,13 +1,12 @@ #pragma once -#include - -#include - #include +#include #include #include -#include "json_errors.hpp" + +#include +#include namespace launchdarkly { diff --git a/libs/internal/include/launchdarkly/serialization/json_primitives.hpp b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp index 1c555af12..4ddb8f208 100644 --- a/libs/internal/include/launchdarkly/serialization/json_primitives.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp @@ -1,9 +1,10 @@ #pragma once +#include +#include + #include #include -#include -#include "json_errors.hpp" namespace launchdarkly { @@ -22,20 +23,20 @@ tl::expected>, JsonError> tag_invoke( return tl::unexpected(JsonError::kSchemaFailure); } - if (json_value.as_array().empty()) { - return std::nullopt; - } - auto const& arr = json_value.as_array(); std::vector items; items.reserve(arr.size()); for (auto const& item : arr) { auto eval_result = - boost::json::value_to>(item); + boost::json::value_to, JsonError>>( + item); if (!eval_result.has_value()) { return tl::unexpected(eval_result.error()); } - items.emplace_back(std::move(eval_result.value())); + auto maybe_val = eval_result.value(); + if (maybe_val) { + items.emplace_back(std::move(maybe_val.value())); + } } return items; } @@ -50,6 +51,11 @@ tl::expected, JsonError> tag_invoke( tl::expected, JsonError>> const& unused, boost::json::value const& json_value); +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value); + tl::expected, JsonError> tag_invoke( boost::json::value_to_tag< tl::expected, JsonError>> const& unused, @@ -69,9 +75,6 @@ tl::expected>, JsonError> tag_invoke( if (!json_value.is_object()) { return tl::unexpected(JsonError::kSchemaFailure); } - if (json_value.as_object().empty()) { - return std::nullopt; - } auto const& obj = json_value.as_object(); std::unordered_map descriptors; for (auto const& pair : obj) { @@ -109,7 +112,7 @@ tl::expected tag_invoke( if (!maybe_val.has_value()) { return tl::unexpected(maybe_val.error()); } - return maybe_val.value().value_or(T{}); + return maybe_val.value().value_or(T()); } } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp new file mode 100644 index 000000000..f0eab96a4 --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value); + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value); + +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp b/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp index 41563b497..f2501bfa3 100644 --- a/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_sdk_data_set.hpp @@ -1,11 +1,15 @@ #pragma once #include +#include + +#include namespace launchdarkly { -tl::expected tag_invoke( +tl::expected, JsonError> tag_invoke( boost::json::value_to_tag< - tl::expected> const& unused, + tl::expected, JsonError>> const& + unused, boost::json::value const& json_value); -} +} // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_segment.hpp b/libs/internal/include/launchdarkly/serialization/json_segment.hpp index f80728345..ce73c719b 100644 --- a/libs/internal/include/launchdarkly/serialization/json_segment.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_segment.hpp @@ -3,36 +3,24 @@ #include #include +#include + namespace launchdarkly { tl::expected, JsonError> tag_invoke( boost::json::value_to_tag, JsonError>> const& unused, boost::json::value const& json_value); -tl::expected tag_invoke( - boost::json::value_to_tag< - tl::expected> const& unused, - boost::json::value const& json_value); - -tl::expected tag_invoke( +tl::expected, JsonError> tag_invoke( boost::json::value_to_tag< - tl::expected> const& unused, + tl::expected, + JsonError>> const& unused, boost::json::value const& json_value); -tl::expected tag_invoke( - boost::json::value_to_tag< - tl::expected> const& unused, - boost::json::value const& json_value); - -tl::expected, JsonError> -tag_invoke(boost::json::value_to_tag< - tl::expected, - JsonError>> const& unused, - boost::json::value const& json_value); - -tl::expected tag_invoke( +tl::expected, JsonError> tag_invoke( boost::json::value_to_tag< - tl::expected> const& unused, + tl::expected, + JsonError>> const& unused, boost::json::value const& json_value); } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_value.hpp b/libs/internal/include/launchdarkly/serialization/json_value.hpp index 07cb8f0d3..c99b1f5bb 100644 --- a/libs/internal/include/launchdarkly/serialization/json_value.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_value.hpp @@ -1,9 +1,9 @@ #pragma once -#include - #include #include + +#include #include namespace launchdarkly { @@ -12,13 +12,14 @@ namespace launchdarkly { * launchdarkly::Value. * @return A Value representation of the boost::json::value. */ -Value tag_invoke(boost::json::value_to_tag const&, - boost::json::value const&); - -tl::expected tag_invoke( - boost::json::value_to_tag> const&, +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const&, boost::json::value const&); +Value tag_invoke(boost::json::value_to_tag const&, + boost::json::value const& json_value); + /** * Method used by boost::json for converting a launchdarkly::Value into a * boost::json::value. diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index 686ae95a8..d2aa82ea3 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -1,44 +1,100 @@ #pragma once -#include -#include #include #include + +#include +#include #include -#define PARSE_FIELD(field, it) \ - if (auto result = \ - boost::json::value_to>( \ - it->value())) { \ - field = result.value(); \ - } else { \ - return tl::make_unexpected(result.error()); \ - } +// Parses a field, propagating an error if the field's value is of the wrong +// type. If the field was null or omitted in the data, it is set to +// default_value. +#define PARSE_FIELD_DEFAULT(field, obj, key, default_value) \ + do { \ + std::optional maybe_val; \ + PARSE_CONDITIONAL_FIELD(maybe_val, obj, key); \ + field = maybe_val.value_or(default_value); \ + } while (0) + +// Parses a field, propagating an error if the field's value is of the wrong +// type. Intended for fields where the "zero value" of that field is a valid +// member of the domain of that field. If the "zero value" of the field is meant +// to denote absence of that field, rather than a valid member of the domain, +// then use PARSE_CONDITIONAL_FIELD in order to avoid discarding the information +// of whether that field was present or not. +#define PARSE_FIELD(field, obj, key) \ + do { \ + static_assert(std::is_default_constructible_v && \ + "field must be default-constructible"); \ + PARSE_FIELD_DEFAULT(field, obj, key, decltype(field){}); \ + } while (0) -// Attempts to parse a field only if it exists in the data. Propagates an error -// if the field's destination type is not compatible with the data. -#define PARSE_OPTIONAL_FIELD(field, obj, key) \ - do { \ - auto const& it = obj.find(key); \ - if (it != obj.end()) { \ - PARSE_FIELD(field, it); \ - } \ +// Parses a field that is conditional and/or has no valid default value. +// This would be the case for fields that depend on the existence of some other +// field. Another scenario would be a string field representing enum values, +// where there's no default defined/empty string is meaningless. It will +// propagate an error if the field's value is of the wrong type. Intended to be +// called on fields of type std::optional. +#define PARSE_CONDITIONAL_FIELD(field, obj, key) \ + do { \ + auto const& it = obj.find(key); \ + if (it != obj.end()) { \ + if (auto result = boost::json::value_to< \ + tl::expected>(it->value())) { \ + field = result.value(); \ + } else { \ + /* Field was of wrong type. */ \ + return tl::make_unexpected(result.error()); \ + } \ + } \ } while (0) -// Propagates an error upwards if the specified field isn't present in the -// data. -#define PARSE_REQUIRED_FIELD(field, obj, key) \ +// Parses a field, propagating an error if it is omitted/null or if the field's +// value is of the wrong type. Use only if the field *must* be present in the +// JSON document. Think twice; this is unlikely - most fields have a +// well-defined default value that can be used if not present. +#define PARSE_REQUIRED_FIELD(field, obj, key) \ + do { \ + auto const& it = obj.find(key); \ + if (it == obj.end()) { \ + /* Ideally report that field is missing, instead of generic \ + * failure */ \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + auto result = boost::json::value_to< \ + tl::expected, JsonError>>( \ + it->value()); \ + if (!result) { \ + /* The field's value is of the wrong type. */ \ + return tl::make_unexpected(result.error()); \ + } \ + /* We have the field, but its value might be null. */ \ + auto const& maybe_val = result.value(); \ + if (!maybe_val) { \ + /* Ideally report that the field was null, instead of generic \ + * failure. */ \ + return tl::make_unexpected(JsonError::kSchemaFailure); \ + } \ + field = std::move(*maybe_val); \ + } while (0) + +#define REQUIRE_OBJECT(value) \ do { \ - auto const& it = obj.find(key); \ - if (it == obj.end()) { \ + if (json_value.is_null()) { \ + return std::nullopt; \ + } \ + if (!json_value.is_object()) { \ return tl::make_unexpected(JsonError::kSchemaFailure); \ } \ - PARSE_FIELD(field, it); \ } while (0) -#define REQUIRE_OBJECT(value) \ +#define REQUIRE_STRING(value) \ do { \ - if (!json_value.is_object()) { \ + if (json_value.is_null()) { \ + return std::nullopt; \ + } \ + if (!json_value.is_string()) { \ return tl::make_unexpected(JsonError::kSchemaFailure); \ } \ } while (0) diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index 26e9249ea..810529c06 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -35,6 +35,8 @@ add_library(${LIBNAME} OBJECT serialization/json_sdk_data_set.cpp serialization/json_segment.cpp serialization/json_primitives.cpp + serialization/json_rule_clause.cpp + serialization/json_flag.cpp encoding/base_64.cpp encoding/sha_256.cpp) diff --git a/libs/internal/src/serialization/json_attributes.cpp b/libs/internal/src/serialization/json_attributes.cpp index 763433889..f9ba610a3 100644 --- a/libs/internal/src/serialization/json_attributes.cpp +++ b/libs/internal/src/serialization/json_attributes.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace launchdarkly { void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/src/serialization/json_context.cpp b/libs/internal/src/serialization/json_context.cpp index 3c667ff8a..cbad0bb7f 100644 --- a/libs/internal/src/serialization/json_context.cpp +++ b/libs/internal/src/serialization/json_context.cpp @@ -1,8 +1,11 @@ #include #include #include +#include #include +#include + #include #include @@ -103,7 +106,12 @@ std::optional ParseSingle(ContextBuilder& builder, attr == meta_iter || attr->value().is_null()) { continue; } - attrs.Set(attr->key(), boost::json::value_to(attr->value())); + auto maybe_unmarshalled_attr = + boost::json::value_to>( + attr->value()); + if (maybe_unmarshalled_attr) { + attrs.Set(attr->key(), maybe_unmarshalled_attr.value()); + } } return std::nullopt; diff --git a/libs/internal/src/serialization/json_evaluation_reason.cpp b/libs/internal/src/serialization/json_evaluation_reason.cpp index 517281dd4..afd6772e4 100644 --- a/libs/internal/src/serialization/json_evaluation_reason.cpp +++ b/libs/internal/src/serialization/json_evaluation_reason.cpp @@ -1,7 +1,9 @@ +#include #include #include #include +#include #include diff --git a/libs/internal/src/serialization/json_evaluation_result.cpp b/libs/internal/src/serialization/json_evaluation_result.cpp index 14406af75..7b7b8a335 100644 --- a/libs/internal/src/serialization/json_evaluation_result.cpp +++ b/libs/internal/src/serialization/json_evaluation_result.cpp @@ -1,9 +1,11 @@ +#include #include #include #include #include #include +#include namespace launchdarkly { @@ -58,7 +60,12 @@ tl::expected, JsonError> tag_invoke( if (value_iter == json_obj.end()) { return tl::unexpected(JsonError::kSchemaFailure); } - auto value = boost::json::value_to(value_iter->value()); + + auto maybe_value = boost::json::value_to>( + value_iter->value()); + if (!maybe_value) { + return tl::unexpected(maybe_value.error()); + } auto* variation_iter = json_obj.find("variation"); auto variation = ValueAsOpt(variation_iter, json_obj.end()); @@ -78,7 +85,7 @@ tl::expected, JsonError> tag_invoke( track_events, track_reason, debug_events_until_date, - EvaluationDetailInternal(value, variation, + EvaluationDetailInternal(*maybe_value, variation, std::make_optional(reason.value()))}; } // We could not parse the reason. @@ -92,7 +99,7 @@ tl::expected, JsonError> tag_invoke( track_events, track_reason, debug_events_until_date, - EvaluationDetailInternal(value, variation, std::nullopt)}; + EvaluationDetailInternal(*maybe_value, variation, std::nullopt)}; } void tag_invoke(boost::json::value_from_tag const& unused, diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp new file mode 100644 index 000000000..4dd40e92e --- /dev/null +++ b/libs/internal/src/serialization/json_flag.cpp @@ -0,0 +1,209 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Rollout rollout; + + PARSE_FIELD(rollout.variations, obj, "variations"); + PARSE_FIELD_DEFAULT(rollout.kind, obj, "kind", + data_model::Flag::Rollout::Kind::kRollout); + PARSE_CONDITIONAL_FIELD(rollout.seed, obj, "seed"); + + auto kind_and_bucket_by = boost::json::value_to, + JsonError>>(json_value); + if (!kind_and_bucket_by) { + return tl::make_unexpected(kind_and_bucket_by.error()); + } + + rollout.contextKind = kind_and_bucket_by->contextKind; + rollout.bucketBy = kind_and_bucket_by->reference; + + return rollout; +} + +tl::expected, + JsonError> +tag_invoke(boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Rollout::WeightedVariation weighted_variation; + PARSE_FIELD(weighted_variation.variation, obj, "variation"); + PARSE_FIELD(weighted_variation.weight, obj, "weight"); + PARSE_FIELD(weighted_variation.untracked, obj, "untracked"); + return weighted_variation; +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_STRING(json_value); + + auto const& str = json_value.as_string(); + if (str == "experiment") { + return data_model::Flag::Rollout::Kind::kExperiment; + } else if (str == "rollout") { + return data_model::Flag::Rollout::Kind::kRollout; + } else { + return data_model::Flag::Rollout::Kind::kUnrecognized; + } +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Prerequisite prerequisite; + PARSE_REQUIRED_FIELD(prerequisite.key, obj, "key"); + PARSE_FIELD(prerequisite.variation, obj, "variation"); + return prerequisite; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Target target; + PARSE_FIELD(target.values, obj, "values"); + PARSE_FIELD(target.variation, obj, "variation"); + PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", "user"); + return target; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::Rule rule; + + PARSE_FIELD(rule.trackEvents, obj, "trackEvents"); + PARSE_FIELD(rule.clauses, obj, "clauses"); + PARSE_CONDITIONAL_FIELD(rule.id, obj, "id"); + + auto variation_or_rollout = boost::json::value_to, JsonError>>( + json_value); + if (!variation_or_rollout) { + return tl::make_unexpected(variation_or_rollout.error()); + } + + rule.variationOrRollout = + variation_or_rollout->value_or(data_model::Flag::Variation(0)); + + return rule; +} + +tl::expected, JsonError> +tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Flag::ClientSideAvailability client_side_availability; + PARSE_FIELD(client_side_availability.usingEnvironmentId, obj, + "usingEnvironmentId"); + PARSE_FIELD(client_side_availability.usingMobileKey, obj, "usingMobileKey"); + return client_side_availability; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + + auto const& obj = json_value.as_object(); + + data_model::Flag flag; + + PARSE_REQUIRED_FIELD(flag.key, obj, "key"); + + PARSE_CONDITIONAL_FIELD(flag.debugEventsUntilDate, obj, + "debugEventsUntilDate"); + PARSE_CONDITIONAL_FIELD(flag.salt, obj, "salt"); + PARSE_CONDITIONAL_FIELD(flag.offVariation, obj, "offVariation"); + + PARSE_FIELD(flag.version, obj, "version"); + PARSE_FIELD(flag.on, obj, "on"); + PARSE_FIELD(flag.variations, obj, "variations"); + + PARSE_FIELD(flag.prerequisites, obj, "prerequisites"); + PARSE_FIELD(flag.targets, obj, "targets"); + PARSE_FIELD(flag.contextTargets, obj, "contextTargets"); + PARSE_FIELD(flag.rules, obj, "rules"); + PARSE_FIELD(flag.clientSide, obj, "clientSide"); + PARSE_FIELD(flag.clientSideAvailability, obj, "clientSideAvailability"); + PARSE_FIELD(flag.trackEvents, obj, "trackEvents"); + PARSE_FIELD(flag.trackEventsFallthrough, obj, "trackEventsFallthrough"); + PARSE_FIELD(flag.fallthrough, obj, "fallthrough"); + return flag; +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + std::optional rollout; + PARSE_CONDITIONAL_FIELD(rollout, obj, "rollout"); + + if (rollout) { + return std::make_optional(*rollout); + } + + data_model::Flag::Variation variation; + PARSE_REQUIRED_FIELD(variation, obj, "variation"); + + return std::make_optional(variation); +} + +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_primitives.cpp b/libs/internal/src/serialization/json_primitives.cpp index 150d42cbe..ea38767bf 100644 --- a/libs/internal/src/serialization/json_primitives.cpp +++ b/libs/internal/src/serialization/json_primitives.cpp @@ -12,9 +12,6 @@ tl::expected, JsonError> tag_invoke( if (!json_value.is_bool()) { return tl::unexpected(JsonError::kSchemaFailure); } - if (!json_value.as_bool()) { - return std::nullopt; - } return json_value.as_bool(); } @@ -29,7 +26,31 @@ tl::expected, JsonError> tag_invoke( if (!json_value.is_number()) { return tl::unexpected(JsonError::kSchemaFailure); } - return json_value.to_number(); + boost::json::error_code ec; + auto val = json_value.to_number(ec); + if (ec) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return val; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (json_value.is_null()) { + return std::nullopt; + } + if (!json_value.is_number()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + boost::json::error_code ec; + auto val = json_value.to_number(ec); + if (ec) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return val; } tl::expected, JsonError> tag_invoke( @@ -43,10 +64,6 @@ tl::expected, JsonError> tag_invoke( if (!json_value.is_string()) { return tl::unexpected(JsonError::kSchemaFailure); } - if (json_value.as_string().empty()) { - return std::nullopt; - } return std::string(json_value.as_string()); } - } // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_rule_clause.cpp b/libs/internal/src/serialization/json_rule_clause.cpp new file mode 100644 index 000000000..df6530d23 --- /dev/null +++ b/libs/internal/src/serialization/json_rule_clause.cpp @@ -0,0 +1,101 @@ +#include +#include +#include +#include +#include +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_OBJECT(json_value); + auto const& obj = json_value.as_object(); + + data_model::Clause clause; + + PARSE_REQUIRED_FIELD(clause.op, obj, "op"); + PARSE_FIELD(clause.values, obj, "values"); + PARSE_FIELD(clause.negate, obj, "negate"); + + auto kind_and_attr = boost::json::value_to, JsonError>>( + json_value); + if (!kind_and_attr) { + return tl::make_unexpected(kind_and_attr.error()); + } + + clause.contextKind = kind_and_attr->contextKind; + clause.attribute = kind_and_attr->reference; + return clause; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_STRING(json_value); + + auto const& str = json_value.as_string(); + + if (str == "") { + // Treating empty string as indicating the field is absent, but could + // also treat it as a valid but unknown value (like kUnrecognized.) + return std::nullopt; + } else if (str == "in") { + return data_model::Clause::Op::kIn; + } else if (str == "endsWith") { + return data_model::Clause::Op::kEndsWith; + } else if (str == "startsWith") { + return data_model::Clause::Op::kStartsWith; + } else if (str == "matches") { + return data_model::Clause::Op::kMatches; + } else if (str == "contains") { + return data_model::Clause::Op::kContains; + } else if (str == "lessThan") { + return data_model::Clause::Op::kLessThan; + } else if (str == "lessThanOrEqual") { + return data_model::Clause::Op::kLessThanOrEqual; + } else if (str == "greaterThan") { + return data_model::Clause::Op::kGreaterThan; + } else if (str == "greaterThanOrEqual") { + return data_model::Clause::Op::kGreaterThanOrEqual; + } else if (str == "before") { + return data_model::Clause::Op::kBefore; + } else if (str == "after") { + return data_model::Clause::Op::kAfter; + } else if (str == "semVerEqual") { + return data_model::Clause::Op::kSemVerEqual; + } else if (str == "semVerLessThan") { + return data_model::Clause::Op::kSemVerLessThan; + } else if (str == "semVerGreaterThan") { + return data_model::Clause::Op::kSemVerGreaterThan; + } else if (str == "segmentMatch") { + return data_model::Clause::Op::kSegmentMatch; + } else { + return data_model::Clause::Op::kUnrecognized; + } +} + +tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + auto maybe_op = boost::json::value_to< + tl::expected, JsonError>>( + json_value); + if (!maybe_op) { + return tl::unexpected(maybe_op.error()); + } + return maybe_op.value().value_or(data_model::Clause::Op::kUnrecognized); +} + +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_sdk_data_set.cpp b/libs/internal/src/serialization/json_sdk_data_set.cpp index 4b02546bc..2c4f4afe3 100644 --- a/libs/internal/src/serialization/json_sdk_data_set.cpp +++ b/libs/internal/src/serialization/json_sdk_data_set.cpp @@ -1,13 +1,15 @@ #include +#include #include #include #include #include namespace launchdarkly { -tl::expected tag_invoke( +tl::expected, JsonError> tag_invoke( boost::json::value_to_tag< - tl::expected> const& unused, + tl::expected, JsonError>> const& + unused, boost::json::value const& json_value) { boost::ignore_unused(unused); @@ -17,7 +19,7 @@ tl::expected tag_invoke( data_model::SDKDataSet data_set; - PARSE_OPTIONAL_FIELD(data_set.segments, obj, "segments"); + PARSE_CONDITIONAL_FIELD(data_set.segments, obj, "segments"); return data_set; } diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp index 57482b7df..3ef22453b 100644 --- a/libs/internal/src/serialization/json_segment.cpp +++ b/libs/internal/src/serialization/json_segment.cpp @@ -1,14 +1,17 @@ #include +#include +#include #include +#include #include -#include #include namespace launchdarkly { -tl::expected tag_invoke( +tl::expected, JsonError> tag_invoke( boost::json::value_to_tag< - tl::expected> const& unused, + tl::expected, + JsonError>> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); @@ -17,15 +20,17 @@ tl::expected tag_invoke( data_model::Segment::Target target; - PARSE_REQUIRED_FIELD(target.contextKind, obj, "contextKind"); - PARSE_REQUIRED_FIELD(target.values, obj, "values"); + PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", "user"); + + PARSE_FIELD(target.values, obj, "values"); return target; } -tl::expected tag_invoke( +tl::expected, JsonError> tag_invoke( boost::json::value_to_tag< - tl::expected> const& unused, + tl::expected, + JsonError>> const& unused, boost::json::value const& json_value) { boost::ignore_unused(unused); @@ -34,126 +39,24 @@ tl::expected tag_invoke( data_model::Segment::Rule rule; - PARSE_REQUIRED_FIELD(rule.clauses, obj, "clauses"); + PARSE_FIELD(rule.clauses, obj, "clauses"); - PARSE_OPTIONAL_FIELD(rule.rolloutContextKind, obj, "rolloutContextKind"); - PARSE_OPTIONAL_FIELD(rule.weight, obj, "weight"); - PARSE_OPTIONAL_FIELD(rule.id, obj, "id"); + PARSE_CONDITIONAL_FIELD(rule.weight, obj, "weight"); + PARSE_CONDITIONAL_FIELD(rule.id, obj, "id"); - std::optional literal_or_ref; - PARSE_OPTIONAL_FIELD(literal_or_ref, obj, "bucketBy"); + auto kind_and_bucket_by = boost::json::value_to, + JsonError>>(json_value); + if (!kind_and_bucket_by) { + return tl::make_unexpected(kind_and_bucket_by.error()); + } - rule.bucketBy = MapOpt( - literal_or_ref, - [has_context = rule.rolloutContextKind.has_value()](auto&& ref) { - if (has_context) { - return AttributeReference::FromReferenceStr(ref); - } else { - return AttributeReference::FromLiteralStr(ref); - } - }); + rule.bucketBy = kind_and_bucket_by->reference; + rule.rolloutContextKind = kind_and_bucket_by->contextKind; return rule; } -tl::expected tag_invoke( - boost::json::value_to_tag< - tl::expected> const& unused, - boost::json::value const& json_value) { - boost::ignore_unused(unused); - - REQUIRE_OBJECT(json_value); - auto const& obj = json_value.as_object(); - - data_model::Segment::Clause clause; - - PARSE_REQUIRED_FIELD(clause.op, obj, "op"); - PARSE_REQUIRED_FIELD(clause.values, obj, "values"); - - PARSE_OPTIONAL_FIELD(clause.negate, obj, "negate"); - PARSE_OPTIONAL_FIELD(clause.contextKind, obj, "contextKind"); - - std::optional literal_or_ref; - PARSE_OPTIONAL_FIELD(literal_or_ref, obj, "attribute"); - - clause.attribute = MapOpt( - literal_or_ref, - [has_context = clause.contextKind.has_value()](auto&& ref) { - if (has_context) { - return AttributeReference::FromReferenceStr(ref); - } else { - return AttributeReference::FromLiteralStr(ref); - } - }); - - return clause; -} - -tl::expected, JsonError> -tag_invoke(boost::json::value_to_tag< - tl::expected, - JsonError>> const& unused, - boost::json::value const& json_value) { - boost::ignore_unused(unused); - if (json_value.is_null()) { - return std::nullopt; - } - if (!json_value.is_string()) { - return tl::unexpected(JsonError::kSchemaFailure); - } - if (json_value.as_string().empty()) { - return std::nullopt; - } - auto const& str = json_value.as_string(); - - if (str == "in") { - return data_model::Segment::Clause::Op::kIn; - } else if (str == "endsWith") { - return data_model::Segment::Clause::Op::kEndsWith; - } else if (str == "startsWith") { - return data_model::Segment::Clause::Op::kStartsWith; - } else if (str == "matches") { - return data_model::Segment::Clause::Op::kMatches; - } else if (str == "contains") { - return data_model::Segment::Clause::Op::kContains; - } else if (str == "lessThan") { - return data_model::Segment::Clause::Op::kLessThan; - } else if (str == "lessThanOrEqual") { - return data_model::Segment::Clause::Op::kLessThanOrEqual; - } else if (str == "greaterThan") { - return data_model::Segment::Clause::Op::kGreaterThan; - } else if (str == "greaterThanOrEqual") { - return data_model::Segment::Clause::Op::kGreaterThanOrEqual; - } else if (str == "before") { - return data_model::Segment::Clause::Op::kBefore; - } else if (str == "after") { - return data_model::Segment::Clause::Op::kAfter; - } else if (str == "semVerEqual") { - return data_model::Segment::Clause::Op::kSemVerEqual; - } else if (str == "semVerLessThan") { - return data_model::Segment::Clause::Op::kSemVerLessThan; - } else if (str == "semVerGreaterThan") { - return data_model::Segment::Clause::Op::kSemVerGreaterThan; - } else if (str == "segmentMatch") { - return data_model::Segment::Clause::Op::kSegmentMatch; - } else { - return data_model::Segment::Clause::Op::kUnrecognized; - } -} - -tl::expected tag_invoke( - boost::json::value_to_tag< - tl::expected> const& unused, - boost::json::value const& json_value) { - boost::ignore_unused(unused); - auto maybe_op = boost::json::value_to, JsonError>>(json_value); - if (!maybe_op) { - return tl::unexpected(maybe_op.error()); - } - return maybe_op.value().value_or(data_model::Segment::Clause::Op::kOmitted); -} - tl::expected, JsonError> tag_invoke( boost::json::value_to_tag, JsonError>> const& unused, @@ -179,17 +82,17 @@ tl::expected, JsonError> tag_invoke( PARSE_REQUIRED_FIELD(segment.key, obj, "key"); PARSE_REQUIRED_FIELD(segment.version, obj, "version"); - PARSE_OPTIONAL_FIELD(segment.excluded, obj, "excluded"); - PARSE_OPTIONAL_FIELD(segment.included, obj, "included"); - - PARSE_OPTIONAL_FIELD(segment.generation, obj, "generation"); - PARSE_OPTIONAL_FIELD(segment.salt, obj, "salt"); - PARSE_OPTIONAL_FIELD(segment.unbounded, obj, "unbounded"); - - PARSE_OPTIONAL_FIELD(segment.includedContexts, obj, "includedContexts"); - PARSE_OPTIONAL_FIELD(segment.excludedContexts, obj, "excludedContexts"); - - PARSE_OPTIONAL_FIELD(segment.rules, obj, "rules"); + PARSE_CONDITIONAL_FIELD(segment.generation, obj, "generation"); + PARSE_CONDITIONAL_FIELD(segment.salt, obj, "salt"); + PARSE_CONDITIONAL_FIELD(segment.unboundedContextKind, obj, + "unboundedContextKind"); + + PARSE_FIELD(segment.excluded, obj, "excluded"); + PARSE_FIELD(segment.included, obj, "included"); + PARSE_FIELD(segment.unbounded, obj, "unbounded"); + PARSE_FIELD(segment.includedContexts, obj, "includedContexts"); + PARSE_FIELD(segment.excludedContexts, obj, "excludedContexts"); + PARSE_FIELD(segment.rules, obj, "rules"); return segment; } diff --git a/libs/internal/src/serialization/json_value.cpp b/libs/internal/src/serialization/json_value.cpp index 6271463c6..704c71f70 100644 --- a/libs/internal/src/serialization/json_value.cpp +++ b/libs/internal/src/serialization/json_value.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -8,9 +9,12 @@ namespace launchdarkly { // constructors. Replacing them with braced init lists would result in all types // being lists. -Value tag_invoke(boost::json::value_to_tag const& unused, - boost::json::value const& json_value) { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& unused, + boost::json::value const& json_value) { boost::ignore_unused(unused); + // The name of the function needs to be tag_invoke for boost::json. // The conditions in these switches explicitly use the constructors, because @@ -32,7 +36,12 @@ Value tag_invoke(boost::json::value_to_tag const& unused, auto vec = json_value.as_array(); std::vector values; for (auto const& item : vec) { - values.push_back(boost::json::value_to(item)); + auto value = + boost::json::value_to>(item); + if (!value) { + return tl::make_unexpected(value.error()); + } + values.emplace_back(std::move(*value)); } return Value(values); } @@ -40,8 +49,13 @@ Value tag_invoke(boost::json::value_to_tag const& unused, auto& map = json_value.as_object(); std::map values; for (auto const& pair : map) { - auto value = boost::json::value_to(pair.value()); - values.emplace(pair.key().data(), std::move(value)); + auto value = + boost::json::value_to>( + pair.value()); + if (!value) { + return tl::make_unexpected(value.error()); + } + values.emplace(pair.key().data(), std::move(*value)); } return Value(std::move(values)); } @@ -51,6 +65,13 @@ Value tag_invoke(boost::json::value_to_tag const& unused, assert(!"All types need to be handled."); } +Value tag_invoke(boost::json::value_to_tag const&, + boost::json::value const& json_value) { + auto val = + boost::json::value_to>(json_value); + return val ? std::move(*val) : Value(); +} + void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, Value const& ld_value) { diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 63a1860ff..a718933d1 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -1,6 +1,8 @@ #include +#include #include +#include #include #include @@ -27,7 +29,8 @@ TEST(SDKDataSetTests, DeserializesZeroSegments) { boost::json::value_to>( boost::json::parse(R"({"segments":{}})")); ASSERT_TRUE(result); - ASSERT_FALSE(result->segments); + ASSERT_TRUE(result->segments); + ASSERT_TRUE(result->segments->empty()); } TEST(SegmentTests, DeserializesMinimumValid) { @@ -50,7 +53,7 @@ TEST(SegmentTests, TolerantOfUnrecognizedFields) { ASSERT_TRUE(result.value()); } -TEST(RuleTests, DeserializesMinimumValid) { +TEST(SegmentRuleTests, DeserializesMinimumValid) { auto result = boost::json::value_to< tl::expected>(boost::json::parse( R"({"clauses": [{"attribute": "", "op": "in", "values": ["a"]}]})")); @@ -60,10 +63,10 @@ TEST(RuleTests, DeserializesMinimumValid) { ASSERT_EQ(clauses.size(), 1); auto const& clause = clauses.at(0); - ASSERT_EQ(clause.op, data_model::Segment::Clause::Op::kIn); + ASSERT_EQ(clause.op, data_model::Clause::Op::kIn); } -TEST(RuleTests, TolerantOfUnrecognizedFields) { +TEST(SegmentRuleTests, TolerantOfUnrecognizedFields) { auto result = boost::json::value_to< tl::expected>(boost::json::parse( R"({"somethingRandom": true, "clauses": [{"attribute": "", "op": "in", "values": ["a"]}]})")); @@ -71,7 +74,7 @@ TEST(RuleTests, TolerantOfUnrecognizedFields) { ASSERT_TRUE(result); } -TEST(RuleTests, DeserializesSimpleAttributeReference) { +TEST(SegmentRuleTests, DeserializesSimpleAttributeReference) { auto result = boost::json::value_to< tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "bar", "clauses": []})")); @@ -80,7 +83,7 @@ TEST(RuleTests, DeserializesSimpleAttributeReference) { ASSERT_EQ(result->bucketBy, AttributeReference("bar")); } -TEST(RuleTests, DeserializesPointerAttributeReference) { +TEST(SegmentRuleTests, DeserializesPointerAttributeReference) { auto result = boost::json::value_to< tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "/foo/bar", "clauses": []})")); @@ -89,7 +92,7 @@ TEST(RuleTests, DeserializesPointerAttributeReference) { ASSERT_EQ(result->bucketBy, AttributeReference("/foo/bar")); } -TEST(RuleTests, DeserializesEscapedReference) { +TEST(SegmentRuleTests, DeserializesEscapedReference) { auto result = boost::json::value_to< tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "/~1foo~1bar", "clauses": []})")); @@ -98,7 +101,7 @@ TEST(RuleTests, DeserializesEscapedReference) { ASSERT_EQ(result->bucketBy, AttributeReference("/~1foo~1bar")); } -TEST(RuleTests, DeserializesLiteralReference) { +TEST(SegmentRuleTests, DeserializesLiteralAttributeName) { auto result = boost::json::value_to< tl::expected>( boost::json::parse(R"({"bucketBy" : "/~1foo~1bar", "clauses": []})")); @@ -108,42 +111,42 @@ TEST(RuleTests, DeserializesLiteralReference) { } TEST(ClauseTests, DeserializesMinimumValid) { - auto result = boost::json::value_to< - tl::expected>( - boost::json::parse(R"({"op": "segmentMatch", "values": []})")); + auto result = + boost::json::value_to>( + boost::json::parse(R"({"op": "segmentMatch", "values": []})")); ASSERT_TRUE(result); - ASSERT_EQ(result->op, data_model::Segment::Clause::Op::kSegmentMatch); + ASSERT_EQ(result->op, data_model::Clause::Op::kSegmentMatch); ASSERT_TRUE(result->values.empty()); } TEST(ClauseTests, TolerantOfUnrecognizedFields) { auto result = boost::json::value_to< - tl::expected>(boost::json::parse( + tl::expected>(boost::json::parse( R"({"somethingRandom": true, "attribute": "", "op": "in", "values": ["a"]})")); ASSERT_TRUE(result); } TEST(ClauseTests, TolerantOfEmptyAttribute) { - auto result = boost::json::value_to< - tl::expected>( - boost::json::parse( - R"({"attribute": "", "op": "segmentMatch", "values": ["a"]})")); + auto result = + boost::json::value_to>( + boost::json::parse( + R"({"attribute": "", "op": "segmentMatch", "values": ["a"]})")); ASSERT_TRUE(result); - ASSERT_FALSE(result->attribute); + ASSERT_FALSE(result->attribute.Valid()); } TEST(ClauseTests, TolerantOfUnrecognizedOperator) { auto result = boost::json::value_to< - tl::expected>(boost::json::parse( + tl::expected>(boost::json::parse( R"({"attribute": "", "op": "notAnActualOperator", "values": ["a"]})")); ASSERT_TRUE(result); - ASSERT_EQ(result->op, data_model::Segment::Clause::Op::kUnrecognized); + ASSERT_EQ(result->op, data_model::Clause::Op::kUnrecognized); } TEST(ClauseTests, DeserializesSimpleAttributeReference) { auto result = boost::json::value_to< - tl::expected>(boost::json::parse( + tl::expected>(boost::json::parse( R"({"attribute": "foo", "op": "in", "values": ["a"], "contextKind" : "user"})")); ASSERT_TRUE(result); ASSERT_EQ(result->attribute, AttributeReference("foo")); @@ -151,7 +154,7 @@ TEST(ClauseTests, DeserializesSimpleAttributeReference) { TEST(ClauseTests, DeserializesPointerAttributeReference) { auto result = boost::json::value_to< - tl::expected>(boost::json::parse( + tl::expected>(boost::json::parse( R"({"attribute": "/foo/bar", "op": "in", "values": ["a"], "contextKind" : "user"})")); ASSERT_TRUE(result); ASSERT_EQ(result->attribute, AttributeReference("/foo/bar")); @@ -159,18 +162,186 @@ TEST(ClauseTests, DeserializesPointerAttributeReference) { TEST(ClauseTests, DeserializesEscapedReference) { auto result = boost::json::value_to< - tl::expected>(boost::json::parse( + tl::expected>(boost::json::parse( R"({"attribute": "/~1foo~1bar", "op": "in", "values": ["a"], "contextKind" : "user"})")); ASSERT_TRUE(result); ASSERT_EQ(result->attribute, AttributeReference("/~1foo~1bar")); } -TEST(ClauseTests, DeserializesLiteralAttributeReference) { - auto result = boost::json::value_to< - tl::expected>( - boost::json::parse( - R"({"attribute": "/foo/bar", "op": "in", "values": ["a"]})")); +TEST(ClauseTests, DeserializesLiteralAttributeName) { + auto result = + boost::json::value_to>( + boost::json::parse( + R"({"attribute": "/foo/bar", "op": "in", "values": ["a"]})")); ASSERT_TRUE(result); ASSERT_EQ(result->attribute, AttributeReference::FromLiteralStr("/foo/bar")); } + +TEST(RolloutTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kRollout); + ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->bucketBy, "key"); +} + +TEST(RolloutTests, DeserializesAllFieldsWithAttributeReference) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"kind": "experiment", "contextKind": "org", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); + ASSERT_EQ(result->contextKind, "org"); + ASSERT_EQ(result->bucketBy, "/foo/bar"); + ASSERT_EQ(result->seed, 123); + ASSERT_TRUE(result->variations.empty()); +} + +TEST(RolloutTests, DeserializesAllFieldsWithLiteralAttributeName) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"kind": "experiment", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); + ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->bucketBy, "/~1foo~1bar"); + ASSERT_EQ(result->seed, 123); + ASSERT_TRUE(result->variations.empty()); +} + +TEST(WeightedVariationTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->variation, 0); + ASSERT_EQ(result->weight, 0); + ASSERT_FALSE(result->untracked); +} + +TEST(WeightedVariationTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse( + R"({"variation" : 2, "weight" : 123, "untracked" : true})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->variation, 2); + ASSERT_EQ(result->weight, 123); + ASSERT_TRUE(result->untracked); +} + +TEST(PrerequisiteTests, DeserializeFailsWithoutKey) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_FALSE(result); +} + +TEST(PrerequisiteTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"key" : "foo"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->variation, 0); + ASSERT_EQ(result->key, "foo"); +} + +TEST(PrerequisiteTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"key" : "foo", "variation" : 123})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->key, "foo"); + ASSERT_EQ(result->variation, 123); +} + +TEST(PrerequisiteTests, DeserializeFailsWithNegativeVariation) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"key" : "foo", "variation" : -123})")); + ASSERT_FALSE(result); +} + +TEST(TargetTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->variation, 0); + ASSERT_TRUE(result->values.empty()); +} + +TEST(TargetTests, DeserializesFailsWithNegativeVariation) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({"variation" : -123})")); + ASSERT_FALSE(result); +} + +TEST(TargetTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"variation" : 123, "values" : ["a"], "contextKind" : "org"})")); + ASSERT_TRUE(result); + ASSERT_EQ(result->contextKind, "org"); + ASSERT_EQ(result->variation, 123); + ASSERT_EQ(result->values.size(), 1); + ASSERT_EQ(result->values[0], "a"); +} + +TEST(FlagRuleTests, DeserializesMinimumValid) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"variation" : 123})")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->trackEvents); + ASSERT_TRUE(result->clauses.empty()); + ASSERT_FALSE(result->id); + ASSERT_EQ(std::get(result->variationOrRollout), + data_model::Flag::Variation(123)); +} + +TEST(FlagRuleTests, DeserializesRollout) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"rollout" : {}})")); + ASSERT_TRUE(result); + ASSERT_EQ( + std::get(result->variationOrRollout).kind, + data_model::Flag::Rollout::Kind::kRollout); +} + +TEST(FlagRuleTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"id" : "foo", "variation" : 123, "trackEvents" : true, "clauses" : []})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->trackEvents); + ASSERT_TRUE(result->clauses.empty()); + ASSERT_EQ(result->id, "foo"); + ASSERT_EQ(std::get(result->variationOrRollout), + data_model::Flag::Variation(123)); +} + +TEST(ClientSideAvailabilityTests, DeserializesMinimumValid) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse(R"({})")); + ASSERT_TRUE(result); + ASSERT_FALSE(result->usingMobileKey); + ASSERT_FALSE(result->usingEnvironmentId); +} + +TEST(ClientSideAvailabilityTests, DeserializesAllFields) { + auto result = boost::json::value_to< + tl::expected>( + boost::json::parse( + R"({"usingMobileKey" : true, "usingEnvironmentId" : true})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->usingMobileKey); + ASSERT_TRUE(result->usingEnvironmentId); +} From 09d2bd60297be74e7c2acbc598c0f4d517389a62 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 6 Jul 2023 10:48:34 -0700 Subject: [PATCH 06/56] feat: add Flag model to SDKDataSet (#159) Adds in missing `Flag` model to the top-level `SDKDataSet`. Additionally, both the flag/segment fields now use use `PARSE_FIELD`, so we can get an empty `unordered_map` if they are missing. --- .../include/launchdarkly/data_model/flag.hpp | 7 +++++++ .../launchdarkly/data_model/sdk_data_set.hpp | 6 +++--- .../include/launchdarkly/data_model/segment.hpp | 6 ++++++ .../src/serialization/json_sdk_data_set.cpp | 4 +++- .../tests/data_model_serialization_test.cpp | 14 +++++++++++--- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index 8c3d0bc4e..282dd4bad 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -83,5 +83,12 @@ struct Flag { bool trackEvents; bool trackEventsFallthrough; std::optional debugEventsUntilDate; + + /** + * Returns the flag's version. Satisfies ItemDescriptor template + * constraints. + * @return Version of this flag. + */ + [[nodiscard]] inline std::uint64_t Version() const { return version; } }; } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp index aa0d6be8e..50edf0183 100644 --- a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp +++ b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -14,9 +15,8 @@ namespace launchdarkly::data_model { struct SDKDataSet { using FlagKey = std::string; using SegmentKey = std::string; - // std::unordered_map> flags; - std::optional>> - segments; + std::unordered_map> flags; + std::unordered_map> segments; }; } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/segment.hpp b/libs/internal/include/launchdarkly/data_model/segment.hpp index 63003b4a4..cb048796a 100644 --- a/libs/internal/include/launchdarkly/data_model/segment.hpp +++ b/libs/internal/include/launchdarkly/data_model/segment.hpp @@ -48,6 +48,12 @@ struct Segment { // TODO(cwaldren): make Kind a real type that is deserialized, so we can // make empty string an error. + + /** + * Returns the segment's version. Satisfies ItemDescriptor template + * constraints. + * @return Version of this segment. + */ [[nodiscard]] inline std::uint64_t Version() const { return version; } }; } // namespace launchdarkly::data_model diff --git a/libs/internal/src/serialization/json_sdk_data_set.cpp b/libs/internal/src/serialization/json_sdk_data_set.cpp index 2c4f4afe3..84043c377 100644 --- a/libs/internal/src/serialization/json_sdk_data_set.cpp +++ b/libs/internal/src/serialization/json_sdk_data_set.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -19,7 +20,8 @@ tl::expected, JsonError> tag_invoke( data_model::SDKDataSet data_set; - PARSE_CONDITIONAL_FIELD(data_set.segments, obj, "segments"); + PARSE_FIELD(data_set.flags, obj, "flags"); + PARSE_FIELD(data_set.segments, obj, "segments"); return data_set; } diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index a718933d1..43bf8eaa4 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -13,7 +13,8 @@ TEST(SDKDataSetTests, DeserializesEmptyDataSet) { boost::json::value_to>( boost::json::parse("{}")); ASSERT_TRUE(result); - ASSERT_FALSE(result->segments); + ASSERT_TRUE(result->segments.empty()); + ASSERT_TRUE(result->flags.empty()); } TEST(SDKDataSetTests, ErrorOnInvalidSchema) { @@ -29,8 +30,15 @@ TEST(SDKDataSetTests, DeserializesZeroSegments) { boost::json::value_to>( boost::json::parse(R"({"segments":{}})")); ASSERT_TRUE(result); - ASSERT_TRUE(result->segments); - ASSERT_TRUE(result->segments->empty()); + ASSERT_TRUE(result->segments.empty()); +} + +TEST(SDKDataSetTests, DeserializesZeroFlags) { + auto result = + boost::json::value_to>( + boost::json::parse(R"({"flags":{}})")); + ASSERT_TRUE(result); + ASSERT_TRUE(result->flags.empty()); } TEST(SegmentTests, DeserializesMinimumValid) { From 836730585925493a4cd35a6ef7ce2732a79a4533 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 7 Jul 2023 08:33:01 -0700 Subject: [PATCH 07/56] chore: Implement architecture diagram for data store. (#161) --- architecture/server_store_arch.md | 98 +++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 architecture/server_store_arch.md diff --git a/architecture/server_store_arch.md b/architecture/server_store_arch.md new file mode 100644 index 000000000..324425904 --- /dev/null +++ b/architecture/server_store_arch.md @@ -0,0 +1,98 @@ +# Server Data Store Architecture + +```mermaid + +classDiagram + Client --* IDataStore : Contains an IDataStore which is\n either a MemoryStore or a PersistentStore + Client --* DataStoreUpdater + Client --* IChangeNotifier + + IDataStore <|-- MemoryStore + IDataSourceUpdateSink <|-- MemoryStore + + IDataStore <|-- PersistentStore + IDataSourceUpdateSink <|-- PersistentStore + IDataSourceUpdateSink <|-- DataStoreUpdater + + IChangeNotifier <|-- DataStoreUpdater + + DataStoreUpdater --> IDataStore + + PersistentStore --* MemoryStore : PersistentStore contains a MemoryStore + PersistentStore --* TtlTracker + + IPersistentStoreCore <|-- RedisPersistentStore + + note for IPersistentStoreCore "The Get/All/Initialized are behaviorally\n const, but cache/memoize." + + IPersistentStoreCore --> SerializedItemDescriptor + IPersistentStoreCore --> PersistentKind + PersistentStore --* IPersistentStoreCore + + class PersistentKind{ + +std::string namespace + %% There are some cases where the store may need to extract a version from the serialized representation. + %% Specifically when the store cannot put the version in a column, such as with Redis. + +DeserializeVersion(std::string data): uint64_t + } + + class SerializedItemDescriptor{ + +uint64_t version + +bool deleted + +std::string serializedItem + } + + class IPersistentStoreCore { + <> + +Init(OrderedDataSets dataSets) + +Upsert(PersistentKind kind, std::string key, SerializedItemDescriptor descriptor) SerializedItemDescriptor + + +const Get(PersistentKind kind, std::string key) SerializedItemDescriptor + +const All(PersistentKind kind) std::unordered_map<std::string, SerializedItemDescriptor> + + +const Description() std::string const& + +const Initialized() bool + } + + + class IDataSourceUpdateSink{ + <> + +void Init(SDKDataSet allData) + +void Upsert(std::string key, ItemDescriptor~Flag~ data) + +void Upsert(std::string key, ItemDescriptor~Segment~ data) + } + + note for IDataStore "The shared_ptr from GetFlag or GetSegment may be null." + + class IDataStore{ + <> + +const GetFlag(std::string key) std::shared_ptr<const ItemDescriptor<Flag>> + +const GetSegment(std::string key) std::shared_ptr<const ItemDescriptor<Segment>> + +const AllFlags() std::unordered_map<std::string, std::shared_ptr<const ItemDescriptor<Flag>>> + +const AllSegments() std::unordered_map<std::string, std::shared_ptr<const ItemDescriptor<Segment>>> + +const Initialized() bool + +const Description() string + } + + class TtlTracker{ + } + + class MemoryStore{ + } + + class PersistentStore{ + +PersistentStore(std::shared_ptr~IPersistentStoreCore~ core) + } + + class RedisPersistentStore{ + + } + + class IChangeNotifier{ + +OnChange(std::function<void(std::shared_ptr<ChangeSet>)> handler): std::unique_ptr~IConnection~ + } + + class DataStoreUpdater{ + + } +``` \ No newline at end of file From 2781af0b07fe2bbefae11976c9508b40c2e97f54 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 7 Jul 2023 11:14:38 -0700 Subject: [PATCH 08/56] fix: Add various missing headers. (#163) --- .github/workflows/client.yml | 2 +- .github/workflows/common.yml | 2 +- .github/workflows/cpp-linter.yml | 2 +- .github/workflows/internal.yml | 2 +- .../include/launchdarkly/config/shared/built/events.hpp | 1 + .../include/launchdarkly/serialization/json_primitives.hpp | 3 +++ .../include/launchdarkly/serialization/value_mapping.hpp | 5 ++++- libs/internal/src/serialization/value_mapping.cpp | 2 ++ 8 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index fa25e9ee4..4e72ea5d6 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -6,7 +6,7 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ main, server-side ] paths-ignore: - '**.md' diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index 9c1ce1752..d33bc4423 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -6,7 +6,7 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ main, server-side ] paths-ignore: - '**.md' diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 55a3f4d9e..4a969f4be 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -7,7 +7,7 @@ on: push: branches: [ "main" ] pull_request: - branches: [ "main" ] + branches: [ "main", server-side ] jobs: cpp-linter: diff --git a/.github/workflows/internal.yml b/.github/workflows/internal.yml index 0986b723d..c8f998a4a 100644 --- a/.github/workflows/internal.yml +++ b/.github/workflows/internal.yml @@ -6,7 +6,7 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ main, server-side ] paths-ignore: - '**.md' diff --git a/libs/common/include/launchdarkly/config/shared/built/events.hpp b/libs/common/include/launchdarkly/config/shared/built/events.hpp index f7c59e0ec..2b3e76c35 100644 --- a/libs/common/include/launchdarkly/config/shared/built/events.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/events.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include diff --git a/libs/internal/include/launchdarkly/serialization/json_primitives.hpp b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp index 4ddb8f208..268b9596a 100644 --- a/libs/internal/include/launchdarkly/serialization/json_primitives.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_primitives.hpp @@ -6,6 +6,9 @@ #include #include +#include +#include + namespace launchdarkly { template diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index d2aa82ea3..5d7d7484a 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -3,10 +3,13 @@ #include #include -#include #include +#include #include +#include +#include + // Parses a field, propagating an error if the field's value is of the wrong // type. If the field was null or omitted in the data, it is set to // default_value. diff --git a/libs/internal/src/serialization/value_mapping.cpp b/libs/internal/src/serialization/value_mapping.cpp index db8d75c70..b83183725 100644 --- a/libs/internal/src/serialization/value_mapping.cpp +++ b/libs/internal/src/serialization/value_mapping.cpp @@ -1,5 +1,7 @@ #include +#include + namespace launchdarkly { template <> From 90627f227cc31f66acd0b5ab239695faf697363e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Tue, 11 Jul 2023 14:09:28 -0700 Subject: [PATCH 09/56] chore: Implement server data source architecture diagram. (#167) --- architecture/server_data_source_arch.md | 118 ++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 architecture/server_data_source_arch.md diff --git a/architecture/server_data_source_arch.md b/architecture/server_data_source_arch.md new file mode 100644 index 000000000..254e8675f --- /dev/null +++ b/architecture/server_data_source_arch.md @@ -0,0 +1,118 @@ +# Server Data Source Architecture + +```mermaid +classDiagram + direction LR + Client --* IDataSource + Client --* IDataSourceStatusProvider + + IDataSource <|-- PollingDataSource + IDataSource <|-- StreamingDataSource + + PollingDataSource --* DataSourceStatusManager + StreamingDataSource --* DataSourceStatusManager + + PollingDataSource --* DataSourceEventHandler + StreamingDataSource --* DataSourceEventHandler + + DataSourceEventHandler --> DataSourceStatusManager + + IDataSourceStatusProvider <-- DataSourceStatusManager + + DataSourceStatusManager --> DataSourceState + DataSourceStatusManager --> ErrorInfo + DataSourceStatusManager --> DataSourceStatus + + DataSourceStatus --* DataSourceState + DataSourceStatus --* ErrorInfo + DataSourceEventHandler --> DataSourceEventHandler_MessageStatus + + + note for IDataSource "Common for Client/Server" + + class IDataSource { + <> + +Start() void + +ShutdownAsync(std::function~void()~ ) void + } + + note for IDataSourceStatusProvider "Different for client/server" + + class IDataSourceStatusProvider { + <> + +const Status() DataSourceStatus + +OnDataSourceStatusChange(std::function<void(DataSourceStatus)> ) std::unique_ptr~IConnection~ + +OnDataSourceStatusChangeEx(std::function<bool(DataSourceStatus)> ) void + } + + note for DataSourceState "Different for client/server" + + class DataSourceState { + <> + Initializing + Valid + Interrupted + Off + } + + note for ErrorInfo_ErrorKind "Common for client/server" + + class ErrorInfo_ErrorKind { + <> + Unknown + NetworkError + ErrorResponse + InvalidData + StoreError + } + + note for ErrorInfo "Common for client/server" + + ErrorInfo --* ErrorInfo_ErrorKind + + class ErrorInfo { + +const Kind() ErrorKind + +const StatusCode() StatusCodeType + +const Message() std::string const& + +const Time() DateTime + } + + class PollingDataSource { + + } + + class StreamingDataSource { + + } + + note for DataSourceEventHandler "Different for client/server" + + + class DataSourceEventHandler { + +HandleMessage(std::string const& type, std::string const& data) MessageStatus + } + + note for DataSourceEventHandler_MessageStatus "Common for client/server" + + class DataSourceEventHandler_MessageStatus { + <> + MessageHandled + InvalidMessage + UnhandledVerb + } + + class DataSourceStatus { + +const State() DataSourceState + +const StateSince() DateTime + +const LastError() std::optional~ErrorInfo~ + } + + class DataSourceStatusManager { + +SetState(DataSourceStatus status) void + +SetState(DataSourceState state, StatusCodeType code, std::string message) void + +SetState(DataSourceState state, ErrorInfo_ErrorKind kind, std::string message) void + +SetError(ErrorInfo::ErrorKind kind, std::string message) void + +SetError(StatusCodeType code, std::string message) void + } + +``` \ No newline at end of file From f2d96a4c35ac5e523d8d308ffc5a68582a788a4f Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 12 Jul 2023 11:19:00 -0700 Subject: [PATCH 10/56] feat: Implement basic in-memory store and change handling. (#165) Co-authored-by: Casey Waldren --- libs/client-sdk/src/CMakeLists.txt | 2 - .../data_source_status_manager.cpp | 7 +- .../src/flag_manager/flag_updater.cpp | 7 +- .../launchdarkly/data_model/sdk_data_set.hpp | 9 +- .../signals}/boost_signal_connection.hpp | 4 +- libs/internal/src/CMakeLists.txt | 4 +- .../src/signals}/boost_signal_connection.cpp | 6 +- .../server_side/change_notifier.hpp | 42 ++ libs/server-sdk/src/CMakeLists.txt | 11 +- .../data_source/data_source_update_sink.hpp | 31 ++ libs/server-sdk/src/data_store/data_kind.hpp | 7 + libs/server-sdk/src/data_store/data_store.hpp | 81 ++++ .../src/data_store/data_store_updater.cpp | 76 ++++ .../src/data_store/data_store_updater.hpp | 122 +++++ .../src/data_store/dependency_tracker.cpp | 196 ++++++++ .../src/data_store/dependency_tracker.hpp | 173 +++++++ .../server-sdk/src/data_store/descriptors.hpp | 12 + .../src/data_store/memory_store.cpp | 75 +++ .../src/data_store/memory_store.hpp | 44 ++ .../tests/data_store_updater_test.cpp | 430 ++++++++++++++++++ .../tests/dependency_tracker_test.cpp | 297 ++++++++++++ libs/server-sdk/tests/memory_store_test.cpp | 286 ++++++++++++ 22 files changed, 1904 insertions(+), 18 deletions(-) rename libs/{client-sdk/src => internal/include/launchdarkly/signals}/boost_signal_connection.hpp (78%) rename libs/{client-sdk/src => internal/src/signals}/boost_signal_connection.cpp (55%) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp create mode 100644 libs/server-sdk/src/data_source/data_source_update_sink.hpp create mode 100644 libs/server-sdk/src/data_store/data_kind.hpp create mode 100644 libs/server-sdk/src/data_store/data_store.hpp create mode 100644 libs/server-sdk/src/data_store/data_store_updater.cpp create mode 100644 libs/server-sdk/src/data_store/data_store_updater.hpp create mode 100644 libs/server-sdk/src/data_store/dependency_tracker.cpp create mode 100644 libs/server-sdk/src/data_store/dependency_tracker.hpp create mode 100644 libs/server-sdk/src/data_store/descriptors.hpp create mode 100644 libs/server-sdk/src/data_store/memory_store.cpp create mode 100644 libs/server-sdk/src/data_store/memory_store.hpp create mode 100644 libs/server-sdk/tests/data_store_updater_test.cpp create mode 100644 libs/server-sdk/tests/dependency_tracker_test.cpp create mode 100644 libs/server-sdk/tests/memory_store_test.cpp diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index 8eba21d76..23e3681b9 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -17,10 +17,8 @@ add_library(${LIBNAME} data_sources/data_source_status_manager.cpp event_processor/event_processor.cpp event_processor/null_event_processor.cpp - boost_signal_connection.cpp client_impl.cpp client.cpp - boost_signal_connection.hpp client_impl.hpp data_sources/data_source.hpp data_sources/data_source_event_handler.hpp diff --git a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp b/libs/client-sdk/src/data_sources/data_source_status_manager.cpp index c7f196868..618ed8610 100644 --- a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp +++ b/libs/client-sdk/src/data_sources/data_source_status_manager.cpp @@ -4,8 +4,8 @@ #include #include +#include -#include "../boost_signal_connection.hpp" #include "data_source_status_manager.hpp" namespace launchdarkly::client_side::data_sources { @@ -104,7 +104,8 @@ DataSourceStatus DataSourceStatusManager::Status() const { std::unique_ptr DataSourceStatusManager::OnDataSourceStatusChange( std::function handler) { std::lock_guard lock{status_mutex_}; - return std::make_unique< ::launchdarkly::client_side::SignalConnection>( + return std::make_unique< + ::launchdarkly::internal::signals::SignalConnection>( data_source_status_signal_.connect(handler)); } @@ -112,7 +113,7 @@ std::unique_ptr DataSourceStatusManager::OnDataSourceStatusChangeEx( std::function handler) { std::lock_guard lock{status_mutex_}; - return std::make_unique< ::launchdarkly::client_side::SignalConnection>( + return std::make_unique( data_source_status_signal_.connect_extended( [handler](boost::signals2::connection const& conn, data_sources::DataSourceStatus status) { diff --git a/libs/client-sdk/src/flag_manager/flag_updater.cpp b/libs/client-sdk/src/flag_manager/flag_updater.cpp index 34b4469d5..4a316f8d8 100644 --- a/libs/client-sdk/src/flag_manager/flag_updater.cpp +++ b/libs/client-sdk/src/flag_manager/flag_updater.cpp @@ -1,6 +1,7 @@ #include -#include "../boost_signal_connection.hpp" +#include + #include "flag_updater.hpp" namespace launchdarkly::client_side::flag_manager { @@ -74,7 +75,7 @@ void FlagUpdater::DispatchEvent(FlagValueChangeEvent event) { auto handler = signals_.find(event.FlagName()); if (handler != signals_.end()) { if (handler->second.empty()) { - // Empty, remove it from the map so it doesn't count toward + // Empty, remove it from the map, so it doesn't count toward // future calculations. signals_.erase(event.FlagName()); } else { @@ -123,7 +124,7 @@ std::unique_ptr FlagUpdater::OnFlagChange( std::string const& key, std::function)> handler) { std::lock_guard lock{signal_mutex_}; - return std::make_unique< ::launchdarkly::client_side::SignalConnection>( + return std::make_unique( signals_[key].connect(handler)); } diff --git a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp index 50edf0183..6fb24a228 100644 --- a/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp +++ b/libs/internal/include/launchdarkly/data_model/sdk_data_set.hpp @@ -13,10 +13,15 @@ namespace launchdarkly::data_model { struct SDKDataSet { + template + using Collection = std::unordered_map>; using FlagKey = std::string; using SegmentKey = std::string; - std::unordered_map> flags; - std::unordered_map> segments; + using Flags = Collection; + using Segments = Collection; + + Flags flags; + Segments segments; }; } // namespace launchdarkly::data_model diff --git a/libs/client-sdk/src/boost_signal_connection.hpp b/libs/internal/include/launchdarkly/signals/boost_signal_connection.hpp similarity index 78% rename from libs/client-sdk/src/boost_signal_connection.hpp rename to libs/internal/include/launchdarkly/signals/boost_signal_connection.hpp index 16eeda9f9..14e92348a 100644 --- a/libs/client-sdk/src/boost_signal_connection.hpp +++ b/libs/internal/include/launchdarkly/signals/boost_signal_connection.hpp @@ -4,7 +4,7 @@ #include -namespace launchdarkly::client_side { +namespace launchdarkly::internal::signals { class SignalConnection : public IConnection { public: @@ -16,4 +16,4 @@ class SignalConnection : public IConnection { boost::signals2::connection connection_; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::internal::signals diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index 810529c06..9fb801885 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -5,6 +5,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/network/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/events/*.hpp" + "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/signals/*.hpp" ) # Automatic library: static or dynamic based on user config. @@ -38,7 +39,8 @@ add_library(${LIBNAME} OBJECT serialization/json_rule_clause.cpp serialization/json_flag.cpp encoding/base_64.cpp - encoding/sha_256.cpp) + encoding/sha_256.cpp + signals/boost_signal_connection.cpp) add_library(launchdarkly::internal ALIAS ${LIBNAME}) diff --git a/libs/client-sdk/src/boost_signal_connection.cpp b/libs/internal/src/signals/boost_signal_connection.cpp similarity index 55% rename from libs/client-sdk/src/boost_signal_connection.cpp rename to libs/internal/src/signals/boost_signal_connection.cpp index 30cb90604..29f4323d1 100644 --- a/libs/client-sdk/src/boost_signal_connection.cpp +++ b/libs/internal/src/signals/boost_signal_connection.cpp @@ -1,6 +1,6 @@ -#include "boost_signal_connection.hpp" +#include -namespace launchdarkly::client_side { +namespace launchdarkly::internal::signals { SignalConnection::SignalConnection(boost::signals2::connection connection) : connection_(std::move(connection)) {} @@ -9,4 +9,4 @@ void SignalConnection::Disconnect() { connection_.disconnect(); } -} // namespace launchdarkly::client_side +} // namespace launchdarkly::internal::signals diff --git a/libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp b/libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp new file mode 100644 index 000000000..70f2fe17e --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/change_notifier.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +/** + * Interface to allow listening for flag changes. Notification events should + * be distributed after the store has been updated. + */ +class IChangeNotifier { + public: + using ChangeSet = std::set; + using ChangeHandler = std::function)>; + + /** + * Listen for changes to flag configuration. The change handler will be + * called with a set of affected flag keys. Changes include flags whose + * dependencies (either other flags, or segments) changed. + * + * @param signal The handler for the changes. + * @return A connection which can be used to stop listening. + */ + virtual std::unique_ptr OnFlagChange( + ChangeHandler handler) = 0; + + virtual ~IChangeNotifier() = default; + IChangeNotifier(IChangeNotifier const& item) = delete; + IChangeNotifier(IChangeNotifier&& item) = delete; + IChangeNotifier& operator=(IChangeNotifier const&) = delete; + IChangeNotifier& operator=(IChangeNotifier&&) = delete; + + protected: + IChangeNotifier() = default; +}; + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index b229d78a5..46b0e0272 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -6,14 +6,21 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS # Automatic library: static or dynamic based on user config. add_library(${LIBNAME} - ${HEADER_LIST}) + ${HEADER_LIST} + data_source/data_source_update_sink.hpp + data_store/data_store.hpp + data_store/data_store_updater.hpp + data_store/data_store_updater.cpp + data_store/memory_store.cpp + data_store/dependency_tracker.hpp + data_store/dependency_tracker.cpp data_store/descriptors.hpp) if (MSVC OR (NOT BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) else () - # The default static lib builds, for linux, are positition independent. + # The default static lib builds, for linux, are position independent. # So they do not link into a shared object without issues. So, when # building shared objects do not link the static libraries and instead # use the "src.hpp" files for required libraries. diff --git a/libs/server-sdk/src/data_source/data_source_update_sink.hpp b/libs/server-sdk/src/data_source/data_source_update_sink.hpp new file mode 100644 index 000000000..61d3d6d01 --- /dev/null +++ b/libs/server-sdk/src/data_source/data_source_update_sink.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +#include "../data_store/descriptors.hpp" + +namespace launchdarkly::server_side::data_source { +/** + * Interface for handling updates from LaunchDarkly. + */ +class IDataSourceUpdateSink { + public: + virtual void Init(launchdarkly::data_model::SDKDataSet data_set) = 0; + virtual void Upsert(std::string const& key, + data_store::FlagDescriptor flag) = 0; + virtual void Upsert(std::string const& key, + data_store::SegmentDescriptor segment) = 0; + + IDataSourceUpdateSink(IDataSourceUpdateSink const& item) = delete; + IDataSourceUpdateSink(IDataSourceUpdateSink&& item) = delete; + IDataSourceUpdateSink& operator=(IDataSourceUpdateSink const&) = delete; + IDataSourceUpdateSink& operator=(IDataSourceUpdateSink&&) = delete; + virtual ~IDataSourceUpdateSink() = default; + + protected: + IDataSourceUpdateSink() = default; +}; +} // namespace launchdarkly::server_side::data_source diff --git a/libs/server-sdk/src/data_store/data_kind.hpp b/libs/server-sdk/src/data_store/data_kind.hpp new file mode 100644 index 000000000..17ead105b --- /dev/null +++ b/libs/server-sdk/src/data_store/data_kind.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::data_store { +enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/data_store.hpp b/libs/server-sdk/src/data_store/data_store.hpp new file mode 100644 index 000000000..3ae0184e0 --- /dev/null +++ b/libs/server-sdk/src/data_store/data_store.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "descriptors.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_store { + +/** + * Interface for readonly access to SDK data. + */ +class IDataStore { + public: + /** + * Get a flag from the store. + * + * @param key The key for the flag. + * @return Returns a shared_ptr to the FlagDescriptor, or a nullptr if there + * is no such flag or the flag was deleted. + */ + [[nodiscard]] virtual std::shared_ptr GetFlag( + std::string const& key) const = 0; + + /** + * Get a segment from the store. + * + * @param key The key for the segment. + * @return Returns a shared_ptr to the SegmentDescriptor, or a nullptr if + * there is no such segment, or the segment was deleted. + */ + [[nodiscard]] virtual std::shared_ptr GetSegment( + std::string const& key) const = 0; + + /** + * Get all of the flags. + * + * @return Returns an unordered map of FlagDescriptors. + */ + [[nodiscard]] virtual std::unordered_map> + AllFlags() const = 0; + + /** + * Get all of the segments. + * + * @return Returns an unordered map of SegmentDescriptors. + */ + [[nodiscard]] virtual std::unordered_map> + AllSegments() const = 0; + + /** + * Check if the store is initialized. + * + * @return Returns true if the store is initialized. + */ + [[nodiscard]] virtual bool Initialized() const = 0; + + /** + * Get a description of the store. + * @return Returns a string containing a description of the store. + */ + [[nodiscard]] virtual std::string const& Description() const = 0; + + IDataStore(IDataStore const& item) = delete; + IDataStore(IDataStore&& item) = delete; + IDataStore& operator=(IDataStore const&) = delete; + IDataStore& operator=(IDataStore&&) = delete; + virtual ~IDataStore() = default; + + protected: + IDataStore() = default; +}; + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/data_store_updater.cpp b/libs/server-sdk/src/data_store/data_store_updater.cpp new file mode 100644 index 000000000..e018ba6d5 --- /dev/null +++ b/libs/server-sdk/src/data_store/data_store_updater.cpp @@ -0,0 +1,76 @@ +#include "data_store_updater.hpp" + +#include +#include + +namespace launchdarkly::server_side::data_store { + +std::unique_ptr DataStoreUpdater::OnFlagChange( + launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { + std::lock_guard lock{signal_mutex_}; + + return std::make_unique( + signals_.connect(handler)); +} + +void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { + // Optional outside the HasListeners() scope, this allows for the changes + // to be calculated before the update and then the notification to be + // sent after the update completes. + std::optional change_notifications; + if (HasListeners()) { + DependencySet updated_items; + + CalculateChanges(DataKind::kFlag, store_->AllFlags(), data_set.flags, + updated_items); + CalculateChanges(DataKind::kSegment, store_->AllSegments(), + data_set.segments, updated_items); + change_notifications = updated_items; + } + + dependency_tracker_.Clear(); + for (auto const& flag : data_set.flags) { + dependency_tracker_.UpdateDependencies(flag.first, flag.second); + } + for (auto const& segment : data_set.segments) { + dependency_tracker_.UpdateDependencies(segment.first, segment.second); + } + // Data will move into the store, so we want to update dependencies before + // it is moved. + sink_->Init(std::move(data_set)); + // After updating the sink, let listeners know of changes. + if (change_notifications) { + NotifyChanges(std::move(*change_notifications)); + } +} + +void DataStoreUpdater::Upsert(std::string const& key, + data_store::FlagDescriptor flag) { + UpsertCommon(DataKind::kFlag, key, store_->GetFlag(key), std::move(flag)); +} + +void DataStoreUpdater::Upsert(std::string const& key, + data_store::SegmentDescriptor segment) { + UpsertCommon(DataKind::kSegment, key, store_->GetSegment(key), + std::move(segment)); +} + +bool DataStoreUpdater::HasListeners() const { + std::lock_guard lock{signal_mutex_}; + return !signals_.empty(); +} + +void DataStoreUpdater::NotifyChanges(DependencySet changes) { + std::lock_guard lock{signal_mutex_}; + auto flag_changes = changes.SetForKind(DataKind::kFlag); + // Only emit an event if there are changes. + if (!flag_changes.empty()) { + signals_(std::make_shared(std::move(flag_changes))); + } +} + +DataStoreUpdater::DataStoreUpdater(std::shared_ptr sink, + std::shared_ptr store) + : sink_(std::move(sink)), store_(std::move(store)) {} + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/data_store_updater.hpp b/libs/server-sdk/src/data_store/data_store_updater.hpp new file mode 100644 index 000000000..e76758af3 --- /dev/null +++ b/libs/server-sdk/src/data_store/data_store_updater.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include "../data_source/data_source_update_sink.hpp" +#include "data_store.hpp" +#include "dependency_tracker.hpp" + +#include + +#include + +#include + +namespace launchdarkly::server_side::data_store { + +class DataStoreUpdater + : public launchdarkly::server_side::data_source::IDataSourceUpdateSink, + public launchdarkly::server_side::IChangeNotifier { + public: + template + using Collection = data_model::SDKDataSet::Collection; + + template + using SharedItem = std::shared_ptr>; + + template + using SharedCollection = + std::unordered_map>; + + DataStoreUpdater(std::shared_ptr sink, + std::shared_ptr store); + + std::unique_ptr OnFlagChange(ChangeHandler handler) override; + + void Init(launchdarkly::data_model::SDKDataSet data_set) override; + void Upsert(std::string const& key, FlagDescriptor flag) override; + void Upsert(std::string const& key, SegmentDescriptor segment) override; + ~DataStoreUpdater() override = default; + + DataStoreUpdater(DataStoreUpdater const& item) = delete; + DataStoreUpdater(DataStoreUpdater&& item) = delete; + DataStoreUpdater& operator=(DataStoreUpdater const&) = delete; + DataStoreUpdater& operator=(DataStoreUpdater&&) = delete; + + private: + bool HasListeners() const; + + template + void UpsertCommon( + DataKind kind, + std::string key, + SharedItem existing, + launchdarkly::data_model::ItemDescriptor updated) { + if (existing && (updated.version <= existing->version)) { + // Out of order update, ignore it. + return; + } + + dependency_tracker_.UpdateDependencies(key, updated); + + if (HasListeners()) { + auto updated_deps = DependencySet(); + dependency_tracker_.CalculateChanges(kind, key, updated_deps); + NotifyChanges(updated_deps); + } + + sink_->Upsert(key, updated); + } + + template + void CalculateChanges( + DataKind kind, + SharedCollection const& existing_flags_or_segments, + Collection const& new_flags_or_segments, + DependencySet& updated_items) { + for (auto const& old_flag_or_segment : existing_flags_or_segments) { + auto new_flag_or_segment = + new_flags_or_segments.find(old_flag_or_segment.first); + if (new_flag_or_segment != new_flags_or_segments.end() && + new_flag_or_segment->second.version <= + old_flag_or_segment.second->version) { + continue; + } + + // Deleted. + dependency_tracker_.CalculateChanges( + kind, old_flag_or_segment.first, updated_items); + } + + for (auto const& flag_or_segment : new_flags_or_segments) { + auto oldItem = + existing_flags_or_segments.find(flag_or_segment.first); + if (oldItem != existing_flags_or_segments.end() && + flag_or_segment.second.version <= oldItem->second->version) { + continue; + } + + // Updated or new. + dependency_tracker_.CalculateChanges(kind, flag_or_segment.first, + updated_items); + } + } + + void NotifyChanges(DependencySet changes); + + std::shared_ptr sink_; + std::shared_ptr store_; + + boost::signals2::signal)> signals_; + + // Recursive mutex so that has_listeners can non-conditionally lock + // the mutex. Otherwise, a pre-condition for the call would be holding + // the mutex, which is more difficult to keep consistent over the code + // lifetime. + // + // Signals themselves are thread-safe, and this mutex only allows us to + // prevent the addition of listeners between the listener check, calculation + // and dispatch of events. + mutable std::recursive_mutex signal_mutex_; + + DependencyTracker dependency_tracker_; +}; +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/dependency_tracker.cpp b/libs/server-sdk/src/data_store/dependency_tracker.cpp new file mode 100644 index 000000000..f8010756c --- /dev/null +++ b/libs/server-sdk/src/data_store/dependency_tracker.cpp @@ -0,0 +1,196 @@ +#include "dependency_tracker.hpp" + +#include + +namespace launchdarkly::server_side::data_store { + +DependencySet::DependencySet() + : data_{ + TaggedData>(DataKind::kFlag), + TaggedData>(DataKind::kSegment), + } {} + +void DependencySet::Set(DataKind kind, std::string key) { + Data(kind).emplace(std::move(key)); +} + +void DependencySet::Remove(DataKind kind, std::string const& key) { + Data(kind).erase(key); +} + +bool DependencySet::Contains(DataKind kind, std::string const& key) const { + return Data(kind).count(key) != 0; +} + +std::size_t DependencySet::Size() const { + std::size_t size = 0; + for (auto data_kind : data_) { + size += data_kind.Data().size(); + } + return size; +} + +std::array>, 2>::const_iterator +DependencySet::begin() const { + return data_.begin(); +} + +std::array>, 2>::const_iterator +DependencySet::end() const { + return data_.end(); +} + +std::set const& DependencySet::SetForKind(DataKind kind) { + return Data(kind); +} + +std::set const& DependencySet::Data(DataKind kind) const { + return data_[static_cast>(kind)].Data(); +} + +std::set& DependencySet::Data(DataKind kind) { + return data_[static_cast>(kind)].Data(); +} + +DependencyMap::DependencyMap() + : data_{ + TaggedData>( + DataKind::kFlag), + TaggedData>( + DataKind::kSegment), + } {} + +void DependencyMap::Set(DataKind kind, std::string key, DependencySet val) { + data_[static_cast>(kind)].Data().emplace( + std::move(key), std::move(val)); +} + +std::optional DependencyMap::Get(DataKind kind, + std::string const& key) const { + auto const& scope = + data_[static_cast>(kind)].Data(); + auto found = scope.find(key); + if (found != scope.end()) { + return found->second; + } + return std::nullopt; +} + +void DependencyMap::Clear() { + for (auto& data_kind : data_) { + data_kind.Data().clear(); + } +} + +std::array>, + 2>::const_iterator +DependencyMap::begin() const { + return data_.begin(); +} + +std::array>, + 2>::const_iterator +DependencyMap::end() const { + return data_.end(); +} + +void DependencyTracker::UpdateDependencies( + std::string const& key, + DependencyTracker::FlagDescriptor const& flag) { + DependencySet dependencies; + if (flag.item) { + for (auto const& prereq : flag.item->prerequisites) { + dependencies.Set(DataKind::kFlag, prereq.key); + } + + for (auto const& rule : flag.item->rules) { + CalculateClauseDeps(dependencies, rule.clauses); + } + } + UpdateDependencies(DataKind::kFlag, key, dependencies); +} + +void DependencyTracker::UpdateDependencies( + std::string const& key, + DependencyTracker::SegmentDescriptor const& segment) { + DependencySet dependencies; + if (segment.item) { + for (auto const& rule : segment.item->rules) { + CalculateClauseDeps(dependencies, rule.clauses); + } + } + UpdateDependencies(DataKind::kSegment, key, dependencies); +} + +// Function intentionally uses recursion. +// NOLINTBEGIN misc-no-recursion + +void DependencyTracker::CalculateChanges(DataKind kind, + std::string const& key, + DependencySet& dependency_set) { + if (!dependency_set.Contains(kind, key)) { + dependency_set.Set(kind, key); + auto affected_items = dependencies_to_.Get(kind, key); + if (affected_items) { + for (auto& deps_by_kind : *affected_items) { + for (auto& dep : deps_by_kind.Data()) { + CalculateChanges(deps_by_kind.Kind(), dep, dependency_set); + } + } + } + } +} + +// NOLINTEND misc-no-recursion + +void DependencyTracker::UpdateDependencies(DataKind kind, + std::string const& key, + DependencySet const& deps) { + auto current_deps = dependencies_from_.Get(kind, key); + if (current_deps) { + for (auto const& deps_by_kind : *current_deps) { + auto kind_of_dep = deps_by_kind.Kind(); + for (auto const& dep : deps_by_kind.Data()) { + auto deps_to_this_dep = dependencies_to_.Get(kind_of_dep, dep); + if (deps_to_this_dep) { + deps_to_this_dep->Remove(kind_of_dep, key); + } + } + } + } + + dependencies_from_.Set(kind, key, deps); + for (auto const& deps_by_kind : deps) { + for (auto const& dep : deps_by_kind.Data()) { + auto deps_to_this_dep = + dependencies_to_.Get(deps_by_kind.Kind(), dep); + if (!deps_to_this_dep) { + auto new_deps_to_this_dep = DependencySet(); + new_deps_to_this_dep.Set(kind, key); + dependencies_to_.Set(deps_by_kind.Kind(), dep, + new_deps_to_this_dep); + } else { + deps_to_this_dep->Set(kind, key); + } + } + } +} + +void DependencyTracker::CalculateClauseDeps( + DependencySet& dependencies, + std::vector const& clauses) { + for (auto const& clause : clauses) { + if (clause.op == data_model::Clause::Op::kSegmentMatch) { + for (auto const& value : clause.values) { + dependencies.Set(DataKind::kSegment, value.AsString()); + } + } + } +} + +void DependencyTracker::Clear() { + dependencies_to_.Clear(); + dependencies_from_.Clear(); +} + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_store/dependency_tracker.hpp new file mode 100644 index 000000000..7ab6f7f0c --- /dev/null +++ b/libs/server-sdk/src/data_store/dependency_tracker.hpp @@ -0,0 +1,173 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "data_kind.hpp" + +namespace launchdarkly::server_side::data_store { + +/** + * Class which can be used to tag a collection with the DataKind that collection + * is for. This is primarily to decrease the complexity of iterating collections + * allowing for a kvp style iteration, but with an array storage container. + * @tparam Storage + */ +template +class TaggedData { + public: + explicit TaggedData(DataKind kind) : kind_(kind) {} + [[nodiscard]] DataKind Kind() const { return kind_; } + [[nodiscard]] Storage const& Data() const { return storage_; } + + [[nodiscard]] Storage& Data() { return storage_; } + + private: + DataKind kind_; + Storage storage_; +}; + +/** + * Class used to maintain a set of dependencies. Each dependency may be either + * a flag or segment. + * For instance, if we have a flagA, which has a prerequisite of flagB, and + * a segmentMatch targeting segmentA, then its dependency set would be + * ``` + * [{DataKind::kFlag, "flagB"}, {DataKind::kSegment, "segmentA"}] + * ``` + */ +class DependencySet { + public: + DependencySet(); + using DataType = std::array>, + static_cast(DataKind::kKindCount)>; + void Set(DataKind kind, std::string key); + + void Remove(DataKind kind, std::string const& key); + + [[nodiscard]] bool Contains(DataKind kind, std::string const& key) const; + + [[nodiscard]] std::set const& SetForKind(DataKind kind); + + /** + * Return the size of all the data kind sets. + * @return The combined size of all the data kind sets. + */ + [[nodiscard]] std::size_t Size() const; + + [[nodiscard]] typename DataType::const_iterator begin() const; + + [[nodiscard]] typename DataType::const_iterator end() const; + + private: + [[nodiscard]] std::set const& Data(DataKind kind) const; + + [[nodiscard]] std::set& Data(DataKind kind); + + DataType data_; +}; + +/** + * Class used to map flag/segments to their set of dependencies. + * For instance, if we have a flagA, which has a prerequisite of flagB, and + * a segmentMatch targeting segmentA, then a dependency map, containing + * this set, would be: + * ``` + * {{DataKind::kFlag, "flagA"}, [{DataKind::kFlag, "flagB"}, + * {DataKind::kSegment, "segmentA"}]} + * ``` + */ +class DependencyMap { + public: + DependencyMap(); + using DataType = + std::array>, + static_cast(DataKind::kKindCount)>; + void Set(DataKind kind, std::string key, DependencySet val); + + [[nodiscard]] std::optional Get( + DataKind kind, + std::string const& key) const; + + void Clear(); + + [[nodiscard]] typename DataType::const_iterator begin() const; + + [[nodiscard]] typename DataType::const_iterator end() const; + + private: + DataType data_; +}; + +/** + * This class implements a mechanism of tracking dependencies of flags and + * segments. Both the forward dependencies (flag A depends on flag B) but also + * the reverse (flag B is depended on by flagA). + */ +class DependencyTracker { + public: + using FlagDescriptor = data_model::ItemDescriptor; + using SegmentDescriptor = data_model::ItemDescriptor; + + /** + * Update the dependency tracker with a new or updated flag. + * + * @param key The key for the flag. + * @param flag A descriptor for the flag. + */ + void UpdateDependencies(std::string const& key, FlagDescriptor const& flag); + + /** + * Update the dependency tracker with a new or updated segment. + * + * @param key The key for the segment. + * @param flag A descriptor for the segment. + */ + void UpdateDependencies(std::string const& key, + SegmentDescriptor const& segment); + + /** + * Given the current dependencies, determine what flags or segments may be + * impacted by a change to the given flag/segment. + * + * @param kind The kind of data. + * @param key The key for the data. + * @param dependency_set A dependency set, which dependencies are + * accumulated in. + */ + void CalculateChanges(DataKind kind, + std::string const& key, + DependencySet& dependency_set); + + /** + * Clear all existing dependencies. + */ + void Clear(); + + private: + /** + * Common logic for dependency updates used for both flags and segments. + */ + void UpdateDependencies(DataKind kind, + std::string const& key, + DependencySet const& deps); + + DependencyMap dependencies_from_; + DependencyMap dependencies_to_; + + /** + * Determine dependencies for a set of clauses. + * @param dependencies A set of dependencies to extend. + * @param clauses The clauses to determine dependencies for. + */ + static void CalculateClauseDeps( + DependencySet& dependencies, + std::vector const& clauses); +}; + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/descriptors.hpp b/libs/server-sdk/src/data_store/descriptors.hpp new file mode 100644 index 000000000..075d3a31d --- /dev/null +++ b/libs/server-sdk/src/data_store/descriptors.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly::server_side::data_store { +using FlagDescriptor = + launchdarkly::data_model::ItemDescriptor; +using SegmentDescriptor = + launchdarkly::data_model::ItemDescriptor; +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/memory_store.cpp b/libs/server-sdk/src/data_store/memory_store.cpp new file mode 100644 index 000000000..321c6249d --- /dev/null +++ b/libs/server-sdk/src/data_store/memory_store.cpp @@ -0,0 +1,75 @@ + + +#include "memory_store.hpp" + +namespace launchdarkly::server_side::data_store { + +std::shared_ptr MemoryStore::GetFlag( + std::string const& key) const { + std::lock_guard lock{data_mutex_}; + auto found = flags_.find(key); + if (found != flags_.end()) { + return found->second; + } + return nullptr; +} + +std::shared_ptr MemoryStore::GetSegment( + std::string const& key) const { + std::lock_guard lock{data_mutex_}; + auto found = segments_.find(key); + if (found != segments_.end()) { + return found->second; + } + return nullptr; +} + +std::unordered_map> +MemoryStore::AllFlags() const { + std::lock_guard lock{data_mutex_}; + return {flags_}; +} + +std::unordered_map> +MemoryStore::AllSegments() const { + std::lock_guard lock{data_mutex_}; + return {segments_}; +} + +bool MemoryStore::Initialized() const { + std::lock_guard lock{data_mutex_}; + return initialized_; +} + +std::string const& MemoryStore::Description() const { + return description_; +} + +void MemoryStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { + std::lock_guard lock{data_mutex_}; + initialized_ = true; + flags_.clear(); + segments_.clear(); + for (auto flag : dataSet.flags) { + flags_.emplace(flag.first, std::make_shared( + std::move(flag.second))); + } + for (auto segment : dataSet.segments) { + segments_.emplace(segment.first, std::make_shared( + std::move(segment.second))); + } +} + +void MemoryStore::Upsert(std::string const& key, + data_store::FlagDescriptor flag) { + std::lock_guard lock{data_mutex_}; + flags_[key] = std::make_shared(std::move(flag)); +} + +void MemoryStore::Upsert(std::string const& key, + data_store::SegmentDescriptor segment) { + std::lock_guard lock{data_mutex_}; + segments_[key] = std::make_shared(std::move(segment)); +} + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/memory_store.hpp b/libs/server-sdk/src/data_store/memory_store.hpp new file mode 100644 index 000000000..ea3e11a2d --- /dev/null +++ b/libs/server-sdk/src/data_store/memory_store.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "../data_source/data_source_update_sink.hpp" +#include "data_store.hpp" + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_store { + +class MemoryStore : public IDataStore, + public data_source::IDataSourceUpdateSink { + public: + std::shared_ptr GetFlag( + std::string const& key) const override; + std::shared_ptr GetSegment( + std::string const& key) const override; + + std::unordered_map> AllFlags() + const override; + std::unordered_map> + AllSegments() const override; + + bool Initialized() const override; + std::string const& Description() const override; + + void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Upsert(std::string const& key, FlagDescriptor flag) override; + void Upsert(std::string const& key, SegmentDescriptor segment) override; + + ~MemoryStore() override = default; + + private: + static inline std::string description_ = "memory"; + std::unordered_map> flags_; + std::unordered_map> + segments_; + bool initialized_ = false; + mutable std::mutex data_mutex_; +}; + +} // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/tests/data_store_updater_test.cpp b/libs/server-sdk/tests/data_store_updater_test.cpp new file mode 100644 index 000000000..5125fbea6 --- /dev/null +++ b/libs/server-sdk/tests/data_store_updater_test.cpp @@ -0,0 +1,430 @@ +#include + +#include "data_store/data_store_updater.hpp" +#include "data_store/descriptors.hpp" +#include "data_store/memory_store.hpp" + +using launchdarkly::data_model::SDKDataSet; +using launchdarkly::server_side::data_store::DataStoreUpdater; +using launchdarkly::server_side::data_store::FlagDescriptor; +using launchdarkly::server_side::data_store::IDataStore; +using launchdarkly::server_side::data_store::MemoryStore; +using launchdarkly::server_side::data_store::SegmentDescriptor; + +using launchdarkly::Value; +using launchdarkly::data_model::Flag; +using launchdarkly::data_model::Segment; + +TEST(DataStoreUpdaterTest, DoesNotInitializeStoreUntilInit) { + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + EXPECT_FALSE(store->Initialized()); +} + +TEST(DataStoreUpdaterTest, InitializesStore) { + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + updater.Init(SDKDataSet()); + EXPECT_TRUE(store->Initialized()); +} + +TEST(DataStoreUpdaterTest, InitPropagatesData) { + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + Flag flag; + flag.version = 1; + flag.key = "flagA"; + flag.on = true; + flag.variations = std::vector{true, false}; + Flag::Variation variation = 0; + flag.fallthrough = variation; + + auto segment = Segment(); + segment.version = 1; + segment.key = "segmentA"; + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag)}}, + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment)}}, + }); + + auto fetched_flag = store->GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); + + auto fetched_segment = store->GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + Flag flag_a_v1; + flag_a_v1.version = 1; + flag_a_v1.key = "flagA"; + flag_a_v1.on = true; + flag_a_v1.variations = std::vector{true, false}; + Flag::Variation variation = 0; + flag_a_v1.fallthrough = variation; + + Flag flag_b_v1; + flag_b_v1.version = 1; + flag_b_v1.key = "flagA"; + flag_b_v1.on = true; + flag_b_v1.variations = std::vector{true, false}; + flag_b_v1.fallthrough = variation; + + Flag flab_c_v1; + flab_c_v1.version = 1; + flab_c_v1.key = "flagA"; + flab_c_v1.on = true; + flab_c_v1.variations = std::vector{true, false}; + flab_c_v1.fallthrough = variation; + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a_v1)}, + {"flagB", FlagDescriptor(flag_b_v1)}}, + std::unordered_map(), + }); + + Flag flag_a_v2; + flag_a_v2.version = 2; + flag_a_v2.key = "flagA"; + flag_a_v2.on = true; + flag_a_v2.variations = std::vector{true, false}; + flag_a_v2.fallthrough = variation; + + // Not updated. + Flag flag_c_v1_second; + flag_c_v1_second.version = 1; + flag_c_v1_second.key = "flagC"; + flag_c_v1_second.on = true; + flag_c_v1_second.variations = std::vector{true, false}; + flag_c_v1_second.fallthrough = variation; + + // New flag + Flag flag_d; + flag_d.version = 2; + flag_d.key = "flagD"; + flag_d.on = true; + flag_d.variations = std::vector{true, false}; + flag_d.fallthrough = variation; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + std::vector diff; + auto expectedSet = std::set{"flagA", "flagB", "flagD"}; + std::set_difference(expectedSet.begin(), expectedSet.end(), + changeset->begin(), changeset->end(), + std::inserter(diff, diff.begin())); + EXPECT_EQ(0, diff.size()); + }); + + // Updated flag A, deleted flag B, added flag C. + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a_v2)}, + {"flagD", FlagDescriptor(flag_d)}, + {"flagC", FlagDescriptor(flag_c_v1_second)}}, + std::unordered_map(), + }); + + EXPECT_TRUE(got_event); +} + +TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + updater.Upsert("flagA", FlagDescriptor(flag_a)); + + auto fetched_flag = store->GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.version = 2; + flag_a_2.key = "flagA"; + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag = store->GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(2, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { + Flag flag_a; + flag_a.version = 2; + flag_a.key = "flagA"; + flag_a.variations = std::vector{"potato", "ham"}; + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.version = 1; + flag_a_2.key = "flagA"; + flag_a.variations = std::vector{"potato"}; + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag = store->GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(2, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); + EXPECT_EQ(2, fetched_flag->item->variations.size()); + EXPECT_EQ(std::string("potato"), + fetched_flag->item->variations[0].AsString()); + EXPECT_EQ(std::string("ham"), fetched_flag->item->variations[1].AsString()); +} + +TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + updater.Upsert("segmentA", SegmentDescriptor(segment_a)); + + auto fetched_segment = store->GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}}, + }); + + Segment segment_a_2; + segment_a_2.version = 2; + segment_a_2.key = "segmentA"; + + updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + auto fetched_segment = store->GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(2, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { + Segment segment_a; + segment_a.version = 2; + segment_a.key = "segmentA"; + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}}, + }); + + Segment segment_a_2; + segment_a_2.version = 1; + segment_a_2.key = "segmentA"; + + updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + auto fetched_segment = store->GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(2, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { + Flag flag_a; + Flag flag_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.key = "flagA"; + flag_a_2.version = 2; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + std::vector diff; + auto expectedSet = std::set{"flagA", "flagB"}; + std::set_difference(expectedSet.begin(), expectedSet.end(), + changeset->begin(), changeset->end(), + std::inserter(diff, diff.begin())); + EXPECT_EQ(0, diff.size()); + }); + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + EXPECT_EQ(true, got_event); +} + +TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { + Flag flag_a; + Flag flag_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}, + }, + }); + + Segment segment_a_2; + segment_a_2.key = "flagA"; + segment_a_2.version = 2; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + }); + + updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + EXPECT_EQ(false, got_event); +} + +TEST(DataStoreUpdaterTest, NoEventOnDiscardedUpsert) { + Flag flag_a; + Flag flag_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + auto store = std::make_shared(); + DataStoreUpdater updater(store, store); + + updater.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.key = "flagA"; + flag_a_2.version = 1; + + std::atomic got_event(false); + updater.OnFlagChange( + [&got_event](std::shared_ptr> changeset) { + got_event = true; + }); + + updater.Upsert("flagA", FlagDescriptor(flag_a_2)); + + EXPECT_EQ(false, got_event); +} diff --git a/libs/server-sdk/tests/dependency_tracker_test.cpp b/libs/server-sdk/tests/dependency_tracker_test.cpp new file mode 100644 index 000000000..3bfacb643 --- /dev/null +++ b/libs/server-sdk/tests/dependency_tracker_test.cpp @@ -0,0 +1,297 @@ +#include + +#include "data_store/dependency_tracker.hpp" +#include "data_store/descriptors.hpp" + +using launchdarkly::server_side::data_store::DataKind; +using launchdarkly::server_side::data_store::DependencyMap; +using launchdarkly::server_side::data_store::DependencySet; +using launchdarkly::server_side::data_store::DependencyTracker; +using launchdarkly::server_side::data_store::FlagDescriptor; +using launchdarkly::server_side::data_store::SegmentDescriptor; + +using launchdarkly::AttributeReference; +using launchdarkly::Value; +using launchdarkly::data_model::Clause; +using launchdarkly::data_model::Flag; +using launchdarkly::data_model::ItemDescriptor; +using launchdarkly::data_model::Segment; + +TEST(ScopedSetTest, CanAddItem) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); +} + +TEST(ScopedSetTest, CanCheckIfContains) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); + + EXPECT_TRUE(set.Contains(DataKind::kFlag, "flagA")); + EXPECT_FALSE(set.Contains(DataKind::kFlag, "flagB")); + EXPECT_FALSE(set.Contains(DataKind::kSegment, "flagA")); +} + +TEST(ScopedSetTest, CanRemoveItem) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); + set.Remove(DataKind::kFlag, "flagA"); + EXPECT_FALSE(set.Contains(DataKind::kFlag, "flagA")); +} + +TEST(ScopedSetTest, CanIterate) { + DependencySet set; + set.Set(DataKind::kFlag, "flagA"); + set.Set(DataKind::kFlag, "flagB"); + set.Set(DataKind::kSegment, "segmentA"); + set.Set(DataKind::kSegment, "segmentB"); + + auto count = 0; + auto expectations = + std::vector{"flagA", "flagB", "segmentA", "segmentB"}; + + for (auto& ns : set) { + if (count == 0) { + EXPECT_EQ(DataKind::kFlag, ns.Kind()); + } else { + EXPECT_EQ(DataKind::kSegment, ns.Kind()); + } + for (auto val : ns.Data()) { + EXPECT_EQ(expectations[count], val); + count++; + } + } + EXPECT_EQ(4, count); +} + +TEST(ScopedMapTest, CanAddItem) { + DependencyMap map; + DependencySet deps; + deps.Set(DataKind::kSegment, "segmentA"); + + map.Set(DataKind::kFlag, "flagA", deps); +} + +TEST(ScopedMapTest, CanGetItem) { + DependencyMap map; + DependencySet deps; + deps.Set(DataKind::kSegment, "segmentA"); + + map.Set(DataKind::kFlag, "flagA", deps); + + EXPECT_TRUE(map.Get(DataKind::kFlag, "flagA") + ->Contains(DataKind::kSegment, "segmentA")); +} + +TEST(ScopedMapTest, CanIterate) { + DependencyMap map; + + DependencySet dep_flags; + dep_flags.Set(DataKind::kSegment, "segmentA"); + dep_flags.Set(DataKind::kFlag, "flagB"); + + DependencySet depSegments; + depSegments.Set(DataKind::kSegment, "segmentB"); + + map.Set(DataKind::kFlag, "flagA", dep_flags); + map.Set(DataKind::kSegment, "segmentA", depSegments); + + auto expectationKeys = + std::set{"segmentA", "flagB", "segmentB"}; + auto expectationKinds = std::vector{ + DataKind::kFlag, DataKind::kSegment, DataKind::kSegment}; + + auto count = 0; + for (auto& ns : map) { + if (count == 0) { + EXPECT_EQ(DataKind::kFlag, ns.Kind()); + } else { + EXPECT_EQ(DataKind::kSegment, ns.Kind()); + } + for (auto const& depSet : ns.Data()) { + for (auto const& deps : depSet.second) { + for (auto& dep : deps.Data()) { + EXPECT_EQ(expectationKinds[count], deps.Kind()); + EXPECT_TRUE(expectationKeys.count(dep) != 0); + expectationKeys.erase(dep); + count++; + } + } + } + } + EXPECT_EQ(3, count); +} + +TEST(ScopedMapTest, CanClear) { + DependencyMap map; + + DependencySet dep_flags; + dep_flags.Set(DataKind::kSegment, "segmentA"); + dep_flags.Set(DataKind::kFlag, "flagB"); + + DependencySet dep_segments; + dep_segments.Set(DataKind::kSegment, "segmentB"); + + map.Set(DataKind::kFlag, "flagA", dep_flags); + map.Set(DataKind::kSegment, "segmentA", dep_segments); + map.Clear(); + + for (auto& ns : map) { + for ([[maybe_unused]] auto& set_set : ns.Data()) { + GTEST_FAIL(); + } + } +} + +TEST(DependencyTrackerTest, TreatsPrerequisitesAsDependencies) { + DependencyTracker tracker; + + Flag flag_a; + Flag flag_b; + Flag flag_c; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + // Unused, to make sure not everything is just included in the dependencies. + flag_c.key = "flagC"; + flag_c.version = 1; + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); + tracker.UpdateDependencies("flagB", FlagDescriptor(flag_b)); + tracker.UpdateDependencies("flagC", FlagDescriptor(flag_c)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kFlag, "flagA", changes); + + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagB")); + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagA")); + EXPECT_EQ(2, changes.Size()); +} + +TEST(DependencyTrackerTest, UsesSegmentRulesToCalculateDependencies) { + DependencyTracker tracker; + + Flag flag_a; + Segment segment_a; + + Flag flag_b; + Segment segment_b; + + flag_a.key = "flagA"; + flag_a.version = 1; + + segment_a.key = "segmentA"; + segment_a.version = 1; + + // flagB and segmentB are unused. + flag_b.key = "flagB"; + flag_b.version = 1; + + segment_b.key = "segmentB"; + segment_b.version = 1; + + flag_a.rules.push_back(Flag::Rule{std::vector{ + Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, + "user", AttributeReference()}}}); + + tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); + tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); + + tracker.UpdateDependencies("flagB", FlagDescriptor(flag_b)); + tracker.UpdateDependencies("segmentB", SegmentDescriptor(segment_b)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kSegment, "segmentA", changes); + + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagA")); + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentA")); + EXPECT_EQ(2, changes.Size()); +} + +TEST(DependencyTrackerTest, TracksSegmentDependencyOfPrerequisite) { + DependencyTracker tracker; + + Flag flag_a; + Flag flag_b; + Segment segment_a; + + flag_a.key = "flagA"; + flag_a.version = 1; + + flag_b.key = "flagB"; + flag_b.version = 1; + + segment_a.key = "segmentA"; + segment_a.version = 1; + + flag_a.rules.push_back(Flag::Rule{std::vector{ + Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, + "", AttributeReference()}}}); + + flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); + + tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); + tracker.UpdateDependencies("flagB", FlagDescriptor(flag_b)); + tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kSegment, "segmentA", changes); + + // The segment itself was changed. + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentA")); + // flagA has a rule which depends on segmentA. + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagA")); + // flagB has a prerequisite of flagA. + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "flagB")); + EXPECT_EQ(3, changes.Size()); +} + +TEST(DependencyTrackerTest, HandlesSegmentsDependentOnOtherSegments) { + DependencyTracker tracker; + + Segment segment_a; + Segment segment_b; + Segment segment_c; + + segment_a.key = "segmentA"; + segment_a.version = 1; + + segment_b.key = "segmentB"; + segment_b.version = 1; + + segment_c.key = "segmentC"; + segment_c.version = 1; + + segment_b.rules.push_back(Segment::Rule{ + std::vector{Clause{Clause::Op::kSegmentMatch, + std::vector{"segmentA"}, false, + "user", AttributeReference()}}, + std::nullopt, std::nullopt, "", AttributeReference()}); + + tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); + tracker.UpdateDependencies("segmentB", SegmentDescriptor(segment_b)); + tracker.UpdateDependencies("segmentC", SegmentDescriptor(segment_c)); + + DependencySet changes; + tracker.CalculateChanges(DataKind::kSegment, "segmentA", changes); + + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentB")); + EXPECT_TRUE(changes.Contains(DataKind::kSegment, "segmentA")); + EXPECT_EQ(2, changes.Size()); +} + +TEST(DependencyTrackerTest, HandlesUpdateForSomethingThatDoesNotExist) { + // This shouldn't happen, but it should also not break. + DependencyTracker tracker; + + DependencySet changes; + tracker.CalculateChanges(DataKind::kFlag, "potato", changes); + + EXPECT_EQ(1, changes.Size()); + EXPECT_TRUE(changes.Contains(DataKind::kFlag, "potato")); +} diff --git a/libs/server-sdk/tests/memory_store_test.cpp b/libs/server-sdk/tests/memory_store_test.cpp new file mode 100644 index 000000000..852d112dc --- /dev/null +++ b/libs/server-sdk/tests/memory_store_test.cpp @@ -0,0 +1,286 @@ +#include + +#include "data_store/descriptors.hpp" +#include "data_store/memory_store.hpp" + +using launchdarkly::data_model::SDKDataSet; +using launchdarkly::server_side::data_store::FlagDescriptor; +using launchdarkly::server_side::data_store::IDataStore; +using launchdarkly::server_side::data_store::MemoryStore; +using launchdarkly::server_side::data_store::SegmentDescriptor; + +using launchdarkly::Value; +using launchdarkly::data_model::Flag; +using launchdarkly::data_model::Segment; + +TEST(MemoryStoreTest, StartsUninitialized) { + MemoryStore store; + EXPECT_FALSE(store.Initialized()); +} + +TEST(MemoryStoreTest, IsInitializedAfterInit) { + MemoryStore store; + store.Init(SDKDataSet()); + EXPECT_TRUE(store.Initialized()); +} + +TEST(MemoryStoreTest, HasDescription) { + MemoryStore store; + EXPECT_EQ(std::string("memory"), store.Description()); +} + +TEST(MemoryStoreTest, CanGetFlag) { + MemoryStore store; + Flag flag; + flag.version = 1; + flag.key = "flagA"; + flag.on = true; + flag.variations = std::vector{true, false}; + Flag::Variation variation = 0; + flag.fallthrough = variation; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag)}}, + std::unordered_map(), + }); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(MemoryStoreTest, CanGetAllFlags) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + Flag flag_b; + flag_b.version = 2; + flag_b.key = "flagB"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}, + {"flagB", FlagDescriptor(flag_b)}}, + std::unordered_map(), + }); + + auto fetched = store.AllFlags(); + EXPECT_EQ(2, fetched.size()); + + EXPECT_EQ(std::string("flagA"), fetched["flagA"]->item->key); + EXPECT_EQ(std::string("flagB"), fetched["flagB"]->item->key); +} + +TEST(MemoryStoreTest, CanGetAllFlagsWhenThereAreNoFlags) { + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + + auto fetched = store.AllFlags(); + EXPECT_EQ(0, fetched.size()); +} + +TEST(MemoryStoreTest, CanGetSegment) { + MemoryStore store; + auto segment = Segment(); + segment.version = 1; + segment.key = "segmentA"; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment)}}, + }); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(MemoryStoreTest, CanGetAllSegments) { + auto segment_a = Segment(); + segment_a.version = 1; + segment_a.key = "segmentA"; + + auto segment_b = Segment(); + segment_b.version = 2; + segment_b.key = "segmentB"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}, + {"segmentB", SegmentDescriptor(segment_b)}}, + }); + + auto fetched = store.AllSegments(); + EXPECT_EQ(2, fetched.size()); + + EXPECT_EQ(std::string("segmentA"), fetched["segmentA"]->item->key); + EXPECT_EQ(std::string("segmentB"), fetched["segmentB"]->item->key); +} + +TEST(MemoryStoreTest, CanGetAllSegmentsWhenThereAreNoSegments) { + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + + auto fetched = store.AllSegments(); + EXPECT_EQ(0, fetched.size()); +} + +TEST(MemoryStoreTest, GetMissingFlagOrSegment) { + MemoryStore store; + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_FALSE(fetched_flag); + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_FALSE(fetched_segment); +} + +TEST(MemoryStoreTest, CanUpsertNewFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + store.Upsert("flagA", FlagDescriptor(flag_a)); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(1, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(MemoryStoreTest, CanUpsertExitingFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + Flag flag_a_2; + flag_a_2.version = 2; + flag_a_2.key = "flagA"; + + store.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag = store.GetFlag("flagA"); + EXPECT_TRUE(fetched_flag); + EXPECT_TRUE(fetched_flag->item); + EXPECT_EQ("flagA", fetched_flag->item->key); + EXPECT_EQ(2, fetched_flag->item->version); + EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); +} + +TEST(MemoryStoreTest, CanUpsertNewSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map(), + }); + store.Upsert("segmentA", SegmentDescriptor(segment_a)); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(1, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(MemoryStoreTest, CanUpsertExitingSegment) { + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map(), + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}}, + }); + + Segment segment_a_2; + segment_a_2.version = 2; + segment_a_2.key = "segmentA"; + + store.Upsert("segmentA", SegmentDescriptor(segment_a_2)); + + auto fetched_segment = store.GetSegment("segmentA"); + EXPECT_TRUE(fetched_segment); + EXPECT_TRUE(fetched_segment->item); + EXPECT_EQ("segmentA", fetched_segment->item->key); + EXPECT_EQ(2, fetched_segment->item->version); + EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); +} + +TEST(MemoryStoreTest, OriginalFlagValidAfterUpsertOfFlag) { + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + flag_a.variations = std::vector{"potato", "ham"}; + + MemoryStore store; + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + auto fetched_flag_before = store.GetFlag("flagA"); + + Flag flag_a_2; + flag_a_2.version = 2; + flag_a_2.key = "flagA"; + flag_a_2.variations = std::vector{"potato"}; + + store.Upsert("flagA", FlagDescriptor(flag_a_2)); + + auto fetched_flag_after = store.GetFlag("flagA"); + + EXPECT_TRUE(fetched_flag_before); + EXPECT_TRUE(fetched_flag_before->item); + EXPECT_EQ("flagA", fetched_flag_before->item->key); + EXPECT_EQ(1, fetched_flag_before->item->version); + EXPECT_EQ(fetched_flag_before->version, fetched_flag_before->item->version); + EXPECT_EQ(2, fetched_flag_before->item->variations.size()); + EXPECT_EQ(std::string("potato"), + fetched_flag_before->item->variations[0].AsString()); + EXPECT_EQ(std::string("ham"), + fetched_flag_before->item->variations[1].AsString()); + + EXPECT_TRUE(fetched_flag_after); + EXPECT_TRUE(fetched_flag_after->item); + EXPECT_EQ("flagA", fetched_flag_after->item->key); + EXPECT_EQ(2, fetched_flag_after->item->version); + EXPECT_EQ(fetched_flag_after->version, fetched_flag_after->item->version); + EXPECT_EQ(1, fetched_flag_after->item->variations.size()); + EXPECT_EQ(std::string("potato"), + fetched_flag_after->item->variations[0].AsString()); +} From 3c129792ecc53965be1e2159cc87a65779f08d32 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:56:15 -0700 Subject: [PATCH 11/56] chore: Refactor to allow sharing data source status and IDataSource. (#174) Co-authored-by: Casey Waldren --- .../client_side/data_source_status.hpp | 225 ++++-------------- libs/client-sdk/src/CMakeLists.txt | 1 - libs/client-sdk/src/client_impl.cpp | 2 +- libs/client-sdk/src/client_impl.hpp | 9 +- .../data_source_event_handler.hpp | 2 +- .../src/data_sources/data_source_status.cpp | 81 ------- .../src/data_sources/null_data_source.hpp | 5 +- .../src/data_sources/polling_data_source.hpp | 4 +- .../data_sources/streaming_data_source.hpp | 4 +- .../tests/data_source_status_test.cpp | 36 +++ .../data_sources/data_source_status_base.hpp | 80 +++++++ .../data_source_status_error_info.hpp | 60 +++++ .../data_source_status_error_kind.hpp | 42 ++++ libs/common/src/CMakeLists.txt | 5 +- .../data_source_status_error_info.cpp | 19 ++ .../data_source_status_error_kind.cpp | 30 +++ libs/common/tests/data_source_status_test.cpp | 71 ++++++ .../data_sources/data_source.hpp | 2 +- libs/internal/src/CMakeLists.txt | 1 + libs/server-sdk/src/CMakeLists.txt | 2 +- .../data_source_update_sink.hpp | 0 .../src/data_store/data_store_updater.hpp | 2 +- .../src/data_store/memory_store.hpp | 2 +- 23 files changed, 407 insertions(+), 278 deletions(-) create mode 100644 libs/client-sdk/tests/data_source_status_test.cpp create mode 100644 libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp create mode 100644 libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp create mode 100644 libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp create mode 100644 libs/common/src/data_sources/data_source_status_error_info.cpp create mode 100644 libs/common/src/data_sources/data_source_status_error_kind.cpp create mode 100644 libs/common/tests/data_source_status_test.cpp rename libs/{client-sdk/src => internal/include/launchdarkly}/data_sources/data_source.hpp (92%) rename libs/server-sdk/src/{data_source => data_sources}/data_source_update_sink.hpp (100%) diff --git a/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp b/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp index c1591509e..cdf005a35 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp +++ b/libs/client-sdk/include/launchdarkly/client_side/data_source_status.hpp @@ -5,202 +5,75 @@ #include #include #include -#include -#include +#include #include +#include namespace launchdarkly::client_side::data_sources { -class DataSourceStatus { - public: - using DateTime = std::chrono::time_point; - +/** + * Enumeration of possible data source states. + */ +enum class ClientDataSourceState { /** - * Enumeration of possible data source states. + * The initial state of the data source when the SDK is being + * initialized. + * + * If it encounters an error that requires it to retry initialization, + * the state will remain at kInitializing until it either succeeds and + * becomes kValid, or permanently fails and becomes kShutdown. */ - enum class DataSourceState { - /** - * The initial state of the data source when the SDK is being - * initialized. - * - * If it encounters an error that requires it to retry initialization, - * the state will remain at kInitializing until it either succeeds and - * becomes kValid, or permanently fails and becomes kShutdown. - */ - kInitializing = 0, - - /** - * Indicates that the data source is currently operational and has not - * had any problems since the last time it received data. - * - * In streaming mode, this means that there is currently an open stream - * connection and that at least one initial message has been received on - * the stream. In polling mode, it means that the last poll request - * succeeded. - */ - kValid = 1, - - /** - * Indicates that the data source encountered an error that it will - * attempt to recover from. - * - * In streaming mode, this means that the stream connection failed, or - * had to be dropped due to some other error, and will be retried after - * a backoff delay. In polling mode, it means that the last poll request - * failed, and a new poll request will be made after the configured - * polling interval. - */ - kInterrupted = 2, - - /** - * Indicates that the application has told the SDK to stay offline. - */ - kSetOffline = 3, - - /** - * Indicates that the data source has been permanently shut down. - * - * This could be because it encountered an unrecoverable error (for - * instance, the LaunchDarkly service rejected the SDK key; an invalid - * SDK key will never become valid), or because the SDK client was - * explicitly shut down. - */ - kShutdown = 4, - - // BackgroundDisabled, - // TODO: A plugin of sorts would likely be required for some - // functionality like this. kNetworkUnavailable, - }; + kInitializing = 0, /** - * A description of an error condition that the data source encountered. + * Indicates that the data source is currently operational and has not + * had any problems since the last time it received data. + * + * In streaming mode, this means that there is currently an open stream + * connection and that at least one initial message has been received on + * the stream. In polling mode, it means that the last poll request + * succeeded. */ - class ErrorInfo { - public: - using StatusCodeType = uint64_t; - - /** - * An enumeration describing the general type of an error. - */ - enum class ErrorKind { - /** - * An unexpected error, such as an uncaught exception, further - * described by the error message. - */ - kUnknown = 0, - - /** - * An I/O error such as a dropped connection. - */ - kNetworkError = 1, - - /** - * The LaunchDarkly service returned an HTTP response with an error - * status, available in the status code. - */ - kErrorResponse = 2, - - /** - * The SDK received malformed data from the LaunchDarkly service. - */ - kInvalidData = 3, - - /** - * The data source itself is working, but when it tried to put an - * update into the data store, the data store failed (so the SDK may - * not have the latest data). - */ - kStoreError = 4 - }; - - /** - * An enumerated value representing the general category of the error. - */ - [[nodiscard]] ErrorKind Kind() const; - - /** - * The HTTP status code if the error was ErrorKind::kErrorResponse. - */ - [[nodiscard]] StatusCodeType StatusCode() const; - - /** - * Any additional human-readable information relevant to the error. - * - * The format is subject to change and should not be relied on - * programmatically. - */ - [[nodiscard]] std::string const& Message() const; - - /** - * The date/time that the error occurred. - */ - [[nodiscard]] DateTime Time() const; - - ErrorInfo(ErrorKind kind, - StatusCodeType status_code, - std::string message, - DateTime time); - - private: - ErrorKind kind_; - StatusCodeType status_code_; - std::string message_; - DateTime time_; - }; + kValid = 1, /** - * An enumerated value representing the overall current state of the data - * source. + * Indicates that the data source encountered an error that it will + * attempt to recover from. + * + * In streaming mode, this means that the stream connection failed, or + * had to be dropped due to some other error, and will be retried after + * a backoff delay. In polling mode, it means that the last poll request + * failed, and a new poll request will be made after the configured + * polling interval. */ - [[nodiscard]] DataSourceState State() const; + kInterrupted = 2, /** - * The date/time that the value of State most recently changed. - * - * The meaning of this depends on the current state: - * - For DataSourceState::kInitializing, it is the time that the SDK started - * initializing. - * - For DataSourceState::kValid, it is the time that the data - * source most recently entered a valid state, after previously having been - * DataSourceState::kInitializing or an invalid state such as - * DataSourceState::kInterrupted. - * - For DataSourceState::kInterrupted, it is the time that the data source - * most recently entered an error state, after previously having been - * DataSourceState::kValid. - * - For DataSourceState::kShutdown, it is the time that the data source - * encountered an unrecoverable error or that the SDK was explicitly shut - * down. + * Indicates that the application has told the SDK to stay offline. */ - [[nodiscard]] DateTime StateSince() const; + kSetOffline = 3, /** - * Information about the last error that the data source encountered, if - * any. + * Indicates that the data source has been permanently shut down. * - * This property should be updated whenever the data source encounters a - * problem, even if it does not cause the state to change. For instance, if - * a stream connection fails and the state changes to - * DataSourceState::kInterrupted, and then subsequent attempts to restart - * the connection also fail, the state will remain - * DataSourceState::kInterrupted but the error information will be updated - * each time-- and the last error will still be reported in this property - * even if the state later becomes DataSourceState::kValid. + * This could be because it encountered an unrecoverable error (for + * instance, the LaunchDarkly service rejected the SDK key; an invalid + * SDK key will never become valid), or because the SDK client was + * explicitly shut down. */ - [[nodiscard]] std::optional LastError() const; - - DataSourceStatus(DataSourceState state, - DateTime state_since, - std::optional last_error); + kShutdown = 4, - DataSourceStatus(DataSourceStatus const& status); + // BackgroundDisabled, - private: - DataSourceState state_; - DateTime state_since_; - std::optional last_error_; + // TODO: A plugin of sorts would likely be required to implement + // network availability. + // kNetworkUnavailable, }; +using DataSourceStatus = + common::data_sources::DataSourceStatusBase; + /** * Interface for accessing and listening to the data source status. */ @@ -210,7 +83,7 @@ class IDataSourceStatusProvider { * The current status of the data source. Suitable for broadcast to * data source status listeners. */ - virtual DataSourceStatus Status() const = 0; + [[nodiscard]] virtual DataSourceStatus Status() const = 0; /** * Listen to changes to the data source status. @@ -246,12 +119,6 @@ class IDataSourceStatusProvider { std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state); -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo::ErrorKind const& kind); - std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo const& error); - } // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index 23e3681b9..ef6f64b38 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -20,7 +20,6 @@ add_library(${LIBNAME} client_impl.cpp client.cpp client_impl.hpp - data_sources/data_source.hpp data_sources/data_source_event_handler.hpp data_sources/data_source_status_manager.hpp data_sources/data_source_update_sink.hpp diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index 224d71555..ed753dfb7 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -32,7 +32,7 @@ using launchdarkly::client_side::data_sources::DataSourceStatus; using launchdarkly::config::shared::built::DataSourceConfig; using launchdarkly::config::shared::built::HttpProperties; -static std::shared_ptr MakeDataSource( +static std::shared_ptr<::launchdarkly::data_sources::IDataSource> MakeDataSource( HttpProperties const& http_properties, Config const& config, Context const& context, diff --git a/libs/client-sdk/src/client_impl.hpp b/libs/client-sdk/src/client_impl.hpp index 20ab4cf5d..6c226b9d8 100644 --- a/libs/client-sdk/src/client_impl.hpp +++ b/libs/client-sdk/src/client_impl.hpp @@ -19,11 +19,11 @@ #include #include #include +#include #include #include #include -#include "data_sources/data_source.hpp" #include "data_sources/data_source_status_manager.hpp" #include "event_processor.hpp" #include "flag_manager/flag_manager.hpp" @@ -37,7 +37,7 @@ class ClientImpl : public IClient { ClientImpl(ClientImpl const&) = delete; ClientImpl& operator=(ClientImpl) = delete; ClientImpl& operator=(ClientImpl&& other) = delete; - + bool Initialized() const override; using FlagKey = std::string; @@ -129,9 +129,10 @@ class ClientImpl : public IClient { mutable std::shared_mutex context_mutex_; flag_manager::FlagManager flag_manager_; - std::function()> data_source_factory_; + std::function()> + data_source_factory_; - std::shared_ptr data_source_; + std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; std::unique_ptr event_processor_; diff --git a/libs/client-sdk/src/data_sources/data_source_event_handler.hpp b/libs/client-sdk/src/data_sources/data_source_event_handler.hpp index a7115f2aa..90866d480 100644 --- a/libs/client-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/client-sdk/src/data_sources/data_source_event_handler.hpp @@ -2,13 +2,13 @@ #include -#include "data_source.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" #include #include #include +#include #include namespace launchdarkly::client_side::data_sources { diff --git a/libs/client-sdk/src/data_sources/data_source_status.cpp b/libs/client-sdk/src/data_sources/data_source_status.cpp index 56770c61d..437b5b357 100644 --- a/libs/client-sdk/src/data_sources/data_source_status.cpp +++ b/libs/client-sdk/src/data_sources/data_source_status.cpp @@ -1,59 +1,9 @@ - #include -#include #include namespace launchdarkly::client_side::data_sources { -DataSourceStatus::ErrorInfo::ErrorKind DataSourceStatus::ErrorInfo::Kind() - const { - return kind_; -} -DataSourceStatus::ErrorInfo::StatusCodeType -DataSourceStatus::ErrorInfo::StatusCode() const { - return status_code_; -} -std::string const& DataSourceStatus::ErrorInfo::Message() const { - return message_; -} -DataSourceStatus::DateTime DataSourceStatus::ErrorInfo::Time() const { - return time_; -} - -DataSourceStatus::ErrorInfo::ErrorInfo(ErrorInfo::ErrorKind kind, - ErrorInfo::StatusCodeType status_code, - std::string message, - DataSourceStatus::DateTime time) - : kind_(kind), - status_code_(status_code), - message_(std::move(message)), - time_(time) {} - -DataSourceStatus::DataSourceState DataSourceStatus::State() const { - return state_; -} - -DataSourceStatus::DateTime DataSourceStatus::StateSince() const { - return state_since_; -} - -std::optional DataSourceStatus::LastError() const { - return last_error_; -} - -DataSourceStatus::DataSourceStatus(DataSourceStatus const& status) - : state_(status.State()), - state_since_(status.StateSince()), - last_error_(status.LastError()) {} - -DataSourceStatus::DataSourceStatus(DataSourceState state, - DataSourceStatus::DateTime state_since, - std::optional last_error) - : state_(state), - state_since_(state_since), - last_error_(std::move(last_error)) {} - std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state) { switch (state) { @@ -77,28 +27,6 @@ std::ostream& operator<<(std::ostream& out, return out; } -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo::ErrorKind const& kind) { - switch (kind) { - case DataSourceStatus::ErrorInfo::ErrorKind::kUnknown: - out << "UNKNOWN"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError: - out << "NETWORK_ERROR"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse: - out << "ERROR_RESPONSE"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData: - out << "INVALID_DATA"; - break; - case DataSourceStatus::ErrorInfo::ErrorKind::kStoreError: - out << "STORE_ERROR"; - break; - } - return out; -} - std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { std::time_t as_time_t = std::chrono::system_clock::to_time_t(status.StateSince()); @@ -111,13 +39,4 @@ std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { return out; } -std::ostream& operator<<(std::ostream& out, - DataSourceStatus::ErrorInfo const& error) { - std::time_t as_time_t = std::chrono::system_clock::to_time_t(error.Time()); - out << "Error(" << error.Kind() << ", " << error.Message() - << ", StatusCode(" << error.StatusCode() << "), Since(" - << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << "))"; - return out; -} - } // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/null_data_source.hpp b/libs/client-sdk/src/data_sources/null_data_source.hpp index b99ec07f0..e2fa7cc22 100644 --- a/libs/client-sdk/src/data_sources/null_data_source.hpp +++ b/libs/client-sdk/src/data_sources/null_data_source.hpp @@ -2,12 +2,13 @@ #include -#include "data_source.hpp" #include "data_source_status_manager.hpp" +#include + namespace launchdarkly::client_side::data_sources { -class NullDataSource : public IDataSource { +class NullDataSource : public ::launchdarkly::data_sources::IDataSource { public: explicit NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager); diff --git a/libs/client-sdk/src/data_sources/polling_data_source.hpp b/libs/client-sdk/src/data_sources/polling_data_source.hpp index 53e7656c1..62874b1f5 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.hpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.hpp @@ -4,7 +4,6 @@ #include -#include "data_source.hpp" #include "data_source_event_handler.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" @@ -12,13 +11,14 @@ #include #include #include +#include #include #include namespace launchdarkly::client_side::data_sources { class PollingDataSource - : public IDataSource, + : public ::launchdarkly::data_sources::IDataSource, public std::enable_shared_from_this { public: PollingDataSource( diff --git a/libs/client-sdk/src/data_sources/streaming_data_source.hpp b/libs/client-sdk/src/data_sources/streaming_data_source.hpp index a8eec90af..430e77888 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.hpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.hpp @@ -5,7 +5,6 @@ using namespace std::chrono_literals; #include -#include "data_source.hpp" #include "data_source_event_handler.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" @@ -16,13 +15,14 @@ using namespace std::chrono_literals; #include #include #include +#include #include #include namespace launchdarkly::client_side::data_sources { class StreamingDataSource final - : public IDataSource, + : public ::launchdarkly::data_sources::IDataSource, public std::enable_shared_from_this { public: StreamingDataSource( diff --git a/libs/client-sdk/tests/data_source_status_test.cpp b/libs/client-sdk/tests/data_source_status_test.cpp new file mode 100644 index 000000000..109778287 --- /dev/null +++ b/libs/client-sdk/tests/data_source_status_test.cpp @@ -0,0 +1,36 @@ +#include + +#include + +using launchdarkly::client_side::data_sources::DataSourceStatus; + +TEST(DataSourceStatusTest, OstreamBasicStatus) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + + DataSourceStatus status(DataSourceStatus::DataSourceState::kInitializing, + time, std::nullopt); + + std::stringstream ss; + ss << status; + EXPECT_EQ("Status(INITIALIZING, Since(1970-01-01 00:00:00))", ss.str()); +} + +TEST(DataSourceStatusTest, OStreamErrorInfo) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + + DataSourceStatus status( + DataSourceStatus::DataSourceState::kInterrupted, time, + launchdarkly::common::data_sources::DataSourceStatusErrorInfo( + launchdarkly::common::data_sources::DataSourceStatusErrorKind:: + kInvalidData, + 404, "Bad times", time)); + + std::stringstream ss; + ss << status; + EXPECT_EQ( + "Status(INTERRUPTED, Since(1970-01-01 00:00:00), Error(INVALID_DATA, " + "Bad times, StatusCode(404), Since(1970-01-01 00:00:00)))", + ss.str()); +} diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp new file mode 100644 index 000000000..3f758f2e0 --- /dev/null +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_base.hpp @@ -0,0 +1,80 @@ +#include +#include + +#include +#include + +// Common is included in the namespace to disambiguate from client/server +// for backward compatibility. +namespace launchdarkly::common::data_sources { + +template +class DataSourceStatusBase { + public: + using ErrorKind = DataSourceStatusErrorKind; + using ErrorInfo = DataSourceStatusErrorInfo; + using DateTime = std::chrono::time_point; + using DataSourceState = TDataSourceState; + + /** + * An enumerated value representing the overall current state of the data + * source. + */ + [[nodiscard]] DataSourceState State() const { return state_; } + + /** + * The date/time that the value of State most recently changed. + * + * The meaning of this depends on the current state: + * - For DataSourceState::kInitializing, it is the time that the SDK started + * initializing. + * - For DataSourceState::kValid, it is the time that the data + * source most recently entered a valid state, after previously having been + * DataSourceState::kInitializing or an invalid state such as + * DataSourceState::kInterrupted. + * - For DataSourceState::kInterrupted, it is the time that the data source + * most recently entered an error state, after previously having been + * DataSourceState::kValid. + * - For DataSourceState::kShutdown (client-side) or DataSourceState::kOff + * (server-side), it is the time that the data source encountered an + * unrecoverable error or that the SDK was explicitly shut down. + */ + [[nodiscard]] DateTime StateSince() const { return state_since_; } + + /** + * Information about the last error that the data source encountered, if + * any. + * + * This property should be updated whenever the data source encounters a + * problem, even if it does not cause the state to change. For instance, if + * a stream connection fails and the state changes to + * DataSourceState::kInterrupted, and then subsequent attempts to restart + * the connection also fail, the state will remain + * DataSourceState::kInterrupted but the error information will be updated + * each time-- and the last error will still be reported in this property + * even if the state later becomes DataSourceState::kValid. + */ + [[nodiscard]] std::optional LastError() const { + return last_error_; + } + + DataSourceStatusBase(DataSourceState state, + DateTime state_since, + std::optional last_error) + : state_(state), + state_since_(state_since), + last_error_(std::move(last_error)) {} + + ~DataSourceStatusBase() = default; + DataSourceStatusBase(DataSourceStatusBase const& item) = default; + DataSourceStatusBase(DataSourceStatusBase&& item) noexcept = default; + DataSourceStatusBase& operator=(DataSourceStatusBase const&) = delete; + DataSourceStatusBase& operator=(DataSourceStatusBase&&) = delete; + + private: + DataSourceState state_; + DateTime state_since_; + std::optional last_error_; +}; + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp new file mode 100644 index 000000000..f7ddfa4d0 --- /dev/null +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include + +namespace launchdarkly::common::data_sources { + +/** + * A description of an error condition that the data source encountered. + */ +class DataSourceStatusErrorInfo { + public: + using StatusCodeType = uint64_t; + using ErrorKind = DataSourceStatusErrorKind; + using DateTime = std::chrono::time_point; + + /** + * An enumerated value representing the general category of the error. + */ + [[nodiscard]] ErrorKind Kind() const { return kind_; } + + /** + * The HTTP status code if the error was ErrorKind::kErrorResponse. + */ + [[nodiscard]] StatusCodeType StatusCode() const { return status_code_; } + + /** + * Any additional human-readable information relevant to the error. + * + * The format is subject to change and should not be relied on + * programmatically. + */ + [[nodiscard]] std::string const& Message() const { return message_; } + + /** + * The date/time that the error occurred. + */ + [[nodiscard]] DateTime Time() const { return time_; } + + DataSourceStatusErrorInfo(ErrorKind kind, + StatusCodeType status_code, + std::string message, + DateTime time) + : kind_(kind), + status_code_(status_code), + message_(std::move(message)), + time_(time) {} + + private: + ErrorKind kind_; + StatusCodeType status_code_; + std::string message_; + DateTime time_; +}; + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorInfo const& error); + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp new file mode 100644 index 000000000..f540d3d65 --- /dev/null +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp @@ -0,0 +1,42 @@ +#pragma once + +namespace launchdarkly::common::data_sources { + +/** + * An enumeration describing the general type of an error. + */ +enum class DataSourceStatusErrorKind { + /** + * An unexpected error, such as an uncaught exception, further + * described by the error message. + */ + kUnknown = 0, + + /** + * An I/O error such as a dropped connection. + */ + kNetworkError = 1, + + /** + * The LaunchDarkly service returned an HTTP response with an error + * status, available in the status code. + */ + kErrorResponse = 2, + + /** + * The SDK received malformed data from the LaunchDarkly service. + */ + kInvalidData = 3, + + /** + * The data source itself is working, but when it tried to put an + * update into the data store, the data store failed (so the SDK may + * not have the latest data). + */ + kStoreError = 4 +}; + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorKind const& kind); + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index de243e89c..6b8fb67fc 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -11,6 +11,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/config/shared/built/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/data/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/logging/*.hpp" + "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/data_sources/*.hpp" ) # Automatic library: static or dynamic based on user config. @@ -50,7 +51,9 @@ add_library(${LIBNAME} OBJECT bindings/c/data/evaluation_detail.cpp bindings/c/memory_routines.cpp config/logging_builder.cpp - bindings/c/listener_connection.cpp) + bindings/c/listener_connection.cpp + data_sources/data_source_status_error_kind.cpp + data_sources/data_source_status_error_info.cpp) add_library(launchdarkly::common ALIAS ${LIBNAME}) diff --git a/libs/common/src/data_sources/data_source_status_error_info.cpp b/libs/common/src/data_sources/data_source_status_error_info.cpp new file mode 100644 index 000000000..b0299af5f --- /dev/null +++ b/libs/common/src/data_sources/data_source_status_error_info.cpp @@ -0,0 +1,19 @@ +#include +#include +#include +#include + +#include + +namespace launchdarkly::common::data_sources { + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorInfo const& error) { + std::time_t as_time_t = std::chrono::system_clock::to_time_t(error.Time()); + out << "Error(" << error.Kind() << ", " << error.Message() + << ", StatusCode(" << error.StatusCode() << "), Since(" + << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << "))"; + return out; +} + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/src/data_sources/data_source_status_error_kind.cpp b/libs/common/src/data_sources/data_source_status_error_kind.cpp new file mode 100644 index 000000000..69545eb16 --- /dev/null +++ b/libs/common/src/data_sources/data_source_status_error_kind.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include + +namespace launchdarkly::common::data_sources { + +std::ostream& operator<<(std::ostream& out, + DataSourceStatusErrorKind const& kind) { + switch (kind) { + case DataSourceStatusErrorKind::kUnknown: + out << "UNKNOWN"; + break; + case DataSourceStatusErrorKind::kNetworkError: + out << "NETWORK_ERROR"; + break; + case DataSourceStatusErrorKind::kErrorResponse: + out << "ERROR_RESPONSE"; + break; + case DataSourceStatusErrorKind::kInvalidData: + out << "INVALID_DATA"; + break; + case DataSourceStatusErrorKind::kStoreError: + out << "STORE_ERROR"; + break; + } + return out; +} + +} // namespace launchdarkly::common::data_sources diff --git a/libs/common/tests/data_source_status_test.cpp b/libs/common/tests/data_source_status_test.cpp new file mode 100644 index 000000000..c3b165803 --- /dev/null +++ b/libs/common/tests/data_source_status_test.cpp @@ -0,0 +1,71 @@ +#include + +#include + +namespace test_things { +enum class TestDataSourceStates { kStateA = 0, kStateB = 1, kStateC = 2 }; + +using DataSourceStatus = + launchdarkly::common::data_sources::DataSourceStatusBase< + TestDataSourceStates>; + +std::ostream& operator<<(std::ostream& out, TestDataSourceStates const& state) { + switch (state) { + case TestDataSourceStates::kStateA: + out << "kStateA"; + break; + case TestDataSourceStates::kStateB: + out << "kStateB"; + break; + case TestDataSourceStates::kStateC: + out << "kStateC"; + break; + } + + return out; +} + +std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { + std::time_t as_time_t = + std::chrono::system_clock::to_time_t(status.StateSince()); + out << "Status(" << status.State() << ", Since(" + << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << ")"; + if (status.LastError()) { + out << ", " << status.LastError().value(); + } + out << ")"; + return out; +} +} // namespace test_things + +TEST(DataSourceStatusTest, OstreamBasicStatus) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + ; + test_things::DataSourceStatus status( + test_things::DataSourceStatus::DataSourceState::kStateA, time, + std::nullopt); + + std::stringstream ss; + ss << status; + EXPECT_EQ("Status(kStateA, Since(1970-01-01 00:00:00))", ss.str()); +} + +TEST(DataSourceStatusTest, OStreamErrorInfo) { + auto time = + std::chrono::system_clock::time_point{std::chrono::milliseconds{0}}; + ; + test_things::DataSourceStatus status( + test_things::DataSourceStatus::DataSourceState::kStateC, time, + launchdarkly::common::data_sources::DataSourceStatusErrorInfo( + launchdarkly::common::data_sources::DataSourceStatusErrorKind:: + kInvalidData, + 404, "Bad times", time)); + + std::stringstream ss; + ss << status; + EXPECT_EQ( + "Status(kStateC, Since(1970-01-01 00:00:00), Error(INVALID_DATA, Bad " + "times, StatusCode(404), Since(1970-01-01 00:00:00)))", + ss.str()); +} diff --git a/libs/client-sdk/src/data_sources/data_source.hpp b/libs/internal/include/launchdarkly/data_sources/data_source.hpp similarity index 92% rename from libs/client-sdk/src/data_sources/data_source.hpp rename to libs/internal/include/launchdarkly/data_sources/data_source.hpp index 368f684ed..8c6d6d807 100644 --- a/libs/client-sdk/src/data_sources/data_source.hpp +++ b/libs/internal/include/launchdarkly/data_sources/data_source.hpp @@ -1,6 +1,6 @@ #pragma once #include -namespace launchdarkly::client_side { +namespace launchdarkly::data_sources { class IDataSource { public: diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index 9fb801885..99b4c9dd3 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -6,6 +6,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/serialization/events/*.hpp" "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/signals/*.hpp" + "${LaunchDarklyInternalSdk_SOURCE_DIR}/include/launchdarkly/data_sources/*.hpp" ) # Automatic library: static or dynamic based on user config. diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 46b0e0272..cb6184295 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -7,7 +7,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS add_library(${LIBNAME} ${HEADER_LIST} - data_source/data_source_update_sink.hpp + data_sources/data_source_update_sink.hpp data_store/data_store.hpp data_store/data_store_updater.hpp data_store/data_store_updater.cpp diff --git a/libs/server-sdk/src/data_source/data_source_update_sink.hpp b/libs/server-sdk/src/data_sources/data_source_update_sink.hpp similarity index 100% rename from libs/server-sdk/src/data_source/data_source_update_sink.hpp rename to libs/server-sdk/src/data_sources/data_source_update_sink.hpp diff --git a/libs/server-sdk/src/data_store/data_store_updater.hpp b/libs/server-sdk/src/data_store/data_store_updater.hpp index e76758af3..e3e3ea869 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.hpp +++ b/libs/server-sdk/src/data_store/data_store_updater.hpp @@ -1,6 +1,6 @@ #pragma once -#include "../data_source/data_source_update_sink.hpp" +#include "../data_sources/data_source_update_sink.hpp" #include "data_store.hpp" #include "dependency_tracker.hpp" diff --git a/libs/server-sdk/src/data_store/memory_store.hpp b/libs/server-sdk/src/data_store/memory_store.hpp index ea3e11a2d..df5160b74 100644 --- a/libs/server-sdk/src/data_store/memory_store.hpp +++ b/libs/server-sdk/src/data_store/memory_store.hpp @@ -1,6 +1,6 @@ #pragma once -#include "../data_source/data_source_update_sink.hpp" +#include "../data_sources/data_source_update_sink.hpp" #include "data_store.hpp" #include From 08aaa664f4a885f6056aa8b50a430df89536d889 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 17 Jul 2023 08:52:27 -0700 Subject: [PATCH 12/56] feat: Implement streaming data source. (#179) Co-authored-by: Casey Waldren --- .clang-tidy | 2 +- libs/client-sdk/src/CMakeLists.txt | 1 - .../data_source_status_manager.cpp | 129 --------- .../data_source_status_manager.hpp | 89 +------ .../launchdarkly/data_sources/data_source.hpp | 2 +- .../data_source_status_manager_base.hpp | 188 ++++++++++++++ .../server_side/data_source_status.hpp | 116 +++++++++ libs/server-sdk/src/CMakeLists.txt | 11 +- .../data_source_event_handler.cpp | 241 +++++++++++++++++ .../data_source_event_handler.hpp | 124 +++++++++ .../src/data_sources/data_source_status.cpp | 40 +++ .../data_source_status_manager.hpp | 28 ++ .../data_sources/data_source_update_sink.hpp | 4 +- .../src/data_sources/polling_data_source.cpp | 245 ++++++++++++++++++ .../src/data_sources/polling_data_source.hpp | 58 +++++ .../data_sources/streaming_data_source.cpp | 152 +++++++++++ .../data_sources/streaming_data_source.hpp | 55 ++++ .../src/data_store/data_store_updater.hpp | 2 +- .../src/data_store/memory_store.hpp | 10 +- libs/server-sdk/tests/CMakeLists.txt | 2 +- .../tests/data_source_event_handler_test.cpp | 202 +++++++++++++++ 21 files changed, 1484 insertions(+), 217 deletions(-) delete mode 100644 libs/client-sdk/src/data_sources/data_source_status_manager.cpp create mode 100644 libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp create mode 100644 libs/server-sdk/src/data_sources/data_source_event_handler.cpp create mode 100644 libs/server-sdk/src/data_sources/data_source_event_handler.hpp create mode 100644 libs/server-sdk/src/data_sources/data_source_status.cpp create mode 100644 libs/server-sdk/src/data_sources/data_source_status_manager.hpp create mode 100644 libs/server-sdk/src/data_sources/polling_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/polling_data_source.hpp create mode 100644 libs/server-sdk/src/data_sources/streaming_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/streaming_data_source.hpp create mode 100644 libs/server-sdk/tests/data_source_event_handler_test.cpp diff --git a/.clang-tidy b/.clang-tidy index 418e839a5..154cb4e32 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,5 +1,5 @@ --- CheckOptions: - { key: readability-identifier-length.IgnoredParameterNames, value: 'i|j|k|c|os|it' } - - { key: readability-identifier-length.IgnoredVariableNames, value: 'ec|id' } + - { key: readability-identifier-length.IgnoredVariableNames, value: 'ec|id|it' } - { key: readability-identifier-length.IgnoredLoopCounterNames, value: 'i|j|k|c|os|it' } diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index ef6f64b38..e7ce7f890 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -14,7 +14,6 @@ add_library(${LIBNAME} flag_manager/flag_updater.cpp flag_manager/flag_change_event.cpp data_sources/data_source_status.cpp - data_sources/data_source_status_manager.cpp event_processor/event_processor.cpp event_processor/null_event_processor.cpp client_impl.cpp diff --git a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp b/libs/client-sdk/src/data_sources/data_source_status_manager.cpp deleted file mode 100644 index 618ed8610..000000000 --- a/libs/client-sdk/src/data_sources/data_source_status_manager.cpp +++ /dev/null @@ -1,129 +0,0 @@ -#include -#include -#include -#include - -#include -#include - -#include "data_source_status_manager.hpp" - -namespace launchdarkly::client_side::data_sources { - -void DataSourceStatusManager::SetState( - DataSourceStatus::DataSourceState state) { - bool changed = UpdateState(state); - if (changed) { - data_source_status_signal_(std::move(Status())); - } -} - -void DataSourceStatusManager::SetState( - DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message) { - { - std::lock_guard lock(status_mutex_); - - UpdateState(state); - - last_error_ = DataSourceStatus::ErrorInfo( - DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, - message, std::chrono::system_clock::now()); - } - - data_source_status_signal_(std::move(Status())); -} -bool DataSourceStatusManager::UpdateState( - DataSourceStatus::DataSourceState const& requested_state) { - std::lock_guard lock(status_mutex_); - - // If initializing, then interruptions remain initializing. - auto new_state = - (requested_state == DataSourceStatus::DataSourceState::kInterrupted && - state_ == DataSourceStatus::DataSourceState::kInitializing) - ? DataSourceStatus::DataSourceState:: - kInitializing // see comment on - // IDataSourceUpdateSink.UpdateStatus - : requested_state; - auto changed = state_ != new_state; - if (changed) { - state_ = new_state; - state_since_ = std::chrono::system_clock::now(); - } - return changed; -} - -void DataSourceStatusManager::SetState( - DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message) { - { - std::lock_guard lock(status_mutex_); - - UpdateState(state); - - last_error_ = DataSourceStatus::ErrorInfo( - kind, 0, std::move(message), std::chrono::system_clock::now()); - } - - data_source_status_signal_(Status()); -} - -void DataSourceStatusManager::SetError( - DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message) { - { - std::lock_guard lock(status_mutex_); - last_error_ = DataSourceStatus::ErrorInfo( - kind, 0, std::move(message), std::chrono::system_clock::now()); - state_since_ = std::chrono::system_clock::now(); - } - - data_source_status_signal_(Status()); -} - -void DataSourceStatusManager::SetError( - DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message) { - { - std::lock_guard lock(status_mutex_); - last_error_ = DataSourceStatus::ErrorInfo( - DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, - message, std::chrono::system_clock::now()); - state_since_ = std::chrono::system_clock::now(); - } - data_source_status_signal_(Status()); -} - -DataSourceStatus DataSourceStatusManager::Status() const { - std::lock_guard lock(status_mutex_); - return {state_, state_since_, last_error_}; -} - -std::unique_ptr DataSourceStatusManager::OnDataSourceStatusChange( - std::function handler) { - std::lock_guard lock{status_mutex_}; - return std::make_unique< - ::launchdarkly::internal::signals::SignalConnection>( - data_source_status_signal_.connect(handler)); -} - -std::unique_ptr -DataSourceStatusManager::OnDataSourceStatusChangeEx( - std::function handler) { - std::lock_guard lock{status_mutex_}; - return std::make_unique( - data_source_status_signal_.connect_extended( - [handler](boost::signals2::connection const& conn, - data_sources::DataSourceStatus status) { - if (handler(status)) { - conn.disconnect(); - } - })); -} -DataSourceStatusManager::DataSourceStatusManager() - : state_(DataSourceStatus::DataSourceState::kInitializing), - state_since_(std::chrono::system_clock::now()) {} - -} // namespace launchdarkly::client_side::data_sources diff --git a/libs/client-sdk/src/data_sources/data_source_status_manager.hpp b/libs/client-sdk/src/data_sources/data_source_status_manager.hpp index 62bab885c..b2b52eb4e 100644 --- a/libs/client-sdk/src/data_sources/data_source_status_manager.hpp +++ b/libs/client-sdk/src/data_sources/data_source_status_manager.hpp @@ -7,89 +7,22 @@ #include #include +#include namespace launchdarkly::client_side::data_sources { -/** - * Class that manages updates to the data source status and implements an - * interface to get the current status and listen to status changes. - */ -class DataSourceStatusManager : public IDataSourceStatusProvider { +class DataSourceStatusManager + : public internal::data_sources::DataSourceStatusManagerBase< + DataSourceStatus, + IDataSourceStatusProvider> { public: - using DataSourceStatusHandler = - std::function; + DataSourceStatusManager() = default; - /** - * Set the state. - * - * @param state The new state. - */ - void SetState(DataSourceStatus::DataSourceState state); - - /** - * If an error and state change happen simultaneously, then they should - * be updated simultaneously. - * - * @param state The new state. - * @param code Status code for an http error. - * @param message The message to associate with the error. - */ - void SetState(DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message); - - /** - * If an error and state change happen simultaneously, then they should - * be updated simultaneously. - * - * @param state The new state. - * @param kind The error kind. - * @param message The message to associate with the error. - */ - void SetState(DataSourceStatus::DataSourceState state, - DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message); - - /** - * Set an error with the given kind and message. - * - * For ErrorInfo::ErrorKind::kErrorResponse use the - * SetError(ErrorInfo::StatusCodeType) method. - * @param kind The kind of the error. - * @param message A message for the error. - */ - void SetError(DataSourceStatus::ErrorInfo::ErrorKind kind, - std::string message); - - /** - * Set an error based on the given status code. - * @param code The status code of the error. - */ - void SetError(DataSourceStatus::ErrorInfo::StatusCodeType code, - std::string message); - // TODO: Handle error codes once the EventSource supports it. sc-204392 - - DataSourceStatus Status() const override; - - std::unique_ptr OnDataSourceStatusChange( - std::function handler) - override; - - std::unique_ptr OnDataSourceStatusChangeEx( - std::function handler) - override; - - DataSourceStatusManager(); - - private: - DataSourceStatus::DataSourceState state_; - DataSourceStatus::DateTime state_since_; - std::optional last_error_; - - boost::signals2::signal - data_source_status_signal_; - mutable std::recursive_mutex status_mutex_; - bool UpdateState(DataSourceStatus::DataSourceState const& requested_state); + ~DataSourceStatusManager() override = default; + DataSourceStatusManager(DataSourceStatusManager const& item) = delete; + DataSourceStatusManager(DataSourceStatusManager&& item) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager const&) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; }; } // namespace launchdarkly::client_side::data_sources diff --git a/libs/internal/include/launchdarkly/data_sources/data_source.hpp b/libs/internal/include/launchdarkly/data_sources/data_source.hpp index 8c6d6d807..6ec7fe06f 100644 --- a/libs/internal/include/launchdarkly/data_sources/data_source.hpp +++ b/libs/internal/include/launchdarkly/data_sources/data_source.hpp @@ -16,4 +16,4 @@ class IDataSource { IDataSource() = default; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::data_sources diff --git a/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp b/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp new file mode 100644 index 000000000..fdf0f4a59 --- /dev/null +++ b/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp @@ -0,0 +1,188 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +namespace launchdarkly::internal::data_sources { + +/** + * Class that manages updates to the data source status and implements an + * interface to get the current status and listen to status changes. + */ +template +class DataSourceStatusManagerBase : public TInterface { + public: + /** + * Set the state. + * + * @param state The new state. + */ + void SetState(typename TDataSourceStatus::DataSourceState state) { + bool changed = UpdateState(state); + if (changed) { + data_source_status_signal_(Status()); + } + } + + /** + * If an error and state change happen simultaneously, then they should + * be updated simultaneously. + * + * @param state The new state. + * @param code Status code for an http error. + * @param message The message to associate with the error. + */ + void SetState(typename TDataSourceStatus::DataSourceState state, + typename TDataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message) { + { + std::lock_guard lock(status_mutex_); + + UpdateState(state); + + last_error_ = typename TDataSourceStatus::ErrorInfo( + TDataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, + message, std::chrono::system_clock::now()); + } + + data_source_status_signal_(Status()); + } + + /** + * If an error and state change happen simultaneously, then they should + * be updated simultaneously. + * + * @param state The new state. + * @param kind The error kind. + * @param message The message to associate with the error. + */ + void SetState(typename TDataSourceStatus::DataSourceState state, + typename TDataSourceStatus::ErrorInfo::ErrorKind kind, + std::string message) { + { + std::lock_guard lock(status_mutex_); + + UpdateState(state); + + last_error_ = typename TDataSourceStatus::ErrorInfo( + kind, 0, std::move(message), std::chrono::system_clock::now()); + } + + data_source_status_signal_(Status()); + } + + /** + * Set an error with the given kind and message. + * + * For ErrorInfo::ErrorKind::kErrorResponse use the + * SetError(ErrorInfo::StatusCodeType) method. + * @param kind The kind of the error. + * @param message A message for the error. + */ + void SetError(typename TDataSourceStatus::ErrorInfo::ErrorKind kind, + std::string message) { + { + std::lock_guard lock(status_mutex_); + last_error_ = typename TDataSourceStatus::ErrorInfo( + kind, 0, std::move(message), std::chrono::system_clock::now()); + state_since_ = std::chrono::system_clock::now(); + } + + data_source_status_signal_(Status()); + } + + /** + * Set an error based on the given status code. + * @param code The status code of the error. + */ + void SetError(typename TDataSourceStatus::ErrorInfo::StatusCodeType code, + std::string message) { + { + std::lock_guard lock(status_mutex_); + last_error_ = typename TDataSourceStatus::ErrorInfo( + TDataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, code, + message, std::chrono::system_clock::now()); + state_since_ = std::chrono::system_clock::now(); + } + data_source_status_signal_(Status()); + } + + TDataSourceStatus Status() const override { + return {state_, state_since_, last_error_}; + } + + std::unique_ptr OnDataSourceStatusChange( + std::function handler) override { + std::lock_guard lock{status_mutex_}; + return std::make_unique< + ::launchdarkly::internal::signals::SignalConnection>( + data_source_status_signal_.connect(handler)); + } + + std::unique_ptr OnDataSourceStatusChangeEx( + std::function handler) override { + return std::make_unique< + launchdarkly::internal::signals::SignalConnection>( + data_source_status_signal_.connect_extended( + [handler](boost::signals2::connection const& conn, + TDataSourceStatus status) { + if (handler(status)) { + conn.disconnect(); + } + })); + } + DataSourceStatusManagerBase() + : state_(TDataSourceStatus::DataSourceState::kInitializing), + state_since_(std::chrono::system_clock::now()) {} + + virtual ~DataSourceStatusManagerBase() = default; + DataSourceStatusManagerBase(DataSourceStatusManagerBase const& item) = + delete; + DataSourceStatusManagerBase(DataSourceStatusManagerBase&& item) = delete; + DataSourceStatusManagerBase& operator=(DataSourceStatusManagerBase const&) = + delete; + DataSourceStatusManagerBase& operator=(DataSourceStatusManagerBase&&) = + delete; + + private: + typename TDataSourceStatus::DataSourceState state_; + typename TDataSourceStatus::DateTime state_since_; + std::optional last_error_; + + boost::signals2::signal + data_source_status_signal_; + mutable std::recursive_mutex status_mutex_; + + bool UpdateState( + typename TDataSourceStatus::DataSourceState const& requested_state) { + std::lock_guard lock(status_mutex_); + + // Interrupted and initializing are common to server and client. + // If logic specific to client or server states was needed, then + // the implementation would need to be re-organized to allow overriding + // the method. + + // If initializing, then interruptions remain initializing. + auto new_state = + (requested_state == + TDataSourceStatus::DataSourceState::kInterrupted && + state_ == TDataSourceStatus::DataSourceState::kInitializing) + ? TDataSourceStatus::DataSourceState:: + kInitializing // see comment on + // IDataSourceUpdateSink.UpdateStatus + : requested_state; + auto changed = state_ != new_state; + if (changed) { + state_ = new_state; + state_since_ = std::chrono::system_clock::now(); + } + return changed; + } +}; + +} // namespace launchdarkly::internal::data_sources diff --git a/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp b/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp new file mode 100644 index 000000000..b0b540d55 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp @@ -0,0 +1,116 @@ +#pragma once + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side::data_sources { + +/** + * Enumeration of possible data source states. + */ +enum class ServerDataSourceState { + /** + * The initial state of the data source when the SDK is being + * initialized. + * + * If it encounters an error that requires it to retry initialization, + * the state will remain at kInitializing until it either succeeds and + * becomes kValid, or permanently fails and becomes kShutdown. + */ + kInitializing = 0, + + /** + * Indicates that the data source is currently operational and has not + * had any problems since the last time it received data. + * + * In streaming mode, this means that there is currently an open stream + * connection and that at least one initial message has been received on + * the stream. In polling mode, it means that the last poll request + * succeeded. + */ + kValid = 1, + + /** + * Indicates that the data source encountered an error that it will + * attempt to recover from. + * + * In streaming mode, this means that the stream connection failed, or + * had to be dropped due to some other error, and will be retried after + * a backoff delay. In polling mode, it means that the last poll request + * failed, and a new poll request will be made after the configured + * polling interval. + */ + kInterrupted = 2, + + /** + * Indicates that the data source has been permanently shut down. + * + * This could be because it encountered an unrecoverable error (for + * instance, the LaunchDarkly service rejected the SDK key; an invalid + * SDK key will never become valid), or because the SDK client was + * explicitly closed. + */ + kOff = 3, +}; + +using DataSourceStatus = + common::data_sources::DataSourceStatusBase; + +/** + * Interface for accessing and listening to the data source status. + */ +class IDataSourceStatusProvider { + public: + /** + * The current status of the data source. Suitable for broadcast to + * data source status listeners. + */ + [[nodiscard]] virtual DataSourceStatus Status() const = 0; + + /** + * Listen to changes to the data source status. + * + * @param handler Function which will be called with the new status. + * @return A IConnection which can be used to stop listening to the status. + */ + virtual std::unique_ptr OnDataSourceStatusChange( + std::function handler) = 0; + + /** + * Listen to changes to the data source status, with ability for listener + * to unregister itself. + * + * @param handler Function which will be called with the new status. Return + * true to unregister. + * @return A IConnection which can be used to stop listening to the status. + */ + virtual std::unique_ptr OnDataSourceStatusChangeEx( + std::function handler) = 0; + + virtual ~IDataSourceStatusProvider() = default; + IDataSourceStatusProvider(IDataSourceStatusProvider const& item) = delete; + IDataSourceStatusProvider(IDataSourceStatusProvider&& item) = delete; + IDataSourceStatusProvider& operator=(IDataSourceStatusProvider const&) = + delete; + IDataSourceStatusProvider& operator=(IDataSourceStatusProvider&&) = delete; + + protected: + IDataSourceStatusProvider() = default; +}; + +std::ostream& operator<<(std::ostream& out, + DataSourceStatus::DataSourceState const& state); + +std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index cb6184295..780ecb553 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -13,7 +13,16 @@ add_library(${LIBNAME} data_store/data_store_updater.cpp data_store/memory_store.cpp data_store/dependency_tracker.hpp - data_store/dependency_tracker.cpp data_store/descriptors.hpp) + data_store/dependency_tracker.cpp + data_store/descriptors.hpp + data_sources/data_source_event_handler.cpp + data_sources/data_source_event_handler.hpp + data_sources/data_source_status.cpp + data_sources/polling_data_source.hpp + data_sources/polling_data_source.cpp + data_sources/data_source_status_manager.hpp + data_sources/streaming_data_source.hpp + data_sources/streaming_data_source.cpp) if (MSVC OR (NOT BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} diff --git a/libs/server-sdk/src/data_sources/data_source_event_handler.cpp b/libs/server-sdk/src/data_sources/data_source_event_handler.cpp new file mode 100644 index 000000000..c3a3d7970 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.cpp @@ -0,0 +1,241 @@ +#include "data_source_event_handler.hpp" + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include + +#include "tl/expected.hpp" + +namespace launchdarkly::server_side::data_sources { + +static char const* const kErrorParsingPut = "Could not parse PUT message"; +static char const* const kErrorPutInvalid = + "PUT message contained invalid data"; +static char const* const kErrorParsingPatch = "Could not parse PATCH message"; +static char const* const kErrorPatchInvalid = + "PATCH message contained invalid data"; +static char const* const kErrorParsingDelete = "Could not parse DELETE message"; +static char const* const kErrorDeleteInvalid = + "DELETE message contained invalid data\""; + +template +tl::expected Patch( + std::string const& path, + boost::json::object const& obj) { + auto const* data_iter = obj.find("data"); + if (data_iter == obj.end()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + auto data = + boost::json::value_to, JsonError>>( + data_iter->value()); + if (!data.has_value()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + return DataSourceEventHandler::Patch{ + TStreamingDataKind::Key(path), + data_model::ItemDescriptor(data->value())}; +} + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + DataSourceEventHandler::Put put; + std::string path; + auto const& obj = json_value.as_object(); + PARSE_FIELD(path, obj, "path"); + // We don't know what to do with a path other than "/". + if (path != "/") { + return std::nullopt; + } + PARSE_FIELD(put.data, obj, "data"); + + return put; +} + +tl::expected, JsonError> +tag_invoke(boost::json::value_to_tag< + tl::expected, + JsonError>> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + auto const& obj = json_value.as_object(); + + std::string path; + PARSE_FIELD(path, obj, "path"); + + if (StreamingDataKinds::Flag::IsKind(path)) { + return Patch(path, obj); + } + + if (StreamingDataKinds::Segment::IsKind(path)) { + return Patch(path, + obj); + } + + return std::nullopt; +} + +static tl::expected tag_invoke( + boost::json::value_to_tag< + tl::expected> const& unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + if (!json_value.is_object()) { + return tl::unexpected(JsonError::kSchemaFailure); + } + + auto const& obj = json_value.as_object(); + + DataSourceEventHandler::Delete del; + PARSE_REQUIRED_FIELD(del.version, obj, "version"); + std::string path; + PARSE_REQUIRED_FIELD(path, obj, "path"); + + auto kind = StreamingDataKinds::Kind(path); + auto key = StreamingDataKinds::Key(path); + + if (kind.has_value() && key.has_value()) { + del.kind = *kind; + del.key = *key; + return del; + } + return tl::unexpected(JsonError::kSchemaFailure); +} + +DataSourceEventHandler::DataSourceEventHandler( + IDataSourceUpdateSink& handler, + Logger const& logger, + DataSourceStatusManager& status_manager) + : handler_(handler), logger_(logger), status_manager_(status_manager) {} + +DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( + std::string const& type, + std::string const& data) { + if (type == "put") { + boost::json::error_code error_code; + auto parsed = boost::json::parse(data, error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingPut; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingPut); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + auto res = + boost::json::value_to, JsonError>>( + parsed); + + if (!res.has_value()) { + LD_LOG(logger_, LogLevel::kError) << kErrorPutInvalid; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorPutInvalid); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + // Check the inner optional. + if (res->has_value()) { + handler_.Init(std::move((*res)->data)); + status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + if (type == "patch") { + boost::json::error_code error_code; + auto parsed = boost::json::parse(data, error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingPut; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingPatch); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + auto res = boost::json::value_to< + tl::expected, JsonError>>(parsed); + + if (!res.has_value()) { + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorPatchInvalid); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + // This references the optional inside the expected. + if (res->has_value()) { + auto const& patch = (**res); + auto const& key = patch.key; + std::visit([this, &key](auto&& arg) { handler_.Upsert(key, arg); }, + patch.data); + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + // We didn't recognize the type of the patch. So we ignore it. + return DataSourceEventHandler::MessageStatus::kMessageHandled; + } + if (type == "delete") { + boost::json::error_code error_code; + auto parsed = boost::json::parse(data, error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingDelete; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingDelete); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + auto res = + boost::json::value_to>(parsed); + + if (res.has_value()) { + switch (res->kind) { + case data_store::DataKind::kFlag: { + handler_.Upsert(res->key, + data_store::FlagDescriptor(res->version)); + return DataSourceEventHandler::MessageStatus:: + kMessageHandled; + } + case data_store::DataKind::kSegment: { + handler_.Upsert( + res->key, data_store::SegmentDescriptor(res->version)); + return DataSourceEventHandler::MessageStatus:: + kMessageHandled; + } + default: { + } break; + } + } + + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorDeleteInvalid); + return DataSourceEventHandler::MessageStatus::kInvalidMessage; + } + + return DataSourceEventHandler::MessageStatus::kUnhandledVerb; +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp new file mode 100644 index 000000000..798796506 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include + +#include + +#include "../data_store/data_kind.hpp" +#include "data_source_status_manager.hpp" +#include "data_source_update_sink.hpp" + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +// The FlagsPath and SegmentsPath are made to turn a string literal into a type +// for use in a template. +// You can use a char array as a const char* template +// parameter, but this causes a number of issues with the clang linter. + +struct FlagsPath { + static constexpr std::string_view path = "/flags/"; +}; + +struct SegmentsPath { + static constexpr std::string_view path = "/segments/"; +}; + +template +class StreamingDataKind { + public: + static data_store::DataKind Kind() { return kind; } + static bool IsKind(std::string const& patch_path) { + return patch_path.rfind(TPath::path) == 0; + } + static std::string Key(std::string const& patch_path) { + return patch_path.substr(TPath::path.size()); + } +}; + +struct StreamingDataKinds { + using Flag = StreamingDataKind; + using Segment = + StreamingDataKind; + + static std::optional Kind(std::string const& path) { + if (Flag::IsKind(path)) { + return data_store::DataKind::kFlag; + } + if (Segment::IsKind(path)) { + return data_store::DataKind::kSegment; + } + return std::nullopt; + } + + static std::optional Key(std::string const& path) { + if (Flag::IsKind(path)) { + return Flag::Key(path); + } + if (Segment::IsKind(path)) { + return Segment::Key(path); + } + return std::nullopt; + } +}; + +/** + * This class handles LaunchDarkly events, parses them, and then uses + * a IDataSourceUpdateSink to process the parsed events. + * + * This is only used for streaming. For server polling the shape of the poll + * response is different than the put, so there is limited utility in + * sharing this handler. + */ +class DataSourceEventHandler { + public: + /** + * Status indicating if the message was processed, or if there + * was an issue encountered. + */ + enum class MessageStatus { + kMessageHandled, + kInvalidMessage, + kUnhandledVerb + }; + + struct Put { + data_model::SDKDataSet data; + }; + + struct Patch { + std::string key; + std::variant + data; + }; + + struct Delete { + std::string key; + data_store::DataKind kind; + uint64_t version; + }; + + DataSourceEventHandler(IDataSourceUpdateSink& handler, + Logger const& logger, + DataSourceStatusManager& status_manager); + + /** + * Handles an event from the LaunchDarkly service. + * @param type The type of the event. "put"/"patch"/"delete". + * @param data The content of the event. + * @return A status indicating if the message could be handled. + */ + MessageStatus HandleMessage(std::string const& type, + std::string const& data); + + private: + IDataSourceUpdateSink& handler_; + Logger const& logger_; + DataSourceStatusManager& status_manager_; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_status.cpp b/libs/server-sdk/src/data_sources/data_source_status.cpp new file mode 100644 index 000000000..6881ed4ba --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_status.cpp @@ -0,0 +1,40 @@ +#include + +#include + +namespace launchdarkly::server_side::data_sources { + +std::ostream& operator<<(std::ostream& out, + DataSourceStatus::DataSourceState const& state) { + switch (state) { + case DataSourceStatus::DataSourceState::kInitializing: + out << "INITIALIZING"; + break; + case DataSourceStatus::DataSourceState::kValid: + out << "VALID"; + break; + case DataSourceStatus::DataSourceState::kInterrupted: + out << "INTERRUPTED"; + break; + case DataSourceStatus::DataSourceState::kOff: + out << "OFF"; + break; + } + + return out; +} + +std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { + std::time_t as_time_t = + std::chrono::system_clock::to_time_t(status.StateSince()); + out << "Status(" << status.State() << ", Since(" + << std::put_time(std::gmtime(&as_time_t), "%Y-%m-%d %H:%M:%S") << ")"; + auto const& last_error = status.LastError(); + if (last_error.has_value()) { + out << ", " << last_error.value(); + } + out << ")"; + return out; +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_status_manager.hpp b/libs/server-sdk/src/data_sources/data_source_status_manager.hpp new file mode 100644 index 000000000..d19040ed8 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_status_manager.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class DataSourceStatusManager + : public internal::data_sources::DataSourceStatusManagerBase< + DataSourceStatus, + IDataSourceStatusProvider> { + public: + DataSourceStatusManager() = default; + + ~DataSourceStatusManager() override = default; + DataSourceStatusManager(DataSourceStatusManager const& item) = delete; + DataSourceStatusManager(DataSourceStatusManager&& item) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager const&) = delete; + DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_update_sink.hpp b/libs/server-sdk/src/data_sources/data_source_update_sink.hpp index 61d3d6d01..294f82ee7 100644 --- a/libs/server-sdk/src/data_sources/data_source_update_sink.hpp +++ b/libs/server-sdk/src/data_sources/data_source_update_sink.hpp @@ -7,7 +7,7 @@ #include "../data_store/descriptors.hpp" -namespace launchdarkly::server_side::data_source { +namespace launchdarkly::server_side::data_sources { /** * Interface for handling updates from LaunchDarkly. */ @@ -28,4 +28,4 @@ class IDataSourceUpdateSink { protected: IDataSourceUpdateSink() = default; }; -} // namespace launchdarkly::server_side::data_source +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/polling_data_source.cpp b/libs/server-sdk/src/data_sources/polling_data_source.cpp new file mode 100644 index 000000000..2f250de01 --- /dev/null +++ b/libs/server-sdk/src/data_sources/polling_data_source.cpp @@ -0,0 +1,245 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "data_source_update_sink.hpp" +#include "polling_data_source.hpp" + +namespace launchdarkly::server_side::data_sources { + +static char const* const kErrorParsingPut = "Could not parse polling payload"; +static char const* const kErrorPutInvalid = + "Polling payload contained invalid data"; + +static char const* const kCouldNotParseEndpoint = + "Could not parse polling endpoint URL"; + +static network::HttpRequest MakeRequest( + config::shared::built::DataSourceConfig const& + data_source_config, + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::HttpProperties const& http_properties) { + auto url = std::make_optional(endpoints.PollingBaseUrl()); + + auto const& polling_config = std::get< + config::shared::built::PollingConfig>( + data_source_config.method); + + url = network::AppendUrl(url, polling_config.polling_get_path); + + network::HttpRequest::BodyType body; + network::HttpMethod method = network::HttpMethod::kGet; + + config::shared::builders::HttpPropertiesBuilder + builder(http_properties); + + // If no URL is set, then we will fail the request. + return {url.value_or(""), method, builder.Build(), body}; +} + +PollingDataSource::PollingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig const& + data_source_config, + config::shared::built::HttpProperties const& http_properties, + boost::asio::any_io_executor const& ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger) + : ioc_(ioc), + logger_(logger), + status_manager_(status_manager), + update_sink_(handler), + requester_(ioc), + timer_(ioc), + polling_interval_( + std::get< + config::shared::built::PollingConfig>( + data_source_config.method) + .poll_interval), + request_(MakeRequest(data_source_config, endpoints, http_properties)) { + auto const& polling_config = std::get< + config::shared::built::PollingConfig>( + data_source_config.method); + if (polling_interval_ < polling_config.min_polling_interval) { + LD_LOG(logger_, LogLevel::kWarn) + << "Polling interval too frequent, defaulting to " + << std::chrono::duration_cast( + polling_config.min_polling_interval) + .count() + << " seconds"; + + polling_interval_ = polling_config.min_polling_interval; + } +} + +void PollingDataSource::DoPoll() { + last_poll_start_ = std::chrono::system_clock::now(); + + auto weak_self = weak_from_this(); + requester_.Request(request_, [weak_self](network::HttpResult const& res) { + if (auto self = weak_self.lock()) { + self->HandlePollResult(res); + } + }); +} + +void PollingDataSource::HandlePollResult(network::HttpResult const& res) { + auto header_etag = res.Headers().find("etag"); + bool has_etag = header_etag != res.Headers().end(); + + if (etag_ && has_etag) { + if (etag_.value() == header_etag->second) { + // Got the same etag; we know the content has not changed. + // So we can just start the next timer. + + // We don't need to update the "request_" because it would have + // the same Etag. + StartPollingTimer(); + return; + } + } + + if (has_etag) { + config::shared::builders::HttpPropertiesBuilder< + config::shared::ServerSDK> + builder(request_.Properties()); + builder.Header("If-None-Match", header_etag->second); + request_ = network::HttpRequest(request_, builder.Build()); + + etag_ = header_etag->second; + } + + if (res.IsError()) { + auto const& error_message = res.ErrorMessage(); + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + error_message.has_value() ? *error_message : "unknown error"); + LD_LOG(logger_, LogLevel::kWarn) + << "Polling for feature flag updates failed: " + << (error_message.has_value() ? *error_message : "unknown error"); + } else if (res.Status() == 200) { + auto const& body = res.Body(); + if (body.has_value()) { + boost::json::error_code error_code; + auto parsed = boost::json::parse(body.value(), error_code); + if (error_code) { + LD_LOG(logger_, LogLevel::kError) << kErrorParsingPut; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorParsingPut); + return; + } + auto poll_result = boost::json::value_to< + tl::expected>(parsed); + + if (poll_result.has_value()) { + update_sink_.Init(std::move(*poll_result)); + status_manager_.SetState( + DataSourceStatus::DataSourceState::kValid); + return; + } + LD_LOG(logger_, LogLevel::kError) << kErrorPutInvalid; + status_manager_.SetError( + DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, + kErrorPutInvalid); + return; + } + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, + DataSourceStatus::ErrorInfo::ErrorKind::kUnknown, + "polling response contained no body."); + + } else if (res.Status() == 304) { + // This should be handled ahead of here, but if we get a 304, + // and it didn't have an etag, we still don't want to try to + // parse the body. + } else { + if (network::IsRecoverableStatus(res.Status())) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInterrupted, res.Status(), + launchdarkly::network::ErrorForStatusCode( + res.Status(), "polling request", "will retry")); + } else { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, res.Status(), + launchdarkly::network::ErrorForStatusCode( + res.Status(), "polling request", std::nullopt)); + // We are giving up. Do not start a new polling request. + return; + } + } + + StartPollingTimer(); +} + +void PollingDataSource::StartPollingTimer() { + auto time_since_poll_seconds = + std::chrono::duration_cast( + std::chrono::system_clock::now() - last_poll_start_); + + // Calculate a delay based on the polling interval and the duration elapsed + // since the last poll. + + // Example: If the poll took 5 seconds, and the interval is 30 seconds, then + // we want to poll after 25 seconds. We do not want the interval to be + // negative, so we clamp it to 0. + auto delay = std::chrono::seconds(std::max( + polling_interval_ - time_since_poll_seconds, std::chrono::seconds(0))); + + timer_.cancel(); + timer_.expires_after(delay); + + auto weak_self = weak_from_this(); + + timer_.async_wait([weak_self](boost::system::error_code const& ec) { + if (ec == boost::asio::error::operation_aborted) { + // The timer was canceled. Stop polling. + return; + } + if (auto self = weak_self.lock()) { + if (ec) { + // Something unexpected happened. Log it and continue to try + // polling. + LD_LOG(self->logger_, LogLevel::kError) + << "Unexpected error in polling timer: " << ec.message(); + } + self->DoPoll(); + } + }); +} + +void PollingDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + if (!request_.Valid()) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + + // No need to attempt to poll if the URL is not valid. + return; + } + + DoPoll(); +} + +void PollingDataSource::ShutdownAsync(std::function completion) { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + timer_.cancel(); + if (completion) { + boost::asio::post(timer_.get_executor(), completion); + } +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/polling_data_source.hpp b/libs/server-sdk/src/data_sources/polling_data_source.hpp new file mode 100644 index 000000000..1d99417b3 --- /dev/null +++ b/libs/server-sdk/src/data_sources/polling_data_source.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include + +#include "data_source_event_handler.hpp" +#include "data_source_status_manager.hpp" +#include "data_source_update_sink.hpp" + +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class PollingDataSource + : public ::launchdarkly::data_sources::IDataSource, + public std::enable_shared_from_this { + public: + PollingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig< + config::shared::ServerSDK> const& data_source_config, + config::shared::built::HttpProperties const& http_properties, + boost::asio::any_io_executor const& ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger); + + void Start() override; + void ShutdownAsync(std::function completion) override; + + private: + void DoPoll(); + void HandlePollResult(network::HttpResult const& res); + + DataSourceStatusManager& status_manager_; + std::string polling_endpoint_; + + network::AsioRequester requester_; + Logger const& logger_; + boost::asio::any_io_executor ioc_; + std::chrono::seconds polling_interval_; + network::HttpRequest request_; + std::optional etag_; + + boost::asio::steady_timer timer_; + std::chrono::time_point last_poll_start_; + IDataSourceUpdateSink& update_sink_; + + void StartPollingTimer(); +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.cpp b/libs/server-sdk/src/data_sources/streaming_data_source.cpp new file mode 100644 index 000000000..b2f9e8a7d --- /dev/null +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include + +#include + +#include "streaming_data_source.hpp" + +#include + +namespace launchdarkly::server_side::data_sources { + +static char const* const kCouldNotParseEndpoint = + "Could not parse streaming endpoint URL"; + +static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { + switch (error) { + case sse::Error::NoContent: + return "server responded 204 (No Content), will not attempt to " + "reconnect"; + case sse::Error::InvalidRedirectLocation: + return "server responded with an invalid redirection"; + case sse::Error::UnrecoverableClientError: + return "unrecoverable client-side error"; + } +} + +StreamingDataSource::StreamingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig const& + data_source_config, + config::shared::built::HttpProperties http_properties, + boost::asio::any_io_executor ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger) + : exec_(std::move(ioc)), + logger_(logger), + status_manager_(status_manager), + data_source_handler_( + DataSourceEventHandler(handler, logger, status_manager_)), + http_config_(std::move(http_properties)), + streaming_config_( + std::get>(data_source_config.method)), + streaming_endpoint_(endpoints.StreamingBaseUrl()) {} + +void StreamingDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + + auto updated_url = network::AppendUrl(streaming_endpoint_, + streaming_config_.streaming_path); + + // Bad URL, don't set the client. Start will then report the bad status. + if (!updated_url) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } + + auto uri_components = boost::urls::parse_uri(*updated_url); + + // Unlikely that it could be parsed earlier, and it cannot be parsed now. + if (!uri_components) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } + + // TODO: Initial reconnect delay. sc-204393 + boost::urls::url url = uri_components.value(); + + auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); + + client_builder.method(boost::beast::http::verb::get); + + // TODO: can the read timeout be shared with *all* http requests? Or should + // it have a default in defaults.hpp? This must be greater than the + // heartbeat interval of the streaming service. + client_builder.read_timeout(std::chrono::minutes(5)); + + client_builder.write_timeout(http_config_.WriteTimeout()); + + client_builder.connect_timeout(http_config_.ConnectTimeout()); + + for (auto const& header : http_config_.BaseHeaders()) { + client_builder.header(header.first, header.second); + } + + // TODO: Handle proxy support. sc-204386 + + auto weak_self = weak_from_this(); + + client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { + if (auto self = weak_self.lock()) { + self->data_source_handler_.HandleMessage(event.type(), + event.data()); + // TODO: Use the result of handle message to restart the + // event source if we got bad data. sc-204387 + } + }); + + client_builder.logger([weak_self](auto msg) { + if (auto self = weak_self.lock()) { + LD_LOG(self->logger_, LogLevel::kDebug) << msg; + } + }); + + client_builder.errors([weak_self](auto error) { + if (auto self = weak_self.lock()) { + auto error_string = DataSourceErrorToString(error); + LD_LOG(self->logger_, LogLevel::kError) << error_string; + self->status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, + error_string); + } + }); + + client_ = client_builder.build(); + + if (!client_) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } + client_->run(); +} + +void StreamingDataSource::ShutdownAsync(std::function completion) { + if (client_) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInitializing); + return client_->async_shutdown(std::move(completion)); + } + if (completion) { + boost::asio::post(exec_, completion); + } +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.hpp b/libs/server-sdk/src/data_sources/streaming_data_source.hpp new file mode 100644 index 000000000..9663501f0 --- /dev/null +++ b/libs/server-sdk/src/data_sources/streaming_data_source.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +using namespace std::chrono_literals; + +#include + +#include "data_source_event_handler.hpp" +#include "data_source_status_manager.hpp" +#include "data_source_update_sink.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class StreamingDataSource final + : public ::launchdarkly::data_sources::IDataSource, + public std::enable_shared_from_this { + public: + StreamingDataSource( + config::shared::built::ServiceEndpoints const& endpoints, + config::shared::built::DataSourceConfig< + config::shared::ServerSDK> const& data_source_config, + config::shared::built::HttpProperties http_properties, + boost::asio::any_io_executor ioc, + IDataSourceUpdateSink& handler, + DataSourceStatusManager& status_manager, + Logger const& logger); + + void Start() override; + void ShutdownAsync(std::function completion) override; + + private: + boost::asio::any_io_executor exec_; + DataSourceStatusManager& status_manager_; + DataSourceEventHandler data_source_handler_; + std::string streaming_endpoint_; + + config::shared::built::StreamingConfig + streaming_config_; + + config::shared::built::HttpProperties http_config_; + + Logger const& logger_; + std::shared_ptr client_; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_store/data_store_updater.hpp b/libs/server-sdk/src/data_store/data_store_updater.hpp index e3e3ea869..c9bda40ce 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.hpp +++ b/libs/server-sdk/src/data_store/data_store_updater.hpp @@ -13,7 +13,7 @@ namespace launchdarkly::server_side::data_store { class DataStoreUpdater - : public launchdarkly::server_side::data_source::IDataSourceUpdateSink, + : public launchdarkly::server_side::data_sources::IDataSourceUpdateSink, public launchdarkly::server_side::IChangeNotifier { public: template diff --git a/libs/server-sdk/src/data_store/memory_store.hpp b/libs/server-sdk/src/data_store/memory_store.hpp index df5160b74..f8758e4a8 100644 --- a/libs/server-sdk/src/data_store/memory_store.hpp +++ b/libs/server-sdk/src/data_store/memory_store.hpp @@ -11,7 +11,7 @@ namespace launchdarkly::server_side::data_store { class MemoryStore : public IDataStore, - public data_source::IDataSourceUpdateSink { + public data_sources::IDataSourceUpdateSink { public: std::shared_ptr GetFlag( std::string const& key) const override; @@ -30,10 +30,16 @@ class MemoryStore : public IDataStore, void Upsert(std::string const& key, FlagDescriptor flag) override; void Upsert(std::string const& key, SegmentDescriptor segment) override; + MemoryStore() = default; ~MemoryStore() override = default; + MemoryStore(MemoryStore const& item) = delete; + MemoryStore(MemoryStore&& item) = delete; + MemoryStore& operator=(MemoryStore const&) = delete; + MemoryStore& operator=(MemoryStore&&) = delete; + private: - static inline std::string description_ = "memory"; + static inline const std::string description_ = "memory"; std::unordered_map> flags_; std::unordered_map> segments_; diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index d22b1d918..f8918cd3f 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -14,6 +14,6 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") add_executable(gtest_${LIBNAME} ${tests}) -target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal GTest::gtest_main) +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse GTest::gtest_main) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/data_source_event_handler_test.cpp b/libs/server-sdk/tests/data_source_event_handler_test.cpp new file mode 100644 index 000000000..4092c78a2 --- /dev/null +++ b/libs/server-sdk/tests/data_source_event_handler_test.cpp @@ -0,0 +1,202 @@ +#include + +#include +#include "data_sources/data_source_event_handler.hpp" +#include "data_store/memory_store.hpp" + +using namespace launchdarkly; +using namespace launchdarkly::server_side; +using namespace launchdarkly::server_side::data_sources; +using namespace launchdarkly::server_side::data_store; + +TEST(DataSourceEventHandlerTests, HandlesEmptyPutMessage) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", R"({"path":"/", "data":{}})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + ASSERT_TRUE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kValid, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesInvalidPut) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", "{sorry"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); + ASSERT_FALSE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesInvalidPatch) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", "{sorry"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); + ASSERT_FALSE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesPatchForUnknownPath) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage( + "patch", R"({"path":"potato", "data": "SPUD"})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesPutForUnknownPath) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage( + "put", R"({"path":"potato", "data": "SPUD"})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesInvalidDelete) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + auto res = event_handler.HandleMessage("put", "{sorry"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kInvalidMessage, res); + ASSERT_FALSE(store->Initialized()); + EXPECT_EQ(0, store->AllFlags().size()); + EXPECT_EQ(0, store->AllSegments().size()); + EXPECT_EQ(DataSourceStatus::DataSourceState::kInitializing, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesPayloadWithFlagAndSegment) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + auto payload = + R"({"path":"/","data":{"segments":{"special":{"key":"special","included":["bob"], + "version":2}},"flags":{"HasBob":{"key":"HasBob","on":true,"fallthrough": + {"variation":1},"variations":[true,false],"version":4}}}})"; + auto res = event_handler.HandleMessage("put", payload); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, res); + ASSERT_TRUE(store->Initialized()); + EXPECT_EQ(1, store->AllFlags().size()); + EXPECT_EQ(1, store->AllSegments().size()); + EXPECT_TRUE(store->GetFlag("HasBob")); + EXPECT_TRUE(store->GetSegment("special")); + EXPECT_EQ(DataSourceStatus::DataSourceState::kValid, + manager.Status().State()); +} + +TEST(DataSourceEventHandlerTests, HandlesValidFlagPatch) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage("put", "{}"); + + auto patch_res = event_handler.HandleMessage( + "patch", + R"({"path": "/flags/flagA", "data":{"key": "flagA", "version":2}})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + EXPECT_EQ(1, store->AllFlags().size()); +} + +TEST(DataSourceEventHandlerTests, HandlesValidSegmentPatch) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage("put", "{}"); + + auto patch_res = event_handler.HandleMessage( + "patch", + R"({"path": "/segments/segmentA", "data":{"key": "segmentA", "version":2}})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + EXPECT_EQ(1, store->AllSegments().size()); +} + +TEST(DataSourceEventHandlerTests, HandlesDeleteFlag) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage( + "put", R"({"path":"/","data":{"segments":{})" + R"(, "flags":{"flagA": {"key":"flagA", "version": 0}}}})"); + + ASSERT_TRUE(store->GetFlag("flagA")->item); + + auto patch_res = event_handler.HandleMessage( + "delete", R"({"path": "/flags/flagA", "version": 1})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + ASSERT_FALSE(store->GetFlag("flagA")->item); +} + +TEST(DataSourceEventHandlerTests, HandlesDeleteSegment) { + auto logger = launchdarkly::logging::NullLogger(); + auto store = std::make_shared(); + DataSourceStatusManager manager; + DataSourceEventHandler event_handler(*store, logger, manager); + + event_handler.HandleMessage( + "put", + R"({"path":"/","data":{"flags":{})" + R"(, "segments":{"segmentA": {"key":"segmentA", "version": 0}}}})"); + + ASSERT_TRUE(store->GetSegment("segmentA")->item); + + auto patch_res = event_handler.HandleMessage( + "delete", R"({"path": "/segments/segmentA", "version": 1})"); + + ASSERT_EQ(DataSourceEventHandler::MessageStatus::kMessageHandled, + patch_res); + + ASSERT_FALSE(store->GetSegment("segmentA")->item); +} From 3692736e599dd7db4ae76f3ffaf84cbfd845ef76 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 17 Jul 2023 09:54:41 -0700 Subject: [PATCH 13/56] chore: Update data store updater use reference. (#181) --- .../src/data_store/data_store_updater.cpp | 16 +++---- .../src/data_store/data_store_updater.hpp | 9 ++-- .../tests/data_store_updater_test.cpp | 46 +++++++++---------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/libs/server-sdk/src/data_store/data_store_updater.cpp b/libs/server-sdk/src/data_store/data_store_updater.cpp index e018ba6d5..973a7d585 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.cpp +++ b/libs/server-sdk/src/data_store/data_store_updater.cpp @@ -21,9 +21,9 @@ void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { if (HasListeners()) { DependencySet updated_items; - CalculateChanges(DataKind::kFlag, store_->AllFlags(), data_set.flags, + CalculateChanges(DataKind::kFlag, store_.AllFlags(), data_set.flags, updated_items); - CalculateChanges(DataKind::kSegment, store_->AllSegments(), + CalculateChanges(DataKind::kSegment, store_.AllSegments(), data_set.segments, updated_items); change_notifications = updated_items; } @@ -37,7 +37,7 @@ void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { } // Data will move into the store, so we want to update dependencies before // it is moved. - sink_->Init(std::move(data_set)); + sink_.Init(std::move(data_set)); // After updating the sink, let listeners know of changes. if (change_notifications) { NotifyChanges(std::move(*change_notifications)); @@ -46,12 +46,12 @@ void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { void DataStoreUpdater::Upsert(std::string const& key, data_store::FlagDescriptor flag) { - UpsertCommon(DataKind::kFlag, key, store_->GetFlag(key), std::move(flag)); + UpsertCommon(DataKind::kFlag, key, store_.GetFlag(key), std::move(flag)); } void DataStoreUpdater::Upsert(std::string const& key, data_store::SegmentDescriptor segment) { - UpsertCommon(DataKind::kSegment, key, store_->GetSegment(key), + UpsertCommon(DataKind::kSegment, key, store_.GetSegment(key), std::move(segment)); } @@ -69,8 +69,8 @@ void DataStoreUpdater::NotifyChanges(DependencySet changes) { } } -DataStoreUpdater::DataStoreUpdater(std::shared_ptr sink, - std::shared_ptr store) - : sink_(std::move(sink)), store_(std::move(store)) {} +DataStoreUpdater::DataStoreUpdater(IDataSourceUpdateSink& sink, + IDataStore const& store) + : sink_(sink), store_(store) {} } // namespace launchdarkly::server_side::data_store diff --git a/libs/server-sdk/src/data_store/data_store_updater.hpp b/libs/server-sdk/src/data_store/data_store_updater.hpp index c9bda40ce..0b5a2e153 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.hpp +++ b/libs/server-sdk/src/data_store/data_store_updater.hpp @@ -26,8 +26,7 @@ class DataStoreUpdater using SharedCollection = std::unordered_map>; - DataStoreUpdater(std::shared_ptr sink, - std::shared_ptr store); + DataStoreUpdater(IDataSourceUpdateSink& sink, IDataStore const& store); std::unique_ptr OnFlagChange(ChangeHandler handler) override; @@ -63,7 +62,7 @@ class DataStoreUpdater NotifyChanges(updated_deps); } - sink_->Upsert(key, updated); + sink_.Upsert(key, updated); } template @@ -102,8 +101,8 @@ class DataStoreUpdater void NotifyChanges(DependencySet changes); - std::shared_ptr sink_; - std::shared_ptr store_; + IDataSourceUpdateSink& sink_; + IDataStore const& store_; boost::signals2::signal)> signals_; diff --git a/libs/server-sdk/tests/data_store_updater_test.cpp b/libs/server-sdk/tests/data_store_updater_test.cpp index 5125fbea6..87fc139b9 100644 --- a/libs/server-sdk/tests/data_store_updater_test.cpp +++ b/libs/server-sdk/tests/data_store_updater_test.cpp @@ -16,20 +16,20 @@ using launchdarkly::data_model::Flag; using launchdarkly::data_model::Segment; TEST(DataStoreUpdaterTest, DoesNotInitializeStoreUntilInit) { - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); - EXPECT_FALSE(store->Initialized()); + EXPECT_FALSE(store.Initialized()); } TEST(DataStoreUpdaterTest, InitializesStore) { - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet()); - EXPECT_TRUE(store->Initialized()); + EXPECT_TRUE(store.Initialized()); } TEST(DataStoreUpdaterTest, InitPropagatesData) { - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); Flag flag; flag.version = 1; @@ -50,14 +50,14 @@ TEST(DataStoreUpdaterTest, InitPropagatesData) { {"segmentA", SegmentDescriptor(segment)}}, }); - auto fetched_flag = store->GetFlag("flagA"); + auto fetched_flag = store.GetFlag("flagA"); EXPECT_TRUE(fetched_flag); EXPECT_TRUE(fetched_flag->item); EXPECT_EQ("flagA", fetched_flag->item->key); EXPECT_EQ(1, fetched_flag->item->version); EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); - auto fetched_segment = store->GetSegment("segmentA"); + auto fetched_segment = store.GetSegment("segmentA"); EXPECT_TRUE(fetched_segment); EXPECT_TRUE(fetched_segment->item); EXPECT_EQ("segmentA", fetched_segment->item->key); @@ -66,7 +66,7 @@ TEST(DataStoreUpdaterTest, InitPropagatesData) { } TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); Flag flag_a_v1; flag_a_v1.version = 1; @@ -145,7 +145,7 @@ TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { } TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); Flag flag_a; @@ -158,7 +158,7 @@ TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { }); updater.Upsert("flagA", FlagDescriptor(flag_a)); - auto fetched_flag = store->GetFlag("flagA"); + auto fetched_flag = store.GetFlag("flagA"); EXPECT_TRUE(fetched_flag); EXPECT_TRUE(fetched_flag->item); EXPECT_EQ("flagA", fetched_flag->item->key); @@ -171,7 +171,7 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { flag_a.version = 1; flag_a.key = "flagA"; - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet{ @@ -186,7 +186,7 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { updater.Upsert("flagA", FlagDescriptor(flag_a_2)); - auto fetched_flag = store->GetFlag("flagA"); + auto fetched_flag = store.GetFlag("flagA"); EXPECT_TRUE(fetched_flag); EXPECT_TRUE(fetched_flag->item); EXPECT_EQ("flagA", fetched_flag->item->key); @@ -200,7 +200,7 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { flag_a.key = "flagA"; flag_a.variations = std::vector{"potato", "ham"}; - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet{ @@ -216,7 +216,7 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { updater.Upsert("flagA", FlagDescriptor(flag_a_2)); - auto fetched_flag = store->GetFlag("flagA"); + auto fetched_flag = store.GetFlag("flagA"); EXPECT_TRUE(fetched_flag); EXPECT_TRUE(fetched_flag->item); EXPECT_EQ("flagA", fetched_flag->item->key); @@ -233,7 +233,7 @@ TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { segment_a.version = 1; segment_a.key = "segmentA"; - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet{ @@ -242,7 +242,7 @@ TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { }); updater.Upsert("segmentA", SegmentDescriptor(segment_a)); - auto fetched_segment = store->GetSegment("segmentA"); + auto fetched_segment = store.GetSegment("segmentA"); EXPECT_TRUE(fetched_segment); EXPECT_TRUE(fetched_segment->item); EXPECT_EQ("segmentA", fetched_segment->item->key); @@ -255,7 +255,7 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { segment_a.version = 1; segment_a.key = "segmentA"; - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet{ @@ -270,7 +270,7 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); - auto fetched_segment = store->GetSegment("segmentA"); + auto fetched_segment = store.GetSegment("segmentA"); EXPECT_TRUE(fetched_segment); EXPECT_TRUE(fetched_segment->item); EXPECT_EQ("segmentA", fetched_segment->item->key); @@ -283,7 +283,7 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { segment_a.version = 2; segment_a.key = "segmentA"; - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet{ @@ -298,7 +298,7 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { updater.Upsert("segmentA", SegmentDescriptor(segment_a_2)); - auto fetched_segment = store->GetSegment("segmentA"); + auto fetched_segment = store.GetSegment("segmentA"); EXPECT_TRUE(fetched_segment); EXPECT_TRUE(fetched_segment->item); EXPECT_EQ("segmentA", fetched_segment->item->key); @@ -318,7 +318,7 @@ TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet{ @@ -361,7 +361,7 @@ TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); Segment segment_a; @@ -404,7 +404,7 @@ TEST(DataStoreUpdaterTest, NoEventOnDiscardedUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); - auto store = std::make_shared(); + MemoryStore store; DataStoreUpdater updater(store, store); updater.Init(SDKDataSet{ From 99dea8696f610527efae7f0baed5e51a3bc8c328 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 18 Jul 2023 16:15:42 -0700 Subject: [PATCH 14/56] feat: add ContextKind type to data model (#184) Previously, context kind was an `std::string`. The requirement that it not be an empty string was enforced by parent data model deserializers. Now, we have a dedicated type-safe wrapper via`BOOST_STRONG_TYPEDEF`. The custom `tag_invoke` enforces the invariant so it's not possible to deserialize it inconsistently. --- .../data_model/context_aware_reference.hpp | 8 +++-- .../launchdarkly/data_model/context_kind.hpp | 11 +++++++ .../include/launchdarkly/data_model/flag.hpp | 3 +- .../json_context_aware_reference.hpp | 11 +++---- .../serialization/json_context_kind.hpp | 19 ++++++++++++ libs/internal/src/CMakeLists.txt | 1 + .../src/serialization/json_context_kind.cpp | 24 ++++++++++++++ libs/internal/src/serialization/json_flag.cpp | 4 ++- .../tests/data_model_serialization_test.cpp | 31 ++++++++++++++----- .../tests/dependency_tracker_test.cpp | 9 +++--- 10 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 libs/internal/include/launchdarkly/data_model/context_kind.hpp create mode 100644 libs/internal/include/launchdarkly/serialization/json_context_kind.hpp create mode 100644 libs/internal/src/serialization/json_context_kind.cpp diff --git a/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp index 836dc20b4..f176cf403 100644 --- a/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp +++ b/libs/internal/include/launchdarkly/data_model/context_aware_reference.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include + #include namespace launchdarkly::data_model { @@ -46,16 +48,18 @@ struct ContextAwareReference< std::is_same::value>::type> { using fields = FieldNames; - std::string contextKind; + ContextKind contextKind; AttributeReference reference; }; +// NOLINTBEGIN cppcoreguidelines-macro-usage #define DEFINE_CONTEXT_KIND_FIELD(name) \ - std::string name; \ + ContextKind name; \ constexpr static const char* kContextFieldName = #name; #define DEFINE_ATTRIBUTE_REFERENCE_FIELD(name) \ AttributeReference name; \ constexpr static const char* kReferenceFieldName = #name; +// NOLINTEND cppcoreguidelines-macro-usage } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/context_kind.hpp b/libs/internal/include/launchdarkly/data_model/context_kind.hpp new file mode 100644 index 000000000..efa5b342e --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/context_kind.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include + +namespace launchdarkly::data_model { + +BOOST_STRONG_TYPEDEF(std::string, ContextKind); + +} // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index 282dd4bad..65d56d12a 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -15,8 +16,6 @@ namespace launchdarkly::data_model { struct Flag { - using ContextKind = std::string; - struct Rollout { enum class Kind { kUnrecognized = 0, diff --git a/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp index 77b56af90..c9eac9038 100644 --- a/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_context_aware_reference.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -25,15 +26,10 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - std::optional kind; + std::optional kind; PARSE_CONDITIONAL_FIELD(kind, obj, Type::fields::kContextFieldName); - if (kind && *kind == "") { - // Empty string is not a valid kind. - return tl::make_unexpected(JsonError::kSchemaFailure); - } - std::string attr_ref_or_name; PARSE_FIELD_DEFAULT(attr_ref_or_name, obj, Type::fields::kReferenceFieldName, "key"); @@ -43,7 +39,8 @@ tl::expected, JsonError> tag_invoke( AttributeReference::FromReferenceStr(attr_ref_or_name)}; } - return Type{"user", AttributeReference::FromLiteralStr(attr_ref_or_name)}; + return Type{data_model::ContextKind("user"), + AttributeReference::FromLiteralStr(attr_ref_or_name)}; } } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp new file mode 100644 index 000000000..c7046a00b --- /dev/null +++ b/libs/internal/include/launchdarkly/serialization/json_context_kind.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +#include +#include + +#include + +namespace launchdarkly { + +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value); + +} // namespace launchdarkly diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index 99b4c9dd3..e61c03098 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(${LIBNAME} OBJECT serialization/json_primitives.cpp serialization/json_rule_clause.cpp serialization/json_flag.cpp + serialization/json_context_kind.cpp encoding/base_64.cpp encoding/sha_256.cpp signals/boost_signal_connection.cpp) diff --git a/libs/internal/src/serialization/json_context_kind.cpp b/libs/internal/src/serialization/json_context_kind.cpp new file mode 100644 index 000000000..96f8e6114 --- /dev/null +++ b/libs/internal/src/serialization/json_context_kind.cpp @@ -0,0 +1,24 @@ +#include +#include + +#include + +namespace launchdarkly { +tl::expected, JsonError> tag_invoke( + boost::json::value_to_tag< + tl::expected, JsonError>> const& + unused, + boost::json::value const& json_value) { + boost::ignore_unused(unused); + + REQUIRE_STRING(json_value); + auto const& str = json_value.as_string(); + + if (str.empty()) { + /* Empty string is not a valid context kind. */ + return tl::make_unexpected(JsonError::kSchemaFailure); + } + + return data_model::ContextKind(str.c_str()); +} +} // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index 4dd40e92e..f25d48a8d 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -100,7 +101,8 @@ tl::expected, JsonError> tag_invoke( data_model::Flag::Target target; PARSE_FIELD(target.values, obj, "values"); PARSE_FIELD(target.variation, obj, "variation"); - PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", "user"); + PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", + data_model::ContextKind("user")); return target; } diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 43bf8eaa4..8e1feb5ee 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -7,6 +7,7 @@ #include using namespace launchdarkly; +using launchdarkly::data_model::ContextKind; TEST(SDKDataSetTests, DeserializesEmptyDataSet) { auto result = @@ -87,7 +88,7 @@ TEST(SegmentRuleTests, DeserializesSimpleAttributeReference) { tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "bar", "clauses": []})")); ASSERT_TRUE(result); - ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); ASSERT_EQ(result->bucketBy, AttributeReference("bar")); } @@ -96,7 +97,7 @@ TEST(SegmentRuleTests, DeserializesPointerAttributeReference) { tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "/foo/bar", "clauses": []})")); ASSERT_TRUE(result); - ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); ASSERT_EQ(result->bucketBy, AttributeReference("/foo/bar")); } @@ -105,10 +106,17 @@ TEST(SegmentRuleTests, DeserializesEscapedReference) { tl::expected>(boost::json::parse( R"({"rolloutContextKind" : "foo", "bucketBy" : "/~1foo~1bar", "clauses": []})")); ASSERT_TRUE(result); - ASSERT_EQ(result->rolloutContextKind, "foo"); + ASSERT_EQ(result->rolloutContextKind, ContextKind("foo")); ASSERT_EQ(result->bucketBy, AttributeReference("/~1foo~1bar")); } +TEST(SegmentRuleTests, RejectsEmptyContextKind) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"rolloutContextKind" : "", "bucketBy" : "/~1foo~1bar", "clauses": []})")); + ASSERT_FALSE(result); +} + TEST(SegmentRuleTests, DeserializesLiteralAttributeName) { auto result = boost::json::value_to< tl::expected>( @@ -176,6 +184,13 @@ TEST(ClauseTests, DeserializesEscapedReference) { ASSERT_EQ(result->attribute, AttributeReference("/~1foo~1bar")); } +TEST(ClauseTests, RejectsEmptyContextKind) { + auto result = boost::json::value_to< + tl::expected>(boost::json::parse( + R"({"attribute": "/~1foo~1bar", "op": "in", "values": ["a"], "contextKind" : ""})")); + ASSERT_FALSE(result); +} + TEST(ClauseTests, DeserializesLiteralAttributeName) { auto result = boost::json::value_to>( @@ -192,7 +207,7 @@ TEST(RolloutTests, DeserializesMinimumValid) { boost::json::parse(R"({})")); ASSERT_TRUE(result); ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kRollout); - ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->contextKind, ContextKind("user")); ASSERT_EQ(result->bucketBy, "key"); } @@ -202,7 +217,7 @@ TEST(RolloutTests, DeserializesAllFieldsWithAttributeReference) { R"({"kind": "experiment", "contextKind": "org", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); ASSERT_TRUE(result); ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); - ASSERT_EQ(result->contextKind, "org"); + ASSERT_EQ(result->contextKind, ContextKind("org")); ASSERT_EQ(result->bucketBy, "/foo/bar"); ASSERT_EQ(result->seed, 123); ASSERT_TRUE(result->variations.empty()); @@ -214,7 +229,7 @@ TEST(RolloutTests, DeserializesAllFieldsWithLiteralAttributeName) { R"({"kind": "experiment", "bucketBy": "/foo/bar", "seed" : 123, "variations" : []})")); ASSERT_TRUE(result); ASSERT_EQ(result->kind, data_model::Flag::Rollout::Kind::kExperiment); - ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->contextKind, ContextKind("user")); ASSERT_EQ(result->bucketBy, "/~1foo~1bar"); ASSERT_EQ(result->seed, 123); ASSERT_TRUE(result->variations.empty()); @@ -278,7 +293,7 @@ TEST(TargetTests, DeserializesMinimumValid) { tl::expected>( boost::json::parse(R"({})")); ASSERT_TRUE(result); - ASSERT_EQ(result->contextKind, "user"); + ASSERT_EQ(result->contextKind, ContextKind("user")); ASSERT_EQ(result->variation, 0); ASSERT_TRUE(result->values.empty()); } @@ -295,7 +310,7 @@ TEST(TargetTests, DeserializesAllFields) { tl::expected>(boost::json::parse( R"({"variation" : 123, "values" : ["a"], "contextKind" : "org"})")); ASSERT_TRUE(result); - ASSERT_EQ(result->contextKind, "org"); + ASSERT_EQ(result->contextKind, ContextKind("org")); ASSERT_EQ(result->variation, 123); ASSERT_EQ(result->values.size(), 1); ASSERT_EQ(result->values[0], "a"); diff --git a/libs/server-sdk/tests/dependency_tracker_test.cpp b/libs/server-sdk/tests/dependency_tracker_test.cpp index 3bfacb643..bbffbc09b 100644 --- a/libs/server-sdk/tests/dependency_tracker_test.cpp +++ b/libs/server-sdk/tests/dependency_tracker_test.cpp @@ -13,6 +13,7 @@ using launchdarkly::server_side::data_store::SegmentDescriptor; using launchdarkly::AttributeReference; using launchdarkly::Value; using launchdarkly::data_model::Clause; +using launchdarkly::data_model::ContextKind; using launchdarkly::data_model::Flag; using launchdarkly::data_model::ItemDescriptor; using launchdarkly::data_model::Segment; @@ -197,7 +198,7 @@ TEST(DependencyTrackerTest, UsesSegmentRulesToCalculateDependencies) { flag_a.rules.push_back(Flag::Rule{std::vector{ Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, - "user", AttributeReference()}}}); + ContextKind("user"), AttributeReference()}}}); tracker.UpdateDependencies("flagA", FlagDescriptor(flag_a)); tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); @@ -231,7 +232,7 @@ TEST(DependencyTrackerTest, TracksSegmentDependencyOfPrerequisite) { flag_a.rules.push_back(Flag::Rule{std::vector{ Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, - "", AttributeReference()}}}); + ContextKind(""), AttributeReference()}}}); flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); @@ -270,8 +271,8 @@ TEST(DependencyTrackerTest, HandlesSegmentsDependentOnOtherSegments) { segment_b.rules.push_back(Segment::Rule{ std::vector{Clause{Clause::Op::kSegmentMatch, std::vector{"segmentA"}, false, - "user", AttributeReference()}}, - std::nullopt, std::nullopt, "", AttributeReference()}); + ContextKind("user"), AttributeReference()}}, + std::nullopt, std::nullopt, ContextKind(""), AttributeReference()}); tracker.UpdateDependencies("segmentA", SegmentDescriptor(segment_a)); tracker.UpdateDependencies("segmentB", SegmentDescriptor(segment_b)); From e5992ef8b5d7b36f19a79f379c3e3adb680ff79e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 18 Jul 2023 16:37:21 -0700 Subject: [PATCH 15/56] feat: evaluation engine (#183) This commit contains the server-side evaluation engine and associated tests. --- cmake/rfc3339_timestamp.cmake | 31 ++ libs/common/include/launchdarkly/context.hpp | 5 +- .../launchdarkly/data/evaluation_detail.hpp | 30 +- .../launchdarkly/data/evaluation_reason.hpp | 36 ++ .../launchdarkly/detail/c_binding_helpers.hpp | 7 +- .../launchdarkly/logging/log_level.hpp | 4 + libs/common/include/launchdarkly/value.hpp | 20 + libs/common/src/context.cpp | 4 +- libs/common/src/data/evaluation_detail.cpp | 19 +- libs/common/src/data/evaluation_reason.cpp | 33 ++ libs/common/src/log_level.cpp | 5 + libs/common/src/value.cpp | 19 + .../launchdarkly/data_model/context_kind.hpp | 4 + .../include/launchdarkly/data_model/flag.hpp | 18 +- .../launchdarkly/data_model/rule_clause.hpp | 5 + .../launchdarkly/data_model/segment.hpp | 4 +- .../include/launchdarkly/encoding/base_16.hpp | 21 + .../include/launchdarkly/encoding/sha_1.hpp | 10 + .../include/launchdarkly/encoding/sha_256.hpp | 2 +- libs/internal/src/CMakeLists.txt | 3 + libs/internal/src/data_model/flag.cpp | 27 ++ libs/internal/src/data_model/rule_clause.cpp | 62 +++ libs/internal/src/encoding/sha_1.cpp | 18 + libs/internal/src/encoding/sha_256.cpp | 15 +- .../src/serialization/json_context.cpp | 3 +- .../serialization/json_evaluation_reason.cpp | 8 + libs/internal/tests/ld_logger_test.cpp | 6 +- libs/internal/tests/sha_1_test.cpp | 20 + libs/internal/tests/sha_256_test.cpp | 19 +- libs/server-sdk/CMakeLists.txt | 3 + libs/server-sdk/src/CMakeLists.txt | 16 +- libs/server-sdk/src/evaluation/bucketing.cpp | 205 ++++++++++ libs/server-sdk/src/evaluation/bucketing.hpp | 123 ++++++ .../evaluation/detail/evaluation_stack.cpp | 30 ++ .../evaluation/detail/evaluation_stack.hpp | 61 +++ .../evaluation/detail/semver_operations.cpp | 203 ++++++++++ .../evaluation/detail/semver_operations.hpp | 88 +++++ .../detail/timestamp_operations.cpp | 55 +++ .../detail/timestamp_operations.hpp | 16 + .../src/evaluation/evaluation_error.cpp | 62 +++ .../src/evaluation/evaluation_error.hpp | 28 ++ libs/server-sdk/src/evaluation/evaluator.cpp | 212 ++++++++++ libs/server-sdk/src/evaluation/evaluator.hpp | 47 +++ libs/server-sdk/src/evaluation/operators.cpp | 149 +++++++ libs/server-sdk/src/evaluation/operators.hpp | 10 + libs/server-sdk/src/evaluation/rules.cpp | 220 +++++++++++ libs/server-sdk/src/evaluation/rules.hpp | 60 +++ libs/server-sdk/tests/CMakeLists.txt | 5 +- libs/server-sdk/tests/bucketing_tests.cpp | 367 ++++++++++++++++++ .../tests/evaluation_stack_test.cpp | 53 +++ libs/server-sdk/tests/evaluator_tests.cpp | 248 ++++++++++++ libs/server-sdk/tests/operator_tests.cpp | 258 ++++++++++++ libs/server-sdk/tests/rule_tests.cpp | 251 ++++++++++++ libs/server-sdk/tests/semver_tests.cpp | 66 ++++ libs/server-sdk/tests/spy_logger.hpp | 94 +++++ libs/server-sdk/tests/test_store.cpp | 307 +++++++++++++++ libs/server-sdk/tests/test_store.hpp | 19 + libs/server-sdk/tests/timestamp_tests.cpp | 94 +++++ 58 files changed, 3753 insertions(+), 55 deletions(-) create mode 100644 cmake/rfc3339_timestamp.cmake create mode 100644 libs/internal/include/launchdarkly/encoding/base_16.hpp create mode 100644 libs/internal/include/launchdarkly/encoding/sha_1.hpp create mode 100644 libs/internal/src/data_model/flag.cpp create mode 100644 libs/internal/src/data_model/rule_clause.cpp create mode 100644 libs/internal/src/encoding/sha_1.cpp create mode 100644 libs/internal/tests/sha_1_test.cpp create mode 100644 libs/server-sdk/src/evaluation/bucketing.cpp create mode 100644 libs/server-sdk/src/evaluation/bucketing.hpp create mode 100644 libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp create mode 100644 libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp create mode 100644 libs/server-sdk/src/evaluation/detail/semver_operations.cpp create mode 100644 libs/server-sdk/src/evaluation/detail/semver_operations.hpp create mode 100644 libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp create mode 100644 libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp create mode 100644 libs/server-sdk/src/evaluation/evaluation_error.cpp create mode 100644 libs/server-sdk/src/evaluation/evaluation_error.hpp create mode 100644 libs/server-sdk/src/evaluation/evaluator.cpp create mode 100644 libs/server-sdk/src/evaluation/evaluator.hpp create mode 100644 libs/server-sdk/src/evaluation/operators.cpp create mode 100644 libs/server-sdk/src/evaluation/operators.hpp create mode 100644 libs/server-sdk/src/evaluation/rules.cpp create mode 100644 libs/server-sdk/src/evaluation/rules.hpp create mode 100644 libs/server-sdk/tests/bucketing_tests.cpp create mode 100644 libs/server-sdk/tests/evaluation_stack_test.cpp create mode 100644 libs/server-sdk/tests/evaluator_tests.cpp create mode 100644 libs/server-sdk/tests/operator_tests.cpp create mode 100644 libs/server-sdk/tests/rule_tests.cpp create mode 100644 libs/server-sdk/tests/semver_tests.cpp create mode 100644 libs/server-sdk/tests/spy_logger.hpp create mode 100644 libs/server-sdk/tests/test_store.cpp create mode 100644 libs/server-sdk/tests/test_store.hpp create mode 100644 libs/server-sdk/tests/timestamp_tests.cpp diff --git a/cmake/rfc3339_timestamp.cmake b/cmake/rfc3339_timestamp.cmake new file mode 100644 index 000000000..b114f1938 --- /dev/null +++ b/cmake/rfc3339_timestamp.cmake @@ -0,0 +1,31 @@ +FetchContent_Declare(timestamp + GIT_REPOSITORY https://github.com/chansen/c-timestamp + GIT_TAG "b205c407ae6680d23d74359ac00444b80989792f" + ) + +FetchContent_GetProperties(timestamp) +if (NOT timestamp_POPULATED) + FetchContent_Populate(timestamp) +endif () + +add_library(timestamp OBJECT + ${timestamp_SOURCE_DIR}/timestamp_tm.c + ${timestamp_SOURCE_DIR}/timestamp_valid.c + ${timestamp_SOURCE_DIR}/timestamp_parse.c + ) + +if (BUILD_SHARED_LIBS) + set_target_properties(timestamp PROPERTIES + POSITION_INDEPENDENT_CODE 1 + C_VISIBILITY_PRESET hidden + ) +endif () + +target_include_directories(timestamp PUBLIC + $ + $ + ) +install( + TARGETS timestamp + EXPORT ${PROJECT_NAME}-targets +) diff --git a/libs/common/include/launchdarkly/context.hpp b/libs/common/include/launchdarkly/context.hpp index 9c63229e8..81a3cad88 100644 --- a/libs/common/include/launchdarkly/context.hpp +++ b/libs/common/include/launchdarkly/context.hpp @@ -57,8 +57,9 @@ class Context final { * @param ref The reference to the desired attribute. * @return The attribute Value or a Value representing null. */ - Value const& Get(std::string const& kind, - launchdarkly::AttributeReference const& ref); + [[nodiscard]] Value const& Get( + std::string const& kind, + launchdarkly::AttributeReference const& ref) const; /** * Check if a context is valid. diff --git a/libs/common/include/launchdarkly/data/evaluation_detail.hpp b/libs/common/include/launchdarkly/data/evaluation_detail.hpp index 7263f17d9..e0d2cceec 100644 --- a/libs/common/include/launchdarkly/data/evaluation_detail.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_detail.hpp @@ -33,7 +33,15 @@ class EvaluationDetail { * @param error_kind Kind of the error. * @param default_value Default value. */ - EvaluationDetail(enum EvaluationReason::ErrorKind error_kind, T default_value); + EvaluationDetail(enum EvaluationReason::ErrorKind error_kind, + T default_value); + + /** + * Constructs an EvaluationDetail consisting of a reason but no value. + * This is used when a flag has no appropriate fallback value. + * @param reason The reason. + */ + EvaluationDetail(EvaluationReason reason); /** * @return A reference to the variation value. For convenience, the * @@ -52,11 +60,24 @@ class EvaluationDetail { */ [[nodiscard]] std::optional const& Reason() const; + /** + * @return True if the evaluation resulted in an error. + * TODO(sc209960) + */ + [[nodiscard]] bool IsError() const; + /** * @return A reference to the variation value. */ T const& operator*() const; + /** + * @return True if the evaluation was successful (i.e. IsError returns + * false.) + * TODO(sc209960) + */ + explicit operator bool() const; + private: T value_; std::optional variation_index_; @@ -64,9 +85,10 @@ class EvaluationDetail { }; /* - * Holds details for the C bindings, omitting the generic type parameter that is - * needed for EvaluationDetail. Instead, the bindings will directly return - * the evaluation result, and fill in a detail structure using an out parameter. + * Holds details for the C bindings, omitting the generic type parameter + * that is needed for EvaluationDetail. Instead, the bindings will + * directly return the evaluation result, and fill in a detail structure + * using an out parameter. */ struct CEvaluationDetail { template diff --git a/libs/common/include/launchdarkly/data/evaluation_reason.hpp b/libs/common/include/launchdarkly/data/evaluation_reason.hpp index 5238af9a8..75a6fa282 100644 --- a/libs/common/include/launchdarkly/data/evaluation_reason.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_reason.hpp @@ -119,6 +119,42 @@ class EvaluationReason { explicit EvaluationReason(enum ErrorKind error_kind); + /** + * The flag was off. + */ + static EvaluationReason Off(); + + /** + * The flag didn't return a variation due to a prerequisite failing. + */ + static EvaluationReason PrerequisiteFailed(std::string prerequisite_key); + + /** + * The flag evaluated to a particular variation due to a target match. + */ + static EvaluationReason TargetMatch(); + + /** + * The flag evaluated to its fallthrough value. + * @param in_experiment Whether the flag is part of an experiment. + */ + static EvaluationReason Fallthrough(bool in_experiment); + + /** + * The flag evaluated to a particular variation because it matched a rule. + * @param rule_index Index of the rule. + * @param rule_id ID of the rule. + * @param in_experiment Whether the flag is part of an experiment. + */ + static EvaluationReason RuleMatch(std::size_t rule_index, + std::optional rule_id, + bool in_experiment); + + /** + * The flag data was malformed. + */ + static EvaluationReason MalformedFlag(); + friend std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason); diff --git a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp index d9cb21d05..99772b9b6 100644 --- a/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp +++ b/libs/common/include/launchdarkly/detail/c_binding_helpers.hpp @@ -1,11 +1,12 @@ #include -#include -#include #include -#include #include +#include +#include +#include + namespace launchdarkly { template struct has_result_type : std::false_type {}; diff --git a/libs/common/include/launchdarkly/logging/log_level.hpp b/libs/common/include/launchdarkly/logging/log_level.hpp index 46977cbfc..5338b2908 100644 --- a/libs/common/include/launchdarkly/logging/log_level.hpp +++ b/libs/common/include/launchdarkly/logging/log_level.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace launchdarkly { /** * Log levels with kDebug being lowest severity and kError being highest @@ -28,4 +30,6 @@ char const* GetLogLevelName(LogLevel level, char const* default_); */ LogLevel GetLogLevelEnum(char const* name, LogLevel default_); +std::ostream& operator<<(std::ostream& out, LogLevel const& level); + } // namespace launchdarkly diff --git a/libs/common/include/launchdarkly/value.hpp b/libs/common/include/launchdarkly/value.hpp index b176d2c4e..1354c427d 100644 --- a/libs/common/include/launchdarkly/value.hpp +++ b/libs/common/include/launchdarkly/value.hpp @@ -428,4 +428,24 @@ bool operator!=(Value::Array const& lhs, Value::Array const& rhs); bool operator==(Value::Object const& lhs, Value::Object const& rhs); bool operator!=(Value::Object const& lhs, Value::Object const& rhs); +/* Returns true if both values are numbers and lhs < rhs. Returns false if + * either value is not a number. + */ +bool operator<(Value const& lhs, Value const& rhs); + +/* Returns true if both values are numbers and lhs > rhs. Returns false if + * either value is not a number. + */ +bool operator>(Value const& lhs, Value const& rhs); + +/* Returns true if both values are numbers and lhs <= rhs. Returns false if + * either value is not a number. + */ +bool operator<=(Value const& lhs, Value const& rhs); + +/* Returns true if both values are numbers and lhs >= rhs. Returns false if + * either value is not a number. + */ +bool operator>=(Value const& lhs, Value const& rhs); + } // namespace launchdarkly diff --git a/libs/common/src/context.cpp b/libs/common/src/context.cpp index 42d734be6..ad301a0e5 100644 --- a/libs/common/src/context.cpp +++ b/libs/common/src/context.cpp @@ -40,7 +40,7 @@ Context::Context(std::map attributes) } Value const& Context::Get(std::string const& kind, - AttributeReference const& ref) { + AttributeReference const& ref) const { auto found = attributes_.find(kind); if (found != attributes_.end()) { return found->second.Get(ref); @@ -64,7 +64,7 @@ std::string Context::make_canonical_key() { if (kinds_to_keys_.size() == 1) { if (auto iterator = kinds_to_keys_.find("user"); iterator != kinds_to_keys_.end()) { - return std::string(iterator->second); + return iterator->second; } } std::stringstream stream; diff --git a/libs/common/src/data/evaluation_detail.cpp b/libs/common/src/data/evaluation_detail.cpp index 72093b267..81a7aba3b 100644 --- a/libs/common/src/data/evaluation_detail.cpp +++ b/libs/common/src/data/evaluation_detail.cpp @@ -14,12 +14,17 @@ EvaluationDetail::EvaluationDetail( reason_(std::move(reason)) {} template -EvaluationDetail::EvaluationDetail(enum EvaluationReason::ErrorKind error_kind, - T default_value) +EvaluationDetail::EvaluationDetail( + enum EvaluationReason::ErrorKind error_kind, + T default_value) : value_(std::move(default_value)), variation_index_(std::nullopt), reason_(error_kind) {} +template +EvaluationDetail::EvaluationDetail(EvaluationReason reason) + : value_(), variation_index_(std::nullopt), reason_(std::move(reason)) {} + template T const& EvaluationDetail::Value() const { return value_; @@ -39,6 +44,16 @@ T const& EvaluationDetail::operator*() const { return value_; } +template +[[nodiscard]] bool EvaluationDetail::IsError() const { + return reason_.has_value() && reason_->ErrorKind().has_value(); +} + +template +EvaluationDetail::operator bool() const { + return !IsError(); +} + template class EvaluationDetail; template class EvaluationDetail; template class EvaluationDetail; diff --git a/libs/common/src/data/evaluation_reason.cpp b/libs/common/src/data/evaluation_reason.cpp index a626ace5e..7fb433007 100644 --- a/libs/common/src/data/evaluation_reason.cpp +++ b/libs/common/src/data/evaluation_reason.cpp @@ -56,6 +56,39 @@ EvaluationReason::EvaluationReason(enum ErrorKind error_kind) false, std::nullopt) {} +EvaluationReason EvaluationReason::Off() { + return {Kind::kOff, std::nullopt, std::nullopt, std::nullopt, + std::nullopt, false, std::nullopt}; +} + +EvaluationReason EvaluationReason::PrerequisiteFailed( + std::string prerequisite_key) { + return { + Kind::kPrerequisiteFailed, std::nullopt, std::nullopt, std::nullopt, + std::move(prerequisite_key), false, std::nullopt}; +} + +EvaluationReason EvaluationReason::TargetMatch() { + return {Kind::kTargetMatch, std::nullopt, std::nullopt, std::nullopt, + std::nullopt, false, std::nullopt}; +} + +EvaluationReason EvaluationReason::Fallthrough(bool in_experiment) { + return {Kind::kFallthrough, std::nullopt, std::nullopt, std::nullopt, + std::nullopt, in_experiment, std::nullopt}; +} + +EvaluationReason EvaluationReason::RuleMatch(std::size_t rule_index, + std::optional rule_id, + bool in_experiment) { + return {Kind::kRuleMatch, std::nullopt, rule_index, std::move(rule_id), + std::nullopt, in_experiment, std::nullopt}; +} + +EvaluationReason EvaluationReason::MalformedFlag() { + return EvaluationReason{ErrorKind::kMalformedFlag}; +} + std::ostream& operator<<(std::ostream& out, EvaluationReason const& reason) { out << "{"; out << " kind: " << reason.kind_; diff --git a/libs/common/src/log_level.cpp b/libs/common/src/log_level.cpp index e4c6b29b0..d708d056f 100644 --- a/libs/common/src/log_level.cpp +++ b/libs/common/src/log_level.cpp @@ -44,4 +44,9 @@ LogLevel GetLogLevelEnum(char const* level, LogLevel default_) { return default_; } +std::ostream& operator<<(std::ostream& out, LogLevel const& level) { + out << GetLogLevelName(level, "unknown"); + return out; +} + } // namespace launchdarkly diff --git a/libs/common/src/value.cpp b/libs/common/src/value.cpp index bc28777fa..70bc20339 100644 --- a/libs/common/src/value.cpp +++ b/libs/common/src/value.cpp @@ -264,4 +264,23 @@ bool operator!=(Value::Object const& lhs, Value::Object const& rhs) { return !(lhs == rhs); } +inline bool BothNumbers(Value const& lhs, Value const& rhs) { + return lhs.IsNumber() && rhs.IsNumber(); +} + +bool operator<(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && lhs.AsDouble() < rhs.AsDouble(); +} + +bool operator>(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && rhs < lhs; +} + +bool operator<=(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && !(lhs > rhs); +} + +bool operator>=(Value const& lhs, Value const& rhs) { + return BothNumbers(lhs, rhs) && !(lhs < rhs); +} } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/data_model/context_kind.hpp b/libs/internal/include/launchdarkly/data_model/context_kind.hpp index efa5b342e..6d4690311 100644 --- a/libs/internal/include/launchdarkly/data_model/context_kind.hpp +++ b/libs/internal/include/launchdarkly/data_model/context_kind.hpp @@ -8,4 +8,8 @@ namespace launchdarkly::data_model { BOOST_STRONG_TYPEDEF(std::string, ContextKind); +inline bool IsUser(ContextKind const& kind) noexcept { + return kind.t == "user"; +} + } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index 65d56d12a..fc312d666 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -16,6 +16,9 @@ namespace launchdarkly::data_model { struct Flag { + using Variation = std::uint64_t; + using Weight = std::uint64_t; + struct Rollout { enum class Kind { kUnrecognized = 0, @@ -24,9 +27,16 @@ struct Flag { }; struct WeightedVariation { - std::uint64_t variation; - std::uint64_t weight; + Variation variation; + Weight weight; bool untracked; + + WeightedVariation() = default; + WeightedVariation(Variation index, Weight weight); + static WeightedVariation Untracked(Variation index, Weight weight); + + private: + WeightedVariation(Variation index, Weight weight, bool untracked); }; std::vector variations; @@ -36,9 +46,11 @@ struct Flag { DEFINE_ATTRIBUTE_REFERENCE_FIELD(bucketBy) DEFINE_CONTEXT_KIND_FIELD(contextKind) + + Rollout() = default; + Rollout(std::vector); }; - using Variation = std::uint64_t; using VariationOrRollout = std::variant; struct Prerequisite { diff --git a/libs/internal/include/launchdarkly/data_model/rule_clause.hpp b/libs/internal/include/launchdarkly/data_model/rule_clause.hpp index 751f43fa7..fd11916cf 100644 --- a/libs/internal/include/launchdarkly/data_model/rule_clause.hpp +++ b/libs/internal/include/launchdarkly/data_model/rule_clause.hpp @@ -3,6 +3,8 @@ #include #include +#include "context_aware_reference.hpp" + #include #include #include @@ -35,4 +37,7 @@ struct Clause { DEFINE_CONTEXT_KIND_FIELD(contextKind) DEFINE_ATTRIBUTE_REFERENCE_FIELD(attribute) }; + +std::ostream& operator<<(std::ostream& os, Clause::Op operator_); + } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/data_model/segment.hpp b/libs/internal/include/launchdarkly/data_model/segment.hpp index cb048796a..559eb0098 100644 --- a/libs/internal/include/launchdarkly/data_model/segment.hpp +++ b/libs/internal/include/launchdarkly/data_model/segment.hpp @@ -46,8 +46,8 @@ struct Segment { std::optional unboundedContextKind; std::optional generation; - // TODO(cwaldren): make Kind a real type that is deserialized, so we can - // make empty string an error. + // TODO(sc209882): in data model, ensure empty Kind string is error + // condition. /** * Returns the segment's version. Satisfies ItemDescriptor template diff --git a/libs/internal/include/launchdarkly/encoding/base_16.hpp b/libs/internal/include/launchdarkly/encoding/base_16.hpp new file mode 100644 index 000000000..0ffae71b4 --- /dev/null +++ b/libs/internal/include/launchdarkly/encoding/base_16.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +namespace launchdarkly::encoding { + +template +std::string Base16Encode(std::array arr) { + std::stringstream output_stream; + output_stream << std::hex << std::noshowbase; + for (unsigned char byte : arr) { + output_stream << std::setw(2) << std::setfill('0') + << static_cast(byte); + } + return output_stream.str(); +} + +} // namespace launchdarkly::encoding diff --git a/libs/internal/include/launchdarkly/encoding/sha_1.hpp b/libs/internal/include/launchdarkly/encoding/sha_1.hpp new file mode 100644 index 000000000..e233757c3 --- /dev/null +++ b/libs/internal/include/launchdarkly/encoding/sha_1.hpp @@ -0,0 +1,10 @@ +#pragma once +#include +#include + +#include + +namespace launchdarkly::encoding { +std::array Sha1String( + std::string const& input); +} diff --git a/libs/internal/include/launchdarkly/encoding/sha_256.hpp b/libs/internal/include/launchdarkly/encoding/sha_256.hpp index f4b354346..d9d71d5b4 100644 --- a/libs/internal/include/launchdarkly/encoding/sha_256.hpp +++ b/libs/internal/include/launchdarkly/encoding/sha_256.hpp @@ -3,7 +3,7 @@ #include #include -#include "openssl/sha.h" +#include namespace launchdarkly::encoding { diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index e61c03098..eabd90097 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -40,8 +40,11 @@ add_library(${LIBNAME} OBJECT serialization/json_rule_clause.cpp serialization/json_flag.cpp serialization/json_context_kind.cpp + data_model/rule_clause.cpp + data_model/flag.cpp encoding/base_64.cpp encoding/sha_256.cpp + encoding/sha_1.cpp signals/boost_signal_connection.cpp) add_library(launchdarkly::internal ALIAS ${LIBNAME}) diff --git a/libs/internal/src/data_model/flag.cpp b/libs/internal/src/data_model/flag.cpp new file mode 100644 index 000000000..78d4f4651 --- /dev/null +++ b/libs/internal/src/data_model/flag.cpp @@ -0,0 +1,27 @@ +#include + +namespace launchdarkly::data_model { + +Flag::Rollout::WeightedVariation::WeightedVariation(Flag::Variation variation_, + Flag::Weight weight_) + : WeightedVariation(variation_, weight_, false) {} + +Flag::Rollout::WeightedVariation::WeightedVariation(Flag::Variation variation_, + Flag::Weight weight_, + bool untracked_) + : variation(variation_), weight(weight_), untracked(untracked_) {} + +Flag::Rollout::WeightedVariation Flag::Rollout::WeightedVariation::Untracked( + Flag::Variation variation_, + Flag::Weight weight_) { + return {variation_, weight_, true}; +} + +Flag::Rollout::Rollout(std::vector variations_) + : variations(std::move(variations_)), + kind(Kind::kRollout), + seed(std::nullopt), + bucketBy("key"), + contextKind("user") {} + +} // namespace launchdarkly::data_model diff --git a/libs/internal/src/data_model/rule_clause.cpp b/libs/internal/src/data_model/rule_clause.cpp new file mode 100644 index 000000000..664fa14aa --- /dev/null +++ b/libs/internal/src/data_model/rule_clause.cpp @@ -0,0 +1,62 @@ +#include + +namespace launchdarkly::data_model { + +std::ostream& operator<<(std::ostream& os, Clause::Op operator_) { + switch (operator_) { + case Clause::Op::kUnrecognized: + os << "unrecognized"; + break; + case Clause::Op::kIn: + os << "in"; + break; + case Clause::Op::kStartsWith: + os << "startsWith"; + break; + case Clause::Op::kEndsWith: + os << "endsWith"; + break; + case Clause::Op::kMatches: + os << "matches"; + break; + case Clause::Op::kContains: + os << "contains"; + break; + case Clause::Op::kLessThan: + os << "lessThan"; + break; + case Clause::Op::kLessThanOrEqual: + os << "lessThanOrEqual"; + break; + case Clause::Op::kGreaterThan: + os << "greaterThan"; + break; + case Clause::Op::kGreaterThanOrEqual: + os << "greaterThanOrEqual"; + break; + case Clause::Op::kBefore: + os << "before"; + break; + case Clause::Op::kAfter: + os << "after"; + break; + case Clause::Op::kSemVerEqual: + os << "semVerEqual"; + break; + case Clause::Op::kSemVerLessThan: + os << "semVerLessThan"; + break; + case Clause::Op::kSemVerGreaterThan: + os << "semVerGreaterThan"; + break; + case Clause::Op::kSegmentMatch: + os << "segmentMatch"; + break; + default: + os << "unknown"; + break; + } + return os; +} + +} // namespace launchdarkly::data_model diff --git a/libs/internal/src/encoding/sha_1.cpp b/libs/internal/src/encoding/sha_1.cpp new file mode 100644 index 000000000..c516995bb --- /dev/null +++ b/libs/internal/src/encoding/sha_1.cpp @@ -0,0 +1,18 @@ +#include + +#include + +namespace launchdarkly::encoding { + +std::array Sha1String( + std::string const& input) { + std::array hash{}; + + SHA_CTX sha; + SHA1_Init(&sha); + SHA1_Update(&sha, input.c_str(), input.size()); + SHA1_Final(hash.data(), &sha); + + return hash; +} +} // namespace launchdarkly::encoding diff --git a/libs/internal/src/encoding/sha_256.cpp b/libs/internal/src/encoding/sha_256.cpp index f968a8a98..a9c208979 100644 --- a/libs/internal/src/encoding/sha_256.cpp +++ b/libs/internal/src/encoding/sha_256.cpp @@ -1,24 +1,19 @@ -#include "openssl/sha.h" +#include #include -#include -#include -#include - namespace launchdarkly::encoding { std::array Sha256String( std::string const& input) { - unsigned char hash[SHA256_DIGEST_LENGTH]; + std::array hash{}; + SHA256_CTX sha256; SHA256_Init(&sha256); SHA256_Update(&sha256, input.c_str(), input.size()); - SHA256_Final(hash, &sha256); + SHA256_Final(hash.data(), &sha256); - std::array out; - std::copy(std::begin(hash), std::end(hash), out.begin()); - return out; + return hash; } } // namespace launchdarkly::encoding diff --git a/libs/internal/src/serialization/json_context.cpp b/libs/internal/src/serialization/json_context.cpp index cbad0bb7f..f61810196 100644 --- a/libs/internal/src/serialization/json_context.cpp +++ b/libs/internal/src/serialization/json_context.cpp @@ -4,12 +4,11 @@ #include #include +#include #include #include -#include - namespace launchdarkly { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, diff --git a/libs/internal/src/serialization/json_evaluation_reason.cpp b/libs/internal/src/serialization/json_evaluation_reason.cpp index afd6772e4..1ae824144 100644 --- a/libs/internal/src/serialization/json_evaluation_reason.cpp +++ b/libs/internal/src/serialization/json_evaluation_reason.cpp @@ -13,6 +13,7 @@ tl::expected tag_invoke( boost::json::value_to_tag< tl::expected> const& unused, boost::json::value const& json_value) { + boost::ignore_unused(unused); if (!json_value.is_string()) { return tl::unexpected(JsonError::kSchemaFailure); } @@ -41,6 +42,7 @@ tl::expected tag_invoke( void tag_invoke(boost::json::value_from_tag const& unused, boost::json::value& json_value, enum EvaluationReason::Kind const& kind) { + boost::ignore_unused(unused); auto& str = json_value.emplace_string(); switch (kind) { case EvaluationReason::Kind::kOff: @@ -68,6 +70,8 @@ tl::expected tag_invoke( boost::json::value_to_tag> const& unused, boost::json::value const& json_value) { + boost::ignore_unused(unused); + if (!json_value.is_string()) { return tl::unexpected(JsonError::kSchemaFailure); } @@ -96,6 +100,8 @@ tl::expected tag_invoke( void tag_invoke(boost::json::value_from_tag const& unused, boost::json::value& json_value, enum EvaluationReason::ErrorKind const& kind) { + boost::ignore_unused(unused); + auto& str = json_value.emplace_string(); switch (kind) { case EvaluationReason::ErrorKind::kClientNotReady: @@ -184,6 +190,8 @@ tl::expected tag_invoke( void tag_invoke(boost::json::value_from_tag const& unused, boost::json::value& json_value, EvaluationReason const& reason) { + boost::ignore_unused(unused); + auto& obj = json_value.emplace_object(); obj.emplace("kind", boost::json::value_from(reason.Kind())); if (auto error_kind = reason.ErrorKind()) { diff --git a/libs/internal/tests/ld_logger_test.cpp b/libs/internal/tests/ld_logger_test.cpp index cf393d332..d0072dcc1 100644 --- a/libs/internal/tests/ld_logger_test.cpp +++ b/libs/internal/tests/ld_logger_test.cpp @@ -86,11 +86,7 @@ INSTANTIATE_TEST_SUITE_P(LDLoggerTest, testing::Values(LogLevel::kDebug, LogLevel::kInfo, LogLevel::kWarn, - LogLevel::kError), - [](testing::TestParamInfo const& info) { - return launchdarkly::GetLogLevelName(info.param, - "unknown"); - }); + LogLevel::kError)); TEST(LDLoggerTest, UsesOstreamForEnabledLevel) { Messages messages; diff --git a/libs/internal/tests/sha_1_test.cpp b/libs/internal/tests/sha_1_test.cpp new file mode 100644 index 000000000..a491de37e --- /dev/null +++ b/libs/internal/tests/sha_1_test.cpp @@ -0,0 +1,20 @@ +#include + +#include "launchdarkly/encoding/base_16.hpp" +#include "launchdarkly/encoding/sha_1.hpp" + +using namespace launchdarkly::encoding; + +TEST(Sha1, CanEncodeString) { + // Test vectors from + // https://www.di-mgt.com.au/sha_testvectors.html + EXPECT_EQ(std::string("da39a3ee5e6b4b0d3255bfef95601890afd80709"), + Base16Encode(Sha1String(""))); + + EXPECT_EQ(std::string("a9993e364706816aba3e25717850c26c9cd0d89d"), + Base16Encode(Sha1String("abc"))); + + EXPECT_EQ(std::string("84983e441c3bd26ebaae4aa1f95129e5e54670f1"), + Base16Encode(Sha1String( + "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"))); +} diff --git a/libs/internal/tests/sha_256_test.cpp b/libs/internal/tests/sha_256_test.cpp index 99105a755..1e417aef8 100644 --- a/libs/internal/tests/sha_256_test.cpp +++ b/libs/internal/tests/sha_256_test.cpp @@ -1,18 +1,9 @@ #include +#include "launchdarkly/encoding/base_16.hpp" #include "launchdarkly/encoding/sha_256.hpp" -using launchdarkly::encoding::Sha256String; - -static std::string HexEncode(std::array arr) { - std::stringstream output_stream; - output_stream << std::hex << std::noshowbase; - for (unsigned char byte : arr) { - output_stream << std::setw(2) << std::setfill('0') - << static_cast(byte); - } - return output_stream.str(); -} +using namespace launchdarkly::encoding; TEST(Sha256, CanEncodeString) { // Test vectors from @@ -20,16 +11,16 @@ TEST(Sha256, CanEncodeString) { EXPECT_EQ( std::string( "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), - HexEncode(Sha256String(""))); + Base16Encode(Sha256String(""))); EXPECT_EQ( std::string( "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"), - HexEncode(Sha256String("abc"))); + Base16Encode(Sha256String("abc"))); EXPECT_EQ( std::string( "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"), - HexEncode(Sha256String( + Base16Encode(Sha256String( "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"))); } diff --git a/libs/server-sdk/CMakeLists.txt b/libs/server-sdk/CMakeLists.txt index 36bd7b6d0..5f41ab6ba 100644 --- a/libs/server-sdk/CMakeLists.txt +++ b/libs/server-sdk/CMakeLists.txt @@ -27,6 +27,9 @@ endif () # Needed to fetch external dependencies. include(FetchContent) +# Needed to parse RFC3339 dates in flag rules. +include(${CMAKE_FILES}/rfc3339_timestamp.cmake) + # Add main SDK sources. add_subdirectory(src) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 780ecb553..0c298e70d 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -7,6 +7,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS add_library(${LIBNAME} ${HEADER_LIST} + boost.cpp data_sources/data_source_update_sink.hpp data_store/data_store.hpp data_store/data_store_updater.hpp @@ -22,12 +23,21 @@ add_library(${LIBNAME} data_sources/polling_data_source.cpp data_sources/data_source_status_manager.hpp data_sources/streaming_data_source.hpp - data_sources/streaming_data_source.cpp) + data_sources/streaming_data_source.cpp + evaluation/evaluator.cpp + evaluation/rules.cpp + evaluation/bucketing.cpp + evaluation/operators.cpp + evaluation/evaluation_error.cpp + evaluation/detail/evaluation_stack.cpp + evaluation/detail/semver_operations.cpp + evaluation/detail/timestamp_operations.cpp + ) if (MSVC OR (NOT BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common - PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy) + PRIVATE Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy timestamp) else () # The default static lib builds, for linux, are position independent. # So they do not link into a shared object without issues. So, when @@ -36,7 +46,7 @@ else () # macOS shares the same path for simplicity. target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common - PRIVATE Boost::headers launchdarkly::sse launchdarkly::internal foxy) + PRIVATE Boost::headers launchdarkly::sse launchdarkly::internal foxy timestamp) target_sources(${LIBNAME} PRIVATE boost.cpp) endif () diff --git a/libs/server-sdk/src/evaluation/bucketing.cpp b/libs/server-sdk/src/evaluation/bucketing.cpp new file mode 100644 index 000000000..7ebd7fb8f --- /dev/null +++ b/libs/server-sdk/src/evaluation/bucketing.cpp @@ -0,0 +1,205 @@ +#include "bucketing.hpp" + +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side::evaluation { + +using namespace launchdarkly::data_model; + +double const kBucketHashScale = static_cast(0x0FFFFFFFFFFFFFFF); + +AttributeReference const& Key(); + +std::optional ContextHash(Value const& value, + BucketPrefix prefix); + +std::optional BucketValue(Value const& value); + +bool IsIntegral(double f); + +BucketPrefix::BucketPrefix(Seed seed) : prefix_(seed) {} + +BucketPrefix::BucketPrefix(std::string key, std::string salt) + : prefix_(KeyAndSalt{key, salt}) {} + +std::ostream& operator<<(std::ostream& os, BucketPrefix const& prefix) { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + os << arg.key << "." << arg.salt; + } else if constexpr (std::is_same_v) { + os << arg; + } + }, + prefix.prefix_); + return os; +} + +BucketResult::BucketResult(Flag::Rollout::WeightedVariation weighted_variation, + bool is_experiment) + : variation_index_(weighted_variation.variation), + in_experiment_(is_experiment && !weighted_variation.untracked) {} + +BucketResult::BucketResult(Flag::Variation variation, bool in_experiment) + : variation_index_(variation), in_experiment_(in_experiment) {} + +BucketResult::BucketResult(Flag::Variation variation) + : variation_index_(variation), in_experiment_(false) {} + +std::size_t BucketResult::VariationIndex() const { + return variation_index_; +} + +bool BucketResult::InExperiment() const { + return in_experiment_; +} + +tl::expected, Error> Bucket( + Context const& context, + AttributeReference const& by_attr, + BucketPrefix const& prefix, + bool is_experiment, + std::string const& context_kind) { + AttributeReference const& ref = is_experiment ? Key() : by_attr; + + if (!ref.Valid()) { + return tl::make_unexpected( + Error::InvalidAttributeReference(ref.RedactionName())); + } + + Value const& value = context.Get(context_kind, ref); + + bool is_bucketable = value.Type() == Value::Type::kNumber || + value.Type() == Value::Type::kString; + + if (is_bucketable) { + return std::make_pair(ContextHash(value, prefix).value_or(0.0), + RolloutKindLookup::kPresent); + } + + auto rollout_context_found = + std::count(context.Kinds().begin(), context.Kinds().end(), + context_kind) > 0; + + return std::make_pair(0.0, rollout_context_found + ? RolloutKindLookup::kPresent + : RolloutKindLookup::kAbsent); +} + +AttributeReference const& Key() { + static AttributeReference const key{"key"}; + LD_ASSERT(key.Valid()); + return key; +} + +std::optional ContextHash(Value const& value, BucketPrefix prefix) { + using namespace launchdarkly::encoding; + + std::optional id = BucketValue(value); + if (!id) { + return std::nullopt; + } + + std::stringstream input; + input << prefix << "." << *id; + + std::array const sha1hash = + Sha1String(input.str()); + + std::string const sha1hash_hexed = Base16Encode(sha1hash); + + std::string const sha1hash_hexed_first_15 = sha1hash_hexed.substr(0, 15); + + try { + unsigned long long as_number = + std::stoull(sha1hash_hexed_first_15.data(), nullptr, /* base */ 16); + + double as_double = static_cast(as_number); + return as_double / kBucketHashScale; + + } catch (std::invalid_argument) { + return std::nullopt; + } catch (std::out_of_range) { + return std::nullopt; + } +} + +std::optional BucketValue(Value const& value) { + switch (value.Type()) { + case Value::Type::kString: + return value.AsString(); + case Value::Type::kNumber: { + if (IsIntegral(value.AsDouble())) { + return std::to_string(value.AsInt()); + } + return std::nullopt; + } + default: + return std::nullopt; + } +} + +bool IsIntegral(double f) { + return std::trunc(f) == f; +} + +tl::expected Variation( + Flag::VariationOrRollout const& vr, + std::string const& flag_key, + Context const& context, + std::optional const& salt) { + if (!salt) { + return tl::make_unexpected(Error::MissingSalt(flag_key)); + } + return std::visit( + [&](auto&& arg) -> tl::expected { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return BucketResult(arg); + } else if constexpr (std::is_same_v) { + if (arg.variations.empty()) { + return tl::make_unexpected( + Error::RolloutMissingVariations()); + } + + bool is_experiment = + arg.kind == Flag::Rollout::Kind::kExperiment; + + std::optional prefix = + arg.seed ? BucketPrefix(*arg.seed) + : BucketPrefix(flag_key, *salt); + + auto bucketing_result = Bucket(context, arg.bucketBy, *prefix, + is_experiment, arg.contextKind); + if (!bucketing_result) { + return tl::make_unexpected(bucketing_result.error()); + } + + auto [bucket, lookup] = *bucketing_result; + + double sum = 0.0; + + for (const auto& variation : arg.variations) { + sum += variation.weight / kBucketScale; + if (bucket < sum) { + return BucketResult( + variation, + is_experiment && + lookup == RolloutKindLookup::kPresent); + } + } + + return BucketResult( + arg.variations.back(), + is_experiment && lookup == RolloutKindLookup::kPresent); + } + }, + vr); +} +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/bucketing.hpp b/libs/server-sdk/src/evaluation/bucketing.hpp new file mode 100644 index 000000000..b536f1921 --- /dev/null +++ b/libs/server-sdk/src/evaluation/bucketing.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include "evaluation_error.hpp" + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::evaluation { + +double const kBucketScale = 100'000.0; + +enum RolloutKindLookup { + /* The rollout's context kind was found in the supplied evaluation context. + */ + kPresent, + /* The rollout's context kind was not found in the supplied evaluation + * context. */ + kAbsent +}; + +/** + * Bucketing is performed by hashing an input string. This string + * may be comprised of a seed (if the flag rule has a seed) or a combined + * key/salt pair. + */ +class BucketPrefix { + public: + struct KeyAndSalt { + std::string key; + std::string salt; + }; + + using Seed = std::int64_t; + + /** + * Constructs a BucketPrefix from a seed value. + * @param seed Value of the seed. + */ + explicit BucketPrefix(Seed seed); + + /** + * Constructs a BucketPrefix from a key and salt. + * @param key Key to use. + * @param salt Salt to use. + */ + BucketPrefix(std::string key, std::string salt); + + friend std::ostream& operator<<(std::ostream& os, + BucketPrefix const& prefix); + + private: + std::variant prefix_; +}; + +using ContextHashValue = float; + +/** + * Computes the context hash value for an attribute in the given context + * identified by the given attribute reference. The hash value is + * augmented with the supplied bucket prefix. + * + * @param context Context to query. + * @param by_attr Identifier of the attribute to hash. If is_experiment is true, + * then "key" will be used regardless of by_attr's value. + * @param prefix Prefix to use when hashing. + * @param is_experiment Whether this rollout is an experiment. + * @param context_kind Which kind to inspect in the context. + * @return A context hash value and indication of whether or not context_kind + * was found in the context. + */ +tl::expected, Error> Bucket( + Context const& context, + AttributeReference const& by_attr, + BucketPrefix const& prefix, + bool is_experiment, + std::string const& context_kind); + +class BucketResult { + public: + BucketResult( + data_model::Flag::Rollout::WeightedVariation weighted_variation, + bool is_experiment); + + BucketResult(data_model::Flag::Variation variation, bool in_experiment); + + BucketResult(data_model::Flag::Variation variation); + + [[nodiscard]] std::size_t VariationIndex() const; + + [[nodiscard]] bool InExperiment() const; + + private: + std::size_t variation_index_; + bool in_experiment_; +}; + +/** + * Given a variation or rollout and associated flag key, computes the proper + * variation index for the context. For a plain variation, this is simply the + * variation index. For a rollout, this utilizes the Bucket function. + * + * @param vr Variation or rollout. + * @param flag_key Key of flag. + * @param context Context to bucket. + * @param salt Salt to use when bucketing. + * @return A BucketResult on success, or an error if bucketing failed. + */ +tl::expected Variation( + data_model::Flag::VariationOrRollout const& vr, + std::string const& flag_key, + launchdarkly::Context const& context, + std::optional const& salt); + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp b/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp new file mode 100644 index 000000000..f27a1dd1a --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp @@ -0,0 +1,30 @@ +#include "evaluation_stack.hpp" + +namespace launchdarkly::server_side::evaluation::detail { + +Guard::Guard(std::unordered_set& set, std::string const& key) + : set_(set), key_(key) { + set_.insert(key_); +} + +Guard::~Guard() { + set_.erase(key_); +} + +std::optional EvaluationStack::NoticePrerequisite( + std::string const& prerequisite_key) { + if (prerequisites_seen_.count(prerequisite_key) != 0) { + return std::nullopt; + } + return std::make_optional(prerequisites_seen_, prerequisite_key); +} + +std::optional EvaluationStack::NoticeSegment( + std::string const& segment_key) { + if (segments_seen_.count(segment_key) != 0) { + return std::nullopt; + } + return std::make_optional(segments_seen_, segment_key); +} + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp b/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp new file mode 100644 index 000000000..384b9af93 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +/** + * Guard is an object used to track that a segment or flag key has been noticed. + * Upon destruction, the key is forgotten. + */ +struct Guard { + Guard(std::unordered_set& set, std::string const& key); + ~Guard(); + + Guard(Guard const&) = delete; + Guard& operator=(Guard const&) = delete; + + Guard(Guard&&) = delete; + Guard& operator=(Guard&&) = delete; + + private: + std::unordered_set& set_; + std::string const& key_; +}; + +/** + * EvaluationStack is used to track which segments and flags have been noticed + * during evaluation in order to detect circular references. + */ +class EvaluationStack { + public: + EvaluationStack() = default; + + /** + * If the given prerequisite key has not been seen, marks it as seen + * and returns a Guard object. Otherwise, returns std::nullopt. + * + * @param prerequisite_key Key of the prerequisite. + * @return Guard object if not seen before, otherwise std::nullopt. + */ + [[nodiscard]] std::optional NoticePrerequisite( + std::string const& prerequisite_key); + + /** + * If the given segment key has not been seen, marks it as seen + * and returns a Guard object. Otherwise, returns std::nullopt. + * + * @param prerequisite_key Key of the segment. + * @return Guard object if not seen before, otherwise std::nullopt. + */ + [[nodiscard]] std::optional NoticeSegment( + std::string const& segment_key); + + private: + std::unordered_set prerequisites_seen_; + std::unordered_set segments_seen_; +}; + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/semver_operations.cpp b/libs/server-sdk/src/evaluation/detail/semver_operations.cpp new file mode 100644 index 000000000..1ecc471d7 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/semver_operations.cpp @@ -0,0 +1,203 @@ +#include "semver_operations.hpp" + +#include + +#include +#include +#include + +#include + +namespace launchdarkly::server_side::evaluation::detail { + +/* + * Official SemVer 2.0 Regex + * https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + * + * Modified for LaunchDarkly usage to allow missing minor and patch versions, + * i.e. "1" means "1.0.0" or "1.2" means "1.2.0". + */ +char const* const kSemVerRegex = + R"(^(?0|[1-9]\d*)(\.(?0|[1-9]\d*))?(\.(?0|[1-9]\d*))?(?:-(?(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"; + +/** + * Cache the parsed regex so it doesn't need to be rebuilt constantly. + * + * From Boost docs: + * Class basic_regex and its typedefs regex and wregex are thread safe, in that + * compiled regular expressions can safely be shared between threads. + */ +static boost::regex const& SemVerRegex() { + static boost::regex regex{kSemVerRegex, boost::regex_constants::no_except}; + LD_ASSERT(regex.status() == 0); + return regex; +} + +SemVer::SemVer(VersionType major, + VersionType minor, + VersionType patch, + std::vector prerelease) + : major_(major), minor_(minor), patch_(patch), prerelease_(prerelease) {} + +SemVer::SemVer(VersionType major, VersionType minor, VersionType patch) + : major_(major), minor_(minor), patch_(patch), prerelease_(std::nullopt) {} + +SemVer::SemVer() : SemVer(0, 0, 0) {} + +SemVer::VersionType SemVer::Major() const { + return major_; +} + +SemVer::VersionType SemVer::Minor() const { + return minor_; +} + +SemVer::VersionType SemVer::Patch() const { + return patch_; +} + +std::optional> const& SemVer::Prerelease() const { + return prerelease_; +} + +bool operator<(SemVer::Token const& lhs, SemVer::Token const& rhs) { + if (lhs.index() != rhs.index()) { + /* Numeric identifiers (index 0 of variant) always have lower precedence +than non-numeric identifiers. */ + return lhs.index() < rhs.index(); + } + if (lhs.index() == 0) { + return std::get<0>(lhs) < std::get<0>(rhs); + } + return std::get<1>(lhs) < std::get<1>(rhs); +} + +bool operator==(SemVer const& lhs, SemVer const& rhs) { + return lhs.Major() == rhs.Major() && lhs.Minor() == rhs.Minor() && + lhs.Patch() == rhs.Patch() && lhs.Prerelease() == rhs.Prerelease(); +} + +bool operator<(SemVer const& lhs, SemVer const& rhs) { + if (lhs.Major() < rhs.Major()) { + return true; + } + if (lhs.Major() > rhs.Major()) { + return false; + } + if (lhs.Minor() < rhs.Minor()) { + return true; + } + if (lhs.Minor() > rhs.Minor()) { + return false; + } + if (lhs.Patch() < rhs.Patch()) { + return true; + } + if (lhs.Patch() > rhs.Patch()) { + return false; + } + // At this point, lhs and rhs have equal major/minor/patch versions. + if (!lhs.Prerelease() && !rhs.Prerelease()) { + return false; + } + if (lhs.Prerelease() && !rhs.Prerelease()) { + return true; + } + if (!lhs.Prerelease() && rhs.Prerelease()) { + return false; + } + return *lhs.Prerelease() < *rhs.Prerelease(); +} + +bool operator>(SemVer const& lhs, SemVer const& rhs) { + return rhs < lhs; +} + +std::optional SemVer::Parse(std::string const& value) { + if (value.empty()) { + return std::nullopt; + } + + boost::regex const& semver_regex = SemVerRegex(); + boost::smatch match; + + try { + if (!boost::regex_match(value, match, semver_regex)) { + // Not a semantic version. + return std::nullopt; + } + } catch (std::runtime_error) { + /* std::runtime_error if the complexity of matching the expression + * against an N character string begins to exceed O(N2), or if the + * program runs out of stack space while matching the expression + * (if Boost.Regex is configured in recursive mode), or if the matcher + * exhausts its permitted memory allocation (if Boost.Regex + * is configured in non-recursive mode).*/ + return std::nullopt; + } + + SemVer::VersionType major = 0; + SemVer::VersionType minor = 0; + SemVer::VersionType patch = 0; + + try { + if (match["major"].matched) { + major = boost::lexical_cast(match["major"]); + } + if (match["minor"].matched) { + minor = boost::lexical_cast(match["minor"]); + } + if (match["patch"].matched) { + patch = boost::lexical_cast(match["patch"]); + } + } catch (boost::bad_lexical_cast) { + return std::nullopt; + } + + if (!match["prerelease"].matched) { + return SemVer{major, minor, patch}; + } + + std::vector tokens; + boost::split(tokens, match["prerelease"], boost::is_any_of(".")); + + std::vector prerelease; + + std::transform( + tokens.begin(), tokens.end(), std::back_inserter(prerelease), + [](std::string const& token) -> SemVer::Token { + try { + return boost::lexical_cast(token); + } catch (boost::bad_lexical_cast) { + return token; + } + }); + + return SemVer{major, minor, patch, prerelease}; +} + +std::ostream& operator<<(std::ostream& out, SemVer::Token const& token) { + if (token.index() == 0) { + out << std::get<0>(token); + } else { + out << std::get<1>(token); + } + return out; +} + +std::ostream& operator<<(std::ostream& out, SemVer const& ver) { + out << ver.Major() << "." << ver.Minor() << "." << ver.Patch(); + if (ver.Prerelease()) { + out << "-"; + + for (auto it = ver.Prerelease()->begin(); it != ver.Prerelease()->end(); + ++it) { + out << *it; + if (std::next(it) != ver.Prerelease()->end()) { + out << "."; + } + } + } + return out; +} +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/semver_operations.hpp b/libs/server-sdk/src/evaluation/detail/semver_operations.hpp new file mode 100644 index 000000000..85b371116 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/semver_operations.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include +#include +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +/** + * Represents a LaunchDarkly-flavored Semantic Version v2. + * + * Semantic versions can be compared using ==, <, and >. + * + * The main difference from the official spec is that missing minor and patch + * versions are allowed, i.e. "1" means "1.0.0" and "1.2" means "1.2.0". + */ +class SemVer { + public: + using VersionType = std::uint64_t; + + using Token = std::variant; + + /** + * Constructs a SemVer representing "0.0.0". + */ + SemVer(); + + /** + * Constructs a SemVer from a major, minor, patch, and prerelease. The + * prerelease consists of list of string/nonzero-number tokens, e.g. + * ["alpha", 1"]. + * @param major Major version. + * @param minor Minor version. + * @param patch Patch version. + * @param prerelease Prerelease tokens. + */ + SemVer(VersionType major, + VersionType minor, + VersionType patch, + std::vector prerelease); + + /** + * Constructs a SemVer from a major, minor, and patch. + * @param major Major version. + * @param minor Minor version. + * @param patch Patch version. + */ + SemVer(VersionType major, VersionType minor, VersionType patch); + + [[nodiscard]] SemVer::VersionType Major() const; + [[nodiscard]] SemVer::VersionType Minor() const; + [[nodiscard]] SemVer::VersionType Patch() const; + + [[nodiscard]] std::optional> const& Prerelease() const; + + /** + * Attempts to parse a semantic version string, returning std::nullopt on + * failure. Build information is discarded. + * @param value Version string, e.g. "1.2.3-alpha.1". + * @return SemVer on success, or std::nullopt on failure. + */ + [[nodiscard]] static std::optional Parse(std::string const& value); + + private: + VersionType major_; + VersionType minor_; + VersionType patch_; + std::optional> prerelease_; +}; + +bool operator<(SemVer::Token const& lhs, SemVer::Token const& rhs); + +bool operator==(SemVer const& lhs, SemVer const& rhs); + +bool operator<(SemVer const& lhs, SemVer const& rhs); + +bool operator>(SemVer const& lhs, SemVer const& rhs); + +/** Prints a SemVer to an ostream. If the SemVer was parsed from a string + * containing a build string, it will not be present as this information + * is discarded when parsing. + */ +std::ostream& operator<<(std::ostream& out, SemVer const& sv); + +std::ostream& operator<<(std::ostream& out, SemVer::Token const& sv); + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp b/libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp new file mode 100644 index 000000000..57389a6a6 --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/timestamp_operations.cpp @@ -0,0 +1,55 @@ +#include "timestamp_operations.hpp" + +#include "timestamp.h" + +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +std::optional MillisecondsToTimepoint(double ms); + +std::optional RFC3339ToTimepoint(std::string const& timestamp); + +std::optional ToTimepoint(Value const& value) { + if (value.Type() == Value::Type::kNumber) { + double const epoch_ms = value.AsDouble(); + return MillisecondsToTimepoint(epoch_ms); + } + if (value.Type() == Value::Type::kString) { + std::string const& rfc3339_timestamp = value.AsString(); + return RFC3339ToTimepoint(rfc3339_timestamp); + } + return std::nullopt; +} + +std::optional MillisecondsToTimepoint(double ms) { + if (ms < 0.0) { + return std::nullopt; + } + if (std::trunc(ms) == ms) { + return std::chrono::system_clock::time_point{ + std::chrono::milliseconds{static_cast(ms)}}; + } + return std::nullopt; +} + +std::optional RFC3339ToTimepoint(std::string const& timestamp) { + if (timestamp.empty()) { + return std::nullopt; + } + + timestamp_t ts{}; + if (timestamp_parse(timestamp.c_str(), timestamp.size(), &ts)) { + return std::nullopt; + } + + Timepoint epoch{}; + epoch += std::chrono::seconds{ts.sec}; + epoch += + std::chrono::floor(std::chrono::nanoseconds{ts.nsec}); + + return epoch; +} + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp b/libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp new file mode 100644 index 000000000..5c25b84dd --- /dev/null +++ b/libs/server-sdk/src/evaluation/detail/timestamp_operations.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::evaluation::detail { + +using Clock = std::chrono::system_clock; +using Timepoint = Clock::time_point; + +[[nodiscard]] std::optional ToTimepoint(Value const& value); + +} // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/evaluation_error.cpp b/libs/server-sdk/src/evaluation/evaluation_error.cpp new file mode 100644 index 000000000..7f81c02f2 --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluation_error.cpp @@ -0,0 +1,62 @@ +#include "evaluation_error.hpp" +#include + +namespace launchdarkly::server_side::evaluation { + +Error::Error(char const* format, std::string arg) + : format_{format}, arg_{std::move(arg)} {} + +Error::Error(char const* format, std::int64_t arg) + : Error(format, std::to_string(arg)) {} + +Error::Error(char const* msg) : format_{msg}, arg_{std::nullopt} {} + +Error Error::CyclicSegmentReference(std::string segment_key) { + return { + "segment rule referencing segment \"%1%\" caused a circular " + "reference; this is probably a temporary condition due to an " + "incomplete update", + std::move(segment_key)}; +} + +Error Error::CyclicPrerequisiteReference(std::string prereq_key) { + return { + "prerequisite relationship to \"%1%\" caused a circular " + "reference; this is probably a temporary condition due to an " + "incomplete update", + std::move(prereq_key)}; +} + +Error Error::MissingSalt(std::string key) { + return {"\"%1%\" is missing a salt", std::move(key)}; +} + +Error Error::RolloutMissingVariations() { + return {"rollout or experiment with no variations"}; +} + +Error Error::InvalidAttributeReference(std::string ref) { + return {"invalid attribute reference: \"%1%\"", std::move(ref)}; +} + +Error Error::NonexistentVariationIndex(std::int64_t index) { + return { + "rule, fallthrough, or target referenced a nonexistent variation index " + "(%1%)", + index}; +} + +std::ostream& operator<<(std::ostream& out, Error const& err) { + if (err.arg_ == std::nullopt) { + out << err.format_; + } else { + out << boost::format(err.format_) % *err.arg_; + } + return out; +} + +bool operator==(Error const& lhs, Error const& rhs) { + return lhs.format_ == rhs.format_ && lhs.arg_ == rhs.arg_; +} + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/evaluation_error.hpp b/libs/server-sdk/src/evaluation/evaluation_error.hpp new file mode 100644 index 000000000..30a9c60f0 --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluation_error.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::evaluation { + +class Error { + public: + static Error CyclicSegmentReference(std::string segment_key); + static Error CyclicPrerequisiteReference(std::string prereq_key); + static Error InvalidAttributeReference(std::string ref); + static Error RolloutMissingVariations(); + static Error NonexistentVariationIndex(std::int64_t index); + static Error MissingSalt(std::string item_key); + + friend std::ostream& operator<<(std::ostream& out, Error const& arr); + friend bool operator==(Error const& lhs, Error const& rhs); + + private: + Error(char const* format, std::string arg); + Error(char const* format, std::int64_t arg); + Error(char const* msg); + + char const* format_; + std::optional arg_; +}; + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp new file mode 100644 index 000000000..f1d1cc4b2 --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -0,0 +1,212 @@ +#include "evaluator.hpp" +#include "rules.hpp" + +#include +#include + +#include +#include + +namespace launchdarkly::server_side::evaluation { + +using namespace data_model; + +std::optional AnyTargetMatchVariation( + launchdarkly::Context const& context, + Flag const& flag); + +std::optional TargetMatchVariation( + launchdarkly::Context const& context, + Flag::Target const& target); + +Evaluator::Evaluator(Logger& logger, data_store::IDataStore const& store) + : logger_(logger), store_(store), stack_() {} + +EvaluationDetail Evaluator::Evaluate( + Flag const& flag, + launchdarkly::Context const& context) const { + return Evaluate("", flag, context); +} + +EvaluationDetail Evaluator::Evaluate( + std::string const& parent_key, + Flag const& flag, + launchdarkly::Context const& context) const { + if (auto guard = stack_.NoticePrerequisite(flag.key)) { + if (!flag.on) { + return OffValue(flag, EvaluationReason::Off()); + } + + for (Flag::Prerequisite const& p : flag.prerequisites) { + std::shared_ptr maybe_flag = + store_.GetFlag(p.key); + + if (!maybe_flag) { + return OffValue(flag, + EvaluationReason::PrerequisiteFailed(p.key)); + } + + data_store::FlagDescriptor const& descriptor = *maybe_flag; + + if (!descriptor.item) { + // This flag existed at some point, but has since been deleted. + return OffValue(flag, + EvaluationReason::PrerequisiteFailed(p.key)); + } + + // Recursive call; cycles are detected by the guard. + EvaluationDetail detailed_evaluation = + Evaluate(flag.key, *descriptor.item, context); + + if (detailed_evaluation.IsError()) { + return detailed_evaluation; + } + + std::optional variation_index = + detailed_evaluation.VariationIndex(); + + // TODO(209589) prerequisite events. + + if (!descriptor.item->on || variation_index != p.variation) { + return OffValue(flag, + EvaluationReason::PrerequisiteFailed(p.key)); + } + } + } else { + LogError(parent_key, Error::CyclicPrerequisiteReference(flag.key)); + return OffValue(flag, EvaluationReason::MalformedFlag()); + } + + // If the flag is on, all prerequisites are on and valid, then + // determine if the context matches any targets. + // + // This happens before rule evaluation to ensure targets always have + // priority. + + if (auto variation_index = AnyTargetMatchVariation(context, flag)) { + return FlagVariation(flag, *variation_index, + EvaluationReason::TargetMatch()); + } + + for (std::size_t rule_index = 0; rule_index < flag.rules.size(); + rule_index++) { + auto const& rule = flag.rules[rule_index]; + + tl::expected rule_match = + Match(rule, context, store_, stack_); + + if (!rule_match) { + LogError(flag.key, rule_match.error()); + return EvaluationReason::MalformedFlag(); + } + + if (!(rule_match.value())) { + continue; + } + + tl::expected result = + Variation(rule.variationOrRollout, flag.key, context, flag.salt); + + if (!result) { + LogError(flag.key, result.error()); + return EvaluationReason::MalformedFlag(); + } + + EvaluationReason reason = EvaluationReason::RuleMatch( + rule_index, rule.id, result->InExperiment()); + + return FlagVariation(flag, result->VariationIndex(), std::move(reason)); + } + + // If there were no rule matches, then return the fallthrough variation. + + tl::expected result = + Variation(flag.fallthrough, flag.key, context, flag.salt); + + if (!result) { + LogError(flag.key, result.error()); + return EvaluationReason::MalformedFlag(); + } + + EvaluationReason reason = + EvaluationReason::Fallthrough(result->InExperiment()); + + return FlagVariation(flag, result->VariationIndex(), std::move(reason)); +} + +EvaluationDetail Evaluator::FlagVariation( + Flag const& flag, + Flag::Variation variation_index, + EvaluationReason reason) const { + if (variation_index >= flag.variations.size()) { + LogError(flag.key, Error::NonexistentVariationIndex(variation_index)); + return EvaluationReason::MalformedFlag(); + } + + return {flag.variations.at(variation_index), variation_index, + std::move(reason)}; +} + +EvaluationDetail Evaluator::OffValue(Flag const& flag, + EvaluationReason reason) const { + if (flag.offVariation) { + return FlagVariation(flag, *flag.offVariation, std::move(reason)); + } + + return reason; +} + +std::optional AnyTargetMatchVariation( + launchdarkly::Context const& context, + Flag const& flag) { + if (flag.contextTargets.empty()) { + for (auto const& target : flag.targets) { + if (auto index = TargetMatchVariation(context, target)) { + return index; + } + } + return std::nullopt; + } + + for (auto const& context_target : flag.contextTargets) { + if (IsUser(context_target.contextKind) && + context_target.values.empty()) { + for (auto const& target : flag.targets) { + if (target.variation == context_target.variation) { + if (auto index = TargetMatchVariation(context, target)) { + return index; + } + } + } + } else if (auto index = TargetMatchVariation(context, context_target)) { + return index; + } + } + + return std::nullopt; +} + +std::optional TargetMatchVariation( + launchdarkly::Context const& context, + Flag::Target const& target) { + Value const& key = context.Get(target.contextKind, "key"); + if (key.IsNull()) { + return std::nullopt; + } + + for (auto const& value : target.values) { + if (value == key) { + return target.variation; + } + } + + return std::nullopt; +} + +void Evaluator::LogError(std::string const& key, Error const& error) const { + LD_LOG(logger_, LogLevel::kError) + << "Invalid flag configuration detected in flag \"" << key + << "\": " << error; +} + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp new file mode 100644 index 000000000..d2db9603c --- /dev/null +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../data_store/data_store.hpp" +#include "bucketing.hpp" +#include "detail/evaluation_stack.hpp" +#include "evaluation_error.hpp" + +#include + +namespace launchdarkly::server_side::evaluation { + +class Evaluator { + public: + Evaluator(Logger& logger, data_store::IDataStore const& store); + + [[nodiscard]] EvaluationDetail Evaluate( + data_model::Flag const& flag, + launchdarkly::Context const& context) const; + + private: + [[nodiscard]] EvaluationDetail Evaluate( + std::string const& parent_key, + data_model::Flag const& flag, + launchdarkly::Context const& context) const; + + [[nodiscard]] EvaluationDetail FlagVariation( + data_model::Flag const& flag, + data_model::Flag::Variation variation_index, + EvaluationReason reason) const; + + [[nodiscard]] EvaluationDetail OffValue( + data_model::Flag const& flag, + EvaluationReason reason) const; + + void LogError(std::string const& key, Error const& error) const; + + Logger& logger_; + data_store::IDataStore const& store_; + mutable detail::EvaluationStack stack_; +}; +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/operators.cpp b/libs/server-sdk/src/evaluation/operators.cpp new file mode 100644 index 000000000..d5c98edb4 --- /dev/null +++ b/libs/server-sdk/src/evaluation/operators.cpp @@ -0,0 +1,149 @@ +#include "operators.hpp" +#include "detail/semver_operations.hpp" +#include "detail/timestamp_operations.hpp" + +#include + +namespace launchdarkly::server_side::evaluation::operators { + +template +bool StringOp(Value const& context_value, + Value const& clause_value, + Callable&& op) { + if (!(context_value.Type() == Value::Type::kString && + clause_value.Type() == Value::Type::kString)) { + return false; + } + return op(context_value.AsString(), clause_value.AsString()); +} + +template +bool SemverOp(Value const& context_value, + Value const& clause_value, + Callable&& op) { + return StringOp(context_value, clause_value, + [op = std::move(op)](std::string const& context, + std::string const& clause) { + auto context_semver = detail::SemVer::Parse(context); + if (!context_semver) { + return false; + } + + auto clause_semver = detail::SemVer::Parse(clause); + if (!clause_semver) { + return false; + } + + return op(*context_semver, *clause_semver); + }); +} + +template +bool TimeOp(Value const& context_value, + Value const& clause_value, + Callable&& op) { + auto context_tp = detail::ToTimepoint(context_value); + if (!context_tp) { + return false; + } + auto clause_tp = detail::ToTimepoint(clause_value); + if (!clause_tp) { + return false; + } + return op(*context_tp, *clause_tp); +} + +bool StartsWith(std::string const& context_value, + std::string const& clause_value) { + return clause_value.size() <= context_value.size() && + std::equal(clause_value.begin(), clause_value.end(), + context_value.begin()); +} + +bool EndsWith(std::string const& context_value, + std::string const& clause_value) { + return clause_value.size() <= context_value.size() && + std::equal(clause_value.rbegin(), clause_value.rend(), + context_value.rbegin()); +} + +bool Contains(std::string const& context_value, + std::string const& clause_value) { + return context_value.find(clause_value) != std::string::npos; +} + +/* RegexMatch uses boost::regex instead of std::regex, because the former + * appears to be significantly more performant according to boost benchmarks. + * For more information, see here: + * https://www.boost.org/doc/libs/1_82_0/libs/regex/doc/html/boost_regex/background/performance.html + */ +bool RegexMatch(std::string const& context_value, + std::string const& clause_value) { + /* See here for FAQ on boost::regex exceptions: + * https:// + * www.boost.org/doc/libs/1_82_0/libs/regex/doc/html/boost_regex/background/faq.html + * */ + try { + return boost::regex_search(context_value, boost::regex(clause_value)); + } catch (boost::bad_expression) { + // boost::bad_expression can be thrown by basic_regex when compiling a + // regular expression. + return false; + } catch (boost::regex_error) { + // boost::regex_error thrown on stack exhaustion + return false; + } catch (std::runtime_error) { + // std::runtime_error can be thrown when a call + // to regex_search results in an "everlasting" search + return false; + } +} + +bool Match(data_model::Clause::Op op, + Value const& context_value, + Value const& clause_value) { + switch (op) { + case data_model::Clause::Op::kIn: + return context_value == clause_value; + case data_model::Clause::Op::kStartsWith: + return StringOp(context_value, clause_value, StartsWith); + case data_model::Clause::Op::kEndsWith: + return StringOp(context_value, clause_value, EndsWith); + case data_model::Clause::Op::kMatches: + return StringOp(context_value, clause_value, RegexMatch); + case data_model::Clause::Op::kContains: + return StringOp(context_value, clause_value, Contains); + case data_model::Clause::Op::kLessThan: + return context_value < clause_value; + case data_model::Clause::Op::kLessThanOrEqual: + return context_value <= clause_value; + case data_model::Clause::Op::kGreaterThan: + return context_value > clause_value; + case data_model::Clause::Op::kGreaterThanOrEqual: + return context_value >= clause_value; + case data_model::Clause::Op::kBefore: + return TimeOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs < rhs; }); + case data_model::Clause::Op::kAfter: + return TimeOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs > rhs; }); + case data_model::Clause::Op::kSemVerEqual: + return SemverOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs == rhs; }); + case data_model::Clause::Op::kSemVerLessThan: + return SemverOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs < rhs; }); + case data_model::Clause::Op::kSemVerGreaterThan: + return SemverOp( + context_value, clause_value, + [](auto const& lhs, auto const& rhs) { return lhs > rhs; }); + default: + return false; + } +} + +} // namespace launchdarkly::server_side::evaluation::operators diff --git a/libs/server-sdk/src/evaluation/operators.hpp b/libs/server-sdk/src/evaluation/operators.hpp new file mode 100644 index 000000000..159c1d614 --- /dev/null +++ b/libs/server-sdk/src/evaluation/operators.hpp @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace launchdarkly::server_side::evaluation::operators { + +bool Match(data_model::Clause::Op op, + Value const& context_value, + Value const& clause_value); + +} // namespace launchdarkly::server_side::evaluation::operators diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp new file mode 100644 index 000000000..b43a0ef89 --- /dev/null +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -0,0 +1,220 @@ +#include "rules.hpp" +#include "bucketing.hpp" +#include "operators.hpp" + +namespace launchdarkly::server_side::evaluation { + +using namespace data_model; + +bool MaybeNegate(Clause const& clause, bool value) { + if (clause.negate) { + return !value; + } + return value; +} + +tl::expected Match(Flag::Rule const& rule, + launchdarkly::Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + for (Clause const& clause : rule.clauses) { + tl::expected result = Match(clause, context, store, stack); + if (!result) { + return result; + } + if (!(result.value())) { + return false; + } + } + return true; +} + +tl::expected Match(Segment::Rule const& rule, + Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack, + std::string const& key, + std::string const& salt) { + for (Clause const& clause : rule.clauses) { + auto maybe_match = Match(clause, context, store, stack); + if (!maybe_match) { + return tl::make_unexpected(maybe_match.error()); + } + if (!(maybe_match.value())) { + return false; + } + } + + if (rule.weight && rule.weight >= 0.0) { + BucketPrefix prefix(key, salt); + auto maybe_bucket = Bucket(context, rule.bucketBy, prefix, false, + rule.rolloutContextKind); + if (!maybe_bucket) { + return tl::make_unexpected(maybe_bucket.error()); + } + auto [bucket, ignored] = *maybe_bucket; + return bucket < (*rule.weight / kBucketScale); + } + + return true; +} + +tl::expected Match(Clause const& clause, + launchdarkly::Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + if (clause.op == Clause::Op::kSegmentMatch) { + return MatchSegment(clause, context, store, stack); + } + return MatchNonSegment(clause, context); +} + +tl::expected MatchSegment(Clause const& clause, + launchdarkly::Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + for (Value const& value : clause.values) { + // A segment key represented as a Value is a string; non-strings are + // ignored. + if (value.Type() != Value::Type::kString) { + continue; + } + + std::string const& segment_key = value.AsString(); + + std::shared_ptr segment_ptr = + store.GetSegment(segment_key); + + if (!segment_ptr || !segment_ptr->item) { + // Segments that don't exist are ignored. + continue; + } + + auto maybe_contains = + Contains(*segment_ptr->item, context, store, stack); + + if (!maybe_contains) { + return tl::make_unexpected(maybe_contains.error()); + } + + if (maybe_contains.value()) { + return MaybeNegate(clause, true); + } + } + + return MaybeNegate(clause, false); +} + +tl::expected MatchNonSegment( + Clause const& clause, + launchdarkly::Context const& context) { + if (!clause.attribute.Valid()) { + return tl::make_unexpected( + Error::InvalidAttributeReference(clause.attribute.RedactionName())); + } + + if (clause.attribute.IsKind()) { + for (auto const& clause_value : clause.values) { + for (auto const& kind : context.Kinds()) { + if (operators::Match(clause.op, kind, clause_value)) { + return MaybeNegate(clause, true); + } + } + } + return MaybeNegate(clause, false); + } + + Value const& attribute = context.Get(clause.contextKind, clause.attribute); + if (attribute.IsNull()) { + return false; + } + + if (attribute.IsArray()) { + for (Value const& clause_value : clause.values) { + for (Value const& context_value : attribute.AsArray()) { + if (operators::Match(clause.op, context_value, clause_value)) { + return MaybeNegate(clause, true); + } + } + } + return MaybeNegate(clause, false); + } + + if (std::any_of(clause.values.begin(), clause.values.end(), + [&](Value const& clause_value) { + return operators::Match(clause.op, attribute, + clause_value); + })) { + return MaybeNegate(clause, true); + } + + return MaybeNegate(clause, false); +} + +tl::expected Contains(Segment const& segment, + Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack) { + auto guard = stack.NoticeSegment(segment.key); + if (!guard) { + return tl::make_unexpected(Error::CyclicSegmentReference(segment.key)); + } + + if (segment.unbounded) { + // TODO(sc209881): set big segment status to NOT_CONFIGURED. + return false; + } + + if (IsTargeted(context, segment.included, segment.includedContexts)) { + return true; + } + + if (IsTargeted(context, segment.excluded, segment.excludedContexts)) { + return false; + } + + for (auto const& rule : segment.rules) { + if (!segment.salt) { + return tl::make_unexpected(Error::MissingSalt(segment.key)); + } + tl::expected maybe_match = + Match(rule, context, store, stack, segment.key, *segment.salt); + if (!maybe_match) { + return tl::make_unexpected(maybe_match.error()); + } + if (maybe_match.value()) { + return true; + } + } + + return false; +} + +bool IsTargeted(Context const& context, + std::vector const& keys, + std::vector const& targets) { + if (IsUser(context) && targets.empty()) { + return std::find(keys.begin(), keys.end(), context.CanonicalKey()) != + keys.end(); + } + + for (auto const& target : targets) { + Value const& key = context.Get(target.contextKind, "key"); + if (!key.IsString()) { + continue; + } + if (std::find(target.values.begin(), target.values.end(), key) != + target.values.end()) { + return true; + } + } + + return false; +} + +bool IsUser(Context const& context) { + auto const& kinds = context.Kinds(); + return kinds.size() == 1 && kinds[0] == "user"; +} + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/rules.hpp b/libs/server-sdk/src/evaluation/rules.hpp new file mode 100644 index 000000000..5ca9a974e --- /dev/null +++ b/libs/server-sdk/src/evaluation/rules.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include + +#include "../data_store/data_store.hpp" +#include "detail/evaluation_stack.hpp" +#include "evaluation_error.hpp" + +#include + +#include + +namespace launchdarkly::server_side::evaluation { + +[[nodiscard]] tl::expected Match( + data_model::Flag::Rule const&, + Context const&, + data_store::IDataStore const& store, + detail::EvaluationStack& stack); + +[[nodiscard]] tl::expected Match(data_model::Clause const&, + Context const&, + data_store::IDataStore const&, + detail::EvaluationStack&); + +[[nodiscard]] tl::expected Match( + data_model::Segment::Rule const& rule, + Context const& context, + data_store::IDataStore const& store, + detail::EvaluationStack& stack, + std::string const& key, + std::string const& salt); + +[[nodiscard]] tl::expected MatchSegment( + data_model::Clause const&, + Context const&, + data_store::IDataStore const&, + detail::EvaluationStack& stack); + +[[nodiscard]] tl::expected MatchNonSegment( + data_model::Clause const&, + Context const&); + +[[nodiscard]] tl::expected Contains( + data_model::Segment const&, + Context const&, + data_store::IDataStore const& store, + detail::EvaluationStack& stack); + +[[nodiscard]] bool MaybeNegate(data_model::Clause const& clause, bool value); + +[[nodiscard]] bool IsTargeted(Context const&, + std::vector const&, + std::vector const&); + +[[nodiscard]] bool IsUser(Context const& context); + +} // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index f8918cd3f..707abb839 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -13,7 +13,8 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") add_executable(gtest_${LIBNAME} - ${tests}) -target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse GTest::gtest_main) + ${tests} + ) +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/bucketing_tests.cpp b/libs/server-sdk/tests/bucketing_tests.cpp new file mode 100644 index 000000000..23d63b4ae --- /dev/null +++ b/libs/server-sdk/tests/bucketing_tests.cpp @@ -0,0 +1,367 @@ +#include "evaluation/bucketing.hpp" +#include "evaluation/evaluator.hpp" + +#include +#include + +using namespace launchdarkly; +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::evaluation; +using WeightedVariation = Flag::Rollout::WeightedVariation; + +/** + * Note: These tests are meant to be exact duplicates of tests + * in other SDKs. Do not change any of the values unless they + * are also changed in other SDKs. These are not traditional behavioral + * tests so much as consistency tests to guarantee that the implementation + * is identical across SDKs. + * + * Tests in this file may derive from BucketingConsistencyTests to gain access + * to shared constants. + */ +class BucketingConsistencyTests : public ::testing::Test { + public: + // Bucket results must be no more than this distance from the expected + // value. + double const kBucketTolerance = 0.0000001; + const std::string kHashKey = "hashKey"; + const std::string kSalt = "saltyA"; +}; + +TEST_F(BucketingConsistencyTests, BucketContextByKey) { + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto tests = + std::vector>{{"userKeyA", 0.42157587}, + {"userKeyB", 0.6708485}, + {"userKeyC", 0.10343106}}; + + for (auto [key, bucket] : tests) { + auto context = ContextBuilder().Kind("user", key).Build(); + auto result = Bucket(context, "key", kPrefix, false, "user"); + ASSERT_TRUE(result) + << key << " should be bucketed but got " << result.error(); + + ASSERT_NEAR(result->first, bucket, kBucketTolerance); + } +} + +TEST_F(BucketingConsistencyTests, BucketContextByKeyWithSeed) { + const BucketPrefix kPrefix{61}; + + auto tests = + std::vector>{{"userKeyA", 0.09801207}, + {"userKeyB", 0.14483777}, + {"userKeyC", 0.9242641}}; + + for (auto [key, bucket] : tests) { + auto context = ContextBuilder().Kind("user", key).Build(); + auto result = Bucket(context, "key", kPrefix, false, "user"); + ASSERT_TRUE(result) + << key << " should be bucketed but got " << result.error(); + + ASSERT_NEAR(result->first, bucket, kBucketTolerance); + + auto result_different_seed = + Bucket(context, "key", BucketPrefix{60}, false, "user"); + ASSERT_TRUE(result_different_seed) + << key << " should be bucketed but got " + << result_different_seed.error(); + + ASSERT_NE(result_different_seed->first, result->first); + } +} + +TEST_F(BucketingConsistencyTests, BucketContextByInvalidReference) { + const BucketPrefix kPrefix{kHashKey, kSalt}; + const AttributeReference kInvalidRef; + + ASSERT_FALSE(kInvalidRef.Valid()); + + auto context = ContextBuilder().Kind("user", "userKeyA").Build(); + auto result = Bucket(context, kInvalidRef, kPrefix, false, "user"); + + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), + Error::InvalidAttributeReference(kInvalidRef.RedactionName())); +} + +TEST_F(BucketingConsistencyTests, BucketContextByIntAttribute) { + const std::string kUserKey = "userKeyD"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = + ContextBuilder().Kind("user", kUserKey).Set("intAttr", 33'333).Build(); + auto result = Bucket(context, "intAttr", kPrefix, false, "user"); + + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.54771423, kBucketTolerance); +} + +TEST_F(BucketingConsistencyTests, BucketContextByStringifiedIntAttribute) { + const std::string kUserKey = "userKeyD"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = ContextBuilder() + .Kind("user", kUserKey) + .Set("stringAttr", "33333") + .Build(); + auto result = Bucket(context, "stringAttr", kPrefix, false, "user"); + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.54771423, kBucketTolerance); +} + +TEST_F(BucketingConsistencyTests, BucketContextByFloatAttributeNotAllowed) { + const std::string kUserKey = "userKeyE"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = ContextBuilder() + .Kind("user", kUserKey) + .Set("floatAttr", 999.999) + .Build(); + auto result = Bucket(context, "floatAttr", kPrefix, false, "user"); + + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.0, kBucketTolerance); +} + +TEST_F(BucketingConsistencyTests, BucketContextByFloatAttributeThatIsInteger) { + const std::string kUserKey = "userKeyE"; + const BucketPrefix kPrefix{kHashKey, kSalt}; + + auto context = ContextBuilder() + .Kind("user", kUserKey) + .Set("floatAttr", 33333.0) + .Build(); + auto result = Bucket(context, "floatAttr", kPrefix, false, "user"); + + ASSERT_TRUE(result) << kUserKey << " should be bucketed but got " + << result.error(); + ASSERT_NEAR(result->first, 0.54771423, kBucketTolerance); +} + +// Parameterized tests may be instantiated with one or more BucketTests for +// convenience. +struct BucketTest { + // Context key. + std::string key; + // Expected bucket value as a string; this is only used for printing on + // error. + std::string expectedBucket; + // Expected computed variation index. + Flag::Variation expectedVariation; + // Whether the context was determined to be in an experiment. + bool expectedInExperiment; + // The rollout used for the test, which may be a percent rollout or an + // experiment. + Flag::Rollout rollout; +}; + +#define IN_EXPERIMENT true +#define NOT_IN_EXPERIMENT false + +class BucketVariationTest : public BucketingConsistencyTests, + public ::testing::WithParamInterface {}; + +static Flag::Rollout PercentRollout() { + WeightedVariation wv0(0, 60'000); + WeightedVariation wv1(1, 40'000); + return Flag::Rollout({wv0, wv1}); +} + +static Flag::Rollout ExperimentRollout() { + auto wv0 = WeightedVariation(0, 10'000); + auto wv1 = WeightedVariation(1, 20'000); + auto wv0_untracked = WeightedVariation::Untracked(0, 70'000); + Flag::Rollout rollout({wv0, wv1, wv0_untracked}); + rollout.kind = Flag::Rollout::Kind::kExperiment; + rollout.seed = 61; + return rollout; +} + +static Flag::Rollout IncompleteWeighting() { + WeightedVariation wv0(0, 1); + WeightedVariation wv1(1, 2); + WeightedVariation wv2(2, 3); + + return Flag::Rollout({wv0, wv1, wv2}); +} + +TEST_P(BucketVariationTest, VariationIndexForContext) { + auto const& param = GetParam(); + + auto result = + Variation(param.rollout, kHashKey, + ContextBuilder().Kind("user", param.key).Build(), kSalt); + + ASSERT_TRUE(result) << param.key + << " should be assigned a bucket result, but got: " + << result.error(); + + ASSERT_EQ(result->VariationIndex(), param.expectedVariation) + << param.key << " (bucket " << param.expectedBucket + << ") should get variation " << param.expectedVariation << ", but got " + << result->VariationIndex(); + + ASSERT_EQ(result->InExperiment(), param.expectedInExperiment) + << param.key << " " + << (param.expectedInExperiment ? "should" : "should not") + << " be in experiment"; +} + +INSTANTIATE_TEST_SUITE_P( + PercentRollout, + BucketVariationTest, + ::testing::ValuesIn({BucketTest{"userKeyA", "0.42157587", 0, + NOT_IN_EXPERIMENT, PercentRollout()}, + BucketTest{"userKeyB", "0.6708485", 1, + NOT_IN_EXPERIMENT, PercentRollout()}, + BucketTest{"userKeyC", "0.10343106", 0, + NOT_IN_EXPERIMENT, PercentRollout()}})); + +INSTANTIATE_TEST_SUITE_P(ExperimentRollout, + BucketVariationTest, + ::testing::ValuesIn({ + BucketTest{"userKeyA", "0.09801207", 0, + IN_EXPERIMENT, ExperimentRollout()}, + BucketTest{"userKeyB", "0.14483777", 1, + IN_EXPERIMENT, ExperimentRollout()}, + BucketTest{"userKeyC", "0.9242641", 0, + NOT_IN_EXPERIMENT, ExperimentRollout()}, + })); + +INSTANTIATE_TEST_SUITE_P(IncompleteWeightingDefaultsToLastVariation, + BucketVariationTest, + ::testing::ValuesIn({ + BucketTest{"userKeyD", "0.7816281", 2, + NOT_IN_EXPERIMENT, + IncompleteWeighting()}, + })); + +#undef IN_EXPERIMENT +#undef NOT_IN_EXPERIMENT + +TEST_F(BucketingConsistencyTests, VariationIndexForContextWithCustomAttribute) { + WeightedVariation wv0(0, 60'000); + WeightedVariation wv1(1, 40'000); + + Flag::Rollout rollout({wv0, wv1}); + rollout.bucketBy = "intAttr"; + + auto tests = std::vector>{ + {33'333, 0, "0.54771423"}, {99'999, 1, "0.7309658"}}; + + for (auto [bucketAttr, expectedVariation, expectedBucket] : tests) { + auto result = Variation(rollout, kHashKey, + ContextBuilder() + .Kind("user", "userKeyA") + .Set("intAttr", bucketAttr) + .Build(), + kSalt); + + ASSERT_TRUE(result) + << "userKeyA should be assigned a bucket result, but got: " + << result.error(); + + ASSERT_EQ(result->VariationIndex(), expectedVariation) + << "userKeyA (bucket " << expectedBucket + << ") should get variation " << expectedVariation << ", but got " + << result->VariationIndex(); + + ASSERT_EQ(result->InExperiment(), false) + << "userKeyA should not be in experiment"; + } +} + +struct ExperimentBucketTest { + std::optional seed; + std::string key; + Flag::Variation expectedVariationIndex; +}; + +class ExperimentBucketingTests + : public BucketingConsistencyTests, + public ::testing::WithParamInterface {}; + +TEST_P(ExperimentBucketingTests, VariationIndexForExperiment) { + auto const& params = GetParam(); + + WeightedVariation wv0(0, 10'000); + WeightedVariation wv1(1, 20'000); + WeightedVariation wv2(2, 70'000); + + Flag::Rollout rollout({wv0, wv1, wv2}); + rollout.bucketBy = "numberAttr"; + rollout.kind = Flag::Rollout::Kind::kExperiment; + rollout.seed = params.seed; + + auto result = Variation(rollout, kHashKey, + ContextBuilder() + .Kind("user", params.key) + .Set("numberAttr", 0.6708485) + .Build(), + kSalt); + + ASSERT_TRUE(result); + + ASSERT_EQ(result->VariationIndex(), params.expectedVariationIndex) + << params.key << " with seed " + << (params.seed ? std::to_string(*params.seed) : "(none)") + << " should get variation " << params.expectedVariationIndex; +} + +INSTANTIATE_TEST_SUITE_P( + VariationsForSeedsAndKeys, + ExperimentBucketingTests, + ::testing::ValuesIn({ + ExperimentBucketTest{std::nullopt, "userKeyA", 2}, // 0.42157587, + ExperimentBucketTest{std::nullopt, "userKeyB", 2}, // 0.6708485, + ExperimentBucketTest{std::nullopt, "userKeyC", 1}, // 0.10343106, + ExperimentBucketTest{61, "userKeyA", 0}, // 0.09801207, + ExperimentBucketTest{61, "userKeyB", 1}, // 0.14483777, + ExperimentBucketTest{61, "userKeyC", 2} // 0.9242641, + })); + +TEST_F(BucketingConsistencyTests, + BucketValueBeyondLastBucketIsPinnedToLastBucket) { + WeightedVariation wv0(0, 5'000); + WeightedVariation wv1(1, 5'000); + + Flag::Rollout rollout({wv0, wv1}); + rollout.seed = 61; + + auto context = ContextBuilder() + .Kind("user", "userKeyD") + .Set("intAttr", 99'999) + .Build(); + + auto result = Variation(rollout, kHashKey, context, kSalt); + + ASSERT_TRUE(result); + ASSERT_EQ(result->VariationIndex(), 1); + ASSERT_FALSE(result->InExperiment()); +} + +TEST_F(BucketingConsistencyTests, + BucketValueBeyongLastBucketIsPinnedToLastBucketForExperiment) { + WeightedVariation wv0(0, 5'000); + WeightedVariation wv1(1, 5'000); + + Flag::Rollout rollout({wv0, wv1}); + rollout.seed = 61; + rollout.kind = Flag::Rollout::Kind::kExperiment; + + auto context = ContextBuilder() + .Kind("user", "userKeyD") + .Set("intAttr", 99'999) + .Build(); + + auto result = Variation(rollout, kHashKey, context, kSalt); + + ASSERT_TRUE(result); + ASSERT_EQ(result->VariationIndex(), 1); + ASSERT_TRUE(result->InExperiment()); +} diff --git a/libs/server-sdk/tests/evaluation_stack_test.cpp b/libs/server-sdk/tests/evaluation_stack_test.cpp new file mode 100644 index 000000000..23193c8df --- /dev/null +++ b/libs/server-sdk/tests/evaluation_stack_test.cpp @@ -0,0 +1,53 @@ +#include + +#include "evaluation/detail/evaluation_stack.hpp" + +using namespace launchdarkly::server_side::evaluation::detail; + +TEST(EvalStackTests, SegmentIsNoticed) { + EvaluationStack stack; + auto g1 = stack.NoticeSegment("foo"); + ASSERT_TRUE(g1); + ASSERT_FALSE(stack.NoticeSegment("foo")); +} + +TEST(EvalStackTests, PrereqIsNoticed) { + EvaluationStack stack; + auto g1 = stack.NoticePrerequisite("foo"); + ASSERT_TRUE(g1); + ASSERT_FALSE(stack.NoticePrerequisite("foo")); +} + +TEST(EvalStackTests, NestedScopes) { + EvaluationStack stack; + { + auto g1 = stack.NoticeSegment("foo"); + ASSERT_TRUE(g1); + ASSERT_FALSE(stack.NoticeSegment("foo")); + { + auto g2 = stack.NoticeSegment("bar"); + ASSERT_TRUE(g2); + ASSERT_FALSE(stack.NoticeSegment("bar")); + ASSERT_FALSE(stack.NoticeSegment("foo")); + } + ASSERT_TRUE(stack.NoticeSegment("bar")); + } + ASSERT_TRUE(stack.NoticeSegment("foo")); + ASSERT_TRUE(stack.NoticeSegment("bar")); +} + +TEST(EvalStackTests, SegmentAndPrereqHaveSeparateCaches) { + EvaluationStack stack; + auto g1 = stack.NoticeSegment("foo"); + ASSERT_TRUE(g1); + auto g2 = stack.NoticePrerequisite("foo"); + ASSERT_TRUE(g2); +} + +TEST(EvalStackTests, ImmediateDestructionOfGuard) { + EvaluationStack stack; + + ASSERT_TRUE(stack.NoticeSegment("foo")); + ASSERT_TRUE(stack.NoticeSegment("foo")); + ASSERT_TRUE(stack.NoticeSegment("foo")); +} diff --git a/libs/server-sdk/tests/evaluator_tests.cpp b/libs/server-sdk/tests/evaluator_tests.cpp new file mode 100644 index 000000000..b91991cc6 --- /dev/null +++ b/libs/server-sdk/tests/evaluator_tests.cpp @@ -0,0 +1,248 @@ +#include "evaluation/evaluator.hpp" +#include "test_store.hpp" + +#include +#include + +#include "spy_logger.hpp" + +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +/** + * Use if the test does not require inspecting log messages. + */ +class EvaluatorTests : public ::testing::Test { + public: + EvaluatorTests() + : logger_(logging::NullLogger()), + store_(test_store::TestData()), + eval_(logger_, *store_) {} + + private: + Logger logger_; + + protected: + std::unique_ptr store_; + evaluation::Evaluator const eval_; +}; + +/** + * Use if the test requires making assertions based on log messages generated + * during evaluation. + */ +class EvaluatorTestsWithLogs : public ::testing::Test { + public: + EvaluatorTestsWithLogs() + : messages_(std::make_shared()), + logger_(messages_), + store_(test_store::TestData()), + eval_(logger_, *store_) {} + + protected: + std::shared_ptr messages_; + + private: + Logger logger_; + + protected: + std::unique_ptr store_; + evaluation::Evaluator const eval_; +}; + +TEST_F(EvaluatorTests, BasicChanges) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("user", "bob").Build(); + + auto flag = store_->GetFlag("flagWithTarget")->item.value(); + + ASSERT_FALSE(flag.on); + + auto detail = eval_.Evaluate(flag, alice); + + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), EvaluationReason::Off()); + + // flip off variation + flag.offVariation = 1; + detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.VariationIndex(), 1); + ASSERT_EQ(*detail, Value(true)); + + // off variation unspecified + flag.offVariation = std::nullopt; + detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.VariationIndex(), std::nullopt); + ASSERT_EQ(*detail, Value::Null()); + + // flip targeting on + flag.on = true; + detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.VariationIndex(), 1); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), EvaluationReason::TargetMatch()); + + // flip default variation + flag.fallthrough = data_model::Flag::Variation{0}; + detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(*detail, Value(false)); + + // bob's reason should still be TargetMatch even though his value is now the + // default + detail = eval_.Evaluate(flag, bob); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), EvaluationReason::TargetMatch()); +} + +TEST_F(EvaluatorTests, EvaluateWithMatchesOpGroups) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder() + .Kind("user", "bob") + .Set("groups", {"my-group"}) + .Build(); + + auto flag = store_->GetFlag("flagWithMatchesOpOnGroups")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch( + 0, "6a7755ac-e47a-40ea-9579-a09dd5f061bd", false)); +} + +TEST_F(EvaluatorTests, EvaluateWithMatchesOpKinds) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("company", "bob").Build(); + + auto flag = store_->GetFlag("flagWithMatchesOpOnKinds")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch( + 0, "6a7755ac-e47a-40ea-9579-a09dd5f061bd", false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + + auto new_bob = ContextBuilder().Kind("org", "bob").Build(); + detail = eval_.Evaluate(flag, new_bob); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.VariationIndex(), 0); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch( + 0, "6a7755ac-e47a-40ea-9579-a09dd5f061bd", false)); +} + +TEST_F(EvaluatorTestsWithLogs, PrerequisiteCycle) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + + auto flag = store_->GetFlag("cycleFlagA")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_FALSE(detail); + ASSERT_EQ(detail.Reason(), EvaluationReason::MalformedFlag()); + ASSERT_TRUE(messages_->Count(1)); + ASSERT_TRUE(messages_->Contains(0, LogLevel::kError, "circular reference")); +} + +TEST_F(EvaluatorTests, FlagWithSegment) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("user", "bob").Build(); + + auto flag = store_->GetFlag("flagWithSegmentMatchRule")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch(0, "match-rule", false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(true)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); +} + +TEST_F(EvaluatorTests, FlagWithSegmentContainingRules) { + auto alice = ContextBuilder().Kind("user", "alice").Build(); + auto bob = ContextBuilder().Kind("user", "bob").Build(); + + auto flag = + store_->GetFlag("flagWithSegmentMatchesUserAlice")->item.value(); + + auto detail = eval_.Evaluate(flag, alice); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.Reason(), + EvaluationReason::RuleMatch(0, "match-rule", false)); + ASSERT_EQ(*detail, Value(false)); + + detail = eval_.Evaluate(flag, bob); + ASSERT_TRUE(detail); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); + ASSERT_EQ(*detail, Value(true)); +} + +TEST_F(EvaluatorTests, FlagWithExperiment) { + auto user_a = ContextBuilder().Kind("user", "userKeyA").Build(); + auto user_b = ContextBuilder().Kind("user", "userKeyB").Build(); + auto user_c = ContextBuilder().Kind("user", "userKeyC").Build(); + + auto flag = store_->GetFlag("flagWithExperiment")->item.value(); + + auto detail = eval_.Evaluate(flag, user_a); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_TRUE(detail.Reason()->InExperiment()); + + detail = eval_.Evaluate(flag, user_b); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(true)); + ASSERT_TRUE(detail.Reason()->InExperiment()); + + detail = eval_.Evaluate(flag, user_c); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_FALSE(detail.Reason()->InExperiment()); +} + +TEST_F(EvaluatorTests, FlagWithExperimentTargetingMissingContext) { + auto flag = + store_->GetFlag("flagWithExperimentTargetingContext")->item.value(); + + auto user_a = ContextBuilder().Kind("user", "userKeyA").Build(); + + auto detail = eval_.Evaluate(flag, user_a); + ASSERT_TRUE(detail); + ASSERT_EQ(*detail, Value(false)); + ASSERT_EQ(detail.Reason(), EvaluationReason::Fallthrough(false)); +} diff --git a/libs/server-sdk/tests/operator_tests.cpp b/libs/server-sdk/tests/operator_tests.cpp new file mode 100644 index 000000000..727fa66ce --- /dev/null +++ b/libs/server-sdk/tests/operator_tests.cpp @@ -0,0 +1,258 @@ +#include + +#include + +#include "evaluation/evaluator.hpp" +#include "evaluation/operators.hpp" +#include "evaluation/rules.hpp" + +using namespace launchdarkly::server_side::evaluation::operators; +using namespace launchdarkly::data_model; +using namespace launchdarkly; + +TEST(OpTests, StartsWith) { + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "", "")); + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "a", "")); + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "a", "a")); + + EXPECT_TRUE(Match(Clause::Op::kStartsWith, "food", "foo")); + EXPECT_FALSE(Match(Clause::Op::kStartsWith, "foo", "food")); + + EXPECT_FALSE(Match(Clause::Op::kStartsWith, "Food", "foo")); +} + +TEST(OpTests, EndsWith) { + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "", "")); + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "a", "")); + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "a", "a")); + + EXPECT_TRUE(Match(Clause::Op::kEndsWith, "food", "ood")); + EXPECT_FALSE(Match(Clause::Op::kEndsWith, "ood", "food")); + + EXPECT_FALSE(Match(Clause::Op::kEndsWith, "FOOD", "ood")); +} + +TEST(OpTests, NumericComparisons) { + EXPECT_TRUE(Match(Clause::Op::kLessThan, 0, 1)); + EXPECT_FALSE(Match(Clause::Op::kLessThan, 1, 0)); + EXPECT_FALSE(Match(Clause::Op::kLessThan, 0, 0)); + + EXPECT_TRUE(Match(Clause::Op::kGreaterThan, 1, 0)); + EXPECT_FALSE(Match(Clause::Op::kGreaterThan, 0, 1)); + EXPECT_FALSE(Match(Clause::Op::kGreaterThan, 0, 0)); + + EXPECT_TRUE(Match(Clause::Op::kLessThanOrEqual, 0, 1)); + EXPECT_TRUE(Match(Clause::Op::kLessThanOrEqual, 0, 0)); + EXPECT_FALSE(Match(Clause::Op::kLessThanOrEqual, 1, 0)); + + EXPECT_TRUE(Match(Clause::Op::kGreaterThanOrEqual, 1, 0)); + EXPECT_TRUE(Match(Clause::Op::kGreaterThanOrEqual, 0, 0)); + EXPECT_FALSE(Match(Clause::Op::kGreaterThanOrEqual, 0, 1)); +} + +// We can only support microsecond precision due to resolution of the +// system_clock::time_point. +// +// The spec says we should support no more than 9 digits +// (nanoseconds.) This test attempts to verify that microsecond precision +// differences are handled. +TEST(OpTests, DateComparisonMicrosecondPrecision) { + auto dates = std::vector>{ + // Using Zulu suffix. + {"2023-10-08T02:00:00.000001Z", "2023-10-08T02:00:00.000002Z"}, + // Using offset suffix. + {"2023-10-08T02:00:00.000001+00:00", + "2023-10-08T02:00:00.000002+00:00"}}; + + for (auto const& [date1, date2] : dates) { + EXPECT_TRUE(Match(Clause::Op::kBefore, date1, date2)) + << date1 << " < " << date2; + + EXPECT_FALSE(Match(Clause::Op::kAfter, date1, date2)) + << date1 << " not > " << date2; + + EXPECT_FALSE(Match(Clause::Op::kBefore, date2, date1)) + << date2 << " not < " << date1; + + EXPECT_TRUE(Match(Clause::Op::kAfter, date2, date1)) + << date2 << " > " << date1; + } +} + +TEST(OpTests, DateComparisonFailsWithMoreThanMicrosecondPrecision) { + auto dates = std::vector>{ + // Using Zulu suffix. + {"2023-10-08T02:00:00.000001Z", "2023-10-08T02:00:00.0000011Z"}, + // Using offset suffix. + {"2023-10-08T02:00:00.0000000001+00:00", + "2023-10-08T02:00:00.00000000011+00:00"}}; + + for (auto const& [date1, date2] : dates) { + EXPECT_FALSE(Match(Clause::Op::kBefore, date1, date2)) + << date1 << " < " << date2; + + EXPECT_FALSE(Match(Clause::Op::kAfter, date1, date2)) + << date1 << " not > " << date2; + + EXPECT_FALSE(Match(Clause::Op::kBefore, date2, date1)) + << date2 << " not < " << date1; + + EXPECT_FALSE(Match(Clause::Op::kAfter, date2, date1)) + << date2 << " > " << date1; + } +} + +// Because RFC3339 timestamps may use 'Z' to indicate a 00:00 offset, +// we should ensure these timestamps can be compared to timestamps using normal +// offsets. +TEST(OpTests, AcceptsZuluAndNormalTimezoneOffsets) { + const std::string kDate1 = "1985-04-12T23:20:50Z"; + const std::string kDate2 = "1986-04-12T23:20:50-01:00"; + + EXPECT_TRUE(Match(Clause::Op::kBefore, kDate1, kDate2)); + EXPECT_FALSE(Match(Clause::Op::kAfter, kDate1, kDate2)); + + EXPECT_FALSE(Match(Clause::Op::kBefore, kDate2, kDate1)); + EXPECT_TRUE(Match(Clause::Op::kAfter, kDate2, kDate1)); +} + +TEST(OpTests, InvalidDates) { + EXPECT_FALSE(Match(Clause::Op::kBefore, "2021-01-08T02:00:00-00:00", + "2021-12345-08T02:00:00-00:00")); + + EXPECT_FALSE(Match(Clause::Op::kAfter, "2021-12345-08T02:00:00-00:00", + "2021-01-08T02:00:00-00:00")); + + EXPECT_FALSE(Match(Clause::Op::kBefore, "foo", "bar")); + + EXPECT_FALSE(Match(Clause::Op::kAfter, "foo", "bar")); + + EXPECT_FALSE(Match(Clause::Op::kBefore, "", "bar")); + EXPECT_FALSE(Match(Clause::Op::kAfter, "", "bar")); + + EXPECT_FALSE(Match(Clause::Op::kBefore, "foo", "")); + EXPECT_FALSE(Match(Clause::Op::kAfter, "foo", "")); +} + +struct RegexTest { + std::string input; + std::string regex; + bool shouldMatch; +}; + +class RegexTests : public ::testing::TestWithParam {}; + +TEST_P(RegexTests, Matches) { + auto const& param = GetParam(); + auto const result = Match(Clause::Op::kMatches, param.input, param.regex); + + EXPECT_EQ(result, param.shouldMatch) + << "input: (" << (param.input.empty() ? "empty string" : param.input) + << ")\nregex: (" << (param.regex.empty() ? "empty string" : param.regex) + << ")"; +} + +#define MATCH true +#define NO_MATCH false + +INSTANTIATE_TEST_SUITE_P(RegexComparisons, + RegexTests, + ::testing::ValuesIn({ + RegexTest{"", "", MATCH}, + RegexTest{"a", "", MATCH}, + RegexTest{"a", "a", MATCH}, + RegexTest{"a", ".", MATCH}, + RegexTest{"hello world", "hello.*rld", MATCH}, + RegexTest{"hello world", "hello.*orl", MATCH}, + RegexTest{"hello world", "l+", MATCH}, + RegexTest{"hello world", "(world|planet)", MATCH}, + RegexTest{"", ".", NO_MATCH}, + RegexTest{"", R"(\)", NO_MATCH}, + RegexTest{"hello world", "aloha", NO_MATCH}, + RegexTest{"hello world", "***bad regex", NO_MATCH}, + })); + +#define SEMVER_NOT_EQUAL "!=" +#define SEMVER_EQUAL "==" +#define SEMVER_GREATER ">" +#define SEMVER_LESS "<" + +struct SemVerTest { + std::string lhs; + std::string rhs; + std::string op; + bool shouldMatch; +}; + +class SemVerTests : public ::testing::TestWithParam {}; + +TEST_P(SemVerTests, Matches) { + auto const& param = GetParam(); + + bool result = false; + bool swapped = false; + + if (param.op == SEMVER_EQUAL) { + result = Match(Clause::Op::kSemVerEqual, param.lhs, param.rhs); + swapped = Match(Clause::Op::kSemVerEqual, param.rhs, param.lhs); + } else if (param.op == SEMVER_NOT_EQUAL) { + result = !Match(Clause::Op::kSemVerEqual, param.lhs, param.rhs); + swapped = !Match(Clause::Op::kSemVerEqual, param.rhs, param.lhs); + } else if (param.op == SEMVER_GREATER) { + result = Match(Clause::Op::kSemVerGreaterThan, param.lhs, param.rhs); + swapped = Match(Clause::Op::kSemVerLessThan, param.rhs, param.lhs); + } else if (param.op == SEMVER_LESS) { + result = Match(Clause::Op::kSemVerLessThan, param.lhs, param.rhs); + swapped = Match(Clause::Op::kSemVerGreaterThan, param.rhs, param.lhs); + } else { + FAIL() << "Invalid operator: " << param.op; + } + + EXPECT_EQ(result, param.shouldMatch) + << param.lhs << " " << param.op << " " << param.rhs << " should be " + << (param.shouldMatch ? "true" : "false"); + + EXPECT_EQ(result, swapped) + << "commutative property invalid for " << param.lhs << " " << param.op + << " " << param.rhs; +} + +INSTANTIATE_TEST_SUITE_P( + SemVerComparisons, + SemVerTests, + ::testing::ValuesIn( + {SemVerTest{"2.0.0", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2.0", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2", "2.0.0+123", SEMVER_EQUAL, MATCH}, + SemVerTest{"2+456", "2.0.0+123", SEMVER_EQUAL, MATCH}, + SemVerTest{"2.0.0", "3.0.0", SEMVER_NOT_EQUAL, MATCH}, + SemVerTest{"2.0.0", "2.1.0", SEMVER_NOT_EQUAL, MATCH}, + SemVerTest{"2.0.0", "2.0.1", SEMVER_NOT_EQUAL, MATCH}, + SemVerTest{"3.0.0", "2.0.0", SEMVER_GREATER, MATCH}, + SemVerTest{"2.1.0", "2.0.0", SEMVER_GREATER, MATCH}, + SemVerTest{"2.0.1", "2.0.0", SEMVER_GREATER, MATCH}, + SemVerTest{"2.0.0", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"1.9.0", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"2.0.0-rc", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"2.0.0+build", "2.0.0", SEMVER_GREATER, NO_MATCH}, + SemVerTest{"2.0.0+build", "2.0.0", SEMVER_EQUAL, MATCH}, + SemVerTest{"2.0.0", "200", SEMVER_EQUAL, NO_MATCH}, + SemVerTest{"2.0.0-rc.10.green", "2.0.0-rc.2.green", SEMVER_GREATER, + MATCH}, + SemVerTest{"2.0.0-rc.2.red", "2.0.0-rc.2.green", SEMVER_GREATER, + MATCH}, + SemVerTest{"2.0.0-rc.2.green.1", "2.0.0-rc.2.green", SEMVER_GREATER, + MATCH}, + SemVerTest{"2.0.0-rc.1.very.long.prerelease.version.1234567.keeps." + "going+123124", + "2.0.0", SEMVER_LESS, MATCH}, + SemVerTest{"1", "2", SEMVER_LESS, MATCH}, + SemVerTest{"0", "1", SEMVER_LESS, MATCH}})); + +#undef SEMVER_NOT_EQUAL +#undef SEMVER_EQUAL +#undef SEMVER_GREATER +#undef SEMVER_LESS +#undef MATCH +#undef NO_MATCH diff --git a/libs/server-sdk/tests/rule_tests.cpp b/libs/server-sdk/tests/rule_tests.cpp new file mode 100644 index 000000000..ca4ce7ae1 --- /dev/null +++ b/libs/server-sdk/tests/rule_tests.cpp @@ -0,0 +1,251 @@ +#include + +#include + +#include "evaluation/evaluator.hpp" +#include "evaluation/rules.hpp" + +#include "test_store.hpp" + +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side; +using namespace launchdarkly; + +struct ClauseTest { + Clause::Op op; + launchdarkly::Value contextValue; + launchdarkly::Value clauseValue; + bool expected; +}; + +class AllOperatorsTest : public ::testing::TestWithParam { + public: + const static std::string DATE_STR1; + const static std::string DATE_STR2; + const static int DATE_MS1; + const static int DATE_MS2; + const static int DATE_MS_NEGATIVE; + const static std::string INVALID_DATE; +}; + +const std::string AllOperatorsTest::DATE_STR1 = "2017-12-06T00:00:00.000-07:00"; +const std::string AllOperatorsTest::DATE_STR2 = "2017-12-06T00:01:01.000-07:00"; +int const AllOperatorsTest::DATE_MS1 = 10000000; +int const AllOperatorsTest::DATE_MS2 = 10000001; +int const AllOperatorsTest::DATE_MS_NEGATIVE = -10000; +const std::string AllOperatorsTest::INVALID_DATE = "hey what's this?"; + +TEST_P(AllOperatorsTest, Matches) { + using namespace launchdarkly::server_side::evaluation::detail; + using namespace launchdarkly; + + auto const& param = GetParam(); + + std::vector clauseValues; + + if (param.clauseValue.IsArray()) { + auto const& as_array = param.clauseValue.AsArray(); + clauseValues = std::vector{as_array.begin(), as_array.end()}; + } else { + clauseValues.push_back(param.clauseValue); + } + + Clause clause{param.op, std::move(clauseValues), false, ContextKind("user"), + "attr"}; + + auto context = launchdarkly::ContextBuilder() + .Kind("user", "key") + .Set("attr", param.contextValue) + .Build(); + ASSERT_TRUE(context.Valid()); + + EvaluationStack stack; + + auto store = test_store::Empty(); + + auto result = launchdarkly::server_side::evaluation::Match(clause, context, + *store, stack); + ASSERT_EQ(result, param.expected) + << context.Get("user", "attr") << " " << clause.op << " " + << clause.values << " should be " << param.expected; +} + +#define MATCH true +#define NO_MATCH false + +INSTANTIATE_TEST_SUITE_P( + NumericClauses, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, 99, 99, MATCH}, + ClauseTest{Clause::Op::kIn, 99.0, 99, MATCH}, + ClauseTest{Clause::Op::kIn, 99, 99.0, MATCH}, + ClauseTest{Clause::Op::kIn, 99, + std::vector{99, 98, 97, 96}, MATCH}, + ClauseTest{Clause::Op::kIn, 99.0001, 99.0001, MATCH}, + ClauseTest{Clause::Op::kIn, 99.0001, + std::vector{99.0001, 98.0, 97.0, 96.0}, + MATCH}, + ClauseTest{Clause::Op::kLessThan, 1, 1.99999, MATCH}, + ClauseTest{Clause::Op::kLessThan, 1.99999, 1, NO_MATCH}, + ClauseTest{Clause::Op::kLessThan, 1, 2, MATCH}, + ClauseTest{Clause::Op::kLessThanOrEqual, 1, 1.0, MATCH}, + ClauseTest{Clause::Op::kGreaterThan, 2, 1.99999, MATCH}, + ClauseTest{Clause::Op::kGreaterThan, 1.99999, 2, NO_MATCH}, + ClauseTest{Clause::Op::kGreaterThan, 2, 1, MATCH}, + ClauseTest{Clause::Op::kGreaterThanOrEqual, 1, 1.0, MATCH}, + + })); + +INSTANTIATE_TEST_SUITE_P( + StringClauses, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, "x", "x", MATCH}, + ClauseTest{Clause::Op::kIn, "x", + std::vector{"x", "a", "b", "c"}, MATCH}, + ClauseTest{Clause::Op::kIn, "x", "xyz", NO_MATCH}, + ClauseTest{Clause::Op::kStartsWith, "xyz", "x", MATCH}, + ClauseTest{Clause::Op::kStartsWith, "x", "xyz", NO_MATCH}, + ClauseTest{Clause::Op::kEndsWith, "xyz", "z", MATCH}, + ClauseTest{Clause::Op::kEndsWith, "z", "xyz", NO_MATCH}, + ClauseTest{Clause::Op::kContains, "xyz", "y", MATCH}, + ClauseTest{Clause::Op::kContains, "y", "xyz", NO_MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + MixedStringAndNumbers, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kIn, 99, "99", NO_MATCH}, + ClauseTest{Clause::Op::kContains, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kStartsWith, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kEndsWith, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kLessThanOrEqual, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kLessThanOrEqual, 99, "99", NO_MATCH}, + ClauseTest{Clause::Op::kGreaterThanOrEqual, "99", 99, NO_MATCH}, + ClauseTest{Clause::Op::kGreaterThanOrEqual, 99, "99", NO_MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + BooleanEquality, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, true, true, MATCH}, + ClauseTest{Clause::Op::kIn, false, false, MATCH}, + ClauseTest{Clause::Op::kIn, true, false, NO_MATCH}, + ClauseTest{Clause::Op::kIn, false, true, NO_MATCH}, + ClauseTest{Clause::Op::kIn, true, + std::vector{false, true}, MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + ArrayEquality, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, {{"x"}}, {{"x"}}, MATCH}, + ClauseTest{Clause::Op::kIn, {{"x"}}, {"x"}, NO_MATCH}, + ClauseTest{Clause::Op::kIn, {{"x"}}, {{"x"}, {"a"}, {"b"}}, MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + ObjectEquality, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kIn, Value::Object({{"x", "1"}}), + Value::Object({{"x", "1"}}), MATCH}, + ClauseTest{Clause::Op::kIn, Value::Object({{"x", "1"}}), + std::vector{ + Value::Object({{"x", "1"}}), + Value::Object({{"a", "2"}}), + Value::Object({{"b", "3"}}), + }, + MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + RegexMatch, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kMatches, "hello world", "hello.*rld", MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "hello.*orl", MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "l+", MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "(world|planet)", + MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "aloha", NO_MATCH}, + ClauseTest{Clause::Op::kMatches, "hello world", "***bad regex", + NO_MATCH}, + })); + +INSTANTIATE_TEST_SUITE_P( + DateClauses, + AllOperatorsTest, + ::testing::ValuesIn({ + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR2, MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS2, MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR2, + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS2, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, Value::Null(), + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::INVALID_DATE, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR2, + AllOperatorsTest::DATE_STR1, MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS2, + AllOperatorsTest::DATE_MS1, MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR2, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS2, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, Value::Null(), + AllOperatorsTest::DATE_STR1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_STR1, + AllOperatorsTest::INVALID_DATE, NO_MATCH}, + ClauseTest{Clause::Op::kBefore, AllOperatorsTest::DATE_MS_NEGATIVE, + AllOperatorsTest::DATE_MS1, NO_MATCH}, + ClauseTest{Clause::Op::kAfter, AllOperatorsTest::DATE_MS1, + AllOperatorsTest::DATE_MS_NEGATIVE, NO_MATCH}, + + })); + +INSTANTIATE_TEST_SUITE_P( + SemVerTests, + AllOperatorsTest, + ::testing::ValuesIn( + {ClauseTest{Clause::Op::kSemVerEqual, "2.0.0", "2.0.0", MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2.0", "2.0.0", MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2-rc1", "2.0.0-rc1", MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2+build2", "2.0.0+build2", + MATCH}, + ClauseTest{Clause::Op::kSemVerEqual, "2.0.0", "2.0.1", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.0", "2.0.1", MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0", "2.0.1", MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.1", "2.0.0", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.1", "2.0", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.1", "xbad%ver", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerLessThan, "2.0.0-rc", "2.0.0-rc.beta", + MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.1", "2.0", MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "10.0.1", "2.0", MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.0", "2.0.1", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0", "2.0.1", NO_MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.1", "xbad%ver", + NO_MATCH}, + ClauseTest{Clause::Op::kSemVerGreaterThan, "2.0.0-rc.1", "2.0.0-rc.0", + MATCH}})); + +#undef MATCH +#undef NO_MATCH diff --git a/libs/server-sdk/tests/semver_tests.cpp b/libs/server-sdk/tests/semver_tests.cpp new file mode 100644 index 000000000..61cfe6121 --- /dev/null +++ b/libs/server-sdk/tests/semver_tests.cpp @@ -0,0 +1,66 @@ +#include +#include "evaluation/detail/semver_operations.hpp" + +using namespace launchdarkly::server_side::evaluation::detail; + +TEST(SemVer, DefaultConstruction) { + SemVer version; + EXPECT_EQ(version.Major(), 0); + EXPECT_EQ(version.Minor(), 0); + EXPECT_EQ(version.Patch(), 0); + EXPECT_FALSE(version.Prerelease()); +} + +TEST(SemVer, MinimalVersion) { + SemVer version{1, 2, 3}; + EXPECT_EQ(version.Major(), 1); + EXPECT_EQ(version.Minor(), 2); + EXPECT_EQ(version.Patch(), 3); + EXPECT_FALSE(version.Prerelease()); +} + +TEST(SemVer, ParseMinimalVersion) { + auto version = SemVer::Parse("1.2.3"); + ASSERT_TRUE(version); + EXPECT_EQ(version->Major(), 1); + EXPECT_EQ(version->Minor(), 2); + EXPECT_EQ(version->Patch(), 3); + EXPECT_FALSE(version->Prerelease()); +} + +TEST(SemVer, ParsePrereleaseVersion) { + auto version = SemVer::Parse("1.2.3-alpha.123.foo"); + ASSERT_TRUE(version); + ASSERT_TRUE(version->Prerelease()); + + auto const& pre = *version->Prerelease(); + ASSERT_EQ(pre.size(), 3); + EXPECT_EQ(pre[0], SemVer::Token("alpha")); + EXPECT_EQ(pre[1], SemVer::Token(123ull)); + EXPECT_EQ(pre[2], SemVer::Token("foo")); +} + +TEST(SemVer, ParseInvalid) { + ASSERT_FALSE(SemVer::Parse("")); + ASSERT_FALSE(SemVer::Parse("v1.2.3")); + ASSERT_FALSE(SemVer::Parse("foo")); + ASSERT_FALSE(SemVer::Parse("1.2.3 ")); + ASSERT_FALSE(SemVer::Parse("1.2.3.alpha.1")); + ASSERT_FALSE(SemVer::Parse("1.2.3.4")); + ASSERT_FALSE(SemVer::Parse("1.2.3-_")); +} + +TEST(SemVer, BasicComparison) { + EXPECT_LT(SemVer::Parse("1.0.0"), SemVer::Parse("2.0.0")); + ASSERT_LT(SemVer::Parse("1.0.0-alpha.1"), SemVer::Parse("1.0.0")); + + ASSERT_GT(SemVer::Parse("2.0.0"), SemVer::Parse("1.0.0")); + ASSERT_GT(SemVer::Parse("1.0.0"), SemVer::Parse("1.0.0-alpha.1")); + + ASSERT_EQ(SemVer::Parse("1.0.0"), SemVer::Parse("1.0.0")); + + // Build is irrelevant for comparisons. + ASSERT_EQ(SemVer::Parse("1.2.3+build12345"), SemVer::Parse("1.2.3")); + ASSERT_EQ(SemVer::Parse("1.2.3-alpha.1+1234"), + SemVer::Parse("1.2.3-alpha.1+4567")); +} diff --git a/libs/server-sdk/tests/spy_logger.hpp b/libs/server-sdk/tests/spy_logger.hpp new file mode 100644 index 000000000..a04edb197 --- /dev/null +++ b/libs/server-sdk/tests/spy_logger.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include + +#include + +#include + +namespace launchdarkly::logging { + +class SpyLoggerBackend : public launchdarkly::ILogBackend { + public: + using Record = std::pair; + + SpyLoggerBackend() : messages_() {} + + /** + * Always returns true. + */ + [[nodiscard]] bool Enabled(LogLevel level) noexcept override { + return true; + } + + /** + * Records the message internally. + */ + void Write(LogLevel level, std::string message) noexcept override { + messages_.push_back({level, std::move(message)}); + } + + /** + * Asserts that 'count' messages were recorded. + * @param count Number of expected messages. + */ + [[nodiscard]] testing::AssertionResult Count(std::size_t count) const { + if (messages_.size() == count) { + return testing::AssertionSuccess(); + } + return testing::AssertionFailure() + << "Expected " << count << " messages, got " << messages_.size(); + } + + [[nodiscard]] testing::AssertionResult Equals( + std::size_t index, + LogLevel level, + std::string const& expected) const { + return GetIndex(index, level, [&](auto const& actual) { + if (actual.second != expected) { + return testing::AssertionFailure() + << "Expected message " << index << " to be " << expected + << ", got " << actual.second; + } + return testing::AssertionSuccess(); + }); + } + + [[nodiscard]] testing::AssertionResult Contains( + std::size_t index, + LogLevel level, + std::string const& expected) const { + return GetIndex( + index, level, [&](auto const& actual) -> testing::AssertionResult { + if (actual.second.find(expected) != std::string::npos) { + return testing::AssertionSuccess(); + } + return testing::AssertionFailure() + << "Expected message " << index << " to contain " + << expected << ", got " << actual.second; + }); + } + + private: + [[nodiscard]] testing::AssertionResult GetIndex( + std::size_t index, + LogLevel level, + std::function const& f) const { + if (index >= messages_.size()) { + return testing::AssertionFailure() + << "Message index " << index << " out of range"; + } + auto const& record = messages_[index]; + if (level != record.first) { + return testing::AssertionFailure() + << "Expected message " << index << " to be " << level + << ", got " << record.first; + } + return f(record); + } + using Records = std::vector; + Records messages_; +}; + +} // namespace launchdarkly::logging diff --git a/libs/server-sdk/tests/test_store.cpp b/libs/server-sdk/tests/test_store.cpp new file mode 100644 index 000000000..128eb2b8d --- /dev/null +++ b/libs/server-sdk/tests/test_store.cpp @@ -0,0 +1,307 @@ +#include "test_store.hpp" + +#include "data_store/memory_store.hpp" + +#include +#include + +namespace launchdarkly::server_side::test_store { + +std::unique_ptr Empty() { + auto store = std::make_unique(); + store->Init({}); + return store; +} + +data_store::FlagDescriptor Flag(char const* json) { + auto val = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(json)); + assert(val.has_value()); + assert(val.value().has_value()); + return data_store::FlagDescriptor{val.value().value()}; +} + +data_store::SegmentDescriptor Segment(char const* json) { + auto val = boost::json::value_to< + tl::expected, JsonError>>( + boost::json::parse(json)); + assert(val.has_value()); + assert(val.value().has_value()); + return data_store::SegmentDescriptor{val.value().value()}; +} + +std::unique_ptr TestData() { + auto store = std::make_unique(); + store->Init({}); + + store->Upsert("segmentWithNoRules", Segment(R"({ + "key": "segmentWithNoRules", + "included": ["alice"], + "excluded": [], + "rules": [], + "salt": "salty", + "version": 1 + })")); + store->Upsert("segmentWithRuleMatchesUserAlice", Segment(R"({ + "key": "segmentWithRuleMatchesUserAlice", + "included": [], + "excluded": [], + "rules": [{ + "id": "rule-1", + "clauses": [{ + "attribute": "key", + "negate": false, + "op": "in", + "values": ["alice"], + "contextKind": "user" + }] + }], + "salt": "salty", + "version": 1 + })")); + + store->Upsert("flagWithTarget", Flag(R"( + { + "key": "flagWithTarget", + "version": 42, + "on": false, + "targets": [{ + "values": ["bob"], + "variation": 0 + }], + "rules": [], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty" + })")); + + store->Upsert("flagWithMatchesOpOnGroups", Flag(R"({ + "key": "flagWithMatchesOpOnGroups", + "version": 42, + "on": true, + "targets": [], + "rules": [ + { + "variation": 0, + "id": "6a7755ac-e47a-40ea-9579-a09dd5f061bd", + "clauses": [ + { + "attribute": "groups", + "op": "matches", + "values": [ + "^\\w+" + ], + "negate": false + } + ], + "trackEvents": true + } + ], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": true, + "debugEventsUntilDate": 1500000000 + })")); + + store->Upsert("flagWithMatchesOpOnKinds", Flag(R"({ + "key": "flagWithMatchesOpOnKinds", + "version": 42, + "on": true, + "targets": [], + "rules": [ + { + "variation": 0, + "id": "6a7755ac-e47a-40ea-9579-a09dd5f061bd", + "clauses": [ + { + "attribute": "kind", + "op": "matches", + "values": [ + "^[ou]" + ], + "negate": false + } + ], + "trackEvents": true + } + ], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": true, + "debugEventsUntilDate": 1500000000 + })")); + + store->Upsert("cycleFlagA", Flag(R"({ + "key": "cycleFlagA", + "targets": [], + "rules": [], + "salt": "salty", + "prerequisites": [{ + "key": "cycleFlagB", + "variation": 0 + }], + "on": true, + "fallthrough": {"variation": 0}, + "offVariation": 1, + "variations": [true, false] + })")); + store->Upsert("cycleFlagB", Flag(R"({ + "key": "cycleFlagB", + "targets": [], + "rules": [], + "salt": "salty", + "prerequisites": [{ + "key": "cycleFlagA", + "variation": 0 + }], + "on": true, + "fallthrough": {"variation": 0}, + "offVariation": 1, + "variations": [true, false] + })")); + + store->Upsert("flagWithExperiment", Flag(R"({ + "key": "flagWithExperiment", + "version": 42, + "on": true, + "targets": [], + "rules": [], + "prerequisites": [], + "fallthrough": { + "rollout": { + "kind": "experiment", + "seed": 61, + "variations": [ + {"variation": 0, "weight": 10000, "untracked": false}, + {"variation": 1, "weight": 20000, "untracked": false}, + {"variation": 0, "weight": 70000, "untracked": true} + ] + } + }, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": 1500000000 + })")); + store->Upsert("flagWithExperimentTargetingContext", Flag(R"({ + "key": "flagWithExperimentTargetingContext", + "version": 42, + "on": true, + "targets": [], + "rules": [], + "prerequisites": [], + "fallthrough": { + "rollout": { + "kind": "experiment", + "contextKind": "org", + "seed": 61, + "variations": [ + {"variation": 0, "weight": 10000, "untracked": false}, + {"variation": 1, "weight": 20000, "untracked": false}, + {"variation": 0, "weight": 70000, "untracked": true} + ] + } + }, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty", + "trackEvents": false, + "trackEventsFallthrough": false, + "debugEventsUntilDate": 1500000000 + })")); + store->Upsert("flagWithSegmentMatchRule", Flag(R"({ + "key": "flagWithSegmentMatchRule", + "version": 42, + "on": true, + "targets": [], + "rules": [{ + "id": "match-rule", + "clauses": [{ + "contextKind": "user", + "attribute": "key", + "negate": false, + "op": "segmentMatch", + "values": ["segmentWithNoRules"] + }], + "variation": 0, + "trackEvents": false + }], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty" + })")); + + store->Upsert("flagWithSegmentMatchesUserAlice", Flag(R"({ + "key": "flagWithSegmentMatchesUserAlice", + "version": 42, + "on": true, + "targets": [], + "rules": [{ + "id": "match-rule", + "clauses": [{ + "op": "segmentMatch", + "values": ["segmentWithRuleMatchesUserAlice"] + }], + "variation": 0, + "trackEvents": false + }], + "prerequisites": [], + "fallthrough": {"variation": 1}, + "offVariation": 0, + "variations": [false, true], + "clientSide": true, + "clientSideAvailability": { + "usingEnvironmentId": true, + "usingMobileKey": true + }, + "salt": "salty" + })")); + return store; +} + +} // namespace launchdarkly::server_side::test_store diff --git a/libs/server-sdk/tests/test_store.hpp b/libs/server-sdk/tests/test_store.hpp new file mode 100644 index 000000000..4b0a37c44 --- /dev/null +++ b/libs/server-sdk/tests/test_store.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "data_store/data_store.hpp" + +#include + +namespace launchdarkly::server_side::test_store { + +/** + * @return A data store preloaded with flags/segments for unit tests. + */ +std::unique_ptr TestData(); + +/** + * @return An initialized, but empty, data store. + */ +std::unique_ptr Empty(); + +} // namespace launchdarkly::server_side::test_store diff --git a/libs/server-sdk/tests/timestamp_tests.cpp b/libs/server-sdk/tests/timestamp_tests.cpp new file mode 100644 index 000000000..78db3107d --- /dev/null +++ b/libs/server-sdk/tests/timestamp_tests.cpp @@ -0,0 +1,94 @@ +#include "evaluation/detail/timestamp_operations.hpp" + +#include + +#include + +using namespace launchdarkly::server_side::evaluation::detail; +using namespace std::chrono_literals; + +static Timepoint BasicDate() { + return std::chrono::system_clock::from_time_t(1577836800); +} + +struct TimestampTest { + launchdarkly::Value input; + char const* explanation; + std::optional expected; +}; + +class TimestampTests : public ::testing::TestWithParam {}; +TEST_P(TimestampTests, ExpectedTimestampIsParsed) { + auto const& param = GetParam(); + + std::optional result = ToTimepoint(param.input); + + constexpr auto print_tp = + [](std::optional const& expected) -> std::string { + if (expected) { + return std::to_string(expected.value().time_since_epoch().count()); + } else { + return "(none)"; + } + }; + + ASSERT_EQ(result, param.expected) + << param.explanation << ": input was " << param.input << ", expected " + << print_tp(param.expected) << " but got " << print_tp(result); +} + +INSTANTIATE_TEST_SUITE_P( + ValidTimestamps, + TimestampTests, + ::testing::ValuesIn({ + TimestampTest{0.0, "default constructed", Timepoint{}}, + TimestampTest{1000.0, "1 second", Timepoint{1s}}, + TimestampTest{1000.0 * 60, "60 seconds", Timepoint{60s}}, + TimestampTest{1000.0 * 60 * 60, "1 hour", Timepoint{60min}}, + TimestampTest{"2020-01-01T00:00:00Z", "with Zulu offset", BasicDate()}, + TimestampTest{"2020-01-01T00:00:00+00:00", "with normal offset", + BasicDate()}, + TimestampTest{"2020-01-01T01:00:00+01:00", "with 1hr offset", + BasicDate()}, + TimestampTest{"2020-01-01T01:00:00+01:00", + "with colon-delimited offset", BasicDate()}, + + TimestampTest{"2020-01-01T00:00:00.123Z", "with milliseconds", + BasicDate() + 123ms}, + TimestampTest{"2020-01-01T00:00:00.123+00:00", + "with milliseconds and offset", BasicDate() + 123ms}, + TimestampTest{"2020-01-01T00:00:00.000123Z", "with microseconds ", + BasicDate() + 123us}, + TimestampTest{"2020-01-01T00:00:00.000123+00:00", + "with microseconds and offset", BasicDate() + 123us}, + TimestampTest{"2020-01-01T00:00:00.123456789Z", + "floor nanoseconds with zulu offset", + BasicDate() + 123ms + 456us}, + TimestampTest{"2020-01-01T01:00:00.123456789+01:00", + "floor nanoseconds with offset", + BasicDate() + 123ms + 456us}, + + })); + +INSTANTIATE_TEST_SUITE_P( + InvalidTimestamps, + TimestampTests, + ::testing::ValuesIn({ + TimestampTest{0.1, "not an integer", std::nullopt}, + TimestampTest{1000.2, "not an integer", std::nullopt}, + TimestampTest{123456.789, "not an integer", std::nullopt}, + TimestampTest{-1000.5, "not an integer", std::nullopt}, + TimestampTest{-1000.0, "negative integer", std::nullopt}, + TimestampTest{"", "empty string", std::nullopt}, + TimestampTest{"2020-01-01T00:00:00/foo", "invalid offset", + std::nullopt}, + TimestampTest{"2020-01-01T00:00:00.0000000001Z", + "more than 9 digits of precision", std::nullopt}, + TimestampTest{launchdarkly::Value::Null(), "not a number or string", + std::nullopt}, + TimestampTest{launchdarkly::Value::Array(), "not a number or string", + std::nullopt}, + TimestampTest{launchdarkly::Value::Object(), "not a number or string", + std::nullopt}, + + })); From bb75d4b01c63d59193d8e485e90509dc0b5f5422 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 18 Jul 2023 17:45:12 -0700 Subject: [PATCH 16/56] feat: initial pass of server-side Client object (#176) Adds a server-side Client object. --- libs/client-sdk/src/CMakeLists.txt | 5 - libs/client-sdk/src/client_impl.cpp | 45 ++- libs/client-sdk/src/client_impl.hpp | 7 +- .../src/event_processor/event_processor.cpp | 25 -- .../src/event_processor/event_processor.hpp | 29 -- .../events/asio_event_processor.hpp | 47 +-- .../launchdarkly/events/client_events.hpp | 50 --- .../events/{ => data}/common_events.hpp | 55 ++- .../launchdarkly/events/data/events.hpp | 17 + .../events/data/server_events.hpp | 12 + .../events/{ => detail}/event_batch.hpp | 8 +- .../events/{ => detail}/lru_cache.hpp | 6 +- .../events/{ => detail}/outbox.hpp | 7 +- .../events/{ => detail}/parse_date_header.hpp | 4 +- .../events/{ => detail}/request_worker.hpp | 6 +- .../events/{ => detail}/summarizer.hpp | 10 +- .../events/{ => detail}/worker_pool.hpp | 15 +- .../events/event_processor_interface.hpp} | 6 +- .../include/launchdarkly/events/events.hpp | 18 - .../events}/null_event_processor.hpp | 6 +- .../launchdarkly/events/server_events.hpp | 12 - .../serialization/events/json_events.hpp | 28 +- libs/internal/src/CMakeLists.txt | 3 +- .../src/events/asio_event_processor.cpp | 46 +-- .../{client_events.cpp => common_events.cpp} | 6 +- libs/internal/src/events/event_batch.cpp | 6 +- libs/internal/src/events/lru_cache.cpp | 6 +- .../src/events}/null_event_processor.cpp | 6 +- libs/internal/src/events/outbox.cpp | 6 +- libs/internal/src/events/request_worker.cpp | 8 +- libs/internal/src/events/summarizer.cpp | 8 +- libs/internal/src/events/worker_pool.cpp | 8 +- .../src/serialization/events/json_events.cpp | 12 +- libs/internal/tests/event_processor_test.cpp | 19 +- .../tests/event_serialization_test.cpp | 18 +- libs/internal/tests/event_summarizer_test.cpp | 11 +- libs/internal/tests/lru_cache_test.cpp | 4 +- libs/internal/tests/request_worker_test.cpp | 3 +- .../launchdarkly/server_side/client.hpp | 331 +++++++++++++++++ libs/server-sdk/src/CMakeLists.txt | 3 + libs/server-sdk/src/client.cpp | 111 ++++++ libs/server-sdk/src/client_impl.cpp | 332 ++++++++++++++++++ libs/server-sdk/src/client_impl.hpp | 148 ++++++++ .../src/data_sources/null_data_source.cpp | 19 + .../src/data_sources/null_data_source.hpp | 23 ++ libs/server-sdk/src/evaluation/evaluator.cpp | 4 +- libs/server-sdk/src/evaluation/evaluator.hpp | 10 +- libs/server-sdk/tests/client_test.cpp | 70 ++++ libs/server-sdk/tests/evaluator_tests.cpp | 4 +- 49 files changed, 1310 insertions(+), 333 deletions(-) delete mode 100644 libs/client-sdk/src/event_processor/event_processor.cpp delete mode 100644 libs/client-sdk/src/event_processor/event_processor.hpp delete mode 100644 libs/internal/include/launchdarkly/events/client_events.hpp rename libs/internal/include/launchdarkly/events/{ => data}/common_events.hpp (50%) create mode 100644 libs/internal/include/launchdarkly/events/data/events.hpp create mode 100644 libs/internal/include/launchdarkly/events/data/server_events.hpp rename libs/internal/include/launchdarkly/events/{ => detail}/event_batch.hpp (92%) rename libs/internal/include/launchdarkly/events/{ => detail}/lru_cache.hpp (91%) rename libs/internal/include/launchdarkly/events/{ => detail}/outbox.hpp (90%) rename libs/internal/include/launchdarkly/events/{ => detail}/parse_date_header.hpp (93%) rename libs/internal/include/launchdarkly/events/{ => detail}/request_worker.hpp (97%) rename libs/internal/include/launchdarkly/events/{ => detail}/summarizer.hpp (92%) rename libs/internal/include/launchdarkly/events/{ => detail}/worker_pool.hpp (93%) rename libs/{client-sdk/src/event_processor.hpp => internal/include/launchdarkly/events/event_processor_interface.hpp} (89%) delete mode 100644 libs/internal/include/launchdarkly/events/events.hpp rename libs/{client-sdk/src/event_processor => internal/include/launchdarkly/events}/null_event_processor.hpp (64%) delete mode 100644 libs/internal/include/launchdarkly/events/server_events.hpp rename libs/internal/src/events/{client_events.cpp => common_events.cpp} (68%) rename libs/{client-sdk/src/event_processor => internal/src/events}/null_event_processor.cpp (54%) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/client.hpp create mode 100644 libs/server-sdk/src/client.cpp create mode 100644 libs/server-sdk/src/client_impl.cpp create mode 100644 libs/server-sdk/src/client_impl.hpp create mode 100644 libs/server-sdk/src/data_sources/null_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/null_data_source.hpp create mode 100644 libs/server-sdk/tests/client_test.cpp diff --git a/libs/client-sdk/src/CMakeLists.txt b/libs/client-sdk/src/CMakeLists.txt index e7ce7f890..aef6199ee 100644 --- a/libs/client-sdk/src/CMakeLists.txt +++ b/libs/client-sdk/src/CMakeLists.txt @@ -14,8 +14,6 @@ add_library(${LIBNAME} flag_manager/flag_updater.cpp flag_manager/flag_change_event.cpp data_sources/data_source_status.cpp - event_processor/event_processor.cpp - event_processor/null_event_processor.cpp client_impl.cpp client.cpp client_impl.hpp @@ -24,11 +22,8 @@ add_library(${LIBNAME} data_sources/data_source_update_sink.hpp data_sources/polling_data_source.hpp data_sources/streaming_data_source.hpp - event_processor/event_processor.hpp - event_processor/null_event_processor.hpp flag_manager/flag_store.hpp flag_manager/flag_updater.hpp - event_processor.hpp bindings/c/sdk.cpp data_sources/null_data_source.cpp flag_manager/context_index.cpp diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index ed753dfb7..fee995486 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -1,21 +1,19 @@ - -#include - -#include -#include - #include "client_impl.hpp" #include "data_sources/null_data_source.hpp" #include "data_sources/polling_data_source.hpp" #include "data_sources/streaming_data_source.hpp" -#include "event_processor/event_processor.hpp" -#include "event_processor/null_event_processor.hpp" +#include +#include #include #include #include +#include +#include +#include + namespace launchdarkly::client_side { // The ASIO implementation assumes that the io_context will be run from a @@ -32,14 +30,14 @@ using launchdarkly::client_side::data_sources::DataSourceStatus; using launchdarkly::config::shared::built::DataSourceConfig; using launchdarkly::config::shared::built::HttpProperties; -static std::shared_ptr<::launchdarkly::data_sources::IDataSource> MakeDataSource( - HttpProperties const& http_properties, - Config const& config, - Context const& context, - boost::asio::any_io_executor const& executor, - IDataSourceUpdateSink& flag_updater, - data_sources::DataSourceStatusManager& status_manager, - Logger& logger) { +static std::shared_ptr<::launchdarkly::data_sources::IDataSource> +MakeDataSource(HttpProperties const& http_properties, + Config const& config, + Context const& context, + boost::asio::any_io_executor const& executor, + IDataSourceUpdateSink& flag_updater, + data_sources::DataSourceStatusManager& status_manager, + Logger& logger) { if (config.Offline()) { return std::make_shared(executor, status_manager); @@ -112,14 +110,15 @@ ClientImpl::ClientImpl(Config config, flag_manager_.LoadCache(context_); if (config.Events().Enabled() && !config.Offline()) { - event_processor_ = std::make_unique( - ioc_.get_executor(), config.ServiceEndpoints(), config.Events(), - http_properties_, logger_); + event_processor_ = + std::make_unique>( + ioc_.get_executor(), config.ServiceEndpoints(), config.Events(), + http_properties_, logger_); } else { - event_processor_ = std::make_unique(); + event_processor_ = std::make_unique(); } - event_processor_->SendAsync(events::client::IdentifyEventParams{ + event_processor_->SendAsync(events::IdentifyEventParams{ std::chrono::system_clock::now(), context_}); run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); @@ -143,7 +142,7 @@ static bool IsInitialized(DataSourceStatus::DataSourceState state) { std::future ClientImpl::IdentifyAsync(Context context) { UpdateContextSynchronized(context); flag_manager_.LoadCache(context); - event_processor_->SendAsync(events::client::IdentifyEventParams{ + event_processor_->SendAsync(events::IdentifyEventParams{ std::chrono::system_clock::now(), std::move(context)}); return StartAsyncInternal(IsInitializedSuccessfully); @@ -234,7 +233,7 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, bool detailed) { auto desc = flag_manager_.Store().Get(key); - events::client::FeatureEventParams event = { + events::FeatureEventParams event = { std::chrono::system_clock::now(), key, ReadContextSynchronized([](Context const& c) { return c; }), diff --git a/libs/client-sdk/src/client_impl.hpp b/libs/client-sdk/src/client_impl.hpp index 6c226b9d8..c273e2ad1 100644 --- a/libs/client-sdk/src/client_impl.hpp +++ b/libs/client-sdk/src/client_impl.hpp @@ -11,7 +11,7 @@ #include #include -#include "tl/expected.hpp" +#include #include #include @@ -24,8 +24,9 @@ #include #include +#include + #include "data_sources/data_source_status_manager.hpp" -#include "event_processor.hpp" #include "flag_manager/flag_manager.hpp" namespace launchdarkly::client_side { @@ -134,7 +135,7 @@ class ClientImpl : public IClient { std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; - std::unique_ptr event_processor_; + std::unique_ptr event_processor_; mutable std::mutex init_mutex_; std::condition_variable init_waiter_; diff --git a/libs/client-sdk/src/event_processor/event_processor.cpp b/libs/client-sdk/src/event_processor/event_processor.cpp deleted file mode 100644 index ac78fa1c6..000000000 --- a/libs/client-sdk/src/event_processor/event_processor.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "event_processor.hpp" - -namespace launchdarkly::client_side { - -EventProcessor::EventProcessor( - boost::asio::any_io_executor const& io, - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::Events const& events_config, - config::shared::built::HttpProperties const& http_properties, - Logger& logger) - : impl_(io, endpoints, events_config, http_properties, logger) {} - -void EventProcessor::SendAsync(events::InputEvent event) { - impl_.AsyncSend(std::move(event)); -} - -void EventProcessor::FlushAsync() { - impl_.AsyncFlush(); -} - -void EventProcessor::ShutdownAsync() { - impl_.AsyncClose(); -} - -} // namespace launchdarkly::client_side diff --git a/libs/client-sdk/src/event_processor/event_processor.hpp b/libs/client-sdk/src/event_processor/event_processor.hpp deleted file mode 100644 index dd8d7b06d..000000000 --- a/libs/client-sdk/src/event_processor/event_processor.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include - -#include -#include -#include -#include - -#include "../event_processor.hpp" - -namespace launchdarkly::client_side { - -class EventProcessor : public IEventProcessor { - public: - EventProcessor(boost::asio::any_io_executor const& io, - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::Events const& events_config, - config::shared::built::HttpProperties const& http_properties, - Logger& logger); - void SendAsync(events::InputEvent event) override; - void FlushAsync() override; - void ShutdownAsync() override; - - private: - events::AsioEventProcessor impl_; -}; - -} // namespace launchdarkly::client_side diff --git a/libs/internal/include/launchdarkly/events/asio_event_processor.hpp b/libs/internal/include/launchdarkly/events/asio_event_processor.hpp index 134abf029..a531877e8 100644 --- a/libs/internal/include/launchdarkly/events/asio_event_processor.hpp +++ b/libs/internal/include/launchdarkly/events/asio_event_processor.hpp @@ -1,5 +1,19 @@ #pragma once +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + #include #include #include @@ -10,23 +24,10 @@ #include #include -#include -#include -#include -#include -#include -#include - -#include "event_batch.hpp" -#include "events.hpp" -#include "outbox.hpp" -#include "summarizer.hpp" -#include "worker_pool.hpp" - namespace launchdarkly::events { template -class AsioEventProcessor { +class AsioEventProcessor : public IEventProcessor { public: AsioEventProcessor( boost::asio::any_io_executor const& io, @@ -35,11 +36,11 @@ class AsioEventProcessor { config::shared::built::HttpProperties const& http_properties, Logger& logger); - void AsyncFlush(); + virtual void FlushAsync() override; - void AsyncSend(InputEvent event); + virtual void SendAsync(events::InputEvent event) override; - void AsyncClose(); + virtual void ShutdownAsync() override; private: using Clock = std::chrono::system_clock; @@ -49,8 +50,8 @@ class AsioEventProcessor { }; boost::asio::any_io_executor io_; - Outbox outbox_; - Summarizer summarizer_; + detail::Outbox outbox_; + detail::Summarizer summarizer_; std::chrono::milliseconds flush_interval_; boost::asio::steady_timer timer_; @@ -61,7 +62,7 @@ class AsioEventProcessor { boost::uuids::random_generator uuids_; - WorkerPool workers_; + detail::WorkerPool workers_; std::size_t inbox_capacity_; std::size_t inbox_size_; @@ -75,13 +76,13 @@ class AsioEventProcessor { launchdarkly::ContextFilter filter_; - LRUCache context_key_cache_; + detail::LRUCache context_key_cache_; Logger& logger_; void HandleSend(InputEvent event); - std::optional CreateBatch(); + std::optional CreateBatch(); void Flush(FlushTrigger flush_type); @@ -93,7 +94,7 @@ class AsioEventProcessor { void InboxDecrement(); void OnEventDeliveryResult(std::size_t count, - RequestWorker::DeliveryResult); + detail::RequestWorker::DeliveryResult); }; } // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/client_events.hpp b/libs/internal/include/launchdarkly/events/client_events.hpp deleted file mode 100644 index 595a6ae0d..000000000 --- a/libs/internal/include/launchdarkly/events/client_events.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "common_events.hpp" - -namespace launchdarkly::events::client { - -struct IdentifyEventParams { - Date creation_date; - Context context; -}; - -struct IdentifyEvent { - Date creation_date; - EventContext context; -}; - -struct FeatureEventParams { - Date creation_date; - std::string key; - Context context; - 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; - std::optional version; - std::optional variation; - Value value; - std::optional reason; - Value default_; - - explicit FeatureEventBase(FeatureEventParams const& params); -}; - -struct FeatureEvent : public FeatureEventBase { - ContextKeys context_keys; -}; - -struct DebugEvent : public FeatureEventBase { - EventContext context; -}; - -} // namespace launchdarkly::events::client diff --git a/libs/internal/include/launchdarkly/events/common_events.hpp b/libs/internal/include/launchdarkly/events/data/common_events.hpp similarity index 50% rename from libs/internal/include/launchdarkly/events/common_events.hpp rename to libs/internal/include/launchdarkly/events/data/common_events.hpp index a2f95c8be..667567c30 100644 --- a/libs/internal/include/launchdarkly/events/common_events.hpp +++ b/libs/internal/include/launchdarkly/events/data/common_events.hpp @@ -1,15 +1,15 @@ #pragma once -#include -#include -#include - -#include - #include #include #include +#include + +#include +#include +#include + namespace launchdarkly::events { using Value = launchdarkly::Value; @@ -36,4 +36,47 @@ struct TrackEventParams { // Track (custom) events are directly serialized from their parameters. using TrackEvent = TrackEventParams; +struct IdentifyEventParams { + Date creation_date; + Context context; +}; + +struct IdentifyEvent { + Date creation_date; + EventContext context; +}; + +struct FeatureEventParams { + Date creation_date; + std::string key; + Context context; + 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; + std::optional version; + std::optional variation; + Value value; + std::optional reason; + Value default_; + + explicit FeatureEventBase(FeatureEventParams const& params); +}; + +struct FeatureEvent : public FeatureEventBase { + ContextKeys context_keys; +}; + +struct DebugEvent : public FeatureEventBase { + EventContext context; +}; + } // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/data/events.hpp b/libs/internal/include/launchdarkly/events/data/events.hpp new file mode 100644 index 000000000..1f474f2de --- /dev/null +++ b/libs/internal/include/launchdarkly/events/data/events.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace launchdarkly::events { + +using InputEvent = + std::variant; + +using OutputEvent = std::variant; + +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/data/server_events.hpp b/libs/internal/include/launchdarkly/events/data/server_events.hpp new file mode 100644 index 000000000..393f0ff0f --- /dev/null +++ b/libs/internal/include/launchdarkly/events/data/server_events.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace launchdarkly::events::server_side { + +struct IndexEvent { + Date creation_date; + EventContext context; +}; + +} // namespace launchdarkly::events::server_side diff --git a/libs/internal/include/launchdarkly/events/event_batch.hpp b/libs/internal/include/launchdarkly/events/detail/event_batch.hpp similarity index 92% rename from libs/internal/include/launchdarkly/events/event_batch.hpp rename to libs/internal/include/launchdarkly/events/detail/event_batch.hpp index 10af02fce..71a866cdf 100644 --- a/libs/internal/include/launchdarkly/events/event_batch.hpp +++ b/libs/internal/include/launchdarkly/events/detail/event_batch.hpp @@ -1,11 +1,13 @@ #pragma once -#include #include #include + +#include + #include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * EventBatch represents a batch of events being sent to LaunchDarkly as @@ -43,4 +45,4 @@ class EventBatch { network::HttpRequest request_; }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/lru_cache.hpp b/libs/internal/include/launchdarkly/events/detail/lru_cache.hpp similarity index 91% rename from libs/internal/include/launchdarkly/events/lru_cache.hpp rename to libs/internal/include/launchdarkly/events/detail/lru_cache.hpp index 0714507e6..423831dfe 100644 --- a/libs/internal/include/launchdarkly/events/lru_cache.hpp +++ b/libs/internal/include/launchdarkly/events/detail/lru_cache.hpp @@ -1,8 +1,10 @@ #pragma once + #include #include #include -namespace launchdarkly::events { + +namespace launchdarkly::events::detail { class LRUCache { public: @@ -38,4 +40,4 @@ class LRUCache { KeyList list_; }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/outbox.hpp b/libs/internal/include/launchdarkly/events/detail/outbox.hpp similarity index 90% rename from libs/internal/include/launchdarkly/events/outbox.hpp rename to libs/internal/include/launchdarkly/events/detail/outbox.hpp index b51dc0fcf..64712a183 100644 --- a/libs/internal/include/launchdarkly/events/outbox.hpp +++ b/libs/internal/include/launchdarkly/events/detail/outbox.hpp @@ -1,11 +1,12 @@ #pragma once +#include + #include #include #include -#include "events.hpp" -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * Represents a fixed-size queue for holding output events, which are events @@ -49,4 +50,4 @@ class Outbox { bool Push(OutputEvent item); }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/parse_date_header.hpp b/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp similarity index 93% rename from libs/internal/include/launchdarkly/events/parse_date_header.hpp rename to libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp index 2d78ab43b..972270c0b 100644 --- a/libs/internal/include/launchdarkly/events/parse_date_header.hpp +++ b/libs/internal/include/launchdarkly/events/detail/parse_date_header.hpp @@ -5,7 +5,7 @@ #include #include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { template static std::optional ParseDateHeader( @@ -40,4 +40,4 @@ static std::optional ParseDateHeader( return Clock::from_time_t(real_gm_t); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/request_worker.hpp b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp similarity index 97% rename from libs/internal/include/launchdarkly/events/request_worker.hpp rename to libs/internal/include/launchdarkly/events/detail/request_worker.hpp index 22f846469..d4fac42f3 100644 --- a/libs/internal/include/launchdarkly/events/request_worker.hpp +++ b/libs/internal/include/launchdarkly/events/detail/request_worker.hpp @@ -9,9 +9,9 @@ #include #include -#include "event_batch.hpp" +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { enum class State { /* Worker is ready for a new job. */ @@ -168,4 +168,4 @@ class RequestWorker { void OnDeliveryAttempt(network::HttpResult request, ResultCallback cb); }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/summarizer.hpp b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp similarity index 92% rename from libs/internal/include/launchdarkly/events/summarizer.hpp rename to libs/internal/include/launchdarkly/events/detail/summarizer.hpp index ce6a553fd..37a23ef72 100644 --- a/libs/internal/include/launchdarkly/events/summarizer.hpp +++ b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp @@ -7,11 +7,11 @@ #include #include -#include +#include "launchdarkly/value.hpp" -#include "events.hpp" +#include "launchdarkly/events/data/events.hpp" -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * Summarizer is responsible for accepting FeatureEventParams (the context @@ -39,7 +39,7 @@ class Summarizer { * Updates the summary with a feature event. * @param event Feature event. */ - void Update(client::FeatureEventParams const& event); + void Update(events::FeatureEventParams const& event); /** * Marks the summary as finished at a given timestamp. @@ -111,4 +111,4 @@ class Summarizer { std::unordered_map features_; }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/include/launchdarkly/events/worker_pool.hpp b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp similarity index 93% rename from libs/internal/include/launchdarkly/events/worker_pool.hpp rename to libs/internal/include/launchdarkly/events/detail/worker_pool.hpp index bb66b2ff2..9b29e32df 100644 --- a/libs/internal/include/launchdarkly/events/worker_pool.hpp +++ b/libs/internal/include/launchdarkly/events/detail/worker_pool.hpp @@ -1,5 +1,10 @@ #pragma once +#include +#include +#include +#include + #include #include @@ -7,13 +12,7 @@ #include #include -#include -#include -#include - -#include "request_worker.hpp" - -namespace launchdarkly::events { +namespace launchdarkly::events::detail { /** * WorkerPool represents a pool of workers capable of delivering event payloads @@ -73,4 +72,4 @@ class WorkerPool { std::vector> workers_; }; -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/client-sdk/src/event_processor.hpp b/libs/internal/include/launchdarkly/events/event_processor_interface.hpp similarity index 89% rename from libs/client-sdk/src/event_processor.hpp rename to libs/internal/include/launchdarkly/events/event_processor_interface.hpp index 9b6b25d0c..4e1a9e631 100644 --- a/libs/client-sdk/src/event_processor.hpp +++ b/libs/internal/include/launchdarkly/events/event_processor_interface.hpp @@ -1,8 +1,8 @@ #pragma once -#include +#include -namespace launchdarkly::client_side { +namespace launchdarkly::events { class IEventProcessor { public: @@ -34,4 +34,4 @@ class IEventProcessor { IEventProcessor() = default; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/events.hpp b/libs/internal/include/launchdarkly/events/events.hpp deleted file mode 100644 index aeaea92ba..000000000 --- a/libs/internal/include/launchdarkly/events/events.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#include "client_events.hpp" -#include "server_events.hpp" - -namespace launchdarkly::events { - -using InputEvent = std::variant; - -using OutputEvent = std::variant; - -} // namespace launchdarkly::events diff --git a/libs/client-sdk/src/event_processor/null_event_processor.hpp b/libs/internal/include/launchdarkly/events/null_event_processor.hpp similarity index 64% rename from libs/client-sdk/src/event_processor/null_event_processor.hpp rename to libs/internal/include/launchdarkly/events/null_event_processor.hpp index 1cb615302..8f228fb54 100644 --- a/libs/client-sdk/src/event_processor/null_event_processor.hpp +++ b/libs/internal/include/launchdarkly/events/null_event_processor.hpp @@ -1,8 +1,8 @@ #pragma once -#include "../event_processor.hpp" +#include -namespace launchdarkly::client_side { +namespace launchdarkly::events { class NullEventProcessor : public IEventProcessor { public: @@ -11,4 +11,4 @@ class NullEventProcessor : public IEventProcessor { void FlushAsync() override; void ShutdownAsync() override; }; -} // namespace launchdarkly::client_side +} // namespace launchdarkly::events diff --git a/libs/internal/include/launchdarkly/events/server_events.hpp b/libs/internal/include/launchdarkly/events/server_events.hpp deleted file mode 100644 index a7b566a2b..000000000 --- a/libs/internal/include/launchdarkly/events/server_events.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "common_events.hpp" - -namespace launchdarkly::events::server { - -struct IndexEvent { - Date creation_date; - EventContext context; -}; - -} // namespace launchdarkly::events::server diff --git a/libs/internal/include/launchdarkly/serialization/events/json_events.hpp b/libs/internal/include/launchdarkly/serialization/events/json_events.hpp index cc819a0b7..1578eed07 100644 --- a/libs/internal/include/launchdarkly/serialization/events/json_events.hpp +++ b/libs/internal/include/launchdarkly/serialization/events/json_events.hpp @@ -1,11 +1,19 @@ #pragma once +#include +#include + #include -#include -#include +namespace launchdarkly::events::server_side { + +void tag_invoke(boost::json::value_from_tag const&, + boost::json::value& json_value, + IndexEvent const& event); +} // namespace launchdarkly::events::server_side + +namespace launchdarkly::events { -namespace launchdarkly::events::client { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, FeatureEvent const& event); @@ -21,16 +29,6 @@ void tag_invoke(boost::json::value_from_tag const&, void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, DebugEvent const& event); -} // namespace launchdarkly::events::client - -namespace launchdarkly::events::server { - -void tag_invoke(boost::json::value_from_tag const&, - boost::json::value& json_value, - IndexEvent const& event); -} // namespace launchdarkly::events::server - -namespace launchdarkly::events { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, @@ -46,7 +44,7 @@ void tag_invoke(boost::json::value_from_tag const&, } // namespace launchdarkly::events -namespace launchdarkly::events { +namespace launchdarkly::events::detail { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, @@ -54,4 +52,4 @@ void tag_invoke(boost::json::value_from_tag const&, void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, Summarizer const& summary); -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/CMakeLists.txt b/libs/internal/src/CMakeLists.txt index eabd90097..e156a8e72 100644 --- a/libs/internal/src/CMakeLists.txt +++ b/libs/internal/src/CMakeLists.txt @@ -14,7 +14,8 @@ add_library(${LIBNAME} OBJECT ${HEADER_LIST} context_filter.cpp events/asio_event_processor.cpp - events/client_events.cpp + events/null_event_processor.cpp + events/common_events.cpp events/event_batch.cpp events/outbox.cpp events/request_worker.cpp diff --git a/libs/internal/src/events/asio_event_processor.cpp b/libs/internal/src/events/asio_event_processor.cpp index 216f98640..734068735 100644 --- a/libs/internal/src/events/asio_event_processor.cpp +++ b/libs/internal/src/events/asio_event_processor.cpp @@ -11,7 +11,7 @@ #include #include -#include +#include namespace http = boost::beast::http; namespace launchdarkly::events { @@ -90,7 +90,7 @@ void AsioEventProcessor::InboxDecrement() { } template -void AsioEventProcessor::AsyncSend(InputEvent input_event) { +void AsioEventProcessor::SendAsync(InputEvent input_event) { if (!InboxIncrement()) { return; } @@ -115,7 +115,7 @@ void AsioEventProcessor::HandleSend(InputEvent event) { template void AsioEventProcessor::Flush(FlushTrigger flush_type) { - workers_.Get([this](RequestWorker* worker) { + workers_.Get([this](detail::RequestWorker* worker) { if (worker == nullptr) { LD_LOG(logger_, LogLevel::kDebug) << "event-processor: no flush workers available; skipping " @@ -130,10 +130,11 @@ void AsioEventProcessor::Flush(FlushTrigger flush_type) { } worker->AsyncDeliver( std::move(*batch), - [this](std::size_t count, RequestWorker::DeliveryResult result) { + [this](std::size_t count, + detail::RequestWorker::DeliveryResult result) { OnEventDeliveryResult(count, result); }); - summarizer_ = Summarizer(Clock::now()); + summarizer_ = detail::Summarizer(Clock::now()); }); if (flush_type == FlushTrigger::Automatic) { @@ -144,7 +145,7 @@ void AsioEventProcessor::Flush(FlushTrigger flush_type) { template void AsioEventProcessor::OnEventDeliveryResult( std::size_t event_count, - RequestWorker::DeliveryResult result) { + detail::RequestWorker::DeliveryResult result) { boost::ignore_unused(event_count); std::visit( @@ -179,17 +180,17 @@ void AsioEventProcessor::ScheduleFlush() { } template -void AsioEventProcessor::AsyncFlush() { +void AsioEventProcessor::FlushAsync() { boost::asio::post(io_, [=] { Flush(FlushTrigger::Manual); }); } template -void AsioEventProcessor::AsyncClose() { +void AsioEventProcessor::ShutdownAsync() { timer_.cancel(); } template -std::optional AsioEventProcessor::CreateBatch() { +std::optional AsioEventProcessor::CreateBatch() { auto events = boost::json::value_from(outbox_.Consume()).as_array(); bool has_summary = @@ -208,7 +209,7 @@ std::optional AsioEventProcessor::CreateBatch() { props.Header(kPayloadIdHeader, boost::lexical_cast(uuids_())); props.Header(to_string(http::field::content_type), "application/json"); - return EventBatch(url_, props.Build(), events); + return detail::EventBatch(url_, props.Build(), events); } template @@ -217,20 +218,20 @@ std::vector AsioEventProcessor::Process( std::vector out; std::visit( overloaded{ - [&](client::FeatureEventParams&& event) { + [&](FeatureEventParams&& event) { summarizer_.Update(event); if constexpr (std::is_same::value) { if (!context_key_cache_.Notice( event.context.CanonicalKey())) { - out.emplace_back( - server::IndexEvent{event.creation_date, - filter_.filter(event.context)}); + out.emplace_back(server_side::IndexEvent{ + event.creation_date, + filter_.filter(event.context)}); } } - client::FeatureEventBase base{event}; + FeatureEventBase base{event}; auto debug_until_date = event.debug_events_until_date; @@ -248,16 +249,16 @@ std::vector AsioEventProcessor::Process( debug_until_date && conservative_now < debug_until_date->t; if (emit_debug_event) { - out.emplace_back(client::DebugEvent{ - base, filter_.filter(event.context)}); + out.emplace_back( + DebugEvent{base, filter_.filter(event.context)}); } if (event.require_full_event) { - out.emplace_back(client::FeatureEvent{ - std::move(base), event.context.KindsToKeys()}); + out.emplace_back(FeatureEvent{std::move(base), + event.context.KindsToKeys()}); } }, - [&](client::IdentifyEventParams&& event) { + [&](IdentifyEventParams&& event) { // Contexts should already have been checked for // validity by this point. assert(event.context.Valid()); @@ -267,8 +268,8 @@ std::vector AsioEventProcessor::Process( context_key_cache_.Notice(event.context.CanonicalKey()); } - out.emplace_back(client::IdentifyEvent{ - event.creation_date, filter_.filter(event.context)}); + out.emplace_back(IdentifyEvent{event.creation_date, + filter_.filter(event.context)}); }, [&](TrackEventParams&& event) { out.emplace_back(std::move(event)); @@ -279,5 +280,6 @@ std::vector AsioEventProcessor::Process( } template class AsioEventProcessor; +template class AsioEventProcessor; } // namespace launchdarkly::events diff --git a/libs/internal/src/events/client_events.cpp b/libs/internal/src/events/common_events.cpp similarity index 68% rename from libs/internal/src/events/client_events.cpp rename to libs/internal/src/events/common_events.cpp index 33c4dcce4..d3d324ec8 100644 --- a/libs/internal/src/events/client_events.cpp +++ b/libs/internal/src/events/common_events.cpp @@ -1,6 +1,6 @@ -#include +#include -namespace launchdarkly::events::client { +namespace launchdarkly::events { FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) : creation_date(params.creation_date), key(params.key), @@ -10,4 +10,4 @@ FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) reason(params.reason), default_(params.default_) {} -} // namespace launchdarkly::events::client +} // namespace launchdarkly::events diff --git a/libs/internal/src/events/event_batch.cpp b/libs/internal/src/events/event_batch.cpp index ade9ad4ae..710affba7 100644 --- a/libs/internal/src/events/event_batch.cpp +++ b/libs/internal/src/events/event_batch.cpp @@ -1,8 +1,8 @@ -#include +#include #include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { EventBatch::EventBatch(std::string url, config::shared::built::HttpProperties http_props, boost::json::value const& events) @@ -24,4 +24,4 @@ std::string EventBatch::Target() const { return request_.Url(); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/lru_cache.cpp b/libs/internal/src/events/lru_cache.cpp index ad39bd0b4..4b92375e9 100644 --- a/libs/internal/src/events/lru_cache.cpp +++ b/libs/internal/src/events/lru_cache.cpp @@ -1,6 +1,6 @@ -#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { LRUCache::LRUCache(std::size_t capacity) : capacity_(capacity), map_(), list_() {} @@ -29,4 +29,4 @@ std::size_t LRUCache::Size() const { return list_.size(); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/client-sdk/src/event_processor/null_event_processor.cpp b/libs/internal/src/events/null_event_processor.cpp similarity index 54% rename from libs/client-sdk/src/event_processor/null_event_processor.cpp rename to libs/internal/src/events/null_event_processor.cpp index b12e710ca..3f6352515 100644 --- a/libs/client-sdk/src/event_processor/null_event_processor.cpp +++ b/libs/internal/src/events/null_event_processor.cpp @@ -1,10 +1,10 @@ -#include "null_event_processor.hpp" +#include -namespace launchdarkly::client_side { +namespace launchdarkly::events { void NullEventProcessor::SendAsync(events::InputEvent event) {} void NullEventProcessor::FlushAsync() {} void NullEventProcessor::ShutdownAsync() {} -} // namespace launchdarkly::client_side +} // namespace launchdarkly::events diff --git a/libs/internal/src/events/outbox.cpp b/libs/internal/src/events/outbox.cpp index 233e1e0f9..7b131670a 100644 --- a/libs/internal/src/events/outbox.cpp +++ b/libs/internal/src/events/outbox.cpp @@ -1,6 +1,6 @@ -#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { Outbox::Outbox(std::size_t capacity) : items_(), capacity_(capacity) {} @@ -38,4 +38,4 @@ bool Outbox::Empty() { return items_.empty(); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/request_worker.cpp b/libs/internal/src/events/request_worker.cpp index a18ed8675..ac8c32d79 100644 --- a/libs/internal/src/events/request_worker.cpp +++ b/libs/internal/src/events/request_worker.cpp @@ -1,7 +1,7 @@ -#include -#include +#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { RequestWorker::RequestWorker(boost::asio::any_io_executor io, std::chrono::milliseconds retry_after, @@ -207,4 +207,4 @@ std::ostream& operator<<(std::ostream& out, Action const& s) { return out; } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/summarizer.cpp b/libs/internal/src/events/summarizer.cpp index a196b5473..1c3ad7202 100644 --- a/libs/internal/src/events/summarizer.cpp +++ b/libs/internal/src/events/summarizer.cpp @@ -1,6 +1,6 @@ -#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { Summarizer::Summarizer(std::chrono::system_clock::time_point start) : start_time_(start) {} @@ -14,7 +14,7 @@ Summarizer::Features() const { return features_; } -void Summarizer::Update(client::FeatureEventParams const& event) { +void Summarizer::Update(events::FeatureEventParams const& event) { auto const& kinds = event.context.Kinds(); auto feature_state_iterator = @@ -69,4 +69,4 @@ std::int32_t Summarizer::VariationSummary::Count() const { Summarizer::State::State(Value default_value) : default_(std::move(default_value)) {} -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/events/worker_pool.cpp b/libs/internal/src/events/worker_pool.cpp index 19ff94b2d..105495310 100644 --- a/libs/internal/src/events/worker_pool.cpp +++ b/libs/internal/src/events/worker_pool.cpp @@ -1,9 +1,9 @@ #include -#include -#include +#include +#include -namespace launchdarkly::events { +namespace launchdarkly::events::detail { WorkerPool::WorkerPool(boost::asio::any_io_executor io, std::size_t pool_size, @@ -16,4 +16,4 @@ WorkerPool::WorkerPool(boost::asio::any_io_executor io, } } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/src/serialization/events/json_events.cpp b/libs/internal/src/serialization/events/json_events.cpp index a57aaa155..ad5f3b8ee 100644 --- a/libs/internal/src/serialization/events/json_events.cpp +++ b/libs/internal/src/serialization/events/json_events.cpp @@ -2,7 +2,7 @@ #include #include -namespace launchdarkly::events::client { +namespace launchdarkly::events { void tag_invoke(boost::json::value_from_tag const& tag, boost::json::value& json_value, FeatureEvent const& event) { @@ -49,9 +49,9 @@ void tag_invoke(boost::json::value_from_tag const& tag, obj.emplace("creationDate", boost::json::value_from(event.creation_date)); obj.emplace("context", event.context); } -} // namespace launchdarkly::events::client +} // namespace launchdarkly::events -namespace launchdarkly::events::server { +namespace launchdarkly::events::server_side { void tag_invoke(boost::json::value_from_tag const&, boost::json::value& json_value, @@ -61,7 +61,7 @@ void tag_invoke(boost::json::value_from_tag const&, obj.emplace("creationDate", boost::json::value_from(event.creation_date)); obj.emplace("context", event.context); } -} // namespace launchdarkly::events::server +} // namespace launchdarkly::events::server_side namespace launchdarkly::events { @@ -100,7 +100,7 @@ void tag_invoke(boost::json::value_from_tag const& tag, } // namespace launchdarkly::events -namespace launchdarkly::events { +namespace launchdarkly::events::detail { void tag_invoke(boost::json::value_from_tag const& tag, boost::json::value& json_value, @@ -135,4 +135,4 @@ void tag_invoke(boost::json::value_from_tag const& tag, obj.emplace("endDate", boost::json::value_from(Date{summary.end_time()})); obj.emplace("features", boost::json::value_from(summary.Features())); } -} // namespace launchdarkly::events +} // namespace launchdarkly::events::detail diff --git a/libs/internal/tests/event_processor_test.cpp b/libs/internal/tests/event_processor_test.cpp index d210f8e8c..f8962ee08 100644 --- a/libs/internal/tests/event_processor_test.cpp +++ b/libs/internal/tests/event_processor_test.cpp @@ -7,12 +7,11 @@ #include #include #include -#include -#include -#include +#include #include using namespace launchdarkly::events; +using namespace launchdarkly::events::detail; using namespace launchdarkly::network; static std::chrono::system_clock::time_point TimeZero() { @@ -82,16 +81,16 @@ TEST(EventProcessorTests, ProcessorCompiles) { auto context = launchdarkly::ContextBuilder().Kind("org", "ld").Build(); ASSERT_TRUE(context.Valid()); - auto identify_event = events::client::IdentifyEventParams{ + auto identify_event = events::IdentifyEventParams{ std::chrono::system_clock::now(), context, }; for (std::size_t i = 0; i < 10; i++) { - processor.AsyncSend(identify_event); + processor.SendAsync(identify_event); } - processor.AsyncClose(); + processor.ShutdownAsync(); ioc_thread.join(); } @@ -99,7 +98,7 @@ TEST(EventProcessorTests, ParseValidDateHeader) { using namespace launchdarkly; using Clock = std::chrono::system_clock; - auto date = events::ParseDateHeader("Wed, 21 Oct 2015 07:28:00 GMT"); + auto date = detail::ParseDateHeader("Wed, 21 Oct 2015 07:28:00 GMT"); ASSERT_TRUE(date); @@ -110,17 +109,17 @@ TEST(EventProcessorTests, ParseValidDateHeader) { TEST(EventProcessorTests, ParseInvalidDateHeader) { using namespace launchdarkly; - auto not_a_date = events::ParseDateHeader( + auto not_a_date = detail::ParseDateHeader( "this is definitely not a date"); ASSERT_FALSE(not_a_date); - auto not_gmt = events::ParseDateHeader( + auto not_gmt = detail::ParseDateHeader( "Wed, 21 Oct 2015 07:28:00 PST"); ASSERT_FALSE(not_gmt); - auto missing_year = events::ParseDateHeader( + auto missing_year = detail::ParseDateHeader( "Wed, 21 Oct 07:28:00 GMT"); ASSERT_FALSE(missing_year); diff --git a/libs/internal/tests/event_serialization_test.cpp b/libs/internal/tests/event_serialization_test.cpp index f34e0ff83..f1f1cddfc 100644 --- a/libs/internal/tests/event_serialization_test.cpp +++ b/libs/internal/tests/event_serialization_test.cpp @@ -3,18 +3,16 @@ #include #include -#include - #include +#include #include -#include namespace launchdarkly::events { TEST(EventSerialization, FeatureEvent) { auto creation_date = std::chrono::system_clock::from_time_t({}); - auto event = events::client::FeatureEvent{ - client::FeatureEventBase(client::FeatureEventParams{ + auto event = events::FeatureEvent{ + events::FeatureEventBase(events::FeatureEventParams{ creation_date, "key", ContextBuilder().Kind("foo", "bar").Build(), @@ -42,9 +40,9 @@ TEST(EventSerialization, DebugEvent) { AttributeReference::SetType attrs; ContextFilter filter(false, attrs); auto context = ContextBuilder().Kind("foo", "bar").Build(); - auto event = events::client::DebugEvent{ - client::FeatureEventBase( - client::FeatureEventBase(client::FeatureEventParams{ + auto event = events::DebugEvent{ + events::FeatureEventBase( + events::FeatureEventBase(events::FeatureEventParams{ creation_date, "key", ContextBuilder().Kind("foo", "bar").Build(), @@ -71,7 +69,7 @@ TEST(EventSerialization, IdentifyEvent) { auto creation_date = std::chrono::system_clock::from_time_t({}); AttributeReference::SetType attrs; ContextFilter filter(false, attrs); - auto event = events::client::IdentifyEvent{ + auto event = events::IdentifyEvent{ creation_date, filter.filter(ContextBuilder().Kind("foo", "bar").Build())}; @@ -86,7 +84,7 @@ TEST(EventSerialization, IndexEvent) { auto creation_date = std::chrono::system_clock::from_time_t({}); AttributeReference::SetType attrs; ContextFilter filter(false, attrs); - auto event = events::server::IndexEvent{ + auto event = events::server_side::IndexEvent{ creation_date, filter.filter(ContextBuilder().Kind("foo", "bar").Build())}; diff --git a/libs/internal/tests/event_summarizer_test.cpp b/libs/internal/tests/event_summarizer_test.cpp index 2922f9b2d..ad3e59e9e 100644 --- a/libs/internal/tests/event_summarizer_test.cpp +++ b/libs/internal/tests/event_summarizer_test.cpp @@ -2,15 +2,14 @@ #include #include #include -#include -#include +#include #include #include #include +#include "launchdarkly/events/detail/summarizer.hpp" using namespace launchdarkly::events; -using namespace launchdarkly::events::client; -using namespace launchdarkly::events; +using namespace launchdarkly::events::detail; static std::chrono::system_clock::time_point TimeZero() { return std::chrono::system_clock::time_point{}; @@ -292,7 +291,7 @@ INSTANTIATE_TEST_SUITE_P( {Summarizer::VariationKey(1, 1), 1}}}}})); TEST(SummarizerTests, MissingFlagCreatesCounterUsingDefaultValue) { - using namespace launchdarkly::events::client; + using namespace launchdarkly::events; using namespace launchdarkly; Summarizer summarizer; @@ -340,7 +339,7 @@ TEST(SummarizerTests, MissingFlagCreatesCounterUsingDefaultValue) { } TEST(SummarizerTests, JsonSerialization) { - using namespace launchdarkly::events::client; + using namespace launchdarkly::events; using namespace launchdarkly; Summarizer summarizer; diff --git a/libs/internal/tests/lru_cache_test.cpp b/libs/internal/tests/lru_cache_test.cpp index 1dd6a8ea9..e1cf64ee0 100644 --- a/libs/internal/tests/lru_cache_test.cpp +++ b/libs/internal/tests/lru_cache_test.cpp @@ -1,7 +1,7 @@ +#include "launchdarkly/events/detail/lru_cache.hpp" #include -#include -using namespace launchdarkly::events; +using namespace launchdarkly::events::detail; TEST(ContextKeyCacheTests, CacheSizeOne) { LRUCache cache(1); diff --git a/libs/internal/tests/request_worker_test.cpp b/libs/internal/tests/request_worker_test.cpp index d38b665e9..645769fad 100644 --- a/libs/internal/tests/request_worker_test.cpp +++ b/libs/internal/tests/request_worker_test.cpp @@ -1,8 +1,9 @@ +#include "launchdarkly/events/detail/request_worker.hpp" #include -#include #include using namespace launchdarkly::events; +using namespace launchdarkly::events::detail; using namespace launchdarkly::network; struct TestCase { diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp new file mode 100644 index 000000000..71614f4cd --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -0,0 +1,331 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +/** + * Interface for the standard SDK client methods and properties. + */ +class IClient { + public: + /** + * Represents the key of a feature flag. + */ + using FlagKey = std::string; + + /** Connects the client to LaunchDarkly's flag delivery endpoints. + * + * If StartAsync isn't called, the client is able to post events but is + * unable to obtain flag data. + * + * The returned future will resolve to true or false based on the logic + * outlined on @ref Initialized. + */ + virtual std::future StartAsync() = 0; + + /** + * Returns a boolean value indicating LaunchDarkly connection and flag state + * within the client. + * + * When you first start the client, once StartAsync has completed, + * Initialized should return true if and only if either 1. it connected to + * LaunchDarkly and successfully retrieved flags, or 2. it started in + * offline mode so there's no need to connect to LaunchDarkly. If the client + * timed out trying to connect to LD, then Initialized returns false (even + * if we do have cached flags). If the client connected and got a 401 error, + * Initialized is will return false. This serves the purpose of letting the + * app know that there was a problem of some kind. + * + * @return True if the client is initialized. + */ + [[nodiscard]] virtual bool Initialized() const = 0; + + /** + * Returns a map from feature flag keys to feature + * flag values for the current context. + * + * This method will not send analytics events back to LaunchDarkly. + * + * @return A map from feature flag keys to values for the current context. + */ + [[nodiscard]] virtual std::unordered_map AllFlagsState() + const = 0; + + /** + * Tracks that the current context performed an event for the given event + * name, and associates it with a numeric metric value. + * + * @param event_name The name of the event. + * @param data A JSON value containing additional data associated with the + * event. + * @param metric_value this value is used by the LaunchDarkly + * experimentation feature in numeric custom metrics, and will also be + * returned as part of the custom event for Data Export + */ + virtual void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) = 0; + + /** + * Tracks that the current context performed an event for the given event + * name, with additional JSON data. + * + * @param event_name The name of the event. + * @param data A JSON value containing additional data associated with the + * event. + */ + virtual void Track(Context const& ctx, + std::string event_name, + Value data) = 0; + + /** + * Tracks that the current context performed an event for the given event + * name. + * + * @param event_name The name of the event. + */ + virtual void Track(Context const& ctx, std::string event_name) = 0; + + /** + * Tells the client that all pending analytics events (if any) should be + * delivered as soon as possible. + */ + virtual void FlushAsync() = 0; + + /** + * Generates an identify event for a context. + * + * @param context The new evaluation context. + */ + + virtual void Identify(Context context) = 0; + + /** + * Returns the boolean value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) = 0; + + /** + * Returns the boolean value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) = 0; + + /** + * Returns the string value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) = 0; + + /** + * Returns the string value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) = 0; + + /** + * Returns the double value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) = 0; + + /** + * Returns the double value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value) = 0; + + /** + * Returns the int value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) = 0; + + /** + * Returns the int value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) = 0; + + /** + * Returns the JSON value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) = 0; + + /** + * Returns the JSON value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @return An evaluation detail object. + */ + virtual EvaluationDetail JsonVariationDetail( + Context const& ctx, + FlagKey const& key, + Value default_value) = 0; + + virtual ~IClient() = default; + IClient(IClient const& item) = delete; + IClient(IClient&& item) = delete; + IClient& operator=(IClient const&) = delete; + IClient& operator=(IClient&&) = delete; + + protected: + IClient() = default; +}; + +class Client : public IClient { + public: + Client(Config config); + + Client(Client&&) = delete; + Client(Client const&) = delete; + Client& operator=(Client) = delete; + Client& operator=(Client&& other) = delete; + + std::future StartAsync() override; + + [[nodiscard]] bool Initialized() const override; + + using FlagKey = std::string; + [[nodiscard]] std::unordered_map AllFlagsState() + const override; + + void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) override; + + void Track(Context const& ctx, std::string event_name, Value data) override; + + void Track(Context const& ctx, std::string event_name) override; + + void FlushAsync() override; + + void Identify(Context context) override; + + bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + EvaluationDetail BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) override; + + EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value) override; + + int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) override; + + EvaluationDetail IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) override; + + Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + EvaluationDetail JsonVariationDetail(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + /** + * Returns the version of the SDK. + * @return String representing version of the SDK. + */ + [[nodiscard]] static char const* Version(); + + private: + inline static char const* const kVersion = + "0.1.0"; // {x-release-please-version} + std::unique_ptr client; +}; + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 0c298e70d..b984ea1b7 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -8,6 +8,8 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS add_library(${LIBNAME} ${HEADER_LIST} boost.cpp + client.cpp + client_impl.cpp data_sources/data_source_update_sink.hpp data_store/data_store.hpp data_store/data_store_updater.hpp @@ -24,6 +26,7 @@ add_library(${LIBNAME} data_sources/data_source_status_manager.hpp data_sources/streaming_data_source.hpp data_sources/streaming_data_source.cpp + data_sources/null_data_source.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp diff --git a/libs/server-sdk/src/client.cpp b/libs/server-sdk/src/client.cpp new file mode 100644 index 000000000..c9b84acde --- /dev/null +++ b/libs/server-sdk/src/client.cpp @@ -0,0 +1,111 @@ +#include + +#include "client_impl.hpp" + +namespace launchdarkly::server_side { + +Client::Client(Config config) + : client(std::make_unique(std::move(config), kVersion)) {} + +bool Client::Initialized() const { + return client->Initialized(); +} + +std::future Client::StartAsync() { + return client->StartAsync(); +} + +using FlagKey = std::string; +[[nodiscard]] std::unordered_map Client::AllFlagsState() const { + return client->AllFlagsState(); +} + +void Client::Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) { + client->Track(ctx, std::move(event_name), std::move(data), metric_value); +} + +void Client::Track(Context const& ctx, std::string event_name, Value data) { + client->Track(ctx, std::move(event_name), std::move(data)); +} + +void Client::Track(Context const& ctx, std::string event_name) { + client->Track(ctx, std::move(event_name)); +} + +void Client::FlushAsync() { + client->FlushAsync(); +} + +void Client::Identify(Context context) { + return client->Identify(std::move(context)); +} + +bool Client::BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) { + return client->BoolVariation(ctx, key, default_value); +} + +EvaluationDetail Client::BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) { + return client->BoolVariationDetail(ctx, key, default_value); +} + +std::string Client::StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) { + return client->StringVariation(ctx, key, std::move(default_value)); +} + +EvaluationDetail Client::StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) { + return client->StringVariationDetail(ctx, key, std::move(default_value)); +} + +double Client::DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) { + return client->DoubleVariation(ctx, key, default_value); +} + +EvaluationDetail Client::DoubleVariationDetail(Context const& ctx, + FlagKey const& key, + double default_value) { + return client->DoubleVariationDetail(ctx, key, default_value); +} + +int Client::IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) { + return client->IntVariation(ctx, key, default_value); +} + +EvaluationDetail Client::IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) { + return client->IntVariationDetail(ctx, key, default_value); +} + +Value Client::JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) { + return client->JsonVariation(ctx, key, std::move(default_value)); +} + +EvaluationDetail Client::JsonVariationDetail(Context const& ctx, + FlagKey const& key, + Value default_value) { + return client->JsonVariationDetail(ctx, key, std::move(default_value)); +} + +char const* Client::Version() { + return kVersion; +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp new file mode 100644 index 000000000..9cd59eb22 --- /dev/null +++ b/libs/server-sdk/src/client_impl.cpp @@ -0,0 +1,332 @@ + +#include + +#include +#include + +#include "client_impl.hpp" + +#include "data_sources/null_data_source.hpp" +#include "data_sources/polling_data_source.hpp" +#include "data_sources/streaming_data_source.hpp" +#include "data_store/memory_store.hpp" + +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +// The ASIO implementation assumes that the io_context will be run from a +// single thread, and applies several optimisations based on this +// assumption. +auto const kAsioConcurrencyHint = 1; + +// Client's destructor attempts to gracefully shut down the datasource +// connection in this amount of time. +auto const kDataSourceShutdownWait = std::chrono::milliseconds(100); + +using config::shared::ServerSDK; +using launchdarkly::config::shared::built::DataSourceConfig; +using launchdarkly::config::shared::built::HttpProperties; +using launchdarkly::server_side::data_sources::DataSourceStatus; + +static std::shared_ptr<::launchdarkly::data_sources::IDataSource> +MakeDataSource(HttpProperties const& http_properties, + Config const& config, + boost::asio::any_io_executor const& executor, + data_sources::IDataSourceUpdateSink& flag_updater, + data_sources::DataSourceStatusManager& status_manager, + Logger& logger) { + if (config.Offline()) { + return std::make_shared(executor, + status_manager); + } + + auto builder = HttpPropertiesBuilder(http_properties); + + auto data_source_properties = builder.Build(); + + if (config.DataSourceConfig().method.index() == 0) { + // TODO: use initial reconnect delay. + return std::make_shared< + launchdarkly::server_side::data_sources::StreamingDataSource>( + config.ServiceEndpoints(), config.DataSourceConfig(), + data_source_properties, executor, flag_updater, status_manager, + logger); + } + return std::make_shared< + launchdarkly::server_side::data_sources::PollingDataSource>( + config.ServiceEndpoints(), config.DataSourceConfig(), + data_source_properties, executor, flag_updater, status_manager, logger); +} + +static Logger MakeLogger(config::shared::built::Logging const& config) { + if (config.disable_logging) { + return {std::make_shared()}; + } + if (config.backend) { + return {config.backend}; + } + return { + std::make_shared(config.level, config.tag)}; +} + +ClientImpl::ClientImpl(Config config, std::string const& version) + : config_(config), + http_properties_( + HttpPropertiesBuilder(config.HttpProperties()) + .Header("user-agent", "CPPClient/" + version) + .Header("authorization", config.SdkKey()) + .Header("x-launchdarkly-tags", config.ApplicationTag()) + .Build()), + logger_(MakeLogger(config.Logging())), + ioc_(kAsioConcurrencyHint), + work_(boost::asio::make_work_guard(ioc_)), + memory_store_(), + data_source_(MakeDataSource(http_properties_, + config_, + ioc_.get_executor(), + memory_store_, + status_manager_, + logger_)), + event_processor_(nullptr), + evaluator_(logger_, memory_store_) { + if (config.Events().Enabled() && !config.Offline()) { + event_processor_ = + std::make_unique>( + ioc_.get_executor(), config.ServiceEndpoints(), config.Events(), + http_properties_, logger_); + } else { + event_processor_ = std::make_unique(); + } + + run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); +} + +// TODO: audit if this is correct for server +// Was an attempt made to initialize the data source, and did that attempt +// succeed? The data source being connected, or not being connected due to +// offline mode, both represent successful terminal states. +static bool IsInitializedSuccessfully(DataSourceStatus::DataSourceState state) { + return state == DataSourceStatus::DataSourceState::kValid; +} + +// TODO: audit if this is correct for server +// Was any attempt made to initialize the data source (with a successful or +// permanent failure outcome?) +static bool IsInitialized(DataSourceStatus::DataSourceState state) { + return IsInitializedSuccessfully(state) || + (state == DataSourceStatus::DataSourceState::kOff); +} + +void ClientImpl::Identify(Context context) { + event_processor_->SendAsync(events::IdentifyEventParams{ + std::chrono::system_clock::now(), std::move(context)}); +} + +std::future ClientImpl::StartAsyncInternal( + std::function result_predicate) { + auto pr = std::make_shared>(); + auto fut = pr->get_future(); + + status_manager_.OnDataSourceStatusChangeEx( + [result_predicate, pr](data_sources::DataSourceStatus status) { + auto state = status.State(); + if (IsInitialized(state)) { + pr->set_value(result_predicate(status.State())); + return true; /* delete this change listener since the + desired state was reached */ + } + return false; /* keep the change listener */ + }); + + return fut; +} + +std::future ClientImpl::StartAsync() { + return StartAsyncInternal(IsInitializedSuccessfully); +} + +bool ClientImpl::Initialized() const { + return IsInitializedSuccessfully(status_manager_.Status().State()); +} + +std::unordered_map ClientImpl::AllFlagsState() const { + std::unordered_map result; + // TODO: implement all flags state (and update signature). + // for (auto& [key, descriptor] : memory_store_.AllFlags()) { + // if (descriptor->item) { + // result.try_emplace(key, descriptor->item->Value()); + // } + // } + return result; +} + +void ClientImpl::TrackInternal(Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value) { + event_processor_->SendAsync(events::TrackEventParams{ + std::chrono::system_clock::now(), std::move(event_name), + ctx.KindsToKeys(), std::move(data), metric_value}); +} + +void ClientImpl::Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) { + this->TrackInternal(ctx, std::move(event_name), std::move(data), + metric_value); +} + +void ClientImpl::Track(Context const& ctx, std::string event_name, Value data) { + this->TrackInternal(ctx, std::move(event_name), std::move(data), + std::nullopt); +} + +void ClientImpl::Track(Context const& ctx, std::string event_name) { + this->TrackInternal(ctx, std::move(event_name), std::nullopt, std::nullopt); +} + +void ClientImpl::FlushAsync() { + event_processor_->FlushAsync(); +} + +template +EvaluationDetail ClientImpl::VariationInternal(Context const& ctx, + FlagKey const& key, + Value default_value, + bool check_type) { + auto desc = memory_store_.GetFlag(key); + + if (!desc || !desc->item) { + if (!Initialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "LaunchDarkly client has not yet been initialized. " + "Returning default value"; + + auto error_reason = + EvaluationReason(EvaluationReason::ErrorKind::kClientNotReady); + return EvaluationDetail(std::move(default_value), std::nullopt, + std::move(error_reason)); + } + + LD_LOG(logger_, LogLevel::kInfo) + << "Unknown feature flag " << key << "; returning default value"; + + auto error_reason = + EvaluationReason(EvaluationReason::ErrorKind::kFlagNotFound); + return EvaluationDetail(std::move(default_value), std::nullopt, + std::move(error_reason)); + + } else if (!Initialized()) { + LD_LOG(logger_, LogLevel::kInfo) + << "LaunchDarkly client has not yet been initialized. " + "Returning cached value"; + } + + assert(desc->item); + + auto const& flag = *(desc->item); + + EvaluationDetail const detail = evaluator_.Evaluate(flag, ctx); + + if (check_type && default_value.Type() != Value::Type::kNull && + detail.Value().Type() != default_value.Type()) { + auto error_reason = + EvaluationReason(EvaluationReason::ErrorKind::kWrongType); + + return EvaluationDetail(std::move(default_value), std::nullopt, + error_reason); + } + + return EvaluationDetail(detail.Value(), detail.VariationIndex(), + detail.Reason()); +} + +EvaluationDetail ClientImpl::BoolVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + bool default_value) { + return VariationInternal(ctx, key, default_value, true); +} + +bool ClientImpl::BoolVariation(Context const& ctx, + IClient::FlagKey const& key, + bool default_value) { + return *VariationInternal(ctx, key, default_value, true); +} + +EvaluationDetail ClientImpl::StringVariationDetail( + Context const& ctx, + ClientImpl::FlagKey const& key, + std::string default_value) { + return VariationInternal(ctx, key, std::move(default_value), + true); +} + +std::string ClientImpl::StringVariation(Context const& ctx, + IClient::FlagKey const& key, + std::string default_value) { + return *VariationInternal(ctx, key, std::move(default_value), + true); +} + +EvaluationDetail ClientImpl::DoubleVariationDetail( + Context const& ctx, + ClientImpl::FlagKey const& key, + double default_value) { + return VariationInternal(ctx, key, default_value, true); +} + +double ClientImpl::DoubleVariation(Context const& ctx, + IClient::FlagKey const& key, + double default_value) { + return *VariationInternal(ctx, key, default_value, true); +} + +EvaluationDetail ClientImpl::IntVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + int default_value) { + return VariationInternal(ctx, key, default_value, true); +} + +int ClientImpl::IntVariation(Context const& ctx, + IClient::FlagKey const& key, + int default_value) { + return *VariationInternal(ctx, key, default_value, true); +} + +EvaluationDetail ClientImpl::JsonVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + Value default_value) { + return VariationInternal(ctx, key, std::move(default_value), false); +} + +Value ClientImpl::JsonVariation(Context const& ctx, + IClient::FlagKey const& key, + Value default_value) { + return *VariationInternal(ctx, key, std::move(default_value), false); +} + +// data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { +// return status_manager_; +// } +// +// flag_manager::IFlagNotifier& ClientImpl::FlagNotifier() { +// return flag_manager_.Notifier(); +// } + +ClientImpl::~ClientImpl() { + ioc_.stop(); + // TODO: Probably not the best. + run_thread_.join(); +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp new file mode 100644 index 000000000..f5491527d --- /dev/null +++ b/libs/server-sdk/src/client_impl.hpp @@ -0,0 +1,148 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "data_sources/data_source_status_manager.hpp" +#include "data_sources/data_source_update_sink.hpp" + +#include "data_store/memory_store.hpp" + +#include "evaluation/evaluator.hpp" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +class ClientImpl : public IClient { + public: + ClientImpl(Config config, std::string const& version); + + ClientImpl(ClientImpl&&) = delete; + ClientImpl(ClientImpl const&) = delete; + ClientImpl& operator=(ClientImpl) = delete; + ClientImpl& operator=(ClientImpl&& other) = delete; + + bool Initialized() const override; + + using FlagKey = std::string; + [[nodiscard]] std::unordered_map AllFlagsState() + const override; + + void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value) override; + + void Track(Context const& ctx, std::string event_name, Value data) override; + + void Track(Context const& ctx, std::string event_name) override; + + void FlushAsync() override; + + void Identify(Context context) override; + + bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + EvaluationDetail BoolVariationDetail(Context const& ctx, + FlagKey const& key, + bool default_value) override; + + std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value) override; + + double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value) override; + + EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value) override; + + int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value) override; + + EvaluationDetail IntVariationDetail(Context const& ctx, + FlagKey const& key, + int default_value) override; + + Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + EvaluationDetail JsonVariationDetail(Context const& ctx, + FlagKey const& key, + Value default_value) override; + + ~ClientImpl(); + + std::future StartAsync() override; + + private: + template + [[nodiscard]] EvaluationDetail VariationInternal(Context const& ctx, + FlagKey const& key, + Value default_value, + bool check_type); + void TrackInternal(Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value); + + std::future StartAsyncInternal( + std::function + predicate); + + Config config_; + Logger logger_; + + launchdarkly::config::shared::built::HttpProperties http_properties_; + + boost::asio::io_context ioc_; + boost::asio::executor_work_guard + work_; + + data_store::MemoryStore memory_store_; + + std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; + + std::unique_ptr event_processor_; + + mutable std::mutex init_mutex_; + std::condition_variable init_waiter_; + + data_sources::DataSourceStatusManager status_manager_; + + evaluation::Evaluator evaluator_; + + std::thread run_thread_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/data_sources/null_data_source.cpp b/libs/server-sdk/src/data_sources/null_data_source.cpp new file mode 100644 index 000000000..223dee39c --- /dev/null +++ b/libs/server-sdk/src/data_sources/null_data_source.cpp @@ -0,0 +1,19 @@ +#include "null_data_source.hpp" + +#include + +namespace launchdarkly::server_side::data_sources { + +void NullDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); +} + +void NullDataSource::ShutdownAsync(std::function complete) { + boost::asio::post(exec_, complete); +} + +NullDataSource::NullDataSource(boost::asio::any_io_executor exec, + DataSourceStatusManager& status_manager) + : status_manager_(status_manager), exec_(exec) {} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/null_data_source.hpp b/libs/server-sdk/src/data_sources/null_data_source.hpp new file mode 100644 index 000000000..7a1102f49 --- /dev/null +++ b/libs/server-sdk/src/data_sources/null_data_source.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include "data_source_status_manager.hpp" + +#include + +#include + +namespace launchdarkly::server_side::data_sources { + +class NullDataSource : public ::launchdarkly::data_sources::IDataSource { + public: + explicit NullDataSource(boost::asio::any_io_executor exec, + DataSourceStatusManager& status_manager); + void Start() override; + void ShutdownAsync(std::function) override; + + private: + DataSourceStatusManager& status_manager_; + boost::asio::any_io_executor exec_; +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index f1d1cc4b2..9241832a6 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -24,14 +24,14 @@ Evaluator::Evaluator(Logger& logger, data_store::IDataStore const& store) EvaluationDetail Evaluator::Evaluate( Flag const& flag, - launchdarkly::Context const& context) const { + launchdarkly::Context const& context) { return Evaluate("", flag, context); } EvaluationDetail Evaluator::Evaluate( std::string const& parent_key, Flag const& flag, - launchdarkly::Context const& context) const { + launchdarkly::Context const& context) { if (auto guard = stack_.NoticePrerequisite(flag.key)) { if (!flag.on) { return OffValue(flag, EvaluationReason::Off()); diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp index d2db9603c..6e22405b9 100644 --- a/libs/server-sdk/src/evaluation/evaluator.hpp +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -19,15 +19,19 @@ class Evaluator { public: Evaluator(Logger& logger, data_store::IDataStore const& store); + /** + * Evaluates a flag for a given context. + * Warning: not thread safe. + */ [[nodiscard]] EvaluationDetail Evaluate( data_model::Flag const& flag, - launchdarkly::Context const& context) const; + launchdarkly::Context const& context); private: [[nodiscard]] EvaluationDetail Evaluate( std::string const& parent_key, data_model::Flag const& flag, - launchdarkly::Context const& context) const; + launchdarkly::Context const& context); [[nodiscard]] EvaluationDetail FlagVariation( data_model::Flag const& flag, @@ -42,6 +46,6 @@ class Evaluator { Logger& logger_; data_store::IDataStore const& store_; - mutable detail::EvaluationStack stack_; + detail::EvaluationStack stack_; }; } // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/tests/client_test.cpp b/libs/server-sdk/tests/client_test.cpp new file mode 100644 index 000000000..315c0a40d --- /dev/null +++ b/libs/server-sdk/tests/client_test.cpp @@ -0,0 +1,70 @@ +#include +#include +#include +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +class ClientTest : public ::testing::Test { + protected: + ClientTest() + : client_(ConfigBuilder("sdk-123").Build().value()), + context_(ContextBuilder().Kind("cat", "shadow").Build()) {} + + Client client_; + Context const context_; +}; + +TEST_F(ClientTest, ClientConstructedWithMinimalConfigAndContextT) { + char const* version = client_.Version(); + ASSERT_TRUE(version); + ASSERT_STREQ(version, "0.1.0"); // {x-release-please-version} +} + +TEST_F(ClientTest, BoolVariationDefaultPassesThrough) { + const std::string flag = "extra-cat-food"; + std::vector values = {true, false}; + for (auto const& v : values) { + ASSERT_EQ(client_.BoolVariation(context_, flag, v), v); + ASSERT_EQ(*client_.BoolVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, StringVariationDefaultPassesThrough) { + const std::string flag = "treat"; + std::vector values = {"chicken", "fish", "cat-grass"}; + for (auto const& v : values) { + ASSERT_EQ(client_.StringVariation(context_, flag, v), v); + ASSERT_EQ(*client_.StringVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, IntVariationDefaultPassesThrough) { + const std::string flag = "weight"; + std::vector values = {0, 12, 13, 24, 1000}; + for (auto const& v : values) { + ASSERT_EQ(client_.IntVariation(context_, flag, v), v); + ASSERT_EQ(*client_.IntVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, DoubleVariationDefaultPassesThrough) { + 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(context_, flag, v), v); + ASSERT_EQ(*client_.DoubleVariationDetail(context_, flag, v), v); + } +} + +TEST_F(ClientTest, JsonVariationDefaultPassesThrough) { + 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(context_, flag, v), v); + ASSERT_EQ(*client_.JsonVariationDetail(context_, flag, v), v); + } +} diff --git a/libs/server-sdk/tests/evaluator_tests.cpp b/libs/server-sdk/tests/evaluator_tests.cpp index b91991cc6..602a8dfe8 100644 --- a/libs/server-sdk/tests/evaluator_tests.cpp +++ b/libs/server-sdk/tests/evaluator_tests.cpp @@ -26,7 +26,7 @@ class EvaluatorTests : public ::testing::Test { protected: std::unique_ptr store_; - evaluation::Evaluator const eval_; + evaluation::Evaluator eval_; }; /** @@ -49,7 +49,7 @@ class EvaluatorTestsWithLogs : public ::testing::Test { protected: std::unique_ptr store_; - evaluation::Evaluator const eval_; + evaluation::Evaluator eval_; }; TEST_F(EvaluatorTests, BasicChanges) { From 662b0b267cb5252b2739f05774bbb6d716e48bee Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:28:53 -0700 Subject: [PATCH 17/56] feat: Add persistent core interface. --- .../integrations/persistent_store_core.hpp | 194 ++++++++++++++++++ libs/server-sdk/src/CMakeLists.txt | 1 + 2 files changed, 195 insertions(+) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp new file mode 100644 index 000000000..cb31344a4 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp @@ -0,0 +1,194 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace launchdarkly::server_side::integrations { + +/** + * A versioned item which can be stored in a persistent store. + */ +struct SerializedItemDescriptor { + uint64_t version; + + /** + * During an Init/Upsert, when this is true, the serializedItem will + * contain a tombstone representation. If the persistence implementation + * can efficiently store the deletion state, and version, then it may + * choose to discard the item. + */ + bool deleted; + + /** + * When reading from a persistent store the serializedItem may be + * std::nullopt for deleted items. + */ + std::optional serializedItem; +}; + +/** + * Represents a namespace of persistent data. + */ +class IPersistentKind { + /** + * The namespace for the data. + */ + [[nodiscard]] virtual std::string const& Namespace(); + + /** + * Deserialize data and return the version of the data. + * + * This is for cases where the persistent store cannot avoid deserializing + * data to determine its version. For instance a Redis store where + * the only columns are the prefixed key and the serialized data. + * + * @param data The data to deserialize. + * @return The version of the data. + */ + [[nodiscard]] virtual uint64_t Version(std::string const& data); +}; + +/** + * Interface for a data store that holds feature flags and related data in a + * serialized form. + * + * This interface should be used for database integrations, or any other data + * store implementation that stores data in some external service. + * The SDK will take care of converting between its own internal data model and + * a serialized string form; the data store interacts only with the serialized + * form. + * + * The SDK will also provide its own caching layer on top of the persistent data + * store; the data store implementation should not provide caching, but simply + * do every query or update that the SDK tells it to do. + * + * Implementations must be thread-safe. + */ +class IPersistentStoreCore { + enum class InitResult { + /** + * The init operation completed successfully. + */ + kSuccess, + + /** + * There was an error with the init operation. + */ + kError, + }; + + enum class UpsertResult { + /** + * The upsert completed successfully. + */ + kSuccess, + + /** + * There was an error with the upsert operation. + */ + kError, + + /** + * The upsert did not encounter errors, but the version of the + * existing item was greater than that the version of the upsert item. + */ + kNotUpdated + }; + + struct Error { + std::string message; + }; + + using GetResult = + tl::expected, Error>; + + using AllResult = + tl::expected, + Error>; + + using ItemKey = std::string; + using KeyItemPair = std::pair; + using OrderedNamepace = std::vector; + using KindCollectionPair = + std::pair; + using OrderedData = std::vector; + + /** + * Overwrites the store's contents with a set of items for each collection. + * + * All previous data should be discarded, regardless of versioning. + * + * The update should be done atomically. If it cannot be done atomically, + * then the store must first add or update each item in the same order that + * they are given in the input data, and then delete any previously stored + * items that were not in the input data. + * + * @param allData The ordered set of data to replace all current data with. + * @return The status of the init operation. + */ + virtual InitResult Init(OrderedData const& allData) = 0; + + /** + * Updates or inserts an item in the specified collection. For updates, the + * object will only be updated if the existing version is less than the new + * version. + * + * @param kind The collection kind to use. + * @param itemKey The unique key for the item within the collection. + * @param item The item to insert or update. + * + * @return The status of the operation. + */ + virtual UpsertResult Upsert(IPersistentKind const& kind, + std::string const& itemKey, + SerializedItemDescriptor const& item) = 0; + + /** + * Retrieves an item from the specified collection, if available. + * + * @param kind The kind of the item. + * @param itemKey The key for the item. + * @return A serialized item descriptor if the item existed, a std::nullopt + * if the item did not exist, or an error. For a deleted item the serialized + * item descriptor may contain a std::nullopt for the serializedItem. + */ + virtual GetResult Get(IPersistentKind const& kind, + std::string const& itemKey) const = 0; + + /** + * Retrieves all items from the specified collection. + * + * If the store contains placeholders for deleted items, it should include + * them in the results, not filter them out. + * @param kind The kind of data to get. + * @return Either all of the items of the type, or an error. If there are + * no items of the specified type, then return an empty collection. + */ + virtual AllResult All(IPersistentKind const& kind) const = 0; + + /** + * Returns true if this store has been initialized. + * + * In a shared data store, the implementation should be able to detect this + * state even if Init was called in a different process, i.e. it must query + * the underlying data store in some way. The method does not need to worry + * about caching this value; the SDK will call it rarely. + * + * @return True if the store has been initialized. + */ + virtual bool Initialized() const = 0; + + /** + * A short description of the store, for instance "Redis". May be used + * in diagnostic information and logging. + * + * @return A short description of the sore. + */ + virtual std::string const& Description() const = 0; +}; +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 780ecb553..991b73882 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -1,6 +1,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" + "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" ) # Automatic library: static or dynamic based on user config. From 7bcf2ea658b3b57b2770700798933e8b490b46da Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:30:20 -0700 Subject: [PATCH 18/56] feat: Add expiration tracker. --- architecture/server_store_arch.md | 4 +- .../persistent/expiration_tracker.cpp | 31 +++++ .../persistent/expiration_tracker.hpp | 125 ++++++++++++++++++ .../persistent/persistent_data_store.cpp | 3 + .../persistent/persistent_data_store.hpp | 51 +++++++ 5 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp create mode 100644 libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp create mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp create mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp diff --git a/architecture/server_store_arch.md b/architecture/server_store_arch.md index 324425904..8b9a1ec31 100644 --- a/architecture/server_store_arch.md +++ b/architecture/server_store_arch.md @@ -19,7 +19,7 @@ classDiagram DataStoreUpdater --> IDataStore PersistentStore --* MemoryStore : PersistentStore contains a MemoryStore - PersistentStore --* TtlTracker + PersistentStore --* ExpirationTracker IPersistentStoreCore <|-- RedisPersistentStore @@ -74,7 +74,7 @@ classDiagram +const Description() string } - class TtlTracker{ + class ExpirationTracker{ } class MemoryStore{ diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp new file mode 100644 index 000000000..33cbb1b05 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp @@ -0,0 +1,31 @@ +#include "expiration_tracker.hpp" + +namespace launchdarkly::server_side::data_store::persistent { + +void ExpirationTracker::Add(std::string const& key, + ExpirationTracker::TimePoint expiration) {} +void ExpirationTracker::Remove(std::string const& key) {} +ExpirationTracker::TrackState ExpirationTracker::State( + std::string const& key, + ExpirationTracker::TimePoint current_time) { + return ExpirationTracker::TrackState::kFresh; +} +void ExpirationTracker::Add(data_store::DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint expiration) {} +void ExpirationTracker::Remove(data_store::DataKind kind, + std::string const& key) {} +ExpirationTracker::TrackState ExpirationTracker::State( + data_store::DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint current_time) { + return ExpirationTracker::TrackState::kFresh; +} +void ExpirationTracker::Clear() {} +void ExpirationTracker::ScopedTtls::Set( + DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint expiration) {} +void ExpirationTracker::ScopedTtls::Remove(DataKind kind, std::string) {} +void ExpirationTracker::ScopedTtls::Clear() {} +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp new file mode 100644 index 000000000..b1d84eec9 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "../../data_store/data_kind.hpp" + +namespace launchdarkly::server_side::data_store::persistent { + +class ExpirationTracker { + public: + using TimePoint = std::chrono::time_point; + + /** + * The state of the key in the tracker. + */ + enum class TrackState { + /** + * The key is tracked and the TTL has not expired. + */ + kFresh, + /** + * The key is tracked and the TTL has expired. + */ + kStale, + /** + * The key is not being tracked. + */ + kNotTracked + }; + + /** + * Add an unscoped key to the tracker. + * + * @param key The key to track. + * @param expiration The time that the key expires. + * used. + */ + void Add(std::string const& key, TimePoint expiration); + + /** + * Remove an unscoped key from the tracker. + * + * @param key The key to stop tracking. + */ + void Remove(std::string const& key); + + /** + * Check the state of an unscoped key. + * + * @param key The key to check. + * @param current_time The current time. + * @return The state of the key. + */ + TrackState State(std::string const& key, TimePoint current_time); + + /** + * Add a scoped key to the tracker. Will use the specified TTL for the kind. + * + * @param kind The scope (kind) of the key. + * @param key The key to track. + * @param expiration The time that the key expires. + */ + void Add(data_store::DataKind kind, + std::string const& key, + TimePoint expiration); + + /** + * Remove a scoped key from the tracker. + * + * @param kind The scope (kind) of the key. + * @param key The key to stop tracking. + */ + void Remove(data_store::DataKind kind, std::string const& key); + + /** + * Check the state of a scoped key. + * + * @param kind The scope (kind) of the key. + * @param key The key to check. + * @return The state of the key. + */ + TrackState State(data_store::DataKind kind, + std::string const& key, + TimePoint current_time); + + /** + * Stop tracking all keys. + */ + void Clear(); + + /** + * Prune expired keys from the tracker. + * @param current_time The current time. + * @return A list of all the kinds and associated keys that expired. + * Unscoped keys will have std::nullopt as the kind. + */ + std::vector, std::string>> Prune( + TimePoint current_time); + + private: + using TtlMap = std::unordered_map; + + TtlMap unscoped_; + + class ScopedTtls { + public: + void Set(DataKind kind, std::string const& key, TimePoint expiration); + void Remove(DataKind kind, std::string); + void Clear(); + + private: + std::array(DataKind::kKindCount)> + scoped; + }; + + ScopedTtls scoped_; +}; + +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp new file mode 100644 index 000000000..706f78796 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp @@ -0,0 +1,3 @@ +#include "persistent_data_store.hpp" + +namespace launchdarkly::server_side::data_store::persistent {} diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp new file mode 100644 index 000000000..16d3dd587 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "../../data_sources/data_source_update_sink.hpp" +#include "../data_store.hpp" +#include "../memory_store.hpp" +#include "expiration_tracker.hpp" + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_store::persistent { + +class PersistentStore : public IDataStore, + public data_sources::IDataSourceUpdateSink { + public: + std::shared_ptr GetFlag( + std::string const& key) const override; + std::shared_ptr GetSegment( + std::string const& key) const override; + + std::unordered_map> AllFlags() + const override; + std::unordered_map> + AllSegments() const override; + + bool Initialized() const override; + std::string const& Description() const override; + + void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Upsert(std::string const& key, FlagDescriptor flag) override; + void Upsert(std::string const& key, SegmentDescriptor segment) override; + + PersistentStore() = default; + ~PersistentStore() override = default; + + PersistentStore(PersistentStore const& item) = delete; + PersistentStore(PersistentStore&& item) = delete; + PersistentStore& operator=(PersistentStore const&) = delete; + PersistentStore& operator=(PersistentStore&&) = delete; + + private: + MemoryStore memory_store_; + std::shared_ptr persistent_store_core_; + ExpirationTracker ttl_tracker_; +}; + +} // namespace launchdarkly::server_side::data_store::persistent \ No newline at end of file From 17c9dc1b3fbc11c05753aa5fe1b37bdf9e1aec18 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 12:34:53 -0700 Subject: [PATCH 19/56] feat: Add expiration tracker. --- libs/server-sdk/src/CMakeLists.txt | 6 +- .../src/data_store/dependency_tracker.cpp | 1 + .../src/data_store/dependency_tracker.hpp | 21 +-- .../persistent/expiration_tracker.cpp | 143 ++++++++++++++++-- .../persistent/expiration_tracker.hpp | 31 +++- .../server-sdk/src/data_store/tagged_data.hpp | 37 +++++ .../tests/expiration_tracker_test.cpp | 122 +++++++++++++++ 7 files changed, 322 insertions(+), 39 deletions(-) create mode 100644 libs/server-sdk/src/data_store/tagged_data.hpp create mode 100644 libs/server-sdk/tests/expiration_tracker_test.cpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 991b73882..e6129ca2f 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -23,7 +23,11 @@ add_library(${LIBNAME} data_sources/polling_data_source.cpp data_sources/data_source_status_manager.hpp data_sources/streaming_data_source.hpp - data_sources/streaming_data_source.cpp) + data_sources/streaming_data_source.cpp + data_store/persistent/persistent_data_store.hpp + data_store/persistent/expiration_tracker.hpp + data_store/persistent/persistent_data_store.cpp + data_store/persistent/expiration_tracker.cpp) if (MSVC OR (NOT BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} diff --git a/libs/server-sdk/src/data_store/dependency_tracker.cpp b/libs/server-sdk/src/data_store/dependency_tracker.cpp index f8010756c..d401fe327 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.cpp +++ b/libs/server-sdk/src/data_store/dependency_tracker.cpp @@ -1,4 +1,5 @@ #include "dependency_tracker.hpp" +#include "tagged_data.hpp" #include diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_store/dependency_tracker.hpp index 7ab6f7f0c..1dddb78b8 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_store/dependency_tracker.hpp @@ -9,29 +9,10 @@ #include #include "data_kind.hpp" +#include "tagged_data.hpp" namespace launchdarkly::server_side::data_store { -/** - * Class which can be used to tag a collection with the DataKind that collection - * is for. This is primarily to decrease the complexity of iterating collections - * allowing for a kvp style iteration, but with an array storage container. - * @tparam Storage - */ -template -class TaggedData { - public: - explicit TaggedData(DataKind kind) : kind_(kind) {} - [[nodiscard]] DataKind Kind() const { return kind_; } - [[nodiscard]] Storage const& Data() const { return storage_; } - - [[nodiscard]] Storage& Data() { return storage_; } - - private: - DataKind kind_; - Storage storage_; -}; - /** * Class used to maintain a set of dependencies. Each dependency may be either * a flag or segment. diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp index 33cbb1b05..e92537c6b 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp @@ -3,29 +3,150 @@ namespace launchdarkly::server_side::data_store::persistent { void ExpirationTracker::Add(std::string const& key, - ExpirationTracker::TimePoint expiration) {} -void ExpirationTracker::Remove(std::string const& key) {} + ExpirationTracker::TimePoint expiration) { + unscoped_.insert({key, expiration}); +} + +void ExpirationTracker::Remove(std::string const& key) { + unscoped_.erase(key); +} + ExpirationTracker::TrackState ExpirationTracker::State( std::string const& key, - ExpirationTracker::TimePoint current_time) { - return ExpirationTracker::TrackState::kFresh; + ExpirationTracker::TimePoint current_time) const { + auto item = unscoped_.find(key); + if (item != unscoped_.end()) { + return State(item->second, current_time); + } + + return ExpirationTracker::TrackState::kNotTracked; } + void ExpirationTracker::Add(data_store::DataKind kind, std::string const& key, - ExpirationTracker::TimePoint expiration) {} + ExpirationTracker::TimePoint expiration) { + scoped_.Set(kind, key, expiration); +} + void ExpirationTracker::Remove(data_store::DataKind kind, - std::string const& key) {} + std::string const& key) { + scoped_.Remove(kind, key); +} + ExpirationTracker::TrackState ExpirationTracker::State( data_store::DataKind kind, std::string const& key, + ExpirationTracker::TimePoint current_time) const { + auto expiration = scoped_.Get(kind, key); + if (expiration.has_value()) { + return State(expiration.value(), current_time); + } + return ExpirationTracker::TrackState::kNotTracked; +} + +void ExpirationTracker::Clear() { + scoped_.Clear(); + unscoped_.clear(); +} +std::vector, std::string>> +ExpirationTracker::Prune(ExpirationTracker::TimePoint current_time) { + std::vector, std::string>> pruned; + + // Determine everything to be pruned. + for (auto const& item : unscoped_) { + if (State(item.second, current_time) == + ExpirationTracker::TrackState::kStale) { + pruned.push_back({std::nullopt, item.first}); + } + } + for (auto const& scope : scoped_) { + for (auto const& item : scope.Data()) { + if (State(item.second, current_time) == + ExpirationTracker::TrackState::kStale) { + pruned.push_back({scope.Kind(), item.first}); + } + } + } + + // Do the actual prune. + for (auto const& item : pruned) { + if (item.first.has_value()) { + scoped_.Remove(item.first.value(), item.second); + } else { + unscoped_.erase(item.second); + } + } + return pruned; +} +ExpirationTracker::TrackState ExpirationTracker::State( + ExpirationTracker::TimePoint expiration, ExpirationTracker::TimePoint current_time) { - return ExpirationTracker::TrackState::kFresh; + if (expiration > current_time) { + return ExpirationTracker::TrackState::kFresh; + } + return ExpirationTracker::TrackState::kStale; } -void ExpirationTracker::Clear() {} + void ExpirationTracker::ScopedTtls::Set( DataKind kind, std::string const& key, - ExpirationTracker::TimePoint expiration) {} -void ExpirationTracker::ScopedTtls::Remove(DataKind kind, std::string) {} -void ExpirationTracker::ScopedTtls::Clear() {} + ExpirationTracker::TimePoint expiration) { + data_[static_cast>(kind)].Data().insert( + {key, expiration}); +} + +void ExpirationTracker::ScopedTtls::Remove(DataKind kind, + std::string const& key) { + data_[static_cast>(kind)].Data().erase( + key); +} + +void ExpirationTracker::ScopedTtls::Clear() { + for (auto& scope : data_) { + scope.Data().clear(); + } +} + +std::optional ExpirationTracker::ScopedTtls::Get( + DataKind kind, + std::string const& key) const { + auto const& scope = + data_[static_cast>(kind)]; + auto found = scope.Data().find(key); + if (found != scope.Data().end()) { + return found->second; + } + return std::nullopt; +} +ExpirationTracker::ScopedTtls::ScopedTtls() + : data_{ + TaggedData(DataKind::kFlag), + TaggedData(DataKind::kSegment), + } {} + +std::array, 2>::iterator +ExpirationTracker::ScopedTtls::begin() { + return data_.begin(); +} + +std::array, 2>::iterator +ExpirationTracker::ScopedTtls::end() { + return data_.end(); +} + +std::ostream& operator<<(std::ostream& out, + ExpirationTracker::TrackState const& state) { + switch (state) { + case ExpirationTracker::TrackState::kFresh: + out << "FRESH"; + break; + case ExpirationTracker::TrackState::kStale: + out << "STALE"; + break; + case ExpirationTracker::TrackState::kNotTracked: + out << "NOT_TRACKED"; + break; + } + return out; +} } // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp index b1d84eec9..641eee8fa 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -9,6 +9,7 @@ #include #include "../../data_store/data_kind.hpp" +#include "../tagged_data.hpp" namespace launchdarkly::server_side::data_store::persistent { @@ -21,11 +22,11 @@ class ExpirationTracker { */ enum class TrackState { /** - * The key is tracked and the TTL has not expired. + * The key is tracked and the key expiration is in the future. */ kFresh, /** - * The key is tracked and the TTL has expired. + * The key is tracked and the expiration is either now or in the past. */ kStale, /** @@ -57,7 +58,7 @@ class ExpirationTracker { * @param current_time The current time. * @return The state of the key. */ - TrackState State(std::string const& key, TimePoint current_time); + TrackState State(std::string const& key, TimePoint current_time) const; /** * Add a scoped key to the tracker. Will use the specified TTL for the kind. @@ -87,7 +88,7 @@ class ExpirationTracker { */ TrackState State(data_store::DataKind kind, std::string const& key, - TimePoint current_time); + TimePoint current_time) const; /** * Stop tracking all keys. @@ -108,18 +109,34 @@ class ExpirationTracker { TtlMap unscoped_; + static ExpirationTracker::TrackState State( + ExpirationTracker::TimePoint expiration, + ExpirationTracker::TimePoint current_time); + class ScopedTtls { public: + ScopedTtls(); + + using DataType = + std::array, + static_cast(DataKind::kKindCount)>; void Set(DataKind kind, std::string const& key, TimePoint expiration); - void Remove(DataKind kind, std::string); + void Remove(DataKind kind, std::string const& key); + std::optional Get(DataKind kind, + std::string const& key) const; void Clear(); + [[nodiscard]] typename DataType::iterator begin(); + + [[nodiscard]] typename DataType::iterator end(); + private: - std::array(DataKind::kKindCount)> - scoped; + DataType data_; }; ScopedTtls scoped_; }; +std::ostream& operator<<(std::ostream& out, ExpirationTracker::TrackState const& state); + } // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/tagged_data.hpp b/libs/server-sdk/src/data_store/tagged_data.hpp new file mode 100644 index 000000000..6ff087860 --- /dev/null +++ b/libs/server-sdk/src/data_store/tagged_data.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "data_kind.hpp" + +namespace launchdarkly::server_side::data_store { +/** + * Class which can be used to tag a collection with the DataKind that collection + * is for. This is primarily to decrease the complexity of iterating collections + * allowing for a kvp style iteration, but with an array storage container. + * @tparam Storage + */ +template +class TaggedData { + public: + explicit TaggedData(launchdarkly::server_side::data_store::DataKind kind) + : kind_(kind) {} + [[nodiscard]] launchdarkly::server_side::data_store::DataKind Kind() const { + return kind_; + } + [[nodiscard]] Storage const& Data() const { return storage_; } + + [[nodiscard]] Storage& Data() { return storage_; } + + private: + launchdarkly::server_side::data_store::DataKind kind_; + Storage storage_; +}; + +} diff --git a/libs/server-sdk/tests/expiration_tracker_test.cpp b/libs/server-sdk/tests/expiration_tracker_test.cpp new file mode 100644 index 000000000..a8d17f99c --- /dev/null +++ b/libs/server-sdk/tests/expiration_tracker_test.cpp @@ -0,0 +1,122 @@ +#include + +#include "data_store/persistent/expiration_tracker.hpp" + +using launchdarkly::server_side::data_store::DataKind; +using launchdarkly::server_side::data_store::persistent::ExpirationTracker; + +ExpirationTracker::TimePoint Second(uint64_t second) { + return std::chrono::steady_clock::time_point{std::chrono::seconds{second}}; +} + +TEST(ExpirationTrackerTest, CanTrackUnscopedItem) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(10)); + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State("Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State("Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanGetStateOfUntrackedUnscopedItem) { + ExpirationTracker tracker; + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(0))); +} + +TEST(ExpirationTrackerTest, CanTrackScopedItem) { + ExpirationTracker tracker; + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State(DataKind::kFlag, "Potato", Second(11))); + + // Is not considered unscoped. + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(11))); + + // The wrong scope is not tracked. + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanTrackSameKeyInMultipleScopes) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(0)); + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + tracker.Add(DataKind::kSegment, "Potato", Second(20)); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State("Potato", Second(9))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "Potato", Second(9))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "Potato", Second(10))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State(DataKind::kFlag, "Potato", Second(11))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanClear) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(0)); + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + tracker.Add(DataKind::kSegment, "Potato", Second(20)); + + tracker.Clear(); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kFlag, "Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "Potato", Second(0))); +} + +TEST(ExpirationTrackerTest, CanPrune) { + ExpirationTracker tracker; + tracker.Add("freshUnscoped", Second(100)); + tracker.Add(DataKind::kFlag, "freshFlag", Second(100)); + tracker.Add(DataKind::kSegment, "freshSegment", Second(100)); + + tracker.Add("staleUnscoped", Second(50)); + tracker.Add(DataKind::kFlag, "staleFlag", Second(50)); + tracker.Add(DataKind::kSegment, "staleSegment", Second(50)); + + auto pruned = tracker.Prune(Second(80)); + EXPECT_EQ(3, pruned.size()); + std::vector, std::string>> + expected_pruned{{std::nullopt, "staleUnscoped"}, + {DataKind::kFlag, "staleFlag"}, + {DataKind::kSegment, "staleSegment"}}; + EXPECT_EQ(expected_pruned, pruned); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("staleUnscoped", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kFlag, "staleFlag", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "staleSegment", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State("freshUnscoped", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "freshFlag", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "freshSegment", Second(80))); +} From 27bd74971f9fc1d51797e62889883b087fe176f9 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 12:42:07 -0700 Subject: [PATCH 20/56] Constructor/destructor/move/assign. --- .../integrations/persistent_store_core.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp index cb31344a4..afcfa5e58 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp @@ -51,6 +51,15 @@ class IPersistentKind { * @return The version of the data. */ [[nodiscard]] virtual uint64_t Version(std::string const& data); + + IPersistentKind(IPersistentKind const& item) = delete; + IPersistentKind(IPersistentKind&& item) = delete; + IPersistentKind& operator=(IPersistentKind const&) = delete; + IPersistentKind& operator=(IPersistentKind&&) = delete; + virtual ~IPersistentKind() = default; + + protected: + IPersistentKind() = default; }; /** @@ -190,5 +199,14 @@ class IPersistentStoreCore { * @return A short description of the sore. */ virtual std::string const& Description() const = 0; + + IPersistentStoreCore(IPersistentStoreCore const& item) = delete; + IPersistentStoreCore(IPersistentStoreCore&& item) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore const&) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore&&) = delete; + virtual ~IPersistentStoreCore() = default; + + protected: + IPersistentStoreCore() = default; }; } // namespace launchdarkly::server_side::integrations From ed60a670b7a0fd3836a71415453f0c3a469af1f9 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:04:01 -0700 Subject: [PATCH 21/56] feat: Implement common server persistence. --- .../shared/builders/persistence_builder.hpp | 33 +++++++++++++++++++ .../config/shared/built/persistence.hpp | 9 ++++- .../persistence}/persistent_store_core.hpp | 4 +-- libs/server-sdk/src/CMakeLists.txt | 1 - 4 files changed, 43 insertions(+), 4 deletions(-) rename libs/{server-sdk/include/launchdarkly/server_side/integrations => common/include/launchdarkly/persistence}/persistent_store_core.hpp (98%) diff --git a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp index b1d4ebe5f..304b65abd 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp @@ -84,6 +84,39 @@ class PersistenceBuilder { template <> class PersistenceBuilder { public: + /** + * Set the core persistence implementation. + * + * @param core The core persistence implementation. + * @return A reference to this builder. + */ + PersistenceBuilder& Core( + std::shared_ptr core); + + /** + * How long something in the cache is considered fresh. + * + * Each item that is cached will have its age tracked. If the age of + * the item exceeds the cache refresh time, then an attempt will be made + * to refresh the item next time it is requested. + * + * When ActiveEviction is set to false then the item will remain cached + * and that cached value will be used if attempts to refresh the value fail. + * + * If ActiveEviction is set to true, then expired items will be periodically + * removed from the cache. + * + * @param cache_refresh_time + * @return + */ + PersistenceBuilder& CacheRefreshTime( + std::chrono::seconds cache_refresh_time); + + PersistenceBuilder& ActiveEviction(bool active_eviction); + + PersistenceBuilder& EvictionInterval( + std::chrono::seconds eviction_interval); + [[nodiscard]] built::Persistence Build() const { return built::Persistence(); } diff --git a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp index 326d7b53f..7e7ffe53d 100644 --- a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp @@ -1,9 +1,11 @@ #pragma once +#include #include #include #include +#include namespace launchdarkly::config::shared::built { @@ -18,6 +20,11 @@ struct Persistence { }; template <> -struct Persistence {}; +struct Persistence { + std::shared_ptr implementation; + std::chrono::seconds cache_refresh_time; + bool active_eviction; + std::chrono::seconds eviction_interval; +}; } // namespace launchdarkly::config::shared::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp similarity index 98% rename from libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp rename to libs/common/include/launchdarkly/persistence/persistent_store_core.hpp index afcfa5e58..98a7aa328 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp +++ b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp @@ -8,7 +8,7 @@ #include -namespace launchdarkly::server_side::integrations { +namespace launchdarkly::persistence { /** * A versioned item which can be stored in a persistent store. @@ -209,4 +209,4 @@ class IPersistentStoreCore { protected: IPersistentStoreCore() = default; }; -} // namespace launchdarkly::server_side::integrations +} // namespace launchdarkly::persistence diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index e6129ca2f..c3e70221a 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -1,7 +1,6 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" - "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" ) # Automatic library: static or dynamic based on user config. From 02d8de10e1fe17aead561cf19d04774380fa5592 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:04:55 -0700 Subject: [PATCH 22/56] Add public specifiers. --- .../server_side/integrations/persistent_store_core.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp index afcfa5e58..a49f389be 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp @@ -35,6 +35,7 @@ struct SerializedItemDescriptor { * Represents a namespace of persistent data. */ class IPersistentKind { + public: /** * The namespace for the data. */ @@ -79,6 +80,7 @@ class IPersistentKind { * Implementations must be thread-safe. */ class IPersistentStoreCore { + public: enum class InitResult { /** * The init operation completed successfully. From decaf96acb400af0b94838cc2320e03c6c88ae30 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:07:07 -0700 Subject: [PATCH 23/56] Merge updates. --- .../src/data_store/persistent/persistent_data_store.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp index 16d3dd587..328275bd2 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -5,7 +5,7 @@ #include "../memory_store.hpp" #include "expiration_tracker.hpp" -#include +#include #include #include @@ -44,7 +44,7 @@ class PersistentStore : public IDataStore, private: MemoryStore memory_store_; - std::shared_ptr persistent_store_core_; + std::shared_ptr persistent_store_core_; ExpirationTracker ttl_tracker_; }; From 06e3c931340a84ef3efca13293266548e93e59b3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:14:01 -0700 Subject: [PATCH 24/56] Start adding defaults. --- .../shared/builders/persistence_builder.hpp | 36 +++++++++++++++---- .../launchdarkly/config/shared/defaults.hpp | 3 ++ 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp index 304b65abd..add1b42cd 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp @@ -88,7 +88,7 @@ class PersistenceBuilder { * Set the core persistence implementation. * * @param core The core persistence implementation. - * @return A reference to this builder. + * @return A reference to this builder. */ PersistenceBuilder& Core( std::shared_ptr core); @@ -106,20 +106,42 @@ class PersistenceBuilder { * If ActiveEviction is set to true, then expired items will be periodically * removed from the cache. * - * @param cache_refresh_time - * @return + * @param cache_refresh_time The time, in seconds, cached data remains + * fresh. + * @return A reference to this builder. */ PersistenceBuilder& CacheRefreshTime( - std::chrono::seconds cache_refresh_time); + std::chrono::seconds cache_refresh_time) { + persistence_.cache_refresh_time = cache_refresh_time; + } - PersistenceBuilder& ActiveEviction(bool active_eviction); + /** + * Enable/disable active eviction. + * + * Defaults to disabled. + * @param active_eviction True to enable. + * @return A reference to this builder. + */ + PersistenceBuilder& ActiveEviction(bool active_eviction) { + persistence_.active_eviction = active_eviction; + } + /** + * If active eviction is enabled, then this specifies the time between + * active evictions. + * @param eviction_interval The interval, in seconds, between cache flushes. + * @return A reference to this builder. + */ PersistenceBuilder& EvictionInterval( - std::chrono::seconds eviction_interval); + std::chrono::seconds eviction_interval) { + persistence_.eviction_interval = eviction_interval; + } [[nodiscard]] built::Persistence Build() const { - return built::Persistence(); + return persistence_; } + private: + built::Persistence persistence_; }; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index bd62ae2c4..0ced5dc66 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -112,6 +113,8 @@ struct Defaults { return {std::chrono::seconds{30}, "/sdk/latest-all", std::chrono::seconds{30}}; } + + static auto PersistenceConfig() -> shared::built::Per }; } // namespace launchdarkly::config::shared From a1b7a4457f1d80f64febc5cdb6b52e39b372f2d5 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:15:31 -0700 Subject: [PATCH 25/56] Add extra blank line --- .../src/data_store/persistent/persistent_data_store.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp index 16d3dd587..523e09889 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -48,4 +48,4 @@ class PersistentStore : public IDataStore, ExpirationTracker ttl_tracker_; }; -} // namespace launchdarkly::server_side::data_store::persistent \ No newline at end of file +} // namespace launchdarkly::server_side::data_store::persistent From 0aeb6e8da4cc30c0188dbd8d61b92540ab4bd4fc Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:16:01 -0700 Subject: [PATCH 26/56] Format --- .../src/data_store/persistent/expiration_tracker.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp index 641eee8fa..68104c7e7 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -137,6 +137,7 @@ class ExpirationTracker { ScopedTtls scoped_; }; -std::ostream& operator<<(std::ostream& out, ExpirationTracker::TrackState const& state); +std::ostream& operator<<(std::ostream& out, + ExpirationTracker::TrackState const& state); } // namespace launchdarkly::server_side::data_store::persistent From 674c3434b768ad66970cc749b42e4bc971b30a73 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 13:34:55 -0700 Subject: [PATCH 27/56] feat: Add persistent store core interface. (#187) --- .../integrations/persistent_store_core.hpp | 214 ++++++++++++++++++ libs/server-sdk/src/CMakeLists.txt | 1 + 2 files changed, 215 insertions(+) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp new file mode 100644 index 000000000..a49f389be --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +namespace launchdarkly::server_side::integrations { + +/** + * A versioned item which can be stored in a persistent store. + */ +struct SerializedItemDescriptor { + uint64_t version; + + /** + * During an Init/Upsert, when this is true, the serializedItem will + * contain a tombstone representation. If the persistence implementation + * can efficiently store the deletion state, and version, then it may + * choose to discard the item. + */ + bool deleted; + + /** + * When reading from a persistent store the serializedItem may be + * std::nullopt for deleted items. + */ + std::optional serializedItem; +}; + +/** + * Represents a namespace of persistent data. + */ +class IPersistentKind { + public: + /** + * The namespace for the data. + */ + [[nodiscard]] virtual std::string const& Namespace(); + + /** + * Deserialize data and return the version of the data. + * + * This is for cases where the persistent store cannot avoid deserializing + * data to determine its version. For instance a Redis store where + * the only columns are the prefixed key and the serialized data. + * + * @param data The data to deserialize. + * @return The version of the data. + */ + [[nodiscard]] virtual uint64_t Version(std::string const& data); + + IPersistentKind(IPersistentKind const& item) = delete; + IPersistentKind(IPersistentKind&& item) = delete; + IPersistentKind& operator=(IPersistentKind const&) = delete; + IPersistentKind& operator=(IPersistentKind&&) = delete; + virtual ~IPersistentKind() = default; + + protected: + IPersistentKind() = default; +}; + +/** + * Interface for a data store that holds feature flags and related data in a + * serialized form. + * + * This interface should be used for database integrations, or any other data + * store implementation that stores data in some external service. + * The SDK will take care of converting between its own internal data model and + * a serialized string form; the data store interacts only with the serialized + * form. + * + * The SDK will also provide its own caching layer on top of the persistent data + * store; the data store implementation should not provide caching, but simply + * do every query or update that the SDK tells it to do. + * + * Implementations must be thread-safe. + */ +class IPersistentStoreCore { + public: + enum class InitResult { + /** + * The init operation completed successfully. + */ + kSuccess, + + /** + * There was an error with the init operation. + */ + kError, + }; + + enum class UpsertResult { + /** + * The upsert completed successfully. + */ + kSuccess, + + /** + * There was an error with the upsert operation. + */ + kError, + + /** + * The upsert did not encounter errors, but the version of the + * existing item was greater than that the version of the upsert item. + */ + kNotUpdated + }; + + struct Error { + std::string message; + }; + + using GetResult = + tl::expected, Error>; + + using AllResult = + tl::expected, + Error>; + + using ItemKey = std::string; + using KeyItemPair = std::pair; + using OrderedNamepace = std::vector; + using KindCollectionPair = + std::pair; + using OrderedData = std::vector; + + /** + * Overwrites the store's contents with a set of items for each collection. + * + * All previous data should be discarded, regardless of versioning. + * + * The update should be done atomically. If it cannot be done atomically, + * then the store must first add or update each item in the same order that + * they are given in the input data, and then delete any previously stored + * items that were not in the input data. + * + * @param allData The ordered set of data to replace all current data with. + * @return The status of the init operation. + */ + virtual InitResult Init(OrderedData const& allData) = 0; + + /** + * Updates or inserts an item in the specified collection. For updates, the + * object will only be updated if the existing version is less than the new + * version. + * + * @param kind The collection kind to use. + * @param itemKey The unique key for the item within the collection. + * @param item The item to insert or update. + * + * @return The status of the operation. + */ + virtual UpsertResult Upsert(IPersistentKind const& kind, + std::string const& itemKey, + SerializedItemDescriptor const& item) = 0; + + /** + * Retrieves an item from the specified collection, if available. + * + * @param kind The kind of the item. + * @param itemKey The key for the item. + * @return A serialized item descriptor if the item existed, a std::nullopt + * if the item did not exist, or an error. For a deleted item the serialized + * item descriptor may contain a std::nullopt for the serializedItem. + */ + virtual GetResult Get(IPersistentKind const& kind, + std::string const& itemKey) const = 0; + + /** + * Retrieves all items from the specified collection. + * + * If the store contains placeholders for deleted items, it should include + * them in the results, not filter them out. + * @param kind The kind of data to get. + * @return Either all of the items of the type, or an error. If there are + * no items of the specified type, then return an empty collection. + */ + virtual AllResult All(IPersistentKind const& kind) const = 0; + + /** + * Returns true if this store has been initialized. + * + * In a shared data store, the implementation should be able to detect this + * state even if Init was called in a different process, i.e. it must query + * the underlying data store in some way. The method does not need to worry + * about caching this value; the SDK will call it rarely. + * + * @return True if the store has been initialized. + */ + virtual bool Initialized() const = 0; + + /** + * A short description of the store, for instance "Redis". May be used + * in diagnostic information and logging. + * + * @return A short description of the sore. + */ + virtual std::string const& Description() const = 0; + + IPersistentStoreCore(IPersistentStoreCore const& item) = delete; + IPersistentStoreCore(IPersistentStoreCore&& item) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore const&) = delete; + IPersistentStoreCore& operator=(IPersistentStoreCore&&) = delete; + virtual ~IPersistentStoreCore() = default; + + protected: + IPersistentStoreCore() = default; +}; +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index b984ea1b7..0706e8071 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -1,6 +1,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" + "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" ) # Automatic library: static or dynamic based on user config. From 0ead7055a6f1ae81f921b608ba2d48002448a7ef Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 19 Jul 2023 14:29:50 -0700 Subject: [PATCH 28/56] fix: EvaluationStack should take ownership of key argument (#190) Current API of `EvaluationStack` is very easy to accidentally misuse because it takes a reference to its key argument. If the arg is destroyed, then we'll have a use-after-free. --- .../src/evaluation/detail/evaluation_stack.cpp | 14 +++++++------- .../src/evaluation/detail/evaluation_stack.hpp | 9 ++++----- .../server-sdk/src/evaluation/evaluation_error.hpp | 1 + 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp b/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp index f27a1dd1a..7c971f510 100644 --- a/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp +++ b/libs/server-sdk/src/evaluation/detail/evaluation_stack.cpp @@ -2,8 +2,8 @@ namespace launchdarkly::server_side::evaluation::detail { -Guard::Guard(std::unordered_set& set, std::string const& key) - : set_(set), key_(key) { +Guard::Guard(std::unordered_set& set, std::string key) + : set_(set), key_(std::move(key)) { set_.insert(key_); } @@ -12,19 +12,19 @@ Guard::~Guard() { } std::optional EvaluationStack::NoticePrerequisite( - std::string const& prerequisite_key) { + std::string prerequisite_key) { if (prerequisites_seen_.count(prerequisite_key) != 0) { return std::nullopt; } - return std::make_optional(prerequisites_seen_, prerequisite_key); + return std::make_optional(prerequisites_seen_, + std::move(prerequisite_key)); } -std::optional EvaluationStack::NoticeSegment( - std::string const& segment_key) { +std::optional EvaluationStack::NoticeSegment(std::string segment_key) { if (segments_seen_.count(segment_key) != 0) { return std::nullopt; } - return std::make_optional(segments_seen_, segment_key); + return std::make_optional(segments_seen_, std::move(segment_key)); } } // namespace launchdarkly::server_side::evaluation::detail diff --git a/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp b/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp index 384b9af93..7022dc375 100644 --- a/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp +++ b/libs/server-sdk/src/evaluation/detail/evaluation_stack.hpp @@ -11,7 +11,7 @@ namespace launchdarkly::server_side::evaluation::detail { * Upon destruction, the key is forgotten. */ struct Guard { - Guard(std::unordered_set& set, std::string const& key); + Guard(std::unordered_set& set, std::string key); ~Guard(); Guard(Guard const&) = delete; @@ -22,7 +22,7 @@ struct Guard { private: std::unordered_set& set_; - std::string const& key_; + std::string const key_; }; /** @@ -41,7 +41,7 @@ class EvaluationStack { * @return Guard object if not seen before, otherwise std::nullopt. */ [[nodiscard]] std::optional NoticePrerequisite( - std::string const& prerequisite_key); + std::string prerequisite_key); /** * If the given segment key has not been seen, marks it as seen @@ -50,8 +50,7 @@ class EvaluationStack { * @param prerequisite_key Key of the segment. * @return Guard object if not seen before, otherwise std::nullopt. */ - [[nodiscard]] std::optional NoticeSegment( - std::string const& segment_key); + [[nodiscard]] std::optional NoticeSegment(std::string segment_key); private: std::unordered_set prerequisites_seen_; diff --git a/libs/server-sdk/src/evaluation/evaluation_error.hpp b/libs/server-sdk/src/evaluation/evaluation_error.hpp index 30a9c60f0..7fb52d556 100644 --- a/libs/server-sdk/src/evaluation/evaluation_error.hpp +++ b/libs/server-sdk/src/evaluation/evaluation_error.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace launchdarkly::server_side::evaluation { From 30d2e2143c614a4f6433dcae9e0fc4ec53de0bbc Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 14:37:55 -0700 Subject: [PATCH 29/56] feat: Add expiration tracker. (#188) --- architecture/server_store_arch.md | 4 +- libs/server-sdk/src/CMakeLists.txt | 4 + .../src/data_store/dependency_tracker.cpp | 1 + .../src/data_store/dependency_tracker.hpp | 21 +-- .../persistent/expiration_tracker.cpp | 152 ++++++++++++++++++ .../persistent/expiration_tracker.hpp | 144 +++++++++++++++++ .../persistent/persistent_data_store.cpp | 3 + .../persistent/persistent_data_store.hpp | 51 ++++++ .../server-sdk/src/data_store/tagged_data.hpp | 37 +++++ .../src/evaluation/evaluation_error.hpp | 1 + .../tests/expiration_tracker_test.cpp | 122 ++++++++++++++ 11 files changed, 518 insertions(+), 22 deletions(-) create mode 100644 libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp create mode 100644 libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp create mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp create mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp create mode 100644 libs/server-sdk/src/data_store/tagged_data.hpp create mode 100644 libs/server-sdk/tests/expiration_tracker_test.cpp diff --git a/architecture/server_store_arch.md b/architecture/server_store_arch.md index 324425904..8b9a1ec31 100644 --- a/architecture/server_store_arch.md +++ b/architecture/server_store_arch.md @@ -19,7 +19,7 @@ classDiagram DataStoreUpdater --> IDataStore PersistentStore --* MemoryStore : PersistentStore contains a MemoryStore - PersistentStore --* TtlTracker + PersistentStore --* ExpirationTracker IPersistentStoreCore <|-- RedisPersistentStore @@ -74,7 +74,7 @@ classDiagram +const Description() string } - class TtlTracker{ + class ExpirationTracker{ } class MemoryStore{ diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 0706e8071..0ff18837b 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -36,6 +36,10 @@ add_library(${LIBNAME} evaluation/detail/evaluation_stack.cpp evaluation/detail/semver_operations.cpp evaluation/detail/timestamp_operations.cpp + data_store/persistent/persistent_data_store.hpp + data_store/persistent/expiration_tracker.hpp + data_store/persistent/persistent_data_store.cpp + data_store/persistent/expiration_tracker.cpp ) if (MSVC OR (NOT BUILD_SHARED_LIBS)) diff --git a/libs/server-sdk/src/data_store/dependency_tracker.cpp b/libs/server-sdk/src/data_store/dependency_tracker.cpp index f8010756c..d401fe327 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.cpp +++ b/libs/server-sdk/src/data_store/dependency_tracker.cpp @@ -1,4 +1,5 @@ #include "dependency_tracker.hpp" +#include "tagged_data.hpp" #include diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_store/dependency_tracker.hpp index 7ab6f7f0c..1dddb78b8 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_store/dependency_tracker.hpp @@ -9,29 +9,10 @@ #include #include "data_kind.hpp" +#include "tagged_data.hpp" namespace launchdarkly::server_side::data_store { -/** - * Class which can be used to tag a collection with the DataKind that collection - * is for. This is primarily to decrease the complexity of iterating collections - * allowing for a kvp style iteration, but with an array storage container. - * @tparam Storage - */ -template -class TaggedData { - public: - explicit TaggedData(DataKind kind) : kind_(kind) {} - [[nodiscard]] DataKind Kind() const { return kind_; } - [[nodiscard]] Storage const& Data() const { return storage_; } - - [[nodiscard]] Storage& Data() { return storage_; } - - private: - DataKind kind_; - Storage storage_; -}; - /** * Class used to maintain a set of dependencies. Each dependency may be either * a flag or segment. diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp new file mode 100644 index 000000000..09e6142ef --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp @@ -0,0 +1,152 @@ +#include "expiration_tracker.hpp" + +namespace launchdarkly::server_side::data_store::persistent { + +void ExpirationTracker::Add(std::string const& key, + ExpirationTracker::TimePoint expiration) { + unscoped_.insert({key, expiration}); +} + +void ExpirationTracker::Remove(std::string const& key) { + unscoped_.erase(key); +} + +ExpirationTracker::TrackState ExpirationTracker::State( + std::string const& key, + ExpirationTracker::TimePoint current_time) const { + auto item = unscoped_.find(key); + if (item != unscoped_.end()) { + return State(item->second, current_time); + } + + return ExpirationTracker::TrackState::kNotTracked; +} + +void ExpirationTracker::Add(data_store::DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint expiration) { + scoped_.Set(kind, key, expiration); +} + +void ExpirationTracker::Remove(data_store::DataKind kind, + std::string const& key) { + scoped_.Remove(kind, key); +} + +ExpirationTracker::TrackState ExpirationTracker::State( + data_store::DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint current_time) const { + auto expiration = scoped_.Get(kind, key); + if (expiration.has_value()) { + return State(expiration.value(), current_time); + } + return ExpirationTracker::TrackState::kNotTracked; +} + +void ExpirationTracker::Clear() { + scoped_.Clear(); + unscoped_.clear(); +} +std::vector, std::string>> +ExpirationTracker::Prune(ExpirationTracker::TimePoint current_time) { + std::vector, std::string>> pruned; + + // Determine everything to be pruned. + for (auto const& item : unscoped_) { + if (State(item.second, current_time) == + ExpirationTracker::TrackState::kStale) { + pruned.emplace_back(std::nullopt, item.first); + } + } + for (auto const& scope : scoped_) { + for (auto const& item : scope.Data()) { + if (State(item.second, current_time) == + ExpirationTracker::TrackState::kStale) { + pruned.emplace_back(scope.Kind(), item.first); + } + } + } + + // Do the actual prune. + for (auto const& item : pruned) { + if (item.first.has_value()) { + scoped_.Remove(item.first.value(), item.second); + } else { + unscoped_.erase(item.second); + } + } + return pruned; +} +ExpirationTracker::TrackState ExpirationTracker::State( + ExpirationTracker::TimePoint expiration, + ExpirationTracker::TimePoint current_time) { + if (expiration > current_time) { + return ExpirationTracker::TrackState::kFresh; + } + return ExpirationTracker::TrackState::kStale; +} + +void ExpirationTracker::ScopedTtls::Set( + DataKind kind, + std::string const& key, + ExpirationTracker::TimePoint expiration) { + data_[static_cast>(kind)].Data().insert( + {key, expiration}); +} + +void ExpirationTracker::ScopedTtls::Remove(DataKind kind, + std::string const& key) { + data_[static_cast>(kind)].Data().erase( + key); +} + +void ExpirationTracker::ScopedTtls::Clear() { + for (auto& scope : data_) { + scope.Data().clear(); + } +} + +std::optional ExpirationTracker::ScopedTtls::Get( + DataKind kind, + std::string const& key) const { + auto const& scope = + data_[static_cast>(kind)]; + auto found = scope.Data().find(key); + if (found != scope.Data().end()) { + return found->second; + } + return std::nullopt; +} +ExpirationTracker::ScopedTtls::ScopedTtls() + : data_{ + TaggedData(DataKind::kFlag), + TaggedData(DataKind::kSegment), + } {} + +std::array, 2>::iterator +ExpirationTracker::ScopedTtls::begin() { + return data_.begin(); +} + +std::array, 2>::iterator +ExpirationTracker::ScopedTtls::end() { + return data_.end(); +} + +std::ostream& operator<<(std::ostream& out, + ExpirationTracker::TrackState const& state) { + switch (state) { + case ExpirationTracker::TrackState::kFresh: + out << "FRESH"; + break; + case ExpirationTracker::TrackState::kStale: + out << "STALE"; + break; + case ExpirationTracker::TrackState::kNotTracked: + out << "NOT_TRACKED"; + break; + } + return out; +} +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp new file mode 100644 index 000000000..c3cd9cdd2 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "../../data_store/data_kind.hpp" +#include "../tagged_data.hpp" + +namespace launchdarkly::server_side::data_store::persistent { + +class ExpirationTracker { + public: + using TimePoint = std::chrono::time_point; + + /** + * The state of the key in the tracker. + */ + enum class TrackState { + /** + * The key is tracked and the key expiration is in the future. + */ + kFresh, + /** + * The key is tracked and the expiration is either now or in the past. + */ + kStale, + /** + * The key is not being tracked. + */ + kNotTracked + }; + + /** + * Add an unscoped key to the tracker. + * + * @param key The key to track. + * @param expiration The time that the key expires. + * used. + */ + void Add(std::string const& key, TimePoint expiration); + + /** + * Remove an unscoped key from the tracker. + * + * @param key The key to stop tracking. + */ + void Remove(std::string const& key); + + /** + * Check the state of an unscoped key. + * + * @param key The key to check. + * @param current_time The current time. + * @return The state of the key. + */ + TrackState State(std::string const& key, TimePoint current_time) const; + + /** + * Add a scoped key to the tracker. Will use the specified TTL for the kind. + * + * @param kind The scope (kind) of the key. + * @param key The key to track. + * @param expiration The time that the key expires. + */ + void Add(data_store::DataKind kind, + std::string const& key, + TimePoint expiration); + + /** + * Remove a scoped key from the tracker. + * + * @param kind The scope (kind) of the key. + * @param key The key to stop tracking. + */ + void Remove(data_store::DataKind kind, std::string const& key); + + /** + * Check the state of a scoped key. + * + * @param kind The scope (kind) of the key. + * @param key The key to check. + * @return The state of the key. + */ + TrackState State(data_store::DataKind kind, + std::string const& key, + TimePoint current_time) const; + + /** + * Stop tracking all keys. + */ + void Clear(); + + /** + * Prune expired keys from the tracker. + * @param current_time The current time. + * @return A list of all the kinds and associated keys that expired. + * Unscoped keys will have std::nullopt as the kind. + */ + std::vector, std::string>> Prune( + TimePoint current_time); + + private: + using TtlMap = std::unordered_map; + + TtlMap unscoped_; + + static ExpirationTracker::TrackState State( + ExpirationTracker::TimePoint expiration, + ExpirationTracker::TimePoint current_time); + + class ScopedTtls { + public: + ScopedTtls(); + + using DataType = + std::array, + static_cast>( + DataKind::kKindCount)>; + void Set(DataKind kind, std::string const& key, TimePoint expiration); + void Remove(DataKind kind, std::string const& key); + std::optional Get(DataKind kind, + std::string const& key) const; + void Clear(); + + [[nodiscard]] typename DataType::iterator begin(); + + [[nodiscard]] typename DataType::iterator end(); + + private: + DataType data_; + }; + + ScopedTtls scoped_; +}; + +std::ostream& operator<<(std::ostream& out, + ExpirationTracker::TrackState const& state); + +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp new file mode 100644 index 000000000..706f78796 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp @@ -0,0 +1,3 @@ +#include "persistent_data_store.hpp" + +namespace launchdarkly::server_side::data_store::persistent {} diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp new file mode 100644 index 000000000..523e09889 --- /dev/null +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "../../data_sources/data_source_update_sink.hpp" +#include "../data_store.hpp" +#include "../memory_store.hpp" +#include "expiration_tracker.hpp" + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_store::persistent { + +class PersistentStore : public IDataStore, + public data_sources::IDataSourceUpdateSink { + public: + std::shared_ptr GetFlag( + std::string const& key) const override; + std::shared_ptr GetSegment( + std::string const& key) const override; + + std::unordered_map> AllFlags() + const override; + std::unordered_map> + AllSegments() const override; + + bool Initialized() const override; + std::string const& Description() const override; + + void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Upsert(std::string const& key, FlagDescriptor flag) override; + void Upsert(std::string const& key, SegmentDescriptor segment) override; + + PersistentStore() = default; + ~PersistentStore() override = default; + + PersistentStore(PersistentStore const& item) = delete; + PersistentStore(PersistentStore&& item) = delete; + PersistentStore& operator=(PersistentStore const&) = delete; + PersistentStore& operator=(PersistentStore&&) = delete; + + private: + MemoryStore memory_store_; + std::shared_ptr persistent_store_core_; + ExpirationTracker ttl_tracker_; +}; + +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/tagged_data.hpp b/libs/server-sdk/src/data_store/tagged_data.hpp new file mode 100644 index 000000000..6ff087860 --- /dev/null +++ b/libs/server-sdk/src/data_store/tagged_data.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +#include "data_kind.hpp" + +namespace launchdarkly::server_side::data_store { +/** + * Class which can be used to tag a collection with the DataKind that collection + * is for. This is primarily to decrease the complexity of iterating collections + * allowing for a kvp style iteration, but with an array storage container. + * @tparam Storage + */ +template +class TaggedData { + public: + explicit TaggedData(launchdarkly::server_side::data_store::DataKind kind) + : kind_(kind) {} + [[nodiscard]] launchdarkly::server_side::data_store::DataKind Kind() const { + return kind_; + } + [[nodiscard]] Storage const& Data() const { return storage_; } + + [[nodiscard]] Storage& Data() { return storage_; } + + private: + launchdarkly::server_side::data_store::DataKind kind_; + Storage storage_; +}; + +} diff --git a/libs/server-sdk/src/evaluation/evaluation_error.hpp b/libs/server-sdk/src/evaluation/evaluation_error.hpp index 7fb52d556..69aa5ef51 100644 --- a/libs/server-sdk/src/evaluation/evaluation_error.hpp +++ b/libs/server-sdk/src/evaluation/evaluation_error.hpp @@ -2,6 +2,7 @@ #include #include +#include namespace launchdarkly::server_side::evaluation { diff --git a/libs/server-sdk/tests/expiration_tracker_test.cpp b/libs/server-sdk/tests/expiration_tracker_test.cpp new file mode 100644 index 000000000..a8d17f99c --- /dev/null +++ b/libs/server-sdk/tests/expiration_tracker_test.cpp @@ -0,0 +1,122 @@ +#include + +#include "data_store/persistent/expiration_tracker.hpp" + +using launchdarkly::server_side::data_store::DataKind; +using launchdarkly::server_side::data_store::persistent::ExpirationTracker; + +ExpirationTracker::TimePoint Second(uint64_t second) { + return std::chrono::steady_clock::time_point{std::chrono::seconds{second}}; +} + +TEST(ExpirationTrackerTest, CanTrackUnscopedItem) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(10)); + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State("Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State("Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanGetStateOfUntrackedUnscopedItem) { + ExpirationTracker tracker; + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(0))); +} + +TEST(ExpirationTrackerTest, CanTrackScopedItem) { + ExpirationTracker tracker; + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State(DataKind::kFlag, "Potato", Second(11))); + + // Is not considered unscoped. + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(11))); + + // The wrong scope is not tracked. + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanTrackSameKeyInMultipleScopes) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(0)); + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + tracker.Add(DataKind::kSegment, "Potato", Second(20)); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State("Potato", Second(9))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "Potato", Second(9))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "Potato", Second(10))); + + EXPECT_EQ(ExpirationTracker::TrackState::kStale, + tracker.State(DataKind::kFlag, "Potato", Second(11))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "Potato", Second(11))); +} + +TEST(ExpirationTrackerTest, CanClear) { + ExpirationTracker tracker; + tracker.Add("Potato", Second(0)); + tracker.Add(DataKind::kFlag, "Potato", Second(10)); + tracker.Add(DataKind::kSegment, "Potato", Second(20)); + + tracker.Clear(); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kFlag, "Potato", Second(0))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "Potato", Second(0))); +} + +TEST(ExpirationTrackerTest, CanPrune) { + ExpirationTracker tracker; + tracker.Add("freshUnscoped", Second(100)); + tracker.Add(DataKind::kFlag, "freshFlag", Second(100)); + tracker.Add(DataKind::kSegment, "freshSegment", Second(100)); + + tracker.Add("staleUnscoped", Second(50)); + tracker.Add(DataKind::kFlag, "staleFlag", Second(50)); + tracker.Add(DataKind::kSegment, "staleSegment", Second(50)); + + auto pruned = tracker.Prune(Second(80)); + EXPECT_EQ(3, pruned.size()); + std::vector, std::string>> + expected_pruned{{std::nullopt, "staleUnscoped"}, + {DataKind::kFlag, "staleFlag"}, + {DataKind::kSegment, "staleSegment"}}; + EXPECT_EQ(expected_pruned, pruned); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State("staleUnscoped", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kFlag, "staleFlag", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kNotTracked, + tracker.State(DataKind::kSegment, "staleSegment", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State("freshUnscoped", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kFlag, "freshFlag", Second(80))); + + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State(DataKind::kSegment, "freshSegment", Second(80))); +} From 0c9a6aebeb497688838662787740175fff2e59fe Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:10:15 -0700 Subject: [PATCH 30/56] Default persistence config --- .../config/shared/builders/persistence_builder.hpp | 7 +++++++ .../launchdarkly/config/shared/built/persistence.hpp | 1 + .../include/launchdarkly/config/shared/defaults.hpp | 5 ++++- .../src/data_store/persistent/persistent_data_store.hpp | 8 -------- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp index add1b42cd..961ead466 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp @@ -84,6 +84,9 @@ class PersistenceBuilder { template <> class PersistenceBuilder { public: + PersistenceBuilder() + : persistence_(Defaults::PersistenceConfig()) {} + /** * Set the core persistence implementation. * @@ -113,6 +116,7 @@ class PersistenceBuilder { PersistenceBuilder& CacheRefreshTime( std::chrono::seconds cache_refresh_time) { persistence_.cache_refresh_time = cache_refresh_time; + return *this; } /** @@ -124,6 +128,7 @@ class PersistenceBuilder { */ PersistenceBuilder& ActiveEviction(bool active_eviction) { persistence_.active_eviction = active_eviction; + return *this; } /** @@ -135,11 +140,13 @@ class PersistenceBuilder { PersistenceBuilder& EvictionInterval( std::chrono::seconds eviction_interval) { persistence_.eviction_interval = eviction_interval; + return *this; } [[nodiscard]] built::Persistence Build() const { return persistence_; } + private: built::Persistence persistence_; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp index 7e7ffe53d..35a90e918 100644 --- a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 0ced5dc66..b7d958236 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -114,7 +114,10 @@ struct Defaults { std::chrono::seconds{30}}; } - static auto PersistenceConfig() -> shared::built::Per + static auto PersistenceConfig() -> shared::built::Persistence { + return {nullptr, std::chrono::seconds{30}, false, + std::chrono::seconds{10}}; + } }; } // namespace launchdarkly::config::shared diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp index a9b2e06ae..1380df43b 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -5,11 +5,7 @@ #include "../memory_store.hpp" #include "expiration_tracker.hpp" -<<<<<<< HEAD #include -======= -#include ->>>>>>> server-side #include #include @@ -48,11 +44,7 @@ class PersistentStore : public IDataStore, private: MemoryStore memory_store_; -<<<<<<< HEAD std::shared_ptr persistent_store_core_; -======= - std::shared_ptr persistent_store_core_; ->>>>>>> server-side ExpirationTracker ttl_tracker_; }; From 674403857226fd3053ce7d1b27ff0c756d8a744e Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 16:14:07 -0700 Subject: [PATCH 31/56] Get basic persistent store structure in place. --- .../persistence/persistent_store_core.hpp | 4 +- .../integrations/persistent_store_core.hpp | 214 ------------------ .../persistent/persistent_data_store.cpp | 119 +++++++++- .../persistent/persistent_data_store.hpp | 65 +++++- 4 files changed, 182 insertions(+), 220 deletions(-) delete mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp diff --git a/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp index 66e0c3790..772a48f7d 100644 --- a/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp +++ b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp @@ -39,7 +39,7 @@ class IPersistentKind { /** * The namespace for the data. */ - [[nodiscard]] virtual std::string const& Namespace(); + [[nodiscard]] virtual std::string const& Namespace() const; /** * Deserialize data and return the version of the data. @@ -51,7 +51,7 @@ class IPersistentKind { * @param data The data to deserialize. * @return The version of the data. */ - [[nodiscard]] virtual uint64_t Version(std::string const& data); + [[nodiscard]] virtual uint64_t Version(std::string const& data) const; IPersistentKind(IPersistentKind const& item) = delete; IPersistentKind(IPersistentKind&& item) = delete; diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp deleted file mode 100644 index a49f389be..000000000 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp +++ /dev/null @@ -1,214 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -namespace launchdarkly::server_side::integrations { - -/** - * A versioned item which can be stored in a persistent store. - */ -struct SerializedItemDescriptor { - uint64_t version; - - /** - * During an Init/Upsert, when this is true, the serializedItem will - * contain a tombstone representation. If the persistence implementation - * can efficiently store the deletion state, and version, then it may - * choose to discard the item. - */ - bool deleted; - - /** - * When reading from a persistent store the serializedItem may be - * std::nullopt for deleted items. - */ - std::optional serializedItem; -}; - -/** - * Represents a namespace of persistent data. - */ -class IPersistentKind { - public: - /** - * The namespace for the data. - */ - [[nodiscard]] virtual std::string const& Namespace(); - - /** - * Deserialize data and return the version of the data. - * - * This is for cases where the persistent store cannot avoid deserializing - * data to determine its version. For instance a Redis store where - * the only columns are the prefixed key and the serialized data. - * - * @param data The data to deserialize. - * @return The version of the data. - */ - [[nodiscard]] virtual uint64_t Version(std::string const& data); - - IPersistentKind(IPersistentKind const& item) = delete; - IPersistentKind(IPersistentKind&& item) = delete; - IPersistentKind& operator=(IPersistentKind const&) = delete; - IPersistentKind& operator=(IPersistentKind&&) = delete; - virtual ~IPersistentKind() = default; - - protected: - IPersistentKind() = default; -}; - -/** - * Interface for a data store that holds feature flags and related data in a - * serialized form. - * - * This interface should be used for database integrations, or any other data - * store implementation that stores data in some external service. - * The SDK will take care of converting between its own internal data model and - * a serialized string form; the data store interacts only with the serialized - * form. - * - * The SDK will also provide its own caching layer on top of the persistent data - * store; the data store implementation should not provide caching, but simply - * do every query or update that the SDK tells it to do. - * - * Implementations must be thread-safe. - */ -class IPersistentStoreCore { - public: - enum class InitResult { - /** - * The init operation completed successfully. - */ - kSuccess, - - /** - * There was an error with the init operation. - */ - kError, - }; - - enum class UpsertResult { - /** - * The upsert completed successfully. - */ - kSuccess, - - /** - * There was an error with the upsert operation. - */ - kError, - - /** - * The upsert did not encounter errors, but the version of the - * existing item was greater than that the version of the upsert item. - */ - kNotUpdated - }; - - struct Error { - std::string message; - }; - - using GetResult = - tl::expected, Error>; - - using AllResult = - tl::expected, - Error>; - - using ItemKey = std::string; - using KeyItemPair = std::pair; - using OrderedNamepace = std::vector; - using KindCollectionPair = - std::pair; - using OrderedData = std::vector; - - /** - * Overwrites the store's contents with a set of items for each collection. - * - * All previous data should be discarded, regardless of versioning. - * - * The update should be done atomically. If it cannot be done atomically, - * then the store must first add or update each item in the same order that - * they are given in the input data, and then delete any previously stored - * items that were not in the input data. - * - * @param allData The ordered set of data to replace all current data with. - * @return The status of the init operation. - */ - virtual InitResult Init(OrderedData const& allData) = 0; - - /** - * Updates or inserts an item in the specified collection. For updates, the - * object will only be updated if the existing version is less than the new - * version. - * - * @param kind The collection kind to use. - * @param itemKey The unique key for the item within the collection. - * @param item The item to insert or update. - * - * @return The status of the operation. - */ - virtual UpsertResult Upsert(IPersistentKind const& kind, - std::string const& itemKey, - SerializedItemDescriptor const& item) = 0; - - /** - * Retrieves an item from the specified collection, if available. - * - * @param kind The kind of the item. - * @param itemKey The key for the item. - * @return A serialized item descriptor if the item existed, a std::nullopt - * if the item did not exist, or an error. For a deleted item the serialized - * item descriptor may contain a std::nullopt for the serializedItem. - */ - virtual GetResult Get(IPersistentKind const& kind, - std::string const& itemKey) const = 0; - - /** - * Retrieves all items from the specified collection. - * - * If the store contains placeholders for deleted items, it should include - * them in the results, not filter them out. - * @param kind The kind of data to get. - * @return Either all of the items of the type, or an error. If there are - * no items of the specified type, then return an empty collection. - */ - virtual AllResult All(IPersistentKind const& kind) const = 0; - - /** - * Returns true if this store has been initialized. - * - * In a shared data store, the implementation should be able to detect this - * state even if Init was called in a different process, i.e. it must query - * the underlying data store in some way. The method does not need to worry - * about caching this value; the SDK will call it rarely. - * - * @return True if the store has been initialized. - */ - virtual bool Initialized() const = 0; - - /** - * A short description of the store, for instance "Redis". May be used - * in diagnostic information and logging. - * - * @return A short description of the sore. - */ - virtual std::string const& Description() const = 0; - - IPersistentStoreCore(IPersistentStoreCore const& item) = delete; - IPersistentStoreCore(IPersistentStoreCore&& item) = delete; - IPersistentStoreCore& operator=(IPersistentStoreCore const&) = delete; - IPersistentStoreCore& operator=(IPersistentStoreCore&&) = delete; - virtual ~IPersistentStoreCore() = default; - - protected: - IPersistentStoreCore() = default; -}; -} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp index 706f78796..203fbe650 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp @@ -1,3 +1,120 @@ #include "persistent_data_store.hpp" -namespace launchdarkly::server_side::data_store::persistent {} +namespace launchdarkly::server_side::data_store::persistent { +PersistentStore::PersistentStore( + std::shared_ptr core, + std::chrono::seconds cache_refresh_time, + std::optional eviction_interval, + std::function()> time) + : core_(core), time_(time) {} + +std::unordered_map> +PersistentStore::AllFlags() const { + auto state = tracker_.State(Keys::kAllFlags, time_()); + return Get< + std::unordered_map>>( + state, [this]() { RefreshAllFlags(); }, + [this]() { return memory_store_.AllFlags(); }); +} + +std::unordered_map> +PersistentStore::AllSegments() const { + auto state = tracker_.State(Keys::kAllSegments, time_()); + return Get< + std::unordered_map>>( + state, [this]() { RefreshAllSegments(); }, + [this]() { return memory_store_.AllSegments(); }); +} + +bool PersistentStore::Initialized() const { + auto state = tracker_.State(Keys::kInitialized, time_()); + if (initialized_.has_value()) { + if (initialized_.value()) { + return true; + } + if (ExpirationTracker::TrackState::kFresh == state) { + return initialized_.value(); + } + } + RefreshInitState(); + return initialized_.value_or(false); +} + +std::string const& PersistentStore::Description() const { + return core_->Description(); +} +void PersistentStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { + // TODO: Implement sort. +} +void PersistentStore::Upsert(std::string const& key, + SegmentDescriptor segment) { + // TODO: Serialize the item. +} + +void PersistentStore::Upsert(std::string const& key, FlagDescriptor flag) { + // TODO: Serialize the item. +} + +std::shared_ptr PersistentStore::GetSegment( + std::string const& key) const { + auto state = tracker_.State(Keys::kAllSegments, time_()); + return Get>( + state, [this, &key]() { RefreshSegment(key); }, + [this, &key]() { return memory_store_.GetSegment(key); }); +} + +std::shared_ptr PersistentStore::GetFlag( + std::string const& key) const { + auto state = tracker_.State(Keys::kAllSegments, time_()); + return Get>( + state, [this, &key]() { RefreshFlag(key); }, + [this, &key]() { return memory_store_.GetFlag(key); }); +} +void PersistentStore::RefreshAllFlags() const { + auto res = core_->All(Kinds::Flag); + // TODO: Deserialize and put in store. + tracker_.Add(Keys::kAllSegments, time_()); +} + +void PersistentStore::RefreshAllSegments() const { + auto res = core_->All(Kinds::Segment); + // TODO: Deserialize and put in store. + tracker_.Add(Keys::kAllFlags, time_()); +} + +void PersistentStore::RefreshInitState() const { + initialized_ = core_->Initialized(); + tracker_.Add(Keys::kInitialized, time_()); +} + +void PersistentStore::RefreshSegment(std::string const& key) const { + auto res = core_->Get(Kinds::Segment, key); + // TODO: Deserialize and put in store. + tracker_.Add(DataKind::kSegment, key, time_()); +} + +void PersistentStore::RefreshFlag(std::string const& key) const { + auto res = core_->Get(Kinds::Flag, key); + // TODO: Deserialize and put in store. + tracker_.Add(DataKind::kFlag, key, time_()); +} + +std::string const& PersistentStore::SegmentKind::Namespace() const { + return namespace_; +} + +uint64_t PersistentStore::SegmentKind::Version(std::string const& data) const { + // TODO: Deserialize. + return 0; +} + +std::string const& PersistentStore::FlagKind::Namespace() const { + return namespace_; +} + +uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { + // TODO: Deserialize. + return 0; +} + +} // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp index 1380df43b..a166c5fa1 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -17,6 +17,13 @@ namespace launchdarkly::server_side::data_store::persistent { class PersistentStore : public IDataStore, public data_sources::IDataSourceUpdateSink { public: + PersistentStore( + std::shared_ptr core, + std::chrono::seconds cache_refresh_time, + std::optional eviction_interval, + std::function()> + time = []() { return std::chrono::steady_clock::now(); }); + std::shared_ptr GetFlag( std::string const& key) const override; std::shared_ptr GetSegment( @@ -43,9 +50,61 @@ class PersistentStore : public IDataStore, PersistentStore& operator=(PersistentStore&&) = delete; private: - MemoryStore memory_store_; - std::shared_ptr persistent_store_core_; - ExpirationTracker ttl_tracker_; + void RefreshAllFlags() const; + void RefreshAllSegments() const; + void RefreshInitState() const; + void RefreshFlag(std::string const& key) const; + void RefreshSegment(std::string const& key) const; + + template + static TResult Get(ExpirationTracker::TrackState state, + std::function refresh, + std::function get) { + switch (state) { + case ExpirationTracker::TrackState::kStale: + [[fallthrough]]; + case ExpirationTracker::TrackState::kNotTracked: + refresh(); + [[fallthrough]]; + case ExpirationTracker::TrackState::kFresh: + return get(); + } + } + + mutable MemoryStore memory_store_; + std::shared_ptr core_; + mutable ExpirationTracker tracker_; + std::function()> time_; + mutable std::optional initialized_; + + class SegmentKind : public persistence::IPersistentKind { + public: + std::string const& Namespace() const override; + uint64_t Version(std::string const& data) const override; + + private: + static const inline std::string namespace_ = "segments"; + }; + + class FlagKind : public persistence::IPersistentKind { + public: + std::string const& Namespace() const override; + uint64_t Version(std::string const& data) const override; + + private: + static const inline std::string namespace_ = "features"; + }; + + struct Kinds { + static const FlagKind Flag; + static const SegmentKind Segment; + }; + + struct Keys { + static const inline std::string kAllFlags = "allFlags"; + static const inline std::string kAllSegments = "allSegments"; + static const inline std::string kInitialized = "initialized"; + }; }; } // namespace launchdarkly::server_side::data_store::persistent From 846bfd56f197ca63c4055e956802cc93d8f6a4cb Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 19 Jul 2023 16:24:28 -0700 Subject: [PATCH 32/56] Incremental progress. --- .../persistent/persistent_data_store.cpp | 52 +++++++++++++++++-- .../persistent/persistent_data_store.hpp | 10 ++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp index 203fbe650..1a53c789d 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp @@ -45,6 +45,7 @@ std::string const& PersistentStore::Description() const { } void PersistentStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { // TODO: Implement sort. + // TODO: Serialize the items. } void PersistentStore::Upsert(std::string const& key, SegmentDescriptor segment) { @@ -89,14 +90,55 @@ void PersistentStore::RefreshInitState() const { void PersistentStore::RefreshSegment(std::string const& key) const { auto res = core_->Get(Kinds::Segment, key); - // TODO: Deserialize and put in store. - tracker_.Add(DataKind::kSegment, key, time_()); + if (res.has_value()) { + if (res->has_value()) { + auto segment = DeserializeSegment(res->value()); + if (segment.has_value()) { + memory_store_.Upsert(key, segment.value()); + } + // TODO: Log that we got bogus data? + } + tracker_.Add(DataKind::kSegment, key, time_()); + } + // TODO: If there is an actual error, then do we not reset the tracking? } void PersistentStore::RefreshFlag(std::string const& key) const { - auto res = core_->Get(Kinds::Flag, key); - // TODO: Deserialize and put in store. - tracker_.Add(DataKind::kFlag, key, time_()); + auto res = core_->Get(Kinds::Segment, key); + if (res.has_value()) { + if (res->has_value()) { + auto flag = DeserializeFlag(res->value()); + if (flag.has_value()) { + memory_store_.Upsert(key, flag.value()); + } + // TODO: Log that we got bogus data? + } + tracker_.Add(DataKind::kSegment, key, time_()); + } + // TODO: If there is an actual error, then do we not reset the tracking? +} +persistence::SerializedItemDescriptor PersistentStore::Serialize( + FlagDescriptor flag) { + // TODO: Implement + return persistence::SerializedItemDescriptor(); +} + +persistence::SerializedItemDescriptor PersistentStore::Serialize( + SegmentDescriptor segment) { + // TODO: Implement + return persistence::SerializedItemDescriptor(); +} + +std::optional PersistentStore::DeserializeFlag( + persistence::SerializedItemDescriptor flag) { + // TODO: Implement + return launchdarkly::server_side::data_store::FlagDescriptor(0); +} + +std::optional PersistentStore::DeserializeSegment( + persistence::SerializedItemDescriptor segment) { + // TODO: Implement + return launchdarkly::server_side::data_store::SegmentDescriptor(0); } std::string const& PersistentStore::SegmentKind::Namespace() const { diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp index a166c5fa1..0784d717b 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -56,6 +56,16 @@ class PersistentStore : public IDataStore, void RefreshFlag(std::string const& key) const; void RefreshSegment(std::string const& key) const; + static persistence::SerializedItemDescriptor Serialize(FlagDescriptor flag); + static persistence::SerializedItemDescriptor Serialize( + SegmentDescriptor segment); + + static std::optional DeserializeFlag( + persistence::SerializedItemDescriptor flag); + + static std::optional DeserializeSegment( + persistence::SerializedItemDescriptor segment); + template static TResult Get(ExpirationTracker::TrackState state, std::function refresh, From a76b3a8373b14e367557f070b89561db27406457 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Thu, 20 Jul 2023 13:09:25 -0700 Subject: [PATCH 33/56] Serialization progress. --- .../persistence/persistent_store_core.hpp | 6 +- .../persistent/persistent_data_store.cpp | 65 ++++++++++++++++--- .../persistent/persistent_data_store.hpp | 31 +++++++++ .../tests/persistent_data_store_test.cpp | 49 ++++++++++++++ 4 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 libs/server-sdk/tests/persistent_data_store_test.cpp diff --git a/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp index 772a48f7d..515db0b69 100644 --- a/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp +++ b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp @@ -39,7 +39,7 @@ class IPersistentKind { /** * The namespace for the data. */ - [[nodiscard]] virtual std::string const& Namespace() const; + [[nodiscard]] virtual std::string const& Namespace() const = 0; /** * Deserialize data and return the version of the data. @@ -48,10 +48,12 @@ class IPersistentKind { * data to determine its version. For instance a Redis store where * the only columns are the prefixed key and the serialized data. * + * If the data cannot be deserialized, then 0 will be returned. + * * @param data The data to deserialize. * @return The version of the data. */ - [[nodiscard]] virtual uint64_t Version(std::string const& data) const; + [[nodiscard]] virtual uint64_t Version(std::string const& data) const = 0; IPersistentKind(IPersistentKind const& item) = delete; IPersistentKind(IPersistentKind&& item) = delete; diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp index 1a53c789d..f5c38f13a 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp @@ -1,6 +1,13 @@ #include "persistent_data_store.hpp" +#include +#include namespace launchdarkly::server_side::data_store::persistent { + +const PersistentStore::FlagKind PersistentStore::Kinds::Flag = FlagKind(); +const PersistentStore::SegmentKind PersistentStore::Kinds::Segment = + SegmentKind(); + PersistentStore::PersistentStore( std::shared_ptr core, std::chrono::seconds cache_refresh_time, @@ -129,16 +136,60 @@ persistence::SerializedItemDescriptor PersistentStore::Serialize( return persistence::SerializedItemDescriptor(); } +template +static std::optional> Deserialize( + persistence::SerializedItemDescriptor item) { + if (item.deleted) { + return data_model::ItemDescriptor(item.version); + } + + boost::json::error_code error_code; + if (!item.serializedItem.has_value()) { + return std::nullopt; + } + auto parsed = boost::json::parse(item.serializedItem.value(), error_code); + + if (error_code) { + return std::nullopt; + } + + auto res = + boost::json::value_to, JsonError>>( + parsed); + + if (res.has_value() && res->has_value()) { + return data_model::ItemDescriptor(res->value()); + } + + return std::nullopt; +} + std::optional PersistentStore::DeserializeFlag( persistence::SerializedItemDescriptor flag) { - // TODO: Implement - return launchdarkly::server_side::data_store::FlagDescriptor(0); + return Deserialize(flag); } std::optional PersistentStore::DeserializeSegment( persistence::SerializedItemDescriptor segment) { - // TODO: Implement - return launchdarkly::server_side::data_store::SegmentDescriptor(0); + return Deserialize(segment); +} + +template +static uint64_t GetVersion(std::string data) { + boost::json::error_code error_code; + auto parsed = boost::json::parse(data, error_code); + + if (error_code) { + return 0; + } + auto res = + boost::json::value_to, JsonError>>( + parsed); + + if (res.has_value() && res->has_value()) { + return res->value().version; + } + return 0; } std::string const& PersistentStore::SegmentKind::Namespace() const { @@ -146,8 +197,7 @@ std::string const& PersistentStore::SegmentKind::Namespace() const { } uint64_t PersistentStore::SegmentKind::Version(std::string const& data) const { - // TODO: Deserialize. - return 0; + return GetVersion(data); } std::string const& PersistentStore::FlagKind::Namespace() const { @@ -155,8 +205,7 @@ std::string const& PersistentStore::FlagKind::Namespace() const { } uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { - // TODO: Deserialize. - return 0; + return GetVersion(data); } } // namespace launchdarkly::server_side::data_store::persistent diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp index 0784d717b..d0e340306 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -14,6 +14,33 @@ namespace launchdarkly::server_side::data_store::persistent { +class SegmentKind : public persistence::IPersistentKind { + public: + std::string const& Namespace() const override; + uint64_t Version(std::string const& data) const override; + + ~SegmentKind() override = default; + + private: + static const inline std::string namespace_ = "segments"; +}; + +class FlagKind : public persistence::IPersistentKind { + public: + std::string const& Namespace() const override; + uint64_t Version(std::string const& data) const override; + + ~FlagKind() override = default; + + private: + static const inline std::string namespace_ = "features"; +}; + +struct Kinds { + static const FlagKind Flag; + static const SegmentKind Segment; +}; + class PersistentStore : public IDataStore, public data_sources::IDataSourceUpdateSink { public: @@ -92,6 +119,8 @@ class PersistentStore : public IDataStore, std::string const& Namespace() const override; uint64_t Version(std::string const& data) const override; + ~SegmentKind() override = default; + private: static const inline std::string namespace_ = "segments"; }; @@ -101,6 +130,8 @@ class PersistentStore : public IDataStore, std::string const& Namespace() const override; uint64_t Version(std::string const& data) const override; + ~FlagKind() override = default; + private: static const inline std::string namespace_ = "features"; }; diff --git a/libs/server-sdk/tests/persistent_data_store_test.cpp b/libs/server-sdk/tests/persistent_data_store_test.cpp new file mode 100644 index 000000000..4a47b6439 --- /dev/null +++ b/libs/server-sdk/tests/persistent_data_store_test.cpp @@ -0,0 +1,49 @@ +#include + +#include + +#include "data_store/persistent/persistent_data_store.hpp" + +using launchdarkly::persistence::IPersistentStoreCore; +using launchdarkly::server_side::data_store::persistent::PersistentStore; + +class TestCore : public IPersistentStoreCore { + public: + InitResult Init(OrderedData const& allData) override { + return InitResult::kSuccess; + } + + UpsertResult Upsert( + launchdarkly::persistence::IPersistentKind const& kind, + std::string const& itemKey, + launchdarkly::persistence::SerializedItemDescriptor const& item) + override { + return UpsertResult::kNotUpdated; + } + + GetResult Get(launchdarkly::persistence::IPersistentKind const& kind, + std::string const& itemKey) const override { + return launchdarkly::persistence::IPersistentStoreCore::GetResult(); + } + + AllResult All( + launchdarkly::persistence::IPersistentKind const& kind) const override { + return launchdarkly::persistence::IPersistentStoreCore::AllResult(); + } + + bool Initialized() const override { return false; } + + std::string const& Description() const override { return description_; } + + private: + static inline const std::string description_ = "TestCore"; +}; + +TEST(PersistentDataStoreTest, CanInstantiate) { + // launchdarkly::server_side::data_store::persistent::PersistentStore::FlagKind + // flag_kind; + + auto core = std::make_shared(); + PersistentStore persistent_store(core, std::chrono::seconds{30}, + std::nullopt); +} From 213acbd43f3942d3fae2a222191838aaf3bcd2cb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 20 Jul 2023 16:53:01 -0700 Subject: [PATCH 34/56] chore: add event processor architectural diagrams (#192) Also fixes some style / const-correct issues that I found while documenting. --- architecture/event_processor.md | 145 ++++++++++++++++++ .../launchdarkly/events/detail/outbox.hpp | 6 +- .../launchdarkly/events/detail/summarizer.hpp | 4 +- libs/internal/src/events/outbox.cpp | 2 +- libs/internal/src/events/summarizer.cpp | 4 +- .../src/serialization/events/json_events.cpp | 4 +- libs/internal/tests/event_summarizer_test.cpp | 4 +- 7 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 architecture/event_processor.md diff --git a/architecture/event_processor.md b/architecture/event_processor.md new file mode 100644 index 000000000..022ea59ec --- /dev/null +++ b/architecture/event_processor.md @@ -0,0 +1,145 @@ +# Analytic Event Processor + +The Event Processor is responsible for consuming, batching, and delivering events generated +by the server and client-side LaunchDarkly SDKs. + +```mermaid +classDiagram + IEventProcessor <|-- NullEventProcessor + IEventProcessor <|-- AsioEventProcessor + + AsioEventProcessor *-- LRUCache + AsioEventProcessor *-- Outbox + AsioEventProcessor *-- WorkerPool + AsioEventProcessor *-- Summarizer + + RequestWorker *-- EventBatch + WorkerPool *-- "5" RequestWorker + + TrackEvent -- TrackEventParams: (alias) + InputEvent *-- IdentifyEventParams + InputEvent *-- FeatureEventParams + InputEvent *-- TrackEventParams + + + OutputEvent *-- IndexEvent + OutputEvent *-- FeatureEvent + OutputEvent *-- DebugEvent + OutputEvent *-- IdentifyEvent + OutputEvent *-- TrackEvent + + EventBatch --> Outbox: Pulls individual events from.. + EventBatch --> Summarizer: Pulls summary events from.. + + IEventProcessor --> InputEvent + Outbox --> OutputEvent + + Summarizer --> FeatureEventParams + + + class IEventProcessor { + <> + +SendAsync(InputEvent event) void + +FlushAsync() void + +ShutdownAsync() void + } + + class NullEventProcessor { + + } + + class AsioEventProcessor { + + } + + class EventBatch { + +const Count() size_t + +const Request() network:: HttpRequest + +const Target() std:: string + } + + class LRUCache { + +Notice(std:: string value) bool + +const Size() size_t + +Clear() void + } + + class Outbox { + +PushDiscardingOverflow(std:: vector~OutputEvent~ events) bool + +Consume() std:: vector~OutputEvent~ + +const Empty() bool + } + + class RequestWorker { + +const Available() bool + +AsyncDeliver(EventBatch, delivery_callback) + } + + class WorkerPool { + +Get(worker_callback) void + } + + class Summarizer { + +Update(FeatureEventParams) void + +Finish() + +const StartTime() Time + +const EndTime() Time + } + +%% note: the 'namespace' feature isn't supported on Github yet +%% namespace events { + class InputEvent { + +std:: variant + } + + + class OutputEvent { + +std:: variant + } + + class FeatureEventParams { + + } + + class IdentifyEventParams { + + } + + class TrackEventParams { + + } + + class FeatureEvent { + + } + + class DebugEvent { + + } + + class IdentifyEvent { + + } + + class IndexEvent { + + } + + class TrackEvent { + + } + +%% } +``` + +### Notes + +SDKs may be configured to disable events, so `NullEventProcessor` is made available. This component accepts +events generated +by the SDK and discards them. + +If events are enabled, SDKs use the `AsioEventProcessor` implementation, which is an asynchronous processor +utilizing `boost::asio`. + +Most event definitions are shared between the server and client-side SDKs. Unique to the server-side SDK +is `IndexEvent`. diff --git a/libs/internal/include/launchdarkly/events/detail/outbox.hpp b/libs/internal/include/launchdarkly/events/detail/outbox.hpp index 64712a183..50e09422a 100644 --- a/libs/internal/include/launchdarkly/events/detail/outbox.hpp +++ b/libs/internal/include/launchdarkly/events/detail/outbox.hpp @@ -28,18 +28,18 @@ class Outbox { * @return True if all events were accepted; false if >= 1 events were * dropped. */ - bool PushDiscardingOverflow(std::vector events); + [[nodiscard]] bool PushDiscardingOverflow(std::vector events); /** * Consumes all events in the outbox. * @return All events in the outbox, in the order they were pushed. */ - std::vector Consume(); + [[nodiscard]] std::vector Consume(); /** * True if the outbox is empty. */ - bool Empty(); + [[nodiscard]] bool Empty() const; private: std::queue items_; diff --git a/libs/internal/include/launchdarkly/events/detail/summarizer.hpp b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp index 37a23ef72..e87a6055c 100644 --- a/libs/internal/include/launchdarkly/events/detail/summarizer.hpp +++ b/libs/internal/include/launchdarkly/events/detail/summarizer.hpp @@ -55,12 +55,12 @@ class Summarizer { /** * Returns the summary's start time as given in the constructor. */ - [[nodiscard]] Time start_time() const; + [[nodiscard]] Time StartTime() const; /** * Returns the summary's end time as specified using Finish. */ - [[nodiscard]] Time end_time() const; + [[nodiscard]] Time EndTime() const; struct VariationSummary { public: diff --git a/libs/internal/src/events/outbox.cpp b/libs/internal/src/events/outbox.cpp index 7b131670a..33e95d008 100644 --- a/libs/internal/src/events/outbox.cpp +++ b/libs/internal/src/events/outbox.cpp @@ -34,7 +34,7 @@ std::vector Outbox::Consume() { return out; } -bool Outbox::Empty() { +bool Outbox::Empty() const { return items_.empty(); } diff --git a/libs/internal/src/events/summarizer.cpp b/libs/internal/src/events/summarizer.cpp index 1c3ad7202..3ca0ff236 100644 --- a/libs/internal/src/events/summarizer.cpp +++ b/libs/internal/src/events/summarizer.cpp @@ -37,11 +37,11 @@ Summarizer& Summarizer::Finish(Time end_time) { return *this; } -Summarizer::Time Summarizer::start_time() const { +Summarizer::Time Summarizer::StartTime() const { return start_time_; } -Summarizer::Time Summarizer::end_time() const { +Summarizer::Time Summarizer::EndTime() const { return end_time_; } diff --git a/libs/internal/src/serialization/events/json_events.cpp b/libs/internal/src/serialization/events/json_events.cpp index ad5f3b8ee..3c13687c3 100644 --- a/libs/internal/src/serialization/events/json_events.cpp +++ b/libs/internal/src/serialization/events/json_events.cpp @@ -131,8 +131,8 @@ void tag_invoke(boost::json::value_from_tag const& tag, auto& obj = json_value.emplace_object(); obj.emplace("kind", "summary"); obj.emplace("startDate", - boost::json::value_from(Date{summary.start_time()})); - obj.emplace("endDate", boost::json::value_from(Date{summary.end_time()})); + boost::json::value_from(Date{summary.StartTime()})); + obj.emplace("endDate", boost::json::value_from(Date{summary.EndTime()})); obj.emplace("features", boost::json::value_from(summary.Features())); } } // namespace launchdarkly::events::detail diff --git a/libs/internal/tests/event_summarizer_test.cpp b/libs/internal/tests/event_summarizer_test.cpp index ad3e59e9e..9e657ef8c 100644 --- a/libs/internal/tests/event_summarizer_test.cpp +++ b/libs/internal/tests/event_summarizer_test.cpp @@ -26,13 +26,13 @@ TEST(SummarizerTests, IsEmptyOnConstruction) { TEST(SummarizerTests, DefaultConstructionUsesZeroStartTime) { Summarizer summarizer; - ASSERT_EQ(summarizer.start_time(), TimeZero()); + ASSERT_EQ(summarizer.StartTime(), TimeZero()); } TEST(SummarizerTests, ExplicitStartTimeIsCorrect) { auto start = std::chrono::system_clock::from_time_t(12345); Summarizer summarizer(start); - ASSERT_EQ(summarizer.start_time(), start); + ASSERT_EQ(summarizer.StartTime(), start); } struct EvaluationParams { From 0b29ea29bf3a27ca07e7e53b2b2e853d32a45248 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:16:24 -0700 Subject: [PATCH 35/56] feat: Serialize flags and segments. (#194) --- .github/workflows/client.yml | 6 +- .github/workflows/common.yml | 2 +- .github/workflows/internal.yml | 2 +- .github/workflows/sse.yml | 2 +- .../include/launchdarkly/data_model/flag.hpp | 5 +- .../launchdarkly/serialization/json_flag.hpp | 42 ++ .../serialization/json_rule_clause.hpp | 13 + .../serialization/json_segment.hpp | 17 + .../serialization/value_mapping.hpp | 14 + libs/internal/src/data_model/flag.cpp | 2 + libs/internal/src/serialization/json_flag.cpp | 154 +++++++ .../src/serialization/json_rule_clause.cpp | 77 ++++ .../src/serialization/json_segment.cpp | 44 ++ .../src/serialization/value_mapping.cpp | 5 + .../tests/data_model_serialization_test.cpp | 382 ++++++++++++++++++ 15 files changed, 759 insertions(+), 8 deletions(-) diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index fc40cca97..456334652 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -30,14 +30,14 @@ jobs: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} extra_params: '-skip-from ./contract-tests/sdk-contract-tests/test-suppressions.txt' - build-test: + build-test-client: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - uses: ./.github/actions/ci with: cmake_target: launchdarkly-cpp-client - build-test-mac: + build-test-client-mac: runs-on: macos-12 steps: - run: | @@ -52,7 +52,7 @@ jobs: with: cmake_target: launchdarkly-cpp-client platform_version: 12 - build-test-windows: + build-test-client-windows: runs-on: windows-2022 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/common.yml b/.github/workflows/common.yml index d33bc4423..438bcdc6c 100644 --- a/.github/workflows/common.yml +++ b/.github/workflows/common.yml @@ -11,7 +11,7 @@ on: - '**.md' jobs: - build-test: + build-test-common: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/internal.yml b/.github/workflows/internal.yml index c8f998a4a..d116de8bb 100644 --- a/.github/workflows/internal.yml +++ b/.github/workflows/internal.yml @@ -11,7 +11,7 @@ on: - '**.md' jobs: - build-test: + build-test-internal: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/sse.yml b/.github/workflows/sse.yml index b132886eb..9452f6389 100644 --- a/.github/workflows/sse.yml +++ b/.github/workflows/sse.yml @@ -11,7 +11,7 @@ on: - '**.md' jobs: - build-test: + build-test-sse: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index fc312d666..dee59c11e 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -31,7 +31,8 @@ struct Flag { Weight weight; bool untracked; - WeightedVariation() = default; + WeightedVariation(); + WeightedVariation(Variation index, Weight weight); static WeightedVariation Untracked(Variation index, Weight weight); @@ -48,7 +49,7 @@ struct Flag { DEFINE_CONTEXT_KIND_FIELD(contextKind) Rollout() = default; - Rollout(std::vector); + explicit Rollout(std::vector); }; using VariationOrRollout = std::variant; diff --git a/libs/internal/include/launchdarkly/serialization/json_flag.hpp b/libs/internal/include/launchdarkly/serialization/json_flag.hpp index 88c1edd57..775e2cb15 100644 --- a/libs/internal/include/launchdarkly/serialization/json_flag.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_flag.hpp @@ -62,4 +62,46 @@ tl::expected, JsonError> tag_invoke( tl::expected, JsonError>> const& unused, boost::json::value const& json_value); +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout const& rollout); + +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::VariationOrRollout const& variation_or_rollout); + +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::WeightedVariation const& weighted_variation); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::Kind const& kind); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Prerequisite const& prerequisite); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Target const& target); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rule const& rule); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::ClientSideAvailability const& availability); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag const& flag); + +} // namespace data_model } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp index f0eab96a4..a66d85606 100644 --- a/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_rule_clause.hpp @@ -23,4 +23,17 @@ tl::expected tag_invoke( tl::expected> const& unused, boost::json::value const& json_value); +// Serialization needs to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause const& clause); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause::Op const& op); + +} // namespace data_model + } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/json_segment.hpp b/libs/internal/include/launchdarkly/serialization/json_segment.hpp index ce73c719b..83987903e 100644 --- a/libs/internal/include/launchdarkly/serialization/json_segment.hpp +++ b/libs/internal/include/launchdarkly/serialization/json_segment.hpp @@ -23,4 +23,21 @@ tl::expected, JsonError> tag_invoke( JsonError>> const& unused, boost::json::value const& json_value); +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment const& segment); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Target const& target); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Rule const& rule); + +} // namespace data_model + } // namespace launchdarkly diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index 5d7d7484a..753146d1a 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -156,4 +156,18 @@ template <> std::string ValueOrDefault(boost::json::object::const_iterator iterator, boost::json::object::const_iterator end, std::string default_value); + +template +void WriteMinimal(boost::json::object& obj, + std::string const& key, // No copy when not used. + std::optional val) { + if (val.has_value()) { + obj.emplace(key, val.value()); + } +} + +void WriteMinimal(boost::json::object& obj, + std::string const& key, // No copy when not used. + bool val); + } // namespace launchdarkly diff --git a/libs/internal/src/data_model/flag.cpp b/libs/internal/src/data_model/flag.cpp index 78d4f4651..646f36d83 100644 --- a/libs/internal/src/data_model/flag.cpp +++ b/libs/internal/src/data_model/flag.cpp @@ -16,6 +16,8 @@ Flag::Rollout::WeightedVariation Flag::Rollout::WeightedVariation::Untracked( Flag::Weight weight_) { return {variation_, weight_, true}; } +Flag::Rollout::WeightedVariation::WeightedVariation() + : variation(0), weight(0), untracked(false) {} Flag::Rollout::Rollout(std::vector variations_) : variations(std::move(variations_)), diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index f25d48a8d..c6897a492 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -208,4 +208,158 @@ tag_invoke(boost::json::value_to_tag< return std::make_optional(variation); } +namespace data_model { +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::WeightedVariation const& weighted_variation) { + auto& obj = json_value.emplace_object(); + obj.emplace("variation", weighted_variation.variation); + obj.emplace("weight", weighted_variation.weight); + + WriteMinimal(obj, "untracked", weighted_variation.untracked); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout::Kind const& kind) { + switch (kind) { + case Flag::Rollout::Kind::kUnrecognized: + // TODO: Should we be preserving the original string. + break; + case Flag::Rollout::Kind::kExperiment: + json_value.emplace_string() = "experiment"; + break; + case Flag::Rollout::Kind::kRollout: + json_value.emplace_string() = "rollout"; + break; + } +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rollout const& rollout) { + auto& obj = json_value.emplace_object(); + + obj.emplace("variations", boost::json::value_from(rollout.variations)); + if (rollout.kind != Flag::Rollout::Kind::kUnrecognized) { + // TODO: Should we be preserving the original string and putting it in. + obj.emplace("kind", boost::json::value_from(rollout.kind)); + } + WriteMinimal(obj, "seed", rollout.seed); + obj.emplace("bucketBy", rollout.bucketBy.RedactionName()); + obj.emplace("contextKind", rollout.contextKind.t); +} + +void tag_invoke( + boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::VariationOrRollout const& variation_or_rollout) { + auto& obj = json_value.emplace_object(); + std::visit( + [&obj](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + obj.emplace("rollout", boost::json::value_from(arg)); + } else if constexpr (std::is_same_v) { + obj.emplace("variation", arg); + } + }, + variation_or_rollout); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Prerequisite const& prerequisite) { + auto& obj = json_value.emplace_object(); + obj.emplace("key", prerequisite.key); + obj.emplace("variation", prerequisite.variation); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Target const& target) { + auto& obj = json_value.emplace_object(); + obj.emplace("values", boost::json::value_from(target.values)); + obj.emplace("variation", target.variation); + obj.emplace("contextKind", target.contextKind.t); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::ClientSideAvailability const& availability) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "usingEnvironmentId", availability.usingEnvironmentId); + WriteMinimal(obj, "usingMobileKey", availability.usingMobileKey); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag::Rule const& rule) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "trackEvents", rule.trackEvents); + WriteMinimal(obj, "id", rule.id); + std::visit( + [&obj](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + obj.emplace("rollout", boost::json::value_from(arg)); + } else if constexpr (std::is_same_v) { + obj.emplace("variation", arg); + } + }, + rule.variationOrRollout); + obj.emplace("clauses", boost::json::value_from(rule.clauses)); +} + +// The "targets" array in a flag cannot have a contextKind, so this intermediate +// representation allows the flag data model to use Flag::Target, but still +// serialize a user target correctly. +struct UserTarget { + std::vector values; + std::uint64_t variation; + UserTarget(data_model::Flag::Target const& target) + : values(target.values), variation(target.variation) {} +}; + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + UserTarget const& target) { + auto& obj = json_value.emplace_object(); + obj.emplace("values", boost::json::value_from(target.values)); + obj.emplace("variation", target.variation); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Flag const& flag) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "trackEvents", flag.trackEvents); + WriteMinimal(obj, "clientSide", flag.clientSide); + WriteMinimal(obj, "on", flag.on); + WriteMinimal(obj, "trackEventsFallthrough", flag.trackEventsFallthrough); + WriteMinimal(obj, "debugEventsUntilDate", flag.debugEventsUntilDate); + WriteMinimal(obj, "salt", flag.salt); + WriteMinimal(obj, "offVariation", flag.offVariation); + obj.emplace("key", flag.key); + obj.emplace("version", flag.version); + obj.emplace("variations", boost::json::value_from(flag.variations)); + obj.emplace("rules", boost::json::value_from(flag.rules)); + obj.emplace("prerequisites", boost::json::value_from(flag.prerequisites)); + obj.emplace("fallthrough", boost::json::value_from(flag.fallthrough)); + obj.emplace("clientSideAvailability", + boost::json::value_from(flag.clientSideAvailability)); + obj.emplace("contextTargets", boost::json::value_from(flag.contextTargets)); + + std::vector user_targets; + for (auto const& target : flag.targets) { + user_targets.emplace_back(target); + } + obj.emplace("targets", boost::json::value_from(user_targets)); +} + +} // namespace data_model + } // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_rule_clause.cpp b/libs/internal/src/serialization/json_rule_clause.cpp index df6530d23..d4cc6cc54 100644 --- a/libs/internal/src/serialization/json_rule_clause.cpp +++ b/libs/internal/src/serialization/json_rule_clause.cpp @@ -98,4 +98,81 @@ tl::expected tag_invoke( return maybe_op.value().value_or(data_model::Clause::Op::kUnrecognized); } +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause const& clause) { + auto& obj = json_value.emplace_object(); + + obj.emplace("values", boost::json::value_from(clause.values)); + + WriteMinimal(obj, "negate", clause.negate); + + if (clause.op != data_model::Clause::Op::kUnrecognized) { + // TODO: Should we store the original value? + obj.emplace("op", boost::json::value_from(clause.op)); + } + if (clause.attribute.Valid()) { + obj.emplace("attribute", clause.attribute.RedactionName()); + } + obj.emplace("contextKind", clause.contextKind.t); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Clause::Op const& op) { + switch (op) { + case data_model::Clause::Op::kUnrecognized: + // TODO: Should we do anything? + break; + case data_model::Clause::Op::kIn: + json_value.emplace_string() = "in"; + break; + case data_model::Clause::Op::kStartsWith: + json_value.emplace_string() = "startsWith"; + break; + case data_model::Clause::Op::kEndsWith: + json_value.emplace_string() = "endsWith"; + break; + case data_model::Clause::Op::kMatches: + json_value.emplace_string() = "matches"; + break; + case data_model::Clause::Op::kContains: + json_value.emplace_string() = "contains"; + break; + case data_model::Clause::Op::kLessThan: + json_value.emplace_string() = "lessThan"; + break; + case data_model::Clause::Op::kLessThanOrEqual: + json_value.emplace_string() = "lessThanOrEqual"; + break; + case data_model::Clause::Op::kGreaterThan: + json_value.emplace_string() = "greaterThan"; + break; + case data_model::Clause::Op::kGreaterThanOrEqual: + json_value.emplace_string() = "greaterThanOrEqual"; + break; + case data_model::Clause::Op::kBefore: + json_value.emplace_string() = "before"; + break; + case data_model::Clause::Op::kAfter: + json_value.emplace_string() = "after"; + break; + case data_model::Clause::Op::kSemVerEqual: + json_value.emplace_string() = "semVerEqual"; + break; + case data_model::Clause::Op::kSemVerLessThan: + json_value.emplace_string() = "semVerLessThan"; + break; + case data_model::Clause::Op::kSemVerGreaterThan: + json_value.emplace_string() = "semVerGreaterThan"; + break; + case data_model::Clause::Op::kSegmentMatch: + json_value.emplace_string() = "segmentMatch"; + break; + } +} +} // namespace data_model + } // namespace launchdarkly diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp index 3ef22453b..cab5fbb77 100644 --- a/libs/internal/src/serialization/json_segment.cpp +++ b/libs/internal/src/serialization/json_segment.cpp @@ -97,4 +97,48 @@ tl::expected, JsonError> tag_invoke( return segment; } +// Serializers need to be in launchdarkly::data_model for ADL. +namespace data_model { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment const& segment) { + auto& obj = json_value.emplace_object(); + obj.emplace("key", segment.key); + obj.emplace("version", segment.version); + WriteMinimal(obj, "salt", segment.salt); + WriteMinimal(obj, "generation", segment.generation); + WriteMinimal(obj, "unboundedContextKind", segment.unboundedContextKind); + WriteMinimal(obj, "unbounded", segment.unbounded); + + obj.emplace("rules", boost::json::value_from(segment.rules)); + obj.emplace("excluded", boost::json::value_from(segment.excluded)); + obj.emplace("excludedContexts", + boost::json::value_from(segment.excludedContexts)); + obj.emplace("included", boost::json::value_from(segment.included)); + obj.emplace("includedContexts", + boost::json::value_from(segment.includedContexts)); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Target const& target) { + auto& obj = json_value.emplace_object(); + obj.emplace("values", boost::json::value_from(target.values)); + obj.emplace("contextKind", target.contextKind); +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + data_model::Segment::Rule const& rule) { + auto& obj = json_value.emplace_object(); + WriteMinimal(obj, "weight", rule.weight); + WriteMinimal(obj, "id", rule.id); + obj.emplace("clauses", boost::json::value_from(rule.clauses)); + obj.emplace("bucketBy", rule.bucketBy.RedactionName()); + obj.emplace("rolloutContextKind", rule.rolloutContextKind.t); +} + +} // namespace data_model + } // namespace launchdarkly diff --git a/libs/internal/src/serialization/value_mapping.cpp b/libs/internal/src/serialization/value_mapping.cpp index b83183725..7142d8bf7 100644 --- a/libs/internal/src/serialization/value_mapping.cpp +++ b/libs/internal/src/serialization/value_mapping.cpp @@ -70,4 +70,9 @@ uint64_t ValueOrDefault(boost::json::object::const_iterator iterator, return default_value; } +void WriteMinimal(boost::json::object& obj, std::string const& key, bool val) { + if (val) { + obj.emplace(key, val); + } +} } // namespace launchdarkly diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 8e1feb5ee..00ecfcfd9 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -1,5 +1,7 @@ #include +#include + #include #include #include @@ -368,3 +370,383 @@ TEST(ClientSideAvailabilityTests, DeserializesAllFields) { ASSERT_TRUE(result->usingMobileKey); ASSERT_TRUE(result->usingEnvironmentId); } + +TEST(WeightedVariationTests, SerializeAllFields) { + data_model::Flag::Rollout::WeightedVariation variation(1, 2); + variation.untracked = true; + auto json = boost::json::value_from(variation); + + auto expected = boost::json::parse( + R"({"variation": 1, "weight": 2, "untracked": true})"); + + EXPECT_EQ(expected, json); +} + +TEST(WeightedVariationTests, SerializeUntrackedOnlyTrue) { + data_model::Flag::Rollout::WeightedVariation variation(1, 2); + variation.untracked = false; + auto json = boost::json::value_from(variation); + + auto expected = boost::json::parse(R"({"variation": 1, "weight": 2})"); + + EXPECT_EQ(expected, json); +} + +TEST(RolloutTests, SerializeAllFields) { + using Rollout = data_model::Flag::Rollout; + Rollout rollout; + rollout.kind = Rollout::Kind::kExperiment; + rollout.contextKind = "user"; + rollout.bucketBy = AttributeReference("ham"); + rollout.seed = 42; + rollout.variations = { + data_model::Flag::Rollout::WeightedVariation::Untracked(1, 2), {3, 4}}; + + auto json = boost::json::value_from(rollout); + + auto expected = boost::json::parse(R"({ + "kind": "experiment", + "contextKind": "user", + "bucketBy": "ham", + "seed": 42, + "variations": [ + {"variation": 1, "weight": 2, "untracked": true}, + {"variation": 3, "weight": 4} + ] + })"); + + EXPECT_EQ(expected, json); +} + +TEST(VariationOrRolloutTests, SerializeVariation) { + uint64_t value(5); + data_model::Flag::VariationOrRollout variation = value; + + auto json = boost::json::value_from(variation); + + auto expected = boost::json::parse(R"({"variation":5})"); + EXPECT_EQ(expected, json); +} + +TEST(VariationOrRolloutTests, SerializeRollout) { + using Rollout = data_model::Flag::Rollout; + Rollout rollout; + rollout.kind = Rollout::Kind::kExperiment; + rollout.contextKind = "user"; + rollout.bucketBy = AttributeReference("ham"); + rollout.seed = 42; + rollout.variations = { + data_model::Flag::Rollout::WeightedVariation::Untracked(1, 2), {3, 4}}; + data_model::Flag::VariationOrRollout var_or_roll = rollout; + auto json = boost::json::value_from(var_or_roll); + + auto expected = boost::json::parse(R"({ + "rollout":{ + "kind": "experiment", + "contextKind": "user", + "bucketBy": "ham", + "seed": 42, + "variations": [ + {"variation": 1, "weight": 2, "untracked": true}, + {"variation": 3, "weight": 4} + ] + }})"); + EXPECT_EQ(expected, json); +} + +TEST(PrerequisiteTests, SerializeAll) { + data_model::Flag::Prerequisite prerequisite{"potato", 6}; + auto json = boost::json::value_from(prerequisite); + + auto expected = boost::json::parse(R"({"key":"potato","variation":6})"); + EXPECT_EQ(expected, json); +} + +TEST(TargetTests, SerializeAll) { + data_model::Flag::Target target{{"a", "b"}, 42, ContextKind("taco_stand")}; + auto json = boost::json::value_from(target); + + auto expected = boost::json::parse( + R"({"values":["a", "b"], "variation": 42, "contextKind":"taco_stand"})"); + EXPECT_EQ(expected, json); +} + +TEST(ClientSideAvailabilityTests, SerializeAll) { + data_model::Flag::ClientSideAvailability availability{true, true}; + + auto json = boost::json::value_from(availability); + + auto expected = boost::json::parse( + R"({"usingMobileKey": true, "usingEnvironmentId": true})"); + EXPECT_EQ(expected, json); +} + +class ClauseOperatorsFixture + : public ::testing::TestWithParam {}; + +INSTANTIATE_TEST_SUITE_P( + ClauseTests, + ClauseOperatorsFixture, + testing::Values(data_model::Clause::Op::kSegmentMatch, + data_model::Clause::Op::kAfter, + data_model::Clause::Op::kBefore, + data_model::Clause::Op::kContains, + data_model::Clause::Op::kEndsWith, + data_model::Clause::Op::kGreaterThan, + data_model::Clause::Op::kGreaterThanOrEqual, + data_model::Clause::Op::kIn, + data_model::Clause::Op::kLessThan, + data_model::Clause::Op::kLessThanOrEqual, + data_model::Clause::Op::kMatches, + data_model::Clause::Op::kSemVerEqual, + data_model::Clause::Op::kSemVerGreaterThan, + data_model::Clause::Op::kSemVerLessThan, + data_model::Clause::Op::kStartsWith)); + +TEST_P(ClauseOperatorsFixture, AllOperatorsSerializeDeserialize) { + auto op = GetParam(); + + auto serialized = boost::json::serialize(boost::json::value_from(op)); + auto parsed = boost::json::parse(serialized); + auto deserialized = boost::json::value_to< + tl::expected, JsonError>>(parsed); + + EXPECT_EQ(op, **deserialized); +} + +TEST(ClauseTests, SerializeAll) { + data_model::Clause clause{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}; + + auto json = boost::json::value_from(clause); + auto expected = boost::json::parse( + R"({ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + })"); + EXPECT_EQ(expected, json); +} + +TEST(FlagRuleTests, SerializeAllRollout) { + using Rollout = data_model::Flag::Rollout; + Rollout rollout; + rollout.kind = Rollout::Kind::kExperiment; + rollout.contextKind = "user"; + rollout.bucketBy = AttributeReference("ham"); + rollout.seed = 42; + rollout.variations = { + data_model::Flag::Rollout::WeightedVariation::Untracked(1, 2), {3, 4}}; + data_model::Flag::Rule rule{{{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}}, + rollout, + true, + "therule"}; + + auto json = boost::json::value_from(rule); + auto expected = boost::json::parse( + R"({ + "clauses":[{ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + }], + "rollout": { + "kind": "experiment", + "contextKind": "user", + "bucketBy": "ham", + "seed": 42, + "variations": [ + {"variation": 1, "weight": 2, "untracked": true}, + {"variation": 3, "weight": 4} + ] + }, + "trackEvents": true, + "id": "therule" + })"); + EXPECT_EQ(expected, json); +} + +TEST(FlagTests, SerializeAll) { + uint64_t fallthrough(42); + data_model::Flag flag{ + "the-key", + 21, // version + true, // on + fallthrough, // fallthrough + {"a", "b"}, // variations + {{"prereqA", 2}, {"prereqB", 3}}, // prerequisites + {{{ + "1", + "2", + "3", + }, + 12, + ContextKind("user")}}, // targets + {{{ + "4", + "5", + "6", + }, + 24, + ContextKind("bob")}}, // contextTargets + {}, // rules + 84, // offVariation + true, // clientSide + data_model::Flag::ClientSideAvailability{true, true}, + "4242", // salt + true, // trackEvents + true, // trackEventsFalltrhough + 900 // debugEventsUntilDate + }; + + auto json = boost::json::value_from(flag); + auto expected = boost::json::parse( + R"({ + "trackEvents":true, + "clientSide":true, + "on":true, + "trackEventsFallthrough":true, + "debugEventsUntilDate":900, + "salt":"4242", + "offVariation":84, + "key":"the-key", + "version":21, + "variations":["a","b"], + "rules":[], + "prerequisites":[{"key":"prereqA","variation":2}, + {"key":"prereqB","variation":3}], + "fallthrough":{"variation":42}, + "clientSideAvailability": + {"usingEnvironmentId":true,"usingMobileKey":true}, + "contextTargets": + [{"values":["4","5","6"],"variation":24,"contextKind":"bob"}], + "targets":[{"values":["1","2","3"],"variation":12}] + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentTargetTests, SerializeAll) { + data_model::Segment::Target target{"bob", {"bill", "sam"}}; + + auto json = boost::json::value_from(target); + auto expected = boost::json::parse( + R"({ + "contextKind": "bob", + "values": ["bill", "sam"] + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentRuleTests, SerializeAll) { + data_model::Segment::Rule rule{{{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}}, + "ididid", + 300, + ContextKind("bob"), + "/happy"}; + + auto json = boost::json::value_from(rule); + auto expected = boost::json::parse( + R"({ + "clauses": [{ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + }], + "id": "ididid", + "weight": 300, + "rolloutContextKind": "bob", + "bucketBy": "/happy" + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentTests, SerializeBasicAll) { + data_model::Segment segment{ + "my-segment", + 87, + {"bob", "sam"}, + {"sally", "johan"}, + {{"vegetable", {"potato", "yam"}}}, + {{"material", {"cardboard", "plastic"}}}, + {{{{data_model::Clause::Op::kIn, + {"a", "b"}, + true, + ContextKind("bob"), + "/potato"}}, + "ididid", + 300, + ContextKind("bob"), + "/happy"}}, + "salty", + false, + std::nullopt, + std::nullopt, + }; + + auto json = boost::json::value_from(segment); + auto expected = boost::json::parse( + R"({ + "key": "my-segment", + "version": 87, + "included": ["bob", "sam"], + "excluded": ["sally", "johan"], + "includedContexts": + [{"contextKind": "vegetable", "values":["potato", "yam"]}], + "excludedContexts": + [{"contextKind": "material", "values":["cardboard", "plastic"]}], + "salt": "salty", + "rules":[{ + "clauses": [{ + "op": "in", + "negate": true, + "values": ["a", "b"], + "contextKind": "bob", + "attribute": "/potato" + }], + "id": "ididid", + "weight": 300, + "rolloutContextKind": "bob", + "bucketBy": "/happy" + }] + })"); + EXPECT_EQ(expected, json); +} + +TEST(SegmentTests, SerializeUnbounded) { + data_model::Segment segment{"my-segment", 87, {}, {}, {}, {}, + {}, "salty", true, "company", 12}; + + auto json = boost::json::value_from(segment); + auto expected = boost::json::parse( + R"({ + "key": "my-segment", + "version": 87, + "included": [], + "excluded": [], + "includedContexts": [], + "excludedContexts": [], + "salt": "salty", + "rules":[], + "unbounded": true, + "unboundedContextKind": "company", + "generation": 12 + })"); + EXPECT_EQ(expected, json); +} From a592e87aea7de20c34b6a686eecba78f2715df6f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 21 Aug 2023 09:50:43 -0700 Subject: [PATCH 36/56] feat: build server SDK in CI (#198) Adds a build for the server SDK, with various build fixes. --- .github/workflows/server.yml | 49 +++++++++++++++++++ .../launchdarkly/attribute_reference.hpp | 2 +- .../data_source_status_manager_base.hpp | 1 + libs/internal/src/serialization/json_flag.cpp | 18 +++---- .../src/serialization/json_rule_clause.cpp | 2 +- .../src/serialization/json_sdk_data_set.cpp | 2 +- .../src/serialization/json_segment.cpp | 4 +- .../data_sources/streaming_data_source.cpp | 2 + .../src/data_store/dependency_tracker.hpp | 1 + .../persistent/expiration_tracker.hpp | 1 + libs/server-sdk/src/evaluation/bucketing.cpp | 2 +- libs/server-sdk/src/evaluation/bucketing.hpp | 2 +- libs/server-sdk/src/evaluation/operators.cpp | 3 -- libs/server-sdk/tests/operator_tests.cpp | 26 +++++----- libs/server-sdk/tests/rule_tests.cpp | 2 +- libs/server-sdk/tests/timestamp_tests.cpp | 6 --- 16 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/server.yml diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml new file mode 100644 index 000000000..175ed5db3 --- /dev/null +++ b/.github/workflows/server.yml @@ -0,0 +1,49 @@ +name: libs/server-sdk + +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [ main, server-side ] + paths-ignore: + - '**.md' + +jobs: + build-test-server: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + with: + cmake_target: launchdarkly-cpp-server + build-test-server-mac: + runs-on: macos-12 + steps: + - run: | + brew link --overwrite openssl@1.1 + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" >> "$GITHUB_ENV" + # For debugging + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@1.1)" + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + with: + cmake_target: launchdarkly-cpp-server + platform_version: 12 + build-test-server-windows: + runs-on: windows-2022 + steps: + - uses: actions/checkout@v3 + - uses: ilammy/msvc-dev-cmd@v1 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: 'C:\Program Files\OpenSSL' + BOOST_LIBRARY_DIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' + BOOST_LIBRARYDIR: 'C:\local\boost_1_81_0\lib64-msvc-14.3' + with: + cmake_target: launchdarkly-cpp-server + platform_version: 2022 + toolset: msvc diff --git a/libs/common/include/launchdarkly/attribute_reference.hpp b/libs/common/include/launchdarkly/attribute_reference.hpp index c1e077e65..13ff74043 100644 --- a/libs/common/include/launchdarkly/attribute_reference.hpp +++ b/libs/common/include/launchdarkly/attribute_reference.hpp @@ -126,7 +126,7 @@ class AttributeReference { /** * Default constructs an invalid attribute reference. */ - explicit AttributeReference(); + AttributeReference(); bool operator==(AttributeReference const& other) const { return components_ == other.components_; diff --git a/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp b/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp index fdf0f4a59..b791d4e0c 100644 --- a/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp +++ b/libs/internal/include/launchdarkly/data_sources/data_source_status_manager_base.hpp @@ -2,6 +2,7 @@ #include #include +#include #include diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index c6897a492..85e797757 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -19,7 +19,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::Rollout rollout; + data_model::Flag::Rollout rollout{}; PARSE_FIELD(rollout.variations, obj, "variations"); PARSE_FIELD_DEFAULT(rollout.kind, obj, "kind", @@ -49,7 +49,7 @@ tag_invoke(boost::json::value_to_tag, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::Target target; + data_model::Flag::Target target{}; PARSE_FIELD(target.values, obj, "values"); PARSE_FIELD(target.variation, obj, "variation"); PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", @@ -115,7 +115,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::Rule rule; + data_model::Flag::Rule rule{}; PARSE_FIELD(rule.trackEvents, obj, "trackEvents"); PARSE_FIELD(rule.clauses, obj, "clauses"); @@ -144,7 +144,7 @@ tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Flag::ClientSideAvailability client_side_availability; + data_model::Flag::ClientSideAvailability client_side_availability{}; PARSE_FIELD(client_side_availability.usingEnvironmentId, obj, "usingEnvironmentId"); PARSE_FIELD(client_side_availability.usingMobileKey, obj, "usingMobileKey"); @@ -161,7 +161,7 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - data_model::Flag flag; + data_model::Flag flag{}; PARSE_REQUIRED_FIELD(flag.key, obj, "key"); @@ -195,14 +195,14 @@ tag_invoke(boost::json::value_to_tag< REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - std::optional rollout; + std::optional rollout{}; PARSE_CONDITIONAL_FIELD(rollout, obj, "rollout"); if (rollout) { return std::make_optional(*rollout); } - data_model::Flag::Variation variation; + data_model::Flag::Variation variation{}; PARSE_REQUIRED_FIELD(variation, obj, "variation"); return std::make_optional(variation); diff --git a/libs/internal/src/serialization/json_rule_clause.cpp b/libs/internal/src/serialization/json_rule_clause.cpp index d4cc6cc54..febc96f28 100644 --- a/libs/internal/src/serialization/json_rule_clause.cpp +++ b/libs/internal/src/serialization/json_rule_clause.cpp @@ -16,7 +16,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Clause clause; + data_model::Clause clause{}; PARSE_REQUIRED_FIELD(clause.op, obj, "op"); PARSE_FIELD(clause.values, obj, "values"); diff --git a/libs/internal/src/serialization/json_sdk_data_set.cpp b/libs/internal/src/serialization/json_sdk_data_set.cpp index 84043c377..e7b894a0a 100644 --- a/libs/internal/src/serialization/json_sdk_data_set.cpp +++ b/libs/internal/src/serialization/json_sdk_data_set.cpp @@ -18,7 +18,7 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - data_model::SDKDataSet data_set; + data_model::SDKDataSet data_set{}; PARSE_FIELD(data_set.flags, obj, "flags"); PARSE_FIELD(data_set.segments, obj, "segments"); diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp index cab5fbb77..dfd744191 100644 --- a/libs/internal/src/serialization/json_segment.cpp +++ b/libs/internal/src/serialization/json_segment.cpp @@ -37,7 +37,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Segment::Rule rule; + data_model::Segment::Rule rule{}; PARSE_FIELD(rule.clauses, obj, "clauses"); @@ -77,7 +77,7 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); - data_model::Segment segment; + data_model::Segment segment{}; PARSE_REQUIRED_FIELD(segment.key, obj, "key"); PARSE_REQUIRED_FIELD(segment.version, obj, "version"); diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.cpp b/libs/server-sdk/src/data_sources/streaming_data_source.cpp index b2f9e8a7d..e697a522f 100644 --- a/libs/server-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -24,6 +24,8 @@ static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { return "server responded with an invalid redirection"; case sse::Error::UnrecoverableClientError: return "unrecoverable client-side error"; + default: + return "unrecognized error"; } } diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_store/dependency_tracker.hpp index 1dddb78b8..13e32df7f 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_store/dependency_tracker.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp index c3cd9cdd2..bc57398c0 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/libs/server-sdk/src/evaluation/bucketing.cpp b/libs/server-sdk/src/evaluation/bucketing.cpp index 7ebd7fb8f..cd8b7cea2 100644 --- a/libs/server-sdk/src/evaluation/bucketing.cpp +++ b/libs/server-sdk/src/evaluation/bucketing.cpp @@ -98,7 +98,7 @@ AttributeReference const& Key() { return key; } -std::optional ContextHash(Value const& value, BucketPrefix prefix) { +std::optional ContextHash(Value const& value, BucketPrefix prefix) { using namespace launchdarkly::encoding; std::optional id = BucketValue(value); diff --git a/libs/server-sdk/src/evaluation/bucketing.hpp b/libs/server-sdk/src/evaluation/bucketing.hpp index b536f1921..346374590 100644 --- a/libs/server-sdk/src/evaluation/bucketing.hpp +++ b/libs/server-sdk/src/evaluation/bucketing.hpp @@ -61,7 +61,7 @@ class BucketPrefix { std::variant prefix_; }; -using ContextHashValue = float; +using ContextHashValue = double; /** * Computes the context hash value for an attribute in the given context diff --git a/libs/server-sdk/src/evaluation/operators.cpp b/libs/server-sdk/src/evaluation/operators.cpp index d5c98edb4..31ceb960f 100644 --- a/libs/server-sdk/src/evaluation/operators.cpp +++ b/libs/server-sdk/src/evaluation/operators.cpp @@ -89,9 +89,6 @@ bool RegexMatch(std::string const& context_value, // boost::bad_expression can be thrown by basic_regex when compiling a // regular expression. return false; - } catch (boost::regex_error) { - // boost::regex_error thrown on stack exhaustion - return false; } catch (std::runtime_error) { // std::runtime_error can be thrown when a call // to regex_search results in an "everlasting" search diff --git a/libs/server-sdk/tests/operator_tests.cpp b/libs/server-sdk/tests/operator_tests.cpp index 727fa66ce..e804c22e3 100644 --- a/libs/server-sdk/tests/operator_tests.cpp +++ b/libs/server-sdk/tests/operator_tests.cpp @@ -79,7 +79,11 @@ TEST(OpTests, DateComparisonMicrosecondPrecision) { } } -TEST(OpTests, DateComparisonFailsWithMoreThanMicrosecondPrecision) { +// This test is meant to verify that platforms with > microsecond precision +// still compare dates correctly. If the platform doesn't support > microsecond +// precision, then we try to verify that the reverse comparison is also false +// (if we had an equal operator we'd use that instead.) +TEST(OpTests, DateComparisonWithMoreThanMicrosecondPrecision) { auto dates = std::vector>{ // Using Zulu suffix. {"2023-10-08T02:00:00.000001Z", "2023-10-08T02:00:00.0000011Z"}, @@ -88,17 +92,15 @@ TEST(OpTests, DateComparisonFailsWithMoreThanMicrosecondPrecision) { "2023-10-08T02:00:00.00000000011+00:00"}}; for (auto const& [date1, date2] : dates) { - EXPECT_FALSE(Match(Clause::Op::kBefore, date1, date2)) - << date1 << " < " << date2; - - EXPECT_FALSE(Match(Clause::Op::kAfter, date1, date2)) - << date1 << " not > " << date2; - - EXPECT_FALSE(Match(Clause::Op::kBefore, date2, date1)) - << date2 << " not < " << date1; - - EXPECT_FALSE(Match(Clause::Op::kAfter, date2, date1)) - << date2 << " > " << date1; + bool date1_before_date2 = Match(Clause::Op::kBefore, date1, date2); + if (date1_before_date2) { + // Platform seems to support > microsecond precision. + EXPECT_TRUE(Match(Clause::Op::kAfter, date2, date1)) + << date1 << " > " << date2; + } else { + EXPECT_FALSE(Match(Clause::Op::kBefore, date2, date1)) + << date2 << " not < " << date1; + } } } diff --git a/libs/server-sdk/tests/rule_tests.cpp b/libs/server-sdk/tests/rule_tests.cpp index ca4ce7ae1..56d9d0f1b 100644 --- a/libs/server-sdk/tests/rule_tests.cpp +++ b/libs/server-sdk/tests/rule_tests.cpp @@ -67,7 +67,7 @@ TEST_P(AllOperatorsTest, Matches) { *store, stack); ASSERT_EQ(result, param.expected) << context.Get("user", "attr") << " " << clause.op << " " - << clause.values << " should be " << param.expected; + << Value(clause.values) << " should be " << param.expected; } #define MATCH true diff --git a/libs/server-sdk/tests/timestamp_tests.cpp b/libs/server-sdk/tests/timestamp_tests.cpp index 78db3107d..f448d1c94 100644 --- a/libs/server-sdk/tests/timestamp_tests.cpp +++ b/libs/server-sdk/tests/timestamp_tests.cpp @@ -61,12 +61,6 @@ INSTANTIATE_TEST_SUITE_P( BasicDate() + 123us}, TimestampTest{"2020-01-01T00:00:00.000123+00:00", "with microseconds and offset", BasicDate() + 123us}, - TimestampTest{"2020-01-01T00:00:00.123456789Z", - "floor nanoseconds with zulu offset", - BasicDate() + 123ms + 456us}, - TimestampTest{"2020-01-01T01:00:00.123456789+01:00", - "floor nanoseconds with offset", - BasicDate() + 123ms + 456us}, })); From 275bb662fdb8d9a0bdbcbf8081e9045b041debe5 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 23 Aug 2023 10:33:49 -0700 Subject: [PATCH 37/56] feat: hello-cpp-server (#202) Adds a hello app for the Server-side SDK. Modifies the existing apps to fix CMake target name conflicts. --- examples/CMakeLists.txt | 1 + examples/hello-c-client/CMakeLists.txt | 6 +-- examples/hello-cpp-client/CMakeLists.txt | 6 +-- examples/hello-cpp-client/main.cpp | 4 +- examples/hello-cpp-server/CMakeLists.txt | 15 ++++++ examples/hello-cpp-server/main.cpp | 59 ++++++++++++++++++++++++ 6 files changed, 83 insertions(+), 8 deletions(-) create mode 100644 examples/hello-cpp-server/CMakeLists.txt create mode 100644 examples/hello-cpp-server/main.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 74d58b39a..ea3483962 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(hello-c-client) add_subdirectory(hello-cpp-client) +add_subdirectory(hello-cpp-server) diff --git a/examples/hello-c-client/CMakeLists.txt b/examples/hello-c-client/CMakeLists.txt index 5d9ce32bf..4f6e4cc1c 100644 --- a/examples/hello-c-client/CMakeLists.txt +++ b/examples/hello-c-client/CMakeLists.txt @@ -4,12 +4,12 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyHelloCClient VERSION 0.1 - DESCRIPTION "LaunchDarkly Hello C Client" + DESCRIPTION "LaunchDarkly Hello C Client-side SDK" LANGUAGES C ) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) -add_executable(hello-c main.c) -target_link_libraries(hello-c PRIVATE launchdarkly::client launchdarkly::sse launchdarkly::common Threads::Threads) +add_executable(hello-c-client main.c) +target_link_libraries(hello-c-client PRIVATE launchdarkly::client launchdarkly::sse launchdarkly::common Threads::Threads) diff --git a/examples/hello-cpp-client/CMakeLists.txt b/examples/hello-cpp-client/CMakeLists.txt index 0b96fae34..c99ab3215 100644 --- a/examples/hello-cpp-client/CMakeLists.txt +++ b/examples/hello-cpp-client/CMakeLists.txt @@ -4,12 +4,12 @@ cmake_minimum_required(VERSION 3.19) project( LaunchDarklyHelloCPPClient VERSION 0.1 - DESCRIPTION "LaunchDarkly Hello CPP Client" + DESCRIPTION "LaunchDarkly Hello CPP Client-side SDK" LANGUAGES CXX ) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) -add_executable(hello-cpp main.cpp) -target_link_libraries(hello-cpp PRIVATE launchdarkly::client Threads::Threads) +add_executable(hello-cpp-client main.cpp) +target_link_libraries(hello-cpp-client PRIVATE launchdarkly::client Threads::Threads) diff --git a/examples/hello-cpp-client/main.cpp b/examples/hello-cpp-client/main.cpp index 5e0a2c4b7..99ed3c849 100644 --- a/examples/hello-cpp-client/main.cpp +++ b/examples/hello-cpp-client/main.cpp @@ -1,8 +1,8 @@ #include #include -#include #include +#include // Set MOBILE_KEY to your LaunchDarkly mobile key. #define MOBILE_KEY "" @@ -18,7 +18,7 @@ using namespace launchdarkly; int main() { if (!strlen(MOBILE_KEY)) { printf( - "*** Please edit main.c to set MOBILE_KEY to your LaunchDarkly " + "*** Please edit main.cpp to set MOBILE_KEY to your LaunchDarkly " "mobile key first\n\n"); return 1; } diff --git a/examples/hello-cpp-server/CMakeLists.txt b/examples/hello-cpp-server/CMakeLists.txt new file mode 100644 index 000000000..3c2d37cc0 --- /dev/null +++ b/examples/hello-cpp-server/CMakeLists.txt @@ -0,0 +1,15 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyHelloCPPServer + VERSION 0.1 + DESCRIPTION "LaunchDarkly Hello CPP Server-side SDK" + LANGUAGES CXX +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(hello-cpp-server main.cpp) +target_link_libraries(hello-cpp-server PRIVATE launchdarkly::server Threads::Threads) diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp new file mode 100644 index 000000000..d11e59655 --- /dev/null +++ b/examples/hello-cpp-server/main.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include +#include + +// Set MOBILE_KEY to your LaunchDarkly mobile key. +#define MOBILE_KEY "" + +// Set FEATURE_FLAG_KEY to the feature flag key you want to evaluate. +#define FEATURE_FLAG_KEY "my-boolean-flag" + +// Set INIT_TIMEOUT_MILLISECONDS to the amount of time you will wait for +// the client to become initialized. +#define INIT_TIMEOUT_MILLISECONDS 3000 + +using namespace launchdarkly; +int main() { + if (!strlen(MOBILE_KEY)) { + printf( + "*** Please edit main.cpp to set MOBILE_KEY to your LaunchDarkly " + "mobile key first\n\n"); + return 1; + } + + auto config = server_side::ConfigBuilder(MOBILE_KEY).Build(); + if (!config) { + std::cout << "error: config is invalid: " << config.error() << '\n'; + return 1; + } + + auto client = server_side::Client(std::move(*config)); + + auto start_result = client.StartAsync(); + auto status = start_result.wait_for( + std::chrono::milliseconds(INIT_TIMEOUT_MILLISECONDS)); + if (status == std::future_status::ready) { + if (start_result.get()) { + std::cout << "*** SDK successfully initialized!\n\n"; + } else { + std::cout << "*** SDK failed to initialize\n"; + return 1; + } + } else { + std::cout << "*** SDK initialization didn't complete in " + << INIT_TIMEOUT_MILLISECONDS << "ms\n"; + return 1; + } + + auto context = + ContextBuilder().Kind("user", "example-user-key").Name("Sandy").Build(); + + bool flag_value = client.BoolVariation(context, FEATURE_FLAG_KEY, false); + + std::cout << "*** Feature flag '" << FEATURE_FLAG_KEY << "' is " + << (flag_value ? "true" : "false") << " for this user\n\n"; + + return 0; +} From 9b383611dfa6f31fef0dbfeb7acc89fa3a03f07d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 23 Aug 2023 14:38:21 -0700 Subject: [PATCH 38/56] feat: server-side contract tests (#197) Introduces a contract test server for the server-side SDK, which runs in CI with a suppression list. Additionally, implements the AllFlagsState method on the server-side Client object. --- .github/workflows/client.yml | 6 +- .github/workflows/server.yml | 21 +- contract-tests/CMakeLists.txt | 4 +- .../CMakeLists.txt | 12 +- .../README.md | 0 .../include/client_entity.hpp | 2 +- .../include/entity_manager.hpp | 2 +- .../include/server.hpp | 0 .../include/session.hpp | 0 .../src/client_entity.cpp | 0 .../src/entity_manager.cpp | 0 .../src/main.cpp | 2 +- .../src/server.cpp | 0 .../src/session.cpp | 6 +- .../test-suppressions.txt | 0 contract-tests/data-model/CMakeLists.txt | 20 + .../include/data_model/data_model.hpp} | 6 + contract-tests/data-model/src/data_model.cpp | 6 + .../sdk-contract-tests/src/definitions.cpp | 6 - .../server-contract-tests/CMakeLists.txt | 30 ++ .../server-contract-tests/README.md | 35 ++ .../include/client_entity.hpp | 37 ++ .../include/entity_manager.hpp | 54 +++ .../server-contract-tests/include/server.hpp | 50 +++ .../server-contract-tests/include/session.hpp | 95 +++++ .../src/client_entity.cpp | 393 ++++++++++++++++++ .../src/entity_manager.cpp | 152 +++++++ .../server-contract-tests/src/main.cpp | 66 +++ .../server-contract-tests/src/server.cpp | 34 ++ .../server-contract-tests/src/session.cpp | 146 +++++++ .../test-suppressions.txt | 174 ++++++++ libs/client-sdk/src/client_impl.cpp | 1 - .../launchdarkly/data/evaluation_detail.hpp | 8 + libs/common/src/data/evaluation_detail.cpp | 5 + .../include/launchdarkly/data_model/flag.hpp | 16 +- .../events/data/common_events.hpp | 7 +- .../launchdarkly/events/data/events.hpp | 6 +- .../src/events/asio_event_processor.cpp | 18 +- libs/internal/src/events/common_events.cpp | 1 - libs/internal/src/serialization/json_flag.cpp | 5 +- .../src/serialization/json_segment.cpp | 2 +- .../tests/data_model_serialization_test.cpp | 14 +- .../server_side/all_flags_state.hpp | 172 ++++++++ .../launchdarkly/server_side/client.hpp | 14 +- .../serialization/json_all_flags_state.hpp | 16 + libs/server-sdk/src/CMakeLists.txt | 3 + .../src/all_flags_state/all_flags_state.cpp | 86 ++++ .../all_flags_state_builder.cpp | 71 ++++ .../all_flags_state_builder.hpp | 46 ++ .../all_flags_state/json_all_flags_state.cpp | 56 +++ libs/server-sdk/src/client.cpp | 24 +- libs/server-sdk/src/client_impl.cpp | 153 ++++++- libs/server-sdk/src/client_impl.hpp | 9 +- .../data_source_event_handler.cpp | 4 +- .../src/data_store/dependency_tracker.hpp | 1 + .../persistent/expiration_tracker.hpp | 1 + libs/server-sdk/src/evaluation/evaluator.cpp | 2 +- .../server-sdk/tests/all_flags_state_test.cpp | 150 +++++++ libs/server-sdk/tests/client_test.cpp | 9 + libs/server-sdk/tests/test_store.hpp | 12 + 60 files changed, 2191 insertions(+), 80 deletions(-) rename contract-tests/{sdk-contract-tests => client-contract-tests}/CMakeLists.txt (62%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/README.md (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/client_entity.hpp (96%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/entity_manager.hpp (97%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/server.hpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/include/session.hpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/client_entity.cpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/entity_manager.cpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/main.cpp (96%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/server.cpp (100%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/src/session.cpp (97%) rename contract-tests/{sdk-contract-tests => client-contract-tests}/test-suppressions.txt (100%) create mode 100644 contract-tests/data-model/CMakeLists.txt rename contract-tests/{sdk-contract-tests/include/definitions.hpp => data-model/include/data_model/data_model.hpp} (97%) create mode 100644 contract-tests/data-model/src/data_model.cpp delete mode 100644 contract-tests/sdk-contract-tests/src/definitions.cpp create mode 100644 contract-tests/server-contract-tests/CMakeLists.txt create mode 100644 contract-tests/server-contract-tests/README.md create mode 100644 contract-tests/server-contract-tests/include/client_entity.hpp create mode 100644 contract-tests/server-contract-tests/include/entity_manager.hpp create mode 100644 contract-tests/server-contract-tests/include/server.hpp create mode 100644 contract-tests/server-contract-tests/include/session.hpp create mode 100644 contract-tests/server-contract-tests/src/client_entity.cpp create mode 100644 contract-tests/server-contract-tests/src/entity_manager.cpp create mode 100644 contract-tests/server-contract-tests/src/main.cpp create mode 100644 contract-tests/server-contract-tests/src/server.cpp create mode 100644 contract-tests/server-contract-tests/src/session.cpp create mode 100644 contract-tests/server-contract-tests/test-suppressions.txt create mode 100644 libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp create mode 100644 libs/server-sdk/src/all_flags_state/all_flags_state.cpp create mode 100644 libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp create mode 100644 libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp create mode 100644 libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp create mode 100644 libs/server-sdk/tests/all_flags_state_test.cpp diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 1b116027c..173969727 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -16,12 +16,12 @@ jobs: env: # Port the test service (implemented in this repo) should bind to. TEST_SERVICE_PORT: 8123 - TEST_SERVICE_BINARY: ./build/contract-tests/sdk-contract-tests/sdk-tests + TEST_SERVICE_BINARY: ./build/contract-tests/client-contract-tests/client-tests steps: - uses: actions/checkout@v3 - uses: ./.github/actions/ci with: - cmake_target: sdk-tests + cmake_target: client-tests run_tests: false - name: 'Launch test service as background task' run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & @@ -29,7 +29,7 @@ jobs: with: # Inform the test harness of test service's port. test_service_port: ${{ env.TEST_SERVICE_PORT }} - extra_params: '-skip-from ./contract-tests/sdk-contract-tests/test-suppressions.txt' + extra_params: '-skip-from ./contract-tests/client-contract-tests/test-suppressions.txt' build-test-client: runs-on: ubuntu-22.04 steps: diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 175ed5db3..17cf8b19e 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -6,11 +6,30 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main, server-side ] + branches: [ main, server-side, cw/sc-206687/contract-tests ] paths-ignore: - '**.md' jobs: + contract-tests: + runs-on: ubuntu-22.04 + env: + # Port the test service (implemented in this repo) should bind to. + TEST_SERVICE_PORT: 8123 + TEST_SERVICE_BINARY: ./build/contract-tests/server-contract-tests/server-tests + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + with: + cmake_target: server-tests + run_tests: false + - name: 'Launch test service as background task' + run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & + - uses: ./.github/actions/contract-tests + with: + # Inform the test harness of test service's port. + test_service_port: ${{ env.TEST_SERVICE_PORT }} + extra_params: '-skip-from ./contract-tests/server-contract-tests/test-suppressions.txt' build-test-server: runs-on: ubuntu-22.04 steps: diff --git a/contract-tests/CMakeLists.txt b/contract-tests/CMakeLists.txt index cf6a9dd44..4da668ade 100644 --- a/contract-tests/CMakeLists.txt +++ b/contract-tests/CMakeLists.txt @@ -1,2 +1,4 @@ +add_subdirectory(data-model) add_subdirectory(sse-contract-tests) -add_subdirectory(sdk-contract-tests) +add_subdirectory(client-contract-tests) +add_subdirectory(server-contract-tests) diff --git a/contract-tests/sdk-contract-tests/CMakeLists.txt b/contract-tests/client-contract-tests/CMakeLists.txt similarity index 62% rename from contract-tests/sdk-contract-tests/CMakeLists.txt rename to contract-tests/client-contract-tests/CMakeLists.txt index 9e1b0af2a..a77e15c6c 100644 --- a/contract-tests/sdk-contract-tests/CMakeLists.txt +++ b/contract-tests/client-contract-tests/CMakeLists.txt @@ -2,29 +2,29 @@ cmake_minimum_required(VERSION 3.19) project( - LaunchDarklyCPPSDKTestHarness + LaunchDarklyCPPClientSDKTestHarness VERSION 0.1 - DESCRIPTION "LaunchDarkly CPP SDK Test Harness" + DESCRIPTION "LaunchDarkly CPP Client-side SDK Test Harness" LANGUAGES CXX ) include(${CMAKE_FILES}/json.cmake) -add_executable(sdk-tests +add_executable(client-tests src/main.cpp src/server.cpp src/session.cpp - src/definitions.cpp src/entity_manager.cpp src/client_entity.cpp ) -target_link_libraries(sdk-tests PRIVATE +target_link_libraries(client-tests PRIVATE launchdarkly::client launchdarkly::internal foxy nlohmann_json::nlohmann_json Boost::coroutine + contract-test-data-model ) -target_include_directories(sdk-tests PUBLIC include) +target_include_directories(client-tests PUBLIC include) diff --git a/contract-tests/sdk-contract-tests/README.md b/contract-tests/client-contract-tests/README.md similarity index 100% rename from contract-tests/sdk-contract-tests/README.md rename to contract-tests/client-contract-tests/README.md diff --git a/contract-tests/sdk-contract-tests/include/client_entity.hpp b/contract-tests/client-contract-tests/include/client_entity.hpp similarity index 96% rename from contract-tests/sdk-contract-tests/include/client_entity.hpp rename to contract-tests/client-contract-tests/include/client_entity.hpp index 66a60d9cd..2fc40a3fd 100644 --- a/contract-tests/sdk-contract-tests/include/client_entity.hpp +++ b/contract-tests/client-contract-tests/include/client_entity.hpp @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include "definitions.hpp" class ClientEntity { public: diff --git a/contract-tests/sdk-contract-tests/include/entity_manager.hpp b/contract-tests/client-contract-tests/include/entity_manager.hpp similarity index 97% rename from contract-tests/sdk-contract-tests/include/entity_manager.hpp rename to contract-tests/client-contract-tests/include/entity_manager.hpp index e8a5802ad..92c165d62 100644 --- a/contract-tests/sdk-contract-tests/include/entity_manager.hpp +++ b/contract-tests/client-contract-tests/include/entity_manager.hpp @@ -5,8 +5,8 @@ #include #include +#include #include "client_entity.hpp" -#include "definitions.hpp" #include #include diff --git a/contract-tests/sdk-contract-tests/include/server.hpp b/contract-tests/client-contract-tests/include/server.hpp similarity index 100% rename from contract-tests/sdk-contract-tests/include/server.hpp rename to contract-tests/client-contract-tests/include/server.hpp diff --git a/contract-tests/sdk-contract-tests/include/session.hpp b/contract-tests/client-contract-tests/include/session.hpp similarity index 100% rename from contract-tests/sdk-contract-tests/include/session.hpp rename to contract-tests/client-contract-tests/include/session.hpp diff --git a/contract-tests/sdk-contract-tests/src/client_entity.cpp b/contract-tests/client-contract-tests/src/client_entity.cpp similarity index 100% rename from contract-tests/sdk-contract-tests/src/client_entity.cpp rename to contract-tests/client-contract-tests/src/client_entity.cpp diff --git a/contract-tests/sdk-contract-tests/src/entity_manager.cpp b/contract-tests/client-contract-tests/src/entity_manager.cpp similarity index 100% rename from contract-tests/sdk-contract-tests/src/entity_manager.cpp rename to contract-tests/client-contract-tests/src/entity_manager.cpp diff --git a/contract-tests/sdk-contract-tests/src/main.cpp b/contract-tests/client-contract-tests/src/main.cpp similarity index 96% rename from contract-tests/sdk-contract-tests/src/main.cpp rename to contract-tests/client-contract-tests/src/main.cpp index c886b34b8..78d2b3afe 100644 --- a/contract-tests/sdk-contract-tests/src/main.cpp +++ b/contract-tests/client-contract-tests/src/main.cpp @@ -19,7 +19,7 @@ using launchdarkly::LogLevel; int main(int argc, char* argv[]) { launchdarkly::Logger logger{ - std::make_unique("sdk-contract-tests")}; + std::make_unique("client-contract-tests")}; const std::string default_port = "8123"; std::string port = default_port; diff --git a/contract-tests/sdk-contract-tests/src/server.cpp b/contract-tests/client-contract-tests/src/server.cpp similarity index 100% rename from contract-tests/sdk-contract-tests/src/server.cpp rename to contract-tests/client-contract-tests/src/server.cpp diff --git a/contract-tests/sdk-contract-tests/src/session.cpp b/contract-tests/client-contract-tests/src/session.cpp similarity index 97% rename from contract-tests/sdk-contract-tests/src/session.cpp rename to contract-tests/client-contract-tests/src/session.cpp index 8de83db1a..5213ab6f9 100644 --- a/contract-tests/sdk-contract-tests/src/session.cpp +++ b/contract-tests/client-contract-tests/src/session.cpp @@ -1,6 +1,10 @@ #include "session.hpp" + +#include + #include #include + #include const std::string kEntityPath = "/entity/"; @@ -81,7 +85,7 @@ std::optional Session::generate_response(Request& req) { }; if (req.method() == http::verb::get && req.target() == "/") { - return capabilities_response(caps_, "c-client-sdk", "0.0.0"); + return capabilities_response(caps_, "cpp-client-sdk", launchdarkly::client_side::Client::Version()); } if (req.method() == http::verb::head && req.target() == "/") { diff --git a/contract-tests/sdk-contract-tests/test-suppressions.txt b/contract-tests/client-contract-tests/test-suppressions.txt similarity index 100% rename from contract-tests/sdk-contract-tests/test-suppressions.txt rename to contract-tests/client-contract-tests/test-suppressions.txt diff --git a/contract-tests/data-model/CMakeLists.txt b/contract-tests/data-model/CMakeLists.txt new file mode 100644 index 000000000..af39cf433 --- /dev/null +++ b/contract-tests/data-model/CMakeLists.txt @@ -0,0 +1,20 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCPPSDKTestHarnessDataModel + VERSION 0.1 + DESCRIPTION "LaunchDarkly CPP SDK Test Harness Data Model definitions" + LANGUAGES CXX +) + +include(${CMAKE_FILES}/json.cmake) + + +add_library(contract-test-data-model src/data_model.cpp) +target_link_libraries(contract-test-data-model PUBLIC nlohmann_json::nlohmann_json + ) +target_include_directories(contract-test-data-model PUBLIC + $ + $ + ) diff --git a/contract-tests/sdk-contract-tests/include/definitions.hpp b/contract-tests/data-model/include/data_model/data_model.hpp similarity index 97% rename from contract-tests/sdk-contract-tests/include/definitions.hpp rename to contract-tests/data-model/include/data_model/data_model.hpp index 7c08d367c..b4baccb9b 100644 --- a/contract-tests/sdk-contract-tests/include/definitions.hpp +++ b/contract-tests/data-model/include/data_model/data_model.hpp @@ -188,6 +188,7 @@ NLOHMANN_JSON_SERIALIZE_ENUM(ValueType, struct EvaluateFlagParams { std::string flagKey; + std::optional context; ValueType valueType; nlohmann::json defaultValue; bool detail; @@ -195,6 +196,7 @@ struct EvaluateFlagParams { }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateFlagParams, flagKey, + context, valueType, defaultValue, detail); @@ -210,11 +212,13 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateFlagResponse, reason); struct EvaluateAllFlagParams { + std::optional context; std::optional withReasons; std::optional clientSideOnly; std::optional detailsOnlyForTrackedFlags; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateAllFlagParams, + context, withReasons, clientSideOnly, detailsOnlyForTrackedFlags); @@ -226,12 +230,14 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(EvaluateAllFlagsResponse, struct CustomEventParams { std::string eventKey; + std::optional context; std::optional data; std::optional omitNullData; std::optional metricValue; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(CustomEventParams, eventKey, + context, data, omitNullData, metricValue); diff --git a/contract-tests/data-model/src/data_model.cpp b/contract-tests/data-model/src/data_model.cpp new file mode 100644 index 000000000..3172a7c4b --- /dev/null +++ b/contract-tests/data-model/src/data_model.cpp @@ -0,0 +1,6 @@ +#include "data_model/data_model.hpp" + +EvaluateFlagParams::EvaluateFlagParams() + : valueType{ValueType::Unspecified}, detail{false}, context{std::nullopt} {} + +CommandParams::CommandParams() : command{Command::Unknown} {} diff --git a/contract-tests/sdk-contract-tests/src/definitions.cpp b/contract-tests/sdk-contract-tests/src/definitions.cpp deleted file mode 100644 index aba257b39..000000000 --- a/contract-tests/sdk-contract-tests/src/definitions.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "definitions.hpp" - -EvaluateFlagParams::EvaluateFlagParams() - : valueType{ValueType::Unspecified}, detail{false} {} - -CommandParams::CommandParams() : command{Command::Unknown} {} diff --git a/contract-tests/server-contract-tests/CMakeLists.txt b/contract-tests/server-contract-tests/CMakeLists.txt new file mode 100644 index 000000000..3bd94cb96 --- /dev/null +++ b/contract-tests/server-contract-tests/CMakeLists.txt @@ -0,0 +1,30 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCPPServerSDKTestHarness + VERSION 0.1 + DESCRIPTION "LaunchDarkly CPP Server-side SDK Test Harness" + LANGUAGES CXX +) + +include(${CMAKE_FILES}/json.cmake) + +add_executable(server-tests + src/main.cpp + src/server.cpp + src/session.cpp + src/entity_manager.cpp + src/client_entity.cpp + ) + +target_link_libraries(server-tests PRIVATE + launchdarkly::server + launchdarkly::internal + foxy + nlohmann_json::nlohmann_json + Boost::coroutine + contract-test-data-model + ) + +target_include_directories(server-tests PUBLIC include) diff --git a/contract-tests/server-contract-tests/README.md b/contract-tests/server-contract-tests/README.md new file mode 100644 index 000000000..d59cbd039 --- /dev/null +++ b/contract-tests/server-contract-tests/README.md @@ -0,0 +1,35 @@ +## SDK contract tests + +Contract tests have a "test service" on one side, and the "test harness" on +the other. + +This project implements the test service for the C++ Server-side SDK. + +**session (session.hpp)** + +This provides a simple REST API for creating/destroying +test entities. Examples: + +`GET /` - returns the capabilities of this service. + +`DELETE /` - shutdown the service. + +`POST /` - create a new test entity, and return its ID. + +`DELETE /entity/1` - delete the an entity identified by `1`. + +**entity manager (entity_manager.hpp)** + +This manages "entities", which are unique instances of the SDK client. + +**definitions (definitions.hpp)** + +Contains JSON definitions that are used to communicate with the test harness. + +**server (server.hpp)** + +Glues everything together, mainly providing the TCP acceptor that spawns new sessions. + +**session (session.hpp)** + +Prepares HTTP responses based on the results of commands sent to entities. diff --git a/contract-tests/server-contract-tests/include/client_entity.hpp b/contract-tests/server-contract-tests/include/client_entity.hpp new file mode 100644 index 000000000..39b4a3be3 --- /dev/null +++ b/contract-tests/server-contract-tests/include/client_entity.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include + +class ClientEntity { + public: + explicit ClientEntity( + std::unique_ptr client); + + tl::expected Command(CommandParams params); + + private: + tl::expected Evaluate( + EvaluateFlagParams const&); + + tl::expected EvaluateDetail( + EvaluateFlagParams const&, + launchdarkly::Context const&); + + tl::expected EvaluateAll( + EvaluateAllFlagParams const&); + + tl::expected Identify( + IdentifyEventParams const&); + + tl::expected Custom(CustomEventParams const&); + + std::unique_ptr client_; +}; + +static tl::expected ContextConvert( + ContextConvertParams const&); + +static tl::expected ContextBuild( + ContextBuildParams const&); diff --git a/contract-tests/server-contract-tests/include/entity_manager.hpp b/contract-tests/server-contract-tests/include/entity_manager.hpp new file mode 100644 index 000000000..b99633c32 --- /dev/null +++ b/contract-tests/server-contract-tests/include/entity_manager.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "client_entity.hpp" + +#include + +#include +#include + +#include + +#include +#include +#include +#include +#include + +class EventOutbox; + +class EntityManager { + std::unordered_map entities_; + + std::size_t counter_; + boost::asio::any_io_executor executor_; + + launchdarkly::Logger& logger_; + + public: + /** + * Create an entity manager, which can be used to create and destroy + * entities (SSE clients + event channel back to test harness). + * @param executor Executor. + * @param logger Logger. + */ + EntityManager(boost::asio::any_io_executor executor, + launchdarkly::Logger& logger); + /** + * Create an entity with the given configuration. + * @param params Config of the entity. + * @return An ID representing the entity, or none if the entity couldn't + * be created. + */ + std::optional create(ConfigParams const& params); + /** + * Destroy an entity with the given ID. + * @param id ID of the entity. + * @return True if the entity was found and destroyed. + */ + bool destroy(std::string const& id); + + tl::expected command( + std::string const& id, + CommandParams const& params); +}; diff --git a/contract-tests/server-contract-tests/include/server.hpp b/contract-tests/server-contract-tests/include/server.hpp new file mode 100644 index 000000000..655321f04 --- /dev/null +++ b/contract-tests/server-contract-tests/include/server.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include "entity_manager.hpp" + +#include + +#include +#include +#include +#include + +#include + +#include + +namespace net = boost::asio; // from + +using tcp = boost::asio::ip::tcp; // from + +class server { + EntityManager manager_; + launchdarkly::foxy::listener listener_; + std::vector caps_; + launchdarkly::Logger& logger_; + + public: + /** + * Constructs a server, which stands up a REST API at the given + * port and address. The server is ready to accept connections upon + * construction. + * @param ioc IO context. + * @param address Address to bind. + * @param port Port to bind. + * @param logger Logger. + */ + server(net::io_context& ioc, + std::string const& address, + unsigned short port, + launchdarkly::Logger& logger); + /** + * Advertise an optional test-harness capability, such as "comments". + * @param cap + */ + void add_capability(std::string cap); + + /** + * Shuts down the server. + */ + void shutdown(); +}; diff --git a/contract-tests/server-contract-tests/include/session.hpp b/contract-tests/server-contract-tests/include/session.hpp new file mode 100644 index 000000000..029a559e0 --- /dev/null +++ b/contract-tests/server-contract-tests/include/session.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include "entity_manager.hpp" + +#include +#include +#include +#include + +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +class Session : boost::asio::coroutine { + public: + using Request = http::request; + using Response = http::response; + + struct Frame { + Request request_; + Response resp_; + }; + + /** + * Constructs a session, which provides a REST API. + * @param session The HTTP session. + * @param manager Manager through which entities can be created/destroyed. + * @param caps Test service capabilities to advertise. + * @param logger Logger. + */ + Session(launchdarkly::foxy::server_session& session, + EntityManager& manager, + std::vector& caps, + launchdarkly::Logger& logger); + + template + auto operator()(Self& self, + boost::system::error_code ec = {}, + std::size_t const bytes_transferred = 0) -> void { + using launchdarkly::LogLevel; + auto& f = *frame_; + + reenter(*this) { + while (true) { + f.resp_ = {}; + f.request_ = {}; + + yield session_.async_read(f.request_, std::move(self)); + if (ec) { + LD_LOG(logger_, LogLevel::kWarn) + << "session: read: " << ec.what(); + break; + } + + if (auto response = generate_response(f.request_)) { + f.resp_ = *response; + } else { + LD_LOG(logger_, LogLevel::kWarn) + << "session: shutdown requested by client"; + std::exit(0); + } + + yield session_.async_write(f.resp_, std::move(self)); + + if (ec) { + LD_LOG(logger_, LogLevel::kWarn) + << "session: write: " << ec.what(); + break; + } + + if (!f.request_.keep_alive()) { + break; + } + } + + return self.complete({}, 0); + } + } + + std::optional generate_response(Request& req); + + private: + launchdarkly::foxy::server_session& session_; + EntityManager& manager_; + std::unique_ptr frame_; + std::vector& caps_; + launchdarkly::Logger& logger_; +}; + +#include diff --git a/contract-tests/server-contract-tests/src/client_entity.cpp b/contract-tests/server-contract-tests/src/client_entity.cpp new file mode 100644 index 000000000..c8fdb40ca --- /dev/null +++ b/contract-tests/server-contract-tests/src/client_entity.cpp @@ -0,0 +1,393 @@ +#include "client_entity.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace launchdarkly::server_side; + +tl::expected ParseContext( + nlohmann::json value) { + boost::system::error_code ec; + auto boost_json_val = boost::json::parse(value.dump(), ec); + if (ec) { + return tl::make_unexpected(ec.what()); + } + + auto maybe_ctx = boost::json::value_to< + tl::expected>( + boost_json_val); + if (!maybe_ctx) { + return tl::make_unexpected( + launchdarkly::ErrorToString(maybe_ctx.error())); + } + + if (!maybe_ctx->Valid()) { + return tl::make_unexpected(maybe_ctx->errors()); + } + + return *maybe_ctx; +} + +ClientEntity::ClientEntity( + std::unique_ptr client) + : client_(std::move(client)) {} + +tl::expected ClientEntity::Identify( + IdentifyEventParams const& params) { + boost::system::error_code ec; + + auto maybe_ctx = ParseContext(params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + client_->Identify(*maybe_ctx); + return nlohmann::json{}; +} + +static void BuildContextFromParams(launchdarkly::ContextBuilder& builder, + ContextSingleParams const& single) { + auto& attrs = builder.Kind(single.kind.value_or("user"), single.key); + if (single.anonymous) { + attrs.Anonymous(*single.anonymous); + } + if (single.name) { + attrs.Name(*single.name); + } + + if (single._private) { + attrs.AddPrivateAttributes(*single._private); + } + + if (single.custom) { + for (auto const& [key, value] : *single.custom) { + auto maybe_attr = boost::json::value_to< + tl::expected>( + boost::json::parse(value.dump())); + if (maybe_attr) { + attrs.Set(key, *maybe_attr); + } + } + } +} + +tl::expected ContextBuild( + ContextBuildParams const& params) { + ContextResponse resp{}; + + auto builder = launchdarkly::ContextBuilder(); + + if (params.multi) { + for (auto const& single : *params.multi) { + BuildContextFromParams(builder, single); + } + } else { + BuildContextFromParams(builder, *params.single); + } + + auto ctx = builder.Build(); + if (!ctx.Valid()) { + resp.error = ctx.errors(); + return resp; + } + + resp.output = boost::json::serialize(boost::json::value_from(ctx)); + return resp; +} + +tl::expected ContextConvert( + ContextConvertParams const& params) { + ContextResponse resp{}; + + boost::system::error_code ec; + auto json_value = boost::json::parse(params.input, ec); + if (ec) { + resp.error = ec.what(); + return resp; + } + + auto maybe_ctx = boost::json::value_to< + tl::expected>( + json_value); + + if (!maybe_ctx) { + resp.error = launchdarkly::ErrorToString(maybe_ctx.error()); + return resp; + } + + if (!maybe_ctx->Valid()) { + resp.error = maybe_ctx->errors(); + return resp; + } + + resp.output = boost::json::serialize(boost::json::value_from(*maybe_ctx)); + return resp; +} + +tl::expected ClientEntity::Custom( + CustomEventParams const& params) { + auto data = + params.data + ? boost::json::value_to< + tl::expected>( + boost::json::parse(params.data->dump())) + : launchdarkly::Value::Null(); + + if (!data) { + return tl::make_unexpected("couldn't parse custom event data"); + } + + if (!params.context) { + return tl::make_unexpected("context is required"); + } + + auto maybe_ctx = ParseContext(*params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + + if (params.omitNullData.value_or(false) && !params.metricValue && + !params.data) { + client_->Track(*maybe_ctx, params.eventKey); + return nlohmann::json{}; + } + + if (!params.metricValue) { + client_->Track(*maybe_ctx, params.eventKey, std::move(*data)); + return nlohmann::json{}; + } + + client_->Track(*maybe_ctx, params.eventKey, std::move(*data), + *params.metricValue); + return nlohmann::json{}; +} + +tl::expected ClientEntity::EvaluateAll( + EvaluateAllFlagParams const& params) { + EvaluateAllFlagsResponse resp{}; + + boost::ignore_unused(params); + + if (!params.context) { + return tl::make_unexpected("context is required"); + } + + auto maybe_ctx = ParseContext(*params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + + AllFlagsState::Options options = AllFlagsState::Options::Default; + if (params.withReasons.value_or(false)) { + options |= AllFlagsState::Options::IncludeReasons; + } + if (params.clientSideOnly.value_or(false)) { + options |= AllFlagsState::Options::ClientSideOnly; + } + if (params.detailsOnlyForTrackedFlags.value_or(false)) { + options |= AllFlagsState::Options::DetailsOnlyForTrackedFlags; + } + + auto state = client_->AllFlagsState(*maybe_ctx, options); + + resp.state = nlohmann::json::parse( + boost::json::serialize(boost::json::value_from(state))); + + return resp; +} + +tl::expected ClientEntity::EvaluateDetail( + EvaluateFlagParams const& params, + launchdarkly::Context const& ctx) { + auto const& key = params.flagKey; + + auto const& defaultVal = params.defaultValue; + + EvaluateFlagResponse result; + + std::optional reason; + + switch (params.valueType) { + case ValueType::Bool: { + auto detail = + client_->BoolVariationDetail(ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::Int: { + auto detail = + client_->IntVariationDetail(ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::Double: { + auto detail = client_->DoubleVariationDetail( + ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::String: { + auto detail = client_->StringVariationDetail( + ctx, key, defaultVal.get()); + result.value = *detail; + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + case ValueType::Any: + case ValueType::Unspecified: { + auto maybe_fallback = boost::json::value_to< + tl::expected>( + boost::json::parse(defaultVal.dump())); + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } + + /* This switcharoo from nlohmann/json to boost/json to Value, then + * back is because we're using nlohmann/json for the test harness + * protocol, but boost::json in the SDK. We could swap over to + * boost::json entirely here to remove the awkwardness. */ + + auto detail = + client_->JsonVariationDetail(ctx, key, *maybe_fallback); + + auto serialized = + boost::json::serialize(boost::json::value_from(*detail)); + + result.value = nlohmann::json::parse(serialized); + reason = detail.Reason(); + result.variationIndex = detail.VariationIndex(); + break; + } + default: + return tl::make_unexpected("unknown variation type"); + } + + result.reason = + reason.has_value() + ? std::make_optional(nlohmann::json::parse( + boost::json::serialize(boost::json::value_from(*reason)))) + : std::nullopt; + + return result; +} +tl::expected ClientEntity::Evaluate( + EvaluateFlagParams const& params) { + auto maybe_ctx = ParseContext(params.context); + if (!maybe_ctx) { + return tl::make_unexpected(maybe_ctx.error()); + } + + if (params.detail) { + return EvaluateDetail(params, *maybe_ctx); + } + + auto const& key = params.flagKey; + + auto const& defaultVal = params.defaultValue; + + EvaluateFlagResponse result; + + switch (params.valueType) { + case ValueType::Bool: + result.value = + client_->BoolVariation(*maybe_ctx, key, defaultVal.get()); + break; + case ValueType::Int: + result.value = + client_->IntVariation(*maybe_ctx, key, defaultVal.get()); + break; + case ValueType::Double: + result.value = client_->DoubleVariation(*maybe_ctx, key, + defaultVal.get()); + break; + case ValueType::String: { + result.value = client_->StringVariation( + *maybe_ctx, key, defaultVal.get()); + break; + } + case ValueType::Any: + case ValueType::Unspecified: { + auto maybe_fallback = boost::json::value_to< + tl::expected>( + boost::json::parse(defaultVal.dump())); + if (!maybe_fallback) { + return tl::make_unexpected("unable to parse fallback value"); + } + /* This switcharoo from nlohmann/json to boost/json to Value, then + * back is because we're using nlohmann/json for the test harness + * protocol, but boost::json in the SDK. We could swap over to + * boost::json entirely here to remove the awkwardness. */ + + auto evaluation = + client_->JsonVariation(*maybe_ctx, key, *maybe_fallback); + + auto serialized = + boost::json::serialize(boost::json::value_from(evaluation)); + + result.value = nlohmann::json::parse(serialized); + break; + } + default: + return tl::make_unexpected("unknown variation type"); + } + + return result; +} +tl::expected ClientEntity::Command( + CommandParams params) { + switch (params.command) { + case Command::Unknown: + return tl::make_unexpected("unknown command"); + case Command::EvaluateFlag: + if (!params.evaluate) { + return tl::make_unexpected("evaluate params must be set"); + } + return Evaluate(*params.evaluate); + case Command::EvaluateAllFlags: + if (!params.evaluateAll) { + return tl::make_unexpected("evaluateAll params must be set"); + } + return EvaluateAll(*params.evaluateAll); + case Command::IdentifyEvent: + if (!params.identifyEvent) { + return tl::make_unexpected("identifyEvent params must be set"); + } + return Identify(*params.identifyEvent); + case Command::CustomEvent: + if (!params.customEvent) { + return tl::make_unexpected("customEvent params must be set"); + } + return Custom(*params.customEvent); + case Command::FlushEvents: + client_->FlushAsync(); + return nlohmann::json{}; + case Command::ContextBuild: + if (!params.contextBuild) { + return tl::make_unexpected("contextBuild params must be set"); + } + return ContextBuild(*params.contextBuild); + case Command::ContextConvert: + if (!params.contextConvert) { + return tl::make_unexpected("contextConvert params must be set"); + } + return ContextConvert(*params.contextConvert); + } + return tl::make_unexpected("unrecognized command"); +} diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp new file mode 100644 index 000000000..c5578d540 --- /dev/null +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -0,0 +1,152 @@ +#include "entity_manager.hpp" +#include + +#include +#include +#include + +using launchdarkly::LogLevel; +using namespace launchdarkly::server_side; + +EntityManager::EntityManager(boost::asio::any_io_executor executor, + launchdarkly::Logger& logger) + : + counter_{0}, + executor_{std::move(executor)}, + logger_{logger} {} + + + +std::optional EntityManager::create(ConfigParams const& in) { + std::string id = std::to_string(counter_++); + + auto config_builder = ConfigBuilder(in.credential); + + auto default_endpoints = + launchdarkly::server_side::Defaults::ServiceEndpoints(); + + auto& endpoints = + config_builder.ServiceEndpoints() + .EventsBaseUrl(default_endpoints.EventsBaseUrl()) + .PollingBaseUrl(default_endpoints.PollingBaseUrl()) + .StreamingBaseUrl(default_endpoints.StreamingBaseUrl()); + + if (in.serviceEndpoints) { + if (in.serviceEndpoints->streaming) { + endpoints.StreamingBaseUrl(*in.serviceEndpoints->streaming); + } + if (in.serviceEndpoints->polling) { + endpoints.PollingBaseUrl(*in.serviceEndpoints->polling); + } + if (in.serviceEndpoints->events) { + endpoints.EventsBaseUrl(*in.serviceEndpoints->events); + } + } + + if (in.streaming) { + if (in.streaming->baseUri) { + endpoints.StreamingBaseUrl(*in.streaming->baseUri); + } + } + + auto& datasource = config_builder.DataSource(); + + if (in.polling) { + if (in.polling->baseUri) { + endpoints.PollingBaseUrl(*in.polling->baseUri); + } + if (!in.streaming) { + auto method = DataSourceBuilder::Polling(); + if (in.polling->pollIntervalMs) { + method.PollInterval( + std::chrono::duration_cast( + std::chrono::milliseconds( + *in.polling->pollIntervalMs))); + } + datasource.Method(std::move(method)); + } + } + + auto& event_config = config_builder.Events(); + + if (in.events) { + ConfigEventParams const& events = *in.events; + + if (events.baseUri) { + endpoints.EventsBaseUrl(*events.baseUri); + } + + if (events.allAttributesPrivate) { + event_config.AllAttributesPrivate(*events.allAttributesPrivate); + } + + if (!events.globalPrivateAttributes.empty()) { + launchdarkly::AttributeReference::SetType attrs( + events.globalPrivateAttributes.begin(), + events.globalPrivateAttributes.end()); + event_config.PrivateAttributes(std::move(attrs)); + } + + if (events.capacity) { + event_config.Capacity(*events.capacity); + } + + if (events.flushIntervalMs) { + event_config.FlushInterval( + std::chrono::milliseconds(*events.flushIntervalMs)); + } + + } else { + event_config.Disable(); + } + + if (in.tags) { + if (in.tags->applicationId) { + config_builder.AppInfo().Identifier(*in.tags->applicationId); + } + if (in.tags->applicationVersion) { + config_builder.AppInfo().Version(*in.tags->applicationVersion); + } + } + + auto config = config_builder.Build(); + if (!config) { + LD_LOG(logger_, LogLevel::kWarn) + << "entity_manager: couldn't build config: " << config.error(); + return std::nullopt; + } + + auto client = std::make_unique(std::move(*config)); + + std::chrono::milliseconds waitForClient = std::chrono::seconds(5); + if (in.startWaitTimeMs) { + waitForClient = std::chrono::milliseconds(*in.startWaitTimeMs); + } + + auto init = client->StartAsync(); + init.wait_for(waitForClient); + + entities_.try_emplace(id, std::move(client)); + + return id; +} + +bool EntityManager::destroy(std::string const& id) { + auto it = entities_.find(id); + if (it == entities_.end()) { + return false; + } + + entities_.erase(it); + return true; +} + +tl::expected EntityManager::command( + std::string const& id, + CommandParams const& params) { + auto it = entities_.find(id); + if (it == entities_.end()) { + return tl::make_unexpected("entity not found"); + } + return it->second.Command(params); +} diff --git a/contract-tests/server-contract-tests/src/main.cpp b/contract-tests/server-contract-tests/src/main.cpp new file mode 100644 index 000000000..61245d1af --- /dev/null +++ b/contract-tests/server-contract-tests/src/main.cpp @@ -0,0 +1,66 @@ +#include "server.hpp" + +#include + +#include +#include +#include +#include +#include + +#include + +namespace net = boost::asio; +namespace beast = boost::beast; + +using launchdarkly::logging::ConsoleBackend; + +using launchdarkly::LogLevel; + +int main(int argc, char* argv[]) { + launchdarkly::Logger logger{ + std::make_unique("server-contract-tests")}; + + const std::string default_port = "8123"; + std::string port = default_port; + if (argc == 2) { + port = + argv[1]; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + } + + try { + net::io_context ioc{1}; + + auto p = boost::lexical_cast(port); + server srv(ioc, "0.0.0.0", p, logger); + + srv.add_capability("server-side"); + srv.add_capability("strongly-typed"); + srv.add_capability("context-type"); + srv.add_capability("service-endpoints"); + srv.add_capability("tags"); + srv.add_capability("server-side-polling"); + + net::signal_set signals{ioc, SIGINT, SIGTERM}; + + boost::asio::spawn(ioc.get_executor(), [&](auto yield) mutable { + signals.async_wait(yield); + LD_LOG(logger, LogLevel::kInfo) << "shutting down.."; + srv.shutdown(); + }); + + ioc.run(); + LD_LOG(logger, LogLevel::kInfo) << "bye!"; + + } catch (boost::bad_lexical_cast&) { + LD_LOG(logger, LogLevel::kError) + << "invalid port (" << port + << "), provide a number (no arguments defaults " + "to port " + << default_port << ")"; + return EXIT_FAILURE; + } catch (std::exception const& e) { + LD_LOG(logger, LogLevel::kError) << e.what(); + return EXIT_FAILURE; + } +} diff --git a/contract-tests/server-contract-tests/src/server.cpp b/contract-tests/server-contract-tests/src/server.cpp new file mode 100644 index 000000000..b7c0a8b88 --- /dev/null +++ b/contract-tests/server-contract-tests/src/server.cpp @@ -0,0 +1,34 @@ +#include "server.hpp" +#include "session.hpp" + +#include +#include +#include +#include + +using launchdarkly::LogLevel; + +server::server(net::io_context& ioc, + std::string const& address, + unsigned short port, + launchdarkly::Logger& logger) + : manager_(ioc.get_executor(), logger), + listener_{ioc.get_executor(), + tcp::endpoint(boost::asio::ip::make_address(address), port)}, + logger_{logger} { + LD_LOG(logger_, LogLevel::kInfo) + << "server: listening on " << address << ":" << port; + listener_.async_accept([this](auto& server) { + return Session(server, manager_, caps_, logger_); + }); +} + +void server::add_capability(std::string cap) { + LD_LOG(logger_, LogLevel::kDebug) + << "server: test capability: <" << cap << ">"; + caps_.push_back(std::move(cap)); +} + +void server::shutdown() { + listener_.shutdown(); +} diff --git a/contract-tests/server-contract-tests/src/session.cpp b/contract-tests/server-contract-tests/src/session.cpp new file mode 100644 index 000000000..16a643c50 --- /dev/null +++ b/contract-tests/server-contract-tests/src/session.cpp @@ -0,0 +1,146 @@ +#include "session.hpp" + +#include + +#include +#include + +#include + +const std::string kEntityPath = "/entity/"; + +namespace net = boost::asio; + +Session::Session(launchdarkly::foxy::server_session& session, + EntityManager& manager, + std::vector& caps, + launchdarkly::Logger& logger) + : session_(session), + frame_(std::make_unique()), + manager_(manager), + caps_(caps), + logger_(logger) {} + +std::optional Session::generate_response(Request& req) { + auto const bad_request = [&req](beast::string_view why) { + Response res{http::status::bad_request, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{"error", why}.dump(); + res.prepare_payload(); + return res; + }; + + auto const not_found = [&req](beast::string_view target) { + Response res{http::status::not_found, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = + "The resource '" + std::string(target) + "' was not found."; + res.prepare_payload(); + return res; + }; + + auto const server_error = [&req](beast::string_view what) { + Response res{http::status::internal_server_error, req.version()}; + res.set(http::field::server, BOOST_BEAST_VERSION_STRING); + res.set(http::field::content_type, "text/html"); + res.keep_alive(req.keep_alive()); + res.body() = "An error occurred: '" + std::string(what) + "'"; + res.prepare_payload(); + return res; + }; + + auto const capabilities_response = + [&req](std::vector const& caps, std::string const& name, + std::string const& version) { + Response res{http::status::ok, req.version()}; + res.set(http::field::content_type, "application/json"); + res.keep_alive(req.keep_alive()); + res.body() = nlohmann::json{ + {"capabilities", caps}, + {"name", name}, + {"clientVersion", + version}}.dump(); + res.prepare_payload(); + return res; + }; + + auto const create_entity_response = [&req](std::string const& id) { + Response res{http::status::ok, req.version()}; + res.keep_alive(req.keep_alive()); + res.set("Location", kEntityPath + id); + res.prepare_payload(); + return res; + }; + + auto const destroy_entity_response = [&req](bool erased) { + auto status = erased ? http::status::ok : http::status::not_found; + Response res{status, req.version()}; + res.keep_alive(req.keep_alive()); + res.prepare_payload(); + return res; + }; + + if (req.method() == http::verb::get && req.target() == "/") { + return capabilities_response(caps_, "cpp-server-sdk", launchdarkly::server_side::Client::Version()); + } + + if (req.method() == http::verb::head && req.target() == "/") { + return http::response{http::status::ok, + req.version()}; + } + + if (req.method() == http::verb::delete_ && req.target() == "/") { + return std::nullopt; + } + + if (req.method() == http::verb::post && req.target() == "/") { + try { + auto json = nlohmann::json::parse(req.body()); + auto params = json.get(); + if (auto entity_id = manager_.create(params.configuration)) { + return create_entity_response(*entity_id); + } + return server_error("couldn't create client entity"); + } catch (nlohmann::json::exception& e) { + return bad_request("unable to parse config JSON"); + } + } + + if (req.method() == http::verb::post && + req.target().starts_with(kEntityPath)) { + std::string entity_id = req.target(); + boost::erase_first(entity_id, kEntityPath); + + try { + auto json = nlohmann::json::parse(req.body()); + auto params = json.get(); + tl::expected res = + manager_.command(entity_id, params); + if (res.has_value()) { + auto response = http::response{ + http::status::ok, req.version()}; + response.body() = res->dump(); + response.prepare_payload(); + return response; + } else { + return bad_request(res.error()); + } + } catch (nlohmann::json::exception& e) { + return bad_request("unable to parse config JSON"); + } + } + + if (req.method() == http::verb::delete_ && + req.target().starts_with(kEntityPath)) { + std::string entity_id = req.target(); + boost::erase_first(entity_id, kEntityPath); + bool erased = manager_.destroy(entity_id); + return destroy_entity_response(erased); + } + + return not_found(req.target()); +} diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt new file mode 100644 index 000000000..eb6130c87 --- /dev/null +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -0,0 +1,174 @@ +evaluation/parameterized/bad attribute reference errors - clause with no attribute/test-flag/evaluate flag with detail +evaluation/parameterized/bad attribute reference errors - empty path component/test-flag/evaluate flag with detail +evaluation/parameterized/bad attribute reference errors - tilde followed by invalid escape character/test-flag/evaluate flag with detail +evaluation/parameterized/bad attribute reference errors - tilde followed by nothing/test-flag/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/off variation too low/off variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/off variation too high/off variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/target variation too low/target variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/target variation too high/target variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/rule variation too low/rule variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (bool)/rule variation too high/rule variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/off variation too low/off variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/off variation too high/off variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/target variation too low/target variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/target variation too high/target variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/rule variation too low/rule variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (int)/rule variation too high/rule variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/off variation too low/off variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/off variation too high/off variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/target variation too low/target variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/target variation too high/target variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/rule variation too low/rule variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (double)/rule variation too high/rule variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/off variation too low/off variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/off variation too high/off variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/target variation too low/target variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/target variation too high/target variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/rule variation too low/rule variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (string)/rule variation too high/rule variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/off variation too low/off variation too low/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/off variation too low/off variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/off variation too high/off variation too high/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/off variation too high/off variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/fallthrough variation too low/fallthrough variation too low/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/fallthrough variation too high/fallthrough variation too high/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/target variation too low/target variation too low/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/target variation too low/target variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/target variation too high/target variation too high/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/target variation too high/target variation too high/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/rule variation too low/rule variation too low/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/rule variation too low/rule variation too low/evaluate flag with detail +evaluation/parameterized/evaluation failures (any)/rule variation too high/rule variation too high/evaluate flag without detail +evaluation/parameterized/evaluation failures (any)/rule variation too high/rule variation too high/evaluate flag with detail +evaluation/parameterized/prerequisites/prerequisite cycle is detected at top level, recursion stops/prerequisite cycle is detected at top level, recursion stops/evaluate flag with detail +evaluation/parameterized/prerequisites/prerequisite cycle is detected at top level, recursion stops/prerequisite cycle is detected at top level, recursion stops/evaluate all flags +evaluation/parameterized/prerequisites/prerequisite cycle is detected at deeper level, recursion stops/prerequisite cycle is detected at deeper level, recursion stops/evaluate flag with detail +evaluation/parameterized/prerequisites/prerequisite cycle is detected at deeper level, recursion stops/prerequisite cycle is detected at deeper level, recursion stops/evaluate all flags +evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/fallthrough rollout/fallthrough rollout/evaluate flag without detail +evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/fallthrough rollout/fallthrough rollout/evaluate flag with detail +evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/fallthrough rollout/fallthrough rollout/evaluate all flags +evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate flag without detail +evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate flag with detail +evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate all flags +evaluation/parameterized/segment match/user matches via include in multi-kind user/user matches via include in multi-kind user/evaluate flag without detail +evaluation/parameterized/segment match/user matches via include in multi-kind user/user matches via include in multi-kind user/evaluate flag with detail +evaluation/parameterized/segment match/user matches via include in multi-kind user/user matches via include in multi-kind user/evaluate all flags +evaluation/parameterized/segment recursion/cycle is detected at top level, recursion stops/cycle is detected at top level, recursion stops/evaluate flag with detail +evaluation/parameterized/segment recursion/cycle is detected below top level, recursion stops/cycle is detected below top level, recursion stops/evaluate flag with detail +events/summary events/prerequisites +events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: bool +events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: int +events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: double +events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: string +events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: any +events/feature events/full feature event for tracked flag/without reason/single kind non-default/valid flag/type: bool +events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: bool +events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: int +events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: double +events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: string +events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: any +events/feature events/full feature event for tracked flag/without reason/multi-kind/valid flag/type: bool +events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: bool +events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: int +events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: double +events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: string +events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: any +events/feature events/full feature event for tracked flag/with reason/single kind default/valid flag/type: bool +events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: bool +events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: int +events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: double +events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: string +events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: any +events/feature events/full feature event for tracked flag/with reason/single kind non-default/valid flag/type: bool +events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: bool +events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: int +events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: double +events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: string +events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: any +events/feature events/full feature event for tracked flag/with reason/multi-kind/valid flag/type: bool +events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: bool +events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: int +events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: double +events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: string +events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: any +events/feature events/evaluating all flags generates no events +events/feature prerequisite events/without reasons +events/feature prerequisite events/with reasons +events/experimentation/experiment in rule +events/experimentation/experiment in fallthrough +streaming/updates/flag patch with same version is not applied +streaming/updates/segment patch with same version is not applied +streaming/updates/flag patch with lower version is not applied +streaming/updates/segment patch with lower version is not applied +streaming/updates/flag delete with same version is not applied +streaming/updates/segment delete with same version is not applied +streaming/updates/flag delete with lower version is not applied +streaming/updates/segment delete with lower version is not applied +streaming/updates/flag delete for previously nonexistent flag is applied +streaming/updates/segment delete for previously nonexistent segment is applied +streaming/retry behavior/retry after IO error on reconnect +streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 400 +streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 408 +streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 429 +streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 500 +streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 503 +streaming/validation/drop and reconnect if stream event has malformed JSON/put event +streaming/validation/drop and reconnect if stream event has malformed JSON/patch event +streaming/validation/drop and reconnect if stream event has malformed JSON/delete event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/put event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/patch event +streaming/validation/drop and reconnect if stream event has well-formed JSON not matching schema/delete event + +# The Server doesn't need to know how to deserialize users. +context type/convert/old user to context/{"key": ""} +context type/convert/old user to context/{"key": "a"} +context type/convert/old user to context/{"key": "a"} +context type/convert/old user to context/{"key": "a", "custom": {"b": true}} +context type/convert/old user to context/{"key": "a", "custom": {"b": 1}} +context type/convert/old user to context/{"key": "a", "custom": {"b": "c"}} +context type/convert/old user to context/{"key": "a", "custom": {"b": [1, 2]}} +context type/convert/old user to context/{"key": "a", "custom": {"b": {"c": 1}}} +context type/convert/old user to context/{"key": "a", "custom": {"b": 1, "c": 2}} +context type/convert/old user to context/{"key": "a", "custom": {"b": 1, "c": null}} +context type/convert/old user to context/{"key": "a", "custom": {}} +context type/convert/old user to context/{"key": "a", "custom": null} +context type/convert/old user to context/{"key": "a", "anonymous": true} +context type/convert/old user to context/{"key": "a", "anonymous": false} +context type/convert/old user to context/{"key": "a", "anonymous": null} +context type/convert/old user to context/{"key": "a", "privateAttributeNames": ["b"]} +context type/convert/old user to context/{"key": "a", "privateAttributeNames": []} +context type/convert/old user to context/{"key": "a", "privateAttributeNames": null} +context type/convert/old user to context/{"key": "a", "name": "b"} +context type/convert/old user to context/{"key": "a", "name": null} +context type/convert/old user to context/{"key": "a", "firstName": "b"} +context type/convert/old user to context/{"key": "a", "firstName": null} +context type/convert/old user to context/{"key": "a", "lastName": "b"} +context type/convert/old user to context/{"key": "a", "lastName": null} +context type/convert/old user to context/{"key": "a", "email": "b"} +context type/convert/old user to context/{"key": "a", "email": null} +context type/convert/old user to context/{"key": "a", "country": "b"} +context type/convert/old user to context/{"key": "a", "country": null} +context type/convert/old user to context/{"key": "a", "avatar": "b"} +context type/convert/old user to context/{"key": "a", "avatar": null} +context type/convert/old user to context/{"key": "a", "ip": "b"} +context type/convert/old user to context/{"key": "a", "ip": null} + +# These appear to only fail in CI, but not locally on Mac. Requires investigation. +evaluation/bucketing/bucket by non-key attribute/in rollouts/invalid value type +evaluation/bucketing/bucket by non-key attribute/in rollouts/attribute not found diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index fee995486..2b4aae39a 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -252,7 +252,6 @@ EvaluationDetail ClientImpl::VariationInternal(FlagKey const& key, << "LaunchDarkly client has not yet been initialized. " "Returning default value"; - // TODO: SC-199918 auto error_reason = EvaluationReason(EvaluationReason::ErrorKind::kClientNotReady); if (eval_reasons_available_) { diff --git a/libs/common/include/launchdarkly/data/evaluation_detail.hpp b/libs/common/include/launchdarkly/data/evaluation_detail.hpp index e0d2cceec..d32428d05 100644 --- a/libs/common/include/launchdarkly/data/evaluation_detail.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_detail.hpp @@ -60,6 +60,14 @@ class EvaluationDetail { */ [[nodiscard]] std::optional const& Reason() const; + /** + * Check if an evaluation reason exists, and if so, if it is of a particular + * kind. + * @param kind Kind to check. + * @return True if a reason exists and matches the given kind. + */ + [[nodiscard]] bool ReasonKindIs(enum EvaluationReason::Kind kind) const; + /** * @return True if the evaluation resulted in an error. * TODO(sc209960) diff --git a/libs/common/src/data/evaluation_detail.cpp b/libs/common/src/data/evaluation_detail.cpp index 81a7aba3b..c6415848d 100644 --- a/libs/common/src/data/evaluation_detail.cpp +++ b/libs/common/src/data/evaluation_detail.cpp @@ -35,6 +35,11 @@ std::optional const& EvaluationDetail::Reason() const { return reason_; } +template +bool EvaluationDetail::ReasonKindIs(enum EvaluationReason::Kind kind) const { + return reason_.has_value() && reason_->Kind() == kind; +} + template std::optional EvaluationDetail::VariationIndex() const { return variation_index_; diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index dee59c11e..5af2f95f8 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -16,8 +16,10 @@ namespace launchdarkly::data_model { struct Flag { - using Variation = std::uint64_t; - using Weight = std::uint64_t; + using Variation = std::int64_t; + using Weight = std::int64_t; + using FlagVersion = std::uint64_t; + using Date = std::uint64_t; struct Rollout { enum class Kind { @@ -56,12 +58,12 @@ struct Flag { struct Prerequisite { std::string key; - std::uint64_t variation; + Variation variation; }; struct Target { std::vector values; - std::uint64_t variation; + Variation variation; ContextKind contextKind; }; @@ -79,7 +81,7 @@ struct Flag { }; std::string key; - std::uint64_t version; + FlagVersion version; bool on; VariationOrRollout fallthrough; std::vector variations; @@ -88,13 +90,13 @@ struct Flag { std::vector targets; std::vector contextTargets; std::vector rules; - std::optional offVariation; + std::optional offVariation; bool clientSide; ClientSideAvailability clientSideAvailability; std::optional salt; bool trackEvents; bool trackEventsFallthrough; - std::optional debugEventsUntilDate; + std::optional debugEventsUntilDate; /** * Returns the flag's version. Satisfies ItemDescriptor template diff --git a/libs/internal/include/launchdarkly/events/data/common_events.hpp b/libs/internal/include/launchdarkly/events/data/common_events.hpp index 667567c30..62066b4f2 100644 --- a/libs/internal/include/launchdarkly/events/data/common_events.hpp +++ b/libs/internal/include/launchdarkly/events/data/common_events.hpp @@ -33,7 +33,12 @@ struct TrackEventParams { std::optional metric_value; }; -// Track (custom) events are directly serialized from their parameters. +struct ServerTrackEventParams : public TrackEventParams { + Context context; +}; + +using ClientTrackEventParams = TrackEventParams; + using TrackEvent = TrackEventParams; struct IdentifyEventParams { diff --git a/libs/internal/include/launchdarkly/events/data/events.hpp b/libs/internal/include/launchdarkly/events/data/events.hpp index 1f474f2de..a0e00eae6 100644 --- a/libs/internal/include/launchdarkly/events/data/events.hpp +++ b/libs/internal/include/launchdarkly/events/data/events.hpp @@ -5,8 +5,10 @@ namespace launchdarkly::events { -using InputEvent = - std::variant; +using InputEvent = std::variant; using OutputEvent = std::variant AsioEventProcessor::Process( out.emplace_back(IdentifyEvent{event.creation_date, filter_.filter(event.context)}); }, - [&](TrackEventParams&& event) { + [&](ClientTrackEventParams&& event) { + out.emplace_back(std::move(event)); + }, + [&](ServerTrackEventParams&& event) { + if constexpr (std::is_same::value) { + if (!context_key_cache_.Notice( + event.context.CanonicalKey())) { + out.emplace_back(server_side::IndexEvent{ + event.creation_date, + filter_.filter(event.context)}); + } + } + + // Object slicing on purpose; the context will be stripped out + // of the ServerTrackEventParams when converted to a + // TrackEventParams. out.emplace_back(std::move(event)); }}, std::move(input_event)); diff --git a/libs/internal/src/events/common_events.cpp b/libs/internal/src/events/common_events.cpp index d3d324ec8..623f8d187 100644 --- a/libs/internal/src/events/common_events.cpp +++ b/libs/internal/src/events/common_events.cpp @@ -9,5 +9,4 @@ FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) value(params.value), reason(params.reason), default_(params.default_) {} - } // namespace launchdarkly::events diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index 85e797757..c681a76ac 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -203,7 +203,10 @@ tag_invoke(boost::json::value_to_tag< } data_model::Flag::Variation variation{}; - PARSE_REQUIRED_FIELD(variation, obj, "variation"); + /* If there's no rollout, this must be a variation. If there's no + * data at all, we must treat it as variation 0 (since upstream encoders may + * omit 0.) */ + PARSE_FIELD_DEFAULT(variation, obj, "variation", 0); return std::make_optional(variation); } diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp index dfd744191..157d26521 100644 --- a/libs/internal/src/serialization/json_segment.cpp +++ b/libs/internal/src/serialization/json_segment.cpp @@ -18,7 +18,7 @@ tl::expected, JsonError> tag_invoke( REQUIRE_OBJECT(json_value); auto const& obj = json_value.as_object(); - data_model::Segment::Target target; + data_model::Segment::Target target{}; PARSE_FIELD_DEFAULT(target.contextKind, obj, "contextKind", "user"); diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 00ecfcfd9..020b60751 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -283,11 +283,11 @@ TEST(PrerequisiteTests, DeserializesAllFields) { ASSERT_EQ(result->variation, 123); } -TEST(PrerequisiteTests, DeserializeFailsWithNegativeVariation) { +TEST(PrerequisiteTests, DeserializeSucceedsWithNegativeVariation) { auto result = boost::json::value_to< tl::expected>( boost::json::parse(R"({"key" : "foo", "variation" : -123})")); - ASSERT_FALSE(result); + ASSERT_TRUE(result); } TEST(TargetTests, DeserializesMinimumValid) { @@ -300,11 +300,11 @@ TEST(TargetTests, DeserializesMinimumValid) { ASSERT_TRUE(result->values.empty()); } -TEST(TargetTests, DeserializesFailsWithNegativeVariation) { +TEST(TargetTests, DeserializesSucceedsWithNegativeVariation) { auto result = boost::json::value_to< tl::expected>( boost::json::parse(R"({"variation" : -123})")); - ASSERT_FALSE(result); + ASSERT_TRUE(result); } TEST(TargetTests, DeserializesAllFields) { @@ -419,8 +419,7 @@ TEST(RolloutTests, SerializeAllFields) { } TEST(VariationOrRolloutTests, SerializeVariation) { - uint64_t value(5); - data_model::Flag::VariationOrRollout variation = value; + data_model::Flag::VariationOrRollout variation = 5; auto json = boost::json::value_from(variation); @@ -578,12 +577,11 @@ TEST(FlagRuleTests, SerializeAllRollout) { } TEST(FlagTests, SerializeAll) { - uint64_t fallthrough(42); data_model::Flag flag{ "the-key", 21, // version true, // on - fallthrough, // fallthrough + 42, // fallthrough {"a", "b"}, // variations {{"prereqA", 2}, {"prereqB", 3}}, // prerequisites {{{ diff --git a/libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp b/libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp new file mode 100644 index 000000000..6f4b50b63 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/all_flags_state.hpp @@ -0,0 +1,172 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace launchdarkly::server_side { + +/** + * AllFlagsState is a snapshot of the state of multiple feature flags with + * regard to a specific evaluation context. + * + * Serializing this object to JSON using boost::json::value_from will produce + * the appropriate data structure for bootstrapping the LaunchDarkly JavaScript + * client. + * + * To do this, the header + * must be + * included to make the appropriate `tag_invoke` implementations available to + * boost. + */ +class AllFlagsState { + public: + enum class Options : std::uint8_t { + /** + * Default behavior. + */ + Default = 0, + /** + * Include evaluation reasons in the state object. By default, they + * are not. + */ + IncludeReasons = (1 << 0), + /** + * Include detailed flag metadata only for flags with event tracking + * or debugging turned on. + * + * This reduces the size of the JSON data if you are + * passing the flag state to the front end. + */ + DetailsOnlyForTrackedFlags = (1 << 1), + /** + * Include only flags marked for use with the client-side SDK. + * By default, all flags are included. + */ + ClientSideOnly = (1 << 2) + }; + + /** + * State contains information pertaining to a single feature flag. + */ + class State { + public: + State(std::uint64_t version, + std::optional variation, + std::optional reason, + bool track_events, + bool track_reason, + std::optional debug_events_until_date); + + /** + * @return The flag's version number when it was evaluated. + */ + [[nodiscard]] std::uint64_t Version() const; + + /** + * @return The variation index that was selected for the specified + * evaluation context. + */ + [[nodiscard]] std::optional Variation() const; + + /** + * @return The reason that the flag evaluation produced the specified + * variation. + */ + [[nodiscard]] std::optional const& Reason() const; + + /** + * @return True if a full feature event must be sent when evaluating + * this flag. This will be true if tracking was explicitly enabled for + * this flag for data export, or if the evaluation involved an + * experiment, or both. + */ + [[nodiscard]] bool TrackEvents() const; + + /** + * @return True if the evaluation reason should always be included in + * any full feature event created for this flag, regardless of whether a + * VariationDetail method was called. This will be true if the + * evaluation involved an experiment. + */ + [[nodiscard]] bool TrackReason() const; + + /** + * @return The date on which debug mode expires for this flag, if + * enabled. + */ + [[nodiscard]] std::optional const& DebugEventsUntilDate() + const; + + /** + * + * @return True if the options passed to AllFlagsState, combined with + * the obtained flag state, indicate that some metadata can be left out + * of the JSON serialization. + */ + [[nodiscard]] bool OmitDetails() const; + + friend class AllFlagsStateBuilder; + + private: + std::uint64_t version_; + std::optional variation_; + std::optional reason_; + bool track_events_; + bool track_reason_; + std::optional debug_events_until_date_; + bool omit_details_; + }; + + /** + * @return True if the call to AllFlagsState succeeded. False if there was + * an error, such as the data store being unavailable. When false, the other + * accessors will return empty maps. + */ + [[nodiscard]] bool Valid() const; + + /** + * @return A map of metadata for each flag. + */ + [[nodiscard]] std::unordered_map const& States() const; + + /** + * @return A map of evaluation results for each flag. + */ + [[nodiscard]] std::unordered_map const& Values() const; + + /** + * Constructs an invalid instance of AllFlagsState. + */ + AllFlagsState(); + + /** + * Constructs a valid instance of AllFlagsState. + * @param evaluations A map of evaluation results for each flag. + * @param flags_state A map of metadata for each flag. + */ + AllFlagsState(std::unordered_map evaluations, + std::unordered_map flags_state); + + private: + bool const valid_; + const std::unordered_map flags_state_; + const std::unordered_map evaluations_; +}; + +void operator|=(AllFlagsState::Options& lhs, AllFlagsState::Options rhs); +AllFlagsState::Options operator|(AllFlagsState::Options lhs, + AllFlagsState::Options rhs); + +AllFlagsState::Options operator&(AllFlagsState::Options lhs, + AllFlagsState::Options rhs); + +bool operator==(class AllFlagsState::State const& lhs, + class AllFlagsState::State const& rhs); + +bool operator==(AllFlagsState const& lhs, AllFlagsState const& rhs); + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index 71614f4cd..50a9fda72 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include #include @@ -12,7 +14,6 @@ #include namespace launchdarkly::server_side { - /** * Interface for the standard SDK client methods and properties. */ @@ -58,8 +59,9 @@ class IClient { * * @return A map from feature flag keys to values for the current context. */ - [[nodiscard]] virtual std::unordered_map AllFlagsState() - const = 0; + [[nodiscard]] virtual class AllFlagsState AllFlagsState( + Context const& context, + AllFlagsState::Options options = AllFlagsState::Options::Default) = 0; /** * Tracks that the current context performed an event for the given event @@ -258,8 +260,10 @@ class Client : public IClient { [[nodiscard]] bool Initialized() const override; using FlagKey = std::string; - [[nodiscard]] std::unordered_map AllFlagsState() - const override; + [[nodiscard]] class AllFlagsState AllFlagsState( + Context const& context, + enum AllFlagsState::Options options = + AllFlagsState::Options::Default) override; void Track(Context const& ctx, std::string event_name, diff --git a/libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp b/libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp new file mode 100644 index 000000000..b644119a1 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/serialization/json_all_flags_state.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include + +namespace launchdarkly::server_side { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState::State const& state); + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState const& state); +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 0ff18837b..e0a327085 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -11,6 +11,9 @@ add_library(${LIBNAME} boost.cpp client.cpp client_impl.cpp + all_flags_state/all_flags_state.cpp + all_flags_state/json_all_flags_state.cpp + all_flags_state/all_flags_state_builder.cpp data_sources/data_source_update_sink.hpp data_store/data_store.hpp data_store/data_store_updater.hpp diff --git a/libs/server-sdk/src/all_flags_state/all_flags_state.cpp b/libs/server-sdk/src/all_flags_state/all_flags_state.cpp new file mode 100644 index 000000000..d2b1254ab --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/all_flags_state.cpp @@ -0,0 +1,86 @@ +#include "launchdarkly/server_side/all_flags_state.hpp" + +namespace launchdarkly::server_side { + +AllFlagsState::State::State( + std::uint64_t version, + std::optional variation, + std::optional reason, + bool track_events, + bool track_reason, + std::optional debug_events_until_date) + : version_(version), + variation_(variation), + reason_(reason), + track_events_(track_events), + track_reason_(track_reason), + debug_events_until_date_(debug_events_until_date), + omit_details_(false) {} + +std::uint64_t AllFlagsState::State::Version() const { + return version_; +} + +std::optional AllFlagsState::State::Variation() const { + return variation_; +} + +std::optional const& AllFlagsState::State::Reason() const { + return reason_; +} + +bool AllFlagsState::State::TrackEvents() const { + return track_events_; +} + +bool AllFlagsState::State::TrackReason() const { + return track_reason_; +} + +std::optional const& AllFlagsState::State::DebugEventsUntilDate() + const { + return debug_events_until_date_; +} + +bool AllFlagsState::State::OmitDetails() const { + return omit_details_; +} + +AllFlagsState::AllFlagsState() + : valid_(false), evaluations_(), flags_state_() {} + +AllFlagsState::AllFlagsState(std::unordered_map evaluations, + std::unordered_map flags_state) + : valid_(true), + evaluations_(std::move(evaluations)), + flags_state_(std::move(flags_state)) {} + +bool AllFlagsState::Valid() const { + return valid_; +} + +std::unordered_map const& +AllFlagsState::States() const { + return flags_state_; +} + +std::unordered_map const& AllFlagsState::Values() const { + return evaluations_; +} + +bool operator==(AllFlagsState const& lhs, AllFlagsState const& rhs) { + return lhs.Valid() == rhs.Valid() && lhs.Values() == rhs.Values() && + lhs.States() == rhs.States(); +} + +bool operator==(AllFlagsState::State const& lhs, + AllFlagsState::State const& rhs) { + return lhs.Version() == rhs.Version() && + lhs.Variation() == rhs.Variation() && lhs.Reason() == rhs.Reason() && + lhs.TrackEvents() == rhs.TrackEvents() && + lhs.TrackReason() == rhs.TrackReason() && + lhs.DebugEventsUntilDate() == rhs.DebugEventsUntilDate() && + lhs.OmitDetails() == rhs.OmitDetails(); +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp new file mode 100644 index 000000000..efa4314b6 --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.cpp @@ -0,0 +1,71 @@ +#include "all_flags_state_builder.hpp" + +namespace launchdarkly::server_side { + +bool IsDebuggingEnabled(std::optional debug_events_until); + +AllFlagsStateBuilder::AllFlagsStateBuilder(AllFlagsState::Options options) + : options_(options), flags_state_(), evaluations_() {} + +void AllFlagsStateBuilder::AddFlag(std::string const& key, + Value value, + AllFlagsState::State flag) { + if (IsSet(options_, AllFlagsState::Options::DetailsOnlyForTrackedFlags)) { + if (!flag.TrackEvents() && !flag.TrackReason() && + !IsDebuggingEnabled(flag.DebugEventsUntilDate())) { + flag.omit_details_ = true; + } + } + if (NotSet(options_, AllFlagsState::Options::IncludeReasons) && + !flag.TrackReason()) { + flag.reason_ = std::nullopt; + } + flags_state_.emplace(key, std::move(flag)); + evaluations_.emplace(std::move(key), std::move(value)); +} + +AllFlagsState AllFlagsStateBuilder::Build() { + return AllFlagsState{std::move(evaluations_), std::move(flags_state_)}; +} + +bool IsExperimentationEnabled(data_model::Flag const& flag, + std::optional const& reason) { + if (!reason) { + return false; + } + if (reason->InExperiment()) { + return true; + } + switch (reason->Kind()) { + case EvaluationReason::Kind::kFallthrough: + return flag.trackEventsFallthrough; + case EvaluationReason::Kind::kRuleMatch: + if (!reason->RuleIndex() || + reason->RuleIndex() >= flag.rules.size()) { + return false; + } + return flag.rules.at(*reason->RuleIndex()).trackEvents; + default: + return false; + } +} + +bool IsSet(AllFlagsState::Options options, AllFlagsState::Options flag) { + return (options & flag) == flag; +} + +bool NotSet(AllFlagsState::Options options, AllFlagsState::Options flag) { + return !IsSet(options, flag); +} + +std::uint64_t NowUnixMillis() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +bool IsDebuggingEnabled(std::optional debug_events_until) { + return debug_events_until && *debug_events_until > NowUnixMillis(); +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp new file mode 100644 index 000000000..bda58d815 --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include "../data_store/data_store.hpp" +#include "../evaluation/evaluator.hpp" + +namespace launchdarkly::server_side { + +bool IsExperimentationEnabled(data_model::Flag const& flag, + std::optional const& reason); + +bool IsSet(AllFlagsState::Options options, AllFlagsState::Options flag); +bool NotSet(AllFlagsState::Options options, AllFlagsState::Options flag); + +class AllFlagsStateBuilder { + public: + /** + * Constructs a builder capable of generating a AllFlagsState structure. + * @param options Options affecting the behavior of the builder. + */ + AllFlagsStateBuilder(AllFlagsState::Options options); + + /** + * Adds a flag, including its evaluation result and additional state. + * @param key Key of the flag. + * @param value Value of the flag. + * @param state State of the flag. + */ + void AddFlag(std::string const& key, + Value value, + AllFlagsState::State state); + + /** + * Builds a AllFlagsState structure from the flags added to the builder. + * This operation consumes the builder, and must only be called once. + * @return + */ + [[nodiscard]] AllFlagsState Build(); + + private: + enum AllFlagsState::Options options_; + std::unordered_map flags_state_; + std::unordered_map evaluations_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp b/libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp new file mode 100644 index 000000000..222ac27f7 --- /dev/null +++ b/libs/server-sdk/src/all_flags_state/json_all_flags_state.cpp @@ -0,0 +1,56 @@ +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side { + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState::State const& state) { + boost::ignore_unused(unused); + auto& obj = json_value.emplace_object(); + + if (!state.OmitDetails()) { + obj.emplace("version", state.Version()); + + if (auto const& reason = state.Reason()) { + obj.emplace("reason", boost::json::value_from(*reason)); + } + } + + if (auto const& variation = state.Variation()) { + obj.emplace("variation", *variation); + } + + if (state.TrackEvents()) { + obj.emplace("trackEvents", true); + } + + if (state.TrackReason()) { + obj.emplace("trackReason", true); + } + + if (auto const& date = state.DebugEventsUntilDate()) { + if (*date > 0) { + obj.emplace("debugEventsUntilDate", boost::json::value_from(*date)); + } + } +} + +void tag_invoke(boost::json::value_from_tag const& unused, + boost::json::value& json_value, + server_side::AllFlagsState const& state) { + boost::ignore_unused(unused); + auto& obj = json_value.emplace_object(); + obj.emplace("$valid", state.Valid()); + + obj.emplace("$flagsState", boost::json::value_from(state.States())); + + for (auto const& [k, v] : state.Values()) { + obj.emplace(k, boost::json::value_from(v)); + } +} +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client.cpp b/libs/server-sdk/src/client.cpp index c9b84acde..4a88b054a 100644 --- a/libs/server-sdk/src/client.cpp +++ b/libs/server-sdk/src/client.cpp @@ -4,6 +4,24 @@ namespace launchdarkly::server_side { +void operator|=(AllFlagsState::Options& lhs, AllFlagsState::Options rhs) { + lhs = lhs | rhs; +} + +AllFlagsState::Options operator|(AllFlagsState::Options lhs, + AllFlagsState::Options rhs) { + return static_cast( + static_cast>(lhs) | + static_cast>(rhs)); +} + +AllFlagsState::Options operator&(AllFlagsState::Options lhs, + AllFlagsState::Options rhs) { + return static_cast( + static_cast>(lhs) & + static_cast>(rhs)); +} + Client::Client(Config config) : client(std::make_unique(std::move(config), kVersion)) {} @@ -16,8 +34,10 @@ std::future Client::StartAsync() { } using FlagKey = std::string; -[[nodiscard]] std::unordered_map Client::AllFlagsState() const { - return client->AllFlagsState(); +[[nodiscard]] AllFlagsState Client::AllFlagsState( + Context const& context, + enum AllFlagsState::Options options) { + return client->AllFlagsState(context, options); } void Client::Track(Context const& ctx, diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 9cd59eb22..a54144f29 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -6,6 +6,7 @@ #include "client_impl.hpp" +#include "all_flags_state/all_flags_state_builder.hpp" #include "data_sources/null_data_source.hpp" #include "data_sources/polling_data_source.hpp" #include "data_sources/streaming_data_source.hpp" @@ -144,6 +145,8 @@ std::future ClientImpl::StartAsyncInternal( return false; /* keep the change listener */ }); + data_source_->Start(); + return fut; } @@ -155,24 +158,59 @@ bool ClientImpl::Initialized() const { return IsInitializedSuccessfully(status_manager_.Status().State()); } -std::unordered_map ClientImpl::AllFlagsState() const { +AllFlagsState ClientImpl::AllFlagsState(Context const& context, + AllFlagsState::Options options) { std::unordered_map result; - // TODO: implement all flags state (and update signature). - // for (auto& [key, descriptor] : memory_store_.AllFlags()) { - // if (descriptor->item) { - // result.try_emplace(key, descriptor->item->Value()); - // } - // } - return result; + + if (!Initialized()) { + if (memory_store_.Initialized()) { + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before client has finished " + "initializing; using last known values from data store"; + } else { + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before client has finished " + "initializing. Data store not available. Returning empty " + "state"; + return {}; + } + } + + AllFlagsStateBuilder builder{options}; + + for (auto const& [k, v] : memory_store_.AllFlags()) { + if (!v || !v->item) { + continue; + } + + auto const& flag = *(v->item); + + if (IsSet(options, AllFlagsState::Options::ClientSideOnly) && + !flag.clientSideAvailability.usingEnvironmentId) { + continue; + } + + EvaluationDetail detail = evaluator_.Evaluate(flag, context); + + bool in_experiment = IsExperimentationEnabled(flag, detail.Reason()); + builder.AddFlag(k, detail.Value(), + AllFlagsState::State{ + flag.Version(), detail.VariationIndex(), + detail.Reason(), flag.trackEvents || in_experiment, + in_experiment, flag.debugEventsUntilDate}); + } + + return builder.Build(); } void ClientImpl::TrackInternal(Context const& ctx, std::string event_name, std::optional data, std::optional metric_value) { - event_processor_->SendAsync(events::TrackEventParams{ - std::chrono::system_clock::now(), std::move(event_name), - ctx.KindsToKeys(), std::move(data), metric_value}); + event_processor_->SendAsync(events::ServerTrackEventParams{ + {std::chrono::system_clock::now(), std::move(event_name), + ctx.KindsToKeys(), std::move(data), metric_value}, + ctx}); } void ClientImpl::Track(Context const& ctx, @@ -200,7 +238,21 @@ template EvaluationDetail ClientImpl::VariationInternal(Context const& ctx, FlagKey const& key, Value default_value, - bool check_type) { + bool check_type, + bool detailed) { + events::FeatureEventParams event = { + std::chrono::system_clock::now(), + key, + ctx, + default_value, + default_value, + std::nullopt, + std::nullopt, + std::nullopt, + false, + std::nullopt, + }; + auto desc = memory_store_.GetFlag(key); if (!desc || !desc->item) { @@ -211,6 +263,12 @@ EvaluationDetail ClientImpl::VariationInternal(Context const& ctx, auto error_reason = EvaluationReason(EvaluationReason::ErrorKind::kClientNotReady); + + if (detailed) { + event.reason = error_reason; + } + + event_processor_->SendAsync(std::move(event)); return EvaluationDetail(std::move(default_value), std::nullopt, std::move(error_reason)); } @@ -220,6 +278,11 @@ EvaluationDetail ClientImpl::VariationInternal(Context const& ctx, auto error_reason = EvaluationReason(EvaluationReason::ErrorKind::kFlagNotFound); + + if (detailed) { + event.reason = error_reason; + } + event_processor_->SendAsync(std::move(event)); return EvaluationDetail(std::move(default_value), std::nullopt, std::move(error_reason)); @@ -240,10 +303,53 @@ EvaluationDetail ClientImpl::VariationInternal(Context const& ctx, auto error_reason = EvaluationReason(EvaluationReason::ErrorKind::kWrongType); + if (detailed) { + event.reason = error_reason; + } + event_processor_->SendAsync(std::move(event)); return EvaluationDetail(std::move(default_value), std::nullopt, error_reason); } + event.value = detail.Value(); + event.variation = detail.VariationIndex(); + event.version = flag.Version(); + + if (detailed) { + event.reason = detail.Reason(); + } + + if (flag.debugEventsUntilDate) { + event.debug_events_until_date = + events::Date{std::chrono::system_clock::time_point{ + std::chrono::milliseconds{*flag.debugEventsUntilDate}}}; + } + + bool track_fallthrough = + flag.trackEventsFallthrough && + detail.ReasonKindIs(EvaluationReason::Kind::kFallthrough); + + bool track_rule_match = + detail.ReasonKindIs(EvaluationReason::Kind::kRuleMatch); + + if (track_rule_match) { + auto const& rule_index = detail.Reason()->RuleIndex(); + assert(rule_index && + "evaluation algorithm must produce a rule index in the case of " + "rule " + "match"); + + assert(*rule_index < flag.rules.size() && + "evaluation algorithm must produce a valid rule index in the " + "case of " + "rule match"); + + track_rule_match = flag.rules.at(*rule_index).trackEvents; + } + + event.require_full_event = + flag.trackEvents || track_fallthrough || track_rule_match; + event_processor_->SendAsync(std::move(event)); return EvaluationDetail(detail.Value(), detail.VariationIndex(), detail.Reason()); } @@ -252,13 +358,13 @@ EvaluationDetail ClientImpl::BoolVariationDetail( Context const& ctx, IClient::FlagKey const& key, bool default_value) { - return VariationInternal(ctx, key, default_value, true); + return VariationInternal(ctx, key, default_value, true, true); } bool ClientImpl::BoolVariation(Context const& ctx, IClient::FlagKey const& key, bool default_value) { - return *VariationInternal(ctx, key, default_value, true); + return *VariationInternal(ctx, key, default_value, true, false); } EvaluationDetail ClientImpl::StringVariationDetail( @@ -266,53 +372,55 @@ EvaluationDetail ClientImpl::StringVariationDetail( ClientImpl::FlagKey const& key, std::string default_value) { return VariationInternal(ctx, key, std::move(default_value), - true); + true, true); } std::string ClientImpl::StringVariation(Context const& ctx, IClient::FlagKey const& key, std::string default_value) { return *VariationInternal(ctx, key, std::move(default_value), - true); + true, false); } EvaluationDetail ClientImpl::DoubleVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, double default_value) { - return VariationInternal(ctx, key, default_value, true); + return VariationInternal(ctx, key, default_value, true, true); } double ClientImpl::DoubleVariation(Context const& ctx, IClient::FlagKey const& key, double default_value) { - return *VariationInternal(ctx, key, default_value, true); + return *VariationInternal(ctx, key, default_value, true, false); } EvaluationDetail ClientImpl::IntVariationDetail( Context const& ctx, IClient::FlagKey const& key, int default_value) { - return VariationInternal(ctx, key, default_value, true); + return VariationInternal(ctx, key, default_value, true, true); } int ClientImpl::IntVariation(Context const& ctx, IClient::FlagKey const& key, int default_value) { - return *VariationInternal(ctx, key, default_value, true); + return *VariationInternal(ctx, key, default_value, true, false); } EvaluationDetail ClientImpl::JsonVariationDetail( Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return VariationInternal(ctx, key, std::move(default_value), false); + return VariationInternal(ctx, key, std::move(default_value), false, + true); } Value ClientImpl::JsonVariation(Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return *VariationInternal(ctx, key, std::move(default_value), false); + return *VariationInternal(ctx, key, std::move(default_value), false, + false); } // data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { @@ -328,5 +436,4 @@ ClientImpl::~ClientImpl() { // TODO: Probably not the best. run_thread_.join(); } - } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index f5491527d..beab76374 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -44,8 +44,10 @@ class ClientImpl : public IClient { bool Initialized() const override; using FlagKey = std::string; - [[nodiscard]] std::unordered_map AllFlagsState() - const override; + [[nodiscard]] class AllFlagsState AllFlagsState( + Context const& context, + AllFlagsState::Options options = + AllFlagsState::Options::Default) override; void Track(Context const& ctx, std::string event_name, @@ -111,7 +113,8 @@ class ClientImpl : public IClient { [[nodiscard]] EvaluationDetail VariationInternal(Context const& ctx, FlagKey const& key, Value default_value, - bool check_type); + bool check_type, + bool detailed); void TrackInternal(Context const& ctx, std::string event_name, std::optional data, diff --git a/libs/server-sdk/src/data_sources/data_source_event_handler.cpp b/libs/server-sdk/src/data_sources/data_source_event_handler.cpp index c3a3d7970..2d8c436b3 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.cpp +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.cpp @@ -62,7 +62,7 @@ tl::expected, JsonError> tag_invoke( auto const& obj = json_value.as_object(); PARSE_FIELD(path, obj, "path"); // We don't know what to do with a path other than "/". - if (path != "/") { + if (!(path == "/" || path.empty())) { return std::nullopt; } PARSE_FIELD(put.data, obj, "data"); @@ -148,7 +148,7 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( boost::json::value_to, JsonError>>( parsed); - if (!res.has_value()) { + if (!res) { LD_LOG(logger_, LogLevel::kError) << kErrorPutInvalid; status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_store/dependency_tracker.hpp index 13e32df7f..d62ba2c7c 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_store/dependency_tracker.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp index bc57398c0..219b660f8 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index 9241832a6..4a326e180 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -138,7 +138,7 @@ EvaluationDetail Evaluator::FlagVariation( Flag const& flag, Flag::Variation variation_index, EvaluationReason reason) const { - if (variation_index >= flag.variations.size()) { + if (variation_index < 0 || variation_index >= flag.variations.size()) { LogError(flag.key, Error::NonexistentVariationIndex(variation_index)); return EvaluationReason::MalformedFlag(); } diff --git a/libs/server-sdk/tests/all_flags_state_test.cpp b/libs/server-sdk/tests/all_flags_state_test.cpp new file mode 100644 index 000000000..c08a4fe54 --- /dev/null +++ b/libs/server-sdk/tests/all_flags_state_test.cpp @@ -0,0 +1,150 @@ +#include + +#include "all_flags_state/all_flags_state_builder.hpp" + +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +TEST(AllFlagsTest, Empty) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + ASSERT_TRUE(state.States().empty()); + ASSERT_TRUE(state.Values().empty()); +} + +TEST(AllFlagsTest, DefaultOptions) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + + builder.AddFlag( + "myFlag", true, + AllFlagsState::State{42, 1, std::nullopt, false, false, std::nullopt}); + + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + + auto expected = boost::json::parse(R"({ + "myFlag": true, + "$flagsState": { + "myFlag": { + "version": 42, + "variation": 1 + } + }, + "$valid": true + })"); + + auto got = boost::json::value_from(state); + ASSERT_EQ(got, expected); +} + +TEST(AllFlagsTest, DetailsOnlyForTrackedFlags) { + AllFlagsStateBuilder builder{ + AllFlagsState::Options::DetailsOnlyForTrackedFlags}; + builder.AddFlag( + "myFlagTracked", true, + AllFlagsState::State{42, 1, EvaluationReason::Fallthrough(false), true, + true, std::nullopt}); + builder.AddFlag( + "myFlagUntracked", true, + AllFlagsState::State{42, 1, EvaluationReason::Fallthrough(false), false, + false, std::nullopt}); + + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + + auto expected = boost::json::parse(R"({ + "myFlagTracked" : true, + "myFlagUntracked" : true, + "$flagsState": { + "myFlagTracked": { + "version": 42, + "variation": 1, + "reason":{ + "kind" : "FALLTHROUGH" + }, + "trackReason" : true, + "trackEvents" : true + }, + "myFlagUntracked" : { + "variation" : 1 + } + }, + "$valid": true + })"); + + auto got = boost::json::value_from(state); + ASSERT_EQ(got, expected); +} + +TEST(AllFlagsTest, IncludeReasons) { + AllFlagsStateBuilder builder{AllFlagsState::Options::IncludeReasons}; + builder.AddFlag( + "myFlag", true, + AllFlagsState::State{42, 1, EvaluationReason::Fallthrough(false), false, + false, std::nullopt}); + auto state = builder.Build(); + ASSERT_TRUE(state.Valid()); + + auto expected = boost::json::parse(R"({ + "myFlag": true, + "$flagsState": { + "myFlag": { + "version": 42, + "variation": 1, + "reason" : { + "kind": "FALLTHROUGH" + } + } + }, + "$valid": true + })"); + + auto got = boost::json::value_from(state); + ASSERT_EQ(got, expected); +} + +TEST(AllFlagsTest, FlagValues) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + + const std::size_t kNumFlags = 10; + + for (std::size_t i = 0; i < kNumFlags; i++) { + builder.AddFlag("myFlag" + std::to_string(i), "value", + AllFlagsState::State{42, 1, std::nullopt, false, false, + std::nullopt}); + } + + auto state = builder.Build(); + + auto const& vals = state.Values(); + + ASSERT_EQ(vals.size(), kNumFlags); + + ASSERT_TRUE(std::all_of(vals.begin(), vals.end(), [](auto const& kvp) { + return kvp.second.AsString() == "value"; + })); +} + +TEST(AllFlagsTest, FlagState) { + AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; + + const std::size_t kNumFlags = 10; + + AllFlagsState::State state{42, 1, std::nullopt, false, false, std::nullopt}; + for (std::size_t i = 0; i < kNumFlags; i++) { + builder.AddFlag("myFlag" + std::to_string(i), "value", state); + } + + auto all_flags_state = builder.Build(); + + auto const& states = all_flags_state.States(); + + ASSERT_EQ(states.size(), kNumFlags); + + ASSERT_TRUE(std::all_of(states.begin(), states.end(), [&](auto const& kvp) { + return kvp.second == state; + })); +} diff --git a/libs/server-sdk/tests/client_test.cpp b/libs/server-sdk/tests/client_test.cpp index 315c0a40d..73d0c0dd7 100644 --- a/libs/server-sdk/tests/client_test.cpp +++ b/libs/server-sdk/tests/client_test.cpp @@ -68,3 +68,12 @@ TEST_F(ClientTest, JsonVariationDefaultPassesThrough) { ASSERT_EQ(*client_.JsonVariationDetail(context_, flag, v), v); } } + +TEST_F(ClientTest, AllFlagsStateNotValid) { + // Since we don't have any ability to insert into the data store, assert + // only that the state is not valid. + auto flags = client_.AllFlagsState( + context_, AllFlagsState::Options::IncludeReasons | + AllFlagsState::Options::ClientSideOnly); + ASSERT_FALSE(flags.Valid()); +} diff --git a/libs/server-sdk/tests/test_store.hpp b/libs/server-sdk/tests/test_store.hpp index 4b0a37c44..bfccc008e 100644 --- a/libs/server-sdk/tests/test_store.hpp +++ b/libs/server-sdk/tests/test_store.hpp @@ -16,4 +16,16 @@ std::unique_ptr TestData(); */ std::unique_ptr Empty(); +/** + * Returns a flag suitable for inserting into a memory store, parsed from the + * given JSON representation. + */ +data_store::FlagDescriptor Flag(char const* json); + +/** + * Returns a segment suitable for inserting into a memory store, parsed from the + * given JSON representation. + */ +data_store::SegmentDescriptor Segment(char const* json); + } // namespace launchdarkly::server_side::test_store From 57c29d8e7e66ab240826e73a657186cc5142197c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 24 Aug 2023 11:19:35 -0700 Subject: [PATCH 39/56] fix: multi-kind user segment targeting (#206) The previous logic wouldn't match a multi-kind that had a user as a kind - it only worked if it was a single-kind of type user. --- .../test-suppressions.txt | 3 --- libs/server-sdk/src/evaluation/rules.cpp | 22 +++++++------------ 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt index eb6130c87..770279d04 100644 --- a/contract-tests/server-contract-tests/test-suppressions.txt +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -66,9 +66,6 @@ evaluation/parameterized/rollout or experiment - error for empty variations list evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate flag without detail evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate flag with detail evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate all flags -evaluation/parameterized/segment match/user matches via include in multi-kind user/user matches via include in multi-kind user/evaluate flag without detail -evaluation/parameterized/segment match/user matches via include in multi-kind user/user matches via include in multi-kind user/evaluate flag with detail -evaluation/parameterized/segment match/user matches via include in multi-kind user/user matches via include in multi-kind user/evaluate all flags evaluation/parameterized/segment recursion/cycle is detected at top level, recursion stops/cycle is detected at top level, recursion stops/evaluate flag with detail evaluation/parameterized/segment recursion/cycle is detected below top level, recursion stops/cycle is detected below top level, recursion stops/evaluate flag with detail events/summary events/prerequisites diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp index b43a0ef89..313cc44b1 100644 --- a/libs/server-sdk/src/evaluation/rules.cpp +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -191,14 +191,9 @@ tl::expected Contains(Segment const& segment, } bool IsTargeted(Context const& context, - std::vector const& keys, - std::vector const& targets) { - if (IsUser(context) && targets.empty()) { - return std::find(keys.begin(), keys.end(), context.CanonicalKey()) != - keys.end(); - } - - for (auto const& target : targets) { + std::vector const& user_keys, + std::vector const& context_targets) { + for (auto const& target : context_targets) { Value const& key = context.Get(target.contextKind, "key"); if (!key.IsString()) { continue; @@ -209,12 +204,11 @@ bool IsTargeted(Context const& context, } } - return false; -} + if (auto key = context.Get("user", "key"); !key.IsNull()) { + return std::find(user_keys.begin(), user_keys.end(), key.AsString()) != + user_keys.end(); + } -bool IsUser(Context const& context) { - auto const& kinds = context.Kinds(); - return kinds.size() == 1 && kinds[0] == "user"; + return false; } - } // namespace launchdarkly::server_side::evaluation From ef1061e174c80ef16988d239bf2ab462f74374da Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 24 Aug 2023 11:41:37 -0700 Subject: [PATCH 40/56] fix: handle undefined flag variations in fallthrough (#205) Fixes handling of undefined variations in VariationOrRollout structures. Previously, the lack of a variation was treated as 0. Instead, there is a special case that variation must be defined or else a flag is malformed. --- .../test-suppressions.txt | 5 +---- .../include/launchdarkly/data_model/flag.hpp | 2 +- libs/internal/src/serialization/json_flag.cpp | 21 +++++++++++-------- .../tests/data_model_serialization_test.cpp | 6 ++++-- libs/server-sdk/src/evaluation/bucketing.cpp | 8 +++++-- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt index 770279d04..2fddd2589 100644 --- a/contract-tests/server-contract-tests/test-suppressions.txt +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -1,3 +1,4 @@ +# SC-214431 evaluation/parameterized/bad attribute reference errors - clause with no attribute/test-flag/evaluate flag with detail evaluation/parameterized/bad attribute reference errors - empty path component/test-flag/evaluate flag with detail evaluation/parameterized/bad attribute reference errors - tilde followed by invalid escape character/test-flag/evaluate flag with detail @@ -60,12 +61,8 @@ evaluation/parameterized/prerequisites/prerequisite cycle is detected at top lev evaluation/parameterized/prerequisites/prerequisite cycle is detected at top level, recursion stops/prerequisite cycle is detected at top level, recursion stops/evaluate all flags evaluation/parameterized/prerequisites/prerequisite cycle is detected at deeper level, recursion stops/prerequisite cycle is detected at deeper level, recursion stops/evaluate flag with detail evaluation/parameterized/prerequisites/prerequisite cycle is detected at deeper level, recursion stops/prerequisite cycle is detected at deeper level, recursion stops/evaluate all flags -evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/fallthrough rollout/fallthrough rollout/evaluate flag without detail evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/fallthrough rollout/fallthrough rollout/evaluate flag with detail -evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/fallthrough rollout/fallthrough rollout/evaluate all flags -evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate flag without detail evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate flag with detail -evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate all flags evaluation/parameterized/segment recursion/cycle is detected at top level, recursion stops/cycle is detected at top level, recursion stops/evaluate flag with detail evaluation/parameterized/segment recursion/cycle is detected below top level, recursion stops/cycle is detected below top level, recursion stops/evaluate flag with detail events/summary events/prerequisites diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index 5af2f95f8..36d32246c 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -54,7 +54,7 @@ struct Flag { explicit Rollout(std::vector); }; - using VariationOrRollout = std::variant; + using VariationOrRollout = std::variant, Rollout>; struct Prerequisite { std::string key; diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index c681a76ac..47429e2bd 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -202,13 +202,13 @@ tag_invoke(boost::json::value_to_tag< return std::make_optional(*rollout); } - data_model::Flag::Variation variation{}; - /* If there's no rollout, this must be a variation. If there's no - * data at all, we must treat it as variation 0 (since upstream encoders may - * omit 0.) */ - PARSE_FIELD_DEFAULT(variation, obj, "variation", 0); + std::optional variation; - return std::make_optional(variation); + /* If there's no rollout, this must be a variation. If there's no variation, + * then this will be detected as a malformed flag at evaluation time. */ + PARSE_CONDITIONAL_FIELD(variation, obj, "variation"); + + return variation; } namespace data_model { @@ -264,9 +264,12 @@ void tag_invoke( using T = std::decay_t; if constexpr (std::is_same_v) { obj.emplace("rollout", boost::json::value_from(arg)); - } else if constexpr (std::is_same_v) { - obj.emplace("variation", arg); + } else if constexpr (std::is_same_v< + T, std::optional< + data_model::Flag::Variation>>) { + if (arg) { + obj.emplace("variation", *arg); + } } }, variation_or_rollout); diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 020b60751..02cf3c90e 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -326,7 +326,8 @@ TEST(FlagRuleTests, DeserializesMinimumValid) { ASSERT_FALSE(result->trackEvents); ASSERT_TRUE(result->clauses.empty()); ASSERT_FALSE(result->id); - ASSERT_EQ(std::get(result->variationOrRollout), + ASSERT_EQ(std::get>( + result->variationOrRollout), data_model::Flag::Variation(123)); } @@ -348,7 +349,8 @@ TEST(FlagRuleTests, DeserializesAllFields) { ASSERT_TRUE(result->trackEvents); ASSERT_TRUE(result->clauses.empty()); ASSERT_EQ(result->id, "foo"); - ASSERT_EQ(std::get(result->variationOrRollout), + ASSERT_EQ(std::get>( + result->variationOrRollout), data_model::Flag::Variation(123)); } diff --git a/libs/server-sdk/src/evaluation/bucketing.cpp b/libs/server-sdk/src/evaluation/bucketing.cpp index cd8b7cea2..1333ee1c0 100644 --- a/libs/server-sdk/src/evaluation/bucketing.cpp +++ b/libs/server-sdk/src/evaluation/bucketing.cpp @@ -160,8 +160,12 @@ tl::expected Variation( return std::visit( [&](auto&& arg) -> tl::expected { using T = std::decay_t; - if constexpr (std::is_same_v) { - return BucketResult(arg); + if constexpr (std::is_same_v>) { + if (!arg) { + return tl::make_unexpected( + Error::RolloutMissingVariations()); + } + return BucketResult(*arg); } else if constexpr (std::is_same_v) { if (arg.variations.empty()) { return tl::make_unexpected( From 09c74dd28338c1ff393f95be4bd9da4ee8a01967 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 25 Aug 2023 11:01:17 -0700 Subject: [PATCH 41/56] remove contract test branch from server.yml --- .github/workflows/server.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 17cf8b19e..1ae481daf 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -6,7 +6,7 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main, server-side, cw/sc-206687/contract-tests ] + branches: [ main, server-side ] paths-ignore: - '**.md' From 4ccd79f7a7c47fbb01c58d228323d83f026ddc47 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 25 Aug 2023 14:30:52 -0700 Subject: [PATCH 42/56] fix: refactor Variation methods for correctness & clarity (#203) This significantly refactors the `Variation` internal calls to be much easier to reason about. In the process, it fixes some broken behavior which is now reflected in the suppressions list. It also refactors event generation to use the `EventScope`/`EventFactory` pattern found in other SDKs, such as Rust and Go. --- .../test-suppressions.txt | 112 ------ .../include/launchdarkly/data_model/flag.hpp | 4 + .../events/data/common_events.hpp | 2 + libs/internal/src/data_model/flag.cpp | 22 ++ libs/internal/src/events/common_events.cpp | 3 +- .../src/serialization/events/json_events.cpp | 3 + libs/server-sdk/src/CMakeLists.txt | 1 + .../all_flags_state_builder.hpp | 3 - libs/server-sdk/src/client_impl.cpp | 334 ++++++++++-------- libs/server-sdk/src/client_impl.hpp | 40 ++- libs/server-sdk/src/evaluation/evaluator.cpp | 29 +- libs/server-sdk/src/evaluation/evaluator.hpp | 23 +- libs/server-sdk/src/events/event_factory.cpp | 94 +++++ libs/server-sdk/src/events/event_factory.hpp | 57 +++ libs/server-sdk/src/events/event_scope.hpp | 48 +++ libs/server-sdk/tests/event_factory_tests.cpp | 42 +++ libs/server-sdk/tests/event_scope_test.cpp | 73 ++++ libs/server-sdk/tests/spy_event_processor.hpp | 65 ++++ 18 files changed, 685 insertions(+), 270 deletions(-) create mode 100644 libs/server-sdk/src/events/event_factory.cpp create mode 100644 libs/server-sdk/src/events/event_factory.hpp create mode 100644 libs/server-sdk/src/events/event_scope.hpp create mode 100644 libs/server-sdk/tests/event_factory_tests.cpp create mode 100644 libs/server-sdk/tests/event_scope_test.cpp create mode 100644 libs/server-sdk/tests/spy_event_processor.hpp diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt index 2fddd2589..41ec5ec4d 100644 --- a/contract-tests/server-contract-tests/test-suppressions.txt +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -1,111 +1,3 @@ -# SC-214431 -evaluation/parameterized/bad attribute reference errors - clause with no attribute/test-flag/evaluate flag with detail -evaluation/parameterized/bad attribute reference errors - empty path component/test-flag/evaluate flag with detail -evaluation/parameterized/bad attribute reference errors - tilde followed by invalid escape character/test-flag/evaluate flag with detail -evaluation/parameterized/bad attribute reference errors - tilde followed by nothing/test-flag/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/off variation too low/off variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/off variation too high/off variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/target variation too low/target variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/target variation too high/target variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/rule variation too low/rule variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (bool)/rule variation too high/rule variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/off variation too low/off variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/off variation too high/off variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/target variation too low/target variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/target variation too high/target variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/rule variation too low/rule variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (int)/rule variation too high/rule variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/off variation too low/off variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/off variation too high/off variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/target variation too low/target variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/target variation too high/target variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/rule variation too low/rule variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (double)/rule variation too high/rule variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/off variation too low/off variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/off variation too high/off variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/target variation too low/target variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/target variation too high/target variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/rule variation too low/rule variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (string)/rule variation too high/rule variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/off variation too low/off variation too low/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/off variation too low/off variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/off variation too high/off variation too high/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/off variation too high/off variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/flag is off but has no off variation/flag is off but has no off variation/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/fallthrough variation too low/fallthrough variation too low/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/fallthrough variation too low/fallthrough variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/fallthrough variation too high/fallthrough variation too high/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/fallthrough variation too high/fallthrough variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/target variation too low/target variation too low/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/target variation too low/target variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/target variation too high/target variation too high/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/target variation too high/target variation too high/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/rule variation too low/rule variation too low/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/rule variation too low/rule variation too low/evaluate flag with detail -evaluation/parameterized/evaluation failures (any)/rule variation too high/rule variation too high/evaluate flag without detail -evaluation/parameterized/evaluation failures (any)/rule variation too high/rule variation too high/evaluate flag with detail -evaluation/parameterized/prerequisites/prerequisite cycle is detected at top level, recursion stops/prerequisite cycle is detected at top level, recursion stops/evaluate flag with detail -evaluation/parameterized/prerequisites/prerequisite cycle is detected at top level, recursion stops/prerequisite cycle is detected at top level, recursion stops/evaluate all flags -evaluation/parameterized/prerequisites/prerequisite cycle is detected at deeper level, recursion stops/prerequisite cycle is detected at deeper level, recursion stops/evaluate flag with detail -evaluation/parameterized/prerequisites/prerequisite cycle is detected at deeper level, recursion stops/prerequisite cycle is detected at deeper level, recursion stops/evaluate all flags -evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/fallthrough rollout/fallthrough rollout/evaluate flag with detail -evaluation/parameterized/rollout or experiment - error for empty variations list in rollout/rule rollout/rule rollout/evaluate flag with detail -evaluation/parameterized/segment recursion/cycle is detected at top level, recursion stops/cycle is detected at top level, recursion stops/evaluate flag with detail -evaluation/parameterized/segment recursion/cycle is detected below top level, recursion stops/cycle is detected below top level, recursion stops/evaluate flag with detail -events/summary events/prerequisites -events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: bool -events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: int -events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: double -events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: string -events/feature events/full feature event for tracked flag/without reason/single kind default/malformed flag/type: any -events/feature events/full feature event for tracked flag/without reason/single kind non-default/valid flag/type: bool -events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: bool -events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: int -events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: double -events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: string -events/feature events/full feature event for tracked flag/without reason/single kind non-default/malformed flag/type: any -events/feature events/full feature event for tracked flag/without reason/multi-kind/valid flag/type: bool -events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: bool -events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: int -events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: double -events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: string -events/feature events/full feature event for tracked flag/without reason/multi-kind/malformed flag/type: any -events/feature events/full feature event for tracked flag/with reason/single kind default/valid flag/type: bool -events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: bool -events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: int -events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: double -events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: string -events/feature events/full feature event for tracked flag/with reason/single kind default/malformed flag/type: any -events/feature events/full feature event for tracked flag/with reason/single kind non-default/valid flag/type: bool -events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: bool -events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: int -events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: double -events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: string -events/feature events/full feature event for tracked flag/with reason/single kind non-default/malformed flag/type: any -events/feature events/full feature event for tracked flag/with reason/multi-kind/valid flag/type: bool -events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: bool -events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: int -events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: double -events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: string -events/feature events/full feature event for tracked flag/with reason/multi-kind/malformed flag/type: any -events/feature events/evaluating all flags generates no events -events/feature prerequisite events/without reasons -events/feature prerequisite events/with reasons -events/experimentation/experiment in rule -events/experimentation/experiment in fallthrough streaming/updates/flag patch with same version is not applied streaming/updates/segment patch with same version is not applied streaming/updates/flag patch with lower version is not applied @@ -162,7 +54,3 @@ context type/convert/old user to context/{"key": "a", "avatar": "b"} context type/convert/old user to context/{"key": "a", "avatar": null} context type/convert/old user to context/{"key": "a", "ip": "b"} context type/convert/old user to context/{"key": "a", "ip": null} - -# These appear to only fail in CI, but not locally on Mac. Requires investigation. -evaluation/bucketing/bucket by non-key attribute/in rollouts/invalid value type -evaluation/bucketing/bucket by non-key attribute/in rollouts/attribute not found diff --git a/libs/internal/include/launchdarkly/data_model/flag.hpp b/libs/internal/include/launchdarkly/data_model/flag.hpp index 36d32246c..ebf9b24a9 100644 --- a/libs/internal/include/launchdarkly/data_model/flag.hpp +++ b/libs/internal/include/launchdarkly/data_model/flag.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -104,5 +105,8 @@ struct Flag { * @return Version of this flag. */ [[nodiscard]] inline std::uint64_t Version() const { return version; } + + [[nodiscard]] bool IsExperimentationEnabled( + std::optional const& reason) const; }; } // namespace launchdarkly::data_model diff --git a/libs/internal/include/launchdarkly/events/data/common_events.hpp b/libs/internal/include/launchdarkly/events/data/common_events.hpp index 62066b4f2..b8740319d 100644 --- a/libs/internal/include/launchdarkly/events/data/common_events.hpp +++ b/libs/internal/include/launchdarkly/events/data/common_events.hpp @@ -62,6 +62,7 @@ struct FeatureEventParams { std::optional reason; bool require_full_event; std::optional debug_events_until_date; + std::optional prereq_of; }; struct FeatureEventBase { @@ -72,6 +73,7 @@ struct FeatureEventBase { Value value; std::optional reason; Value default_; + std::optional prereq_of; explicit FeatureEventBase(FeatureEventParams const& params); }; diff --git a/libs/internal/src/data_model/flag.cpp b/libs/internal/src/data_model/flag.cpp index 646f36d83..2a03074a7 100644 --- a/libs/internal/src/data_model/flag.cpp +++ b/libs/internal/src/data_model/flag.cpp @@ -26,4 +26,26 @@ Flag::Rollout::Rollout(std::vector variations_) bucketBy("key"), contextKind("user") {} +bool Flag::IsExperimentationEnabled( + std::optional const& reason) const { + if (!reason) { + return false; + } + if (reason->InExperiment()) { + return true; + } + switch (reason->Kind()) { + case EvaluationReason::Kind::kFallthrough: + return this->trackEventsFallthrough; + case EvaluationReason::Kind::kRuleMatch: + if (!reason->RuleIndex() || + reason->RuleIndex() >= this->rules.size()) { + return false; + } + return this->rules.at(*reason->RuleIndex()).trackEvents; + default: + return false; + } +} + } // namespace launchdarkly::data_model diff --git a/libs/internal/src/events/common_events.cpp b/libs/internal/src/events/common_events.cpp index 623f8d187..e6fdbc096 100644 --- a/libs/internal/src/events/common_events.cpp +++ b/libs/internal/src/events/common_events.cpp @@ -8,5 +8,6 @@ FeatureEventBase::FeatureEventBase(FeatureEventParams const& params) variation(params.variation), value(params.value), reason(params.reason), - default_(params.default_) {} + default_(params.default_), + prereq_of(params.prereq_of) {} } // namespace launchdarkly::events diff --git a/libs/internal/src/serialization/events/json_events.cpp b/libs/internal/src/serialization/events/json_events.cpp index 3c13687c3..36a9b4553 100644 --- a/libs/internal/src/serialization/events/json_events.cpp +++ b/libs/internal/src/serialization/events/json_events.cpp @@ -39,6 +39,9 @@ void tag_invoke(boost::json::value_from_tag const& tag, obj.emplace("reason", boost::json::value_from(*event.reason)); } obj.emplace("default", boost::json::value_from(event.default_)); + if (event.prereq_of) { + obj.emplace("prereqOf", *event.prereq_of); + } } void tag_invoke(boost::json::value_from_tag const& tag, diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index e0a327085..f3afa8aab 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(${LIBNAME} data_store/persistent/expiration_tracker.hpp data_store/persistent/persistent_data_store.cpp data_store/persistent/expiration_tracker.cpp + events/event_factory.cpp ) if (MSVC OR (NOT BUILD_SHARED_LIBS)) diff --git a/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp index bda58d815..34e1a839b 100644 --- a/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp +++ b/libs/server-sdk/src/all_flags_state/all_flags_state_builder.hpp @@ -7,9 +7,6 @@ namespace launchdarkly::server_side { -bool IsExperimentationEnabled(data_model::Flag const& flag, - std::optional const& reason); - bool IsSet(AllFlagsState::Options options, AllFlagsState::Options flag); bool NotSet(AllFlagsState::Options options, AllFlagsState::Options flag); diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index a54144f29..0ffca198b 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -15,7 +15,6 @@ #include #include #include -#include #include #include @@ -76,6 +75,29 @@ static Logger MakeLogger(config::shared::built::Logging const& config) { std::make_shared(config.level, config.tag)}; } +bool EventsEnabled(Config const& config) { + return config.Events().Enabled() && !config.Offline(); +} + +std::unique_ptr> MakeEventProcessor( + Config const& config, + boost::asio::any_io_executor const& exec, + HttpProperties const& http_properties, + Logger& logger) { + if (EventsEnabled(config)) { + return std::make_unique>( + exec, config.ServiceEndpoints(), config.Events(), http_properties, + logger); + } + return nullptr; +} + +/** + * Returns true if the flag pointer is valid and the underlying item is present. + */ +bool IsFlagPresent( + std::shared_ptr const& flag_desc); + ClientImpl::ClientImpl(Config config, std::string const& version) : config_(config), http_properties_( @@ -94,17 +116,14 @@ ClientImpl::ClientImpl(Config config, std::string const& version) memory_store_, status_manager_, logger_)), - event_processor_(nullptr), - evaluator_(logger_, memory_store_) { - if (config.Events().Enabled() && !config.Offline()) { - event_processor_ = - std::make_unique>( - ioc_.get_executor(), config.ServiceEndpoints(), config.Events(), - http_properties_, logger_); - } else { - event_processor_ = std::make_unique(); - } - + event_processor_(MakeEventProcessor(config, + ioc_.get_executor(), + http_properties_, + logger_)), + evaluator_(logger_, memory_store_), + events_default_(event_processor_.get(), EventFactory::WithoutReasons()), + events_with_reasons_(event_processor_.get(), + EventFactory::WithReasons()) { run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); } @@ -125,8 +144,9 @@ static bool IsInitialized(DataSourceStatus::DataSourceState state) { } void ClientImpl::Identify(Context context) { - event_processor_->SendAsync(events::IdentifyEventParams{ - std::chrono::system_clock::now(), std::move(context)}); + events_default_.Send([&](EventFactory const& factory) { + return factory.Identify(std::move(context)); + }); } std::future ClientImpl::StartAsyncInternal( @@ -178,6 +198,8 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, AllFlagsStateBuilder builder{options}; + EventScope no_events; + for (auto const& [k, v] : memory_store_.AllFlags()) { if (!v || !v->item) { continue; @@ -190,9 +212,10 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, continue; } - EvaluationDetail detail = evaluator_.Evaluate(flag, context); + EvaluationDetail detail = + evaluator_.Evaluate(flag, context, no_events); - bool in_experiment = IsExperimentationEnabled(flag, detail.Reason()); + bool in_experiment = flag.IsExperimentationEnabled(detail.Reason()); builder.AddFlag(k, detail.Value(), AllFlagsState::State{ flag.Version(), detail.VariationIndex(), @@ -207,10 +230,10 @@ void ClientImpl::TrackInternal(Context const& ctx, std::string event_name, std::optional data, std::optional metric_value) { - event_processor_->SendAsync(events::ServerTrackEventParams{ - {std::chrono::system_clock::now(), std::move(event_name), - ctx.KindsToKeys(), std::move(data), metric_value}, - ctx}); + events_default_.Send([&](EventFactory const& factory) { + return factory.Custom(ctx, std::move(event_name), std::move(data), + metric_value); + }); } void ClientImpl::Track(Context const& ctx, @@ -231,196 +254,231 @@ void ClientImpl::Track(Context const& ctx, std::string event_name) { } void ClientImpl::FlushAsync() { - event_processor_->FlushAsync(); -} - -template -EvaluationDetail ClientImpl::VariationInternal(Context const& ctx, - FlagKey const& key, - Value default_value, - bool check_type, - bool detailed) { - events::FeatureEventParams event = { - std::chrono::system_clock::now(), - key, - ctx, - default_value, - default_value, - std::nullopt, - std::nullopt, - std::nullopt, - false, - std::nullopt, - }; - - auto desc = memory_store_.GetFlag(key); - - if (!desc || !desc->item) { - if (!Initialized()) { - LD_LOG(logger_, LogLevel::kWarn) - << "LaunchDarkly client has not yet been initialized. " - "Returning default value"; - - auto error_reason = - EvaluationReason(EvaluationReason::ErrorKind::kClientNotReady); - - if (detailed) { - event.reason = error_reason; - } + if (event_processor_) { + event_processor_->FlushAsync(); + } +} - event_processor_->SendAsync(std::move(event)); - return EvaluationDetail(std::move(default_value), std::nullopt, - std::move(error_reason)); +void ClientImpl::LogVariationCall(std::string const& key, + bool flag_present) const { + if (Initialized()) { + if (!flag_present) { + LD_LOG(logger_, LogLevel::kInfo) << "Unknown feature flag " << key + << "; returning default value"; + } + } else { + if (flag_present) { + LD_LOG(logger_, LogLevel::kInfo) + << "LaunchDarkly client has not yet been initialized; using " + "last " + "known flag rules from data store"; + } else { + LD_LOG(logger_, LogLevel::kInfo) + << "LaunchDarkly client has not yet been initialized; " + "returning default value"; } + } +} - LD_LOG(logger_, LogLevel::kInfo) - << "Unknown feature flag " << key << "; returning default value"; +Value ClientImpl::Variation(Context const& ctx, + IClient::FlagKey const& key, + Value const& default_value) { + return *VariationInternal(ctx, key, default_value, events_default_); +} - auto error_reason = - EvaluationReason(EvaluationReason::ErrorKind::kFlagNotFound); +EvaluationDetail ClientImpl::VariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + Value const& default_value) { + return VariationInternal(ctx, key, default_value, events_with_reasons_); +} - if (detailed) { - event.reason = error_reason; - } - event_processor_->SendAsync(std::move(event)); - return EvaluationDetail(std::move(default_value), std::nullopt, - std::move(error_reason)); - - } else if (!Initialized()) { - LD_LOG(logger_, LogLevel::kInfo) - << "LaunchDarkly client has not yet been initialized. " - "Returning cached value"; +EvaluationDetail ClientImpl::VariationInternal( + Context const& context, + IClient::FlagKey const& key, + Value const& default_value, + EventScope const& event_scope) { + if (auto error = PreEvaluationChecks(context)) { + return PostEvaluation(key, context, default_value, *error, event_scope, + std::nullopt); } - assert(desc->item); + auto flag_rule = memory_store_.GetFlag(key); - auto const& flag = *(desc->item); + bool flag_present = IsFlagPresent(flag_rule); - EvaluationDetail const detail = evaluator_.Evaluate(flag, ctx); + LogVariationCall(key, flag_present); - if (check_type && default_value.Type() != Value::Type::kNull && - detail.Value().Type() != default_value.Type()) { - auto error_reason = - EvaluationReason(EvaluationReason::ErrorKind::kWrongType); - - if (detailed) { - event.reason = error_reason; - } - event_processor_->SendAsync(std::move(event)); - return EvaluationDetail(std::move(default_value), std::nullopt, - error_reason); + if (!flag_present) { + return PostEvaluation(key, context, default_value, + EvaluationReason::ErrorKind::kFlagNotFound, + event_scope, std::nullopt); } - event.value = detail.Value(); - event.variation = detail.VariationIndex(); - event.version = flag.Version(); + EvaluationDetail result = + evaluator_.Evaluate(*flag_rule->item, context, event_scope); + return PostEvaluation(key, context, default_value, result, event_scope, + flag_rule.get()->item); +} - if (detailed) { - event.reason = detail.Reason(); +std::optional ClientImpl::PreEvaluationChecks( + Context const& context) { + if (!memory_store_.Initialized()) { + return EvaluationReason::ErrorKind::kClientNotReady; } - - if (flag.debugEventsUntilDate) { - event.debug_events_until_date = - events::Date{std::chrono::system_clock::time_point{ - std::chrono::milliseconds{*flag.debugEventsUntilDate}}}; + if (!context.Valid()) { + return EvaluationReason::ErrorKind::kUserNotSpecified; } + return std::nullopt; +} - bool track_fallthrough = - flag.trackEventsFallthrough && - detail.ReasonKindIs(EvaluationReason::Kind::kFallthrough); - - bool track_rule_match = - detail.ReasonKindIs(EvaluationReason::Kind::kRuleMatch); - - if (track_rule_match) { - auto const& rule_index = detail.Reason()->RuleIndex(); - assert(rule_index && - "evaluation algorithm must produce a rule index in the case of " - "rule " - "match"); - - assert(*rule_index < flag.rules.size() && - "evaluation algorithm must produce a valid rule index in the " - "case of " - "rule match"); - - track_rule_match = flag.rules.at(*rule_index).trackEvents; - } +EvaluationDetail ClientImpl::PostEvaluation( + std::string const& key, + Context const& context, + Value const& default_value, + std::variant> + error_or_detail, + EventScope const& event_scope, + std::optional const& flag) { + return std::visit( + [&](auto&& arg) { + using T = std::decay_t; + // VARIANT: ErrorKind + if constexpr (std::is_same_v) { + auto detail = EvaluationDetail{arg, default_value}; + + event_scope.Send([&](EventFactory const& factory) { + return factory.UnknownFlag(key, context, detail, + default_value); + }); + + return detail; + } + // VARIANT: EvaluationDetail + else if constexpr (std::is_same_v>) { + auto detail = EvaluationDetail{ + (!arg.VariationIndex() ? default_value : arg.Value()), + arg.VariationIndex(), arg.Reason()}; + + event_scope.Send([&](EventFactory const& factory) { + return factory.Eval(key, context, flag, detail, + default_value, std::nullopt); + }); + + return detail; + } + }, + std::move(error_or_detail)); +} - event.require_full_event = - flag.trackEvents || track_fallthrough || track_rule_match; - event_processor_->SendAsync(std::move(event)); - return EvaluationDetail(detail.Value(), detail.VariationIndex(), - detail.Reason()); +bool IsFlagPresent( + std::shared_ptr const& flag_desc) { + return flag_desc && flag_desc->item; } EvaluationDetail ClientImpl::BoolVariationDetail( Context const& ctx, IClient::FlagKey const& key, bool default_value) { - return VariationInternal(ctx, key, default_value, true, true); + auto result = VariationDetail(ctx, key, default_value); + if (result.Value().IsBool()) { + return EvaluationDetail{result.Value(), result.VariationIndex(), + result.Reason()}; + } + return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, + default_value}; } bool ClientImpl::BoolVariation(Context const& ctx, IClient::FlagKey const& key, bool default_value) { - return *VariationInternal(ctx, key, default_value, true, false); + auto result = Variation(ctx, key, default_value); + if (result.IsBool()) { + return result; + } + return default_value; } EvaluationDetail ClientImpl::StringVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, std::string default_value) { - return VariationInternal(ctx, key, std::move(default_value), - true, true); + auto result = VariationDetail(ctx, key, default_value); + if (result.Value().IsString()) { + return EvaluationDetail{ + result.Value(), result.VariationIndex(), result.Reason()}; + } + return EvaluationDetail{ + EvaluationReason::ErrorKind::kWrongType, default_value}; } std::string ClientImpl::StringVariation(Context const& ctx, IClient::FlagKey const& key, std::string default_value) { - return *VariationInternal(ctx, key, std::move(default_value), - true, false); + auto result = Variation(ctx, key, default_value); + if (result.IsString()) { + return result; + } + return default_value; } EvaluationDetail ClientImpl::DoubleVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, double default_value) { - return VariationInternal(ctx, key, default_value, true, true); + auto result = VariationDetail(ctx, key, default_value); + if (result.Value().IsNumber()) { + return EvaluationDetail{result.Value(), result.VariationIndex(), + result.Reason()}; + } + return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, + default_value}; } double ClientImpl::DoubleVariation(Context const& ctx, IClient::FlagKey const& key, double default_value) { - return *VariationInternal(ctx, key, default_value, true, false); + auto result = Variation(ctx, key, default_value); + if (result.IsNumber()) { + return result; + } + return default_value; } EvaluationDetail ClientImpl::IntVariationDetail( Context const& ctx, IClient::FlagKey const& key, int default_value) { - return VariationInternal(ctx, key, default_value, true, true); + auto result = VariationDetail(ctx, key, default_value); + if (result.Value().IsNumber()) { + return EvaluationDetail{result.Value(), result.VariationIndex(), + result.Reason()}; + } + return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, + default_value}; } int ClientImpl::IntVariation(Context const& ctx, IClient::FlagKey const& key, int default_value) { - return *VariationInternal(ctx, key, default_value, true, false); + auto result = Variation(ctx, key, default_value); + if (result.IsNumber()) { + return result; + } + return default_value; } EvaluationDetail ClientImpl::JsonVariationDetail( Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return VariationInternal(ctx, key, std::move(default_value), false, - true); + return VariationDetail(ctx, key, default_value); } Value ClientImpl::JsonVariation(Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return *VariationInternal(ctx, key, std::move(default_value), false, - false); + return Variation(ctx, key, default_value); } // data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index beab76374..aebf2ef7d 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -17,6 +17,8 @@ #include "evaluation/evaluator.hpp" +#include "events/event_scope.hpp" + #include #include @@ -109,12 +111,33 @@ class ClientImpl : public IClient { std::future StartAsync() override; private: - template - [[nodiscard]] EvaluationDetail VariationInternal(Context const& ctx, - FlagKey const& key, - Value default_value, - bool check_type, - bool detailed); + [[nodiscard]] EvaluationDetail VariationInternal( + Context const& ctx, + FlagKey const& key, + Value const& default_value, + EventScope const& scope); + + [[nodiscard]] EvaluationDetail VariationDetail( + Context const& ctx, + FlagKey const& key, + Value const& default_value); + + [[nodiscard]] Value Variation(Context const& ctx, + std::string const& key, + Value const& default_value); + + [[nodiscard]] EvaluationDetail PostEvaluation( + std::string const& key, + Context const& context, + Value const& default_value, + std::variant> + result, + EventScope const& event_scope, + std::optional const& flag); + + [[nodiscard]] std::optional + PreEvaluationChecks(Context const& context); + void TrackInternal(Context const& ctx, std::string event_name, std::optional data, @@ -124,6 +147,8 @@ class ClientImpl : public IClient { std::function predicate); + void LogVariationCall(std::string const& key, bool flag_present) const; + Config config_; Logger logger_; @@ -146,6 +171,9 @@ class ClientImpl : public IClient { evaluation::Evaluator evaluator_; + EventScope const events_default_; + EventScope const events_with_reasons_; + std::thread run_thread_; }; } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index 4a326e180..97057fbd2 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -23,15 +23,23 @@ Evaluator::Evaluator(Logger& logger, data_store::IDataStore const& store) : logger_(logger), store_(store), stack_() {} EvaluationDetail Evaluator::Evaluate( - Flag const& flag, + data_model::Flag const& flag, launchdarkly::Context const& context) { - return Evaluate("", flag, context); + return Evaluate(flag, context, EventScope{}); } EvaluationDetail Evaluator::Evaluate( - std::string const& parent_key, Flag const& flag, - launchdarkly::Context const& context) { + launchdarkly::Context const& context, + EventScope const& event_scope) { + return Evaluate(std::nullopt, flag, context, event_scope); +} + +EvaluationDetail Evaluator::Evaluate( + std::optional parent_key, + Flag const& flag, + launchdarkly::Context const& context, + EventScope const& event_scope) { if (auto guard = stack_.NoticePrerequisite(flag.key)) { if (!flag.on) { return OffValue(flag, EvaluationReason::Off()); @@ -56,7 +64,7 @@ EvaluationDetail Evaluator::Evaluate( // Recursive call; cycles are detected by the guard. EvaluationDetail detailed_evaluation = - Evaluate(flag.key, *descriptor.item, context); + Evaluate(flag.key, *descriptor.item, context, event_scope); if (detailed_evaluation.IsError()) { return detailed_evaluation; @@ -65,7 +73,11 @@ EvaluationDetail Evaluator::Evaluate( std::optional variation_index = detailed_evaluation.VariationIndex(); - // TODO(209589) prerequisite events. + event_scope.Send([&](EventFactory const& factory) { + return factory.Eval(p.key, context, *descriptor.item, + detailed_evaluation, Value::Null(), + flag.key); + }); if (!descriptor.item->on || variation_index != p.variation) { return OffValue(flag, @@ -73,8 +85,9 @@ EvaluationDetail Evaluator::Evaluate( } } } else { - LogError(parent_key, Error::CyclicPrerequisiteReference(flag.key)); - return OffValue(flag, EvaluationReason::MalformedFlag()); + LogError(parent_key.value_or("(no parent)"), + Error::CyclicPrerequisiteReference(flag.key)); + return EvaluationReason::MalformedFlag(); } // If the flag is on, all prerequisites are on and valid, then diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp index 6e22405b9..cf7a15468 100644 --- a/libs/server-sdk/src/evaluation/evaluator.hpp +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -7,6 +7,7 @@ #include #include "../data_store/data_store.hpp" +#include "../events/event_scope.hpp" #include "bucketing.hpp" #include "detail/evaluation_stack.hpp" #include "evaluation_error.hpp" @@ -22,6 +23,23 @@ class Evaluator { /** * Evaluates a flag for a given context. * Warning: not thread safe. + * + * @param flag The flag to evaluate. + * @param context The context to evaluate the flag against. + * @param event_scope The event scope used for recording prerequisite + * events. + */ + [[nodiscard]] EvaluationDetail Evaluate( + data_model::Flag const& flag, + launchdarkly::Context const& context, + EventScope const& event_scope); + + /** + * Evaluates a flag for a given context. Does not record prerequisite + * events. Warning: not thread safe. + * + * @param flag The flag to evaluate. + * @param context The context to evaluate the flag against. */ [[nodiscard]] EvaluationDetail Evaluate( data_model::Flag const& flag, @@ -29,9 +47,10 @@ class Evaluator { private: [[nodiscard]] EvaluationDetail Evaluate( - std::string const& parent_key, + std::optional parent_key, data_model::Flag const& flag, - launchdarkly::Context const& context); + launchdarkly::Context const& context, + EventScope const& event_scope); [[nodiscard]] EvaluationDetail FlagVariation( data_model::Flag const& flag, diff --git a/libs/server-sdk/src/events/event_factory.cpp b/libs/server-sdk/src/events/event_factory.cpp new file mode 100644 index 000000000..2be43f6fe --- /dev/null +++ b/libs/server-sdk/src/events/event_factory.cpp @@ -0,0 +1,94 @@ +#include "event_factory.hpp" + +#include +namespace launchdarkly::server_side { + +EventFactory::EventFactory( + launchdarkly::server_side::EventFactory::ReasonPolicy reason_policy) + : reason_policy_(reason_policy), + now_([]() { return events::Date{std::chrono::system_clock::now()}; }) {} + +EventFactory EventFactory::WithReasons() { + return {ReasonPolicy::Require}; +} + +EventFactory EventFactory::WithoutReasons() { + return {ReasonPolicy::Default}; +} + +events::InputEvent EventFactory::UnknownFlag( + std::string const& key, + launchdarkly::Context const& ctx, + EvaluationDetail detail, + launchdarkly::Value default_val) const { + return FeatureRequest(key, ctx, std::nullopt, detail, default_val, + std::nullopt); +} + +events::InputEvent EventFactory::Eval( + std::string const& key, + Context const& ctx, + std::optional const& flag, + EvaluationDetail detail, + Value default_value, + std::optional prereq_of) const { + return FeatureRequest(key, ctx, flag, detail, default_value, prereq_of); +} + +events::InputEvent EventFactory::Identify(launchdarkly::Context ctx) const { + return events::IdentifyEventParams{now_(), std::move(ctx)}; +} + +events::InputEvent EventFactory::Custom( + Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value) const { + return events::ServerTrackEventParams{ + {now_(), std::move(event_name), ctx.KindsToKeys(), std::move(data), + metric_value}, + ctx}; +} + +events::InputEvent EventFactory::FeatureRequest( + std::string const& key, + launchdarkly::Context const& context, + std::optional const& flag, + EvaluationDetail detail, + launchdarkly::Value default_val, + std::optional prereq_of) const { + bool flag_track_events = false; + bool require_experiment_data = false; + std::optional debug_events_until_date; + + if (flag.has_value()) { + flag_track_events = flag->trackEvents; + require_experiment_data = + flag->IsExperimentationEnabled(detail.Reason()); + if (flag->debugEventsUntilDate) { + debug_events_until_date = + events::Date{std::chrono::system_clock::time_point{ + std::chrono::milliseconds(*flag->debugEventsUntilDate)}}; + } + } + + std::optional reason; + if (reason_policy_ == ReasonPolicy::Require || require_experiment_data) { + reason = detail.Reason(); + } + + return events::FeatureEventParams{ + now_(), + key, + context, + detail.Value(), + default_val, + flag.has_value() ? std::make_optional(flag->version) : std::nullopt, + detail.VariationIndex(), + reason, + flag_track_events || require_experiment_data, + debug_events_until_date, + prereq_of}; +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/events/event_factory.hpp b/libs/server-sdk/src/events/event_factory.hpp new file mode 100644 index 000000000..e9a10eec3 --- /dev/null +++ b/libs/server-sdk/src/events/event_factory.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side { + +class EventFactory { + enum class ReasonPolicy { + Default = 0, + Require = 1, + }; + + public: + [[nodiscard]] static EventFactory WithReasons(); + [[nodiscard]] static EventFactory WithoutReasons(); + + [[nodiscard]] events::InputEvent UnknownFlag(std::string const& key, + Context const& ctx, + EvaluationDetail detail, + Value default_val) const; + + [[nodiscard]] events::InputEvent Eval( + std::string const& key, + Context const& ctx, + std::optional const& flag, + EvaluationDetail detail, + Value default_value, + std::optional prereq_of) const; + + [[nodiscard]] events::InputEvent Identify(Context ctx) const; + + [[nodiscard]] events::InputEvent Custom( + Context const& ctx, + std::string event_name, + std::optional data, + std::optional metric_value) const; + + private: + EventFactory(ReasonPolicy reason_policy); + events::InputEvent FeatureRequest( + std::string const& key, + Context const& ctx, + std::optional const& flag, + EvaluationDetail detail, + Value default_val, + std::optional prereq_of) const; + + ReasonPolicy const reason_policy_; + std::function now_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/events/event_scope.hpp b/libs/server-sdk/src/events/event_scope.hpp new file mode 100644 index 000000000..2eb32ade4 --- /dev/null +++ b/libs/server-sdk/src/events/event_scope.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "event_factory.hpp" + +namespace launchdarkly::server_side { + +/** + * EventScope is responsible for forwarding events to an + * IEventProcessor. If the given interface is nullptr, then events will not + * be forwarded at all. + */ +class EventScope { + public: + /** + * Constructs an EventScope with a non-owned IEventProcessor and factory. + * When Send is called, the factory will be passed to the caller, which must + * return a constructed event. + * @param processor The event processor to forward events to. + * @param factory The factory used for generating events. + */ + EventScope(events::IEventProcessor* processor, EventFactory factory) + : processor_(processor), factory_(std::move(factory)) {} + + /** + * Default constructs an EventScope which will not forward events. + */ + EventScope() : EventScope(nullptr, EventFactory::WithoutReasons()) {} + + /** + * Sends an event created by the given callable. The callable will be + * passed an EventFactory. + * @param callable Returns an InputEvent. + */ + template + void Send(Callable&& callable) const { + if (processor_) { + processor_->SendAsync(callable(factory_)); + } + } + + private: + events::IEventProcessor* processor_; + EventFactory const factory_; +}; + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/tests/event_factory_tests.cpp b/libs/server-sdk/tests/event_factory_tests.cpp new file mode 100644 index 000000000..11eccd1e8 --- /dev/null +++ b/libs/server-sdk/tests/event_factory_tests.cpp @@ -0,0 +1,42 @@ +#include + +#include +#include +#include + +#include "events/event_factory.hpp" + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +class EventFactoryTests : public testing::Test { + public: + EventFactoryTests() + : context_(ContextBuilder().Kind("cat", "shadow").Build()) {} + Context context_; +}; + +TEST_F(EventFactoryTests, IncludesReasonIfInExperiment) { + auto factory = EventFactory::WithoutReasons(); + auto event = + factory.Eval("flag", context_, data_model::Flag{}, + EvaluationReason::Fallthrough(true), false, std::nullopt); + ASSERT_TRUE(std::get(event).reason.has_value()); +} + +TEST_F(EventFactoryTests, DoesNotIncludeReasonIfNotInExperiment) { + auto factory = EventFactory::WithoutReasons(); + auto event = + factory.Eval("flag", context_, data_model::Flag{}, + EvaluationReason::Fallthrough(false), false, std::nullopt); + ASSERT_FALSE( + std::get(event).reason.has_value()); +} + +TEST_F(EventFactoryTests, IncludesReasonIfForcedByFactory) { + auto factory = EventFactory::WithReasons(); + auto event = + factory.Eval("flag", context_, data_model::Flag{}, + EvaluationReason::Fallthrough(false), false, std::nullopt); + ASSERT_TRUE(std::get(event).reason.has_value()); +} diff --git a/libs/server-sdk/tests/event_scope_test.cpp b/libs/server-sdk/tests/event_scope_test.cpp new file mode 100644 index 000000000..4497d6bb3 --- /dev/null +++ b/libs/server-sdk/tests/event_scope_test.cpp @@ -0,0 +1,73 @@ +#include + +#include + +#include "spy_event_processor.hpp" + +#include "events/event_scope.hpp" + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +TEST(EventScope, DefaultConstructedScopeHasNoObservableEffects) { + EventScope default_scope; + default_scope.Send([](EventFactory const& factory) { + return factory.Identify(ContextBuilder().Kind("cat", "shadow").Build()); + }); +} + +TEST(EventScope, SendWithNullProcessorHasNoObservableEffects) { + EventScope scope(nullptr, EventFactory::WithoutReasons()); + scope.Send([](EventFactory const& factory) { + return factory.Identify(ContextBuilder().Kind("cat", "shadow").Build()); + }); +} + +TEST(EventScope, ForwardsEvents) { + SpyEventProcessor processor; + EventScope scope(&processor, EventFactory::WithoutReasons()); + + const std::size_t kEventCount = 10; + + for (std::size_t i = 0; i < kEventCount; ++i) { + scope.Send([](EventFactory const& factory) { + return factory.Identify( + ContextBuilder().Kind("cat", "shadow").Build()); + }); + } + + ASSERT_TRUE(processor.Count(kEventCount)); +} + +TEST(EventScope, ForwardsCorrectEventTypes) { + SpyEventProcessor processor; + EventScope scope(&processor, EventFactory::WithoutReasons()); + + scope.Send([](EventFactory const& factory) { + return factory.Identify(ContextBuilder().Kind("cat", "shadow").Build()); + }); + + scope.Send([](EventFactory const& factory) { + return factory.UnknownFlag( + "flag", ContextBuilder().Kind("cat", "shadow").Build(), + EvaluationReason::Fallthrough(false), true); + }); + + scope.Send([](EventFactory const& factory) { + return factory.Eval("flag", + ContextBuilder().Kind("cat", "shadow").Build(), + std::nullopt, EvaluationReason::Fallthrough(false), + false, std::nullopt); + }); + + scope.Send([](EventFactory const& factory) { + return factory.Custom(ContextBuilder().Kind("cat", "shadow").Build(), + "event", std::nullopt, std::nullopt); + }); + + ASSERT_TRUE(processor.Count(4)); + ASSERT_TRUE(processor.Kind(0)); + ASSERT_TRUE(processor.Kind(1)); + ASSERT_TRUE(processor.Kind(2)); + ASSERT_TRUE(processor.Kind(3)); +} diff --git a/libs/server-sdk/tests/spy_event_processor.hpp b/libs/server-sdk/tests/spy_event_processor.hpp new file mode 100644 index 000000000..2981f39d0 --- /dev/null +++ b/libs/server-sdk/tests/spy_event_processor.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +#include + +namespace launchdarkly { +class SpyEventProcessor : public events::IEventProcessor { + public: + struct Flush {}; + struct Shutdown {}; + + using Record = events::InputEvent; + + SpyEventProcessor() : events_() {} + + void SendAsync(events::InputEvent event) override { + events_.push_back(std::move(event)); + } + + void FlushAsync() override {} + + void ShutdownAsync() override {} + + /** + * Asserts that 'count' events were recorded. + * @param count Number of expected events. + */ + [[nodiscard]] testing::AssertionResult Count(std::size_t count) const { + if (events_.size() == count) { + return testing::AssertionSuccess(); + } + return testing::AssertionFailure() + << "Expected " << count << " events, got " << events_.size(); + } + + template + [[nodiscard]] testing::AssertionResult Kind(std::size_t index) const { + return GetIndex(index, [&](auto const& actual) { + if (std::holds_alternative(actual)) { + return testing::AssertionSuccess(); + } else { + return testing::AssertionFailure() + << "Expected message " << index << " to be of kind " + << typeid(T).name() << ", got variant index " + << actual.index(); + } + }); + } + + private: + [[nodiscard]] testing::AssertionResult GetIndex( + std::size_t index, + std::function const& f) const { + if (index >= events_.size()) { + return testing::AssertionFailure() + << "Event index " << index << " out of range"; + } + auto const& record = events_[index]; + return f(record); + } + using Records = std::vector; + Records events_; +}; +} // namespace launchdarkly From 9ec37f6ae1bd7d90a2c95afd74a9fafe20486a54 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 28 Aug 2023 13:36:01 -0700 Subject: [PATCH 43/56] refactor: push typechecking of variation methods deeper down (#216) This removes a bunch of duplicated work from the top-level typed Variation methods, and pushes it into the internal `Variation` method. --- libs/server-sdk/src/client_impl.cpp | 76 +++++++---------------------- libs/server-sdk/src/client_impl.hpp | 18 +++++-- 2 files changed, 33 insertions(+), 61 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 0ffca198b..796bed031 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -281,16 +281,14 @@ void ClientImpl::LogVariationCall(std::string const& key, } Value ClientImpl::Variation(Context const& ctx, + enum Value::Type value_type, IClient::FlagKey const& key, Value const& default_value) { - return *VariationInternal(ctx, key, default_value, events_default_); -} - -EvaluationDetail ClientImpl::VariationDetail( - Context const& ctx, - IClient::FlagKey const& key, - Value const& default_value) { - return VariationInternal(ctx, key, default_value, events_with_reasons_); + auto result = *VariationInternal(ctx, key, default_value, events_default_); + if (result.Type() != value_type) { + return default_value; + } + return result; } EvaluationDetail ClientImpl::VariationInternal( @@ -380,105 +378,67 @@ EvaluationDetail ClientImpl::BoolVariationDetail( Context const& ctx, IClient::FlagKey const& key, bool default_value) { - auto result = VariationDetail(ctx, key, default_value); - if (result.Value().IsBool()) { - return EvaluationDetail{result.Value(), result.VariationIndex(), - result.Reason()}; - } - return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, - default_value}; + return VariationDetail(ctx, Value::Type::kBool, key, default_value); } bool ClientImpl::BoolVariation(Context const& ctx, IClient::FlagKey const& key, bool default_value) { - auto result = Variation(ctx, key, default_value); - if (result.IsBool()) { - return result; - } - return default_value; + return Variation(ctx, Value::Type::kBool, key, default_value); } EvaluationDetail ClientImpl::StringVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, std::string default_value) { - auto result = VariationDetail(ctx, key, default_value); - if (result.Value().IsString()) { - return EvaluationDetail{ - result.Value(), result.VariationIndex(), result.Reason()}; - } - return EvaluationDetail{ - EvaluationReason::ErrorKind::kWrongType, default_value}; + return VariationDetail(ctx, Value::Type::kString, key, + default_value); } std::string ClientImpl::StringVariation(Context const& ctx, IClient::FlagKey const& key, std::string default_value) { - auto result = Variation(ctx, key, default_value); - if (result.IsString()) { - return result; - } - return default_value; + return Variation(ctx, Value::Type::kString, key, default_value); } EvaluationDetail ClientImpl::DoubleVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, double default_value) { - auto result = VariationDetail(ctx, key, default_value); - if (result.Value().IsNumber()) { - return EvaluationDetail{result.Value(), result.VariationIndex(), - result.Reason()}; - } - return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, - default_value}; + return VariationDetail(ctx, Value::Type::kNumber, key, + default_value); } double ClientImpl::DoubleVariation(Context const& ctx, IClient::FlagKey const& key, double default_value) { - auto result = Variation(ctx, key, default_value); - if (result.IsNumber()) { - return result; - } - return default_value; + return Variation(ctx, Value::Type::kNumber, key, default_value); } EvaluationDetail ClientImpl::IntVariationDetail( Context const& ctx, IClient::FlagKey const& key, int default_value) { - auto result = VariationDetail(ctx, key, default_value); - if (result.Value().IsNumber()) { - return EvaluationDetail{result.Value(), result.VariationIndex(), - result.Reason()}; - } - return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, - default_value}; + return VariationDetail(ctx, Value::Type::kNumber, key, default_value); } int ClientImpl::IntVariation(Context const& ctx, IClient::FlagKey const& key, int default_value) { - auto result = Variation(ctx, key, default_value); - if (result.IsNumber()) { - return result; - } - return default_value; + return Variation(ctx, Value::Type::kNumber, key, default_value); } EvaluationDetail ClientImpl::JsonVariationDetail( Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return VariationDetail(ctx, key, default_value); + return VariationInternal(ctx, key, default_value, events_with_reasons_); } Value ClientImpl::JsonVariation(Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return Variation(ctx, key, default_value); + return *VariationInternal(ctx, key, default_value, events_default_); } // data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index aebf2ef7d..6326ae105 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -117,12 +117,24 @@ class ClientImpl : public IClient { Value const& default_value, EventScope const& scope); - [[nodiscard]] EvaluationDetail VariationDetail( + template + [[nodiscard]] EvaluationDetail VariationDetail( Context const& ctx, - FlagKey const& key, - Value const& default_value); + enum Value::Type value_type, + IClient::FlagKey const& key, + Value const& default_value) { + auto result = + VariationInternal(ctx, key, default_value, events_with_reasons_); + if (result.Value().Type() == value_type) { + return EvaluationDetail{result.Value(), result.VariationIndex(), + result.Reason()}; + } + return EvaluationDetail{EvaluationReason::ErrorKind::kWrongType, + default_value}; + } [[nodiscard]] Value Variation(Context const& ctx, + enum Value::Type value_type, std::string const& key, Value const& default_value); From 66b4c83c96f068bef219e96019582d2b37951830 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 29 Aug 2023 16:36:36 -0700 Subject: [PATCH 44/56] fix: deserializing certain values leads to infinite loop (#224) We had an infinite loop where the `Value` tag invoke deserializer was calling the `tl::expected tag_invoke( - boost::json::value_to_tag> const& tag, - boost::json::value const& json_value) { - return boost::json::value_to(json_value); -} - // NOLINTEND modernize-return-braced-init-list } // namespace launchdarkly From eb1b3ed6d2306e64fe2003f50c9426cb8bd40460 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 29 Aug 2023 18:46:05 -0700 Subject: [PATCH 45/56] feat: add DataSourceStatus() and plumb through IClient interface (#217) --- .../include/launchdarkly/server_side/client.hpp | 10 ++++++++++ libs/server-sdk/src/client.cpp | 4 ++++ libs/server-sdk/src/client_impl.cpp | 9 +++++---- libs/server-sdk/src/client_impl.hpp | 6 ++++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index 50a9fda72..bc13bf85c 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -236,6 +237,13 @@ class IClient { FlagKey const& key, Value default_value) = 0; + /** + * Returns an interface which provides methods for subscribing to data + * source status. + * @return A data source status provider. + */ + virtual data_sources::IDataSourceStatusProvider& DataSourceStatus() = 0; + virtual ~IClient() = default; IClient(IClient const& item) = delete; IClient(IClient&& item) = delete; @@ -320,6 +328,8 @@ class Client : public IClient { FlagKey const& key, Value default_value) override; + data_sources::IDataSourceStatusProvider& DataSourceStatus() override; + /** * Returns the version of the SDK. * @return String representing version of the SDK. diff --git a/libs/server-sdk/src/client.cpp b/libs/server-sdk/src/client.cpp index 4a88b054a..ce992de85 100644 --- a/libs/server-sdk/src/client.cpp +++ b/libs/server-sdk/src/client.cpp @@ -124,6 +124,10 @@ EvaluationDetail Client::JsonVariationDetail(Context const& ctx, return client->JsonVariationDetail(ctx, key, std::move(default_value)); } +data_sources::IDataSourceStatusProvider& Client::DataSourceStatus() { + return client->DataSourceStatus(); +} + char const* Client::Version() { return kVersion; } diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 796bed031..fb049a4aa 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -110,6 +110,7 @@ ClientImpl::ClientImpl(Config config, std::string const& version) ioc_(kAsioConcurrencyHint), work_(boost::asio::make_work_guard(ioc_)), memory_store_(), + status_manager_(), data_source_(MakeDataSource(http_properties_, config_, ioc_.get_executor(), @@ -441,10 +442,10 @@ Value ClientImpl::JsonVariation(Context const& ctx, return *VariationInternal(ctx, key, default_value, events_default_); } -// data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { -// return status_manager_; -// } -// +data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { + return status_manager_; +} + // flag_manager::IFlagNotifier& ClientImpl::FlagNotifier() { // return flag_manager_.Notifier(); // } diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 6326ae105..1a357f3b0 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -106,6 +106,8 @@ class ClientImpl : public IClient { FlagKey const& key, Value default_value) override; + data_sources::IDataSourceStatusProvider& DataSourceStatus() override; + ~ClientImpl(); std::future StartAsync() override; @@ -172,6 +174,8 @@ class ClientImpl : public IClient { data_store::MemoryStore memory_store_; + data_sources::DataSourceStatusManager status_manager_; + std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; std::unique_ptr event_processor_; @@ -179,8 +183,6 @@ class ClientImpl : public IClient { mutable std::mutex init_mutex_; std::condition_variable init_waiter_; - data_sources::DataSourceStatusManager status_manager_; - evaluation::Evaluator evaluator_; EventScope const events_default_; From 371ab0099412a0a68ae61a2984fc33f9d2dcb464 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 30 Aug 2023 15:16:25 -0700 Subject: [PATCH 46/56] refactor: move DataSourceStatus ErrorInfo C Bindings into common (#225) This moves all the `_ErrorInfo_` C bindings out of the client-side SDK and into the common library, allowing them to be shared with the server. This should be backwards compatible because the `sdk.h` includes the new `error_info.h`, providing the same symbols as before. --- .../launchdarkly/client_side/bindings/c/sdk.h | 71 +------------------ libs/client-sdk/src/bindings/c/sdk.cpp | 39 ---------- .../bindings/c/data_source/error_info.h | 59 +++++++++++++++ .../bindings/c/data_source/error_kind.h | 52 ++++++++++++++ .../data_source_status_error_info.hpp | 4 +- .../data_source_status_error_kind.hpp | 2 + libs/common/src/CMakeLists.txt | 1 + .../src/bindings/c/data_source/error_info.cpp | 46 ++++++++++++ 8 files changed, 164 insertions(+), 110 deletions(-) create mode 100644 libs/common/include/launchdarkly/bindings/c/data_source/error_info.h create mode 100644 libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h create mode 100644 libs/common/src/bindings/c/data_source/error_info.cpp diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h index 91762e570..90720a0c8 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -447,7 +448,6 @@ LDClientSDK_FlagNotifier_OnFlagChange(LDClientSDK sdk, struct LDFlagListener listener); typedef struct _LDDataSourceStatus* LDDataSourceStatus; -typedef struct _LDDataSourceStatus_ErrorInfo* LDDataSourceStatus_ErrorInfo; /** * Enumeration of possible data source states. @@ -503,40 +503,6 @@ enum LDDataSourceStatus_State { LD_DATASOURCESTATUS_STATE_SHUTDOWN = 4 }; -/** - * A description of an error condition that the data source encountered. - */ -enum LDDataSourceStatus_ErrorKind { - /** - * An unexpected error, such as an uncaught exception, further - * described by the error message. - */ - LD_DATASOURCESTATUS_ERRORKIND_UNKNOWN = 0, - - /** - * An I/O error such as a dropped connection. - */ - LD_DATASOURCESTATUS_ERRORKIND_NETWORK_ERROR = 1, - - /** - * The LaunchDarkly service returned an HTTP response with an error - * status, available in the status code. - */ - LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE = 2, - - /** - * The SDK received malformed data from the LaunchDarkly service. - */ - LD_DATASOURCESTATUS_ERRORKIND_INVALID_DATA = 3, - - /** - * The data source itself is working, but when it tried to put an - * update into the data store, the data store failed (so the SDK may - * not have the latest data). - */ - LD_DATASOURCESTATUS_ERRORKIND_STORE_ERROR = 4, -}; - /** * Get an enumerated value representing the overall current state of the data * source. @@ -583,34 +549,6 @@ LDDataSourceStatus_GetLastError(LDDataSourceStatus status); */ LD_EXPORT(time_t) LDDataSourceStatus_StateSince(LDDataSourceStatus status); -/** - * Get an enumerated value representing the general category of the error. - */ -LD_EXPORT(enum LDDataSourceStatus_ErrorKind) -LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info); - -/** - * The HTTP status code if the error was - * LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE. - */ -LD_EXPORT(uint64_t) -LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info); - -/** - * Any additional human-readable information relevant to the error. - * - * The format is subject to change and should not be relied on - * programmatically. - */ -LD_EXPORT(char const*) -LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info); - -/** - * The date/time that the error occurred, in seconds since epoch. - */ -LD_EXPORT(time_t) -LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info); - typedef void (*DataSourceStatusCallbackFn)(LDDataSourceStatus status, void* user_data); @@ -685,13 +623,6 @@ LDClientSDK_DataSourceStatus_Status(LDClientSDK sdk); */ LD_EXPORT(void) LDDataSourceStatus_Free(LDDataSourceStatus status); -/** - * Frees the data source status error information. - * @param status The error information to free. - */ -LD_EXPORT(void) -LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info); - #ifdef __cplusplus } #endif diff --git a/libs/client-sdk/src/bindings/c/sdk.cpp b/libs/client-sdk/src/bindings/c/sdk.cpp index cb4f7c0e6..a5c2971d1 100644 --- a/libs/client-sdk/src/bindings/c/sdk.cpp +++ b/libs/client-sdk/src/bindings/c/sdk.cpp @@ -24,9 +24,6 @@ struct Detail; launchdarkly::client_side::data_sources::DataSourceStatus*>(ptr)) #define FROM_DATASOURCESTATUS(ptr) (reinterpret_cast(ptr)) -#define TO_DATASOURCESTATUS_ERRORINFO(ptr) \ - (reinterpret_cast(ptr)) #define FROM_DATASOURCESTATUS_ERRORINFO(ptr) \ (reinterpret_cast(ptr)) @@ -377,37 +374,6 @@ LD_EXPORT(time_t) LDDataSourceStatus_StateSince(LDDataSourceStatus status) { .count(); } -LD_EXPORT(LDDataSourceStatus_ErrorKind) -LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return static_cast( - TO_DATASOURCESTATUS_ERRORINFO(info)->Kind()); -} - -LD_EXPORT(uint64_t) -LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return TO_DATASOURCESTATUS_ERRORINFO(info)->StatusCode(); -} - -LD_EXPORT(char const*) -LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return TO_DATASOURCESTATUS_ERRORINFO(info)->Message().c_str(); -} - -LD_EXPORT(time_t) -LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info) { - LD_ASSERT_NOT_NULL(info); - - return std::chrono::duration_cast( - TO_DATASOURCESTATUS_ERRORINFO(info)->Time().time_since_epoch()) - .count(); -} - LD_EXPORT(void) LDDataSourceStatusListener_Init(struct LDDataSourceStatusListener* listener) { listener->StatusChanged = nullptr; @@ -445,10 +411,5 @@ LD_EXPORT(void) LDDataSourceStatus_Free(LDDataSourceStatus status) { delete TO_DATASOURCESTATUS(status); } -LD_EXPORT(void) -LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info) { - delete TO_DATASOURCESTATUS_ERRORINFO(info); -} - // NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast // NOLINTEND OCInconsistentNamingInspection diff --git a/libs/common/include/launchdarkly/bindings/c/data_source/error_info.h b/libs/common/include/launchdarkly/bindings/c/data_source/error_info.h new file mode 100644 index 000000000..acc51f12d --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/data_source/error_info.h @@ -0,0 +1,59 @@ +/** @file error_info.h + * @brief LaunchDarkly Server-side C Bindings for Data Source Error Info. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDDataSourceStatus_ErrorInfo* LDDataSourceStatus_ErrorInfo; + +/** + * Get an enumerated value representing the general category of the error. + */ +LD_EXPORT(enum LDDataSourceStatus_ErrorKind) +LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info); + +/** + * The HTTP status code if the error was + * LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE. + */ +LD_EXPORT(uint64_t) +LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info); + +/** + * Any additional human-readable information relevant to the error. + * + * The format is subject to change and should not be relied on + * programmatically. + */ +LD_EXPORT(char const*) +LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info); + +/** + * The date/time that the error occurred, in seconds since epoch. + */ +LD_EXPORT(time_t) +LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info); + +/** + * Frees the data source status error information. + * @param status The error information to free. + */ +LD_EXPORT(void) +LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h b/libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h new file mode 100644 index 000000000..29524b862 --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/data_source/error_kind.h @@ -0,0 +1,52 @@ +/** @file error_kind.h + * @brief LaunchDarkly Server-side C Bindings for Data Source Error Kinds. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +/** + * A description of an error condition that the data source encountered. + */ +enum LDDataSourceStatus_ErrorKind { + /** + * An unexpected error, such as an uncaught exception, further + * described by the error message. + */ + LD_DATASOURCESTATUS_ERRORKIND_UNKNOWN = 0, + + /** + * An I/O error such as a dropped connection. + */ + LD_DATASOURCESTATUS_ERRORKIND_NETWORK_ERROR = 1, + + /** + * The LaunchDarkly service returned an HTTP response with an error + * status, available in the status code. + */ + LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE = 2, + + /** + * The SDK received malformed data from the LaunchDarkly service. + */ + LD_DATASOURCESTATUS_ERRORKIND_INVALID_DATA = 3, + + /** + * The data source itself is working, but when it tried to put an + * update into the data store, the data store failed (so the SDK may + * not have the latest data). + */ + LD_DATASOURCESTATUS_ERRORKIND_STORE_ERROR = 4, +}; + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp index f7ddfa4d0..c1d6813c7 100644 --- a/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_error_info.hpp @@ -2,7 +2,9 @@ #include +#include #include +#include namespace launchdarkly::common::data_sources { @@ -11,7 +13,7 @@ namespace launchdarkly::common::data_sources { */ class DataSourceStatusErrorInfo { public: - using StatusCodeType = uint64_t; + using StatusCodeType = std::uint64_t; using ErrorKind = DataSourceStatusErrorKind; using DateTime = std::chrono::time_point; diff --git a/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp b/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp index f540d3d65..6adc7d87e 100644 --- a/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp +++ b/libs/common/include/launchdarkly/data_sources/data_source_status_error_kind.hpp @@ -1,5 +1,7 @@ #pragma once +#include + namespace launchdarkly::common::data_sources { /** diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 066fbb4c3..0130ec962 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -49,6 +49,7 @@ add_library(${LIBNAME} OBJECT bindings/c/listener_connection.cpp bindings/c/flag_listener.cpp bindings/c/memory_routines.cpp + bindings/c/data_source/error_info.cpp log_level.cpp config/persistence_builder.cpp config/logging_builder.cpp diff --git a/libs/common/src/bindings/c/data_source/error_info.cpp b/libs/common/src/bindings/c/data_source/error_info.cpp new file mode 100644 index 000000000..9776de89b --- /dev/null +++ b/libs/common/src/bindings/c/data_source/error_info.cpp @@ -0,0 +1,46 @@ +#include + +#include +#include + +using namespace launchdarkly::common; + +#define TO_DATASOURCESTATUS_ERRORINFO(ptr) \ + (reinterpret_cast< \ + launchdarkly::common::data_sources::DataSourceStatusErrorInfo*>(ptr)) + +LD_EXPORT(LDDataSourceStatus_ErrorKind) +LDDataSourceStatus_ErrorInfo_GetKind(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return static_cast( + TO_DATASOURCESTATUS_ERRORINFO(info)->Kind()); +} + +LD_EXPORT(uint64_t) +LDDataSourceStatus_ErrorInfo_StatusCode(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return TO_DATASOURCESTATUS_ERRORINFO(info)->StatusCode(); +} + +LD_EXPORT(char const*) +LDDataSourceStatus_ErrorInfo_Message(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return TO_DATASOURCESTATUS_ERRORINFO(info)->Message().c_str(); +} + +LD_EXPORT(time_t) +LDDataSourceStatus_ErrorInfo_Time(LDDataSourceStatus_ErrorInfo info) { + LD_ASSERT_NOT_NULL(info); + + return std::chrono::duration_cast( + TO_DATASOURCESTATUS_ERRORINFO(info)->Time().time_since_epoch()) + .count(); +} + +LD_EXPORT(void) +LDDataSourceStatus_ErrorInfo_Free(LDDataSourceStatus_ErrorInfo info) { + delete TO_DATASOURCESTATUS_ERRORINFO(info); +} From 61ddbe7d09fd1b7ff2d64b048ca226dbbb965dd4 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 30 Aug 2023 16:35:52 -0700 Subject: [PATCH 47/56] add Doxygen config and doc.md --- libs/server-sdk/Doxyfile | 94 +++++++++++++++++++++++++++++++++++++ libs/server-sdk/docs/doc.md | 9 ++++ 2 files changed, 103 insertions(+) create mode 100644 libs/server-sdk/Doxyfile create mode 100644 libs/server-sdk/docs/doc.md diff --git a/libs/server-sdk/Doxyfile b/libs/server-sdk/Doxyfile new file mode 100644 index 000000000..c8ba8bd90 --- /dev/null +++ b/libs/server-sdk/Doxyfile @@ -0,0 +1,94 @@ +# Doxyfile 1.8.17 + +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). + +#--------------------------------------------------------------------------- +# Project related configuration options +#--------------------------------------------------------------------------- + +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. +# The default value is: UTF-8. + +DOXYFILE_ENCODING = UTF-8 + +# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by +# double-quotes, unless you are using Doxywizard) that should identify the +# project for which the documentation is generated. This name is used in the +# title of most generated pages and in a few other places. +# The default value is: My Project. + +PROJECT_NAME = "C++ Server-Side SDK" + +# Using the PROJECT_BRIEF tag one can provide an optional one line description +# for a project that appears at the top of each page and should give viewer a +# quick idea about the purpose of the project. Keep the description short. + +PROJECT_BRIEF = "LaunchDarkly SDK" + +# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path +# into which the generated documentation will be written. If a relative path is +# entered, it will be relative to the location where doxygen was started. If +# left blank the current directory will be used. + +OUTPUT_DIRECTORY = docs + +# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want +# to include (a tag file for) the STL sources as input, then you should set this +# tag to YES in order to let doxygen match functions declarations and +# definitions whose arguments contain STL classes (e.g. func(std::string); +# versus func(std::string) {}). This also make the inheritance and collaboration +# diagrams that involve STL classes more complete and accurate. +# The default value is: NO. + +BUILTIN_STL_SUPPORT = YES + +# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or +# enum is documented as struct, union, or enum with the name of the typedef. So +# typedef struct TypeS {} TypeT, will appear in the documentation as a struct +# with name TypeT. When disabled the typedef will appear as a member of a file, +# namespace, or class. And the struct will be named TypeS. This can typically be +# useful for C code in case the coding convention dictates that all compound +# types are typedef'ed and only the typedef is referenced, never the tag name. +# The default value is: NO. + +TYPEDEF_HIDES_STRUCT = YES + +#--------------------------------------------------------------------------- +# Configuration options related to the input files +#--------------------------------------------------------------------------- + +# The INPUT tag is used to specify the files and/or directories that contain +# documented source files. You may enter file names like myfile.cpp or +# directories like /usr/src/myproject. Separate the files or directories with +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING +# Note: If this tag is empty the current directory is searched. + +INPUT = include src docs ../common/include ../common/src + +# The RECURSIVE tag can be used to specify whether or not subdirectories should +# be searched for input files as well. +# The default value is: NO. + +RECURSIVE = YES + +# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that +# is part of the input, its contents will be placed on the main page +# (index.html). This can be useful if you have a project on for instance GitHub +# and want to reuse the introduction page also for the doxygen output. + +USE_MDFILE_AS_MAINPAGE = ./docs/doc.md diff --git a/libs/server-sdk/docs/doc.md b/libs/server-sdk/docs/doc.md new file mode 100644 index 000000000..fbc1d6a9f --- /dev/null +++ b/libs/server-sdk/docs/doc.md @@ -0,0 +1,9 @@ +# SDK Layout and Overview + +## Basic Functionality + +The following pages document the core of the API, every application will use these portions of the SDK: + +- [Client](@ref launchdarkly::server_side::Client) +- [Config Builder](@ref launchdarkly::config::shared::builders::ConfigBuilder) +- [Context Builder](@ref launchdarkly::ContextBuilder) From aba24732fb99843a792f5f1fafd7f5072446466c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 31 Aug 2023 10:08:54 -0700 Subject: [PATCH 48/56] chore: add Server-side README (#226) Adds a new README for the server. Also adds a link from the client-side README to this one. --------- Co-authored-by: Molly --- libs/client-sdk/README.md | 2 + libs/server-sdk/README.md | 121 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 libs/server-sdk/README.md diff --git a/libs/client-sdk/README.md b/libs/client-sdk/README.md index 7cfc8db42..b4e8eab9b 100644 --- a/libs/client-sdk/README.md +++ b/libs/client-sdk/README.md @@ -8,6 +8,8 @@ The LaunchDarkly Client-Side SDK for C/C++ is designed primarily for use in desk It follows the client-side LaunchDarkly model for single-user contexts (much like our mobile or JavaScript SDKs). It is not intended for use in multi-user systems such as web servers and applications. +For using LaunchDarkly in server-side C/C++ applications, refer to our [Server-Side C/C++ SDK](../server-sdk/README.md). + LaunchDarkly overview ------------------------- [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags diff --git a/libs/server-sdk/README.md b/libs/server-sdk/README.md new file mode 100644 index 000000000..25ad3bf40 --- /dev/null +++ b/libs/server-sdk/README.md @@ -0,0 +1,121 @@ +LaunchDarkly Server-Side SDK for C/C++ +=================================== + +[![Actions Status](https://github.com/launchdarkly/cpp-sdks/actions/workflows/server.yml/badge.svg)](https://github.com/launchdarkly/cpp-sdks/actions/workflows/server.yml) +[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/cpp-sdks/libs/server-sdk/docs/html/) + +The LaunchDarkly Server-Side SDK for C/C++ is designed primarily for use in multi-user systems such as web servers +and applications. It follows the server-side LaunchDarkly model for multi-user contexts. +It is not intended for use in desktop and embedded systems applications. + +For using LaunchDarkly in client-side C/C++ applications, refer to our [Client-Side C/C++ SDK](../client-sdk/README.md). + +LaunchDarkly overview +------------------------- +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags +daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) +using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) + +Compatibility +------------------------- + +This version of the LaunchDarkly SDK is compatible with POSIX environments (Linux, OS X, BSD) and Windows. + +Getting started +--------------- + +Download a release archive from +the [Github releases](https://github.com/launchdarkly/cpp-sdks/releases?q=cpp-server&expanded=true) for use in your +project. + +Refer to the [SDK documentation][reference-guide] for complete instructions on +installing and using the SDK. + +### Incorporating the SDK + +The SDK can be used via a C++ or C interface and can be incorporated via a static library or shared object. The static +library and shared object each have their own use cases and limitations. + +The static library supports both the C++ and C interface. When using the static library, you should ensure that it is +compiled using a compatible configuration and toolchain. For instance, when using MSVC, it needs to be using the same +runtime library. + +Using the static library also requires that you have OpenSSL and Boost available at the time of compilation for your +project. + +The C++ API does not have a stable ABI, so if this is important to you, consider using the shared object with the C API. + +Example of basic compilation using the C++ API with a static library using gcc: + +```shell +g++ -I path_to_the_sdk_install/include -O3 -std=c++17 -Llib -fPIE -g main.cpp path_to_the_sdk_install/lib/liblaunchdarkly-cpp-server.a -lpthread -lstdc++ -lcrypto -lssl -lboost_json -lboost_url +``` + +Example of basic compilation using the C API with a static library using msvc: + +```shell +cl /I include /Fe: hello.exe main.cpp /link lib/launchdarkly-cpp-server.lib +``` + +The shared library (so, DLL, dylib), only supports the C interface. The shared object does not require you to have Boost +or OpenSSL available when linking the shared object to your project. + +Example of basic compilation using the C API with a shared library using gcc: + +```shell +gcc -I $(pwd)/include -Llib -fPIE -g main.c liblaunchdarkly-cpp-server.so +``` + +The examples here are to help with getting started, but generally speaking the SDK should be incorporated using your +build system (CMake for instance). + +Learn more +----------- + +Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. +You can also head straight to +the [complete reference guide for this SDK][reference-guide]. + +Testing +------- + +We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test +for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each +method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all +behave correctly. + +Contributing +------------ + +We encourage pull requests and other contributions from the community. Read +our [contributing guidelines](../../CONTRIBUTING.md) for instructions on how to contribute to this SDK. + +About LaunchDarkly +----------- + +* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to + iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. + With LaunchDarkly, you can: + * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), + gathering feedback and bug reports from real-world use cases. + * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on + key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, + or even restart the application with a changed configuration file. + * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get + access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate + maintenance, without taking everything offline. +* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. + Read [our documentation](https://docs.launchdarkly.com/docs) for a complete list. +* Explore LaunchDarkly + * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information + * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and + SDK reference guides + * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API + documentation + * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product + updates + +[reference-guide]: https://docs.launchdarkly.com/sdk/server-side/c-c-- From 1538a768cf93fcb1a3232a2ef8cf5fd377a52cd7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 31 Aug 2023 13:01:35 -0700 Subject: [PATCH 49/56] fix: ensure flag updates go through data store updater (#228) I hadn't wired up the `DataStoreUpdater` component, so we were just inserting flag updates directly in the memory store. With this in place, we can remove the streaming update suppression. --- .../server-contract-tests/test-suppressions.txt | 10 ---------- libs/server-sdk/src/client_impl.cpp | 3 ++- libs/server-sdk/src/client_impl.hpp | 2 ++ 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt index 41ec5ec4d..4de6fa075 100644 --- a/contract-tests/server-contract-tests/test-suppressions.txt +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -1,13 +1,3 @@ -streaming/updates/flag patch with same version is not applied -streaming/updates/segment patch with same version is not applied -streaming/updates/flag patch with lower version is not applied -streaming/updates/segment patch with lower version is not applied -streaming/updates/flag delete with same version is not applied -streaming/updates/segment delete with same version is not applied -streaming/updates/flag delete with lower version is not applied -streaming/updates/segment delete with lower version is not applied -streaming/updates/flag delete for previously nonexistent flag is applied -streaming/updates/segment delete for previously nonexistent segment is applied streaming/retry behavior/retry after IO error on reconnect streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 400 streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 408 diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index fb049a4aa..5d3f9c149 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -111,10 +111,11 @@ ClientImpl::ClientImpl(Config config, std::string const& version) work_(boost::asio::make_work_guard(ioc_)), memory_store_(), status_manager_(), + data_store_updater_(memory_store_, memory_store_), data_source_(MakeDataSource(http_properties_, config_, ioc_.get_executor(), - memory_store_, + data_store_updater_, status_manager_, logger_)), event_processor_(MakeEventProcessor(config, diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 1a357f3b0..d6c77ecee 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -13,6 +13,7 @@ #include "data_sources/data_source_status_manager.hpp" #include "data_sources/data_source_update_sink.hpp" +#include "data_store/data_store_updater.hpp" #include "data_store/memory_store.hpp" #include "evaluation/evaluator.hpp" @@ -175,6 +176,7 @@ class ClientImpl : public IClient { data_store::MemoryStore memory_store_; data_sources::DataSourceStatusManager status_manager_; + data_store::DataStoreUpdater data_store_updater_; std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; From 3669ff54add48cd4d8feb67ba2f4bfe6edcb73f3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 31 Aug 2023 16:26:02 -0700 Subject: [PATCH 50/56] feat: server-side C bindings (#210) Implements a set of C bindings for the server-side SDK. Missing are flag notifier bindings, but this isn't implemented in the C++ side yet either. --- examples/CMakeLists.txt | 3 + .../CMakeLists.txt | 15 + examples/client-and-server-coexistence/main.c | 46 ++ examples/hello-c-client/main.c | 4 +- examples/hello-c-server/CMakeLists.txt | 15 + examples/hello-c-server/main.c | 78 +++ examples/hello-cpp-server/main.cpp | 12 +- .../client_side/bindings/c/config/config.h | 4 +- .../launchdarkly/client_side/bindings/c/sdk.h | 4 +- libs/client-sdk/src/bindings/c/sdk.cpp | 1 - .../tests/client_c_bindings_test.cpp | 2 +- ...ared_function_argument_macro_definitions.h | 8 + .../include/launchdarkly/config/client.hpp | 2 - .../include/launchdarkly/config/server.hpp | 1 + .../c/all_flags_state/all_flags_state.h | 116 ++++ .../server_side/bindings/c/config/builder.h | 346 +++++++++++ .../server_side/bindings/c/config/config.h | 26 + .../launchdarkly/server_side/bindings/c/sdk.h | 581 ++++++++++++++++++ .../launchdarkly/server_side/client.hpp | 16 +- .../server_side/data_source_status.hpp | 3 +- libs/server-sdk/src/CMakeLists.txt | 10 +- .../c/all_flags_state/all_flags_state.cpp | 56 ++ libs/server-sdk/src/bindings/c/builder.cpp | 289 +++++++++ libs/server-sdk/src/bindings/c/config.cpp | 16 + libs/server-sdk/src/bindings/c/sdk.cpp | 414 +++++++++++++ .../tests/server_c_bindings_test.cpp | 161 +++++ 26 files changed, 2205 insertions(+), 24 deletions(-) create mode 100644 examples/client-and-server-coexistence/CMakeLists.txt create mode 100644 examples/client-and-server-coexistence/main.c create mode 100644 examples/hello-c-server/CMakeLists.txt create mode 100644 examples/hello-c-server/main.c create mode 100644 libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h create mode 100644 libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h create mode 100644 libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp create mode 100644 libs/server-sdk/src/bindings/c/builder.cpp create mode 100644 libs/server-sdk/src/bindings/c/config.cpp create mode 100644 libs/server-sdk/src/bindings/c/sdk.cpp create mode 100644 libs/server-sdk/tests/server_c_bindings_test.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index ea3483962..be37f398d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,3 +1,6 @@ add_subdirectory(hello-c-client) add_subdirectory(hello-cpp-client) add_subdirectory(hello-cpp-server) +add_subdirectory(hello-c-server) + +add_subdirectory(client-and-server-coexistence) diff --git a/examples/client-and-server-coexistence/CMakeLists.txt b/examples/client-and-server-coexistence/CMakeLists.txt new file mode 100644 index 000000000..ceb971190 --- /dev/null +++ b/examples/client-and-server-coexistence/CMakeLists.txt @@ -0,0 +1,15 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyCClientAndServerCoexistence + VERSION 0.1 + DESCRIPTION "LaunchDarkly C Client-side and Server-side SDK coexistence in same application" + LANGUAGES C +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(c-client-and-server main.c) +target_link_libraries(c-client-and-server PRIVATE launchdarkly::client launchdarkly::server launchdarkly::sse launchdarkly::common Threads::Threads) diff --git a/examples/client-and-server-coexistence/main.c b/examples/client-and-server-coexistence/main.c new file mode 100644 index 000000000..88c7190ab --- /dev/null +++ b/examples/client-and-server-coexistence/main.c @@ -0,0 +1,46 @@ +/** + * This application intends to verify that the symbols from the + * client-side and server-side SDKs do not clash, thus enabling both SDKs to be + * used within the same application. + */ + +#include +#include + +#include +#include + +#include + +int main() { + LDContextBuilder context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "example-user-key"); + LDContextBuilder_Attributes_SetName(context_builder, "user", "Sandy"); + LDContext context = LDContextBuilder_Build(context_builder); + + LDClientConfigBuilder client_config_builder = + LDClientConfigBuilder_New("foo"); + LDClientConfig client_config = NULL; + + LDStatus client_config_status = + LDClientConfigBuilder_Build(client_config_builder, &client_config); + + if (LDStatus_Ok(client_config_status)) { + LDClientSDK client_sdk = LDClientSDK_New(client_config, context); + LDClientSDK_Free(client_sdk); + } + + LDServerConfigBuilder server_config_builder = + LDServerConfigBuilder_New("foo"); + LDServerConfig server_config = NULL; + + LDStatus server_config_status = + LDServerConfigBuilder_Build(server_config_builder, &server_config); + + if (LDStatus_Ok(server_config_status)) { + LDServerSDK server_sdk = LDServerSDK_New(server_config); + LDServerSDK_Free(server_sdk); + } + + return 0; +} diff --git a/examples/hello-c-client/main.c b/examples/hello-c-client/main.c index b826ffe27..4915ff191 100644 --- a/examples/hello-c-client/main.c +++ b/examples/hello-c-client/main.c @@ -29,7 +29,7 @@ int main() { LDClientConfigBuilder config_builder = LDClientConfigBuilder_New(MOBILE_KEY); - LDClientConfig config; + LDClientConfig config = NULL; LDStatus config_status = LDClientConfigBuilder_Build(config_builder, &config); if (!LDStatus_Ok(config_status)) { @@ -44,7 +44,7 @@ int main() { LDClientSDK client = LDClientSDK_New(config, context); - bool initialized_successfully; + bool initialized_successfully = false; if (LDClientSDK_Start(client, INIT_TIMEOUT_MILLISECONDS, &initialized_successfully)) { if (initialized_successfully) { diff --git a/examples/hello-c-server/CMakeLists.txt b/examples/hello-c-server/CMakeLists.txt new file mode 100644 index 000000000..edd7e0c3e --- /dev/null +++ b/examples/hello-c-server/CMakeLists.txt @@ -0,0 +1,15 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyHelloCServer + VERSION 0.1 + DESCRIPTION "LaunchDarkly Hello C Server-side SDK" + LANGUAGES C +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(hello-c-server main.c) +target_link_libraries(hello-c-server PRIVATE launchdarkly::server launchdarkly::sse launchdarkly::common Threads::Threads) diff --git a/examples/hello-c-server/main.c b/examples/hello-c-server/main.c new file mode 100644 index 000000000..4b3f68fcd --- /dev/null +++ b/examples/hello-c-server/main.c @@ -0,0 +1,78 @@ +#include +#include + +#include + +#include +#include +#include +#include + +// Set SDK_KEY to your LaunchDarkly SKD key. +#define SDK_KEY "" + +// Set FEATURE_FLAG_KEY to the feature flag key you want to evaluate. +#define FEATURE_FLAG_KEY "my-boolean-flag" + +// Set INIT_TIMEOUT_MILLISECONDS to the amount of time you will wait for +// the client to become initialized. +#define INIT_TIMEOUT_MILLISECONDS 3000 + +int main() { + if (!strlen(SDK_KEY)) { + printf( + "*** Please edit main.c to set SDK_KEY to your LaunchDarkly " + "SDK key first\n\n"); + return 1; + } + + LDServerConfigBuilder config_builder = LDServerConfigBuilder_New(SDK_KEY); + + LDServerConfig config = NULL; + LDStatus config_status = + LDServerConfigBuilder_Build(config_builder, &config); + if (!LDStatus_Ok(config_status)) { + printf("error: config is invalid: %s", LDStatus_Error(config_status)); + return 1; + } + + LDServerSDK client = LDServerSDK_New(config); + + bool initialized_successfully = false; + if (LDServerSDK_Start(client, INIT_TIMEOUT_MILLISECONDS, + &initialized_successfully)) { + if (initialized_successfully) { + printf("*** SDK successfully initialized!\n\n"); + } else { + printf("*** SDK failed to initialize\n"); + return 1; + } + } else { + printf("SDK initialization didn't complete in %dms\n", + INIT_TIMEOUT_MILLISECONDS); + return 1; + } + + LDContextBuilder context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "example-user-key"); + LDContextBuilder_Attributes_SetName(context_builder, "user", "Sandy"); + LDContext context = LDContextBuilder_Build(context_builder); + + bool flag_value = + LDServerSDK_BoolVariation(client, context, FEATURE_FLAG_KEY, false); + + printf("*** Feature flag '%s' is %s for this user\n\n", FEATURE_FLAG_KEY, + flag_value ? "true" : "false"); + + // Here we ensure that the SDK shuts down cleanly and has a chance to + // deliver analytics events to LaunchDarkly before the program exits. If + // analytics events are not delivered, the user properties and flag usage + // statistics will not appear on your dashboard. In a normal long-running + // application, the SDK would continue running and events would be delivered + // automatically in the background. + + LDContext_Free(context); + LDServerSDK_Free(client); + + return 0; +} diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index d11e59655..1f975ded5 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -4,8 +4,8 @@ #include #include -// Set MOBILE_KEY to your LaunchDarkly mobile key. -#define MOBILE_KEY "" +// Set SDK_KEY to your LaunchDarkly SDK key. +#define SDK_KEY "" // Set FEATURE_FLAG_KEY to the feature flag key you want to evaluate. #define FEATURE_FLAG_KEY "my-boolean-flag" @@ -16,14 +16,14 @@ using namespace launchdarkly; int main() { - if (!strlen(MOBILE_KEY)) { + if (!strlen(SDK_KEY)) { printf( - "*** Please edit main.cpp to set MOBILE_KEY to your LaunchDarkly " - "mobile key first\n\n"); + "*** Please edit main.cpp to set SDK_KEY to your LaunchDarkly " + "SDK key first\n\n"); return 1; } - auto config = server_side::ConfigBuilder(MOBILE_KEY).Build(); + auto config = server_side::ConfigBuilder(SDK_KEY).Build(); if (!config) { std::cout << "error: config is invalid: " << config.error() << '\n'; return 1; diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h index bb13b4dcd..1b8c154e4 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/config/config.h @@ -14,8 +14,8 @@ extern "C" { // only need to export C interface if typedef struct _LDClientConfig* LDClientConfig; /** - * Frees an unused configuration. Configurations passed into an LDClient must - * not be be freed. + * Free an unused configuration. Configurations used to construct an LDClientSDK + * must not be be freed. * * @param config Config to free. */ diff --git a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h index 90720a0c8..82c886603 100644 --- a/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h +++ b/libs/client-sdk/include/launchdarkly/client_side/bindings/c/sdk.h @@ -13,6 +13,7 @@ #include #include #include +#include #include #include @@ -27,9 +28,6 @@ extern "C" { // only need to export C interface if typedef struct _LDClientSDK* LDClientSDK; -#define LD_NONBLOCKING 0 -#define LD_DISCARD_DETAIL NULL - /** * Constructs a new client-side LaunchDarkly SDK from a configuration and * context. diff --git a/libs/client-sdk/src/bindings/c/sdk.cpp b/libs/client-sdk/src/bindings/c/sdk.cpp index a5c2971d1..da070b98e 100644 --- a/libs/client-sdk/src/bindings/c/sdk.cpp +++ b/libs/client-sdk/src/bindings/c/sdk.cpp @@ -203,7 +203,6 @@ LDClientSDK_StringVariation(LDClientSDK sdk, LD_ASSERT_NOT_NULL(flag_key); LD_ASSERT_NOT_NULL(default_value); - // TODO: custom allocation / free routines return strdup( TO_SDK(sdk)->StringVariation(flag_key, default_value).c_str()); } diff --git a/libs/client-sdk/tests/client_c_bindings_test.cpp b/libs/client-sdk/tests/client_c_bindings_test.cpp index cc48fb1e1..66d0e5272 100644 --- a/libs/client-sdk/tests/client_c_bindings_test.cpp +++ b/libs/client-sdk/tests/client_c_bindings_test.cpp @@ -49,7 +49,7 @@ TEST(ClientBindings, RegisterFlagListener) { ASSERT_TRUE(LDStatus_Ok(status)); LDContextBuilder ctx_builder = LDContextBuilder_New(); - LDContextBuilder_AddKind(ctx_builder, "`user", "shadow"); + LDContextBuilder_AddKind(ctx_builder, "user", "shadow"); LDContext context = LDContextBuilder_Build(ctx_builder); diff --git a/libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h b/libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h new file mode 100644 index 000000000..c97662584 --- /dev/null +++ b/libs/common/include/launchdarkly/bindings/c/shared_function_argument_macro_definitions.h @@ -0,0 +1,8 @@ +/** @file */ +#pragma once + +/* Function should operate asynchronously. */ +#define LD_NONBLOCKING 0 + +/* Function should discard evaluation details. */ +#define LD_DISCARD_DETAIL NULL diff --git a/libs/common/include/launchdarkly/config/client.hpp b/libs/common/include/launchdarkly/config/client.hpp index f218569d3..516c4b85b 100644 --- a/libs/common/include/launchdarkly/config/client.hpp +++ b/libs/common/include/launchdarkly/config/client.hpp @@ -15,9 +15,7 @@ using SDK = config::shared::ClientSDK; using Defaults = config::shared::Defaults; using AppInfoBuilder = config::shared::builders::AppInfoBuilder; using EndpointsBuilder = config::shared::builders::EndpointsBuilder; - using ConfigBuilder = config::shared::builders::ConfigBuilder; - using EventsBuilder = config::shared::builders::EventsBuilder; using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; diff --git a/libs/common/include/launchdarkly/config/server.hpp b/libs/common/include/launchdarkly/config/server.hpp index 960d103cc..d284983f1 100644 --- a/libs/common/include/launchdarkly/config/server.hpp +++ b/libs/common/include/launchdarkly/config/server.hpp @@ -19,6 +19,7 @@ using EventsBuilder = config::shared::builders::EventsBuilder; using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; using DataSourceBuilder = config::shared::builders::DataSourceBuilder; +using LoggingBuilder = config::shared::builders::LoggingBuilder; using PersistenceBuilder = config::shared::builders::PersistenceBuilder; using Config = config::Config; diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h new file mode 100644 index 000000000..ab8d42af9 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/all_flags_state/all_flags_state.h @@ -0,0 +1,116 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDAllFlagsState* LDAllFlagsState; + +/** + * Frees an AllFlagsState. + * @param state The AllFlagState to free. + */ +LD_EXPORT(void) LDAllFlagsState_Free(LDAllFlagsState state); + +/** + * True if the LDAllFlagsState is valid. False if there was + * an error, such as the data store being unavailable. + * + * An invalid LDAllFlagsState can still be serialized successfully to a JSON + * string. + * + * @param state The LDAllFlagState to check for validity. Must not be NULL. + * @return True if the state is valid, false otherwise. + */ +LD_EXPORT(bool) LDAllFlagsState_Valid(LDAllFlagsState state); + +/** + * Serializes the LDAllFlagsState to a JSON string. + * + * This JSON is suitable for bootstrapping a client-side SDK. + * + * @param state The LDAllFlagState to serialize. Must not be NULL. + * @return A JSON string representing the LDAllFlagsState. The caller must free + * the string using LDMemory_FreeString. + */ +LD_EXPORT(char*) +LDAllFlagsState_SerializeJSON(LDAllFlagsState state); + +/** + * Returns the flag value for the context used to generate this + * LDAllFlagsState. + * + * In order to avoid copying when a large value is accessed, + * the returned LDValue is a reference and NOT DIRECTLY OWNED by the caller. Its + * lifetime is managed by the parent LDAllFlagsState object. + * + * WARNING! + * Do not free the returned LDValue. + * Do not in any way access the returned LDValue after the LDAllFlagsState has + * been freed. + * + * If the flag has no value, returns an LDValue of type LDValueType_Null. + * + * To obtain a caller-owned copy of the LDValue not subject to these + * restrictions, call LDValue_NewValue on the result. + * + * @param state An LDAllFlagsState. Must not be NULL. + * @param flag_key Key of the flag. Must not be NULL. + * @return The evaluation result of the flag. The caller MUST NOT free this + * value and MUST NOT access this value after the LDAllFlagsState has been + * freed. + */ +LD_EXPORT(LDValue) +LDAllFlagsState_Value(LDAllFlagsState state, char const* flag_key); + +/** + * Defines options that may be used with LDServerSDK_AllFlagsState. To + * obtain default behavior, pass LD_ALLFLAGSSTATE_DEFAULT. + * + * It is possible to combine multiple options by ORing them together. + * + * Example: + * @code + * LDAllFlagsState state = LDServerSDK_AllFlagsState(sdk, context, + * LD_ALLFLAGSSTATE_INCLUDE_REASONS | LD_ALLFLAGSSTATE_CLIENT_SIDE_ONLY + * ); + * @endcode + */ +enum LDAllFlagsState_Options { + /** + * Default behavior. + */ + LD_ALLFLAGSSTATE_DEFAULT = 0, + /** + * Include evaluation reasons in the state object. By default, they + * are not. + */ + LD_ALLFLAGSSTATE_INCLUDE_REASONS = (1 << 0), + /** + * Include detailed flag metadata only for flags with event tracking + * or debugging turned on. + * + * This reduces the size of the JSON data if you are + * passing the flag state to the front end. + */ + LD_ALLFLAGSSTATE_DETAILS_ONLY_FOR_TRACKED_FLAGS = (1 << 1), + /** + * Include only flags marked for use with the client-side SDK. + * By default, all flags are included. + */ + LD_ALLFLAGSSTATE_CLIENT_SIDE_ONLY = (1 << 2) +}; + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h new file mode 100644 index 000000000..6e7b3e8ca --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h @@ -0,0 +1,346 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include + +#include +#include +#include + +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDServerConfigBuilder* LDServerConfigBuilder; +typedef struct _LDServerDataSourceStreamBuilder* + LDServerDataSourceStreamBuilder; +typedef struct _LDServerDataSourcePollBuilder* LDServerDataSourcePollBuilder; + +/** + * Constructs a client-side config builder. + */ +LD_EXPORT(LDServerConfigBuilder) LDServerConfigBuilder_New(char const* sdk_key); + +/** + * Sets a custom URL for the polling service. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(LDServerConfigBuilder b, + char const* url); +/** + * Sets a custom URL for the streaming service. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(LDServerConfigBuilder b, + char const* url); +/** + * Sets a custom URL for the events service. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(LDServerConfigBuilder b, + char const* url); +/** + * Sets a custom URL for a Relay Proxy instance. The streaming, + * polling, and events URLs are set automatically. + * @param b Client config builder. Must not be NULL. + * @param url Target URL. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( + LDServerConfigBuilder b, + char const* url); + +/** + * Sets an identifier for the application. + * @param b Client config builder. Must not be NULL. + * @param app_id Non-empty string. Must be <= 64 chars. Must be alphanumeric, + * '-', '.', or '_'. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Identifier(LDServerConfigBuilder b, + char const* app_id); + +/** + * Sets a version for the application. + * @param b Client config builder. Must not be NULL. + * @param app_version Non-empty string. Must be <= 64 chars. Must be + * alphanumeric, + * '-', '.', or '_'. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, + char const* app_version); + +/** + * Enables or disables "Offline" mode. True means + * Offline mode is enabled. + * @param b Client config builder. Must not be NULL. + * @param offline True if offline. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Offline(LDServerConfigBuilder b, bool offline); + +/** + * Specify if event-sending should be enabled or not. By default, + * events are enabled. + * @param b Client config builder. Must not be NULL. + * @param enabled True to enable event-sending. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_Enabled(LDServerConfigBuilder b, bool enabled); + +/** + * Sets the capacity of the event processor. When more events are generated + * within the processor's flush interval than this value, events will be + * dropped. + * @param b Client config builder. Must not be NULL. + * @param capacity Event queue capacity. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_Capacity(LDServerConfigBuilder b, size_t capacity); + +/** + * Sets the flush interval of the event processor. The processor queues + * outgoing events based on the capacity parameter; these events are then + * delivered based on the flush interval. + * @param b Client config builder. Must not be NULL. + * @param milliseconds Interval between automatic flushes. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_FlushIntervalMs(LDServerConfigBuilder b, + unsigned int milliseconds); + +/** + * Attribute privacy indicates whether or not attributes should be + * retained by LaunchDarkly after being sent upon initialization, + * and if attributes should later be sent in events. + * + * Attribute privacy may be specified in 3 ways: + * + * (1) To specify that all attributes should be considered private - not + * just those designated private on a per-context basis - call this method + * with true as the parameter. + * + * (2) To specify that a specific set of attributes should be considered + * private - in addition to those designated private on a per-context basis + * - call @ref PrivateAttribute. + * + * (3) To specify private attributes on a per-context basis, it is not + * necessary to call either of these methods, as the default behavior is to + * treat all attributes as non-private unless otherwise specified. + * + * @param b Client config builder. Must not be NULL. + * @param all_attributes_private True for behavior of (1), false for default + * behavior of (2) or (3). + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_AllAttributesPrivate(LDServerConfigBuilder b, + bool all_attributes_private); + +/** + * Specifies a single private attribute. May be called multiple times + * with additional private attributes. + * @param b Client config builder. Must not be NULL. + * @param attribute_reference Attribute to mark private. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, + char const* attribute_reference); + +/** + * Set the streaming configuration for the builder. + * + * A data source may either be streaming or polling. Setting a streaming + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param b Client config builder. Must not be NULL. + * @param stream_builder The streaming builder. The builder is consumed; do not + * free it. + */ +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodStream( + LDServerConfigBuilder b, + LDServerDataSourceStreamBuilder stream_builder); + +/** + * Set the polling configuration for the builder. + * + * A data source may either be streaming or polling. Setting a stream + * builder indicates the data source will use streaming. Setting a polling + * builder will indicate the use of polling. + * + * @param b Client config builder. Must not be NULL. + * @param poll_builder The polling builder. The builder is consumed; do not free + * it. + */ +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodPoll( + LDServerConfigBuilder b, + LDServerDataSourcePollBuilder poll_builder); + +/** + * Creates a new DataSource builder for the Streaming method. + * + * If not passed into the config + * builder, must be manually freed with LDServerDataSourceStreamBuilder_Free. + * + * @return New builder for Streaming method. + */ +LD_EXPORT(LDServerDataSourceStreamBuilder) +LDServerDataSourceStreamBuilder_New(); + +/** + * Sets the initial reconnect delay for the streaming connection. + * + * The streaming service uses a backoff algorithm (with jitter) every time + * the connection needs to be reestablished.The delay for the first + * reconnection will start near this value, and then increase exponentially + * for any subsequent connection failures. + * + * @param b Streaming method builder. Must not be NULL. + * @param milliseconds Initial delay for a reconnection attempt. + */ +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( + LDServerDataSourceStreamBuilder b, + unsigned int milliseconds); + +/** + * Frees a Streaming method builder. Do not call if the builder was consumed by + * the config builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b); + +/** + * Creates a new DataSource builder for the Polling method. + * + * If not passed into the config + * builder, must be manually freed with LDServerDataSourcePollBuilder_Free. + * + * @return New builder for Polling method. + */ + +LD_EXPORT(LDServerDataSourcePollBuilder) +LDServerDataSourcePollBuilder_New(); + +/** + * Sets the interval at which the SDK will poll for feature flag updates. + * @param b Polling method builder. Must not be NULL. + * @param milliseconds Polling interval. + */ +LD_EXPORT(void) +LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, + unsigned int seconds); + +/** + * Frees a Polling method builder. Do not call if the builder was consumed by + * the config builder. + * + * @param b Builder to free. + */ +LD_EXPORT(void) +LDServerDataSourcePollBuilder_Free(LDServerDataSourcePollBuilder b); + +/** + * This should be used for wrapper SDKs to set the wrapper name. + * + * Wrapper information will be included in request headers. + * @param b Client config builder. Must not be NULL. + * @param wrapper_name Name of the wrapper. + */ +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperName(LDServerConfigBuilder b, + char const* wrapper_name); + +/** + * This should be used for wrapper SDKs to set the wrapper version. + * + * Wrapper information will be included in request headers. + * @param b Client config builder. Must not be NULL. + * @param wrapper_version Version of the wrapper. + */ +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperVersion( + LDServerConfigBuilder b, + char const* wrapper_version); + +/** + * Set a custom header value. May be called more than once with additional + * headers. + * + * @param b Client config builder. Must not be NULL. + * @param key Name of the header. Must not be NULL. + * @param value Value of the header. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, + char const* key, + char const* value); + +/** + * Disables the default SDK logging. + * @param b Client config builder. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Disable(LDServerConfigBuilder b); + +/** + * Configures the SDK with basic logging. + * @param b Client config builder. Must not be NULL. + * @param basic_builder The basic logging builder. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Basic(LDServerConfigBuilder b, + LDLoggingBasicBuilder basic_builder); + +/** + * Configures the SDK with custom logging. + * @param b Client config builder. Must not be NULL. + * @param custom_builder The custom logging builder. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Custom(LDServerConfigBuilder b, + LDLoggingCustomBuilder custom_builder); + +/** + * Creates an LDClientConfig. The LDServerConfigBuilder is consumed. + * On success, the config will be stored in out_config; otherwise, + * out_config will be set to NULL and the returned LDStatus will indicate + * the error. + * @param builder Builder to consume. Must not be NULL. + * @param out_config Pointer to where the built config will be + * stored. Must not be NULL. + * @return Error status on failure. + */ +LD_EXPORT(LDStatus) +LDServerConfigBuilder_Build(LDServerConfigBuilder builder, + LDServerConfig* out_config); + +/** + * Frees the builder; only necessary if not calling Build. + * @param builder Builder to free. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Free(LDServerConfigBuilder builder); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h new file mode 100644 index 000000000..b84f3cb26 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/config.h @@ -0,0 +1,26 @@ +/** @file */ +// NOLINTBEGIN modernize-use-using + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDCServerConfig* LDServerConfig; + +/** + * Free the configuration. Configurations passed into an LDServerSDK_New call do + * not need to be freed. + * @param config Config to free. + */ +LD_EXPORT(void) LDServerConfig_Free(LDServerConfig config); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h new file mode 100644 index 000000000..acccc586d --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h @@ -0,0 +1,581 @@ +/** @file sdk.h + * @brief LaunchDarkly Server-side C Bindings. + */ +// NOLINTBEGIN modernize-use-using +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if +// used by C++ source code +#endif + +typedef struct _LDServerSDK* LDServerSDK; + +/** + * Constructs a new server-side LaunchDarkly SDK from a configuration. + * + * @param config The configuration. Ownership is transferred. Do not free or + * access the LDServerConfig in any way after this call, otherwise behavior is + * undefined. Must not be NULL. + * @return New SDK instance. Must be freed with LDServerSDK_Free when no longer + * needed. + */ +LD_EXPORT(LDServerSDK) +LDServerSDK_New(LDServerConfig config); + +/** + * Returns the version of the SDK. + * @return String representation of the SDK version. + */ +LD_EXPORT(char const*) +LDServerSDK_Version(void); + +/** +* Starts the SDK, initiating a connection to LaunchDarkly if not offline. +* +* Only one Start call can be in progress at once; calling it +* concurrently invokes undefined behavior. +* +* The method may be blocking or asynchronous depending on the arguments. +* +* To block, pass a positive milliseconds value and an optional pointer to a +boolean. The return +* value will be true if the SDK started within the specified timeframe, or +false if the +* operation couldn't complete in time. The value of out_succeeded will be true +* if the SDK successfully initialized. +* +* Example: +* @code +* bool initialized_successfully; +* if (LDServerSDK_Start(client, 5000, &initialized_successfully)) { +* // The client was able to initialize in less than 5 seconds. +* if (initialized_successfully) { +* // Initialization succeeded. +* else { +* // Initialization failed. +* } +* } else { +* // The client is still initializing. +* } +* @endcode +* +* To start asynchronously, pass `LD_NONBLOCKING`. In this case, the return +value +* will be false and you may pass NULL to out_succeeded. +* +* @code +* // Returns immediately. +* LDServerSDK_Start(client, LD_NONBLOCKING, NULL); +* @endcode +* +* @param sdk SDK. Must not be NULL. +* @param milliseconds Milliseconds to wait for initialization or +`LD_NONBLOCKING` to return immediately. +* @param out_succeeded Pointer to bool representing successful initialization. +Only +* modified if a positive milliseconds value is passed; may be NULL. +* @return True if the client started within the specified timeframe. +*/ +LD_EXPORT(bool) +LDServerSDK_Start(LDServerSDK sdk, + unsigned int milliseconds, + bool* out_succeeded); + +/** + * Returns a boolean value indicating LaunchDarkly connection and flag state + * within the client. + * + * When you first start the client, once Start has completed, Initialized + * should return true if and only if either 1. it connected to LaunchDarkly and + * successfully retrieved flags, or 2. it started in offline mode so there's no + * need to connect to LaunchDarkly. + * + * If the client timed out trying to connect to + * LD, then Initialized returns false (even if we do have cached flags). If the + * client connected and got a 401 error, Initialized is will return false. This + * serves the purpose of letting the app know that there was a problem of some + * kind. + * + * @param sdk SDK. Must not be NULL. + * @return True if initialized. + */ +LD_EXPORT(bool) LDServerSDK_Initialized(LDServerSDK sdk); + +/** + * Tracks that the given context performed an event with the given event name. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name Name of the event. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackEvent(LDServerSDK sdk, + LDContext context, + char const* event_name); + +/** + * Tracks that the given context performed an event with the given event + * name, and associates it with a numeric metric and value. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name The name of the event. Must not be NULL. + * @param metric_value This value is used by the LaunchDarkly experimentation + * feature in numeric custom metrics, and will also be returned as part of the + * custom event for Data Export. + * @param data A JSON value containing additional data associated with the + * event. Ownership is transferred into the SDK. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackMetric(LDServerSDK sdk, + LDContext context, + char const* event_name, + double metric_value, + LDValue data); + +/** + * Tracks that the given context performed an event with the given event + * name, with additional JSON data. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name The name of the event. Must not be NULL. + * @param data A JSON value containing additional data associated with the + * event. Ownership is transferred. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackData(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDValue data); + +/** + * Requests delivery of all pending analytic events (if any). + * + * You MUST pass `LD_NONBLOCKING` as the second parameter. + * + * @param sdk SDK. Must not be NULL. + * @param milliseconds Must pass `LD_NONBLOCKING`. + */ +LD_EXPORT(void) +LDServerSDK_Flush(LDServerSDK sdk, unsigned int reserved); + +/** + * Generates an identify event for the given context. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDK_Identify(LDServerSDK sdk, LDContext context); + +/** + * Returns the boolean value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(bool) +LDServerSDK_BoolVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value); + +/** + * Returns the boolean value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(bool) +LDServerSDK_BoolVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDEvalDetail* out_detail); + +/** + * Returns the string value of a feature flag for a given flag key and context. + * Ensure the string is freed with LDMemory_FreeString. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDMemory_FreeString. + */ +LD_EXPORT(char*) +LDServerSDK_StringVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value); + +/** + * Returns the string value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. Ensure the + * string is freed with LDMemory_FreeString. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDMemory_FreeString. + */ +LD_EXPORT(char*) +LDServerSDK_StringVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDEvalDetail* out_detail); + +/** + * Returns the int value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_IntVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value); + +/** + * Returns the int value of a feature flag for a given flag key and context, and + * details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_IntVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDEvalDetail* out_detail); + +/** + * Returns the double value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_DoubleVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value); + +/** + * Returns the double value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_DoubleVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDEvalDetail* out_detail); + +/** + * Returns the JSON value of a feature flag for a given flag key and context. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Ownership is NOT + * transferred. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. The returned value + * must be freed using LDValue_Free. + */ +LD_EXPORT(LDValue) +LDServerSDK_JsonVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value); + +/** + * Returns the JSON value of a feature flag for a given flag key and context, + * and details that also describes the way the value was determined. + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Ownership is NOT + * transferred. + * @param detail Out parameter to store the details. May pass LD_DISCARD_DETAILS + * or NULL to discard the details. The details object must be freed with + * LDEvalDetail_Free. + * @return The variation for the given context, or a copy of default_value if + * the flag is disabled in the LaunchDarkly control panel. The returned value + * must be freed using LDValue_Free. + */ +LD_EXPORT(LDValue) +LDServerSDK_JsonVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDEvalDetail* out_detail); + +/** + * Evaluates all flags for a context, returning a data structure containing + * the results and additional flag metadata. + * + * The method's behavior can be controlled by passing a combination of + * one or more options. + * + * A common use-case for AllFlagsState is to generate data suitable for + * bootstrapping the client-side JavaScript SDK. + * + * This method will not send analytics events back to LaunchDarkly. + * + * @param sdk SDK. Must not be NULL. + * @param context The context against which all flags will be evaluated. + * Ownership is NOT transferred. Must not be NULL. + * @param options A combination of one or more options. Pass + * LD_ALLFLAGSSTATE_DEFAULT for default behavior. + * @return An AllFlagsState data structure. Must be freed with + * LDAllFlagsState_Free. + */ +LD_EXPORT(LDAllFlagsState) +LDServerSDK_AllFlagsState(LDServerSDK sdk, + LDContext context, + enum LDAllFlagsState_Options options); + +/** + * Frees the SDK's resources, shutting down any connections. May block. + * @param sdk SDK. + */ +LD_EXPORT(void) LDServerSDK_Free(LDServerSDK sdk); + +typedef struct _LDServerDataSourceStatus* LDServerDataSourceStatus; + +/** + * Enumeration of possible data source states. + */ +enum LDServerDataSourceStatus_State { + /** + * The initial state of the data source when the SDK is being + * initialized. + * + * If it encounters an error that requires it to retry initialization, + * the state will remain at kInitializing until it either succeeds and + * becomes LD_SERVERDATASOURCESTATUS_STATE_VALID, or permanently fails and + * becomes LD_SERVERDATASOURCESTATUS_STATE_SHUTDOWN. + */ + LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING = 0, + + /** + * Indicates that the data source is currently operational and has not + * had any problems since the last time it received data. + * + * In streaming mode, this means that there is currently an open stream + * connection and that at least one initial message has been received on + * the stream. In polling mode, it means that the last poll request + * succeeded. + */ + LD_SERVERDATASOURCESTATUS_STATE_VALID = 1, + + /** + * Indicates that the data source encountered an error that it will + * attempt to recover from. + * + * In streaming mode, this means that the stream connection failed, or + * had to be dropped due to some other error, and will be retried after + * a backoff delay. In polling mode, it means that the last poll request + * failed, and a new poll request will be made after the configured + * polling interval. + */ + LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED = 2, + + /** + * Indicates that the data source has been permanently shut down. + * + * This could be because it encountered an unrecoverable error (for + * instance, the LaunchDarkly service rejected the SDK key; an invalid + * SDK key will never become valid), or because the SDK client was + * explicitly shut down. + */ + LD_SERVERDATASOURCESTATUS_STATE_OFF = 3 +}; + +/** + * Get an enumerated value representing the overall current state of the data + * source. + */ +LD_EXPORT(enum LDServerDataSourceStatus_State) +LDServerDataSourceStatus_GetState(LDServerDataSourceStatus status); + +/** + * Information about the last error that the data source encountered, if + * any. If there has not been an error, then NULL will be returned. + * + * If a non-NULL value is returned, then it should be freed using + * LDDataSourceStatus_ErrorInfo_Free. + * + * This property should be updated whenever the data source encounters a + * problem, even if it does not cause the state to change. For instance, if + * a stream connection fails and the state changes to + * LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED, and then subsequent attempts to + * restart the connection also fail, the state will remain + * LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED but the error information will be + * updated each time-- and the last error will still be reported in this + * property even if the state later becomes + * LD_SERVERDATASOURCESTATUS_STATE_VALID. + */ +LD_EXPORT(LDDataSourceStatus_ErrorInfo) +LDServerDataSourceStatus_GetLastError(LDServerDataSourceStatus status); + +/** + * The date/time that the value of State most recently changed, in seconds + * since epoch. + * + * The meaning of this depends on the current state: + * - For LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING, it is the time that the + * SDK started initializing. + * - For LD_SERVERDATASOURCESTATUS_STATE_VALID, it is the time that the data + * source most recently entered a valid state, after previously having been + * LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING or an invalid state such as + * LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED. + * - For LD_SERVERDATASOURCESTATUS_STATE_INTERRUPTED, it is the time that the + * data source most recently entered an error state, after previously having + * been DataSourceState::kValid. + * - For LD_SERVERDATASOURCESTATUS_STATE_SHUTDOWN, it is the time that the data + * source encountered an unrecoverable error or that the SDK was explicitly shut + * down. + */ +LD_EXPORT(time_t) +LDServerDataSourceStatus_StateSince(LDServerDataSourceStatus status); + +typedef void (*ServerDataSourceStatusCallbackFn)( + LDServerDataSourceStatus status, + void* user_data); + +/** + * Defines a data source status listener which may be used to listen for + * changes to the data source status. + * The struct should be initialized using LDServerDataSourceStatusListener_Init + * before use. + */ +struct LDServerDataSourceStatusListener { + /** + * Callback function which is invoked for data source status changes. + * + * The provided pointers are only valid for the duration of the function + * call (excluding UserData, whose lifetime is controlled by the caller). + * + * @param status The updated data source status. + */ + ServerDataSourceStatusCallbackFn StatusChanged; + + /** + * UserData is forwarded into callback functions. + */ + void* UserData; +}; + +/** + * Initializes a data source status change listener. Must be called before + * passing the listener to LDServerSDK_DataSourceStatus_OnStatusChange. + * + * If the StatusChanged member of the listener struct is not set (NULL), then + * the function will not register a listener. In that case the return value + * will be NULL. + * + * Create the struct, initialize the struct, set the StatusChanged handler + * and optionally UserData, and then pass the struct to + * LDServerSDK_DataSourceStatus_OnStatusChange. NULL will be returned if the + * StatusChanged member of the listener struct is NULL. + * + * @param listener Listener to initialize. + */ +LD_EXPORT(void) +LDServerDataSourceStatusListener_Init( + struct LDServerDataSourceStatusListener* listener); + +/** + * Listen for changes to the data source status. + * + * @param sdk SDK. Must not be NULL. + * @param listener The listener, whose StatusChanged callback will be invoked, + * when the data source status changes. Must not be NULL. + * + * @return A LDListenerConnection. The connection can be freed using + * LDListenerConnection_Free and the listener can be disconnected using + * LDListenerConnection_Disconnect. + */ +LD_EXPORT(LDListenerConnection) +LDServerSDK_DataSourceStatus_OnStatusChange( + LDServerSDK sdk, + struct LDServerDataSourceStatusListener listener); + +/** + * The current status of the data source. + * + * The caller must free the returned value using LDServerDataSourceStatus_Free. + */ +LD_EXPORT(LDServerDataSourceStatus) +LDServerSDK_DataSourceStatus_Status(LDServerSDK sdk); + +/** + * Frees the data source status. + * @param status The data source status to free. + */ +LD_EXPORT(void) LDServerDataSourceStatus_Free(LDServerDataSourceStatus status); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND modernize-use-using diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index bc13bf85c..e7f1b8056 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -53,12 +53,22 @@ class IClient { [[nodiscard]] virtual bool Initialized() const = 0; /** - * Returns a map from feature flag keys to feature - * flag values for the current context. + * Evaluates all flags for a context, returning a data structure containing + * the results and additional flag metadata. + * + * The method's behavior can be controlled by passing a combination of + * one or more options. + * + * A common use-case for AllFlagsState is to generate data suitable for + * bootstrapping the client-side JavaScript SDK. * * This method will not send analytics events back to LaunchDarkly. * - * @return A map from feature flag keys to values for the current context. + * @param context The context against which all flags will be + * evaluated. + * @param options A combination of one or more options. Omitting this + * argument is equivalent to passing AllFlagsState::Options::Default. + * @return An AllFlagsState data structure. */ [[nodiscard]] virtual class AllFlagsState AllFlagsState( Context const& context, diff --git a/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp b/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp index b0b540d55..a8336bf07 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/data_source_status.hpp @@ -31,7 +31,8 @@ enum class ServerDataSourceState { /** * Indicates that the data source is currently operational and has not - * had any problems since the last time it received data. + * had any problems since the last time it received data. Alternatively if + * the client was configured for offline mode, the state will be kValid. * * In streaming mode, this means that there is currently an open stream * connection and that at least one initial message has been received on diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index f3afa8aab..5734c9818 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -2,7 +2,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" - ) +) # Automatic library: static or dynamic based on user config. @@ -44,7 +44,11 @@ add_library(${LIBNAME} data_store/persistent/persistent_data_store.cpp data_store/persistent/expiration_tracker.cpp events/event_factory.cpp - ) + bindings/c/sdk.cpp + bindings/c/builder.cpp + bindings/c/config.cpp + bindings/c/all_flags_state/all_flags_state.cpp +) if (MSVC OR (NOT BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} @@ -77,7 +81,7 @@ endif () install(DIRECTORY "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly" DESTINATION "include" - ) +) # Need the public headers to build. target_include_directories(${LIBNAME} PUBLIC ../include) diff --git a/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp b/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp new file mode 100644 index 000000000..112b18666 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/all_flags_state/all_flags_state.cpp @@ -0,0 +1,56 @@ +#include +#include + +#include + +#include +#include + +#include + +#define TO_ALLFLAGS(ptr) (reinterpret_cast(ptr)) +#define FROM_ALLFLAGS(ptr) (reinterpret_cast(ptr)) + +#define TO_VALUE(ptr) (reinterpret_cast(ptr)) +#define FROM_VALUE(ptr) (reinterpret_cast(ptr)) + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +LD_EXPORT(void) LDAllFlagsState_Free(LDAllFlagsState state) { + delete TO_ALLFLAGS(state); +} + +LD_EXPORT(bool) LDAllFlagsState_Valid(LDAllFlagsState state) { + LD_ASSERT_NOT_NULL(state); + + return TO_ALLFLAGS(state)->Valid(); +} + +LD_EXPORT(char*) +LDAllFlagsState_SerializeJSON(LDAllFlagsState state) { + LD_ASSERT_NOT_NULL(state); + + auto json_value = boost::json::value_from(*TO_ALLFLAGS(state)); + std::string json_str = boost::json::serialize(json_value); + + return strdup(json_str.c_str()); +} + +LD_EXPORT(LDValue) +LDAllFlagsState_Value(LDAllFlagsState state, char const* flag_key) { + LD_ASSERT_NOT_NULL(state); + LD_ASSERT_NOT_NULL(flag_key); + + auto const& values = TO_ALLFLAGS(state)->Values(); + + std::unordered_map::const_iterator iter = + values.find(flag_key); + if (iter == values.end()) { + return FROM_VALUE(const_cast(&Value::Null())); + } + + Value const& val_ref = iter->second; + + return FROM_VALUE(const_cast(&val_ref)); +} diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp new file mode 100644 index 000000000..e6a7c2f75 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -0,0 +1,289 @@ +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTBEGIN OCInconsistentNamingInspection + +#include + +#include +#include + +using namespace launchdarkly::server_side; + +#define TO_BUILDER(ptr) (reinterpret_cast(ptr)) +#define FROM_BUILDER(ptr) (reinterpret_cast(ptr)) + +#define TO_STREAM_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_STREAM_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_POLL_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_POLL_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_BASIC_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_BASIC_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_CUSTOM_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_CUSTOM_LOGGING_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_CUSTOM_PERSISTENCE_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_CUSTOM_PERSISTENCE_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +LD_EXPORT(LDServerConfigBuilder) +LDServerConfigBuilder_New(char const* sdk_key) { + LD_ASSERT_NOT_NULL(sdk_key); + + return FROM_BUILDER(new ConfigBuilder(sdk_key)); +} + +LD_EXPORT(LDStatus) +LDServerConfigBuilder_Build(LDServerConfigBuilder b, + LDServerConfig* out_config) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(out_config); + + return launchdarkly::ConsumeBuilder(b, out_config); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Free(LDServerConfigBuilder builder) { + delete TO_BUILDER(builder); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().PollingBaseUrl(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().StreamingBaseUrl(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().EventsBaseUrl(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( + LDServerConfigBuilder b, + char const* url) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(url); + + TO_BUILDER(b)->ServiceEndpoints().RelayProxyBaseURL(url); +} + +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Identifier(LDServerConfigBuilder b, + char const* app_id) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(app_id); + + TO_BUILDER(b)->AppInfo().Identifier(app_id); +} + +LD_EXPORT(void) +LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, + char const* app_version) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(app_version); + + TO_BUILDER(b)->AppInfo().Version(app_version); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Offline(LDServerConfigBuilder b, bool offline) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Offline(offline); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_Enabled(LDServerConfigBuilder b, bool enabled) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().Enabled(enabled); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_Capacity(LDServerConfigBuilder b, + size_t capacity) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().Capacity(capacity); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_FlushIntervalMs(LDServerConfigBuilder b, + unsigned int milliseconds) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().FlushInterval( + std::chrono::milliseconds{milliseconds}); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_AllAttributesPrivate(LDServerConfigBuilder b, + bool all_attributes_private) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().AllAttributesPrivate(all_attributes_private); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, + char const* attribute_reference) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Events().PrivateAttribute(attribute_reference); +} + +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodStream( + LDServerConfigBuilder b, + LDServerDataSourceStreamBuilder stream_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(stream_builder); + + DataSourceBuilder::Streaming* sb = TO_STREAM_BUILDER(stream_builder); + TO_BUILDER(b)->DataSource().Method(*sb); + LDServerDataSourceStreamBuilder_Free(stream_builder); +} + +LD_EXPORT(void) +LDServerConfigBuilder_DataSource_MethodPoll( + LDServerConfigBuilder b, + LDServerDataSourcePollBuilder poll_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(poll_builder); + + DataSourceBuilder::Polling* pb = TO_POLL_BUILDER(poll_builder); + TO_BUILDER(b)->DataSource().Method(*pb); + LDServerDataSourcePollBuilder_Free(poll_builder); +} + +LD_EXPORT(LDServerDataSourceStreamBuilder) +LDServerDataSourceStreamBuilder_New() { + return FROM_STREAM_BUILDER(new DataSourceBuilder::Streaming()); +} + +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( + LDServerDataSourceStreamBuilder b, + unsigned int milliseconds) { + LD_ASSERT_NOT_NULL(b); + + TO_STREAM_BUILDER(b)->InitialReconnectDelay( + std::chrono::milliseconds{milliseconds}); +} + +LD_EXPORT(void) +LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b) { + delete TO_STREAM_BUILDER(b); +} + +LD_EXPORT(LDServerDataSourcePollBuilder) LDServerDataSourcePollBuilder_New() { + return FROM_POLL_BUILDER(new DataSourceBuilder::Polling()); +} + +LD_EXPORT(void) +LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, + unsigned int seconds) { + LD_ASSERT_NOT_NULL(b); + + TO_POLL_BUILDER(b)->PollInterval(std::chrono::seconds{seconds}); +} + +LD_EXPORT(void) +LDServerDataSourcePollBuilder_Free(LDServerDataSourcePollBuilder b) { + delete TO_POLL_BUILDER(b); +} + +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperName(LDServerConfigBuilder b, + char const* wrapper_name) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(wrapper_name); + + TO_BUILDER(b)->HttpProperties().WrapperName(wrapper_name); +} + +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_WrapperVersion( + LDServerConfigBuilder b, + char const* wrapper_version) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(wrapper_version); + + TO_BUILDER(b)->HttpProperties().WrapperVersion(wrapper_version); +} + +LD_EXPORT(void) +LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, + char const* key, + char const* value) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(key); + LD_ASSERT_NOT_NULL(value); + + TO_BUILDER(b)->HttpProperties().Header(key, value); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Disable(LDServerConfigBuilder b) { + LD_ASSERT_NOT_NULL(b); + + TO_BUILDER(b)->Logging().Logging(LoggingBuilder::NoLogging()); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Basic(LDServerConfigBuilder b, + LDLoggingBasicBuilder basic_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(basic_builder); + + LoggingBuilder::BasicLogging* bb = TO_BASIC_LOGGING_BUILDER(basic_builder); + TO_BUILDER(b)->Logging().Logging(*bb); + LDLoggingBasicBuilder_Free(basic_builder); +} + +LD_EXPORT(void) +LDServerConfigBuilder_Logging_Custom(LDServerConfigBuilder b, + LDLoggingCustomBuilder custom_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(custom_builder); + + LoggingBuilder::CustomLogging* cb = + TO_CUSTOM_LOGGING_BUILDER(custom_builder); + TO_BUILDER(b)->Logging().Logging(*cb); + LDLoggingCustomBuilder_Free(custom_builder); +} + +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/src/bindings/c/config.cpp b/libs/server-sdk/src/bindings/c/config.cpp new file mode 100644 index 000000000..72b23bace --- /dev/null +++ b/libs/server-sdk/src/bindings/c/config.cpp @@ -0,0 +1,16 @@ +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTBEGIN OCInconsistentNamingInspection + +#include +#include + +#define TO_CONFIG(ptr) (reinterpret_cast(ptr)) + +using namespace launchdarkly::server_side; + +LD_EXPORT(void) LDServerConfig_Free(LDServerConfig config) { + delete TO_CONFIG(config); +} + +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp new file mode 100644 index 000000000..78d13f0f8 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -0,0 +1,414 @@ +// NOLINTBEGIN cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTBEGIN OCInconsistentNamingInspection + +#include +#include +#include +#include +#include + +#include +#include + +using namespace launchdarkly::server_side; +using namespace launchdarkly; + +struct Detail; + +#define TO_SDK(ptr) (reinterpret_cast(ptr)) +#define FROM_SDK(ptr) (reinterpret_cast(ptr)) + +#define TO_CONTEXT(ptr) (reinterpret_cast(ptr)) +#define FROM_CONTEXT(ptr) (reinterpret_cast(ptr)) + +#define TO_VALUE(ptr) (reinterpret_cast(ptr)) +#define FROM_VALUE(ptr) (reinterpret_cast(ptr)) + +#define FROM_DETAIL(ptr) (reinterpret_cast(ptr)) + +#define TO_DATASOURCESTATUS(ptr) \ + (reinterpret_cast< \ + launchdarkly::server_side::data_sources::DataSourceStatus*>(ptr)) +#define FROM_DATASOURCESTATUS(ptr) \ + (reinterpret_cast(ptr)) + +#define FROM_DATASOURCESTATUS_ERRORINFO(ptr) \ + (reinterpret_cast(ptr)) + +#define TO_ALLFLAGS(ptr) (reinterpret_cast(ptr)) +#define FROM_ALLFLAGS(ptr) (reinterpret_cast(ptr)) + +/* + * Helper to perform the common functionality of checking if the user + * requested a detail out parameter. If so, we allocate a copy of it + * on the heap and return it along with the result. Otherwise, + * we let it destruct and only return the result. + */ +template +inline static auto MaybeDetail(LDServerSDK sdk, + LDEvalDetail* out_detail, + Callable&& fn) { + auto internal_detail = fn(TO_SDK(sdk)); + + auto result = internal_detail.Value(); + + if (!out_detail) { + return result; + } + + *out_detail = FROM_DETAIL(new CEvaluationDetail(internal_detail)); + + return result; +} + +LD_EXPORT(LDServerSDK) +LDServerSDK_New(LDServerConfig config) { + LD_ASSERT_NOT_NULL(config); + + auto as_cfg = reinterpret_cast(config); + auto sdk = new Client(std::move(*as_cfg)); + + LDServerConfig_Free(config); + + return FROM_SDK(sdk); +} + +LD_EXPORT(char const*) +LDServerSDK_Version(void) { + return Client::Version(); +} + +LD_EXPORT(bool) +LDServerSDK_Start(LDServerSDK sdk, + unsigned int milliseconds, + bool* out_succeeded) { + LD_ASSERT_NOT_NULL(sdk); + + auto future = TO_SDK(sdk)->StartAsync(); + + if (milliseconds == LD_NONBLOCKING) { + return false; + } + + if (future.wait_for(std::chrono::milliseconds{milliseconds}) == + std::future_status::ready) { + if (out_succeeded) { + *out_succeeded = future.get(); + } + return true; + } + + return false; +} + +LD_EXPORT(bool) LDServerSDK_Initialized(LDServerSDK sdk) { + LD_ASSERT_NOT_NULL(sdk); + + return TO_SDK(sdk)->Initialized(); +} + +LD_EXPORT(void) +LDServerSDK_TrackEvent(LDServerSDK sdk, + LDContext context, + char const* event_name) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name); +} + +LD_EXPORT(void) +LDServerSDK_TrackMetric(LDServerSDK sdk, + LDContext context, + char const* event_name, + double metric_value, + LDValue value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + LD_ASSERT_NOT_NULL(value); + + Value* as_value = TO_VALUE(value); + + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, metric_value, + std::move(*as_value)); + + LDValue_Free(value); +} + +LD_EXPORT(void) +LDServerSDK_TrackData(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDValue value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + LD_ASSERT_NOT_NULL(value); + + Value* as_value = TO_VALUE(value); + + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, std::move(*as_value)); + + LDValue_Free(value); +} + +LD_EXPORT(void) +LDServerSDK_Flush(LDServerSDK sdk, unsigned int reserved) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT(reserved == LD_NONBLOCKING); + TO_SDK(sdk)->FlushAsync(); +} + +LD_EXPORT(void) +LDServerSDK_Identify(LDServerSDK sdk, LDContext context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + + TO_SDK(sdk)->Identify(*TO_CONTEXT(context)); +} + +LD_EXPORT(bool) +LDServerSDK_BoolVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return TO_SDK(sdk)->BoolVariation(*TO_CONTEXT(context), flag_key, + default_value); +} + +LD_EXPORT(bool) +LDServerSDK_BoolVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->BoolVariationDetail(*TO_CONTEXT(context), flag_key, + default_value); + }); +} + +LD_EXPORT(char*) +LDServerSDK_StringVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + + return strdup( + TO_SDK(sdk) + ->StringVariation(*TO_CONTEXT(context), flag_key, default_value) + .c_str()); +} + +LD_EXPORT(char*) +LDServerSDK_StringVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + + return strdup(MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->StringVariationDetail( + *TO_CONTEXT(context), flag_key, default_value); + }).c_str()); +} + +LD_EXPORT(int) +LDServerSDK_IntVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return TO_SDK(sdk)->IntVariation(*TO_CONTEXT(context), flag_key, + default_value); +} + +LD_EXPORT(int) +LDServerSDK_IntVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->IntVariationDetail(*TO_CONTEXT(context), flag_key, + default_value); + }); +} + +LD_EXPORT(int) +LDServerSDK_DoubleVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return TO_SDK(sdk)->DoubleVariation(*TO_CONTEXT(context), flag_key, + default_value); +} + +LD_EXPORT(int) +LDServerSDK_DoubleVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + return MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->DoubleVariationDetail(*TO_CONTEXT(context), flag_key, + default_value); + }); +} + +LD_EXPORT(LDValue) +LDServerSDK_JsonVariation(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT(default_value); + + Value* as_value = TO_VALUE(default_value); + + return FROM_VALUE(new Value( + TO_SDK(sdk)->JsonVariation(*TO_CONTEXT(context), flag_key, *as_value))); +} + +LD_EXPORT(LDValue) +LDServerSDK_JsonVariationDetail(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT(default_value); + + Value* as_value = TO_VALUE(default_value); + + return FROM_VALUE( + new Value(MaybeDetail(sdk, out_detail, [&](Client* client) { + return client->JsonVariationDetail(*TO_CONTEXT(context), flag_key, + *as_value); + }))); +} + +LD_EXPORT(LDAllFlagsState) +LDServerSDK_AllFlagsState(LDServerSDK sdk, + LDContext context, + enum LDAllFlagsState_Options options) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + + AllFlagsState state = TO_SDK(sdk)->AllFlagsState( + *TO_CONTEXT(context), static_cast(options)); + + return FROM_ALLFLAGS(new AllFlagsState(std::move(state))); +} + +LD_EXPORT(void) LDServerSDK_Free(LDServerSDK sdk) { + delete TO_SDK(sdk); +} + +LD_EXPORT(LDServerDataSourceStatus_State) +LDServerDataSourceStatus_GetState(LDServerDataSourceStatus status) { + LD_ASSERT_NOT_NULL(status); + return static_cast( + TO_DATASOURCESTATUS(status)->State()); +} + +LD_EXPORT(LDDataSourceStatus_ErrorInfo) +LDServerDataSourceStatus_GetLastError(LDServerDataSourceStatus status) { + LD_ASSERT_NOT_NULL(status); + auto error = TO_DATASOURCESTATUS(status)->LastError(); + if (!error) { + return nullptr; + } + return FROM_DATASOURCESTATUS_ERRORINFO( + new data_sources::DataSourceStatus::ErrorInfo( + error->Kind(), error->StatusCode(), error->Message(), + error->Time())); +} + +LD_EXPORT(time_t) +LDServerDataSourceStatus_StateSince(LDServerDataSourceStatus status) { + LD_ASSERT_NOT_NULL(status); + + return std::chrono::duration_cast( + TO_DATASOURCESTATUS(status)->StateSince().time_since_epoch()) + .count(); +} + +LD_EXPORT(void) +LDServerDataSourceStatusListener_Init( + struct LDServerDataSourceStatusListener* listener) { + listener->StatusChanged = nullptr; + listener->UserData = nullptr; +} + +LD_EXPORT(LDListenerConnection) +LDServerSDK_DataSourceStatus_OnStatusChange( + LDServerSDK sdk, + struct LDServerDataSourceStatusListener listener) { + LD_ASSERT_NOT_NULL(sdk); + + if (listener.StatusChanged) { + auto connection = + TO_SDK(sdk)->DataSourceStatus().OnDataSourceStatusChange( + [listener](data_sources::DataSourceStatus status) { + listener.StatusChanged(FROM_DATASOURCESTATUS(&status), + listener.UserData); + }); + + return reinterpret_cast(connection.release()); + } + + return nullptr; +} + +LD_EXPORT(LDServerDataSourceStatus) +LDServerSDK_DataSourceStatus_Status(LDServerSDK sdk) { + LD_ASSERT_NOT_NULL(sdk); + + return FROM_DATASOURCESTATUS(new data_sources::DataSourceStatus( + TO_SDK(sdk)->DataSourceStatus().Status())); +} + +LD_EXPORT(void) LDServerDataSourceStatus_Free(LDServerDataSourceStatus status) { + delete TO_DATASOURCESTATUS(status); +} + +// NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast +// NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp new file mode 100644 index 000000000..893956cb8 --- /dev/null +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -0,0 +1,161 @@ +#include + +#include +#include +#include + +#include + +#include + +#include + +using launchdarkly::server_side::data_sources::DataSourceStatus; + +TEST(ClientBindings, MinimalInstantiation) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + char const* version = LDServerSDK_Version(); + ASSERT_TRUE(version); + ASSERT_STREQ(version, "0.1.0"); // {x-release-please-version} + + LDServerSDK_Free(sdk); +} + +void StatusListenerFunction(LDServerDataSourceStatus status, void* user_data) { + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_VALID, + LDServerDataSourceStatus_GetState(status)); +} + +// This test registers a listener. It doesn't use the listener, but it +// will at least ensure 1.) Compilation, and 2.) Allow sanitizers to run. +TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + LDServerConfigBuilder_Offline(cfg_builder, true); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + struct LDServerDataSourceStatusListener listener {}; + LDServerDataSourceStatusListener_Init(&listener); + + listener.UserData = const_cast("Potato"); + listener.StatusChanged = StatusListenerFunction; + + LDListenerConnection connection = + LDServerSDK_DataSourceStatus_OnStatusChange(sdk, listener); + + bool success = false; + LDServerSDK_Start(sdk, 3000, &success); + EXPECT_TRUE(success); + + LDListenerConnection_Disconnect(connection); + + LDListenerConnection_Free(connection); + LDServerSDK_Free(sdk); +} + +TEST(ClientBindings, GetStatusOfOfflineClient) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + LDServerConfigBuilder_Offline(cfg_builder, true); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + LDServerDataSourceStatus status_1 = + LDServerSDK_DataSourceStatus_Status(sdk); + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_INITIALIZING, + LDServerDataSourceStatus_GetState(status_1)); + + bool success = false; + LDServerSDK_Start(sdk, 3000, &success); + + LDServerDataSourceStatus status_2 = + LDServerSDK_DataSourceStatus_Status(sdk); + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_VALID, + LDServerDataSourceStatus_GetState(status_2)); + + EXPECT_EQ(nullptr, LDServerDataSourceStatus_GetLastError(status_2)); + + EXPECT_NE(0, LDServerDataSourceStatus_StateSince(status_2)); + + LDServerDataSourceStatus_Free(status_1); + LDServerDataSourceStatus_Free(status_2); + LDServerSDK_Free(sdk); +} + +TEST(ClientBindings, ComplexDataSourceStatus) { + DataSourceStatus status( + DataSourceStatus::DataSourceState::kValid, + std::chrono::time_point{ + std::chrono::seconds{200}}, + DataSourceStatus::ErrorInfo( + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, 404, + "Not found", + std::chrono::time_point{ + std::chrono::seconds{100}})); + + EXPECT_EQ(LD_SERVERDATASOURCESTATUS_STATE_VALID, + LDServerDataSourceStatus_GetState( + reinterpret_cast(&status))); + + EXPECT_EQ(200, LDServerDataSourceStatus_StateSince( + reinterpret_cast(&status))); + + LDDataSourceStatus_ErrorInfo info = LDServerDataSourceStatus_GetLastError( + reinterpret_cast(&status)); + + EXPECT_EQ(LD_DATASOURCESTATUS_ERRORKIND_ERROR_RESPONSE, + LDDataSourceStatus_ErrorInfo_GetKind(info)); + + EXPECT_EQ(std::string("Not found"), + LDDataSourceStatus_ErrorInfo_Message(info)); + + EXPECT_EQ(100, LDDataSourceStatus_ErrorInfo_Time(info)); + + EXPECT_EQ(404, LDDataSourceStatus_ErrorInfo_StatusCode(info)); + + LDDataSourceStatus_ErrorInfo_Free(info); +} + +TEST(ClientBindings, AllFlagsState) { + LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); + + LDServerConfig config; + LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); + ASSERT_TRUE(LDStatus_Ok(status)); + + LDServerSDK sdk = LDServerSDK_New(config); + + LDContextBuilder ctx_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(ctx_builder, "user", "shadow"); + LDContext context = LDContextBuilder_Build(ctx_builder); + + LDAllFlagsState state = + LDServerSDK_AllFlagsState(sdk, context, LD_ALLFLAGSSTATE_DEFAULT); + + ASSERT_FALSE(LDAllFlagsState_Valid(state)); + + char* json = LDAllFlagsState_SerializeJSON(state); + ASSERT_STREQ(json, "{\"$valid\":false,\"$flagsState\":{}}"); + LDMemory_FreeString(json); + + LDValue nonexistent_flag = LDAllFlagsState_Value(state, "nonexistent_flag"); + ASSERT_EQ(LDValue_Type(nonexistent_flag), LDValueType_Null); + + LDAllFlagsState_Free(state); + LDContext_Free(context); + LDServerSDK_Free(sdk); +} From 4889549a0986b140a6f605928695a265dc12875b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 31 Aug 2023 17:04:13 -0700 Subject: [PATCH 51/56] feat: plumb server side initial backoff delay (#232) Hooks up the initial reconnect delay parameter to the server-side streaming data source. Allows us to remove another batch of contract test failures. --- .../src/entity_manager.cpp | 17 +++++++++-------- .../server-contract-tests/test-suppressions.txt | 6 ------ .../src/data_sources/streaming_data_source.cpp | 4 +++- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index c5578d540..8e8235036 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -10,12 +10,7 @@ using namespace launchdarkly::server_side; EntityManager::EntityManager(boost::asio::any_io_executor executor, launchdarkly::Logger& logger) - : - counter_{0}, - executor_{std::move(executor)}, - logger_{logger} {} - - + : counter_{0}, executor_{std::move(executor)}, logger_{logger} {} std::optional EntityManager::create(ConfigParams const& in) { std::string id = std::to_string(counter_++); @@ -43,14 +38,20 @@ std::optional EntityManager::create(ConfigParams const& in) { } } + auto& datasource = config_builder.DataSource(); + if (in.streaming) { if (in.streaming->baseUri) { endpoints.StreamingBaseUrl(*in.streaming->baseUri); } + if (in.streaming->initialRetryDelayMs) { + auto streaming = DataSourceBuilder::Streaming(); + streaming.InitialReconnectDelay( + std::chrono::milliseconds(*in.streaming->initialRetryDelayMs)); + datasource.Method(std::move(streaming)); + } } - auto& datasource = config_builder.DataSource(); - if (in.polling) { if (in.polling->baseUri) { endpoints.PollingBaseUrl(*in.polling->baseUri); diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt index 4de6fa075..89f4db4b8 100644 --- a/contract-tests/server-contract-tests/test-suppressions.txt +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -1,9 +1,3 @@ -streaming/retry behavior/retry after IO error on reconnect -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 400 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 408 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 429 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 500 -streaming/retry behavior/retry after recoverable HTTP error on reconnect/error 503 streaming/validation/drop and reconnect if stream event has malformed JSON/put event streaming/validation/drop and reconnect if stream event has malformed JSON/patch event streaming/validation/drop and reconnect if stream event has malformed JSON/delete event diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.cpp b/libs/server-sdk/src/data_sources/streaming_data_source.cpp index e697a522f..2b76e05ac 100644 --- a/libs/server-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -77,7 +77,6 @@ void StreamingDataSource::Start() { return; } - // TODO: Initial reconnect delay. sc-204393 boost::urls::url url = uri_components.value(); auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); @@ -93,6 +92,9 @@ void StreamingDataSource::Start() { client_builder.connect_timeout(http_config_.ConnectTimeout()); + client_builder.initial_reconnect_delay( + streaming_config_.initial_reconnect_delay); + for (auto const& header : http_config_.BaseHeaders()) { client_builder.header(header.first, header.second); } From d0ce48d681771fc793ced7640c032dd838c74f03 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 31 Aug 2023 17:06:25 -0700 Subject: [PATCH 52/56] add story label to remaining test suppressions: --- contract-tests/server-contract-tests/test-suppressions.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/contract-tests/server-contract-tests/test-suppressions.txt b/contract-tests/server-contract-tests/test-suppressions.txt index 89f4db4b8..da9feca4f 100644 --- a/contract-tests/server-contract-tests/test-suppressions.txt +++ b/contract-tests/server-contract-tests/test-suppressions.txt @@ -1,3 +1,4 @@ +# SC-204387 streaming/validation/drop and reconnect if stream event has malformed JSON/put event streaming/validation/drop and reconnect if stream event has malformed JSON/patch event streaming/validation/drop and reconnect if stream event has malformed JSON/delete event From 1db6db9a91431c22bdf97ca7846f0e1f241badc1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 5 Sep 2023 15:23:45 -0700 Subject: [PATCH 53/56] chore: resolve ValidChar function lint/compile warnings (#234) The argument to `ValidChar` is a `char`, so its max value is `255` rendering the check redundant. This causes a warning on our Mac compilations as well as a linter warning. --------- Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- libs/common/src/config/app_info_builder.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/common/src/config/app_info_builder.cpp b/libs/common/src/config/app_info_builder.cpp index df83d2749..69f163a86 100644 --- a/libs/common/src/config/app_info_builder.cpp +++ b/libs/common/src/config/app_info_builder.cpp @@ -21,9 +21,9 @@ tl::expected AppInfoBuilder::Tag::Build() const { } bool ValidChar(char c) { - if(c > 0 && c < 255) { - // The MSVC implementation of isalnum will assert if the number it outside - // its lookup table. + if (c > 0) { + // The MSVC implementation of isalnum will assert if the number is + // outside its lookup table (0-0xFF, inclusive.) // iswalnum would not, but is less restrictive than desired. return std::isalnum(c) != 0 || c == '-' || c == '.' || c == '_'; } From 61a3bef114fcc627a7cc639d8f2d0baeeb04ebb1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 15 Sep 2023 08:20:48 -0700 Subject: [PATCH 54/56] use async_connect() instead of run() in streaming data source --- .../data_sources/streaming_data_source.cpp | 245 +++++++++--------- 1 file changed, 122 insertions(+), 123 deletions(-) diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.cpp b/libs/server-sdk/src/data_sources/streaming_data_source.cpp index 2b76e05ac..6f0b720d3 100644 --- a/libs/server-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -12,145 +12,144 @@ namespace launchdarkly::server_side::data_sources { -static char const* const kCouldNotParseEndpoint = - "Could not parse streaming endpoint URL"; - -static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { - switch (error) { - case sse::Error::NoContent: - return "server responded 204 (No Content), will not attempt to " - "reconnect"; - case sse::Error::InvalidRedirectLocation: - return "server responded with an invalid redirection"; - case sse::Error::UnrecoverableClientError: - return "unrecoverable client-side error"; - default: - return "unrecognized error"; - } -} - -StreamingDataSource::StreamingDataSource( - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::DataSourceConfig const& - data_source_config, - config::shared::built::HttpProperties http_properties, - boost::asio::any_io_executor ioc, - IDataSourceUpdateSink& handler, - DataSourceStatusManager& status_manager, - Logger const& logger) - : exec_(std::move(ioc)), - logger_(logger), - status_manager_(status_manager), - data_source_handler_( - DataSourceEventHandler(handler, logger, status_manager_)), - http_config_(std::move(http_properties)), - streaming_config_( - std::get>(data_source_config.method)), - streaming_endpoint_(endpoints.StreamingBaseUrl()) {} - -void StreamingDataSource::Start() { - status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); - - auto updated_url = network::AppendUrl(streaming_endpoint_, - streaming_config_.streaming_path); - - // Bad URL, don't set the client. Start will then report the bad status. - if (!updated_url) { - LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; - status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, - kCouldNotParseEndpoint); - return; + static char const *const kCouldNotParseEndpoint = + "Could not parse streaming endpoint URL"; + + static char const *DataSourceErrorToString(launchdarkly::sse::Error error) { + switch (error) { + case sse::Error::NoContent: + return "server responded 204 (No Content), will not attempt to " + "reconnect"; + case sse::Error::InvalidRedirectLocation: + return "server responded with an invalid redirection"; + case sse::Error::UnrecoverableClientError: + return "unrecoverable client-side error"; + default: + return "unrecognized error"; + } } - auto uri_components = boost::urls::parse_uri(*updated_url); - - // Unlikely that it could be parsed earlier, and it cannot be parsed now. - if (!uri_components) { - LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; - status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, - kCouldNotParseEndpoint); - return; - } + StreamingDataSource::StreamingDataSource( + config::shared::built::ServiceEndpoints const &endpoints, + config::shared::built::DataSourceConfig const & + data_source_config, + config::shared::built::HttpProperties http_properties, + boost::asio::any_io_executor ioc, + IDataSourceUpdateSink &handler, + DataSourceStatusManager &status_manager, + Logger const &logger) + : exec_(std::move(ioc)), + logger_(logger), + status_manager_(status_manager), + data_source_handler_( + DataSourceEventHandler(handler, logger, status_manager_)), + http_config_(std::move(http_properties)), + streaming_config_( + std::get>(data_source_config.method)), + streaming_endpoint_(endpoints.StreamingBaseUrl()) {} + + void StreamingDataSource::Start() { + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); + + auto updated_url = network::AppendUrl(streaming_endpoint_, + streaming_config_.streaming_path); + + // Bad URL, don't set the client. Start will then report the bad status. + if (!updated_url) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } - boost::urls::url url = uri_components.value(); + auto uri_components = boost::urls::parse_uri(*updated_url); - auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); + // Unlikely that it could be parsed earlier, and it cannot be parsed now. + if (!uri_components) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; + } - client_builder.method(boost::beast::http::verb::get); + boost::urls::url url = uri_components.value(); - // TODO: can the read timeout be shared with *all* http requests? Or should - // it have a default in defaults.hpp? This must be greater than the - // heartbeat interval of the streaming service. - client_builder.read_timeout(std::chrono::minutes(5)); + auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); - client_builder.write_timeout(http_config_.WriteTimeout()); + client_builder.method(boost::beast::http::verb::get); - client_builder.connect_timeout(http_config_.ConnectTimeout()); + // TODO: can the read timeout be shared with *all* http requests? Or should + // it have a default in defaults.hpp? This must be greater than the + // heartbeat interval of the streaming service. + client_builder.read_timeout(std::chrono::minutes(5)); - client_builder.initial_reconnect_delay( - streaming_config_.initial_reconnect_delay); + client_builder.write_timeout(http_config_.WriteTimeout()); - for (auto const& header : http_config_.BaseHeaders()) { - client_builder.header(header.first, header.second); - } + client_builder.connect_timeout(http_config_.ConnectTimeout()); - // TODO: Handle proxy support. sc-204386 + client_builder.initial_reconnect_delay( + streaming_config_.initial_reconnect_delay); - auto weak_self = weak_from_this(); + for (auto const &header: http_config_.BaseHeaders()) { + client_builder.header(header.first, header.second); + } - client_builder.receiver([weak_self](launchdarkly::sse::Event const& event) { - if (auto self = weak_self.lock()) { - self->data_source_handler_.HandleMessage(event.type(), - event.data()); - // TODO: Use the result of handle message to restart the - // event source if we got bad data. sc-204387 + // TODO: Handle proxy support. sc-204386 + + auto weak_self = weak_from_this(); + + client_builder.receiver([weak_self](launchdarkly::sse::Event const &event) { + if (auto self = weak_self.lock()) { + self->data_source_handler_.HandleMessage(event.type(), + event.data()); + // TODO: Use the result of handle message to restart the + // event source if we got bad data. sc-204387 + } + }); + + client_builder.logger([weak_self](auto msg) { + if (auto self = weak_self.lock()) { + LD_LOG(self->logger_, LogLevel::kDebug) << msg; + } + }); + + client_builder.errors([weak_self](auto error) { + if (auto self = weak_self.lock()) { + auto error_string = DataSourceErrorToString(error); + LD_LOG(self->logger_, LogLevel::kError) << error_string; + self->status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, + error_string); + } + }); + + client_ = client_builder.build(); + + if (!client_) { + LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; + status_manager_.SetState( + DataSourceStatus::DataSourceState::kOff, + DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, + kCouldNotParseEndpoint); + return; } - }); + client_->async_connect(); + } - client_builder.logger([weak_self](auto msg) { - if (auto self = weak_self.lock()) { - LD_LOG(self->logger_, LogLevel::kDebug) << msg; + void StreamingDataSource::ShutdownAsync(std::function completion) { + if (client_) { + status_manager_.SetState( + DataSourceStatus::DataSourceState::kInitializing); + return client_->async_shutdown(std::move(completion)); } - }); - - client_builder.errors([weak_self](auto error) { - if (auto self = weak_self.lock()) { - auto error_string = DataSourceErrorToString(error); - LD_LOG(self->logger_, LogLevel::kError) << error_string; - self->status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kErrorResponse, - error_string); + if (completion) { + boost::asio::post(exec_, completion); } - }); - - client_ = client_builder.build(); - - if (!client_) { - LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; - status_manager_.SetState( - DataSourceStatus::DataSourceState::kOff, - DataSourceStatus::ErrorInfo::ErrorKind::kNetworkError, - kCouldNotParseEndpoint); - return; } - client_->run(); -} - -void StreamingDataSource::ShutdownAsync(std::function completion) { - if (client_) { - status_manager_.SetState( - DataSourceStatus::DataSourceState::kInitializing); - return client_->async_shutdown(std::move(completion)); - } - if (completion) { - boost::asio::post(exec_, completion); - } -} - } // namespace launchdarkly::server_side::data_sources From 8345ebb2279e4d64840a2789689cd408add6b8ed Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 24 Oct 2023 11:05:49 -0700 Subject: [PATCH 55/56] fix merge --- .github/workflows/client.yml | 6 +++--- .../test-suppressions.txt | 0 examples/client-and-server-coexistence/main.c | 1 - .../launchdarkly/data/evaluation_detail.hpp | 21 ------------------- .../src/serialization/json_context_kind.cpp | 1 - 5 files changed, 3 insertions(+), 26 deletions(-) delete mode 100644 contract-tests/client-contract-tests/test-suppressions.txt diff --git a/.github/workflows/client.yml b/.github/workflows/client.yml index 75dc6f508..02252dd4e 100644 --- a/.github/workflows/client.yml +++ b/.github/workflows/client.yml @@ -6,7 +6,7 @@ on: paths-ignore: - '**.md' #Do not need to run CI for markdown changes. pull_request: - branches: [ main ] + branches: [ main, server-side ] paths-ignore: - '**.md' @@ -16,12 +16,12 @@ jobs: env: # Port the test service (implemented in this repo) should bind to. TEST_SERVICE_PORT: 8123 - TEST_SERVICE_BINARY: ./build/contract-tests/sdk-contract-tests/sdk-tests + TEST_SERVICE_BINARY: ./build/contract-tests/client-contract-tests/client-tests steps: - uses: actions/checkout@v3 - uses: ./.github/actions/ci with: - cmake_target: sdk-tests + cmake_target: client-tests run_tests: false - name: 'Launch test service as background task' run: $TEST_SERVICE_BINARY $TEST_SERVICE_PORT 2>&1 & diff --git a/contract-tests/client-contract-tests/test-suppressions.txt b/contract-tests/client-contract-tests/test-suppressions.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/client-and-server-coexistence/main.c b/examples/client-and-server-coexistence/main.c index 935a0a328..d162289c9 100644 --- a/examples/client-and-server-coexistence/main.c +++ b/examples/client-and-server-coexistence/main.c @@ -48,4 +48,3 @@ int main() { return 0; } ->>>>>>> main diff --git a/libs/common/include/launchdarkly/data/evaluation_detail.hpp b/libs/common/include/launchdarkly/data/evaluation_detail.hpp index f784e7428..f8b3dbfc0 100644 --- a/libs/common/include/launchdarkly/data/evaluation_detail.hpp +++ b/libs/common/include/launchdarkly/data/evaluation_detail.hpp @@ -65,32 +65,11 @@ class EvaluationDetail { */ [[nodiscard]] std::optional const& Reason() const; - /** - * Check if an evaluation reason exists, and if so, if it is of a particular - * kind. - * @param kind Kind to check. - * @return True if a reason exists and matches the given kind. - */ - [[nodiscard]] bool ReasonKindIs(enum EvaluationReason::Kind kind) const; - - /** - * @return True if the evaluation resulted in an error. - * TODO(sc209960) - */ - [[nodiscard]] bool IsError() const; - /** * @return A reference to the variation value. */ T const& operator*() const; - /** - * @return True if the evaluation was successful (i.e. IsError returns - * false.) - * TODO(sc209960) - */ - explicit operator bool() const; - private: T value_; std::optional variation_index_; diff --git a/libs/internal/src/serialization/json_context_kind.cpp b/libs/internal/src/serialization/json_context_kind.cpp index b69aaa226..fa00891fb 100644 --- a/libs/internal/src/serialization/json_context_kind.cpp +++ b/libs/internal/src/serialization/json_context_kind.cpp @@ -31,4 +31,3 @@ void tag_invoke(boost::json::value_from_tag const& unused, } } // namespace data_model } // namespace launchdarkly ->>>>>>> main From 1e6f05db172e79649eec9a05f244d15ad188c8da Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 24 Oct 2023 11:21:13 -0700 Subject: [PATCH 56/56] add launchdarkly::unreachable to switch --- .../persistent/persistent_data_store.hpp | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp index d0e340306..1c2663ec4 100644 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ b/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp @@ -5,6 +5,7 @@ #include "../memory_store.hpp" #include "expiration_tracker.hpp" +#include #include #include @@ -22,7 +23,7 @@ class SegmentKind : public persistence::IPersistentKind { ~SegmentKind() override = default; private: - static const inline std::string namespace_ = "segments"; + static inline std::string const namespace_ = "segments"; }; class FlagKind : public persistence::IPersistentKind { @@ -33,12 +34,12 @@ class FlagKind : public persistence::IPersistentKind { ~FlagKind() override = default; private: - static const inline std::string namespace_ = "features"; + static inline std::string const namespace_ = "features"; }; struct Kinds { - static const FlagKind Flag; - static const SegmentKind Segment; + static FlagKind const Flag; + static SegmentKind const Segment; }; class PersistentStore : public IDataStore, @@ -105,6 +106,8 @@ class PersistentStore : public IDataStore, [[fallthrough]]; case ExpirationTracker::TrackState::kFresh: return get(); + default: + launchdarkly::detail::unreachable(); } } @@ -122,7 +125,7 @@ class PersistentStore : public IDataStore, ~SegmentKind() override = default; private: - static const inline std::string namespace_ = "segments"; + static inline std::string const namespace_ = "segments"; }; class FlagKind : public persistence::IPersistentKind { @@ -133,18 +136,18 @@ class PersistentStore : public IDataStore, ~FlagKind() override = default; private: - static const inline std::string namespace_ = "features"; + static inline std::string const namespace_ = "features"; }; struct Kinds { - static const FlagKind Flag; - static const SegmentKind Segment; + static FlagKind const Flag; + static SegmentKind const Segment; }; struct Keys { - static const inline std::string kAllFlags = "allFlags"; - static const inline std::string kAllSegments = "allSegments"; - static const inline std::string kInitialized = "initialized"; + static inline std::string const kAllFlags = "allFlags"; + static inline std::string const kAllSegments = "allSegments"; + static inline std::string const kInitialized = "initialized"; }; };