From c417579d3643dbe2f20396bf9d527d83d15fd6eb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 12 Jun 2023 10:54:33 -0700 Subject: [PATCH 001/244] 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 002/244] 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 003/244] 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 004/244] 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 005/244] 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 006/244] 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 007/244] 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 008/244] 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 009/244] 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 010/244] 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 011/244] 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 012/244] 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 013/244] 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 014/244] 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 015/244] 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 016/244] 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 017/244] 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 018/244] 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 019/244] 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 020/244] 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 021/244] 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 022/244] 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 023/244] 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 024/244] 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 025/244] 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 026/244] 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 027/244] 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 028/244] 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 029/244] 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 030/244] 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 031/244] 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 032/244] 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 033/244] 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 034/244] 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 035/244] 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 036/244] 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 037/244] 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 038/244] 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 039/244] 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 040/244] 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 041/244] 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 042/244] 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 043/244] 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 044/244] 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 045/244] 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 046/244] 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 047/244] 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 048/244] 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 049/244] 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 050/244] 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 051/244] 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 052/244] 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 053/244] 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 054/244] 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 055/244] 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 056/244] 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"; }; }; From 0a7340ae952182bd9d8bdf014c7b071672b5b7c9 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Sep 2023 09:49:08 -0700 Subject: [PATCH 057/244] adding in sync and bootstrap builders --- .../config/shared/builders/config_builder.hpp | 1 - .../config/shared/builders/data_source_builder.hpp | 11 +++++++++-- .../config/shared/built/data_source_config.hpp | 3 +++ libs/common/src/config/data_source_builder.cpp | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp index db81c24e2..dee451379 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp @@ -37,7 +37,6 @@ class ConfigBuilder { launchdarkly::config::shared::builders::PersistenceBuilder; using LoggingBuilder = launchdarkly::config::shared::builders::LoggingBuilder; - /** * A minimal configuration consists of a LaunchDarkly SDK Key. * @param sdk_key SDK Key. diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp index a75c97cca..f94642b32 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp @@ -198,6 +198,12 @@ class DataSourceBuilder { */ DataSourceBuilder& Method(Polling polling_builder); + DataSourceBuilder& Bootstrap(bool enable_bootstrap); + + DataSourceBuilder& Sync(bool enable_sync); + + DataSourceBuilder& Order(std::uint64_t bootstrap_order); + /** * Build a data source config. This is used internal to the SDK. * @@ -207,8 +213,9 @@ class DataSourceBuilder { private: std::variant method_; - bool with_reasons_; - bool use_report_; + bool enable_bootstrap_; + bool enable_sync_; + std::uint64_t order_; }; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 2d3615041..a26356353 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -57,6 +57,9 @@ struct DataSourceConfig { template <> struct DataSourceConfig { std::variant, PollingConfig> method; + bool enable_bootstrap; + bool enable_sync; + std::uint64_t order; }; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 92153f6a4..1105d1334 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -67,7 +67,10 @@ built::DataSourceConfig DataSourceBuilder::Build() const { } DataSourceBuilder::DataSourceBuilder() - : with_reasons_(false), use_report_(false), method_(Streaming()) {} + : method_(Streaming()), + enable_bootstrap_(true), + enable_sync_(false), + order_(0) {} DataSourceBuilder& DataSourceBuilder::Method( StreamingBuilder builder) { From 82d9fa16afe2c739a0e44bc2c8996b7794b6b298 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Sep 2023 11:34:29 -0700 Subject: [PATCH 058/244] creating DataSourcesBuilder --- examples/hello-cpp-server/main.cpp | 10 ++++- .../config/shared/builders/config_builder.hpp | 6 +++ .../shared/builders/data_sources_builder.hpp | 41 +++++++++++++++++++ libs/common/src/config/config_builder.cpp | 6 +++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index 1f975ded5..b9daabe5b 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -23,7 +23,15 @@ int main() { return 1; } - auto config = server_side::ConfigBuilder(SDK_KEY).Build(); + auto cfg_builder = server_side::ConfigBuilder(SDK_KEY); + cfg_builder.DataSource() + .Method( + server_side::DataSourceBuilder::Streaming().InitialReconnectDelay( + std::chrono::seconds(1))) + .Bootstrap(true) + .Order(3); + + auto config = cfg_builder.Build(); if (!config) { std::cout << "error: config is invalid: " << config.error() << '\n'; return 1; diff --git a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp index dee451379..9345fe7e8 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,8 @@ class ConfigBuilder { launchdarkly::config::shared::builders::EventsBuilder; using DataSourceBuilder = launchdarkly::config::shared::builders::DataSourceBuilder; + using DataSourcesBuilder = + launchdarkly::config::shared::builders::DataSourcesBuilder; using HttpPropertiesBuilder = launchdarkly::config::shared::builders::HttpPropertiesBuilder; using PersistenceBuilder = @@ -83,6 +86,8 @@ class ConfigBuilder { */ DataSourceBuilder& DataSource(); + DataSourcesBuilder& DataSources(); + /** * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. * The builder has methods for setting individual HTTP-related properties. @@ -119,6 +124,7 @@ class ConfigBuilder { AppInfoBuilder app_info_builder_; EventsBuilder events_builder_; DataSourceBuilder data_source_builder_; + DataSourcesBuilder data_sources_builder_; HttpPropertiesBuilder http_properties_builder_; LoggingBuilder logging_config_builder_; PersistenceBuilder persistence_builder_; diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp new file mode 100644 index 000000000..63a9374b7 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include + +namespace launchdarkly::config::shared::builders { + +/** + * Used to construct a DataSourcesConfiguration for the specified SDK type. + * @tparam SDK ClientSDK or ServerSDK. + */ +template +class DataSourcesBuilder; + +template <> +class DataSourcesBuilder { + public: + DataSourcesBuilder() {} +}; + +template <> +class DataSourcesBuilder { + public: + DataSourcesBuilder(); + + DataSourcesBuilder& Source(); + + DataSourcesBuilder& Destination(); + [[nodiscard]] built::DataSourceConfig Build() const; + + private: + std::vector> builders_; +}; + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/config_builder.cpp b/libs/common/src/config/config_builder.cpp index 5e58e3b89..d7f594a20 100644 --- a/libs/common/src/config/config_builder.cpp +++ b/libs/common/src/config/config_builder.cpp @@ -35,6 +35,12 @@ ConfigBuilder::DataSource() { return data_source_builder_; } +template +typename ConfigBuilder::DataSourcesBuilder& +ConfigBuilder::DataSources() { + return data_sources_builder_; +} + template typename ConfigBuilder::HttpPropertiesBuilder& ConfigBuilder::HttpProperties() { From cf5792ee5885c5f613238c05b1f4a7e30ba8d506 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Sep 2023 12:05:10 -0700 Subject: [PATCH 059/244] config builders for DataSources are compiling --- examples/hello-cpp-server/main.cpp | 9 ++++- .../shared/builders/data_sources_builder.hpp | 26 ++++++++++++-- .../src/config/data_sources_builder.cpp | 35 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 libs/common/src/config/data_sources_builder.cpp diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index b9daabe5b..60595c9f5 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -24,13 +24,20 @@ int main() { } auto cfg_builder = server_side::ConfigBuilder(SDK_KEY); - cfg_builder.DataSource() + cfg_builder.DataSources() + .Source() .Method( server_side::DataSourceBuilder::Streaming().InitialReconnectDelay( std::chrono::seconds(1))) .Bootstrap(true) .Order(3); + cfg_builder.DataSources() + .Bootstrap() + .Order(launchdarkly::config::shared::builders::BootstrapBuilder::Order:: + Random) + .RandomSeed(1234); + auto config = cfg_builder.Build(); if (!config) { std::cout << "error: config is invalid: " << config.error() << '\n'; diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp index 63a9374b7..8fe52aa01 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp @@ -5,6 +5,8 @@ #include #include +#include + #include #include #include @@ -24,18 +26,38 @@ class DataSourcesBuilder { DataSourcesBuilder() {} }; +class BootstrapBuilder { + public: + BootstrapBuilder(); + enum class Order { ConsistentFirst = 0, Assigned = 1, Random = 2 }; + + using SeedType = std::int64_t; + + BootstrapBuilder& Order(Order order); + + BootstrapBuilder& RandomSeed(SeedType seed); + + private: + enum Order order_; + std::optional seed_; +}; + template <> class DataSourcesBuilder { public: DataSourcesBuilder(); - DataSourcesBuilder& Source(); + DataSourceBuilder& Source(); DataSourcesBuilder& Destination(); + + BootstrapBuilder& Bootstrap(); + [[nodiscard]] built::DataSourceConfig Build() const; private: - std::vector> builders_; + BootstrapBuilder bootstrap_; + std::vector> sources_; }; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_sources_builder.cpp b/libs/common/src/config/data_sources_builder.cpp new file mode 100644 index 000000000..788f1d18b --- /dev/null +++ b/libs/common/src/config/data_sources_builder.cpp @@ -0,0 +1,35 @@ + +#include + +namespace launchdarkly::config::shared::builders { + +BootstrapBuilder::BootstrapBuilder() + : order_(Order::ConsistentFirst), seed_(std::nullopt) {} + +BootstrapBuilder& BootstrapBuilder::Order(enum BootstrapBuilder::Order order) { + order_ = order; + return *this; +} + +BootstrapBuilder& BootstrapBuilder::RandomSeed( + BootstrapBuilder::SeedType seed) { + seed_ = seed; + return *this; +} + +DataSourcesBuilder::DataSourcesBuilder() + : bootstrap_(), sources_() {} + +DataSourceBuilder& DataSourcesBuilder::Source() { + return sources_.emplace_back(); +} + +DataSourcesBuilder& DataSourcesBuilder::Destination() { + return *this; +} + +BootstrapBuilder& DataSourcesBuilder::Bootstrap() { + return bootstrap_; +} + +} // namespace launchdarkly::config::shared::builders From 86e7389a8a90faf53d56bc7c0e623011296669e1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Sep 2023 13:49:03 -0700 Subject: [PATCH 060/244] add PullModeDataSource --- .../launchdarkly/data_sources/data_source.hpp | 19 +-- libs/server-sdk/src/CMakeLists.txt | 14 +-- .../all_flags_state_builder.hpp | 1 - libs/server-sdk/src/client_impl.cpp | 117 +++++++++++------- libs/server-sdk/src/client_impl.hpp | 7 +- .../data_source_event_handler.cpp | 6 +- .../data_source_event_handler.hpp | 3 +- .../data_sources/data_source_interface.hpp | 108 ++++++++++++++++ .../data_sources/data_source_update_sink.hpp | 8 +- .../descriptors.hpp | 4 +- .../memory_store}/memory_store.cpp | 8 +- .../memory_store}/memory_store.hpp | 27 ++-- .../src/data_sources/null_data_source.cpp | 3 + .../src/data_sources/null_data_source.hpp | 8 +- .../src/data_sources/polling_data_source.cpp | 29 ++--- .../src/data_sources/polling_data_source.hpp | 10 +- .../pull_mode/pull_mode_data_source.cpp | 73 +++++++++++ .../pull_mode/pull_mode_data_source.hpp | 53 ++++++++ .../data_sources/streaming_data_source.cpp | 12 +- .../data_sources/streaming_data_source.hpp | 10 +- libs/server-sdk/src/data_store/data_store.hpp | 81 ------------ .../src/data_store/data_store_updater.cpp | 16 +-- .../src/data_store/data_store_updater.hpp | 13 +- .../persistent/persistent_data_store.hpp | 5 +- libs/server-sdk/src/evaluation/evaluator.cpp | 12 +- libs/server-sdk/src/evaluation/evaluator.hpp | 6 +- libs/server-sdk/src/evaluation/rules.cpp | 12 +- libs/server-sdk/src/evaluation/rules.hpp | 12 +- 28 files changed, 419 insertions(+), 258 deletions(-) create mode 100644 libs/server-sdk/src/data_sources/data_source_interface.hpp rename libs/server-sdk/src/{data_store => data_sources}/descriptors.hpp (77%) rename libs/server-sdk/src/{data_store => data_sources/memory_store}/memory_store.cpp (89%) rename libs/server-sdk/src/{data_store => data_sources/memory_store}/memory_store.hpp (59%) create mode 100644 libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp delete mode 100644 libs/server-sdk/src/data_store/data_store.hpp diff --git a/libs/internal/include/launchdarkly/data_sources/data_source.hpp b/libs/internal/include/launchdarkly/data_sources/data_source.hpp index 6ec7fe06f..0eba461a0 100644 --- a/libs/internal/include/launchdarkly/data_sources/data_source.hpp +++ b/libs/internal/include/launchdarkly/data_sources/data_source.hpp @@ -1,19 +1,4 @@ #pragma once -#include -namespace launchdarkly::data_sources { - -class IDataSource { - public: - virtual void Start() = 0; - virtual void ShutdownAsync(std::function) = 0; - virtual ~IDataSource() = default; - IDataSource(IDataSource const& item) = delete; - IDataSource(IDataSource&& item) = delete; - IDataSource& operator=(IDataSource const&) = delete; - IDataSource& operator=(IDataSource&&) = delete; - protected: - IDataSource() = default; -}; - -} // namespace launchdarkly::data_sources +#include +namespace launchdarkly::data_sources {} // namespace launchdarkly::data_sources diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 00569be24..281597a93 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -23,13 +23,12 @@ target_sources(${LIBNAME} 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 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_sources/descriptors.hpp + data_sources/memory_store/memory_store.cpp data_sources/data_source_event_handler.cpp data_sources/data_source_event_handler.hpp data_sources/data_source_status.cpp @@ -38,6 +37,7 @@ target_sources(${LIBNAME} data_sources/data_source_status_manager.hpp data_sources/streaming_data_source.hpp data_sources/streaming_data_source.cpp + data_sources/pull_mode/pull_mode_data_source.cpp data_sources/null_data_source.cpp evaluation/evaluator.cpp evaluation/rules.cpp @@ -47,10 +47,10 @@ target_sources(${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 + # 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 events/event_factory.cpp bindings/c/sdk.cpp bindings/c/builder.cpp 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 34e1a839b..d2e06f351 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 @@ -2,7 +2,6 @@ #include -#include "../data_store/data_store.hpp" #include "../evaluation/evaluator.hpp" namespace launchdarkly::server_side { diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 5dfc5b56b..166ee6d46 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -9,8 +9,8 @@ #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/pull_mode/pull_mode_data_source.hpp" #include "data_sources/streaming_data_source.hpp" -#include "data_store/memory_store.hpp" #include #include @@ -34,33 +34,53 @@ 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); - } +// 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 std::unique_ptr MakeDataSource( + HttpProperties const& http_properties, + Config const& config, + boost::asio::any_io_executor const& executor, + data_sources::DataSourceStatusManager& status_manager, + Logger& logger) { auto builder = HttpPropertiesBuilder(http_properties); auto data_source_properties = builder.Build(); - if (config.DataSourceConfig().method.index() == 0) { - 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>( + // TODO: Check if config is a persistent Store (so, if 'method' is + // Persistent). If so, return a data_sources::PushModeSource instead. + + return std::make_unique( config.ServiceEndpoints(), config.DataSourceConfig(), - data_source_properties, executor, flag_updater, status_manager, logger); + data_source_properties, executor, status_manager, logger); } static Logger MakeLogger(config::shared::built::Logging const& config) { @@ -95,7 +115,7 @@ std::unique_ptr> MakeEventProcessor( * Returns true if the flag pointer is valid and the underlying item is present. */ bool IsFlagPresent( - std::shared_ptr const& flag_desc); + std::shared_ptr const& flag_desc); ClientImpl::ClientImpl(Config config, std::string const& version) : config_(config), @@ -108,20 +128,17 @@ ClientImpl::ClientImpl(Config config, std::string const& version) logger_(MakeLogger(config.Logging())), ioc_(kAsioConcurrencyHint), 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(), - data_store_updater_, status_manager_, logger_)), event_processor_(MakeEventProcessor(config, ioc_.get_executor(), http_properties_, logger_)), - evaluator_(logger_, memory_store_), + evaluator_(logger_, *data_source_), events_default_(event_processor_.get(), EventFactory::WithoutReasons()), events_with_reasons_(event_processor_.get(), EventFactory::WithReasons()) { @@ -159,7 +176,9 @@ std::future ClientImpl::StartAsyncInternal( return false; /* keep the change listener */ }); - data_source_->Start(); + if (auto synchronizer = data_source_->GetSynchronizer().lock()) { + synchronizer->Start(); + } return fut; } @@ -177,24 +196,31 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, std::unordered_map 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 {}; - } + // 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 {}; + // } + + // TODO: Fix to use the single data source status, which takes into + // account whether there is any data ore not. + return {}; } AllFlagsStateBuilder builder{options}; EventScope no_events; - for (auto const& [k, v] : memory_store_.AllFlags()) { + for (auto const& [k, v] : data_source_->AllFlags()) { if (!v || !v->item) { continue; } @@ -295,7 +321,7 @@ EvaluationDetail ClientImpl::VariationInternal( std::nullopt); } - auto flag_rule = memory_store_.GetFlag(key); + auto flag_rule = data_source_->GetFlag(key); bool flag_present = IsFlagPresent(flag_rule); @@ -315,9 +341,10 @@ EvaluationDetail ClientImpl::VariationInternal( std::optional ClientImpl::PreEvaluationChecks( Context const& context) { - if (!memory_store_.Initialized()) { - return EvaluationReason::ErrorKind::kClientNotReady; - } + // if (!memory_store_.Initialized()) { + // return EvaluationReason::ErrorKind::kClientNotReady; + // } + // TODO: Check if initialized if (!context.Valid()) { return EvaluationReason::ErrorKind::kUserNotSpecified; } @@ -364,7 +391,7 @@ EvaluationDetail ClientImpl::PostEvaluation( } bool IsFlagPresent( - std::shared_ptr const& flag_desc) { + std::shared_ptr const& flag_desc) { return flag_desc && flag_desc->item; } diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index d6c77ecee..626622d3a 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -13,8 +13,8 @@ #include "data_sources/data_source_status_manager.hpp" #include "data_sources/data_source_update_sink.hpp" +#include "data_sources/data_source_interface.hpp" #include "data_store/data_store_updater.hpp" -#include "data_store/memory_store.hpp" #include "evaluation/evaluator.hpp" @@ -173,12 +173,9 @@ class ClientImpl : public IClient { boost::asio::executor_work_guard work_; - 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_; + std::unique_ptr data_source_; std::unique_ptr event_processor_; 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 2d8c436b3..16140e271 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 @@ -214,13 +214,13 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( switch (res->kind) { case data_store::DataKind::kFlag: { handler_.Upsert(res->key, - data_store::FlagDescriptor(res->version)); + data_sources::FlagDescriptor(res->version)); return DataSourceEventHandler::MessageStatus:: kMessageHandled; } case data_store::DataKind::kSegment: { - handler_.Upsert( - res->key, data_store::SegmentDescriptor(res->version)); + handler_.Upsert(res->key, data_sources::SegmentDescriptor( + res->version)); return DataSourceEventHandler::MessageStatus:: kMessageHandled; } 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 index 798796506..44858e500 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp @@ -93,8 +93,7 @@ class DataSourceEventHandler { struct Patch { std::string key; - std::variant - data; + std::variant data; }; struct Delete { diff --git a/libs/server-sdk/src/data_sources/data_source_interface.hpp b/libs/server-sdk/src/data_sources/data_source_interface.hpp new file mode 100644 index 000000000..a478dcbb4 --- /dev/null +++ b/libs/server-sdk/src/data_sources/data_source_interface.hpp @@ -0,0 +1,108 @@ +#pragma once +#include "descriptors.hpp" + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class ISynchronizer; +class IBootstrapper; +class IDataDestination; + +class IDataSource { + public: + [[nodiscard]] virtual std::string Identity() const; + + [[nodiscard]] virtual std::weak_ptr GetSynchronizer() const; + [[nodiscard]] virtual std::weak_ptr GetBootstrapper() const; + + [[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; + + virtual ~IDataSource() = default; + IDataSource(IDataSource const& item) = delete; + IDataSource(IDataSource&& item) = delete; + IDataSource& operator=(IDataSource const&) = delete; + IDataSource& operator=(IDataSource&&) = delete; + + protected: + IDataSource() = default; +}; + +class ISynchronizer { + public: + virtual void Init(std::optional initial_data, + IDataDestination& destination) = 0; + virtual void Start() = 0; + virtual void ShutdownAsync(std::function) = 0; + virtual ~ISynchronizer() = default; + ISynchronizer(ISynchronizer const& item) = delete; + ISynchronizer(ISynchronizer&& item) = delete; + ISynchronizer& operator=(ISynchronizer const&) = delete; + ISynchronizer& operator=(ISynchronizer&&) = delete; + + protected: + ISynchronizer() = default; +}; + +class IBootstrapper { + public: + using Error = std::string; + virtual bool IsAuthoritative() const = 0; + virtual tl::expected FetchAll( + std::chrono::milliseconds timeout_hint) = 0; + virtual ~IBootstrapper() = default; + IBootstrapper(IBootstrapper const& item) = delete; + IBootstrapper(IBootstrapper&& item) = delete; + IBootstrapper& operator=(IBootstrapper const&) = delete; + IBootstrapper& operator=(IBootstrapper&&) = delete; + + protected: + IBootstrapper() = default; +}; + +class IDataDestination { + public: + virtual ~IDataDestination() = default; + IDataDestination(IDataDestination const& item) = delete; + IDataDestination(IDataDestination&& item) = delete; + IDataDestination& operator=(IDataDestination const&) = delete; + IDataDestination& operator=(IDataDestination&&) = delete; + + protected: + IDataDestination() = default; +}; +} // 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 294f82ee7..84aedd221 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 @@ -5,7 +5,7 @@ #include #include -#include "../data_store/descriptors.hpp" +#include "descriptors.hpp" namespace launchdarkly::server_side::data_sources { /** @@ -14,10 +14,8 @@ namespace launchdarkly::server_side::data_sources { 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; + virtual void Upsert(std::string const& key, FlagDescriptor flag) = 0; + virtual void Upsert(std::string const& key, SegmentDescriptor segment) = 0; IDataSourceUpdateSink(IDataSourceUpdateSink const& item) = delete; IDataSourceUpdateSink(IDataSourceUpdateSink&& item) = delete; diff --git a/libs/server-sdk/src/data_store/descriptors.hpp b/libs/server-sdk/src/data_sources/descriptors.hpp similarity index 77% rename from libs/server-sdk/src/data_store/descriptors.hpp rename to libs/server-sdk/src/data_sources/descriptors.hpp index 075d3a31d..6b8aa46fc 100644 --- a/libs/server-sdk/src/data_store/descriptors.hpp +++ b/libs/server-sdk/src/data_sources/descriptors.hpp @@ -4,9 +4,9 @@ #include #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_sources { using FlagDescriptor = launchdarkly::data_model::ItemDescriptor; using SegmentDescriptor = launchdarkly::data_model::ItemDescriptor; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_store/memory_store.cpp b/libs/server-sdk/src/data_sources/memory_store/memory_store.cpp similarity index 89% rename from libs/server-sdk/src/data_store/memory_store.cpp rename to libs/server-sdk/src/data_sources/memory_store/memory_store.cpp index 321c6249d..8e69552b9 100644 --- a/libs/server-sdk/src/data_store/memory_store.cpp +++ b/libs/server-sdk/src/data_sources/memory_store/memory_store.cpp @@ -2,7 +2,7 @@ #include "memory_store.hpp" -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_sources { std::shared_ptr MemoryStore::GetFlag( std::string const& key) const { @@ -61,15 +61,15 @@ void MemoryStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { } void MemoryStore::Upsert(std::string const& key, - data_store::FlagDescriptor flag) { + data_sources::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) { + data_sources::SegmentDescriptor segment) { std::lock_guard lock{data_mutex_}; segments_[key] = std::make_shared(std::move(segment)); } -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_store/memory_store.hpp b/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp similarity index 59% rename from libs/server-sdk/src/data_store/memory_store.hpp rename to libs/server-sdk/src/data_sources/memory_store/memory_store.hpp index f8758e4a8..0f8c7c928 100644 --- a/libs/server-sdk/src/data_store/memory_store.hpp +++ b/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp @@ -1,30 +1,25 @@ #pragma once -#include "../data_sources/data_source_update_sink.hpp" -#include "data_store.hpp" - #include #include #include #include +#include "../data_source_update_sink.hpp" -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_sources { -class MemoryStore : public IDataStore, - public data_sources::IDataSourceUpdateSink { +class MemoryStore : 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::shared_ptr GetFlag(std::string const& key) const; + std::shared_ptr GetSegment(std::string const& key) const; std::unordered_map> AllFlags() - const override; + const; std::unordered_map> - AllSegments() const override; + AllSegments() const; - bool Initialized() const override; - std::string const& Description() const override; + bool Initialized() const; + std::string const& Description() const; void Init(launchdarkly::data_model::SDKDataSet dataSet) override; void Upsert(std::string const& key, FlagDescriptor flag) override; @@ -39,7 +34,7 @@ class MemoryStore : public IDataStore, MemoryStore& operator=(MemoryStore&&) = delete; private: - static inline const std::string description_ = "memory"; + static inline std::string const description_ = "memory"; std::unordered_map> flags_; std::unordered_map> segments_; @@ -47,4 +42,4 @@ class MemoryStore : public IDataStore, mutable std::mutex data_mutex_; }; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/null_data_source.cpp b/libs/server-sdk/src/data_sources/null_data_source.cpp index 223dee39c..890676b1b 100644 --- a/libs/server-sdk/src/data_sources/null_data_source.cpp +++ b/libs/server-sdk/src/data_sources/null_data_source.cpp @@ -12,6 +12,9 @@ void NullDataSource::ShutdownAsync(std::function complete) { boost::asio::post(exec_, complete); } +void NullDataSource::Init(std::optional initial_data, + IDataDestination& destination) {} + NullDataSource::NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager) : status_manager_(status_manager), exec_(exec) {} diff --git a/libs/server-sdk/src/data_sources/null_data_source.hpp b/libs/server-sdk/src/data_sources/null_data_source.hpp index 7a1102f49..fb8d3873e 100644 --- a/libs/server-sdk/src/data_sources/null_data_source.hpp +++ b/libs/server-sdk/src/data_sources/null_data_source.hpp @@ -1,17 +1,19 @@ #pragma once +#include "data_source_interface.hpp" #include "data_source_status_manager.hpp" -#include - #include namespace launchdarkly::server_side::data_sources { -class NullDataSource : public ::launchdarkly::data_sources::IDataSource { +class NullDataSource : public ISynchronizer { public: explicit NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager); + + void Init(std::optional initial_data, + IDataDestination& destination) override; void Start() override; void ShutdownAsync(std::function) override; diff --git a/libs/server-sdk/src/data_sources/polling_data_source.cpp b/libs/server-sdk/src/data_sources/polling_data_source.cpp index 2f250de01..f91b160a0 100644 --- a/libs/server-sdk/src/data_sources/polling_data_source.cpp +++ b/libs/server-sdk/src/data_sources/polling_data_source.cpp @@ -23,16 +23,12 @@ 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::PollingConfig const& + polling_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; @@ -47,7 +43,7 @@ static network::HttpRequest MakeRequest( PollingDataSource::PollingDataSource( config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::DataSourceConfig const& + config::shared::built::PollingConfig const& data_source_config, config::shared::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& ioc, @@ -60,27 +56,24 @@ PollingDataSource::PollingDataSource( update_sink_(handler), requester_(ioc), timer_(ioc), - polling_interval_( - std::get< - config::shared::built::PollingConfig>( - data_source_config.method) - .poll_interval), + polling_interval_(data_source_config.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) { + if (polling_interval_ < data_source_config.min_polling_interval) { LD_LOG(logger_, LogLevel::kWarn) << "Polling interval too frequent, defaulting to " << std::chrono::duration_cast( - polling_config.min_polling_interval) + data_source_config.min_polling_interval) .count() << " seconds"; - polling_interval_ = polling_config.min_polling_interval; + polling_interval_ = data_source_config.min_polling_interval; } } +void PollingDataSource::Init(std::optional initial_data, + IDataDestination& destination) { + // TODO: implement +} void PollingDataSource::DoPoll() { last_poll_start_ = std::chrono::system_clock::now(); diff --git a/libs/server-sdk/src/data_sources/polling_data_source.hpp b/libs/server-sdk/src/data_sources/polling_data_source.hpp index 1d99417b3..4f3c11d5e 100644 --- a/libs/server-sdk/src/data_sources/polling_data_source.hpp +++ b/libs/server-sdk/src/data_sources/polling_data_source.hpp @@ -5,32 +5,34 @@ #include #include "data_source_event_handler.hpp" +#include "data_source_interface.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 ISynchronizer, 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::PollingConfig 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 Init(std::optional initial_data, + IDataDestination& destination) override; void Start() override; void ShutdownAsync(std::function completion) override; diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp new file mode 100644 index 000000000..70f8219c9 --- /dev/null +++ b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp @@ -0,0 +1,73 @@ +#include "pull_mode_data_source.hpp" + +#include "../polling_data_source.hpp" +#include "../streaming_data_source.hpp" + +namespace launchdarkly::server_side::data_sources { + +using namespace config::shared::built; + +PullModeSource::PullModeSource( + ServiceEndpoints const& endpoints, + DataSourceConfig const& data_source_config, + HttpProperties http_properties, + boost::asio::any_io_executor ioc, + DataSourceStatusManager& status_manager, + Logger const& logger) + : store_(), synchronizer_(), bootstrapper_() { + std::visit( + [&](auto&& method_config) { + using T = std::decay_t; + if constexpr (std::is_same_v< + T, StreamingConfig>) { + synchronizer_ = + std::make_shared( + endpoints, method_config, http_properties, ioc, store_, + status_manager, logger); + + } else if constexpr (std::is_same_v< + T, PollingConfig< + config::shared::ServerSDK>>) { + synchronizer_ = + std::make_shared( + endpoints, method_config, http_properties, ioc, store_, + status_manager, logger); + } + }, + data_source_config.method); +} + +std::string PullModeSource::Identity() const { + // TODO: Obtain more specific info + return "generic pull-mode source"; +} + +std::weak_ptr PullModeSource::GetSynchronizer() const { + return synchronizer_; +} + +std::weak_ptr PullModeSource::GetBootstrapper() const { + return bootstrapper_; +} + +std::shared_ptr PullModeSource::GetFlag( + std::string const& key) const { + return store_.GetFlag(key); +} + +std::shared_ptr PullModeSource::GetSegment( + std::string const& key) const { + return store_.GetSegment(key); +} + +std::unordered_map> +PullModeSource::AllFlags() const { + return store_.AllFlags(); +} + +std::unordered_map> +PullModeSource::AllSegments() const { + return store_.AllSegments(); +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp new file mode 100644 index 000000000..a8a7d72cb --- /dev/null +++ b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#include "../data_source_event_handler.hpp" +#include "../data_source_interface.hpp" +#include "../data_source_status_manager.hpp" +#include "../data_source_update_sink.hpp" + +#include "../memory_store/memory_store.hpp" + +#include + +namespace launchdarkly::server_side::data_sources { + +class PullModeSource : public IDataSource { + public: + PullModeSource(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, + DataSourceStatusManager& status_manager, + Logger const& logger); + + PullModeSource(PullModeSource const& item) = delete; + PullModeSource(PullModeSource&& item) = delete; + PullModeSource& operator=(PullModeSource const&) = delete; + PullModeSource& operator=(PullModeSource&&) = delete; + + std::string Identity() const override; + + std::weak_ptr GetSynchronizer() const override; + std::weak_ptr GetBootstrapper() const override; + + 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; + + private: + MemoryStore store_; + std::shared_ptr synchronizer_; + std::shared_ptr bootstrapper_; +}; +} // 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 index 0e8b36884..885a0d5ca 100644 --- a/libs/server-sdk/src/data_sources/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_sources/streaming_data_source.cpp @@ -30,7 +30,7 @@ static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { StreamingDataSource::StreamingDataSource( config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::DataSourceConfig const& + config::shared::built::StreamingConfig const& data_source_config, config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, @@ -43,11 +43,15 @@ StreamingDataSource::StreamingDataSource( data_source_handler_( DataSourceEventHandler(handler, logger, status_manager_)), http_config_(std::move(http_properties)), - streaming_config_( - std::get>(data_source_config.method)), + streaming_config_(data_source_config), streaming_endpoint_(endpoints.StreamingBaseUrl()) {} +void StreamingDataSource::Init( + std::optional initial_data, + IDataDestination& destination) { + // TODO: implement +} + void StreamingDataSource::Start() { status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.hpp b/libs/server-sdk/src/data_sources/streaming_data_source.hpp index 9663501f0..bb15f7ea7 100644 --- a/libs/server-sdk/src/data_sources/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_sources/streaming_data_source.hpp @@ -6,6 +6,7 @@ using namespace std::chrono_literals; #include #include "data_source_event_handler.hpp" +#include "data_source_interface.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" @@ -15,26 +16,27 @@ using namespace std::chrono_literals; #include #include #include -#include #include #include namespace launchdarkly::server_side::data_sources { class StreamingDataSource final - : public ::launchdarkly::data_sources::IDataSource, + : public ISynchronizer, 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::StreamingConfig 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 Init(std::optional initial_data, + IDataDestination& destination) override; void Start() override; void ShutdownAsync(std::function completion) override; diff --git a/libs/server-sdk/src/data_store/data_store.hpp b/libs/server-sdk/src/data_store/data_store.hpp deleted file mode 100644 index 3ae0184e0..000000000 --- a/libs/server-sdk/src/data_store/data_store.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#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 index 973a7d585..922fe7f63 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, source_.AllFlags(), data_set.flags, updated_items); - CalculateChanges(DataKind::kSegment, store_.AllSegments(), + CalculateChanges(DataKind::kSegment, source_.AllSegments(), data_set.segments, updated_items); change_notifications = updated_items; } @@ -45,13 +45,13 @@ 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)); + data_sources::FlagDescriptor flag) { + UpsertCommon(DataKind::kFlag, key, source_.GetFlag(key), std::move(flag)); } void DataStoreUpdater::Upsert(std::string const& key, - data_store::SegmentDescriptor segment) { - UpsertCommon(DataKind::kSegment, key, store_.GetSegment(key), + data_sources::SegmentDescriptor segment) { + UpsertCommon(DataKind::kSegment, key, source_.GetSegment(key), std::move(segment)); } @@ -70,7 +70,7 @@ void DataStoreUpdater::NotifyChanges(DependencySet changes) { } DataStoreUpdater::DataStoreUpdater(IDataSourceUpdateSink& sink, - IDataStore const& store) - : sink_(sink), store_(store) {} + data_sources::IDataSource const& source) + : sink_(sink), source_(source) {} } // 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 0b5a2e153..07913c374 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.hpp +++ b/libs/server-sdk/src/data_store/data_store_updater.hpp @@ -1,7 +1,7 @@ #pragma once +#include "../data_sources/data_source_interface.hpp" #include "../data_sources/data_source_update_sink.hpp" -#include "data_store.hpp" #include "dependency_tracker.hpp" #include @@ -26,13 +26,16 @@ class DataStoreUpdater using SharedCollection = std::unordered_map>; - DataStoreUpdater(IDataSourceUpdateSink& sink, IDataStore const& store); + DataStoreUpdater(IDataSourceUpdateSink& sink, + data_sources::IDataSource const& source); 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; + void Upsert(std::string const& key, + data_sources::FlagDescriptor flag) override; + void Upsert(std::string const& key, + data_sources::SegmentDescriptor segment) override; ~DataStoreUpdater() override = default; DataStoreUpdater(DataStoreUpdater const& item) = delete; @@ -102,7 +105,7 @@ class DataStoreUpdater void NotifyChanges(DependencySet changes); IDataSourceUpdateSink& sink_; - IDataStore const& store_; + data_sources::IDataSource const& source_; boost::signals2::signal)> signals_; 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 1c2663ec4..3fe209c8f 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 @@ -1,8 +1,7 @@ #pragma once +#include "../../data_sources/data_source_interface.hpp" #include "../../data_sources/data_source_update_sink.hpp" -#include "../data_store.hpp" -#include "../memory_store.hpp" #include "expiration_tracker.hpp" #include @@ -42,7 +41,7 @@ struct Kinds { static SegmentKind const Segment; }; -class PersistentStore : public IDataStore, +class PersistentStore : public data_sources::IDataSource, public data_sources::IDataSourceUpdateSink { public: PersistentStore( diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index 97057fbd2..22ba533d1 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -19,8 +19,8 @@ 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_() {} +Evaluator::Evaluator(Logger& logger, data_sources::IDataSource const& source) + : logger_(logger), source_(source), stack_() {} EvaluationDetail Evaluator::Evaluate( data_model::Flag const& flag, @@ -46,15 +46,15 @@ EvaluationDetail Evaluator::Evaluate( } for (Flag::Prerequisite const& p : flag.prerequisites) { - std::shared_ptr maybe_flag = - store_.GetFlag(p.key); + std::shared_ptr maybe_flag = + source_.GetFlag(p.key); if (!maybe_flag) { return OffValue(flag, EvaluationReason::PrerequisiteFailed(p.key)); } - data_store::FlagDescriptor const& descriptor = *maybe_flag; + data_sources::FlagDescriptor const& descriptor = *maybe_flag; if (!descriptor.item) { // This flag existed at some point, but has since been deleted. @@ -106,7 +106,7 @@ EvaluationDetail Evaluator::Evaluate( auto const& rule = flag.rules[rule_index]; tl::expected rule_match = - Match(rule, context, store_, stack_); + Match(rule, context, source_, stack_); if (!rule_match) { LogError(flag.key, rule_match.error()); diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp index cf7a15468..018f0a080 100644 --- a/libs/server-sdk/src/evaluation/evaluator.hpp +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -6,7 +6,7 @@ #include #include -#include "../data_store/data_store.hpp" +#include "../data_sources/data_source_interface.hpp" #include "../events/event_scope.hpp" #include "bucketing.hpp" #include "detail/evaluation_stack.hpp" @@ -18,7 +18,7 @@ namespace launchdarkly::server_side::evaluation { class Evaluator { public: - Evaluator(Logger& logger, data_store::IDataStore const& store); + Evaluator(Logger& logger, data_sources::IDataSource const& source); /** * Evaluates a flag for a given context. @@ -64,7 +64,7 @@ class Evaluator { void LogError(std::string const& key, Error const& error) const; Logger& logger_; - data_store::IDataStore const& store_; + data_sources::IDataSource const& source_; detail::EvaluationStack stack_; }; } // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp index 313cc44b1..381d5e93e 100644 --- a/libs/server-sdk/src/evaluation/rules.cpp +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -15,7 +15,7 @@ bool MaybeNegate(Clause const& clause, bool value) { tl::expected Match(Flag::Rule const& rule, launchdarkly::Context const& context, - data_store::IDataStore const& store, + data_sources::IDataSource const& store, detail::EvaluationStack& stack) { for (Clause const& clause : rule.clauses) { tl::expected result = Match(clause, context, store, stack); @@ -31,7 +31,7 @@ tl::expected Match(Flag::Rule const& rule, tl::expected Match(Segment::Rule const& rule, Context const& context, - data_store::IDataStore const& store, + data_sources::IDataSource const& store, detail::EvaluationStack& stack, std::string const& key, std::string const& salt) { @@ -61,7 +61,7 @@ tl::expected Match(Segment::Rule const& rule, tl::expected Match(Clause const& clause, launchdarkly::Context const& context, - data_store::IDataStore const& store, + data_sources::IDataSource const& store, detail::EvaluationStack& stack) { if (clause.op == Clause::Op::kSegmentMatch) { return MatchSegment(clause, context, store, stack); @@ -71,7 +71,7 @@ tl::expected Match(Clause const& clause, tl::expected MatchSegment(Clause const& clause, launchdarkly::Context const& context, - data_store::IDataStore const& store, + data_sources::IDataSource const& store, detail::EvaluationStack& stack) { for (Value const& value : clause.values) { // A segment key represented as a Value is a string; non-strings are @@ -82,7 +82,7 @@ tl::expected MatchSegment(Clause const& clause, std::string const& segment_key = value.AsString(); - std::shared_ptr segment_ptr = + std::shared_ptr segment_ptr = store.GetSegment(segment_key); if (!segment_ptr || !segment_ptr->item) { @@ -153,7 +153,7 @@ tl::expected MatchNonSegment( tl::expected Contains(Segment const& segment, Context const& context, - data_store::IDataStore const& store, + data_sources::IDataSource const& store, detail::EvaluationStack& stack) { auto guard = stack.NoticeSegment(segment.key); if (!guard) { diff --git a/libs/server-sdk/src/evaluation/rules.hpp b/libs/server-sdk/src/evaluation/rules.hpp index 5ca9a974e..67b380196 100644 --- a/libs/server-sdk/src/evaluation/rules.hpp +++ b/libs/server-sdk/src/evaluation/rules.hpp @@ -4,7 +4,7 @@ #include #include -#include "../data_store/data_store.hpp" +#include "../data_sources/data_source_interface.hpp" #include "detail/evaluation_stack.hpp" #include "evaluation_error.hpp" @@ -17,18 +17,18 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Match( data_model::Flag::Rule const&, Context const&, - data_store::IDataStore const& store, + data_sources::IDataSource const& source, detail::EvaluationStack& stack); [[nodiscard]] tl::expected Match(data_model::Clause const&, Context const&, - data_store::IDataStore const&, + data_sources::IDataSource const&, detail::EvaluationStack&); [[nodiscard]] tl::expected Match( data_model::Segment::Rule const& rule, Context const& context, - data_store::IDataStore const& store, + data_sources::IDataSource const& source, detail::EvaluationStack& stack, std::string const& key, std::string const& salt); @@ -36,7 +36,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected MatchSegment( data_model::Clause const&, Context const&, - data_store::IDataStore const&, + data_sources::IDataSource const&, detail::EvaluationStack& stack); [[nodiscard]] tl::expected MatchNonSegment( @@ -46,7 +46,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Contains( data_model::Segment const&, Context const&, - data_store::IDataStore const& store, + data_sources::IDataSource const& source, detail::EvaluationStack& stack); [[nodiscard]] bool MaybeNegate(data_model::Clause const& clause, bool value); From 4f563583d72f48990399399745712aaf68eb31e3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Sep 2023 13:54:17 -0700 Subject: [PATCH 061/244] rename push/pull: --- libs/server-sdk/src/CMakeLists.txt | 1 + libs/server-sdk/src/client_impl.cpp | 6 +- .../data_sources/data_source_interface.hpp | 3 + .../push_mode/push_mode_data_source.cpp | 73 +++++++++++++++++++ .../push_mode/push_mode_data_source.hpp | 53 ++++++++++++++ 5 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp create mode 100644 libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 281597a93..5916ad2e9 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -38,6 +38,7 @@ target_sources(${LIBNAME} data_sources/streaming_data_source.hpp data_sources/streaming_data_source.cpp data_sources/pull_mode/pull_mode_data_source.cpp + data_sources/push_mode/push_mode_data_source.cpp data_sources/null_data_source.cpp evaluation/evaluator.cpp evaluation/rules.cpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 166ee6d46..48345e19e 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -9,7 +9,7 @@ #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/pull_mode/pull_mode_data_source.hpp" +#include "data_sources/push_mode/push_mode_data_source.hpp" #include "data_sources/streaming_data_source.hpp" #include @@ -76,9 +76,9 @@ static std::unique_ptr MakeDataSource( auto data_source_properties = builder.Build(); // TODO: Check if config is a persistent Store (so, if 'method' is - // Persistent). If so, return a data_sources::PushModeSource instead. + // Persistent). If so, return a data_sources::PullModeSource instead. - return std::make_unique( + return std::make_unique( config.ServiceEndpoints(), config.DataSourceConfig(), data_source_properties, executor, status_manager, logger); } diff --git a/libs/server-sdk/src/data_sources/data_source_interface.hpp b/libs/server-sdk/src/data_sources/data_source_interface.hpp index a478dcbb4..f6ac10988 100644 --- a/libs/server-sdk/src/data_sources/data_source_interface.hpp +++ b/libs/server-sdk/src/data_sources/data_source_interface.hpp @@ -21,6 +21,9 @@ class IDataSource { [[nodiscard]] virtual std::weak_ptr GetSynchronizer() const; [[nodiscard]] virtual std::weak_ptr GetBootstrapper() const; + // TODO: Have a GetDataStore() interface? That way we don't need to forward + // methods. + [[nodiscard]] virtual std::shared_ptr GetFlag( std::string const& key) const = 0; diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp new file mode 100644 index 000000000..52d04b631 --- /dev/null +++ b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp @@ -0,0 +1,73 @@ +#include "push_mode_data_source.hpp" + +#include "../polling_data_source.hpp" +#include "../streaming_data_source.hpp" + +namespace launchdarkly::server_side::data_sources { + +using namespace config::shared::built; + +PushModeSource::PushModeSource( + ServiceEndpoints const& endpoints, + DataSourceConfig const& data_source_config, + HttpProperties http_properties, + boost::asio::any_io_executor ioc, + DataSourceStatusManager& status_manager, + Logger const& logger) + : store_(), synchronizer_(), bootstrapper_() { + std::visit( + [&](auto&& method_config) { + using T = std::decay_t; + if constexpr (std::is_same_v< + T, StreamingConfig>) { + synchronizer_ = + std::make_shared( + endpoints, method_config, http_properties, ioc, store_, + status_manager, logger); + + } else if constexpr (std::is_same_v< + T, PollingConfig< + config::shared::ServerSDK>>) { + synchronizer_ = + std::make_shared( + endpoints, method_config, http_properties, ioc, store_, + status_manager, logger); + } + }, + data_source_config.method); +} + +std::string PushModeSource::Identity() const { + // TODO: Obtain more specific info + return "generic pull-mode source"; +} + +std::weak_ptr PushModeSource::GetSynchronizer() const { + return synchronizer_; +} + +std::weak_ptr PushModeSource::GetBootstrapper() const { + return bootstrapper_; +} + +std::shared_ptr PushModeSource::GetFlag( + std::string const& key) const { + return store_.GetFlag(key); +} + +std::shared_ptr PushModeSource::GetSegment( + std::string const& key) const { + return store_.GetSegment(key); +} + +std::unordered_map> +PushModeSource::AllFlags() const { + return store_.AllFlags(); +} + +std::unordered_map> +PushModeSource::AllSegments() const { + return store_.AllSegments(); +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp new file mode 100644 index 000000000..19cdd82ae --- /dev/null +++ b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#include "../data_source_event_handler.hpp" +#include "../data_source_interface.hpp" +#include "../data_source_status_manager.hpp" +#include "../data_source_update_sink.hpp" + +#include "../memory_store/memory_store.hpp" + +#include + +namespace launchdarkly::server_side::data_sources { + +class PushModeSource : public IDataSource { + public: + PushModeSource(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, + DataSourceStatusManager& status_manager, + Logger const& logger); + + PushModeSource(PushModeSource const& item) = delete; + PushModeSource(PushModeSource&& item) = delete; + PushModeSource& operator=(PushModeSource const&) = delete; + PushModeSource& operator=(PushModeSource&&) = delete; + + std::string Identity() const override; + + std::weak_ptr GetSynchronizer() const override; + std::weak_ptr GetBootstrapper() const override; + + 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; + + private: + MemoryStore store_; + std::shared_ptr synchronizer_; + std::shared_ptr bootstrapper_; +}; +} // namespace launchdarkly::server_side::data_sources From b2cb6a4a0227fb8e6cc99c8b0413c4c19b96ffba Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Sep 2023 15:15:02 -0700 Subject: [PATCH 062/244] a lot of re-arranging --- architecture/server_store_arch.md | 10 +- .../launchdarkly/data_sources/data_source.hpp | 4 - libs/server-sdk/src/CMakeLists.txt | 20 +- libs/server-sdk/src/client_impl.hpp | 2 +- .../src/data_sources/adapters/README.md | 21 ++ .../data_source_event_handler.cpp | 2 +- .../data_source_event_handler.hpp | 6 +- .../data_sources/data_source_update_sink.hpp | 29 --- .../data_store_updater.cpp | 2 +- .../data_store_updater.hpp | 8 +- .../dependency_tracker.cpp | 0 .../dependency_tracker.hpp | 0 .../data_sources/implementations/README.md | 22 ++ .../noop}/null_data_source.cpp | 0 .../noop}/null_data_source.hpp | 0 .../polling}/polling_data_source.cpp | 0 .../polling}/polling_data_source.hpp | 0 .../streaming}/streaming_data_source.cpp | 0 .../streaming}/streaming_data_source.hpp | 0 .../interfaces/data_bootstrapper.hpp | 31 +++ .../interfaces/data_destination.hpp | 28 +++ .../data_source.hpp} | 47 +--- .../interfaces/data_synchronizer.hpp | 29 +++ .../memory_store/memory_store.hpp | 4 +- .../pull_mode}/data_kind.hpp | 0 .../pull_mode/data_version_inspectors.cpp | 0 .../pull_mode/data_version_inspectors.hpp | 34 +++ .../expiration}/expiration_tracker.cpp | 0 .../expiration}/expiration_tracker.hpp | 0 .../pull_mode/pull_mode_data_source.cpp | 225 +++++++++++++++--- .../pull_mode/pull_mode_data_source.hpp | 84 ++++++- .../pull_mode}/tagged_data.hpp | 0 .../push_mode/push_mode_data_source.cpp | 2 +- .../push_mode/push_mode_data_source.hpp | 6 +- .../persistent/persistent_data_store.cpp | 211 ---------------- .../persistent/persistent_data_store.hpp | 153 ------------ 36 files changed, 469 insertions(+), 511 deletions(-) delete mode 100644 libs/internal/include/launchdarkly/data_sources/data_source.hpp create mode 100644 libs/server-sdk/src/data_sources/adapters/README.md delete mode 100644 libs/server-sdk/src/data_sources/data_source_update_sink.hpp rename libs/server-sdk/src/{data_store => data_sources}/data_store_updater.cpp (97%) rename libs/server-sdk/src/{data_store => data_sources}/data_store_updater.hpp (95%) rename libs/server-sdk/src/{data_store => data_sources}/dependency_tracker.cpp (100%) rename libs/server-sdk/src/{data_store => data_sources}/dependency_tracker.hpp (100%) create mode 100644 libs/server-sdk/src/data_sources/implementations/README.md rename libs/server-sdk/src/data_sources/{ => implementations/noop}/null_data_source.cpp (100%) rename libs/server-sdk/src/data_sources/{ => implementations/noop}/null_data_source.hpp (100%) rename libs/server-sdk/src/data_sources/{ => implementations/polling}/polling_data_source.cpp (100%) rename libs/server-sdk/src/data_sources/{ => implementations/polling}/polling_data_source.hpp (100%) rename libs/server-sdk/src/data_sources/{ => implementations/streaming}/streaming_data_source.cpp (100%) rename libs/server-sdk/src/data_sources/{ => implementations/streaming}/streaming_data_source.hpp (100%) create mode 100644 libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp create mode 100644 libs/server-sdk/src/data_sources/interfaces/data_destination.hpp rename libs/server-sdk/src/data_sources/{data_source_interface.hpp => interfaces/data_source.hpp} (53%) create mode 100644 libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp rename libs/server-sdk/src/{data_store => data_sources/pull_mode}/data_kind.hpp (100%) create mode 100644 libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.cpp create mode 100644 libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.hpp rename libs/server-sdk/src/{data_store/persistent => data_sources/pull_mode/expiration}/expiration_tracker.cpp (100%) rename libs/server-sdk/src/{data_store/persistent => data_sources/pull_mode/expiration}/expiration_tracker.hpp (100%) rename libs/server-sdk/src/{data_store => data_sources/pull_mode}/tagged_data.hpp (100%) delete mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp delete 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 8b9a1ec31..54ecd7aab 100644 --- a/architecture/server_store_arch.md +++ b/architecture/server_store_arch.md @@ -8,11 +8,11 @@ classDiagram Client --* IChangeNotifier IDataStore <|-- MemoryStore - IDataSourceUpdateSink <|-- MemoryStore + IDataDestination <|-- MemoryStore IDataStore <|-- PersistentStore - IDataSourceUpdateSink <|-- PersistentStore - IDataSourceUpdateSink <|-- DataStoreUpdater + IDataDestination <|-- PersistentStore + IDataDestination <|-- DataStoreUpdater IChangeNotifier <|-- DataStoreUpdater @@ -55,7 +55,7 @@ classDiagram } - class IDataSourceUpdateSink{ + class IDataDestination{ <> +void Init(SDKDataSet allData) +void Upsert(std::string key, ItemDescriptor~Flag~ data) @@ -95,4 +95,4 @@ classDiagram class DataStoreUpdater{ } -``` \ No newline at end of file +``` diff --git a/libs/internal/include/launchdarkly/data_sources/data_source.hpp b/libs/internal/include/launchdarkly/data_sources/data_source.hpp deleted file mode 100644 index 0eba461a0..000000000 --- a/libs/internal/include/launchdarkly/data_sources/data_source.hpp +++ /dev/null @@ -1,4 +0,0 @@ -#pragma once - -#include -namespace launchdarkly::data_sources {} // namespace launchdarkly::data_sources diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 5916ad2e9..dbfdb2abe 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -22,24 +22,20 @@ target_sources(${LIBNAME} 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_updater.hpp - data_store/data_store_updater.cpp - data_store/dependency_tracker.hpp - data_store/dependency_tracker.cpp + data_sources/data_store_updater.cpp + data_sources/dependency_tracker.hpp + data_sources/dependency_tracker.cpp data_sources/descriptors.hpp data_sources/memory_store/memory_store.cpp 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/implementations/polling/polling_data_source.cpp + data_sources/implementations/streaming/streaming_data_source.cpp + data_sources/implementations/noop/null_data_source.cpp data_sources/data_source_status_manager.hpp - data_sources/streaming_data_source.hpp - data_sources/streaming_data_source.cpp data_sources/pull_mode/pull_mode_data_source.cpp data_sources/push_mode/push_mode_data_source.cpp - data_sources/null_data_source.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp @@ -48,10 +44,6 @@ target_sources(${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 events/event_factory.cpp bindings/c/sdk.cpp bindings/c/builder.cpp diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 626622d3a..337879f01 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -10,8 +10,8 @@ #include #include +#include "data_sources/data_destination_interface.hpp" #include "data_sources/data_source_status_manager.hpp" -#include "data_sources/data_source_update_sink.hpp" #include "data_sources/data_source_interface.hpp" #include "data_store/data_store_updater.hpp" diff --git a/libs/server-sdk/src/data_sources/adapters/README.md b/libs/server-sdk/src/data_sources/adapters/README.md new file mode 100644 index 000000000..f4db2ec54 --- /dev/null +++ b/libs/server-sdk/src/data_sources/adapters/README.md @@ -0,0 +1,21 @@ +## Adapters + +Contains utilities for converting from in-memory evaluation models to serialized models, and back again. + +This is a useful building block for implementing various Bootstrapper/Synchronizers/Destinations. + +For example, to build a new Destination that ferries data to Redis, you might pull the `JSONDestination` +off the shelf. + +It accepts memory models, serializes them, and forwards to any `ISerializedDataDestination`. + +``` +IDataDestination -> (serialization step) -> ISerializedDataDestination +``` + +On the other hand, to build a new Bootstrapper that pulls JSON from a web service, you might pull +the `JSONSource` off the shelf. It accepts JSON models, deserializes them, and forwards to any `IDataDestination`. + +``` +ISerializedDataDestination -> (deserialization step) -> IDataDestination +``` 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 16140e271..3110d0165 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 @@ -126,7 +126,7 @@ static tl::expected tag_invoke( } DataSourceEventHandler::DataSourceEventHandler( - IDataSourceUpdateSink& handler, + IDataDestination& handler, Logger const& logger, DataSourceStatusManager& status_manager) : handler_(handler), logger_(logger), status_manager_(status_manager) {} 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 index 44858e500..8c4eab562 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp @@ -5,8 +5,8 @@ #include #include "../data_store/data_kind.hpp" +#include "data_destination_interface.hpp" #include "data_source_status_manager.hpp" -#include "data_source_update_sink.hpp" #include #include @@ -102,7 +102,7 @@ class DataSourceEventHandler { uint64_t version; }; - DataSourceEventHandler(IDataSourceUpdateSink& handler, + DataSourceEventHandler(IDataDestination& handler, Logger const& logger, DataSourceStatusManager& status_manager); @@ -116,7 +116,7 @@ class DataSourceEventHandler { std::string const& data); private: - IDataSourceUpdateSink& handler_; + IDataDestination& handler_; Logger const& logger_; DataSourceStatusManager& status_manager_; }; 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 deleted file mode 100644 index 84aedd221..000000000 --- a/libs/server-sdk/src/data_sources/data_source_update_sink.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "descriptors.hpp" - -namespace launchdarkly::server_side::data_sources { -/** - * 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, FlagDescriptor flag) = 0; - virtual void Upsert(std::string const& key, 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_sources diff --git a/libs/server-sdk/src/data_store/data_store_updater.cpp b/libs/server-sdk/src/data_sources/data_store_updater.cpp similarity index 97% rename from libs/server-sdk/src/data_store/data_store_updater.cpp rename to libs/server-sdk/src/data_sources/data_store_updater.cpp index 922fe7f63..bbbadbe99 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.cpp +++ b/libs/server-sdk/src/data_sources/data_store_updater.cpp @@ -69,7 +69,7 @@ void DataStoreUpdater::NotifyChanges(DependencySet changes) { } } -DataStoreUpdater::DataStoreUpdater(IDataSourceUpdateSink& sink, +DataStoreUpdater::DataStoreUpdater(IDataDestination& sink, data_sources::IDataSource const& source) : sink_(sink), source_(source) {} diff --git a/libs/server-sdk/src/data_store/data_store_updater.hpp b/libs/server-sdk/src/data_sources/data_store_updater.hpp similarity index 95% rename from libs/server-sdk/src/data_store/data_store_updater.hpp rename to libs/server-sdk/src/data_sources/data_store_updater.hpp index 07913c374..90f5a2ea0 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.hpp +++ b/libs/server-sdk/src/data_sources/data_store_updater.hpp @@ -1,7 +1,7 @@ #pragma once +#include "../data_sources/data_destination_interface.hpp" #include "../data_sources/data_source_interface.hpp" -#include "../data_sources/data_source_update_sink.hpp" #include "dependency_tracker.hpp" #include @@ -13,7 +13,7 @@ namespace launchdarkly::server_side::data_store { class DataStoreUpdater - : public launchdarkly::server_side::data_sources::IDataSourceUpdateSink, + : public launchdarkly::server_side::data_sources::IDataDestination, public launchdarkly::server_side::IChangeNotifier { public: template @@ -26,7 +26,7 @@ class DataStoreUpdater using SharedCollection = std::unordered_map>; - DataStoreUpdater(IDataSourceUpdateSink& sink, + DataStoreUpdater(IDataDestination& sink, data_sources::IDataSource const& source); std::unique_ptr OnFlagChange(ChangeHandler handler) override; @@ -104,7 +104,7 @@ class DataStoreUpdater void NotifyChanges(DependencySet changes); - IDataSourceUpdateSink& sink_; + IDataDestination& sink_; data_sources::IDataSource const& source_; boost::signals2::signal)> signals_; diff --git a/libs/server-sdk/src/data_store/dependency_tracker.cpp b/libs/server-sdk/src/data_sources/dependency_tracker.cpp similarity index 100% rename from libs/server-sdk/src/data_store/dependency_tracker.cpp rename to libs/server-sdk/src/data_sources/dependency_tracker.cpp diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_sources/dependency_tracker.hpp similarity index 100% rename from libs/server-sdk/src/data_store/dependency_tracker.hpp rename to libs/server-sdk/src/data_sources/dependency_tracker.hpp diff --git a/libs/server-sdk/src/data_sources/implementations/README.md b/libs/server-sdk/src/data_sources/implementations/README.md new file mode 100644 index 000000000..79e33d4dd --- /dev/null +++ b/libs/server-sdk/src/data_sources/implementations/README.md @@ -0,0 +1,22 @@ +## Data Source Implementations + +This directory contains implementations of `IDataSource`. + +There are two primary schemes supported by the server-side SDK. + +One is to receive push-updates from a web-service (via Server-Sent Events). The other is to pull +data from a database as needed. + +### Push Data Source + +The Push data source is able to operate in "streaming" or "polling" mode. In either mode, flag +updates are delivered in the background from the perspective of the application. + +### Pull Data Source + +The Pull data source fetches flag data on-demand from a database. Because this is likely slow, +it operates a read-through cache wherein each stored item has a TTL indicating when it should be +re-fetched. + +When an item expires, it does not evict it from memory - often stale data is better +than no data. Eviction is disabled at this time, but may be added in a later version. diff --git a/libs/server-sdk/src/data_sources/null_data_source.cpp b/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.cpp similarity index 100% rename from libs/server-sdk/src/data_sources/null_data_source.cpp rename to libs/server-sdk/src/data_sources/implementations/noop/null_data_source.cpp diff --git a/libs/server-sdk/src/data_sources/null_data_source.hpp b/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp similarity index 100% rename from libs/server-sdk/src/data_sources/null_data_source.hpp rename to libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp diff --git a/libs/server-sdk/src/data_sources/polling_data_source.cpp b/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.cpp similarity index 100% rename from libs/server-sdk/src/data_sources/polling_data_source.cpp rename to libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.cpp diff --git a/libs/server-sdk/src/data_sources/polling_data_source.hpp b/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp similarity index 100% rename from libs/server-sdk/src/data_sources/polling_data_source.hpp rename to libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.cpp b/libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.cpp similarity index 100% rename from libs/server-sdk/src/data_sources/streaming_data_source.cpp rename to libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.cpp diff --git a/libs/server-sdk/src/data_sources/streaming_data_source.hpp b/libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.hpp similarity index 100% rename from libs/server-sdk/src/data_sources/streaming_data_source.hpp rename to libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.hpp diff --git a/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp b/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp new file mode 100644 index 000000000..e1959c5e4 --- /dev/null +++ b/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp @@ -0,0 +1,31 @@ +#pragma once +#include "../descriptors.hpp" + +#include + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class IBootstrapper { + public: + using Error = std::string; + virtual bool IsAuthoritative() const = 0; + virtual tl::expected FetchAll( + std::chrono::milliseconds timeout_hint) = 0; + virtual ~IBootstrapper() = default; + IBootstrapper(IBootstrapper const& item) = delete; + IBootstrapper(IBootstrapper&& item) = delete; + IBootstrapper& operator=(IBootstrapper const&) = delete; + IBootstrapper& operator=(IBootstrapper&&) = delete; + + protected: + IBootstrapper() = default; +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp b/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp new file mode 100644 index 000000000..158efd937 --- /dev/null +++ b/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +#include "descriptors.hpp" + +namespace launchdarkly::server_side::data_sources { + +class IDataDestination { + public: + virtual void Init(data_model::SDKDataSet data_set) = 0; + virtual void Upsert(std::string const& key, FlagDescriptor flag) = 0; + virtual void Upsert(std::string const& key, SegmentDescriptor segment) = 0; + virtual std::string Identity() const = 0; + + IDataDestination(IDataDestination const& item) = delete; + IDataDestination(IDataDestination&& item) = delete; + IDataDestination& operator=(IDataDestination const&) = delete; + IDataDestination& operator=(IDataDestination&&) = delete; + virtual ~IDataDestination() = default; + + protected: + IDataDestination() = default; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/data_source_interface.hpp b/libs/server-sdk/src/data_sources/interfaces/data_source.hpp similarity index 53% rename from libs/server-sdk/src/data_sources/data_source_interface.hpp rename to libs/server-sdk/src/data_sources/interfaces/data_source.hpp index f6ac10988..91e69ac01 100644 --- a/libs/server-sdk/src/data_sources/data_source_interface.hpp +++ b/libs/server-sdk/src/data_sources/interfaces/data_source.hpp @@ -18,8 +18,8 @@ class IDataSource { public: [[nodiscard]] virtual std::string Identity() const; - [[nodiscard]] virtual std::weak_ptr GetSynchronizer() const; - [[nodiscard]] virtual std::weak_ptr GetBootstrapper() const; + [[nodiscard]] virtual ISynchronizer* GetSynchronizer(); + [[nodiscard]] virtual IBootstrapper* GetBootstrapper(); // TODO: Have a GetDataStore() interface? That way we don't need to forward // methods. @@ -65,47 +65,4 @@ class IDataSource { IDataSource() = default; }; -class ISynchronizer { - public: - virtual void Init(std::optional initial_data, - IDataDestination& destination) = 0; - virtual void Start() = 0; - virtual void ShutdownAsync(std::function) = 0; - virtual ~ISynchronizer() = default; - ISynchronizer(ISynchronizer const& item) = delete; - ISynchronizer(ISynchronizer&& item) = delete; - ISynchronizer& operator=(ISynchronizer const&) = delete; - ISynchronizer& operator=(ISynchronizer&&) = delete; - - protected: - ISynchronizer() = default; -}; - -class IBootstrapper { - public: - using Error = std::string; - virtual bool IsAuthoritative() const = 0; - virtual tl::expected FetchAll( - std::chrono::milliseconds timeout_hint) = 0; - virtual ~IBootstrapper() = default; - IBootstrapper(IBootstrapper const& item) = delete; - IBootstrapper(IBootstrapper&& item) = delete; - IBootstrapper& operator=(IBootstrapper const&) = delete; - IBootstrapper& operator=(IBootstrapper&&) = delete; - - protected: - IBootstrapper() = default; -}; - -class IDataDestination { - public: - virtual ~IDataDestination() = default; - IDataDestination(IDataDestination const& item) = delete; - IDataDestination(IDataDestination&& item) = delete; - IDataDestination& operator=(IDataDestination const&) = delete; - IDataDestination& operator=(IDataDestination&&) = delete; - - protected: - IDataDestination() = default; -}; } // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp b/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp new file mode 100644 index 000000000..fd88929ed --- /dev/null +++ b/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp @@ -0,0 +1,29 @@ +#pragma once +#include "../descriptors.hpp" + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class ISynchronizer { + public: + virtual void Init(std::optional initial_data, + IDataDestination& destination) = 0; + virtual void Start() = 0; + virtual void ShutdownAsync(std::function) = 0; + virtual ~ISynchronizer() = default; + ISynchronizer(ISynchronizer const& item) = delete; + ISynchronizer(ISynchronizer&& item) = delete; + ISynchronizer& operator=(ISynchronizer const&) = delete; + ISynchronizer& operator=(ISynchronizer&&) = delete; + + protected: + ISynchronizer() = default; +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp b/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp index 0f8c7c928..df2983e63 100644 --- a/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp @@ -4,11 +4,11 @@ #include #include #include -#include "../data_source_update_sink.hpp" +#include "../data_destination_interface.hpp" namespace launchdarkly::server_side::data_sources { -class MemoryStore : public data_sources::IDataSourceUpdateSink { +class MemoryStore : public data_sources::IDataDestination { public: std::shared_ptr GetFlag(std::string const& key) const; std::shared_ptr GetSegment(std::string const& key) const; diff --git a/libs/server-sdk/src/data_store/data_kind.hpp b/libs/server-sdk/src/data_sources/pull_mode/data_kind.hpp similarity index 100% rename from libs/server-sdk/src/data_store/data_kind.hpp rename to libs/server-sdk/src/data_sources/pull_mode/data_kind.hpp diff --git a/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.cpp b/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.hpp b/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.hpp new file mode 100644 index 000000000..4648ce1cd --- /dev/null +++ b/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include + +namespace launchdarkly::server_side::data_sources::pull { +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 inline std::string const 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 inline std::string const namespace_ = "features"; +}; + +struct Kinds { + static FlagKind const Flag; + static SegmentKind const Segment; +}; +} // namespace launchdarkly::server_side::data_sources::pull diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp b/libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.cpp similarity index 100% rename from libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp rename to libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.cpp diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.hpp similarity index 100% rename from libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp rename to libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.hpp diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp index 70f8219c9..9e3e8ce42 100644 --- a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp +++ b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp @@ -3,6 +3,11 @@ #include "../polling_data_source.hpp" #include "../streaming_data_source.hpp" +#include +#include + +#include "data_version_inspectors.hpp" + namespace launchdarkly::server_side::data_sources { using namespace config::shared::built; @@ -14,60 +19,220 @@ PullModeSource::PullModeSource( boost::asio::any_io_executor ioc, DataSourceStatusManager& status_manager, Logger const& logger) - : store_(), synchronizer_(), bootstrapper_() { - std::visit( - [&](auto&& method_config) { - using T = std::decay_t; - if constexpr (std::is_same_v< - T, StreamingConfig>) { - synchronizer_ = - std::make_shared( - endpoints, method_config, http_properties, ioc, store_, - status_manager, logger); - - } else if constexpr (std::is_same_v< - T, PollingConfig< - config::shared::ServerSDK>>) { - synchronizer_ = - std::make_shared( - endpoints, method_config, http_properties, ioc, store_, - status_manager, logger); - } - }, - data_source_config.method); -} + : store_(), synchronizer_(), bootstrapper_() {} std::string PullModeSource::Identity() const { // TODO: Obtain more specific info return "generic pull-mode source"; } -std::weak_ptr PullModeSource::GetSynchronizer() const { - return synchronizer_; +ISynchronizer* PullModeSource::GetSynchronizer() { + return reinterpret_cast(this); } -std::weak_ptr PullModeSource::GetBootstrapper() const { - return bootstrapper_; +IBootstrapper* PullModeSource::GetBootstrapper() { + // Bootstrapping is not supported in Pull Mode sources yet. + // It would be simple: perform a get all. + return nullptr; +} + +void PullModeSource::Init(std::optional initial_data, + IDataDestination& destination) { + // TODO: implement + // This would happen if we bootstrapped from some source, and now that + // data is being passed here for management. + // The data would need to be sorted before passing furthur. +} +void PullModeSource::Start() { + // No-op, because we all data is pulled on demand. +} +void PullModeSource::ShutdownAsync(std::function) { + // Similar to Start, also a no-op since there's nothing to shutdown + // (perhaps explicitly close database client?) } std::shared_ptr PullModeSource::GetFlag( std::string const& key) const { - return store_.GetFlag(key); + auto state = tracker_.State(Keys::kAllSegments, time_()); + return Get>( + state, [this, &key]() { RefreshFlag(key); }, + [this, &key]() { return memory_store_.GetFlag(key); }); } std::shared_ptr PullModeSource::GetSegment( std::string const& key) const { - return store_.GetSegment(key); + auto state = tracker_.State(Keys::kAllSegments, time_()); + return Get>( + state, [this, &key]() { RefreshSegment(key); }, + [this, &key]() { return memory_store_.GetSegment(key); }); } std::unordered_map> PullModeSource::AllFlags() const { - return store_.AllFlags(); + auto state = tracker_.State(Keys::kAllFlags, time_()); + return Get< + std::unordered_map>>( + state, [this]() { RefreshAllFlags(); }, + [this]() { return memory_store_.AllFlags(); }); } std::unordered_map> PullModeSource::AllSegments() const { - return store_.AllSegments(); + auto state = tracker_.State(Keys::kAllSegments, time_()); + return Get< + std::unordered_map>>( + state, [this]() { RefreshAllSegments(); }, + [this]() { return memory_store_.AllSegments(); }); +} + +PersistentStore::FlagKind const PersistentStore::Kinds::Flag = FlagKind(); +PersistentStore::SegmentKind const PersistentStore::Kinds::Segment = + SegmentKind(); + +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); +} + +void PullModeSource::RefreshAllFlags() const { + auto res = core_->All(Kinds::Flag); + // TODO: Deserialize and put in store. + tracker_.Add(Keys::kAllSegments, time_()); +} + +void PullModeSource::RefreshAllSegments() const { + auto res = core_->All(Kinds::Segment); + // TODO: Deserialize and put in store. + tracker_.Add(Keys::kAllFlags, time_()); +} + +void PullModeSource::RefreshInitState() const { + initialized_ = core_->Initialized(); + tracker_.Add(Keys::kInitialized, time_()); +} + +void PullModeSource::RefreshSegment(std::string const& key) const { + auto res = core_->Get(Kinds::Segment, key); + 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 PullModeSource::RefreshFlag(std::string const& key) const { + 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? +} + +// TODO: Move this to the JSONSerializationAdapter +persistence::SerializedItemDescriptor PersistentStore::Serialize( + FlagDescriptor flag) { + // TODO: Implement + return persistence::SerializedItemDescriptor(); +} + +persistence::SerializedItemDescriptor PersistentStore::Serialize( + SegmentDescriptor segment) { + // TODO: Implement + 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) { + return Deserialize(flag); +} + +std::optional PersistentStore::DeserializeSegment( + persistence::SerializedItemDescriptor segment) { + 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 { + return namespace_; +} + +uint64_t PersistentStore::SegmentKind::Version(std::string const& data) const { + return GetVersion(data); +} + +std::string const& PersistentStore::FlagKind::Namespace() const { + return namespace_; +} + +uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { + return GetVersion(data); } } // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp index a8a7d72cb..2750e362d 100644 --- a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp +++ b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp @@ -5,10 +5,10 @@ #include #include +#include "../data_destination_interface.hpp" #include "../data_source_event_handler.hpp" #include "../data_source_interface.hpp" #include "../data_source_status_manager.hpp" -#include "../data_source_update_sink.hpp" #include "../memory_store/memory_store.hpp" @@ -16,7 +16,7 @@ namespace launchdarkly::server_side::data_sources { -class PullModeSource : public IDataSource { +class PullModeSource : public IDataSource, public ISynchronizer { public: PullModeSource(config::shared::built::ServiceEndpoints const& endpoints, config::shared::built::DataSourceConfig< @@ -33,8 +33,8 @@ class PullModeSource : public IDataSource { std::string Identity() const override; - std::weak_ptr GetSynchronizer() const override; - std::weak_ptr GetBootstrapper() const override; + ISynchronizer* GetSynchronizer() override; + IBootstrapper* GetBootstrapper() override; std::shared_ptr GetFlag( std::string const& key) const override; @@ -45,7 +45,83 @@ class PullModeSource : public IDataSource { std::unordered_map> AllSegments() const override; + /* ISynchronizer implementation */ + void Init(std::optional initial_data, + IDataDestination& destination) override; + void Start() override; + void ShutdownAsync(std::function) override; + private: + void RefreshAllFlags() const; + void RefreshAllSegments() const; + void RefreshInitState() const; + 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, + 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; + + ~SegmentKind() override = default; + + private: + static inline std::string const 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 inline std::string const namespace_ = "features"; + }; + + struct Kinds { + static FlagKind const Flag; + static SegmentKind const Segment; + }; + + struct Keys { + static inline std::string const kAllFlags = "allFlags"; + static inline std::string const kAllSegments = "allSegments"; + static inline std::string const kInitialized = "initialized"; + }; + MemoryStore store_; std::shared_ptr synchronizer_; std::shared_ptr bootstrapper_; diff --git a/libs/server-sdk/src/data_store/tagged_data.hpp b/libs/server-sdk/src/data_sources/pull_mode/tagged_data.hpp similarity index 100% rename from libs/server-sdk/src/data_store/tagged_data.hpp rename to libs/server-sdk/src/data_sources/pull_mode/tagged_data.hpp diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp index 52d04b631..7308ee90f 100644 --- a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp +++ b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp @@ -39,7 +39,7 @@ PushModeSource::PushModeSource( std::string PushModeSource::Identity() const { // TODO: Obtain more specific info - return "generic pull-mode source"; + return "generic push-mode source"; } std::weak_ptr PushModeSource::GetSynchronizer() const { diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp index 19cdd82ae..dc758c4cd 100644 --- a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp +++ b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp @@ -5,10 +5,10 @@ #include #include +#include "../data_destination_interface.hpp" #include "../data_source_event_handler.hpp" #include "../data_source_interface.hpp" #include "../data_source_status_manager.hpp" -#include "../data_source_update_sink.hpp" #include "../memory_store/memory_store.hpp" @@ -33,8 +33,8 @@ class PushModeSource : public IDataSource { std::string Identity() const override; - std::weak_ptr GetSynchronizer() const override; - std::weak_ptr GetBootstrapper() const override; + ISynchronizer* GetSynchronizer() const override; + IBootstrapper* GetBootstrapper() const override; std::shared_ptr GetFlag( std::string const& key) const override; 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 deleted file mode 100644 index f5c38f13a..000000000 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#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, - 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. - // TODO: Serialize the items. -} -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); - 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::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(); -} - -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) { - return Deserialize(flag); -} - -std::optional PersistentStore::DeserializeSegment( - persistence::SerializedItemDescriptor segment) { - 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 { - return namespace_; -} - -uint64_t PersistentStore::SegmentKind::Version(std::string const& data) const { - return GetVersion(data); -} - -std::string const& PersistentStore::FlagKind::Namespace() const { - return namespace_; -} - -uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { - 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 deleted file mode 100644 index 3fe209c8f..000000000 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -#include "../../data_sources/data_source_interface.hpp" -#include "../../data_sources/data_source_update_sink.hpp" -#include "expiration_tracker.hpp" - -#include -#include - -#include -#include -#include -#include - -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 inline std::string const 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 inline std::string const namespace_ = "features"; -}; - -struct Kinds { - static FlagKind const Flag; - static SegmentKind const Segment; -}; - -class PersistentStore : public data_sources::IDataSource, - 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( - 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: - void RefreshAllFlags() const; - void RefreshAllSegments() const; - void RefreshInitState() const; - 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, - std::function get) { - switch (state) { - case ExpirationTracker::TrackState::kStale: - [[fallthrough]]; - case ExpirationTracker::TrackState::kNotTracked: - refresh(); - [[fallthrough]]; - case ExpirationTracker::TrackState::kFresh: - return get(); - default: - launchdarkly::detail::unreachable(); - } - } - - 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; - - ~SegmentKind() override = default; - - private: - static inline std::string const 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 inline std::string const namespace_ = "features"; - }; - - struct Kinds { - static FlagKind const Flag; - static SegmentKind const Segment; - }; - - struct Keys { - static inline std::string const kAllFlags = "allFlags"; - static inline std::string const kAllSegments = "allSegments"; - static inline std::string const kInitialized = "initialized"; - }; -}; - -} // namespace launchdarkly::server_side::data_store::persistent From 17bcc1d587acc86db89c350837a94466a599eea5 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 20 Sep 2023 18:29:58 -0700 Subject: [PATCH 063/244] re-integrating the code that was in persistent_store_core into new model --- .../persistence/persistent_store_core.hpp | 216 ------------------ libs/server-sdk/src/CMakeLists.txt | 2 + .../src/data_sources/adapters/README.md | 7 + .../adapters/json_destination.cpp | 24 ++ .../adapters/json_destination.hpp | 21 ++ .../src/data_sources/adapters/json_source.cpp | 56 +++++ .../src/data_sources/adapters/json_source.hpp | 22 ++ .../implementations/noop/null_data_source.hpp | 5 +- .../polling/polling_data_source.hpp | 9 +- .../interfaces/data_bootstrapper.hpp | 1 + .../interfaces/data_destination.hpp | 4 +- .../data_sources/interfaces/data_source.hpp | 2 +- .../interfaces/data_source_lite.hpp | 56 +++++ .../interfaces/data_synchronizer.hpp | 3 + .../serialized_data_destination.hpp | 77 +++++++ .../interfaces/serialized_data_source.hpp | 76 ++++++ .../interfaces/serialized_descriptors.hpp | 99 ++++++++ .../pull_mode/pull_mode_data_source.cpp | 93 +------- .../pull_mode/pull_mode_data_source.hpp | 7 +- .../push_mode/push_mode_data_source.cpp | 13 +- .../push_mode/push_mode_data_source.hpp | 11 +- 21 files changed, 475 insertions(+), 329 deletions(-) delete mode 100644 libs/common/include/launchdarkly/persistence/persistent_store_core.hpp create mode 100644 libs/server-sdk/src/data_sources/adapters/json_destination.cpp create mode 100644 libs/server-sdk/src/data_sources/adapters/json_destination.hpp create mode 100644 libs/server-sdk/src/data_sources/adapters/json_source.cpp create mode 100644 libs/server-sdk/src/data_sources/adapters/json_source.hpp create mode 100644 libs/server-sdk/src/data_sources/interfaces/data_source_lite.hpp create mode 100644 libs/server-sdk/src/data_sources/interfaces/serialized_data_destination.hpp create mode 100644 libs/server-sdk/src/data_sources/interfaces/serialized_data_source.hpp create mode 100644 libs/server-sdk/src/data_sources/interfaces/serialized_descriptors.hpp diff --git a/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp deleted file mode 100644 index 515db0b69..000000000 --- a/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp +++ /dev/null @@ -1,216 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -#include - -namespace launchdarkly::persistence { - -/** - * 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() const = 0; - - /** - * 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. - * - * 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 = 0; - - 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::persistence diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index dbfdb2abe..23f5dcda8 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -27,6 +27,8 @@ target_sources(${LIBNAME} data_sources/dependency_tracker.cpp data_sources/descriptors.hpp data_sources/memory_store/memory_store.cpp + data_sources/adapters/json_destination.cpp + data_sources/adapters/json_source.cpp data_sources/data_source_event_handler.cpp data_sources/data_source_event_handler.hpp data_sources/data_source_status.cpp diff --git a/libs/server-sdk/src/data_sources/adapters/README.md b/libs/server-sdk/src/data_sources/adapters/README.md index f4db2ec54..29e7b32db 100644 --- a/libs/server-sdk/src/data_sources/adapters/README.md +++ b/libs/server-sdk/src/data_sources/adapters/README.md @@ -13,6 +13,13 @@ It accepts memory models, serializes them, and forwards to any `ISerializedDataD IDataDestination -> (serialization step) -> ISerializedDataDestination ``` +To handle pulling data out of Redis, use the `JSONSource`. It pulls from any `ISerializedDataSource`, deserializes it, +and passes it back up by implementing `IDataSource`. + +``` +IDataSource <- (deserialization step) <- ISerializedDataSource +``` + On the other hand, to build a new Bootstrapper that pulls JSON from a web service, you might pull the `JSONSource` off the shelf. It accepts JSON models, deserializes them, and forwards to any `IDataDestination`. diff --git a/libs/server-sdk/src/data_sources/adapters/json_destination.cpp b/libs/server-sdk/src/data_sources/adapters/json_destination.cpp new file mode 100644 index 000000000..19f1e19f7 --- /dev/null +++ b/libs/server-sdk/src/data_sources/adapters/json_destination.cpp @@ -0,0 +1,24 @@ +#include "json_destination.hpp" + +namespace launchdarkly::server_side::data_sources::adapters { + +JsonDestination::JsonDestination(ISerializedDataDestination& destination) + : dest_(destination) {} + +void JsonDestination::Init(data_model::SDKDataSet data_set) { + // TODO: serialize and forward to dest_.Init +} + +void JsonDestination::Upsert(std::string const& key, FlagDescriptor flag) { + // TODO: serialize and forward to dest_.Upsert +} + +void JsonDestination::Upsert(std::string const& key, + SegmentDescriptor segment) { + // TODO: serialize and forward to dest_.Upsert +} + +std::string JsonDestination::Identity() const { + return dest_.Identity(); +} +} // namespace launchdarkly::server_side::data_sources::adapters diff --git a/libs/server-sdk/src/data_sources/adapters/json_destination.hpp b/libs/server-sdk/src/data_sources/adapters/json_destination.hpp new file mode 100644 index 000000000..afb2e4188 --- /dev/null +++ b/libs/server-sdk/src/data_sources/adapters/json_destination.hpp @@ -0,0 +1,21 @@ + +#include "../descriptors.hpp" +#include "../interfaces/data_destination.hpp" +#include "../interfaces/serialized_data_destination.hpp" + +namespace launchdarkly::server_side::data_sources::adapters { + +class JsonDestination : public IDataDestination { + public: + JsonDestination(ISerializedDataDestination& destination); + + void Init(data_model::SDKDataSet data_set) override; + void Upsert(std::string& key, FlagDescriptor flag) override; + void Upsert(std::string& key, SegmentDescriptor segment) override; + std::string Identity() const override; + + private: + ISerializedDataDestination& dest_; +}; + +} // namespace launchdarkly::server_side::data_sources::adapters diff --git a/libs/server-sdk/src/data_sources/adapters/json_source.cpp b/libs/server-sdk/src/data_sources/adapters/json_source.cpp new file mode 100644 index 000000000..076f11caf --- /dev/null +++ b/libs/server-sdk/src/data_sources/adapters/json_source.cpp @@ -0,0 +1,56 @@ +#include "json_source.hpp" + +namespace launchdarkly::server_side::data_sources::adapters { + +template +static std::optional> Deserialize( + 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; +} + +FlagDescriptor JsonSource::GetFlag(std::string& key) const { + // TODO: deserialize then return + ISerializedDataSource::GetResult result = source_.Get(kind, key); +} +SegmentDescriptor JsonSource::GetSegment(std::string& key) const { + // TODO: deserialize then return + ISerializedDataSource::GetResult result = source_.Get(kind, key); +} +std::unordered_map JsonSource::AllFlags() const { + // TODO: deserialize then return + ISerializedDataSource::GetResult result = source_.All(kind); +} +std::unordered_map JsonSource::AllSegments() + const { + // TODO: deserialize then return + + ISerializedDataSource::GetResult result = source_.All(kind); +} + +std::string JsonSource::Identity() const { + return source_.Identity(); +} + +} // namespace launchdarkly::server_side::data_sources::adapters diff --git a/libs/server-sdk/src/data_sources/adapters/json_source.hpp b/libs/server-sdk/src/data_sources/adapters/json_source.hpp new file mode 100644 index 000000000..88de61076 --- /dev/null +++ b/libs/server-sdk/src/data_sources/adapters/json_source.hpp @@ -0,0 +1,22 @@ + +#include "../descriptors.hpp" +#include "../interfaces/data_source_lite.hpp" +#include "../interfaces/serialized_data_source.hpp" + +namespace launchdarkly::server_side::data_sources::adapters { + +class JsonSource : public IDataSourceLite { + public: + FlagDescriptor GetFlag(std::string& key) const override; + SegmentDescriptor GetSegment(std::string& key) const override; + std::unordered_map AllFlags() const override; + std::unordered_map AllSegments() + const override; + std::string Identity() const override; + + public: + private: + ISerializedDataSource& source_; +}; + +} // namespace launchdarkly::server_side::data_sources::adapters diff --git a/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp b/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp index fb8d3873e..12f6faae7 100644 --- a/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp @@ -1,7 +1,8 @@ #pragma once -#include "data_source_interface.hpp" -#include "data_source_status_manager.hpp" +#include "../../data_source_status_manager.hpp" +#include "../../interfaces/data_destination.hpp" +#include "../../interfaces/data_synchronizer.hpp" #include diff --git a/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp b/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp index 4f3c11d5e..8e9d64468 100644 --- a/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp @@ -4,10 +4,10 @@ #include -#include "data_source_event_handler.hpp" -#include "data_source_interface.hpp" -#include "data_source_status_manager.hpp" -#include "data_source_update_sink.hpp" +#include "../../data_source_event_handler.hpp" +#include "../../data_source_status_manager.hpp" +#include "../../interfaces/data_destination.hpp" +#include "../../interfaces/data_synchronizer.hpp" #include #include @@ -27,7 +27,6 @@ class PollingDataSource 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); diff --git a/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp b/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp index e1959c5e4..223083d36 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp +++ b/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp @@ -18,6 +18,7 @@ class IBootstrapper { virtual bool IsAuthoritative() const = 0; virtual tl::expected FetchAll( std::chrono::milliseconds timeout_hint) = 0; + virtual std::string const& Identity() const = 0; virtual ~IBootstrapper() = default; IBootstrapper(IBootstrapper const& item) = delete; IBootstrapper(IBootstrapper&& item) = delete; diff --git a/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp b/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp index 158efd937..9e5fc5355 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp +++ b/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp @@ -5,7 +5,7 @@ #include #include -#include "descriptors.hpp" +#include "../descriptors.hpp" namespace launchdarkly::server_side::data_sources { @@ -14,7 +14,7 @@ class IDataDestination { virtual void Init(data_model::SDKDataSet data_set) = 0; virtual void Upsert(std::string const& key, FlagDescriptor flag) = 0; virtual void Upsert(std::string const& key, SegmentDescriptor segment) = 0; - virtual std::string Identity() const = 0; + virtual std::string const& Identity() const = 0; IDataDestination(IDataDestination const& item) = delete; IDataDestination(IDataDestination&& item) = delete; diff --git a/libs/server-sdk/src/data_sources/interfaces/data_source.hpp b/libs/server-sdk/src/data_sources/interfaces/data_source.hpp index 91e69ac01..c9c02cea9 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_source.hpp +++ b/libs/server-sdk/src/data_sources/interfaces/data_source.hpp @@ -16,7 +16,7 @@ class IDataDestination; class IDataSource { public: - [[nodiscard]] virtual std::string Identity() const; + [[nodiscard]] virtual std::string const& Identity() const; [[nodiscard]] virtual ISynchronizer* GetSynchronizer(); [[nodiscard]] virtual IBootstrapper* GetBootstrapper(); diff --git a/libs/server-sdk/src/data_sources/interfaces/data_source_lite.hpp b/libs/server-sdk/src/data_sources/interfaces/data_source_lite.hpp new file mode 100644 index 000000000..8dd5472e9 --- /dev/null +++ b/libs/server-sdk/src/data_sources/interfaces/data_source_lite.hpp @@ -0,0 +1,56 @@ +#pragma once +#include "../descriptors.hpp" + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_sources { + +class IDataSourceLite { + public: + [[nodiscard]] virtual FlagDescriptor 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 SegmentDescriptor 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; + + [[nodiscard]] virtual std::string const& Identity() const = 0; + + virtual ~IDataSourceLite() = default; + IDataSourceLite(IDataSourceLite const& item) = delete; + IDataSourceLite(IDataSourceLite&& item) = delete; + IDataSourceLite& operator=(IDataSourceLite const&) = delete; + IDataSourceLite& operator=(IDataSourceLite&&) = delete; + + protected: + IDataSourceLite() = default; +}; + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp b/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp index fd88929ed..ca595ef2f 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp +++ b/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp @@ -16,6 +16,9 @@ class ISynchronizer { IDataDestination& destination) = 0; virtual void Start() = 0; virtual void ShutdownAsync(std::function) = 0; + + virtual std::string const& Identity() const = 0; + virtual ~ISynchronizer() = default; ISynchronizer(ISynchronizer const& item) = delete; ISynchronizer(ISynchronizer&& item) = delete; diff --git a/libs/server-sdk/src/data_sources/interfaces/serialized_data_destination.hpp b/libs/server-sdk/src/data_sources/interfaces/serialized_data_destination.hpp new file mode 100644 index 000000000..53774d90c --- /dev/null +++ b/libs/server-sdk/src/data_sources/interfaces/serialized_data_destination.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include "serialized_descriptors.hpp" + +#include +#include +#include +#include + +#include "../descriptors.hpp" + +// TODO: refactor this so that it's more generic to "serialization" and not +// directly +// tied to persistence concept. +#include + +namespace launchdarkly::server_side::data_sources { + +class ISerializedDataDestination { + 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 + }; + + using ItemKey = std::string; + using KeyItemPair = std::pair; + using OrderedNamepace = std::vector; + using KindCollectionPair = + std::pair; + using OrderedData = std::vector; + + virtual InitResult Init(OrderedData sdk_data_set) = 0; + + virtual UpsertResult Upsert(std::string const& kind, + std::string const& key, + SerializedItemDescriptor item) = 0; + + virtual std::string Identity() const = 0; + + ISerializedDataDestination(ISerializedDataDestination const& item) = delete; + ISerializedDataDestination(ISerializedDataDestination&& item) = delete; + ISerializedDataDestination& operator=(ISerializedDataDestination const&) = + delete; + ISerializedDataDestination& operator=(ISerializedDataDestination&&) = + delete; + virtual ~ISerializedDataDestination() = default; + + protected: + ISerializedDataDestination() = default; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/interfaces/serialized_data_source.hpp b/libs/server-sdk/src/data_sources/interfaces/serialized_data_source.hpp new file mode 100644 index 000000000..6f406a115 --- /dev/null +++ b/libs/server-sdk/src/data_sources/interfaces/serialized_data_source.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include "serialized_descriptors.hpp" + +#include + +#include +#include + +namespace launchdarkly::server_side::data_sources { + +/** + * Interface for a data source that provides feature flags and related data in a + * serialized form. + * + * This interface should be used for database integrations, or any other data + * source implementation that retrieves data from some external service. + * + * The SDK will take care of converting between its own internal data model and + * a serialized string form; the source interacts only with the serialized + * form. + * + * The SDK will also provide its own caching layer in front of this source; + * this source should not provide caching, but simply + * do every query or update that the SDK tells it to do. + * + * Implementations must be thread-safe. + */ +class ISerializedDataSource { + public: + virtual ~ISerializedDataSource() = default; + ISerializedDataSource(ISerializedDataSource const& item) = delete; + ISerializedDataSource(ISerializedDataSource&& item) = delete; + ISerializedDataSource& operator=(ISerializedDataSource const&) = delete; + ISerializedDataSource& operator=(ISerializedDataSource&&) = delete; + + struct Error { + std::string message; + }; + + using GetResult = + tl::expected, Error>; + + using AllResult = + tl::expected, + Error>; + + /** + * 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; + + virtual std::string const& Identity() const = 0; + + protected: + ISerializedDataSource() = default; +}; +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/interfaces/serialized_descriptors.hpp b/libs/server-sdk/src/data_sources/interfaces/serialized_descriptors.hpp new file mode 100644 index 000000000..9dbfe6ea3 --- /dev/null +++ b/libs/server-sdk/src/data_sources/interfaces/serialized_descriptors.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include +#include + +namespace launchdarkly::server_side::data_sources { + +/** + * A versioned item which can be stored in a persistent store. + */ +struct SerializedItemDescriptor { + std::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() const = 0; + + /** + * 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. + * + * 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 = 0; + + IPersistentKind(IPersistentKind const& item) = delete; + IPersistentKind(IPersistentKind&& item) = delete; + IPersistentKind& operator=(IPersistentKind const&) = delete; + IPersistentKind& operator=(IPersistentKind&&) = delete; + virtual ~IPersistentKind() = default; + + protected: + IPersistentKind() = default; +}; + +// TODO Find out where to put these + +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 { + return namespace_; +} + +uint64_t PersistentStore::SegmentKind::Version(std::string const& data) const { + return GetVersion(data); +} + +std::string const& PersistentStore::FlagKind::Namespace() const { + return namespace_; +} + +uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { + return GetVersion(data); +} + +} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp index 9e3e8ce42..f32ab9eec 100644 --- a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp +++ b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp @@ -1,7 +1,7 @@ #include "pull_mode_data_source.hpp" -#include "../polling_data_source.hpp" -#include "../streaming_data_source.hpp" +#include "../adapters/json_destination.hpp" +#include "../adapters/json_source.hpp" #include #include @@ -21,9 +21,9 @@ PullModeSource::PullModeSource( Logger const& logger) : store_(), synchronizer_(), bootstrapper_() {} -std::string PullModeSource::Identity() const { +std::string const& PullModeSource::Identity() const { // TODO: Obtain more specific info - return "generic pull-mode source"; + static std::string id = "generic pull-mode source"; } ISynchronizer* PullModeSource::GetSynchronizer() { @@ -150,89 +150,4 @@ void PullModeSource::RefreshFlag(std::string const& key) const { // TODO: If there is an actual error, then do we not reset the tracking? } -// TODO: Move this to the JSONSerializationAdapter -persistence::SerializedItemDescriptor PersistentStore::Serialize( - FlagDescriptor flag) { - // TODO: Implement - return persistence::SerializedItemDescriptor(); -} - -persistence::SerializedItemDescriptor PersistentStore::Serialize( - SegmentDescriptor segment) { - // TODO: Implement - 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) { - return Deserialize(flag); -} - -std::optional PersistentStore::DeserializeSegment( - persistence::SerializedItemDescriptor segment) { - 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 { - return namespace_; -} - -uint64_t PersistentStore::SegmentKind::Version(std::string const& data) const { - return GetVersion(data); -} - -std::string const& PersistentStore::FlagKind::Namespace() const { - return namespace_; -} - -uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { - return GetVersion(data); -} - } // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp index 2750e362d..ba5e76193 100644 --- a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp +++ b/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp @@ -5,9 +5,10 @@ #include #include -#include "../data_destination_interface.hpp" +#include "../interfaces/data_source.hpp" +#include "../interfaces/data_synchronizer.hpp" + #include "../data_source_event_handler.hpp" -#include "../data_source_interface.hpp" #include "../data_source_status_manager.hpp" #include "../memory_store/memory_store.hpp" @@ -31,7 +32,7 @@ class PullModeSource : public IDataSource, public ISynchronizer { PullModeSource& operator=(PullModeSource const&) = delete; PullModeSource& operator=(PullModeSource&&) = delete; - std::string Identity() const override; + std::string const& Identity() const override; ISynchronizer* GetSynchronizer() override; IBootstrapper* GetBootstrapper() override; diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp index 7308ee90f..531d4c341 100644 --- a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp +++ b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp @@ -37,17 +37,18 @@ PushModeSource::PushModeSource( data_source_config.method); } -std::string PushModeSource::Identity() const { +std::string const& PushModeSource::Identity() const { // TODO: Obtain more specific info - return "generic push-mode source"; + static std::string id = "generic push-mode source"; + return id; } -std::weak_ptr PushModeSource::GetSynchronizer() const { - return synchronizer_; +ISynchronizer* PushModeSource::GetSynchronizer() { + return synchronizer_.get(); } -std::weak_ptr PushModeSource::GetBootstrapper() const { - return bootstrapper_; +IBootstrapper* PushModeSource::GetBootstrapper() { + return bootstrapper_.get(); } std::shared_ptr PushModeSource::GetFlag( diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp index dc758c4cd..473d5cf1c 100644 --- a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp +++ b/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp @@ -5,9 +5,10 @@ #include #include -#include "../data_destination_interface.hpp" +#include "../interfaces/data_destination.hpp" +#include "../interfaces/data_source.hpp" + #include "../data_source_event_handler.hpp" -#include "../data_source_interface.hpp" #include "../data_source_status_manager.hpp" #include "../memory_store/memory_store.hpp" @@ -31,10 +32,10 @@ class PushModeSource : public IDataSource { PushModeSource& operator=(PushModeSource const&) = delete; PushModeSource& operator=(PushModeSource&&) = delete; - std::string Identity() const override; + std::string const& Identity() const override; - ISynchronizer* GetSynchronizer() const override; - IBootstrapper* GetBootstrapper() const override; + ISynchronizer* GetSynchronizer() override; + IBootstrapper* GetBootstrapper() override; std::shared_ptr GetFlag( std::string const& key) const override; From 6712ac92182364cfda67470a54d755eaf46e1cc2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 22 Sep 2023 16:10:26 -0700 Subject: [PATCH 064/244] large refactoring of directory structure --- .../config/shared/built/persistence.hpp | 9 ----- .../launchdarkly/data_model/descriptors.hpp | 10 +++++ .../server_side/data_source_status.hpp | 4 +- libs/server-sdk/src/CMakeLists.txt | 30 +++++++-------- libs/server-sdk/src/bindings/c/sdk.cpp | 2 +- libs/server-sdk/src/client_impl.cpp | 6 +-- .../change_notifier_destination.cpp} | 6 +-- .../change_notifier_destination.hpp} | 8 ++-- .../dependency_tracker}/data_kind.hpp | 0 .../dependency_tracker.cpp | 0 .../dependency_tracker.hpp | 11 +++--- .../dependency_tracker}/tagged_data.hpp | 0 .../interfaces/data_bootstrapper.hpp | 0 .../data_dest}/data_destination.hpp | 4 +- .../serialized_data_destination.hpp | 15 ++------ .../data_source/data_pull_source.hpp} | 30 ++++++++------- .../data_source/data_push_source.hpp} | 20 +++++----- .../serialzied_data_pull_source.hpp} | 19 +++++----- .../interfaces/data_system.hpp} | 37 +++++++++---------- .../interfaces/serialized_descriptors.hpp | 4 +- .../memory_store/memory_store.cpp | 4 +- .../memory_store/memory_store.hpp | 4 +- .../serialization_adapters}/README.md | 0 .../json_destination.cpp | 4 +- .../json_destination.hpp | 4 +- .../json_pull_source.cpp} | 6 +-- .../json_pull_source.hpp} | 10 ++--- .../sources}/README.md | 0 .../sources}/noop/null_data_source.cpp | 4 +- .../sources}/noop/null_data_source.hpp | 6 +-- .../sources}/polling/polling_data_source.cpp | 4 +- .../sources}/polling/polling_data_source.hpp | 12 +++--- .../streaming/streaming_data_source.cpp | 4 +- .../streaming/streaming_data_source.hpp | 12 +++--- .../data_source_status.cpp | 4 +- .../data_source_status_manager.hpp | 4 +- .../background_sync_system.cpp} | 6 +-- .../background_sync_system.hpp} | 6 +-- .../background_sync/event_handler.cpp} | 6 +-- .../background_sync/event_handler.hpp} | 4 +- .../lazy_load}/data_version_inspectors.cpp | 0 .../lazy_load}/data_version_inspectors.hpp | 4 +- .../expiration/expiration_tracker.cpp | 0 .../expiration/expiration_tracker.hpp | 0 .../systems/lazy_load/lazy_load_system.cpp} | 6 +-- .../systems/lazy_load/lazy_load_system.hpp} | 8 ++-- .../src/data_sources/descriptors.hpp | 12 ------ .../tests/data_source_event_handler_test.cpp | 2 +- .../tests/server_c_bindings_test.cpp | 2 +- 49 files changed, 162 insertions(+), 191 deletions(-) create mode 100644 libs/internal/include/launchdarkly/data_model/descriptors.hpp rename libs/server-sdk/src/{data_sources/data_store_updater.cpp => data_retrieval/change_notifier/change_notifier_destination.cpp} (94%) rename libs/server-sdk/src/{data_sources/data_store_updater.hpp => data_retrieval/change_notifier/change_notifier_destination.hpp} (94%) rename libs/server-sdk/src/{data_sources/pull_mode => data_retrieval/dependency_tracker}/data_kind.hpp (100%) rename libs/server-sdk/src/{data_sources => data_retrieval/dependency_tracker}/dependency_tracker.cpp (100%) rename libs/server-sdk/src/{data_sources => data_retrieval/dependency_tracker}/dependency_tracker.hpp (99%) rename libs/server-sdk/src/{data_sources/pull_mode => data_retrieval/dependency_tracker}/tagged_data.hpp (100%) rename libs/server-sdk/src/{data_sources => data_retrieval}/interfaces/data_bootstrapper.hpp (100%) rename libs/server-sdk/src/{data_sources/interfaces => data_retrieval/interfaces/data_dest}/data_destination.hpp (89%) rename libs/server-sdk/src/{data_sources/interfaces => data_retrieval/interfaces/data_dest}/serialized_data_destination.hpp (79%) rename libs/server-sdk/src/{data_sources/interfaces/data_source_lite.hpp => data_retrieval/interfaces/data_source/data_pull_source.hpp} (50%) rename libs/server-sdk/src/{data_sources/interfaces/data_synchronizer.hpp => data_retrieval/interfaces/data_source/data_push_source.hpp} (50%) rename libs/server-sdk/src/{data_sources/interfaces/serialized_data_source.hpp => data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp} (79%) rename libs/server-sdk/src/{data_sources/interfaces/data_source.hpp => data_retrieval/interfaces/data_system.hpp} (56%) rename libs/server-sdk/src/{data_sources => data_retrieval}/interfaces/serialized_descriptors.hpp (96%) rename libs/server-sdk/src/{data_sources => data_retrieval}/memory_store/memory_store.cpp (95%) rename libs/server-sdk/src/{data_sources => data_retrieval}/memory_store/memory_store.hpp (93%) rename libs/server-sdk/src/{data_sources/adapters => data_retrieval/serialization_adapters}/README.md (100%) rename libs/server-sdk/src/{data_sources/adapters => data_retrieval/serialization_adapters}/json_destination.cpp (83%) rename libs/server-sdk/src/{data_sources/adapters => data_retrieval/serialization_adapters}/json_destination.hpp (81%) rename libs/server-sdk/src/{data_sources/adapters/json_source.cpp => data_retrieval/serialization_adapters/json_pull_source.cpp} (90%) rename libs/server-sdk/src/{data_sources/adapters/json_source.hpp => data_retrieval/serialization_adapters/json_pull_source.hpp} (62%) rename libs/server-sdk/src/{data_sources/implementations => data_retrieval/sources}/README.md (100%) rename libs/server-sdk/src/{data_sources/implementations => data_retrieval/sources}/noop/null_data_source.cpp (85%) rename libs/server-sdk/src/{data_sources/implementations => data_retrieval/sources}/noop/null_data_source.hpp (80%) rename libs/server-sdk/src/{data_sources/implementations => data_retrieval/sources}/polling/polling_data_source.cpp (98%) rename libs/server-sdk/src/{data_sources/implementations => data_retrieval/sources}/polling/polling_data_source.hpp (83%) rename libs/server-sdk/src/{data_sources/implementations => data_retrieval/sources}/streaming/streaming_data_source.cpp (98%) rename libs/server-sdk/src/{data_sources/implementations => data_retrieval/sources}/streaming/streaming_data_source.hpp (85%) rename libs/server-sdk/src/{data_sources => data_retrieval/status_notifications}/data_source_status.cpp (91%) rename libs/server-sdk/src/{data_sources => data_retrieval/status_notifications}/data_source_status_manager.hpp (88%) rename libs/server-sdk/src/{data_sources/push_mode/push_mode_data_source.cpp => data_retrieval/systems/background_sync/background_sync_system.cpp} (94%) rename libs/server-sdk/src/{data_sources/push_mode/push_mode_data_source.hpp => data_retrieval/systems/background_sync/background_sync_system.hpp} (92%) rename libs/server-sdk/src/{data_sources/data_source_event_handler.cpp => data_retrieval/systems/background_sync/event_handler.cpp} (98%) rename libs/server-sdk/src/{data_sources/data_source_event_handler.hpp => data_retrieval/systems/background_sync/event_handler.hpp} (97%) rename libs/server-sdk/src/{data_sources/pull_mode => data_retrieval/systems/lazy_load}/data_version_inspectors.cpp (100%) rename libs/server-sdk/src/{data_sources/pull_mode => data_retrieval/systems/lazy_load}/data_version_inspectors.hpp (86%) rename libs/server-sdk/src/{data_sources/pull_mode => data_retrieval/systems/lazy_load}/expiration/expiration_tracker.cpp (100%) rename libs/server-sdk/src/{data_sources/pull_mode => data_retrieval/systems/lazy_load}/expiration/expiration_tracker.hpp (100%) rename libs/server-sdk/src/{data_sources/pull_mode/pull_mode_data_source.cpp => data_retrieval/systems/lazy_load/lazy_load_system.cpp} (97%) rename libs/server-sdk/src/{data_sources/pull_mode/pull_mode_data_source.hpp => data_retrieval/systems/lazy_load/lazy_load_system.hpp} (95%) delete mode 100644 libs/server-sdk/src/data_sources/descriptors.hpp diff --git a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp index 35a90e918..f23a60450 100644 --- a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp @@ -6,7 +6,6 @@ #include #include -#include namespace launchdarkly::config::shared::built { @@ -20,12 +19,4 @@ struct Persistence { std::size_t max_contexts_; }; -template <> -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/internal/include/launchdarkly/data_model/descriptors.hpp b/libs/internal/include/launchdarkly/data_model/descriptors.hpp new file mode 100644 index 000000000..eca13005c --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/descriptors.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include +#include + +namespace launchdarkly::data_model { +using FlagDescriptor = ItemDescriptor; +using SegmentDescriptor = ItemDescriptor; +} // namespace launchdarkly::data_model 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 c6f16f7ea..8a5376cfe 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 @@ -13,7 +13,7 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { /** * Enumeration of possible data source states. @@ -114,4 +114,4 @@ std::ostream& operator<<(std::ostream& out, std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 23f5dcda8..ce2394ce1 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -22,22 +22,20 @@ target_sources(${LIBNAME} 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_store_updater.cpp - data_sources/dependency_tracker.hpp - data_sources/dependency_tracker.cpp - data_sources/descriptors.hpp - data_sources/memory_store/memory_store.cpp - data_sources/adapters/json_destination.cpp - data_sources/adapters/json_source.cpp - data_sources/data_source_event_handler.cpp - data_sources/data_source_event_handler.hpp - data_sources/data_source_status.cpp - data_sources/implementations/polling/polling_data_source.cpp - data_sources/implementations/streaming/streaming_data_source.cpp - data_sources/implementations/noop/null_data_source.cpp - data_sources/data_source_status_manager.hpp - data_sources/pull_mode/pull_mode_data_source.cpp - data_sources/push_mode/push_mode_data_source.cpp + data_retrieval/change_notifier/change_notifier_destination.cpp + data_retrieval/dependency_tracker/dependency_tracker.cpp + data_retrieval/memory_store/memory_store.cpp + data_retrieval/serialization_adapters/json_destination.cpp + data_retrieval/serialization_adapters/json_pull_source.cpp + data_retrieval/sources/noop/null_data_source.cpp + data_retrieval/sources/streaming/streaming_data_source.cpp + data_retrieval/sources/polling/polling_data_source.cpp + data_retrieval/status_notifications/data_source_status.cpp + data_retrieval/systems/background_sync/background_sync_system.cpp + data_retrieval/systems/background_sync/event_handler.cpp + data_retrieval/systems/lazy_load/data_version_inspectors.cpp + data_retrieval/systems/lazy_load/lazy_load_system.cpp + data_retrieval/systems/lazy_load/expiration/expiration_tracker.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp index 78d13f0f8..062d6e2d9 100644 --- a/libs/server-sdk/src/bindings/c/sdk.cpp +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -28,7 +28,7 @@ struct Detail; #define TO_DATASOURCESTATUS(ptr) \ (reinterpret_cast< \ - launchdarkly::server_side::data_sources::DataSourceStatus*>(ptr)) + launchdarkly::server_side::data_system::DataSourceStatus*>(ptr)) #define FROM_DATASOURCESTATUS(ptr) \ (reinterpret_cast(ptr)) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 48345e19e..3b8e86aba 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -32,7 +32,7 @@ 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; +using launchdarkly::server_side::data_system::DataSourceStatus; // static std::shared_ptr<::launchdarkly::data_sources::IDataSource> // MakeDataSource(HttpProperties const& http_properties, @@ -53,13 +53,13 @@ using launchdarkly::server_side::data_sources::DataSourceStatus; // if (config.DataSourceConfig().method.index() == 0) { // // TODO: use initial reconnect delay. // return std::make_shared< -// launchdarkly::server_side::data_sources::StreamingDataSource>( +// launchdarkly::server_side::data_system::StreamingDataSource>( // config.ServiceEndpoints(), config.DataSourceConfig(), // data_source_properties, executor, flag_updater, status_manager, // logger); // } // return std::make_shared< -// launchdarkly::server_side::data_sources::PollingDataSource>( +// launchdarkly::server_side::data_system::PollingDataSource>( // config.ServiceEndpoints(), config.DataSourceConfig(), // data_source_properties, executor, flag_updater, status_manager, // logger); diff --git a/libs/server-sdk/src/data_sources/data_store_updater.cpp b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp similarity index 94% rename from libs/server-sdk/src/data_sources/data_store_updater.cpp rename to libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp index bbbadbe99..5999834ec 100644 --- a/libs/server-sdk/src/data_sources/data_store_updater.cpp +++ b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp @@ -1,9 +1,9 @@ -#include "data_store_updater.hpp" +#include "change_notifier_destination.hpp" #include #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_system { std::unique_ptr DataStoreUpdater::OnFlagChange( launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { @@ -73,4 +73,4 @@ DataStoreUpdater::DataStoreUpdater(IDataDestination& sink, data_sources::IDataSource const& source) : sink_(sink), source_(source) {} -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/data_store_updater.hpp b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp similarity index 94% rename from libs/server-sdk/src/data_sources/data_store_updater.hpp rename to libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp index 90f5a2ea0..c347ea788 100644 --- a/libs/server-sdk/src/data_sources/data_store_updater.hpp +++ b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp @@ -10,11 +10,9 @@ #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_system { -class DataStoreUpdater - : public launchdarkly::server_side::data_sources::IDataDestination, - public launchdarkly::server_side::IChangeNotifier { +class DataStoreUpdater : public IDataDestination, public IChangeNotifier { public: template using Collection = data_model::SDKDataSet::Collection; @@ -121,4 +119,4 @@ class DataStoreUpdater DependencyTracker dependency_tracker_; }; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/pull_mode/data_kind.hpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp similarity index 100% rename from libs/server-sdk/src/data_sources/pull_mode/data_kind.hpp rename to libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp diff --git a/libs/server-sdk/src/data_sources/dependency_tracker.cpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp similarity index 100% rename from libs/server-sdk/src/data_sources/dependency_tracker.cpp rename to libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp diff --git a/libs/server-sdk/src/data_sources/dependency_tracker.hpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp similarity index 99% rename from libs/server-sdk/src/data_sources/dependency_tracker.hpp rename to libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp index d62ba2c7c..097acca84 100644 --- a/libs/server-sdk/src/data_sources/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp @@ -1,17 +1,16 @@ #pragma once -#include -#include -#include -#include +#include "data_kind.hpp" +#include "tagged_data.hpp" #include #include #include #include -#include "data_kind.hpp" -#include "tagged_data.hpp" +#include +#include +#include namespace launchdarkly::server_side::data_store { diff --git a/libs/server-sdk/src/data_sources/pull_mode/tagged_data.hpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp similarity index 100% rename from libs/server-sdk/src/data_sources/pull_mode/tagged_data.hpp rename to libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp diff --git a/libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp similarity index 100% rename from libs/server-sdk/src/data_sources/interfaces/data_bootstrapper.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp diff --git a/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp similarity index 89% rename from libs/server-sdk/src/data_sources/interfaces/data_destination.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp index 9e5fc5355..bdc200e61 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp @@ -7,7 +7,7 @@ #include "../descriptors.hpp" -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class IDataDestination { public: @@ -25,4 +25,4 @@ class IDataDestination { protected: IDataDestination() = default; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/interfaces/serialized_data_destination.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp similarity index 79% rename from libs/server-sdk/src/data_sources/interfaces/serialized_data_destination.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp index 53774d90c..05284a43d 100644 --- a/libs/server-sdk/src/data_sources/interfaces/serialized_data_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp @@ -2,19 +2,10 @@ #include "serialized_descriptors.hpp" -#include -#include +#include #include -#include -#include "../descriptors.hpp" - -// TODO: refactor this so that it's more generic to "serialization" and not -// directly -// tied to persistence concept. -#include - -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class ISerializedDataDestination { public: @@ -74,4 +65,4 @@ class ISerializedDataDestination { protected: ISerializedDataDestination() = default; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/interfaces/data_source_lite.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp similarity index 50% rename from libs/server-sdk/src/data_sources/interfaces/data_source_lite.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp index 8dd5472e9..d8ff90dd7 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_source_lite.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp @@ -1,6 +1,6 @@ #pragma once -#include "../descriptors.hpp" +#include #include #include @@ -8,11 +8,11 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { -class IDataSourceLite { +class IDataPullSource { public: - [[nodiscard]] virtual FlagDescriptor GetFlag( + [[nodiscard]] virtual data_model::FlagDescriptor GetFlag( std::string const& key) const = 0; /** @@ -22,7 +22,7 @@ class IDataSourceLite { * @return Returns a shared_ptr to the SegmentDescriptor, or a nullptr if * there is no such segment, or the segment was deleted. */ - [[nodiscard]] virtual SegmentDescriptor GetSegment( + [[nodiscard]] virtual data_model::SegmentDescriptor GetSegment( std::string const& key) const = 0; /** @@ -30,7 +30,8 @@ class IDataSourceLite { * * @return Returns an unordered map of FlagDescriptors. */ - [[nodiscard]] virtual std::unordered_map + [[nodiscard]] virtual std::unordered_map AllFlags() const = 0; /** @@ -38,19 +39,20 @@ class IDataSourceLite { * * @return Returns an unordered map of SegmentDescriptors. */ - [[nodiscard]] virtual std::unordered_map + [[nodiscard]] virtual std::unordered_map AllSegments() const = 0; [[nodiscard]] virtual std::string const& Identity() const = 0; - virtual ~IDataSourceLite() = default; - IDataSourceLite(IDataSourceLite const& item) = delete; - IDataSourceLite(IDataSourceLite&& item) = delete; - IDataSourceLite& operator=(IDataSourceLite const&) = delete; - IDataSourceLite& operator=(IDataSourceLite&&) = delete; + virtual ~IPullSource() = default; + IPullSource(IPullSource const& item) = delete; + IPullSource(IPullSource&& item) = delete; + IPullSource& operator=(IPullSource const&) = delete; + IPullSource& operator=(IPullSource&&) = delete; protected: - IDataSourceLite() = default; + IPullSource() = default; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp similarity index 50% rename from libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp index ca595ef2f..9c8a332ec 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_synchronizer.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp @@ -1,6 +1,6 @@ #pragma once -#include "../descriptors.hpp" +#include #include #include @@ -8,9 +8,9 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { -class ISynchronizer { +class IDataPushSource { public: virtual void Init(std::optional initial_data, IDataDestination& destination) = 0; @@ -19,14 +19,14 @@ class ISynchronizer { virtual std::string const& Identity() const = 0; - virtual ~ISynchronizer() = default; - ISynchronizer(ISynchronizer const& item) = delete; - ISynchronizer(ISynchronizer&& item) = delete; - ISynchronizer& operator=(ISynchronizer const&) = delete; - ISynchronizer& operator=(ISynchronizer&&) = delete; + virtual ~IPushSource() = default; + IPushSource(IPushSource const& item) = delete; + IPushSource(IPushSource&& item) = delete; + IPushSource& operator=(IPushSource const&) = delete; + IPushSource& operator=(IPushSource&&) = delete; protected: - ISynchronizer() = default; + IPushSource() = default; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/interfaces/serialized_data_source.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp similarity index 79% rename from libs/server-sdk/src/data_sources/interfaces/serialized_data_source.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp index 6f406a115..4da90c7d5 100644 --- a/libs/server-sdk/src/data_sources/interfaces/serialized_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp @@ -7,7 +7,7 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { /** * Interface for a data source that provides feature flags and related data in a @@ -26,13 +26,14 @@ namespace launchdarkly::server_side::data_sources { * * Implementations must be thread-safe. */ -class ISerializedDataSource { +class ISerializedDataPullSource { public: - virtual ~ISerializedDataSource() = default; - ISerializedDataSource(ISerializedDataSource const& item) = delete; - ISerializedDataSource(ISerializedDataSource&& item) = delete; - ISerializedDataSource& operator=(ISerializedDataSource const&) = delete; - ISerializedDataSource& operator=(ISerializedDataSource&&) = delete; + virtual ~ISerializedDataPullSource() = default; + ISerializedDataPullSource(ISerializedDataPullSource const& item) = delete; + ISerializedDataPullSource(ISerializedDataPullSource&& item) = delete; + ISerializedDataPullSource& operator=(ISerializedDataPullSource const&) = + delete; + ISerializedDataPullSource& operator=(ISerializedDataPullSource&&) = delete; struct Error { std::string message; @@ -71,6 +72,6 @@ class ISerializedDataSource { virtual std::string const& Identity() const = 0; protected: - ISerializedDataSource() = default; + ISerializedDataPullSource() = default; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/interfaces/data_source.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp similarity index 56% rename from libs/server-sdk/src/data_sources/interfaces/data_source.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp index c9c02cea9..e83eac1cf 100644 --- a/libs/server-sdk/src/data_sources/interfaces/data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp @@ -1,6 +1,6 @@ #pragma once -#include "descriptors.hpp" +#include #include #include @@ -10,11 +10,7 @@ namespace launchdarkly::server_side::data_sources { -class ISynchronizer; -class IBootstrapper; -class IDataDestination; - -class IDataSource { +class IDataSystem { public: [[nodiscard]] virtual std::string const& Identity() const; @@ -24,7 +20,7 @@ class IDataSource { // TODO: Have a GetDataStore() interface? That way we don't need to forward // methods. - [[nodiscard]] virtual std::shared_ptr GetFlag( + [[nodiscard]] virtual std::shared_ptr GetFlag( std::string const& key) const = 0; /** @@ -34,35 +30,36 @@ class IDataSource { * @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; + [[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; + [[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> + [[nodiscard]] virtual std::unordered_map< + std::string, + std::shared_ptr> AllSegments() const = 0; - virtual ~IDataSource() = default; - IDataSource(IDataSource const& item) = delete; - IDataSource(IDataSource&& item) = delete; - IDataSource& operator=(IDataSource const&) = delete; - IDataSource& operator=(IDataSource&&) = delete; + virtual ~IDataSystem() = default; + IDataSystem(IDataSystem const& item) = delete; + IDataSystem(IDataSystem&& item) = delete; + IDataSystem& operator=(IDataSystem const&) = delete; + IDataSystem& operator=(IDataSystem&&) = delete; protected: - IDataSource() = default; + IDataSystem() = default; }; } // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/src/data_sources/interfaces/serialized_descriptors.hpp b/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp similarity index 96% rename from libs/server-sdk/src/data_sources/interfaces/serialized_descriptors.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp index 9dbfe6ea3..9ae3ec8ad 100644 --- a/libs/server-sdk/src/data_sources/interfaces/serialized_descriptors.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { /** * A versioned item which can be stored in a persistent store. @@ -96,4 +96,4 @@ uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { return GetVersion(data); } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/memory_store/memory_store.cpp b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp similarity index 95% rename from libs/server-sdk/src/data_sources/memory_store/memory_store.cpp rename to libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp index 8e69552b9..99ce16499 100644 --- a/libs/server-sdk/src/data_sources/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp @@ -2,7 +2,7 @@ #include "memory_store.hpp" -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { std::shared_ptr MemoryStore::GetFlag( std::string const& key) const { @@ -72,4 +72,4 @@ void MemoryStore::Upsert(std::string const& key, segments_[key] = std::make_shared(std::move(segment)); } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp similarity index 93% rename from libs/server-sdk/src/data_sources/memory_store/memory_store.hpp rename to libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp index df2983e63..cfab36fca 100644 --- a/libs/server-sdk/src/data_sources/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp @@ -6,7 +6,7 @@ #include #include "../data_destination_interface.hpp" -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class MemoryStore : public data_sources::IDataDestination { public: @@ -42,4 +42,4 @@ class MemoryStore : public data_sources::IDataDestination { mutable std::mutex data_mutex_; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/adapters/README.md b/libs/server-sdk/src/data_retrieval/serialization_adapters/README.md similarity index 100% rename from libs/server-sdk/src/data_sources/adapters/README.md rename to libs/server-sdk/src/data_retrieval/serialization_adapters/README.md diff --git a/libs/server-sdk/src/data_sources/adapters/json_destination.cpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp similarity index 83% rename from libs/server-sdk/src/data_sources/adapters/json_destination.cpp rename to libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp index 19f1e19f7..85498987e 100644 --- a/libs/server-sdk/src/data_sources/adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp @@ -1,6 +1,6 @@ #include "json_destination.hpp" -namespace launchdarkly::server_side::data_sources::adapters { +namespace launchdarkly::server_side::data_system::adapters { JsonDestination::JsonDestination(ISerializedDataDestination& destination) : dest_(destination) {} @@ -21,4 +21,4 @@ void JsonDestination::Upsert(std::string const& key, std::string JsonDestination::Identity() const { return dest_.Identity(); } -} // namespace launchdarkly::server_side::data_sources::adapters +} // namespace launchdarkly::server_side::data_system::adapters diff --git a/libs/server-sdk/src/data_sources/adapters/json_destination.hpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp similarity index 81% rename from libs/server-sdk/src/data_sources/adapters/json_destination.hpp rename to libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp index afb2e4188..0af42dbfd 100644 --- a/libs/server-sdk/src/data_sources/adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp @@ -3,7 +3,7 @@ #include "../interfaces/data_destination.hpp" #include "../interfaces/serialized_data_destination.hpp" -namespace launchdarkly::server_side::data_sources::adapters { +namespace launchdarkly::server_side::data_system::adapters { class JsonDestination : public IDataDestination { public: @@ -18,4 +18,4 @@ class JsonDestination : public IDataDestination { ISerializedDataDestination& dest_; }; -} // namespace launchdarkly::server_side::data_sources::adapters +} // namespace launchdarkly::server_side::data_system::adapters diff --git a/libs/server-sdk/src/data_sources/adapters/json_source.cpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp similarity index 90% rename from libs/server-sdk/src/data_sources/adapters/json_source.cpp rename to libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp index 076f11caf..a4d8b63b8 100644 --- a/libs/server-sdk/src/data_sources/adapters/json_source.cpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp @@ -1,6 +1,6 @@ -#include "json_source.hpp" +#include "json_pull_source.hpp" -namespace launchdarkly::server_side::data_sources::adapters { +namespace launchdarkly::server_side::data_system::adapters { template static std::optional> Deserialize( @@ -53,4 +53,4 @@ std::string JsonSource::Identity() const { return source_.Identity(); } -} // namespace launchdarkly::server_side::data_sources::adapters +} // namespace launchdarkly::server_side::data_system::adapters diff --git a/libs/server-sdk/src/data_sources/adapters/json_source.hpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp similarity index 62% rename from libs/server-sdk/src/data_sources/adapters/json_source.hpp rename to libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp index 88de61076..e9be26584 100644 --- a/libs/server-sdk/src/data_sources/adapters/json_source.hpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp @@ -1,11 +1,11 @@ #include "../descriptors.hpp" -#include "../interfaces/data_source_lite.hpp" -#include "../interfaces/serialized_data_source.hpp" +#include "../interfaces/data_pull_source.hpp" +#include "../interfaces/serialzied_data_pull_source.hpp" -namespace launchdarkly::server_side::data_sources::adapters { +namespace launchdarkly::server_side::data_system::adapters { -class JsonSource : public IDataSourceLite { +class JsonSource : public IPullSource { public: FlagDescriptor GetFlag(std::string& key) const override; SegmentDescriptor GetSegment(std::string& key) const override; @@ -19,4 +19,4 @@ class JsonSource : public IDataSourceLite { ISerializedDataSource& source_; }; -} // namespace launchdarkly::server_side::data_sources::adapters +} // namespace launchdarkly::server_side::data_system::adapters diff --git a/libs/server-sdk/src/data_sources/implementations/README.md b/libs/server-sdk/src/data_retrieval/sources/README.md similarity index 100% rename from libs/server-sdk/src/data_sources/implementations/README.md rename to libs/server-sdk/src/data_retrieval/sources/README.md diff --git a/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.cpp b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp similarity index 85% rename from libs/server-sdk/src/data_sources/implementations/noop/null_data_source.cpp rename to libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp index 890676b1b..0f083f75f 100644 --- a/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { void NullDataSource::Start() { status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); @@ -19,4 +19,4 @@ NullDataSource::NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager) : status_manager_(status_manager), exec_(exec) {} -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp similarity index 80% rename from libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp rename to libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp index 12f6faae7..8b1dd3626 100644 --- a/libs/server-sdk/src/data_sources/implementations/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp @@ -2,11 +2,11 @@ #include "../../data_source_status_manager.hpp" #include "../../interfaces/data_destination.hpp" -#include "../../interfaces/data_synchronizer.hpp" +#include "../../interfaces/data_push_source.hpp" #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class NullDataSource : public ISynchronizer { public: @@ -23,4 +23,4 @@ class NullDataSource : public ISynchronizer { boost::asio::any_io_executor exec_; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.cpp b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp similarity index 98% rename from libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.cpp rename to libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp index f91b160a0..51e82db9e 100644 --- a/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp @@ -13,7 +13,7 @@ #include "data_source_update_sink.hpp" #include "polling_data_source.hpp" -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { static char const* const kErrorParsingPut = "Could not parse polling payload"; static char const* const kErrorPutInvalid = @@ -235,4 +235,4 @@ void PollingDataSource::ShutdownAsync(std::function completion) { } } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp similarity index 83% rename from libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp rename to libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp index 8e9d64468..9a620dc7c 100644 --- a/libs/server-sdk/src/data_sources/implementations/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp @@ -4,10 +4,8 @@ #include -#include "../../data_source_event_handler.hpp" -#include "../../data_source_status_manager.hpp" -#include "../../interfaces/data_destination.hpp" -#include "../../interfaces/data_synchronizer.hpp" +#include "../../interfaces/data_dest/data_destination.hpp" +#include "../../interfaces/data_source/data_push_source.hpp" #include #include @@ -15,10 +13,10 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class PollingDataSource - : public ISynchronizer, + : public IDataPushSource, public std::enable_shared_from_this { public: PollingDataSource( @@ -56,4 +54,4 @@ class PollingDataSource void StartPollingTimer(); }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp similarity index 98% rename from libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.cpp rename to libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp index 885a0d5ca..6473c8780 100644 --- a/libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp @@ -9,7 +9,7 @@ #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { static char const* const kCouldNotParseEndpoint = "Could not parse streaming endpoint URL"; @@ -153,4 +153,4 @@ void StreamingDataSource::ShutdownAsync(std::function completion) { boost::asio::post(exec_, completion); } } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp similarity index 85% rename from libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.hpp rename to libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp index bb15f7ea7..8e45c7ce1 100644 --- a/libs/server-sdk/src/data_sources/implementations/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp @@ -5,10 +5,8 @@ using namespace std::chrono_literals; #include -#include "data_source_event_handler.hpp" -#include "data_source_interface.hpp" -#include "data_source_status_manager.hpp" -#include "data_source_update_sink.hpp" +#include "../../interfaces/data_dest/data_destination.hpp" +#include "../../interfaces/data_source/data_push_source.hpp" #include #include @@ -19,10 +17,10 @@ using namespace std::chrono_literals; #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class StreamingDataSource final - : public ISynchronizer, + : public IDataPushSource, public std::enable_shared_from_this { public: StreamingDataSource( @@ -54,4 +52,4 @@ class StreamingDataSource final Logger const& logger_; std::shared_ptr client_; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/data_source_status.cpp b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp similarity index 91% rename from libs/server-sdk/src/data_sources/data_source_status.cpp rename to libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp index 6881ed4ba..a6ee9ec3f 100644 --- a/libs/server-sdk/src/data_sources/data_source_status.cpp +++ b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state) { @@ -37,4 +37,4 @@ std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { return out; } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/data_source_status_manager.hpp b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp similarity index 88% rename from libs/server-sdk/src/data_sources/data_source_status_manager.hpp rename to libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp index d19040ed8..4d13e8106 100644 --- a/libs/server-sdk/src/data_sources/data_source_status_manager.hpp +++ b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp @@ -9,7 +9,7 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class DataSourceStatusManager : public internal::data_sources::DataSourceStatusManagerBase< @@ -25,4 +25,4 @@ class DataSourceStatusManager DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp similarity index 94% rename from libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp rename to libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp index 531d4c341..1ae968b4b 100644 --- a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp @@ -1,9 +1,9 @@ -#include "push_mode_data_source.hpp" +#include "background_sync_system.hpp" #include "../polling_data_source.hpp" #include "../streaming_data_source.hpp" -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { using namespace config::shared::built; @@ -71,4 +71,4 @@ PushModeSource::AllSegments() const { return store_.AllSegments(); } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp similarity index 92% rename from libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp rename to libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp index 473d5cf1c..a36b873dd 100644 --- a/libs/server-sdk/src/data_sources/push_mode/push_mode_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp @@ -6,7 +6,7 @@ #include #include "../interfaces/data_destination.hpp" -#include "../interfaces/data_source.hpp" +#include "../interfaces/data_system.hpp" #include "../data_source_event_handler.hpp" #include "../data_source_status_manager.hpp" @@ -15,7 +15,7 @@ #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class PushModeSource : public IDataSource { public: @@ -51,4 +51,4 @@ class PushModeSource : public IDataSource { std::shared_ptr synchronizer_; std::shared_ptr bootstrapper_; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/data_source_event_handler.cpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp similarity index 98% rename from libs/server-sdk/src/data_sources/data_source_event_handler.cpp rename to libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp index 3110d0165..c23e10b45 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp @@ -1,4 +1,4 @@ -#include "data_source_event_handler.hpp" +#include "event_handler.hpp" #include #include @@ -15,7 +15,7 @@ #include "tl/expected.hpp" -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { static char const* const kErrorParsingPut = "Could not parse PUT message"; static char const* const kErrorPutInvalid = @@ -238,4 +238,4 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( return DataSourceEventHandler::MessageStatus::kUnhandledVerb; } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp similarity index 97% rename from libs/server-sdk/src/data_sources/data_source_event_handler.hpp rename to libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp index 8c4eab562..e01e06261 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp @@ -14,7 +14,7 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { // The FlagsPath and SegmentsPath are made to turn a string literal into a type // for use in a template. @@ -120,4 +120,4 @@ class DataSourceEventHandler { Logger const& logger_; DataSourceStatusManager& status_manager_; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.cpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.cpp similarity index 100% rename from libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.cpp rename to libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.cpp diff --git a/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp similarity index 86% rename from libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.hpp rename to libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp index 4648ce1cd..68ea0c074 100644 --- a/libs/server-sdk/src/data_sources/pull_mode/data_version_inspectors.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp @@ -4,7 +4,7 @@ #include -namespace launchdarkly::server_side::data_sources::pull { +namespace launchdarkly::server_side::data_system::pull { class SegmentKind : public persistence::IPersistentKind { public: std::string const& Namespace() const override; @@ -31,4 +31,4 @@ struct Kinds { static FlagKind const Flag; static SegmentKind const Segment; }; -} // namespace launchdarkly::server_side::data_sources::pull +} // namespace launchdarkly::server_side::data_system::pull diff --git a/libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.cpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.cpp similarity index 100% rename from libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.cpp rename to libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.cpp diff --git a/libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.hpp similarity index 100% rename from libs/server-sdk/src/data_sources/pull_mode/expiration/expiration_tracker.hpp rename to libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.hpp diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp similarity index 97% rename from libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp rename to libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp index f32ab9eec..412d6fd22 100644 --- a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp @@ -1,4 +1,4 @@ -#include "pull_mode_data_source.hpp" +#include "lazy_load_system.hpp" #include "../adapters/json_destination.hpp" #include "../adapters/json_source.hpp" @@ -8,7 +8,7 @@ #include "data_version_inspectors.hpp" -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { using namespace config::shared::built; @@ -150,4 +150,4 @@ void PullModeSource::RefreshFlag(std::string const& key) const { // TODO: If there is an actual error, then do we not reset the tracking? } -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp similarity index 95% rename from libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp rename to libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp index ba5e76193..8857e23d8 100644 --- a/libs/server-sdk/src/data_sources/pull_mode/pull_mode_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp @@ -5,8 +5,8 @@ #include #include -#include "../interfaces/data_source.hpp" -#include "../interfaces/data_synchronizer.hpp" +#include "../interfaces/data_push_source.hpp" +#include "../interfaces/data_system.hpp" #include "../data_source_event_handler.hpp" #include "../data_source_status_manager.hpp" @@ -15,7 +15,7 @@ #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_system { class PullModeSource : public IDataSource, public ISynchronizer { public: @@ -127,4 +127,4 @@ class PullModeSource : public IDataSource, public ISynchronizer { std::shared_ptr synchronizer_; std::shared_ptr bootstrapper_; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_system diff --git a/libs/server-sdk/src/data_sources/descriptors.hpp b/libs/server-sdk/src/data_sources/descriptors.hpp deleted file mode 100644 index 6b8aa46fc..000000000 --- a/libs/server-sdk/src/data_sources/descriptors.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace launchdarkly::server_side::data_sources { -using FlagDescriptor = - launchdarkly::data_model::ItemDescriptor; -using SegmentDescriptor = - launchdarkly::data_model::ItemDescriptor; -} // namespace launchdarkly::server_side::data_sources diff --git a/libs/server-sdk/tests/data_source_event_handler_test.cpp b/libs/server-sdk/tests/data_source_event_handler_test.cpp index 4092c78a2..e25829c07 100644 --- a/libs/server-sdk/tests/data_source_event_handler_test.cpp +++ b/libs/server-sdk/tests/data_source_event_handler_test.cpp @@ -6,7 +6,7 @@ using namespace launchdarkly; using namespace launchdarkly::server_side; -using namespace launchdarkly::server_side::data_sources; +using namespace launchdarkly::server_side::data_system; using namespace launchdarkly::server_side::data_store; TEST(DataSourceEventHandlerTests, HandlesEmptyPutMessage) { diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 893956cb8..37391b504 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -10,7 +10,7 @@ #include -using launchdarkly::server_side::data_sources::DataSourceStatus; +using launchdarkly::server_side::data_system::DataSourceStatus; TEST(ClientBindings, MinimalInstantiation) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); From 984efc41f2da57fc64ccfe0ff49f824a0e812086 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 22 Sep 2023 16:30:04 -0700 Subject: [PATCH 065/244] rename data_system namespace to data_retreival --- .../server_side/data_source_status.hpp | 4 ++-- libs/server-sdk/src/bindings/c/sdk.cpp | 2 +- libs/server-sdk/src/client_impl.cpp | 6 +++--- .../change_notifier_destination.cpp | 4 ++-- .../change_notifier_destination.hpp | 18 +++++++++--------- .../dependency_tracker/data_kind.hpp | 4 ++-- .../dependency_tracker/dependency_tracker.cpp | 4 ++-- .../dependency_tracker/dependency_tracker.hpp | 4 ++-- .../dependency_tracker/tagged_data.hpp | 4 ++-- .../interfaces/data_dest/data_destination.hpp | 4 ++-- .../data_dest/serialized_data_destination.hpp | 4 ++-- .../data_source/data_pull_source.hpp | 4 ++-- .../data_source/data_push_source.hpp | 4 ++-- .../serialzied_data_pull_source.hpp | 4 ++-- .../interfaces/serialized_descriptors.hpp | 4 ++-- .../memory_store/memory_store.cpp | 4 ++-- .../memory_store/memory_store.hpp | 4 ++-- .../json_destination.cpp | 4 ++-- .../json_destination.hpp | 4 ++-- .../json_pull_source.cpp | 4 ++-- .../json_pull_source.hpp | 4 ++-- .../sources/noop/null_data_source.cpp | 4 ++-- .../sources/noop/null_data_source.hpp | 4 ++-- .../sources/polling/polling_data_source.cpp | 4 ++-- .../sources/polling/polling_data_source.hpp | 4 ++-- .../streaming/streaming_data_source.cpp | 4 ++-- .../streaming/streaming_data_source.hpp | 4 ++-- .../data_source_status.cpp | 4 ++-- .../data_source_status_manager.hpp | 4 ++-- .../background_sync/background_sync_system.cpp | 4 ++-- .../background_sync/background_sync_system.hpp | 4 ++-- .../systems/background_sync/event_handler.cpp | 4 ++-- .../systems/background_sync/event_handler.hpp | 4 ++-- .../lazy_load/data_version_inspectors.hpp | 4 ++-- .../systems/lazy_load/lazy_load_system.cpp | 4 ++-- .../systems/lazy_load/lazy_load_system.hpp | 4 ++-- .../tests/data_source_event_handler_test.cpp | 2 +- .../tests/server_c_bindings_test.cpp | 2 +- 38 files changed, 81 insertions(+), 81 deletions(-) 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 8a5376cfe..3ed6f6d0d 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 @@ -13,7 +13,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { /** * Enumeration of possible data source states. @@ -114,4 +114,4 @@ std::ostream& operator<<(std::ostream& out, std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp index 062d6e2d9..1679a8ddf 100644 --- a/libs/server-sdk/src/bindings/c/sdk.cpp +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -28,7 +28,7 @@ struct Detail; #define TO_DATASOURCESTATUS(ptr) \ (reinterpret_cast< \ - launchdarkly::server_side::data_system::DataSourceStatus*>(ptr)) + launchdarkly::server_side::data_retrieval::DataSourceStatus*>(ptr)) #define FROM_DATASOURCESTATUS(ptr) \ (reinterpret_cast(ptr)) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 3b8e86aba..380b96f32 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -32,7 +32,7 @@ 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_system::DataSourceStatus; +using launchdarkly::server_side::data_retrieval::DataSourceStatus; // static std::shared_ptr<::launchdarkly::data_sources::IDataSource> // MakeDataSource(HttpProperties const& http_properties, @@ -53,13 +53,13 @@ using launchdarkly::server_side::data_system::DataSourceStatus; // if (config.DataSourceConfig().method.index() == 0) { // // TODO: use initial reconnect delay. // return std::make_shared< -// launchdarkly::server_side::data_system::StreamingDataSource>( +// launchdarkly::server_side::data_retrieval::StreamingDataSource>( // config.ServiceEndpoints(), config.DataSourceConfig(), // data_source_properties, executor, flag_updater, status_manager, // logger); // } // return std::make_shared< -// launchdarkly::server_side::data_system::PollingDataSource>( +// launchdarkly::server_side::data_retrieval::PollingDataSource>( // config.ServiceEndpoints(), config.DataSourceConfig(), // data_source_properties, executor, flag_updater, status_manager, // logger); diff --git a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp index 5999834ec..a02771c68 100644 --- a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp +++ b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { std::unique_ptr DataStoreUpdater::OnFlagChange( launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { @@ -73,4 +73,4 @@ DataStoreUpdater::DataStoreUpdater(IDataDestination& sink, data_sources::IDataSource const& source) : sink_(sink), source_(source) {} -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp index c347ea788..3c952e8dd 100644 --- a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp @@ -1,16 +1,17 @@ #pragma once -#include "../data_sources/data_destination_interface.hpp" -#include "../data_sources/data_source_interface.hpp" -#include "dependency_tracker.hpp" +#include "../dependency_tracker/dependency_tracker.hpp" +#include "../interfaces/data_dest/data_destination.hpp" +#include "../interfaces/data_source/data_pull_source.hpp" +#include #include #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class DataStoreUpdater : public IDataDestination, public IChangeNotifier { public: @@ -24,16 +25,15 @@ class DataStoreUpdater : public IDataDestination, public IChangeNotifier { using SharedCollection = std::unordered_map>; - DataStoreUpdater(IDataDestination& sink, - data_sources::IDataSource const& source); + DataStoreUpdater(IDataDestination& sink, IDataPullSource const& source); std::unique_ptr OnFlagChange(ChangeHandler handler) override; void Init(launchdarkly::data_model::SDKDataSet data_set) override; void Upsert(std::string const& key, - data_sources::FlagDescriptor flag) override; + data_model::FlagDescriptor flag) override; void Upsert(std::string const& key, - data_sources::SegmentDescriptor segment) override; + data_model::SegmentDescriptor segment) override; ~DataStoreUpdater() override = default; DataStoreUpdater(DataStoreUpdater const& item) = delete; @@ -119,4 +119,4 @@ class DataStoreUpdater : public IDataDestination, public IChangeNotifier { DependencyTracker dependency_tracker_; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp index 17ead105b..33b651d7a 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp +++ b/libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp @@ -2,6 +2,6 @@ #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_retrieval { enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp index d401fe327..671190acb 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp +++ b/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp @@ -3,7 +3,7 @@ #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_retrieval { DependencySet::DependencySet() : data_{ @@ -194,4 +194,4 @@ void DependencyTracker::Clear() { dependencies_from_.Clear(); } -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp index 097acca84..b937b62ea 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp @@ -12,7 +12,7 @@ #include #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_retrieval { /** * Class used to maintain a set of dependencies. Each dependency may be either @@ -152,4 +152,4 @@ class DependencyTracker { std::vector const& clauses); }; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp b/libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp index 320146721..2a44717a5 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp +++ b/libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp @@ -10,7 +10,7 @@ #include "data_kind.hpp" -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_retrieval { /** * 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 @@ -34,4 +34,4 @@ class TaggedData { Storage storage_; }; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp index bdc200e61..9c6a086c9 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp @@ -7,7 +7,7 @@ #include "../descriptors.hpp" -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class IDataDestination { public: @@ -25,4 +25,4 @@ class IDataDestination { protected: IDataDestination() = default; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp index 05284a43d..87bbb1f3b 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp @@ -5,7 +5,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class ISerializedDataDestination { public: @@ -65,4 +65,4 @@ class ISerializedDataDestination { protected: ISerializedDataDestination() = default; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp index d8ff90dd7..1f18842d7 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp @@ -8,7 +8,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class IDataPullSource { public: @@ -55,4 +55,4 @@ class IDataPullSource { IPullSource() = default; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp index 9c8a332ec..130e5282d 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp @@ -8,7 +8,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class IDataPushSource { public: @@ -29,4 +29,4 @@ class IDataPushSource { IPushSource() = default; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp index 4da90c7d5..64fbf8710 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp @@ -7,7 +7,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { /** * Interface for a data source that provides feature flags and related data in a @@ -74,4 +74,4 @@ class ISerializedDataPullSource { protected: ISerializedDataPullSource() = default; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp b/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp index 9ae3ec8ad..eb5a2d34b 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { /** * A versioned item which can be stored in a persistent store. @@ -96,4 +96,4 @@ uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { return GetVersion(data); } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp index 99ce16499..a0c1e64d4 100644 --- a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp @@ -2,7 +2,7 @@ #include "memory_store.hpp" -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { std::shared_ptr MemoryStore::GetFlag( std::string const& key) const { @@ -72,4 +72,4 @@ void MemoryStore::Upsert(std::string const& key, segments_[key] = std::make_shared(std::move(segment)); } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp index cfab36fca..aff378005 100644 --- a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp @@ -6,7 +6,7 @@ #include #include "../data_destination_interface.hpp" -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class MemoryStore : public data_sources::IDataDestination { public: @@ -42,4 +42,4 @@ class MemoryStore : public data_sources::IDataDestination { mutable std::mutex data_mutex_; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp index 85498987e..9c554955a 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp @@ -1,6 +1,6 @@ #include "json_destination.hpp" -namespace launchdarkly::server_side::data_system::adapters { +namespace launchdarkly::server_side::data_retrieval::adapters { JsonDestination::JsonDestination(ISerializedDataDestination& destination) : dest_(destination) {} @@ -21,4 +21,4 @@ void JsonDestination::Upsert(std::string const& key, std::string JsonDestination::Identity() const { return dest_.Identity(); } -} // namespace launchdarkly::server_side::data_system::adapters +} // namespace launchdarkly::server_side::data_retrieval::adapters diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp index 0af42dbfd..0577763da 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp @@ -3,7 +3,7 @@ #include "../interfaces/data_destination.hpp" #include "../interfaces/serialized_data_destination.hpp" -namespace launchdarkly::server_side::data_system::adapters { +namespace launchdarkly::server_side::data_retrieval::adapters { class JsonDestination : public IDataDestination { public: @@ -18,4 +18,4 @@ class JsonDestination : public IDataDestination { ISerializedDataDestination& dest_; }; -} // namespace launchdarkly::server_side::data_system::adapters +} // namespace launchdarkly::server_side::data_retrieval::adapters diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp index a4d8b63b8..7152f037c 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp @@ -1,6 +1,6 @@ #include "json_pull_source.hpp" -namespace launchdarkly::server_side::data_system::adapters { +namespace launchdarkly::server_side::data_retrieval::adapters { template static std::optional> Deserialize( @@ -53,4 +53,4 @@ std::string JsonSource::Identity() const { return source_.Identity(); } -} // namespace launchdarkly::server_side::data_system::adapters +} // namespace launchdarkly::server_side::data_retrieval::adapters diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp index e9be26584..1f4cc8b4b 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp +++ b/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp @@ -3,7 +3,7 @@ #include "../interfaces/data_pull_source.hpp" #include "../interfaces/serialzied_data_pull_source.hpp" -namespace launchdarkly::server_side::data_system::adapters { +namespace launchdarkly::server_side::data_retrieval::adapters { class JsonSource : public IPullSource { public: @@ -19,4 +19,4 @@ class JsonSource : public IPullSource { ISerializedDataSource& source_; }; -} // namespace launchdarkly::server_side::data_system::adapters +} // namespace launchdarkly::server_side::data_retrieval::adapters diff --git a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp index 0f083f75f..379a884be 100644 --- a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { void NullDataSource::Start() { status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); @@ -19,4 +19,4 @@ NullDataSource::NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager) : status_manager_(status_manager), exec_(exec) {} -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp index 8b1dd3626..b167f247b 100644 --- a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp @@ -6,7 +6,7 @@ #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class NullDataSource : public ISynchronizer { public: @@ -23,4 +23,4 @@ class NullDataSource : public ISynchronizer { boost::asio::any_io_executor exec_; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp index 51e82db9e..366e2680d 100644 --- a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp @@ -13,7 +13,7 @@ #include "data_source_update_sink.hpp" #include "polling_data_source.hpp" -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { static char const* const kErrorParsingPut = "Could not parse polling payload"; static char const* const kErrorPutInvalid = @@ -235,4 +235,4 @@ void PollingDataSource::ShutdownAsync(std::function completion) { } } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp index 9a620dc7c..5fecc81d1 100644 --- a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp @@ -13,7 +13,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class PollingDataSource : public IDataPushSource, @@ -54,4 +54,4 @@ class PollingDataSource void StartPollingTimer(); }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp index 6473c8780..4e2f818c7 100644 --- a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp @@ -9,7 +9,7 @@ #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { static char const* const kCouldNotParseEndpoint = "Could not parse streaming endpoint URL"; @@ -153,4 +153,4 @@ void StreamingDataSource::ShutdownAsync(std::function completion) { boost::asio::post(exec_, completion); } } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp index 8e45c7ce1..6a51bfc36 100644 --- a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp @@ -17,7 +17,7 @@ using namespace std::chrono_literals; #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class StreamingDataSource final : public IDataPushSource, @@ -52,4 +52,4 @@ class StreamingDataSource final Logger const& logger_; std::shared_ptr client_; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp index a6ee9ec3f..958439c0e 100644 --- a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp +++ b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state) { @@ -37,4 +37,4 @@ std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { return out; } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp index 4d13e8106..f34599916 100644 --- a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp +++ b/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp @@ -9,7 +9,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class DataSourceStatusManager : public internal::data_sources::DataSourceStatusManagerBase< @@ -25,4 +25,4 @@ class DataSourceStatusManager DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp index 1ae968b4b..1565ac8fc 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp @@ -3,7 +3,7 @@ #include "../polling_data_source.hpp" #include "../streaming_data_source.hpp" -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { using namespace config::shared::built; @@ -71,4 +71,4 @@ PushModeSource::AllSegments() const { return store_.AllSegments(); } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp index a36b873dd..b8dea7c1c 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp @@ -15,7 +15,7 @@ #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class PushModeSource : public IDataSource { public: @@ -51,4 +51,4 @@ class PushModeSource : public IDataSource { std::shared_ptr synchronizer_; std::shared_ptr bootstrapper_; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp index c23e10b45..c5f46b4c8 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp @@ -15,7 +15,7 @@ #include "tl/expected.hpp" -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { static char const* const kErrorParsingPut = "Could not parse PUT message"; static char const* const kErrorPutInvalid = @@ -238,4 +238,4 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( return DataSourceEventHandler::MessageStatus::kUnhandledVerb; } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp index e01e06261..ce31fe56a 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp @@ -14,7 +14,7 @@ #include #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { // The FlagsPath and SegmentsPath are made to turn a string literal into a type // for use in a template. @@ -120,4 +120,4 @@ class DataSourceEventHandler { Logger const& logger_; DataSourceStatusManager& status_manager_; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp index 68ea0c074..2b4775879 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp @@ -4,7 +4,7 @@ #include -namespace launchdarkly::server_side::data_system::pull { +namespace launchdarkly::server_side::data_retrieval::pull { class SegmentKind : public persistence::IPersistentKind { public: std::string const& Namespace() const override; @@ -31,4 +31,4 @@ struct Kinds { static FlagKind const Flag; static SegmentKind const Segment; }; -} // namespace launchdarkly::server_side::data_system::pull +} // namespace launchdarkly::server_side::data_retrieval::pull diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp index 412d6fd22..d4141380e 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp @@ -8,7 +8,7 @@ #include "data_version_inspectors.hpp" -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { using namespace config::shared::built; @@ -150,4 +150,4 @@ void PullModeSource::RefreshFlag(std::string const& key) const { // TODO: If there is an actual error, then do we not reset the tracking? } -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp index 8857e23d8..2b6c0c6c2 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp @@ -15,7 +15,7 @@ #include -namespace launchdarkly::server_side::data_system { +namespace launchdarkly::server_side::data_retrieval { class PullModeSource : public IDataSource, public ISynchronizer { public: @@ -127,4 +127,4 @@ class PullModeSource : public IDataSource, public ISynchronizer { std::shared_ptr synchronizer_; std::shared_ptr bootstrapper_; }; -} // namespace launchdarkly::server_side::data_system +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/tests/data_source_event_handler_test.cpp b/libs/server-sdk/tests/data_source_event_handler_test.cpp index e25829c07..f8fe9de4b 100644 --- a/libs/server-sdk/tests/data_source_event_handler_test.cpp +++ b/libs/server-sdk/tests/data_source_event_handler_test.cpp @@ -6,7 +6,7 @@ using namespace launchdarkly; using namespace launchdarkly::server_side; -using namespace launchdarkly::server_side::data_system; +using namespace launchdarkly::server_side::data_retrieval; using namespace launchdarkly::server_side::data_store; TEST(DataSourceEventHandlerTests, HandlesEmptyPutMessage) { diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 37391b504..0c6a345ea 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -10,7 +10,7 @@ #include -using launchdarkly::server_side::data_system::DataSourceStatus; +using launchdarkly::server_side::data_retrieval::DataSourceStatus; TEST(ClientBindings, MinimalInstantiation) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); From 648cbac7f239ec37b8e3f30642390364391e5485 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 22 Sep 2023 16:44:49 -0700 Subject: [PATCH 066/244] updating client to use IDataSystem --- libs/server-sdk/src/client_impl.cpp | 30 ++++---- libs/server-sdk/src/client_impl.hpp | 15 ++-- .../change_notifier_destination.cpp | 4 +- .../interfaces/data_bootstrapper.hpp | 4 +- .../data_retrieval/interfaces/data_system.hpp | 10 +-- .../memory_store/memory_store.cpp | 4 +- .../background_sync_system.cpp | 24 +++---- .../background_sync_system.hpp | 32 ++++----- .../systems/background_sync/event_handler.cpp | 6 +- .../systems/lazy_load/lazy_load_system.cpp | 34 +++++----- .../systems/lazy_load/lazy_load_system.hpp | 68 ++++++++----------- libs/server-sdk/src/evaluation/evaluator.cpp | 4 +- libs/server-sdk/src/evaluation/rules.cpp | 2 +- 13 files changed, 100 insertions(+), 137 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 380b96f32..f1de311aa 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -7,10 +7,8 @@ #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_retrieval/systems/background_sync/background_sync_system.hpp" #include "data_sources/push_mode/push_mode_data_source.hpp" -#include "data_sources/streaming_data_source.hpp" #include #include @@ -32,7 +30,6 @@ 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_retrieval::DataSourceStatus; // static std::shared_ptr<::launchdarkly::data_sources::IDataSource> // MakeDataSource(HttpProperties const& http_properties, @@ -65,11 +62,11 @@ using launchdarkly::server_side::data_retrieval::DataSourceStatus; // logger); // } -static std::unique_ptr MakeDataSource( +static std::unique_ptr MakeDataSystem( HttpProperties const& http_properties, Config const& config, boost::asio::any_io_executor const& executor, - data_sources::DataSourceStatusManager& status_manager, + data_retrieval::DataSourceStatusManager& status_manager, Logger& logger) { auto builder = HttpPropertiesBuilder(http_properties); @@ -78,7 +75,7 @@ static std::unique_ptr MakeDataSource( // TODO: Check if config is a persistent Store (so, if 'method' is // Persistent). If so, return a data_sources::PullModeSource instead. - return std::make_unique( + return std::make_unique( config.ServiceEndpoints(), config.DataSourceConfig(), data_source_properties, executor, status_manager, logger); } @@ -110,12 +107,13 @@ std::unique_ptr> MakeEventProcessor( } return nullptr; } - -/** - * Returns true if the flag pointer is valid and the underlying item is present. - */ -bool IsFlagPresent( - std::shared_ptr const& flag_desc); +R + /** + * 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), @@ -176,9 +174,7 @@ std::future ClientImpl::StartAsyncInternal( return false; /* keep the change listener */ }); - if (auto synchronizer = data_source_->GetSynchronizer().lock()) { - synchronizer->Start(); - } + data_system_->Initialize(); return fut; } @@ -391,7 +387,7 @@ EvaluationDetail ClientImpl::PostEvaluation( } bool IsFlagPresent( - std::shared_ptr const& flag_desc) { + std::shared_ptr const& flag_desc) { return flag_desc && flag_desc->item; } diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 337879f01..74af22dd9 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -3,21 +3,16 @@ #include #include #include -#include #include #include #include #include #include -#include "data_sources/data_destination_interface.hpp" -#include "data_sources/data_source_status_manager.hpp" - -#include "data_sources/data_source_interface.hpp" -#include "data_store/data_store_updater.hpp" +#include "data_retrieval/interfaces/data_system.hpp" +#include "data_retrieval/status_notifications/data_source_status_manager.hpp" #include "evaluation/evaluator.hpp" - #include "events/event_scope.hpp" #include @@ -159,7 +154,7 @@ class ClientImpl : public IClient { std::optional metric_value); std::future StartAsyncInternal( - std::function + std::function predicate); void LogVariationCall(std::string const& key, bool flag_present) const; @@ -173,9 +168,9 @@ class ClientImpl : public IClient { boost::asio::executor_work_guard work_; - data_sources::DataSourceStatusManager status_manager_; + data_retrieval::DataSourceStatusManager status_manager_; - std::unique_ptr data_source_; + std::unique_ptr data_system_; std::unique_ptr event_processor_; diff --git a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp index a02771c68..0037f5979 100644 --- a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp +++ b/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp @@ -45,12 +45,12 @@ void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { } void DataStoreUpdater::Upsert(std::string const& key, - data_sources::FlagDescriptor flag) { + data_model::FlagDescriptor flag) { UpsertCommon(DataKind::kFlag, key, source_.GetFlag(key), std::move(flag)); } void DataStoreUpdater::Upsert(std::string const& key, - data_sources::SegmentDescriptor segment) { + data_model::SegmentDescriptor segment) { UpsertCommon(DataKind::kSegment, key, source_.GetSegment(key), std::move(segment)); } diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp index 223083d36..599da98ae 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp @@ -10,7 +10,7 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_retrieval { class IBootstrapper { public: @@ -29,4 +29,4 @@ class IBootstrapper { IBootstrapper() = default; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp index e83eac1cf..0e8c74dd9 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp @@ -8,17 +8,13 @@ #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side::data_retrieval { class IDataSystem { public: [[nodiscard]] virtual std::string const& Identity() const; - [[nodiscard]] virtual ISynchronizer* GetSynchronizer(); - [[nodiscard]] virtual IBootstrapper* GetBootstrapper(); - - // TODO: Have a GetDataStore() interface? That way we don't need to forward - // methods. + virtual void Initialize(); [[nodiscard]] virtual std::shared_ptr GetFlag( std::string const& key) const = 0; @@ -62,4 +58,4 @@ class IDataSystem { IDataSystem() = default; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp index a0c1e64d4..bbdc27e14 100644 --- a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp @@ -61,13 +61,13 @@ void MemoryStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { } void MemoryStore::Upsert(std::string const& key, - data_sources::FlagDescriptor flag) { + data_model::FlagDescriptor flag) { std::lock_guard lock{data_mutex_}; flags_[key] = std::make_shared(std::move(flag)); } void MemoryStore::Upsert(std::string const& key, - data_sources::SegmentDescriptor segment) { + data_model::SegmentDescriptor segment) { std::lock_guard lock{data_mutex_}; segments_[key] = std::make_shared(std::move(segment)); } diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp index 1565ac8fc..12e00b0cb 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp @@ -7,7 +7,7 @@ namespace launchdarkly::server_side::data_retrieval { using namespace config::shared::built; -PushModeSource::PushModeSource( +BackgroundSync::BackgroundSync( ServiceEndpoints const& endpoints, DataSourceConfig const& data_source_config, HttpProperties http_properties, @@ -37,37 +37,29 @@ PushModeSource::PushModeSource( data_source_config.method); } -std::string const& PushModeSource::Identity() const { +std::string const& BackgroundSync::Identity() const { // TODO: Obtain more specific info static std::string id = "generic push-mode source"; return id; } -ISynchronizer* PushModeSource::GetSynchronizer() { - return synchronizer_.get(); -} - -IBootstrapper* PushModeSource::GetBootstrapper() { - return bootstrapper_.get(); -} - -std::shared_ptr PushModeSource::GetFlag( +std::shared_ptr BackgroundSync::GetFlag( std::string const& key) const { return store_.GetFlag(key); } -std::shared_ptr PushModeSource::GetSegment( +std::shared_ptr BackgroundSync::GetSegment( std::string const& key) const { return store_.GetSegment(key); } -std::unordered_map> -PushModeSource::AllFlags() const { +std::unordered_map> +BackgroundSync::AllFlags() const { return store_.AllFlags(); } -std::unordered_map> -PushModeSource::AllSegments() const { +std::unordered_map> +BackgroundSync::AllSegments() const { return store_.AllSegments(); } diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp index b8dea7c1c..0ff683717 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp @@ -3,23 +3,20 @@ #include #include #include +#include #include -#include "../interfaces/data_destination.hpp" -#include "../interfaces/data_system.hpp" - -#include "../data_source_event_handler.hpp" -#include "../data_source_status_manager.hpp" - -#include "../memory_store/memory_store.hpp" +#include "../../interfaces/data_system.hpp" +#include "../../memory_store/memory_store.hpp" +#include "../../status_notifications/data_source_status_manager.hpp" #include namespace launchdarkly::server_side::data_retrieval { -class PushModeSource : public IDataSource { +class BackgroundSync : public IDataSystem { public: - PushModeSource(config::shared::built::ServiceEndpoints const& endpoints, + BackgroundSync(config::shared::built::ServiceEndpoints const& endpoints, config::shared::built::DataSourceConfig< config::shared::ServerSDK> const& data_source_config, config::shared::built::HttpProperties http_properties, @@ -27,19 +24,18 @@ class PushModeSource : public IDataSource { DataSourceStatusManager& status_manager, Logger const& logger); - PushModeSource(PushModeSource const& item) = delete; - PushModeSource(PushModeSource&& item) = delete; - PushModeSource& operator=(PushModeSource const&) = delete; - PushModeSource& operator=(PushModeSource&&) = delete; + BackgroundSync(BackgroundSync const& item) = delete; + BackgroundSync(BackgroundSync&& item) = delete; + BackgroundSync& operator=(BackgroundSync const&) = delete; + BackgroundSync& operator=(BackgroundSync&&) = delete; std::string const& Identity() const override; - ISynchronizer* GetSynchronizer() override; - IBootstrapper* GetBootstrapper() override; + void Initialize() override; - std::shared_ptr GetFlag( + std::shared_ptr GetFlag( std::string const& key) const override; - std::shared_ptr GetSegment( + std::shared_ptr GetSegment( std::string const& key) const override; std::unordered_map> AllFlags() const override; @@ -48,7 +44,5 @@ class PushModeSource : public IDataSource { private: MemoryStore store_; - std::shared_ptr synchronizer_; - std::shared_ptr bootstrapper_; }; } // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp index c5f46b4c8..27aedd309 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp @@ -214,13 +214,13 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( switch (res->kind) { case data_store::DataKind::kFlag: { handler_.Upsert(res->key, - data_sources::FlagDescriptor(res->version)); + data_model::FlagDescriptor(res->version)); return DataSourceEventHandler::MessageStatus:: kMessageHandled; } case data_store::DataKind::kSegment: { - handler_.Upsert(res->key, data_sources::SegmentDescriptor( - res->version)); + handler_.Upsert( + res->key, data_model::SegmentDescriptor(res->version)); return DataSourceEventHandler::MessageStatus:: kMessageHandled; } diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp index d4141380e..692ccb62f 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp @@ -12,7 +12,7 @@ namespace launchdarkly::server_side::data_retrieval { using namespace config::shared::built; -PullModeSource::PullModeSource( +LazyLoad::LazyLoad( ServiceEndpoints const& endpoints, DataSourceConfig const& data_source_config, HttpProperties http_properties, @@ -21,37 +21,37 @@ PullModeSource::PullModeSource( Logger const& logger) : store_(), synchronizer_(), bootstrapper_() {} -std::string const& PullModeSource::Identity() const { +std::string const& LazyLoad::Identity() const { // TODO: Obtain more specific info static std::string id = "generic pull-mode source"; } -ISynchronizer* PullModeSource::GetSynchronizer() { +ISynchronizer* LazyLoad::GetSynchronizer() { return reinterpret_cast(this); } -IBootstrapper* PullModeSource::GetBootstrapper() { +IBootstrapper* LazyLoad::GetBootstrapper() { // Bootstrapping is not supported in Pull Mode sources yet. // It would be simple: perform a get all. return nullptr; } -void PullModeSource::Init(std::optional initial_data, - IDataDestination& destination) { +void LazyLoad::Init(std::optional initial_data, + IDataDestination& destination) { // TODO: implement // This would happen if we bootstrapped from some source, and now that // data is being passed here for management. // The data would need to be sorted before passing furthur. } -void PullModeSource::Start() { +void LazyLoad::Start() { // No-op, because we all data is pulled on demand. } -void PullModeSource::ShutdownAsync(std::function) { +void LazyLoad::ShutdownAsync(std::function) { // Similar to Start, also a no-op since there's nothing to shutdown // (perhaps explicitly close database client?) } -std::shared_ptr PullModeSource::GetFlag( +std::shared_ptr LazyLoad::GetFlag( std::string const& key) const { auto state = tracker_.State(Keys::kAllSegments, time_()); return Get>( @@ -59,7 +59,7 @@ std::shared_ptr PullModeSource::GetFlag( [this, &key]() { return memory_store_.GetFlag(key); }); } -std::shared_ptr PullModeSource::GetSegment( +std::shared_ptr LazyLoad::GetSegment( std::string const& key) const { auto state = tracker_.State(Keys::kAllSegments, time_()); return Get>( @@ -68,7 +68,7 @@ std::shared_ptr PullModeSource::GetSegment( } std::unordered_map> -PullModeSource::AllFlags() const { +LazyLoad::AllFlags() const { auto state = tracker_.State(Keys::kAllFlags, time_()); return Get< std::unordered_map>>( @@ -77,7 +77,7 @@ PullModeSource::AllFlags() const { } std::unordered_map> -PullModeSource::AllSegments() const { +LazyLoad::AllSegments() const { auto state = tracker_.State(Keys::kAllSegments, time_()); return Get< std::unordered_map>>( @@ -103,24 +103,24 @@ bool PersistentStore::Initialized() const { return initialized_.value_or(false); } -void PullModeSource::RefreshAllFlags() const { +void LazyLoad::RefreshAllFlags() const { auto res = core_->All(Kinds::Flag); // TODO: Deserialize and put in store. tracker_.Add(Keys::kAllSegments, time_()); } -void PullModeSource::RefreshAllSegments() const { +void LazyLoad::RefreshAllSegments() const { auto res = core_->All(Kinds::Segment); // TODO: Deserialize and put in store. tracker_.Add(Keys::kAllFlags, time_()); } -void PullModeSource::RefreshInitState() const { +void LazyLoad::RefreshInitState() const { initialized_ = core_->Initialized(); tracker_.Add(Keys::kInitialized, time_()); } -void PullModeSource::RefreshSegment(std::string const& key) const { +void LazyLoad::RefreshSegment(std::string const& key) const { auto res = core_->Get(Kinds::Segment, key); if (res.has_value()) { if (res->has_value()) { @@ -135,7 +135,7 @@ void PullModeSource::RefreshSegment(std::string const& key) const { // TODO: If there is an actual error, then do we not reset the tracking? } -void PullModeSource::RefreshFlag(std::string const& key) const { +void LazyLoad::RefreshFlag(std::string const& key) const { auto res = core_->Get(Kinds::Segment, key); if (res.has_value()) { if (res->has_value()) { diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp index 2b6c0c6c2..c90093dcf 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp @@ -3,54 +3,45 @@ #include #include #include +#include #include -#include "../interfaces/data_push_source.hpp" -#include "../interfaces/data_system.hpp" - -#include "../data_source_event_handler.hpp" -#include "../data_source_status_manager.hpp" - -#include "../memory_store/memory_store.hpp" +#include "../../interfaces/data_system.hpp" +#include "../../memory_store/memory_store.hpp" +#include "../../status_notifications/data_source_status_manager.hpp" #include namespace launchdarkly::server_side::data_retrieval { -class PullModeSource : public IDataSource, public ISynchronizer { +class LazyLoad : public IDataSystem { public: - PullModeSource(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, - DataSourceStatusManager& status_manager, - Logger const& logger); - - PullModeSource(PullModeSource const& item) = delete; - PullModeSource(PullModeSource&& item) = delete; - PullModeSource& operator=(PullModeSource const&) = delete; - PullModeSource& operator=(PullModeSource&&) = delete; + LazyLoad(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, + DataSourceStatusManager& status_manager, + Logger const& logger); + + LazyLoad(LazyLoad const& item) = delete; + LazyLoad(LazyLoad&& item) = delete; + LazyLoad& operator=(LazyLoad const&) = delete; + LazyLoad& operator=(LazyLoad&&) = delete; std::string const& Identity() const override; - ISynchronizer* GetSynchronizer() override; - IBootstrapper* GetBootstrapper() override; - - std::shared_ptr GetFlag( + std::shared_ptr GetFlag( std::string const& key) const override; - std::shared_ptr GetSegment( + std::shared_ptr GetSegment( std::string const& key) const override; - std::unordered_map> AllFlags() - const override; - std::unordered_map> + std::unordered_map> + AllFlags() const override; + std::unordered_map> AllSegments() const override; - /* ISynchronizer implementation */ - void Init(std::optional initial_data, - IDataDestination& destination) override; - void Start() override; - void ShutdownAsync(std::function) override; + void Initialize() override; private: void RefreshAllFlags() const; @@ -59,14 +50,15 @@ class PullModeSource : public IDataSource, public ISynchronizer { 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); + data_model::FlagDescriptor flag); + static persistence::SerializedItemDescriptor Serialize( + data_model::SegmentDescriptor segment); - static std::optional DeserializeFlag( + static std::optional DeserializeFlag( persistence::SerializedItemDescriptor flag); - static std::optional DeserializeSegment( + static std::optional DeserializeSegment( persistence::SerializedItemDescriptor segment); template @@ -124,7 +116,5 @@ class PullModeSource : public IDataSource, public ISynchronizer { }; MemoryStore store_; - std::shared_ptr synchronizer_; - std::shared_ptr bootstrapper_; }; } // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index 22ba533d1..336ae72ff 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -46,7 +46,7 @@ EvaluationDetail Evaluator::Evaluate( } for (Flag::Prerequisite const& p : flag.prerequisites) { - std::shared_ptr maybe_flag = + std::shared_ptr maybe_flag = source_.GetFlag(p.key); if (!maybe_flag) { @@ -54,7 +54,7 @@ EvaluationDetail Evaluator::Evaluate( EvaluationReason::PrerequisiteFailed(p.key)); } - data_sources::FlagDescriptor const& descriptor = *maybe_flag; + data_model::FlagDescriptor const& descriptor = *maybe_flag; if (!descriptor.item) { // This flag existed at some point, but has since been deleted. diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp index 381d5e93e..9f491dbce 100644 --- a/libs/server-sdk/src/evaluation/rules.cpp +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -82,7 +82,7 @@ tl::expected MatchSegment(Clause const& clause, std::string const& segment_key = value.AsString(); - std::shared_ptr segment_ptr = + std::shared_ptr segment_ptr = store.GetSegment(segment_key); if (!segment_ptr || !segment_ptr->item) { From 6a6a7b9072abe3fe78510328d2f812fa34d19def Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 22 Sep 2023 16:52:30 -0700 Subject: [PATCH 067/244] fixing lots of includes --- .../interfaces/data_dest/data_destination.hpp | 12 ++--- ...ce.hpp => serialized_data_pull_source.hpp} | 0 .../interfaces/data_store/data_store.hpp | 42 +++++++++++++++ .../data_retrieval/interfaces/data_system.hpp | 41 ++------------- .../background_sync_system.cpp | 2 +- .../background_sync_system.hpp | 15 +++--- .../lazy_load/data_version_inspectors.cpp | 0 .../lazy_load/data_version_inspectors.hpp | 4 +- .../systems/lazy_load/lazy_load_system.cpp | 52 +++++-------------- libs/server-sdk/src/evaluation/rules.cpp | 10 ++-- libs/server-sdk/src/evaluation/rules.hpp | 12 ++--- 11 files changed, 87 insertions(+), 103 deletions(-) rename libs/server-sdk/src/data_retrieval/interfaces/data_source/{serialzied_data_pull_source.hpp => serialized_data_pull_source.hpp} (100%) create mode 100644 libs/server-sdk/src/data_retrieval/interfaces/data_store/data_store.hpp delete mode 100644 libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.cpp diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp index 9c6a086c9..79dac3545 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp @@ -1,19 +1,17 @@ #pragma once -#include -#include +#include #include -#include - -#include "../descriptors.hpp" namespace launchdarkly::server_side::data_retrieval { class IDataDestination { public: virtual void Init(data_model::SDKDataSet data_set) = 0; - virtual void Upsert(std::string const& key, FlagDescriptor flag) = 0; - virtual void Upsert(std::string const& key, SegmentDescriptor segment) = 0; + virtual void Upsert(std::string const& key, + data_model::FlagDescriptor flag) = 0; + virtual void Upsert(std::string const& key, + data_model::SegmentDescriptor segment) = 0; virtual std::string const& Identity() const = 0; IDataDestination(IDataDestination const& item) = delete; diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialized_data_pull_source.hpp similarity index 100% rename from libs/server-sdk/src/data_retrieval/interfaces/data_source/serialzied_data_pull_source.hpp rename to libs/server-sdk/src/data_retrieval/interfaces/data_source/serialized_data_pull_source.hpp diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_store/data_store.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_store/data_store.hpp new file mode 100644 index 000000000..adb8d43cb --- /dev/null +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_store/data_store.hpp @@ -0,0 +1,42 @@ +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_retrieval { + +class IDataStore { + [[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< + std::string, + std::shared_ptr> + AllSegments() const = 0; +}; +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp b/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp index 0e8c74dd9..378441ecd 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp +++ b/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp @@ -3,6 +3,8 @@ #include #include +#include "data_store/data_store.hpp" + #include #include #include @@ -10,43 +12,10 @@ namespace launchdarkly::server_side::data_retrieval { -class IDataSystem { +class IDataSystem : public IDataStore { public: - [[nodiscard]] virtual std::string const& Identity() const; - - virtual void Initialize(); - - [[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< - std::string, - std::shared_ptr> - AllSegments() const = 0; + [[nodiscard]] virtual std::string const& Identity() const = 0; + virtual void Initialize() = 0; virtual ~IDataSystem() = default; IDataSystem(IDataSystem const& item) = delete; diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp index 12e00b0cb..1c841447a 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp @@ -39,7 +39,7 @@ BackgroundSync::BackgroundSync( std::string const& BackgroundSync::Identity() const { // TODO: Obtain more specific info - static std::string id = "generic push-mode source"; + static std::string id = "generic background-sync"; return id; } diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp index 0ff683717..d716ecfca 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp @@ -1,15 +1,15 @@ #pragma once +#include "../../interfaces/data_system.hpp" +#include "../../memory_store/memory_store.hpp" +#include "../../status_notifications/data_source_status_manager.hpp" + #include #include #include #include #include -#include "../../interfaces/data_system.hpp" -#include "../../memory_store/memory_store.hpp" -#include "../../status_notifications/data_source_status_manager.hpp" - #include namespace launchdarkly::server_side::data_retrieval { @@ -37,9 +37,10 @@ class BackgroundSync : public IDataSystem { 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> + std::unordered_map> + AllFlags() const override; + std::unordered_map> AllSegments() const override; private: diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.cpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.cpp deleted file mode 100644 index e69de29bb..000000000 diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp index 2b4775879..9877a9f60 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp @@ -4,7 +4,7 @@ #include -namespace launchdarkly::server_side::data_retrieval::pull { +namespace launchdarkly::server_side::data_retrieval { class SegmentKind : public persistence::IPersistentKind { public: std::string const& Namespace() const override; @@ -31,4 +31,4 @@ struct Kinds { static FlagKind const Flag; static SegmentKind const Segment; }; -} // namespace launchdarkly::server_side::data_retrieval::pull +} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp index 692ccb62f..b3b37e550 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp @@ -1,8 +1,5 @@ #include "lazy_load_system.hpp" -#include "../adapters/json_destination.hpp" -#include "../adapters/json_source.hpp" - #include #include @@ -19,68 +16,45 @@ LazyLoad::LazyLoad( boost::asio::any_io_executor ioc, DataSourceStatusManager& status_manager, Logger const& logger) - : store_(), synchronizer_(), bootstrapper_() {} + : store_() {} std::string const& LazyLoad::Identity() const { // TODO: Obtain more specific info - static std::string id = "generic pull-mode source"; -} - -ISynchronizer* LazyLoad::GetSynchronizer() { - return reinterpret_cast(this); -} - -IBootstrapper* LazyLoad::GetBootstrapper() { - // Bootstrapping is not supported in Pull Mode sources yet. - // It would be simple: perform a get all. - return nullptr; + static std::string id = "generic lazy-loader"; } -void LazyLoad::Init(std::optional initial_data, - IDataDestination& destination) { - // TODO: implement - // This would happen if we bootstrapped from some source, and now that - // data is being passed here for management. - // The data would need to be sorted before passing furthur. -} -void LazyLoad::Start() { - // No-op, because we all data is pulled on demand. -} -void LazyLoad::ShutdownAsync(std::function) { - // Similar to Start, also a no-op since there's nothing to shutdown - // (perhaps explicitly close database client?) -} +void LazyLoad::Initialize() {} -std::shared_ptr LazyLoad::GetFlag( +std::shared_ptr LazyLoad::GetFlag( std::string const& key) const { auto state = tracker_.State(Keys::kAllSegments, time_()); - return Get>( + return Get>( state, [this, &key]() { RefreshFlag(key); }, [this, &key]() { return memory_store_.GetFlag(key); }); } -std::shared_ptr LazyLoad::GetSegment( +std::shared_ptr LazyLoad::GetSegment( std::string const& key) const { auto state = tracker_.State(Keys::kAllSegments, time_()); - return Get>( + return Get>( state, [this, &key]() { RefreshSegment(key); }, [this, &key]() { return memory_store_.GetSegment(key); }); } -std::unordered_map> +std::unordered_map> LazyLoad::AllFlags() const { auto state = tracker_.State(Keys::kAllFlags, time_()); - return Get< - std::unordered_map>>( + return Get>>( state, [this]() { RefreshAllFlags(); }, [this]() { return memory_store_.AllFlags(); }); } -std::unordered_map> +std::unordered_map> LazyLoad::AllSegments() const { auto state = tracker_.State(Keys::kAllSegments, time_()); - return Get< - std::unordered_map>>( + return Get>>( state, [this]() { RefreshAllSegments(); }, [this]() { return memory_store_.AllSegments(); }); } diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp index 9f491dbce..ac6359c6c 100644 --- a/libs/server-sdk/src/evaluation/rules.cpp +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -15,7 +15,7 @@ bool MaybeNegate(Clause const& clause, bool value) { tl::expected Match(Flag::Rule const& rule, launchdarkly::Context const& context, - data_sources::IDataSource const& store, + data_retrieval::IDataStore const& store, detail::EvaluationStack& stack) { for (Clause const& clause : rule.clauses) { tl::expected result = Match(clause, context, store, stack); @@ -31,7 +31,7 @@ tl::expected Match(Flag::Rule const& rule, tl::expected Match(Segment::Rule const& rule, Context const& context, - data_sources::IDataSource const& store, + data_retrieval::IDataStore const& store, detail::EvaluationStack& stack, std::string const& key, std::string const& salt) { @@ -61,7 +61,7 @@ tl::expected Match(Segment::Rule const& rule, tl::expected Match(Clause const& clause, launchdarkly::Context const& context, - data_sources::IDataSource const& store, + data_retrieval::IDataStore const& store, detail::EvaluationStack& stack) { if (clause.op == Clause::Op::kSegmentMatch) { return MatchSegment(clause, context, store, stack); @@ -71,7 +71,7 @@ tl::expected Match(Clause const& clause, tl::expected MatchSegment(Clause const& clause, launchdarkly::Context const& context, - data_sources::IDataSource const& store, + data_retrieval::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 @@ -153,7 +153,7 @@ tl::expected MatchNonSegment( tl::expected Contains(Segment const& segment, Context const& context, - data_sources::IDataSource const& store, + data_retrieval::IDataStore const& store, detail::EvaluationStack& stack) { auto guard = stack.NoticeSegment(segment.key); if (!guard) { diff --git a/libs/server-sdk/src/evaluation/rules.hpp b/libs/server-sdk/src/evaluation/rules.hpp index 67b380196..f282bac28 100644 --- a/libs/server-sdk/src/evaluation/rules.hpp +++ b/libs/server-sdk/src/evaluation/rules.hpp @@ -4,7 +4,7 @@ #include #include -#include "../data_sources/data_source_interface.hpp" +#include "../data_retrieval/interfaces/data_store/data_store.hpp" #include "detail/evaluation_stack.hpp" #include "evaluation_error.hpp" @@ -17,18 +17,18 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Match( data_model::Flag::Rule const&, Context const&, - data_sources::IDataSource const& source, + data_retrieval::IDataStore const& store, detail::EvaluationStack& stack); [[nodiscard]] tl::expected Match(data_model::Clause const&, Context const&, - data_sources::IDataSource const&, + data_retrieval::IDataStore const&, detail::EvaluationStack&); [[nodiscard]] tl::expected Match( data_model::Segment::Rule const& rule, Context const& context, - data_sources::IDataSource const& source, + data_retrieval::IDataStore const& store, detail::EvaluationStack& stack, std::string const& key, std::string const& salt); @@ -36,7 +36,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected MatchSegment( data_model::Clause const&, Context const&, - data_sources::IDataSource const&, + data_retrieval::IDataStore const&, detail::EvaluationStack& stack); [[nodiscard]] tl::expected MatchNonSegment( @@ -46,7 +46,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Contains( data_model::Segment const&, Context const&, - data_sources::IDataSource const& source, + data_retrieval::IDataStore const& store, detail::EvaluationStack& stack); [[nodiscard]] bool MaybeNegate(data_model::Clause const& clause, bool value); From f76e6fd3cadf3a3d9892b606948879c26cc3b992 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 24 Oct 2023 11:38:47 -0700 Subject: [PATCH 068/244] rename data_retrieval dir to data --- .../server_side/data_source_status.hpp | 4 +- libs/server-sdk/src/CMakeLists.txt | 27 +++++----- libs/server-sdk/src/bindings/c/sdk.cpp | 3 +- libs/server-sdk/src/client_impl.cpp | 4 +- .../change_notifier_destination.cpp | 4 +- .../change_notifier_destination.hpp | 4 +- .../dependency_tracker/data_kind.hpp | 4 +- .../dependency_tracker/dependency_tracker.cpp | 4 +- .../dependency_tracker/dependency_tracker.hpp | 4 +- .../dependency_tracker/tagged_data.hpp | 4 +- .../interfaces/data_bootstrapper.hpp | 4 +- .../interfaces/data_dest/data_destination.hpp | 4 +- .../data_dest/serialized_data_destination.hpp | 4 +- .../data_source/data_pull_source.hpp | 4 +- .../data_source/data_push_source.hpp | 4 +- .../serialized_data_pull_source.hpp | 4 +- .../interfaces/data_store/data_store.hpp | 4 +- .../interfaces/data_system.hpp | 4 +- .../interfaces/serialized_descriptors.hpp | 4 +- .../memory_store/memory_store.cpp | 26 ++++----- .../src/data/memory_store/memory_store.hpp | 54 +++++++++++++++++++ .../serialization_adapters/README.md | 0 .../json_destination.cpp | 4 +- .../json_destination.hpp | 4 +- .../json_pull_source.cpp | 4 +- .../json_pull_source.hpp | 4 +- .../sources/README.md | 0 .../sources/noop/null_data_source.cpp | 4 +- .../sources/noop/null_data_source.hpp | 4 +- .../sources/polling/polling_data_source.cpp | 4 +- .../sources/polling/polling_data_source.hpp | 4 +- .../streaming/streaming_data_source.cpp | 4 +- .../streaming/streaming_data_source.hpp | 4 +- .../data_source_status.cpp | 4 +- .../data_source_status_manager.hpp | 4 +- .../background_sync_system.cpp | 4 +- .../background_sync_system.hpp | 16 ++++-- .../systems/background_sync/event_handler.cpp | 4 +- .../systems/background_sync/event_handler.hpp | 4 +- .../expiration/expiration_tracker.cpp | 0 .../expiration/expiration_tracker.hpp | 0 .../systems/lazy_load/lazy_load_system.cpp | 4 +- .../systems/lazy_load/lazy_load_system.hpp | 15 ++++-- .../memory_store/memory_store.hpp | 45 ---------------- .../lazy_load/data_version_inspectors.hpp | 34 ------------ .../tests/data_source_event_handler_test.cpp | 2 +- .../tests/server_c_bindings_test.cpp | 2 +- 47 files changed, 174 insertions(+), 182 deletions(-) rename libs/server-sdk/src/{data_retrieval => data}/change_notifier/change_notifier_destination.cpp (96%) rename libs/server-sdk/src/{data_retrieval => data}/change_notifier/change_notifier_destination.hpp (97%) rename libs/server-sdk/src/{data_retrieval => data}/dependency_tracker/data_kind.hpp (50%) rename libs/server-sdk/src/{data_retrieval => data}/dependency_tracker/dependency_tracker.cpp (98%) rename libs/server-sdk/src/{data_retrieval => data}/dependency_tracker/dependency_tracker.hpp (97%) rename libs/server-sdk/src/{data_retrieval => data}/dependency_tracker/tagged_data.hpp (90%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_bootstrapper.hpp (87%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_dest/data_destination.hpp (88%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_dest/serialized_data_destination.hpp (94%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_source/data_pull_source.hpp (93%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_source/data_push_source.hpp (87%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_source/serialized_data_pull_source.hpp (95%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_store/data_store.hpp (91%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/data_system.hpp (85%) rename libs/server-sdk/src/{data_retrieval => data}/interfaces/serialized_descriptors.hpp (95%) rename libs/server-sdk/src/{data_retrieval => data}/memory_store/memory_store.cpp (64%) create mode 100644 libs/server-sdk/src/data/memory_store/memory_store.hpp rename libs/server-sdk/src/{data_retrieval => data}/serialization_adapters/README.md (100%) rename libs/server-sdk/src/{data_retrieval => data}/serialization_adapters/json_destination.cpp (82%) rename libs/server-sdk/src/{data_retrieval => data}/serialization_adapters/json_destination.hpp (80%) rename libs/server-sdk/src/{data_retrieval => data}/serialization_adapters/json_pull_source.cpp (92%) rename libs/server-sdk/src/{data_retrieval => data}/serialization_adapters/json_pull_source.hpp (81%) rename libs/server-sdk/src/{data_retrieval => data}/sources/README.md (100%) rename libs/server-sdk/src/{data_retrieval => data}/sources/noop/null_data_source.cpp (84%) rename libs/server-sdk/src/{data_retrieval => data}/sources/noop/null_data_source.hpp (86%) rename libs/server-sdk/src/{data_retrieval => data}/sources/polling/polling_data_source.cpp (98%) rename libs/server-sdk/src/{data_retrieval => data}/sources/polling/polling_data_source.hpp (94%) rename libs/server-sdk/src/{data_retrieval => data}/sources/streaming/streaming_data_source.cpp (97%) rename libs/server-sdk/src/{data_retrieval => data}/sources/streaming/streaming_data_source.hpp (94%) rename libs/server-sdk/src/{data_retrieval => data}/status_notifications/data_source_status.cpp (91%) rename libs/server-sdk/src/{data_retrieval => data}/status_notifications/data_source_status_manager.hpp (88%) rename libs/server-sdk/src/{data_retrieval => data}/systems/background_sync/background_sync_system.cpp (95%) rename libs/server-sdk/src/{data_retrieval => data}/systems/background_sync/background_sync_system.hpp (74%) rename libs/server-sdk/src/{data_retrieval => data}/systems/background_sync/event_handler.cpp (98%) rename libs/server-sdk/src/{data_retrieval => data}/systems/background_sync/event_handler.hpp (96%) rename libs/server-sdk/src/{data_retrieval => data}/systems/lazy_load/expiration/expiration_tracker.cpp (100%) rename libs/server-sdk/src/{data_retrieval => data}/systems/lazy_load/expiration/expiration_tracker.hpp (100%) rename libs/server-sdk/src/{data_retrieval => data}/systems/lazy_load/lazy_load_system.cpp (97%) rename libs/server-sdk/src/{data_retrieval => data}/systems/lazy_load/lazy_load_system.hpp (89%) delete mode 100644 libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp delete mode 100644 libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp 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 3ed6f6d0d..d06d4c008 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 @@ -13,7 +13,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { /** * Enumeration of possible data source states. @@ -114,4 +114,4 @@ std::ostream& operator<<(std::ostream& out, std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index ce2394ce1..e076c24ad 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -22,20 +22,19 @@ target_sources(${LIBNAME} all_flags_state/all_flags_state.cpp all_flags_state/json_all_flags_state.cpp all_flags_state/all_flags_state_builder.cpp - data_retrieval/change_notifier/change_notifier_destination.cpp - data_retrieval/dependency_tracker/dependency_tracker.cpp - data_retrieval/memory_store/memory_store.cpp - data_retrieval/serialization_adapters/json_destination.cpp - data_retrieval/serialization_adapters/json_pull_source.cpp - data_retrieval/sources/noop/null_data_source.cpp - data_retrieval/sources/streaming/streaming_data_source.cpp - data_retrieval/sources/polling/polling_data_source.cpp - data_retrieval/status_notifications/data_source_status.cpp - data_retrieval/systems/background_sync/background_sync_system.cpp - data_retrieval/systems/background_sync/event_handler.cpp - data_retrieval/systems/lazy_load/data_version_inspectors.cpp - data_retrieval/systems/lazy_load/lazy_load_system.cpp - data_retrieval/systems/lazy_load/expiration/expiration_tracker.cpp + data/change_notifier/change_notifier_destination.cpp + data/dependency_tracker/dependency_tracker.cpp + data/memory_store/memory_store.cpp + data/serialization_adapters/json_destination.cpp + data/serialization_adapters/json_pull_source.cpp + data/sources/noop/null_data_source.cpp + data/sources/streaming/streaming_data_source.cpp + data/sources/polling/polling_data_source.cpp + data/status_notifications/data_source_status.cpp + data/systems/background_sync/background_sync_system.cpp + data/systems/background_sync/event_handler.cpp + data/systems/lazy_load/lazy_load_system.cpp + data/systems/lazy_load/expiration/expiration_tracker.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp index 1679a8ddf..87ca8eabb 100644 --- a/libs/server-sdk/src/bindings/c/sdk.cpp +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -27,8 +27,7 @@ struct Detail; #define FROM_DETAIL(ptr) (reinterpret_cast(ptr)) #define TO_DATASOURCESTATUS(ptr) \ - (reinterpret_cast< \ - launchdarkly::server_side::data_retrieval::DataSourceStatus*>(ptr)) + (reinterpret_cast(ptr)) #define FROM_DATASOURCESTATUS(ptr) \ (reinterpret_cast(ptr)) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index f1de311aa..adb2bf484 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -50,13 +50,13 @@ using launchdarkly::config::shared::built::HttpProperties; // if (config.DataSourceConfig().method.index() == 0) { // // TODO: use initial reconnect delay. // return std::make_shared< -// launchdarkly::server_side::data_retrieval::StreamingDataSource>( +// launchdarkly::server_side::data::StreamingDataSource>( // config.ServiceEndpoints(), config.DataSourceConfig(), // data_source_properties, executor, flag_updater, status_manager, // logger); // } // return std::make_shared< -// launchdarkly::server_side::data_retrieval::PollingDataSource>( +// launchdarkly::server_side::data::PollingDataSource>( // config.ServiceEndpoints(), config.DataSourceConfig(), // data_source_properties, executor, flag_updater, status_manager, // logger); diff --git a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp b/libs/server-sdk/src/data/change_notifier/change_notifier_destination.cpp similarity index 96% rename from libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp rename to libs/server-sdk/src/data/change_notifier/change_notifier_destination.cpp index 0037f5979..b8eaf8731 100644 --- a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.cpp +++ b/libs/server-sdk/src/data/change_notifier/change_notifier_destination.cpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { std::unique_ptr DataStoreUpdater::OnFlagChange( launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { @@ -73,4 +73,4 @@ DataStoreUpdater::DataStoreUpdater(IDataDestination& sink, data_sources::IDataSource const& source) : sink_(sink), source_(source) {} -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp b/libs/server-sdk/src/data/change_notifier/change_notifier_destination.hpp similarity index 97% rename from libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp rename to libs/server-sdk/src/data/change_notifier/change_notifier_destination.hpp index 3c952e8dd..e77f513d4 100644 --- a/libs/server-sdk/src/data_retrieval/change_notifier/change_notifier_destination.hpp +++ b/libs/server-sdk/src/data/change_notifier/change_notifier_destination.hpp @@ -11,7 +11,7 @@ #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class DataStoreUpdater : public IDataDestination, public IChangeNotifier { public: @@ -119,4 +119,4 @@ class DataStoreUpdater : public IDataDestination, public IChangeNotifier { DependencyTracker dependency_tracker_; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp b/libs/server-sdk/src/data/dependency_tracker/data_kind.hpp similarity index 50% rename from libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp rename to libs/server-sdk/src/data/dependency_tracker/data_kind.hpp index 33b651d7a..724bb86d8 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/data_kind.hpp +++ b/libs/server-sdk/src/data/dependency_tracker/data_kind.hpp @@ -2,6 +2,6 @@ #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp b/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.cpp similarity index 98% rename from libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp rename to libs/server-sdk/src/data/dependency_tracker/dependency_tracker.cpp index 671190acb..4e441927d 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.cpp +++ b/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.cpp @@ -3,7 +3,7 @@ #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { DependencySet::DependencySet() : data_{ @@ -194,4 +194,4 @@ void DependencyTracker::Clear() { dependencies_from_.Clear(); } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp b/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.hpp similarity index 97% rename from libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp rename to libs/server-sdk/src/data/dependency_tracker/dependency_tracker.hpp index b937b62ea..34c482caa 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/dependency_tracker.hpp +++ b/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.hpp @@ -12,7 +12,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { /** * Class used to maintain a set of dependencies. Each dependency may be either @@ -152,4 +152,4 @@ class DependencyTracker { std::vector const& clauses); }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp b/libs/server-sdk/src/data/dependency_tracker/tagged_data.hpp similarity index 90% rename from libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp rename to libs/server-sdk/src/data/dependency_tracker/tagged_data.hpp index 2a44717a5..1aa9f5d45 100644 --- a/libs/server-sdk/src/data_retrieval/dependency_tracker/tagged_data.hpp +++ b/libs/server-sdk/src/data/dependency_tracker/tagged_data.hpp @@ -10,7 +10,7 @@ #include "data_kind.hpp" -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { /** * 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 @@ -34,4 +34,4 @@ class TaggedData { Storage storage_; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp b/libs/server-sdk/src/data/interfaces/data_bootstrapper.hpp similarity index 87% rename from libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp rename to libs/server-sdk/src/data/interfaces/data_bootstrapper.hpp index 599da98ae..7c0cc11b0 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_bootstrapper.hpp +++ b/libs/server-sdk/src/data/interfaces/data_bootstrapper.hpp @@ -10,7 +10,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class IBootstrapper { public: @@ -29,4 +29,4 @@ class IBootstrapper { IBootstrapper() = default; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp b/libs/server-sdk/src/data/interfaces/data_dest/data_destination.hpp similarity index 88% rename from libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp rename to libs/server-sdk/src/data/interfaces/data_dest/data_destination.hpp index 79dac3545..2ac1e9e44 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/data_destination.hpp +++ b/libs/server-sdk/src/data/interfaces/data_dest/data_destination.hpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class IDataDestination { public: @@ -23,4 +23,4 @@ class IDataDestination { protected: IDataDestination() = default; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp b/libs/server-sdk/src/data/interfaces/data_dest/serialized_data_destination.hpp similarity index 94% rename from libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp rename to libs/server-sdk/src/data/interfaces/data_dest/serialized_data_destination.hpp index 87bbb1f3b..49e0d875f 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_dest/serialized_data_destination.hpp +++ b/libs/server-sdk/src/data/interfaces/data_dest/serialized_data_destination.hpp @@ -5,7 +5,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class ISerializedDataDestination { public: @@ -65,4 +65,4 @@ class ISerializedDataDestination { protected: ISerializedDataDestination() = default; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp b/libs/server-sdk/src/data/interfaces/data_source/data_pull_source.hpp similarity index 93% rename from libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp rename to libs/server-sdk/src/data/interfaces/data_source/data_pull_source.hpp index 1f18842d7..8d317930a 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_pull_source.hpp +++ b/libs/server-sdk/src/data/interfaces/data_source/data_pull_source.hpp @@ -8,7 +8,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class IDataPullSource { public: @@ -55,4 +55,4 @@ class IDataPullSource { IPullSource() = default; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp b/libs/server-sdk/src/data/interfaces/data_source/data_push_source.hpp similarity index 87% rename from libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp rename to libs/server-sdk/src/data/interfaces/data_source/data_push_source.hpp index 130e5282d..b9ba7fd0e 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_source/data_push_source.hpp +++ b/libs/server-sdk/src/data/interfaces/data_source/data_push_source.hpp @@ -8,7 +8,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class IDataPushSource { public: @@ -29,4 +29,4 @@ class IDataPushSource { IPushSource() = default; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialized_data_pull_source.hpp b/libs/server-sdk/src/data/interfaces/data_source/serialized_data_pull_source.hpp similarity index 95% rename from libs/server-sdk/src/data_retrieval/interfaces/data_source/serialized_data_pull_source.hpp rename to libs/server-sdk/src/data/interfaces/data_source/serialized_data_pull_source.hpp index 64fbf8710..e1a46a309 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_source/serialized_data_pull_source.hpp +++ b/libs/server-sdk/src/data/interfaces/data_source/serialized_data_pull_source.hpp @@ -7,7 +7,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { /** * Interface for a data source that provides feature flags and related data in a @@ -74,4 +74,4 @@ class ISerializedDataPullSource { protected: ISerializedDataPullSource() = default; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_store/data_store.hpp b/libs/server-sdk/src/data/interfaces/data_store/data_store.hpp similarity index 91% rename from libs/server-sdk/src/data_retrieval/interfaces/data_store/data_store.hpp rename to libs/server-sdk/src/data/interfaces/data_store/data_store.hpp index adb8d43cb..8e087461b 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_store/data_store.hpp +++ b/libs/server-sdk/src/data/interfaces/data_store/data_store.hpp @@ -4,7 +4,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class IDataStore { [[nodiscard]] virtual std::shared_ptr GetFlag( @@ -39,4 +39,4 @@ class IDataStore { std::shared_ptr> AllSegments() const = 0; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp b/libs/server-sdk/src/data/interfaces/data_system.hpp similarity index 85% rename from libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp rename to libs/server-sdk/src/data/interfaces/data_system.hpp index 378441ecd..007d44046 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/data_system.hpp +++ b/libs/server-sdk/src/data/interfaces/data_system.hpp @@ -10,7 +10,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class IDataSystem : public IDataStore { public: @@ -27,4 +27,4 @@ class IDataSystem : public IDataStore { IDataSystem() = default; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp b/libs/server-sdk/src/data/interfaces/serialized_descriptors.hpp similarity index 95% rename from libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp rename to libs/server-sdk/src/data/interfaces/serialized_descriptors.hpp index eb5a2d34b..d52bf492f 100644 --- a/libs/server-sdk/src/data_retrieval/interfaces/serialized_descriptors.hpp +++ b/libs/server-sdk/src/data/interfaces/serialized_descriptors.hpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { /** * A versioned item which can be stored in a persistent store. @@ -96,4 +96,4 @@ uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { return GetVersion(data); } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp b/libs/server-sdk/src/data/memory_store/memory_store.cpp similarity index 64% rename from libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp rename to libs/server-sdk/src/data/memory_store/memory_store.cpp index bbdc27e14..b470bdc35 100644 --- a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data/memory_store/memory_store.cpp @@ -1,10 +1,8 @@ - - #include "memory_store.hpp" -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { -std::shared_ptr MemoryStore::GetFlag( +std::shared_ptr MemoryStore::GetFlag( std::string const& key) const { std::lock_guard lock{data_mutex_}; auto found = flags_.find(key); @@ -14,7 +12,7 @@ std::shared_ptr MemoryStore::GetFlag( return nullptr; } -std::shared_ptr MemoryStore::GetSegment( +std::shared_ptr MemoryStore::GetSegment( std::string const& key) const { std::lock_guard lock{data_mutex_}; auto found = segments_.find(key); @@ -24,13 +22,13 @@ std::shared_ptr MemoryStore::GetSegment( return nullptr; } -std::unordered_map> +std::unordered_map> MemoryStore::AllFlags() const { std::lock_guard lock{data_mutex_}; return {flags_}; } -std::unordered_map> +std::unordered_map> MemoryStore::AllSegments() const { std::lock_guard lock{data_mutex_}; return {segments_}; @@ -51,25 +49,27 @@ void MemoryStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { flags_.clear(); segments_.clear(); for (auto flag : dataSet.flags) { - flags_.emplace(flag.first, std::make_shared( + 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))); + segments_.emplace(segment.first, + std::make_shared( + std::move(segment.second))); } } void MemoryStore::Upsert(std::string const& key, data_model::FlagDescriptor flag) { std::lock_guard lock{data_mutex_}; - flags_[key] = std::make_shared(std::move(flag)); + flags_[key] = std::make_shared(std::move(flag)); } void MemoryStore::Upsert(std::string const& key, data_model::SegmentDescriptor segment) { std::lock_guard lock{data_mutex_}; - segments_[key] = std::make_shared(std::move(segment)); + segments_[key] = + std::make_shared(std::move(segment)); } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data/memory_store/memory_store.hpp b/libs/server-sdk/src/data/memory_store/memory_store.hpp new file mode 100644 index 000000000..77542f4a5 --- /dev/null +++ b/libs/server-sdk/src/data/memory_store/memory_store.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "../interfaces/data_dest/data_destination.hpp" + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data { + +class MemoryStore : public IDataDestination { + public: + std::shared_ptr GetFlag( + std::string const& key) const; + + std::shared_ptr GetSegment( + std::string const& key) const; + + std::unordered_map> + AllFlags() const; + std::unordered_map> + AllSegments() const; + + bool Initialized() const; + std::string const& Description() const; + + void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Upsert(std::string const& key, + data_model::FlagDescriptor flag) override; + void Upsert(std::string const& key, + data_model::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 const description_ = "memory"; + std::unordered_map> + flags_; + std::unordered_map> + segments_; + bool initialized_ = false; + mutable std::mutex data_mutex_; +}; + +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/README.md b/libs/server-sdk/src/data/serialization_adapters/README.md similarity index 100% rename from libs/server-sdk/src/data_retrieval/serialization_adapters/README.md rename to libs/server-sdk/src/data/serialization_adapters/README.md diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data/serialization_adapters/json_destination.cpp similarity index 82% rename from libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp rename to libs/server-sdk/src/data/serialization_adapters/json_destination.cpp index 9c554955a..c48ee6a92 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data/serialization_adapters/json_destination.cpp @@ -1,6 +1,6 @@ #include "json_destination.hpp" -namespace launchdarkly::server_side::data_retrieval::adapters { +namespace launchdarkly::server_side::data::adapters { JsonDestination::JsonDestination(ISerializedDataDestination& destination) : dest_(destination) {} @@ -21,4 +21,4 @@ void JsonDestination::Upsert(std::string const& key, std::string JsonDestination::Identity() const { return dest_.Identity(); } -} // namespace launchdarkly::server_side::data_retrieval::adapters +} // namespace launchdarkly::server_side::data::adapters diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data/serialization_adapters/json_destination.hpp similarity index 80% rename from libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp rename to libs/server-sdk/src/data/serialization_adapters/json_destination.hpp index 0577763da..d48720d10 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data/serialization_adapters/json_destination.hpp @@ -3,7 +3,7 @@ #include "../interfaces/data_destination.hpp" #include "../interfaces/serialized_data_destination.hpp" -namespace launchdarkly::server_side::data_retrieval::adapters { +namespace launchdarkly::server_side::data::adapters { class JsonDestination : public IDataDestination { public: @@ -18,4 +18,4 @@ class JsonDestination : public IDataDestination { ISerializedDataDestination& dest_; }; -} // namespace launchdarkly::server_side::data_retrieval::adapters +} // namespace launchdarkly::server_side::data::adapters diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data/serialization_adapters/json_pull_source.cpp similarity index 92% rename from libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp rename to libs/server-sdk/src/data/serialization_adapters/json_pull_source.cpp index 7152f037c..c08de5bf6 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data/serialization_adapters/json_pull_source.cpp @@ -1,6 +1,6 @@ #include "json_pull_source.hpp" -namespace launchdarkly::server_side::data_retrieval::adapters { +namespace launchdarkly::server_side::data::adapters { template static std::optional> Deserialize( @@ -53,4 +53,4 @@ std::string JsonSource::Identity() const { return source_.Identity(); } -} // namespace launchdarkly::server_side::data_retrieval::adapters +} // namespace launchdarkly::server_side::data::adapters diff --git a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data/serialization_adapters/json_pull_source.hpp similarity index 81% rename from libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp rename to libs/server-sdk/src/data/serialization_adapters/json_pull_source.hpp index 1f4cc8b4b..2c2ccd52a 100644 --- a/libs/server-sdk/src/data_retrieval/serialization_adapters/json_pull_source.hpp +++ b/libs/server-sdk/src/data/serialization_adapters/json_pull_source.hpp @@ -3,7 +3,7 @@ #include "../interfaces/data_pull_source.hpp" #include "../interfaces/serialzied_data_pull_source.hpp" -namespace launchdarkly::server_side::data_retrieval::adapters { +namespace launchdarkly::server_side::data::adapters { class JsonSource : public IPullSource { public: @@ -19,4 +19,4 @@ class JsonSource : public IPullSource { ISerializedDataSource& source_; }; -} // namespace launchdarkly::server_side::data_retrieval::adapters +} // namespace launchdarkly::server_side::data::adapters diff --git a/libs/server-sdk/src/data_retrieval/sources/README.md b/libs/server-sdk/src/data/sources/README.md similarity index 100% rename from libs/server-sdk/src/data_retrieval/sources/README.md rename to libs/server-sdk/src/data/sources/README.md diff --git a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data/sources/noop/null_data_source.cpp similarity index 84% rename from libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp rename to libs/server-sdk/src/data/sources/noop/null_data_source.cpp index 379a884be..6fbae45ed 100644 --- a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data/sources/noop/null_data_source.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { void NullDataSource::Start() { status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); @@ -19,4 +19,4 @@ NullDataSource::NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager) : status_manager_(status_manager), exec_(exec) {} -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data/sources/noop/null_data_source.hpp similarity index 86% rename from libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp rename to libs/server-sdk/src/data/sources/noop/null_data_source.hpp index b167f247b..85814c4b4 100644 --- a/libs/server-sdk/src/data_retrieval/sources/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data/sources/noop/null_data_source.hpp @@ -6,7 +6,7 @@ #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class NullDataSource : public ISynchronizer { public: @@ -23,4 +23,4 @@ class NullDataSource : public ISynchronizer { boost::asio::any_io_executor exec_; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data/sources/polling/polling_data_source.cpp similarity index 98% rename from libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp rename to libs/server-sdk/src/data/sources/polling/polling_data_source.cpp index 366e2680d..3413f059e 100644 --- a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data/sources/polling/polling_data_source.cpp @@ -13,7 +13,7 @@ #include "data_source_update_sink.hpp" #include "polling_data_source.hpp" -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { static char const* const kErrorParsingPut = "Could not parse polling payload"; static char const* const kErrorPutInvalid = @@ -235,4 +235,4 @@ void PollingDataSource::ShutdownAsync(std::function completion) { } } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data/sources/polling/polling_data_source.hpp similarity index 94% rename from libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp rename to libs/server-sdk/src/data/sources/polling/polling_data_source.hpp index 5fecc81d1..9d049f1cb 100644 --- a/libs/server-sdk/src/data_retrieval/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data/sources/polling/polling_data_source.hpp @@ -13,7 +13,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class PollingDataSource : public IDataPushSource, @@ -54,4 +54,4 @@ class PollingDataSource void StartPollingTimer(); }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data/sources/streaming/streaming_data_source.cpp similarity index 97% rename from libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp rename to libs/server-sdk/src/data/sources/streaming/streaming_data_source.cpp index 4e2f818c7..17e8d71a3 100644 --- a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data/sources/streaming/streaming_data_source.cpp @@ -9,7 +9,7 @@ #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { static char const* const kCouldNotParseEndpoint = "Could not parse streaming endpoint URL"; @@ -153,4 +153,4 @@ void StreamingDataSource::ShutdownAsync(std::function completion) { boost::asio::post(exec_, completion); } } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data/sources/streaming/streaming_data_source.hpp similarity index 94% rename from libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp rename to libs/server-sdk/src/data/sources/streaming/streaming_data_source.hpp index 6a51bfc36..cabdc28cc 100644 --- a/libs/server-sdk/src/data_retrieval/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data/sources/streaming/streaming_data_source.hpp @@ -17,7 +17,7 @@ using namespace std::chrono_literals; #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class StreamingDataSource final : public IDataPushSource, @@ -52,4 +52,4 @@ class StreamingDataSource final Logger const& logger_; std::shared_ptr client_; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp b/libs/server-sdk/src/data/status_notifications/data_source_status.cpp similarity index 91% rename from libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp rename to libs/server-sdk/src/data/status_notifications/data_source_status.cpp index 958439c0e..ec4708399 100644 --- a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status.cpp +++ b/libs/server-sdk/src/data/status_notifications/data_source_status.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state) { @@ -37,4 +37,4 @@ std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { return out; } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp b/libs/server-sdk/src/data/status_notifications/data_source_status_manager.hpp similarity index 88% rename from libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp rename to libs/server-sdk/src/data/status_notifications/data_source_status_manager.hpp index f34599916..4e1674ce0 100644 --- a/libs/server-sdk/src/data_retrieval/status_notifications/data_source_status_manager.hpp +++ b/libs/server-sdk/src/data/status_notifications/data_source_status_manager.hpp @@ -9,7 +9,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { class DataSourceStatusManager : public internal::data_sources::DataSourceStatusManagerBase< @@ -25,4 +25,4 @@ class DataSourceStatusManager DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data/systems/background_sync/background_sync_system.cpp similarity index 95% rename from libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp rename to libs/server-sdk/src/data/systems/background_sync/background_sync_system.cpp index 1c841447a..31411403f 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data/systems/background_sync/background_sync_system.cpp @@ -3,7 +3,7 @@ #include "../polling_data_source.hpp" #include "../streaming_data_source.hpp" -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { using namespace config::shared::built; @@ -63,4 +63,4 @@ BackgroundSync::AllSegments() const { return store_.AllSegments(); } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data/systems/background_sync/background_sync_system.hpp similarity index 74% rename from libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp rename to libs/server-sdk/src/data/systems/background_sync/background_sync_system.hpp index d716ecfca..0b32c6b10 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data/systems/background_sync/background_sync_system.hpp @@ -12,8 +12,18 @@ #include -namespace launchdarkly::server_side::data_retrieval { - +namespace launchdarkly::server_side::data { + +/** + * BackgroundSync implements the standard Data System which receives + * data updates from LaunchDarkly as they happen. It updates an in-memory + * store with the data, and always operates with a complete dataset. + * + * The BackgroundSync system is advantageous because it allows flag evaluations + * to take place with up-to-date information. However, some environments may be + * too large to fit in memory, or a direct connection to LaunchDarkly isn't + * desired, necessitating use of the alternate LazyLoad system. + */ class BackgroundSync : public IDataSystem { public: BackgroundSync(config::shared::built::ServiceEndpoints const& endpoints, @@ -46,4 +56,4 @@ class BackgroundSync : public IDataSystem { private: MemoryStore store_; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp b/libs/server-sdk/src/data/systems/background_sync/event_handler.cpp similarity index 98% rename from libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp rename to libs/server-sdk/src/data/systems/background_sync/event_handler.cpp index 27aedd309..d64fdcf75 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.cpp +++ b/libs/server-sdk/src/data/systems/background_sync/event_handler.cpp @@ -15,7 +15,7 @@ #include "tl/expected.hpp" -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { static char const* const kErrorParsingPut = "Could not parse PUT message"; static char const* const kErrorPutInvalid = @@ -238,4 +238,4 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( return DataSourceEventHandler::MessageStatus::kUnhandledVerb; } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp b/libs/server-sdk/src/data/systems/background_sync/event_handler.hpp similarity index 96% rename from libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp rename to libs/server-sdk/src/data/systems/background_sync/event_handler.hpp index ce31fe56a..a256ffec2 100644 --- a/libs/server-sdk/src/data_retrieval/systems/background_sync/event_handler.hpp +++ b/libs/server-sdk/src/data/systems/background_sync/event_handler.hpp @@ -14,7 +14,7 @@ #include #include -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { // The FlagsPath and SegmentsPath are made to turn a string literal into a type // for use in a template. @@ -120,4 +120,4 @@ class DataSourceEventHandler { Logger const& logger_; DataSourceStatusManager& status_manager_; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.cpp b/libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.cpp similarity index 100% rename from libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.cpp rename to libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.cpp diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.hpp b/libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.hpp similarity index 100% rename from libs/server-sdk/src/data_retrieval/systems/lazy_load/expiration/expiration_tracker.hpp rename to libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.hpp diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.cpp similarity index 97% rename from libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp rename to libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.cpp index b3b37e550..302f87295 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.cpp @@ -5,7 +5,7 @@ #include "data_version_inspectors.hpp" -namespace launchdarkly::server_side::data_retrieval { +namespace launchdarkly::server_side::data { using namespace config::shared::built; @@ -124,4 +124,4 @@ void LazyLoad::RefreshFlag(std::string const& key) const { // TODO: If there is an actual error, then do we not reset the tracking? } -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.hpp similarity index 89% rename from libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp rename to libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.hpp index c90093dcf..d1a298f99 100644 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.hpp @@ -12,8 +12,17 @@ #include -namespace launchdarkly::server_side::data_retrieval { - +namespace launchdarkly::server_side::data { + +/** + * LazyLoad implements a Data System that pulls data from a persistent store + * on-demand. It is intended for use cases where holding an entire environment + * in memory isn't desired, or for uses that disallow direct communication + * with LaunchDarkly servers. + * + * LazyLoad is able to remain efficient because it caches responses from the + * store. Over time, data becomes stale causing the system to refresh data. + */ class LazyLoad : public IDataSystem { public: LazyLoad(config::shared::built::ServiceEndpoints const& endpoints, @@ -117,4 +126,4 @@ class LazyLoad : public IDataSystem { MemoryStore store_; }; -} // namespace launchdarkly::server_side::data_retrieval +} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp b/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp deleted file mode 100644 index aff378005..000000000 --- a/libs/server-sdk/src/data_retrieval/memory_store/memory_store.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include "../data_destination_interface.hpp" - -namespace launchdarkly::server_side::data_retrieval { - -class MemoryStore : public data_sources::IDataDestination { - public: - std::shared_ptr GetFlag(std::string const& key) const; - std::shared_ptr GetSegment(std::string const& key) const; - - std::unordered_map> AllFlags() - const; - std::unordered_map> - AllSegments() const; - - bool Initialized() const; - std::string const& Description() const; - - 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() = 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 const description_ = "memory"; - std::unordered_map> flags_; - std::unordered_map> - segments_; - bool initialized_ = false; - mutable std::mutex data_mutex_; -}; - -} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp b/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp deleted file mode 100644 index 9877a9f60..000000000 --- a/libs/server-sdk/src/data_retrieval/systems/lazy_load/data_version_inspectors.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include - -#include - -namespace launchdarkly::server_side::data_retrieval { -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 inline std::string const 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 inline std::string const namespace_ = "features"; -}; - -struct Kinds { - static FlagKind const Flag; - static SegmentKind const Segment; -}; -} // namespace launchdarkly::server_side::data_retrieval diff --git a/libs/server-sdk/tests/data_source_event_handler_test.cpp b/libs/server-sdk/tests/data_source_event_handler_test.cpp index f8fe9de4b..b19c92b6f 100644 --- a/libs/server-sdk/tests/data_source_event_handler_test.cpp +++ b/libs/server-sdk/tests/data_source_event_handler_test.cpp @@ -6,7 +6,7 @@ using namespace launchdarkly; using namespace launchdarkly::server_side; -using namespace launchdarkly::server_side::data_retrieval; +using namespace launchdarkly::server_side::data; using namespace launchdarkly::server_side::data_store; TEST(DataSourceEventHandlerTests, HandlesEmptyPutMessage) { diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 0c6a345ea..3c047f9dc 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -10,7 +10,7 @@ #include -using launchdarkly::server_side::data_retrieval::DataSourceStatus; +using launchdarkly::server_side::data::DataSourceStatus; TEST(ClientBindings, MinimalInstantiation) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); From d3d5ff25ece2ee25723f5a4a48fe6c370a2da360 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 24 Oct 2023 13:12:39 -0700 Subject: [PATCH 069/244] massive refactoring of namespaces --- .../integrations/persistent_store_core.hpp | 214 ------------------ .../integrations}/serialized_descriptors.hpp | 4 +- libs/server-sdk/src/CMakeLists.txt | 26 +-- libs/server-sdk/src/client_impl.cpp | 12 +- libs/server-sdk/src/client_impl.hpp | 15 +- .../src/data/dependency_tracker/data_kind.hpp | 7 - .../src/data/interfaces/data_bootstrapper.hpp | 32 --- .../src/data/interfaces/data_system.hpp | 30 --- .../json_destination.hpp | 21 -- .../json_pull_source.hpp | 22 -- .../change_notifier_destination.cpp | 6 +- .../change_notifier_destination.hpp | 15 +- .../dependency_tracker/data_kind.hpp | 7 + .../dependency_tracker/dependency_tracker.cpp | 4 +- .../dependency_tracker/dependency_tracker.hpp | 4 +- .../dependency_tracker/tagged_data.hpp | 4 +- .../memory_store/memory_store.cpp | 4 +- .../memory_store/memory_store.hpp | 8 +- .../serialization_adapters/README.md | 8 +- .../json_destination.cpp | 12 +- .../json_destination.hpp | 20 ++ .../json_pull_source.cpp | 30 ++- .../json_pull_source.hpp | 24 ++ .../sources/README.md | 0 .../sources/noop/null_data_source.cpp | 2 +- .../sources/noop/null_data_source.hpp | 2 +- .../sources/polling/polling_data_source.cpp | 12 +- .../sources/polling/polling_data_source.hpp | 21 +- .../streaming/streaming_data_source.cpp | 14 +- .../streaming/streaming_data_source.hpp | 23 +- .../data_source_status.cpp | 8 +- .../data_source_status_manager.hpp | 15 +- .../bootstrapper/ibootstrapper.hpp | 72 ++++++ .../destination/idestination.hpp} | 18 +- .../destination/iserialized_destination.hpp} | 34 +-- .../source/ipull_source.hpp} | 6 +- .../source/ipush_source.hpp} | 10 +- .../source/iserialized_pull_source.hpp} | 19 +- .../store/istore.hpp} | 6 +- .../src/data_interfaces/system/isystem.hpp | 30 +++ .../background_sync_system.cpp | 7 +- .../background_sync_system.hpp | 12 +- .../background_sync/event_handler.cpp | 2 +- .../background_sync/event_handler.hpp | 4 +- .../expiration/expiration_tracker.cpp | 0 .../expiration/expiration_tracker.hpp | 0 .../lazy_load/lazy_load_system.cpp | 4 +- .../lazy_load/lazy_load_system.hpp | 14 +- 48 files changed, 348 insertions(+), 516 deletions(-) delete mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp rename libs/server-sdk/{src/data/interfaces => include/launchdarkly/server_side/integrations}/serialized_descriptors.hpp (96%) delete mode 100644 libs/server-sdk/src/data/dependency_tracker/data_kind.hpp delete mode 100644 libs/server-sdk/src/data/interfaces/data_bootstrapper.hpp delete mode 100644 libs/server-sdk/src/data/interfaces/data_system.hpp delete mode 100644 libs/server-sdk/src/data/serialization_adapters/json_destination.hpp delete mode 100644 libs/server-sdk/src/data/serialization_adapters/json_pull_source.hpp rename libs/server-sdk/src/{data => data_components}/change_notifier/change_notifier_destination.cpp (94%) rename libs/server-sdk/src/{data => data_components}/change_notifier/change_notifier_destination.hpp (90%) create mode 100644 libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp rename libs/server-sdk/src/{data => data_components}/dependency_tracker/dependency_tracker.cpp (98%) rename libs/server-sdk/src/{data => data_components}/dependency_tracker/dependency_tracker.hpp (97%) rename libs/server-sdk/src/{data => data_components}/dependency_tracker/tagged_data.hpp (90%) rename libs/server-sdk/src/{data => data_components}/memory_store/memory_store.cpp (95%) rename libs/server-sdk/src/{data => data_components}/memory_store/memory_store.hpp (86%) rename libs/server-sdk/src/{data => data_components}/serialization_adapters/README.md (78%) rename libs/server-sdk/src/{data => data_components}/serialization_adapters/json_destination.cpp (51%) create mode 100644 libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp rename libs/server-sdk/src/{data => data_components}/serialization_adapters/json_pull_source.cpp (50%) create mode 100644 libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp rename libs/server-sdk/src/{data => data_components}/sources/README.md (100%) rename libs/server-sdk/src/{data => data_components}/sources/noop/null_data_source.cpp (91%) rename libs/server-sdk/src/{data => data_components}/sources/noop/null_data_source.hpp (93%) rename libs/server-sdk/src/{data => data_components}/sources/polling/polling_data_source.cpp (98%) rename libs/server-sdk/src/{data => data_components}/sources/polling/polling_data_source.hpp (82%) rename libs/server-sdk/src/{data => data_components}/sources/streaming/streaming_data_source.cpp (97%) rename libs/server-sdk/src/{data => data_components}/sources/streaming/streaming_data_source.hpp (83%) rename libs/server-sdk/src/{data => data_components}/status_notifications/data_source_status.cpp (91%) rename libs/server-sdk/src/{data => data_components}/status_notifications/data_source_status_manager.hpp (87%) create mode 100644 libs/server-sdk/src/data_interfaces/bootstrapper/ibootstrapper.hpp rename libs/server-sdk/src/{data/interfaces/data_dest/data_destination.hpp => data_interfaces/destination/idestination.hpp} (52%) rename libs/server-sdk/src/{data/interfaces/data_dest/serialized_data_destination.hpp => data_interfaces/destination/iserialized_destination.hpp} (53%) rename libs/server-sdk/src/{data/interfaces/data_source/data_pull_source.hpp => data_interfaces/source/ipull_source.hpp} (92%) rename libs/server-sdk/src/{data/interfaces/data_source/data_push_source.hpp => data_interfaces/source/ipush_source.hpp} (75%) rename libs/server-sdk/src/{data/interfaces/data_source/serialized_data_pull_source.hpp => data_interfaces/source/iserialized_pull_source.hpp} (80%) rename libs/server-sdk/src/{data/interfaces/data_store/data_store.hpp => data_interfaces/store/istore.hpp} (89%) create mode 100644 libs/server-sdk/src/data_interfaces/system/isystem.hpp rename libs/server-sdk/src/{data/systems => data_systems}/background_sync/background_sync_system.cpp (92%) rename libs/server-sdk/src/{data/systems => data_systems}/background_sync/background_sync_system.hpp (85%) rename libs/server-sdk/src/{data/systems => data_systems}/background_sync/event_handler.cpp (99%) rename libs/server-sdk/src/{data/systems => data_systems}/background_sync/event_handler.hpp (97%) rename libs/server-sdk/src/{data/systems => data_systems}/lazy_load/expiration/expiration_tracker.cpp (100%) rename libs/server-sdk/src/{data/systems => data_systems}/lazy_load/expiration/expiration_tracker.hpp (100%) rename libs/server-sdk/src/{data/systems => data_systems}/lazy_load/lazy_load_system.cpp (97%) rename libs/server-sdk/src/{data/systems => data_systems}/lazy_load/lazy_load_system.hpp (92%) 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/interfaces/serialized_descriptors.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp similarity index 96% rename from libs/server-sdk/src/data/interfaces/serialized_descriptors.hpp rename to libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp index d52bf492f..e6eb1e81c 100644 --- a/libs/server-sdk/src/data/interfaces/serialized_descriptors.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::integrations { /** * A versioned item which can be stored in a persistent store. @@ -96,4 +96,4 @@ uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { return GetVersion(data); } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index e076c24ad..a89f32d8a 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -22,19 +22,19 @@ target_sources(${LIBNAME} all_flags_state/all_flags_state.cpp all_flags_state/json_all_flags_state.cpp all_flags_state/all_flags_state_builder.cpp - data/change_notifier/change_notifier_destination.cpp - data/dependency_tracker/dependency_tracker.cpp - data/memory_store/memory_store.cpp - data/serialization_adapters/json_destination.cpp - data/serialization_adapters/json_pull_source.cpp - data/sources/noop/null_data_source.cpp - data/sources/streaming/streaming_data_source.cpp - data/sources/polling/polling_data_source.cpp - data/status_notifications/data_source_status.cpp - data/systems/background_sync/background_sync_system.cpp - data/systems/background_sync/event_handler.cpp - data/systems/lazy_load/lazy_load_system.cpp - data/systems/lazy_load/expiration/expiration_tracker.cpp + data_components/change_notifier/change_notifier_destination.cpp + data_components/dependency_tracker/dependency_tracker.cpp + data_components/memory_store/memory_store.cpp + data_components/serialization_adapters/json_destination.cpp + data_components/serialization_adapters/json_pull_source.cpp + data_components/sources/noop/null_data_source.cpp + data_components/sources/streaming/streaming_data_source.cpp + data_components/sources/polling/polling_data_source.cpp + data_components/status_notifications/data_source_status.cpp + data_systems/background_sync/background_sync_system.cpp + data_systems/background_sync/event_handler.cpp + data_systems/lazy_load/lazy_load_system.cpp + data_systems/lazy_load/expiration/expiration_tracker.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index adb2bf484..047d5cdf4 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -1,9 +1,3 @@ - -#include - -#include -#include - #include "client_impl.hpp" #include "all_flags_state/all_flags_state_builder.hpp" @@ -16,6 +10,10 @@ #include #include +#include +#include +#include + namespace launchdarkly::server_side { // The ASIO implementation assumes that the io_context will be run from a @@ -62,7 +60,7 @@ using launchdarkly::config::shared::built::HttpProperties; // logger); // } -static std::unique_ptr MakeDataSystem( +static std::unique_ptr MakeDataSystem( HttpProperties const& http_properties, Config const& config, boost::asio::any_io_executor const& executor, diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 74af22dd9..23f2f9312 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -1,5 +1,10 @@ #pragma once +#include "data_components/status_notifications/data_source_status_manager.hpp" +#include "data_interfaces/system/isystem.hpp" +#include "evaluation/evaluator.hpp" +#include "events/event_scope.hpp" + #include #include #include @@ -9,12 +14,6 @@ #include #include -#include "data_retrieval/interfaces/data_system.hpp" -#include "data_retrieval/status_notifications/data_source_status_manager.hpp" - -#include "evaluation/evaluator.hpp" -#include "events/event_scope.hpp" - #include #include @@ -102,7 +101,7 @@ class ClientImpl : public IClient { FlagKey const& key, Value default_value) override; - data_sources::IDataSourceStatusProvider& DataSourceStatus() override; + data_interfaces::IDataSourceStatusProvider& DataSourceStatus() override; ~ClientImpl(); @@ -170,7 +169,7 @@ class ClientImpl : public IClient { data_retrieval::DataSourceStatusManager status_manager_; - std::unique_ptr data_system_; + std::unique_ptr data_system_; std::unique_ptr event_processor_; diff --git a/libs/server-sdk/src/data/dependency_tracker/data_kind.hpp b/libs/server-sdk/src/data/dependency_tracker/data_kind.hpp deleted file mode 100644 index 724bb86d8..000000000 --- a/libs/server-sdk/src/data/dependency_tracker/data_kind.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#pragma once - -#include - -namespace launchdarkly::server_side::data { -enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; -} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data/interfaces/data_bootstrapper.hpp b/libs/server-sdk/src/data/interfaces/data_bootstrapper.hpp deleted file mode 100644 index 7c0cc11b0..000000000 --- a/libs/server-sdk/src/data/interfaces/data_bootstrapper.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once -#include "../descriptors.hpp" - -#include - -#include - -#include -#include -#include -#include - -namespace launchdarkly::server_side::data { - -class IBootstrapper { - public: - using Error = std::string; - virtual bool IsAuthoritative() const = 0; - virtual tl::expected FetchAll( - std::chrono::milliseconds timeout_hint) = 0; - virtual std::string const& Identity() const = 0; - virtual ~IBootstrapper() = default; - IBootstrapper(IBootstrapper const& item) = delete; - IBootstrapper(IBootstrapper&& item) = delete; - IBootstrapper& operator=(IBootstrapper const&) = delete; - IBootstrapper& operator=(IBootstrapper&&) = delete; - - protected: - IBootstrapper() = default; -}; - -} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data/interfaces/data_system.hpp b/libs/server-sdk/src/data/interfaces/data_system.hpp deleted file mode 100644 index 007d44046..000000000 --- a/libs/server-sdk/src/data/interfaces/data_system.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -#include "data_store/data_store.hpp" - -#include -#include -#include -#include - -namespace launchdarkly::server_side::data { - -class IDataSystem : public IDataStore { - public: - [[nodiscard]] virtual std::string const& Identity() const = 0; - virtual void Initialize() = 0; - - virtual ~IDataSystem() = default; - IDataSystem(IDataSystem const& item) = delete; - IDataSystem(IDataSystem&& item) = delete; - IDataSystem& operator=(IDataSystem const&) = delete; - IDataSystem& operator=(IDataSystem&&) = delete; - - protected: - IDataSystem() = default; -}; - -} // namespace launchdarkly::server_side::data diff --git a/libs/server-sdk/src/data/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data/serialization_adapters/json_destination.hpp deleted file mode 100644 index d48720d10..000000000 --- a/libs/server-sdk/src/data/serialization_adapters/json_destination.hpp +++ /dev/null @@ -1,21 +0,0 @@ - -#include "../descriptors.hpp" -#include "../interfaces/data_destination.hpp" -#include "../interfaces/serialized_data_destination.hpp" - -namespace launchdarkly::server_side::data::adapters { - -class JsonDestination : public IDataDestination { - public: - JsonDestination(ISerializedDataDestination& destination); - - void Init(data_model::SDKDataSet data_set) override; - void Upsert(std::string& key, FlagDescriptor flag) override; - void Upsert(std::string& key, SegmentDescriptor segment) override; - std::string Identity() const override; - - private: - ISerializedDataDestination& dest_; -}; - -} // namespace launchdarkly::server_side::data::adapters diff --git a/libs/server-sdk/src/data/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data/serialization_adapters/json_pull_source.hpp deleted file mode 100644 index 2c2ccd52a..000000000 --- a/libs/server-sdk/src/data/serialization_adapters/json_pull_source.hpp +++ /dev/null @@ -1,22 +0,0 @@ - -#include "../descriptors.hpp" -#include "../interfaces/data_pull_source.hpp" -#include "../interfaces/serialzied_data_pull_source.hpp" - -namespace launchdarkly::server_side::data::adapters { - -class JsonSource : public IPullSource { - public: - FlagDescriptor GetFlag(std::string& key) const override; - SegmentDescriptor GetSegment(std::string& key) const override; - std::unordered_map AllFlags() const override; - std::unordered_map AllSegments() - const override; - std::string Identity() const override; - - public: - private: - ISerializedDataSource& source_; -}; - -} // namespace launchdarkly::server_side::data::adapters diff --git a/libs/server-sdk/src/data/change_notifier/change_notifier_destination.cpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp similarity index 94% rename from libs/server-sdk/src/data/change_notifier/change_notifier_destination.cpp rename to libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp index b8eaf8731..d1b1e3684 100644 --- a/libs/server-sdk/src/data/change_notifier/change_notifier_destination.cpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp @@ -3,7 +3,7 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { std::unique_ptr DataStoreUpdater::OnFlagChange( launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { @@ -69,8 +69,8 @@ void DataStoreUpdater::NotifyChanges(DependencySet changes) { } } -DataStoreUpdater::DataStoreUpdater(IDataDestination& sink, +DataStoreUpdater::DataStoreUpdater(IDestination& sink, data_sources::IDataSource const& source) : sink_(sink), source_(source) {} -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/change_notifier/change_notifier_destination.hpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp similarity index 90% rename from libs/server-sdk/src/data/change_notifier/change_notifier_destination.hpp rename to libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp index e77f513d4..b2e4921f4 100644 --- a/libs/server-sdk/src/data/change_notifier/change_notifier_destination.hpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp @@ -1,8 +1,8 @@ #pragma once +#include "../../data_interfaces/destination/idestination.hpp" +#include "../../data_interfaces/source/ipull_source.hpp" #include "../dependency_tracker/dependency_tracker.hpp" -#include "../interfaces/data_dest/data_destination.hpp" -#include "../interfaces/data_source/data_pull_source.hpp" #include #include @@ -11,9 +11,10 @@ #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { -class DataStoreUpdater : public IDataDestination, public IChangeNotifier { +class DataStoreUpdater : public data_interfaces::IDestination, + public IChangeNotifier { public: template using Collection = data_model::SDKDataSet::Collection; @@ -25,7 +26,7 @@ class DataStoreUpdater : public IDataDestination, public IChangeNotifier { using SharedCollection = std::unordered_map>; - DataStoreUpdater(IDataDestination& sink, IDataPullSource const& source); + DataStoreUpdater(IDestination& sink, IPullSource const& source); std::unique_ptr OnFlagChange(ChangeHandler handler) override; @@ -102,7 +103,7 @@ class DataStoreUpdater : public IDataDestination, public IChangeNotifier { void NotifyChanges(DependencySet changes); - IDataDestination& sink_; + IDestination& sink_; data_sources::IDataSource const& source_; boost::signals2::signal)> signals_; @@ -119,4 +120,4 @@ class DataStoreUpdater : public IDataDestination, public IChangeNotifier { DependencyTracker dependency_tracker_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp b/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp new file mode 100644 index 000000000..4f231b6fa --- /dev/null +++ b/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::data_components { +enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.cpp b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp similarity index 98% rename from libs/server-sdk/src/data/dependency_tracker/dependency_tracker.cpp rename to libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp index 4e441927d..62647475b 100644 --- a/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.cpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp @@ -3,7 +3,7 @@ #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { DependencySet::DependencySet() : data_{ @@ -194,4 +194,4 @@ void DependencyTracker::Clear() { dependencies_from_.Clear(); } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.hpp b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp similarity index 97% rename from libs/server-sdk/src/data/dependency_tracker/dependency_tracker.hpp rename to libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp index 34c482caa..5bb1d0df9 100644 --- a/libs/server-sdk/src/data/dependency_tracker/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp @@ -12,7 +12,7 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { /** * Class used to maintain a set of dependencies. Each dependency may be either @@ -152,4 +152,4 @@ class DependencyTracker { std::vector const& clauses); }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/dependency_tracker/tagged_data.hpp b/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp similarity index 90% rename from libs/server-sdk/src/data/dependency_tracker/tagged_data.hpp rename to libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp index 1aa9f5d45..9b0639a97 100644 --- a/libs/server-sdk/src/data/dependency_tracker/tagged_data.hpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp @@ -10,7 +10,7 @@ #include "data_kind.hpp" -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { /** * 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 @@ -34,4 +34,4 @@ class TaggedData { Storage storage_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp similarity index 95% rename from libs/server-sdk/src/data/memory_store/memory_store.cpp rename to libs/server-sdk/src/data_components/memory_store/memory_store.cpp index b470bdc35..df7c08db2 100644 --- a/libs/server-sdk/src/data/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -1,6 +1,6 @@ #include "memory_store.hpp" -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { std::shared_ptr MemoryStore::GetFlag( std::string const& key) const { @@ -72,4 +72,4 @@ void MemoryStore::Upsert(std::string const& key, std::make_shared(std::move(segment)); } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp similarity index 86% rename from libs/server-sdk/src/data/memory_store/memory_store.hpp rename to libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 77542f4a5..3f844b515 100644 --- a/libs/server-sdk/src/data/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -1,15 +1,15 @@ #pragma once -#include "../interfaces/data_dest/data_destination.hpp" +#include "../../data_interfaces/destination/idestination.hpp" #include #include #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { -class MemoryStore : public IDataDestination { +class MemoryStore : public data_interfaces::IDestination { public: std::shared_ptr GetFlag( std::string const& key) const; @@ -51,4 +51,4 @@ class MemoryStore : public IDataDestination { mutable std::mutex data_mutex_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/serialization_adapters/README.md b/libs/server-sdk/src/data_components/serialization_adapters/README.md similarity index 78% rename from libs/server-sdk/src/data/serialization_adapters/README.md rename to libs/server-sdk/src/data_components/serialization_adapters/README.md index 29e7b32db..08bf3de35 100644 --- a/libs/server-sdk/src/data/serialization_adapters/README.md +++ b/libs/server-sdk/src/data_components/serialization_adapters/README.md @@ -7,10 +7,10 @@ This is a useful building block for implementing various Bootstrapper/Synchroniz For example, to build a new Destination that ferries data to Redis, you might pull the `JSONDestination` off the shelf. -It accepts memory models, serializes them, and forwards to any `ISerializedDataDestination`. +It accepts memory models, serializes them, and forwards to any `ISerializedDestination`. ``` -IDataDestination -> (serialization step) -> ISerializedDataDestination +IDestination -> (serialization step) -> ISerializedDestination ``` To handle pulling data out of Redis, use the `JSONSource`. It pulls from any `ISerializedDataSource`, deserializes it, @@ -21,8 +21,8 @@ IDataSource <- (deserialization step) <- ISerializedDataSource ``` On the other hand, to build a new Bootstrapper that pulls JSON from a web service, you might pull -the `JSONSource` off the shelf. It accepts JSON models, deserializes them, and forwards to any `IDataDestination`. +the `JSONSource` off the shelf. It accepts JSON models, deserializes them, and forwards to any `IDestination`. ``` -ISerializedDataDestination -> (deserialization step) -> IDataDestination +ISerializedDestination -> (deserialization step) -> IDestination ``` diff --git a/libs/server-sdk/src/data/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp similarity index 51% rename from libs/server-sdk/src/data/serialization_adapters/json_destination.cpp rename to libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index c48ee6a92..9bf28472a 100644 --- a/libs/server-sdk/src/data/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -1,24 +1,26 @@ #include "json_destination.hpp" -namespace launchdarkly::server_side::data::adapters { +namespace launchdarkly::server_side::data_components { -JsonDestination::JsonDestination(ISerializedDataDestination& destination) +JsonDestination::JsonDestination( + data_interfaces::ISerializedDestination& destination) : dest_(destination) {} void JsonDestination::Init(data_model::SDKDataSet data_set) { // TODO: serialize and forward to dest_.Init } -void JsonDestination::Upsert(std::string const& key, FlagDescriptor flag) { +void JsonDestination::Upsert(std::string const& key, + data_model::FlagDescriptor flag) { // TODO: serialize and forward to dest_.Upsert } void JsonDestination::Upsert(std::string const& key, - SegmentDescriptor segment) { + data_model::SegmentDescriptor segment) { // TODO: serialize and forward to dest_.Upsert } std::string JsonDestination::Identity() const { return dest_.Identity(); } -} // namespace launchdarkly::server_side::data::adapters +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp new file mode 100644 index 000000000..f9841b535 --- /dev/null +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -0,0 +1,20 @@ +#include "../../data_interfaces/destination/idestination.hpp" +#include "../../data_interfaces/destination/iserialized_destination.hpp" + +namespace launchdarkly::server_side::data_components { + +class JsonDestination : public data_interfaces::IDestination { + public: + JsonDestination(data_interfaces::ISerializedDestination& destination); + + void Init(data_model::SDKDataSet data_set) override; + void Upsert(std::string& key, data_model::FlagDescriptor flag) override; + void Upsert(std::string& key, + data_model::SegmentDescriptor segment) override; + std::string Identity() const override; + + private: + data_interfaces::ISerializedDestination& dest_; +}; + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp similarity index 50% rename from libs/server-sdk/src/data/serialization_adapters/json_pull_source.cpp rename to libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp index c08de5bf6..1719aef40 100644 --- a/libs/server-sdk/src/data/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp @@ -1,10 +1,11 @@ #include "json_pull_source.hpp" +#include -namespace launchdarkly::server_side::data::adapters { +namespace launchdarkly::server_side::data_components { template static std::optional> Deserialize( - SerializedItemDescriptor item) { + integrations::SerializedItemDescriptor item) { if (item.deleted) { return data_model::ItemDescriptor(item.version); } @@ -30,27 +31,32 @@ static std::optional> Deserialize( return std::nullopt; } -FlagDescriptor JsonSource::GetFlag(std::string& key) const { +data_model::FlagDescriptor JsonSource::GetFlag(std::string& key) const { // TODO: deserialize then return - ISerializedDataSource::GetResult result = source_.Get(kind, key); + data_interfaces::ISerializedDataPullSource::GetResult result = + source_.Get(kind, key); } -SegmentDescriptor JsonSource::GetSegment(std::string& key) const { +data_model::SegmentDescriptor JsonSource::GetSegment(std::string& key) const { // TODO: deserialize then return - ISerializedDataSource::GetResult result = source_.Get(kind, key); + data_interfaces::ISerializedDataPullSource::GetResult result = + source_.Get(kind, key); } -std::unordered_map JsonSource::AllFlags() const { +std::unordered_map +JsonSource::AllFlags() const { // TODO: deserialize then return - ISerializedDataSource::GetResult result = source_.All(kind); + data_interfaces::ISerializedDataPullSource::GetResult result = + source_.All(kind); } -std::unordered_map JsonSource::AllSegments() - const { +std::unordered_map +JsonSource::AllSegments() const { // TODO: deserialize then return - ISerializedDataSource::GetResult result = source_.All(kind); + data_interfaces::ISerializedDataPullSource::GetResult result = + source_.All(kind); } std::string JsonSource::Identity() const { return source_.Identity(); } -} // namespace launchdarkly::server_side::data::adapters +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp new file mode 100644 index 000000000..af382979e --- /dev/null +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp @@ -0,0 +1,24 @@ +#include "../../data_interfaces/destination/idestination.hpp" +#include "../../data_interfaces/destination/iserialized_destination.hpp" + +#include "../../data_interfaces/source/ipull_source.hpp" +#include "../../data_interfaces/source/iserialized_pull_source.hpp" + +namespace launchdarkly::server_side::data_components { + +class JsonSource : public data_interfaces::IPullSource { + public: + data_model::FlagDescriptor GetFlag(std::string& key) const override; + data_model::SegmentDescriptor GetSegment(std::string& key) const override; + std::unordered_map AllFlags() + const override; + std::unordered_map AllSegments() + const override; + std::string Identity() const override; + + public: + private: + data_interfaces::ISerializedDataPullSource& source_; +}; + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/sources/README.md b/libs/server-sdk/src/data_components/sources/README.md similarity index 100% rename from libs/server-sdk/src/data/sources/README.md rename to libs/server-sdk/src/data_components/sources/README.md diff --git a/libs/server-sdk/src/data/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data_components/sources/noop/null_data_source.cpp similarity index 91% rename from libs/server-sdk/src/data/sources/noop/null_data_source.cpp rename to libs/server-sdk/src/data_components/sources/noop/null_data_source.cpp index 6fbae45ed..7736d6f37 100644 --- a/libs/server-sdk/src/data/sources/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data_components/sources/noop/null_data_source.cpp @@ -13,7 +13,7 @@ void NullDataSource::ShutdownAsync(std::function complete) { } void NullDataSource::Init(std::optional initial_data, - IDataDestination& destination) {} + IDestination& destination) {} NullDataSource::NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager) diff --git a/libs/server-sdk/src/data/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp similarity index 93% rename from libs/server-sdk/src/data/sources/noop/null_data_source.hpp rename to libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp index 85814c4b4..88eee2ea0 100644 --- a/libs/server-sdk/src/data/sources/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp @@ -14,7 +14,7 @@ class NullDataSource : public ISynchronizer { DataSourceStatusManager& status_manager); void Init(std::optional initial_data, - IDataDestination& destination) override; + IDestination& destination) override; void Start() override; void ShutdownAsync(std::function) override; diff --git a/libs/server-sdk/src/data/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_components/sources/polling/polling_data_source.cpp similarity index 98% rename from libs/server-sdk/src/data/sources/polling/polling_data_source.cpp rename to libs/server-sdk/src/data_components/sources/polling/polling_data_source.cpp index 3413f059e..c83aa2570 100644 --- a/libs/server-sdk/src/data/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_components/sources/polling/polling_data_source.cpp @@ -1,4 +1,5 @@ -#include +#include "polling_data_source.hpp" +#include "data_source_update_sink.hpp" #include #include @@ -10,10 +11,9 @@ #include #include -#include "data_source_update_sink.hpp" -#include "polling_data_source.hpp" +#include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { static char const* const kErrorParsingPut = "Could not parse polling payload"; static char const* const kErrorPutInvalid = @@ -71,7 +71,7 @@ PollingDataSource::PollingDataSource( } void PollingDataSource::Init(std::optional initial_data, - IDataDestination& destination) { + IDestination& destination) { // TODO: implement } void PollingDataSource::DoPoll() { @@ -235,4 +235,4 @@ void PollingDataSource::ShutdownAsync(std::function completion) { } } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp similarity index 82% rename from libs/server-sdk/src/data/sources/polling/polling_data_source.hpp rename to libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp index 9d049f1cb..80a9d4162 100644 --- a/libs/server-sdk/src/data/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp @@ -1,11 +1,6 @@ #pragma once - -#include - -#include - -#include "../../interfaces/data_dest/data_destination.hpp" -#include "../../interfaces/data_source/data_push_source.hpp" +#include "../../../data_interfaces/destination/idestination.hpp" +#include "../../../data_interfaces/source/ipush_source.hpp" #include #include @@ -13,10 +8,14 @@ #include #include -namespace launchdarkly::server_side::data { +#include + +#include + +namespace launchdarkly::server_side::data_components { class PollingDataSource - : public IDataPushSource, + : public data_interfaces::IPushSource, public std::enable_shared_from_this { public: PollingDataSource( @@ -29,7 +28,7 @@ class PollingDataSource Logger const& logger); void Init(std::optional initial_data, - IDataDestination& destination) override; + IDestination& destination) override; void Start() override; void ShutdownAsync(std::function completion) override; @@ -54,4 +53,4 @@ class PollingDataSource void StartPollingTimer(); }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.cpp similarity index 97% rename from libs/server-sdk/src/data/sources/streaming/streaming_data_source.cpp rename to libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.cpp index 17e8d71a3..37e129c32 100644 --- a/libs/server-sdk/src/data/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.cpp @@ -1,3 +1,7 @@ +#include "streaming_data_source.hpp" + +#include + #include #include #include @@ -5,11 +9,7 @@ #include -#include "streaming_data_source.hpp" - -#include - -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { static char const* const kCouldNotParseEndpoint = "Could not parse streaming endpoint URL"; @@ -48,7 +48,7 @@ StreamingDataSource::StreamingDataSource( void StreamingDataSource::Init( std::optional initial_data, - IDataDestination& destination) { + IDestination& destination) { // TODO: implement } @@ -153,4 +153,4 @@ void StreamingDataSource::ShutdownAsync(std::function completion) { boost::asio::post(exec_, completion); } } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp similarity index 83% rename from libs/server-sdk/src/data/sources/streaming/streaming_data_source.hpp rename to libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp index cabdc28cc..71a80dc3f 100644 --- a/libs/server-sdk/src/data/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp @@ -1,12 +1,7 @@ #pragma once -#include -using namespace std::chrono_literals; - -#include - -#include "../../interfaces/data_dest/data_destination.hpp" -#include "../../interfaces/data_source/data_push_source.hpp" +#include "../../../data_interfaces/destination/idestination.hpp" +#include "../../../data_interfaces/source/ipush_source.hpp" #include #include @@ -17,10 +12,16 @@ using namespace std::chrono_literals; #include #include -namespace launchdarkly::server_side::data { +#include + +#include + +using namespace std::chrono_literals; + +namespace launchdarkly::server_side::data_components { class StreamingDataSource final - : public IDataPushSource, + : public data_interfaces::IPushSource, public std::enable_shared_from_this { public: StreamingDataSource( @@ -34,7 +35,7 @@ class StreamingDataSource final Logger const& logger); void Init(std::optional initial_data, - IDataDestination& destination) override; + IDestination& destination) override; void Start() override; void ShutdownAsync(std::function completion) override; @@ -52,4 +53,4 @@ class StreamingDataSource final Logger const& logger_; std::shared_ptr client_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/status_notifications/data_source_status.cpp b/libs/server-sdk/src/data_components/status_notifications/data_source_status.cpp similarity index 91% rename from libs/server-sdk/src/data/status_notifications/data_source_status.cpp rename to libs/server-sdk/src/data_components/status_notifications/data_source_status.cpp index ec4708399..aa347b0bb 100644 --- a/libs/server-sdk/src/data/status_notifications/data_source_status.cpp +++ b/libs/server-sdk/src/data_components/status_notifications/data_source_status.cpp @@ -1,8 +1,8 @@ -#include - #include -namespace launchdarkly::server_side::data { +#include + +namespace launchdarkly::server_side::data_components { std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state) { @@ -37,4 +37,4 @@ std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { return out; } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data/status_notifications/data_source_status_manager.hpp b/libs/server-sdk/src/data_components/status_notifications/data_source_status_manager.hpp similarity index 87% rename from libs/server-sdk/src/data/status_notifications/data_source_status_manager.hpp rename to libs/server-sdk/src/data_components/status_notifications/data_source_status_manager.hpp index 4e1674ce0..d8cbd9172 100644 --- a/libs/server-sdk/src/data/status_notifications/data_source_status_manager.hpp +++ b/libs/server-sdk/src/data_components/status_notifications/data_source_status_manager.hpp @@ -1,15 +1,14 @@ #pragma once - -#include -#include - -#include - #include #include #include -namespace launchdarkly::server_side::data { +#include + +#include +#include + +namespace launchdarkly::server_side::data_components { class DataSourceStatusManager : public internal::data_sources::DataSourceStatusManagerBase< @@ -25,4 +24,4 @@ class DataSourceStatusManager DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_interfaces/bootstrapper/ibootstrapper.hpp b/libs/server-sdk/src/data_interfaces/bootstrapper/ibootstrapper.hpp new file mode 100644 index 000000000..9192920b8 --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/bootstrapper/ibootstrapper.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include + +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_interfaces { + +/** + * Defines a component that can fetch a complete dataset for use in a + * Data System. Bootstrapping takes place when the SDK starts, and is + * responsible for provisioning the initial data that an SDK uses before it can + * begin the on-going synchronization process. + */ +class IBootstrapper { + public: + class Error { + public: + enum class Kind { + None, + Timeout, + Auth, + }; + + static Error Timeout(std::string detail) { + return Error(Kind::Timeout, std::move(detail)); + } + + static Error Auth(std::string detail) { + return Error(Kind::Auth, std::move(detail)); + } + + Error() : kind(Kind::None), detail(std::nullopt) {} + + private: + Error(Kind kind, std::optional detail) + : kind(kind), detail(std::move(detail)) {} + Kind kind; + std::optional detail; + }; + + /** + * Fetch a complete dataset. This method must invokable multiple times. + * @param timeout_hint amount of time to spend fetching data. If the time + * limit is reached, return Error::Timeout. + * @return A complete SDKDataSet on success, or an error indicating why it + * couldn't be retrieved. + */ + virtual tl::expected FetchAll( + std::chrono::milliseconds timeout_hint) = 0; + + /** + * @return A display-suitable name of the bootstrapper. + */ + virtual std::string const& Identity() const = 0; + + virtual ~IBootstrapper() = default; + IBootstrapper(IBootstrapper const& item) = delete; + IBootstrapper(IBootstrapper&& item) = delete; + IBootstrapper& operator=(IBootstrapper const&) = delete; + IBootstrapper& operator=(IBootstrapper&&) = delete; + + protected: + IBootstrapper() = default; +}; + +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data/interfaces/data_dest/data_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp similarity index 52% rename from libs/server-sdk/src/data/interfaces/data_dest/data_destination.hpp rename to libs/server-sdk/src/data_interfaces/destination/idestination.hpp index 2ac1e9e44..00d2e199d 100644 --- a/libs/server-sdk/src/data/interfaces/data_dest/data_destination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp @@ -3,9 +3,9 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_interfaces { -class IDataDestination { +class IDestination { public: virtual void Init(data_model::SDKDataSet data_set) = 0; virtual void Upsert(std::string const& key, @@ -14,13 +14,13 @@ class IDataDestination { data_model::SegmentDescriptor segment) = 0; virtual std::string const& Identity() const = 0; - IDataDestination(IDataDestination const& item) = delete; - IDataDestination(IDataDestination&& item) = delete; - IDataDestination& operator=(IDataDestination const&) = delete; - IDataDestination& operator=(IDataDestination&&) = delete; - virtual ~IDataDestination() = default; + IDestination(IDestination const& item) = delete; + IDestination(IDestination&& item) = delete; + IDestination& operator=(IDestination const&) = delete; + IDestination& operator=(IDestination&&) = delete; + virtual ~IDestination() = default; protected: - IDataDestination() = default; + IDestination() = default; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data/interfaces/data_dest/serialized_data_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp similarity index 53% rename from libs/server-sdk/src/data/interfaces/data_dest/serialized_data_destination.hpp rename to libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp index 49e0d875f..af1aa3727 100644 --- a/libs/server-sdk/src/data/interfaces/data_dest/serialized_data_destination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp @@ -1,13 +1,13 @@ #pragma once -#include "serialized_descriptors.hpp" +#include #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_interfaces { -class ISerializedDataDestination { +class ISerializedDestination { public: enum class InitResult { /** @@ -40,29 +40,29 @@ class ISerializedDataDestination { }; using ItemKey = std::string; - using KeyItemPair = std::pair; + using KeyItemPair = + std::pair; using OrderedNamepace = std::vector; using KindCollectionPair = - std::pair; + std::pair; using OrderedData = std::vector; virtual InitResult Init(OrderedData sdk_data_set) = 0; - virtual UpsertResult Upsert(std::string const& kind, - std::string const& key, - SerializedItemDescriptor item) = 0; + virtual UpsertResult Upsert( + std::string const& kind, + std::string const& key, + integrations::SerializedItemDescriptor item) = 0; virtual std::string Identity() const = 0; - ISerializedDataDestination(ISerializedDataDestination const& item) = delete; - ISerializedDataDestination(ISerializedDataDestination&& item) = delete; - ISerializedDataDestination& operator=(ISerializedDataDestination const&) = - delete; - ISerializedDataDestination& operator=(ISerializedDataDestination&&) = - delete; - virtual ~ISerializedDataDestination() = default; + ISerializedDestination(ISerializedDestination const& item) = delete; + ISerializedDestination(ISerializedDestination&& item) = delete; + ISerializedDestination& operator=(ISerializedDestination const&) = delete; + ISerializedDestination& operator=(ISerializedDestination&&) = delete; + virtual ~ISerializedDestination() = default; protected: - ISerializedDataDestination() = default; + ISerializedDestination() = default; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data/interfaces/data_source/data_pull_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp similarity index 92% rename from libs/server-sdk/src/data/interfaces/data_source/data_pull_source.hpp rename to libs/server-sdk/src/data_interfaces/source/ipull_source.hpp index 8d317930a..933f1622e 100644 --- a/libs/server-sdk/src/data/interfaces/data_source/data_pull_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp @@ -8,9 +8,9 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_interfaces { -class IDataPullSource { +class IPullSource { public: [[nodiscard]] virtual data_model::FlagDescriptor GetFlag( std::string const& key) const = 0; @@ -55,4 +55,4 @@ class IDataPullSource { IPullSource() = default; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data/interfaces/data_source/data_push_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp similarity index 75% rename from libs/server-sdk/src/data/interfaces/data_source/data_push_source.hpp rename to libs/server-sdk/src/data_interfaces/source/ipush_source.hpp index b9ba7fd0e..efec2efe9 100644 --- a/libs/server-sdk/src/data/interfaces/data_source/data_push_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp @@ -1,5 +1,7 @@ #pragma once +#include "../destination/idestination.hpp" + #include #include @@ -8,12 +10,12 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_interfaces { -class IDataPushSource { +class IPushSource { public: virtual void Init(std::optional initial_data, - IDataDestination& destination) = 0; + IDestination& destination) = 0; virtual void Start() = 0; virtual void ShutdownAsync(std::function) = 0; @@ -29,4 +31,4 @@ class IDataPushSource { IPushSource() = default; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data/interfaces/data_source/serialized_data_pull_source.hpp b/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp similarity index 80% rename from libs/server-sdk/src/data/interfaces/data_source/serialized_data_pull_source.hpp rename to libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp index e1a46a309..2cabcc9ff 100644 --- a/libs/server-sdk/src/data/interfaces/data_source/serialized_data_pull_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp @@ -1,13 +1,13 @@ #pragma once -#include "serialized_descriptors.hpp" +#include #include #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_interfaces { /** * Interface for a data source that provides feature flags and related data in a @@ -40,12 +40,13 @@ class ISerializedDataPullSource { }; using GetResult = - tl::expected, Error>; - - using AllResult = - tl::expected, + tl::expected, Error>; + using AllResult = tl::expected< + std::unordered_map, + Error>; + /** * Retrieves an item from the specified collection, if available. * @@ -55,7 +56,7 @@ class ISerializedDataPullSource { * 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, + virtual GetResult Get(integrations::IPersistentKind const& kind, std::string const& itemKey) const = 0; /** @@ -67,11 +68,11 @@ class ISerializedDataPullSource { * @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; + virtual AllResult All(integrations::IPersistentKind const& kind) const = 0; virtual std::string const& Identity() const = 0; protected: ISerializedDataPullSource() = default; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data/interfaces/data_store/data_store.hpp b/libs/server-sdk/src/data_interfaces/store/istore.hpp similarity index 89% rename from libs/server-sdk/src/data/interfaces/data_store/data_store.hpp rename to libs/server-sdk/src/data_interfaces/store/istore.hpp index 8e087461b..f4bd8bef0 100644 --- a/libs/server-sdk/src/data/interfaces/data_store/data_store.hpp +++ b/libs/server-sdk/src/data_interfaces/store/istore.hpp @@ -4,9 +4,9 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_interfaces { -class IDataStore { +class IStore { [[nodiscard]] virtual std::shared_ptr GetFlag( std::string const& key) const = 0; @@ -39,4 +39,4 @@ class IDataStore { std::shared_ptr> AllSegments() const = 0; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/system/isystem.hpp b/libs/server-sdk/src/data_interfaces/system/isystem.hpp new file mode 100644 index 000000000..e9417a90e --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/system/isystem.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "../store/istore.hpp" + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_interfaces { + +class ISystem : public IStore { + public: + [[nodiscard]] virtual std::string const& Identity() const = 0; + virtual void Initialize() = 0; + + virtual ~ISystem() = default; + ISystem(ISystem const& item) = delete; + ISystem(ISystem&& item) = delete; + ISystem& operator=(ISystem const&) = delete; + ISystem& operator=(ISystem&&) = delete; + + protected: + ISystem() = default; +}; + +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data/systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp similarity index 92% rename from libs/server-sdk/src/data/systems/background_sync/background_sync_system.cpp rename to libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 31411403f..1e728d655 100644 --- a/libs/server-sdk/src/data/systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -1,9 +1,6 @@ #include "background_sync_system.hpp" -#include "../polling_data_source.hpp" -#include "../streaming_data_source.hpp" - -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_systems { using namespace config::shared::built; @@ -63,4 +60,4 @@ BackgroundSync::AllSegments() const { return store_.AllSegments(); } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data/systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp similarity index 85% rename from libs/server-sdk/src/data/systems/background_sync/background_sync_system.hpp rename to libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 0b32c6b10..8d9d17ed5 100644 --- a/libs/server-sdk/src/data/systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -1,8 +1,8 @@ #pragma once -#include "../../interfaces/data_system.hpp" -#include "../../memory_store/memory_store.hpp" -#include "../../status_notifications/data_source_status_manager.hpp" +#include "../../data_components/memory_store/memory_store.hpp" +#include "../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../data_interfaces/system/isystem.hpp" #include #include @@ -12,7 +12,7 @@ #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_systems { /** * BackgroundSync implements the standard Data System which receives @@ -24,7 +24,7 @@ namespace launchdarkly::server_side::data { * too large to fit in memory, or a direct connection to LaunchDarkly isn't * desired, necessitating use of the alternate LazyLoad system. */ -class BackgroundSync : public IDataSystem { +class BackgroundSync : public data_interfaces::ISystem { public: BackgroundSync(config::shared::built::ServiceEndpoints const& endpoints, config::shared::built::DataSourceConfig< @@ -56,4 +56,4 @@ class BackgroundSync : public IDataSystem { private: MemoryStore store_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data/systems/background_sync/event_handler.cpp b/libs/server-sdk/src/data_systems/background_sync/event_handler.cpp similarity index 99% rename from libs/server-sdk/src/data/systems/background_sync/event_handler.cpp rename to libs/server-sdk/src/data_systems/background_sync/event_handler.cpp index d64fdcf75..dcc0695f2 100644 --- a/libs/server-sdk/src/data/systems/background_sync/event_handler.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/event_handler.cpp @@ -126,7 +126,7 @@ static tl::expected tag_invoke( } DataSourceEventHandler::DataSourceEventHandler( - IDataDestination& handler, + IDestination& handler, Logger const& logger, DataSourceStatusManager& status_manager) : handler_(handler), logger_(logger), status_manager_(status_manager) {} diff --git a/libs/server-sdk/src/data/systems/background_sync/event_handler.hpp b/libs/server-sdk/src/data_systems/background_sync/event_handler.hpp similarity index 97% rename from libs/server-sdk/src/data/systems/background_sync/event_handler.hpp rename to libs/server-sdk/src/data_systems/background_sync/event_handler.hpp index a256ffec2..57494edd7 100644 --- a/libs/server-sdk/src/data/systems/background_sync/event_handler.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/event_handler.hpp @@ -102,7 +102,7 @@ class DataSourceEventHandler { uint64_t version; }; - DataSourceEventHandler(IDataDestination& handler, + DataSourceEventHandler(IDestination& handler, Logger const& logger, DataSourceStatusManager& status_manager); @@ -116,7 +116,7 @@ class DataSourceEventHandler { std::string const& data); private: - IDataDestination& handler_; + IDestination& handler_; Logger const& logger_; DataSourceStatusManager& status_manager_; }; diff --git a/libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.cpp b/libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.cpp similarity index 100% rename from libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.cpp rename to libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.cpp diff --git a/libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.hpp b/libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.hpp similarity index 100% rename from libs/server-sdk/src/data/systems/lazy_load/expiration/expiration_tracker.hpp rename to libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.hpp diff --git a/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp similarity index 97% rename from libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.cpp rename to libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 302f87295..4898d09c7 100644 --- a/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -5,7 +5,7 @@ #include "data_version_inspectors.hpp" -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_systems { using namespace config::shared::built; @@ -124,4 +124,4 @@ void LazyLoad::RefreshFlag(std::string const& key) const { // TODO: If there is an actual error, then do we not reset the tracking? } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp similarity index 92% rename from libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.hpp rename to libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index d1a298f99..62e120834 100644 --- a/libs/server-sdk/src/data/systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -1,18 +1,18 @@ #pragma once +#include "../../data_components/memory_store/memory_store.hpp" +#include "../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../data_interfaces/system/isystem.hpp" + #include #include #include #include #include -#include "../../interfaces/data_system.hpp" -#include "../../memory_store/memory_store.hpp" -#include "../../status_notifications/data_source_status_manager.hpp" - #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_systems { /** * LazyLoad implements a Data System that pulls data from a persistent store @@ -23,7 +23,7 @@ namespace launchdarkly::server_side::data { * LazyLoad is able to remain efficient because it caches responses from the * store. Over time, data becomes stale causing the system to refresh data. */ -class LazyLoad : public IDataSystem { +class LazyLoad : public data_interfaces::ISystem { public: LazyLoad(config::shared::built::ServiceEndpoints const& endpoints, config::shared::built::DataSourceConfig< @@ -126,4 +126,4 @@ class LazyLoad : public IDataSystem { MemoryStore store_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_systems From caf7665c09a8aece2e769739068312492d5faa35 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 25 Oct 2023 15:30:39 -0700 Subject: [PATCH 070/244] approaching compilation --- .../shared/builders/persistence_builder.hpp | 115 +++++++++--------- .../config/shared/built/persistence.hpp | 3 + .../launchdarkly/config/shared/defaults.hpp | 3 +- .../launchdarkly/server_side/client.hpp | 4 +- .../server_side/data_source_status.hpp | 8 +- libs/server-sdk/src/CMakeLists.txt | 1 - libs/server-sdk/src/bindings/c/sdk.cpp | 2 +- .../dependency_tracker/tagged_data.hpp | 9 +- .../json_destination.cpp | 2 +- .../json_destination.hpp | 12 +- .../json_pull_source.cpp | 10 +- .../json_pull_source.hpp | 22 ++-- .../sources/noop/null_data_source.hpp | 17 +-- .../sources/polling/polling_data_source.hpp | 8 +- .../streaming/streaming_data_source.hpp | 10 +- .../destination/idestination.hpp | 5 +- .../destination/iserialized_destination.hpp | 2 +- .../data_interfaces/source/ipush_source.hpp | 2 +- .../src/data_interfaces/store/istore.hpp | 10 ++ .../data_source_status.cpp | 4 +- libs/server-sdk/src/evaluation/evaluator.cpp | 2 +- libs/server-sdk/src/evaluation/evaluator.hpp | 9 +- 22 files changed, 152 insertions(+), 108 deletions(-) rename libs/server-sdk/src/{data_components/status_notifications => }/data_source_status.cpp (91%) 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 961ead466..8ee88f97a 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp @@ -87,61 +87,66 @@ class PersistenceBuilder { PersistenceBuilder() : persistence_(Defaults::PersistenceConfig()) {} - /** - * 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 The time, in seconds, cached data remains - * fresh. - * @return A reference to this builder. - */ - PersistenceBuilder& CacheRefreshTime( - std::chrono::seconds cache_refresh_time) { - persistence_.cache_refresh_time = cache_refresh_time; - return *this; - } - - /** - * 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; - return *this; - } - - /** - * 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) { - persistence_.eviction_interval = eviction_interval; - return *this; - } + // /** + // * 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 The time, in seconds, cached data remains + // * fresh. + // * @return A reference to this builder. + // */ + // PersistenceBuilder& CacheRefreshTime( + // std::chrono::seconds cache_refresh_time) { + // persistence_.cache_refresh_time = cache_refresh_time; + // return *this; + // } + // + // /** + // * 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; + // return *this; + // } + // + // /** + // * 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) { + // persistence_.eviction_interval = eviction_interval; + // return *this; + // } [[nodiscard]] built::Persistence Build() const { return persistence_; diff --git a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp index f23a60450..cee3d3f09 100644 --- a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp @@ -19,4 +19,7 @@ struct Persistence { std::size_t max_contexts_; }; +template <> +struct Persistence {}; + } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index b7d958236..b0e4ab2b2 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -115,8 +115,7 @@ struct Defaults { } static auto PersistenceConfig() -> shared::built::Persistence { - return {nullptr, std::chrono::seconds{30}, false, - std::chrono::seconds{10}}; + return {}; } }; diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index e7f1b8056..c301594e8 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -252,7 +252,7 @@ class IClient { * source status. * @return A data source status provider. */ - virtual data_sources::IDataSourceStatusProvider& DataSourceStatus() = 0; + virtual IDataSourceStatusProvider& DataSourceStatus() = 0; virtual ~IClient() = default; IClient(IClient const& item) = delete; @@ -338,7 +338,7 @@ class Client : public IClient { FlagKey const& key, Value default_value) override; - data_sources::IDataSourceStatusProvider& DataSourceStatus() override; + IDataSourceStatusProvider& DataSourceStatus() override; /** * Returns the version of the SDK. 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 d06d4c008..81484600f 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 @@ -13,7 +13,7 @@ #include #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side { /** * Enumeration of possible data source states. @@ -85,7 +85,7 @@ class IDataSourceStatusProvider { * @return A IConnection which can be used to stop listening to the status. */ virtual std::unique_ptr OnDataSourceStatusChange( - std::function handler) = 0; + std::function handler) = 0; /** * Listen to changes to the data source status, with ability for listener @@ -96,7 +96,7 @@ class IDataSourceStatusProvider { * @return A IConnection which can be used to stop listening to the status. */ virtual std::unique_ptr OnDataSourceStatusChangeEx( - std::function handler) = 0; + std::function handler) = 0; virtual ~IDataSourceStatusProvider() = default; IDataSourceStatusProvider(IDataSourceStatusProvider const& item) = delete; @@ -114,4 +114,4 @@ std::ostream& operator<<(std::ostream& out, std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index a89f32d8a..86ec11b05 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -30,7 +30,6 @@ target_sources(${LIBNAME} data_components/sources/noop/null_data_source.cpp data_components/sources/streaming/streaming_data_source.cpp data_components/sources/polling/polling_data_source.cpp - data_components/status_notifications/data_source_status.cpp data_systems/background_sync/background_sync_system.cpp data_systems/background_sync/event_handler.cpp data_systems/lazy_load/lazy_load_system.cpp diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp index 87ca8eabb..01536ff94 100644 --- a/libs/server-sdk/src/bindings/c/sdk.cpp +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -27,7 +27,7 @@ struct Detail; #define FROM_DETAIL(ptr) (reinterpret_cast(ptr)) #define TO_DATASOURCESTATUS(ptr) \ - (reinterpret_cast(ptr)) + (reinterpret_cast(ptr)) #define FROM_DATASOURCESTATUS(ptr) \ (reinterpret_cast(ptr)) diff --git a/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp b/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp index 9b0639a97..b38c7f251 100644 --- a/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp @@ -20,17 +20,14 @@ namespace launchdarkly::server_side::data_components { 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_; - } + 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: - launchdarkly::server_side::data_store::DataKind kind_; + DataKind kind_; Storage storage_; }; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 9bf28472a..9cb5f7dd1 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -20,7 +20,7 @@ void JsonDestination::Upsert(std::string const& key, // TODO: serialize and forward to dest_.Upsert } -std::string JsonDestination::Identity() const { +std::string const& JsonDestination::Identity() const { return dest_.Identity(); } } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index f9841b535..88f6c8cb9 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -7,11 +7,15 @@ class JsonDestination : public data_interfaces::IDestination { public: JsonDestination(data_interfaces::ISerializedDestination& destination); - void Init(data_model::SDKDataSet data_set) override; - void Upsert(std::string& key, data_model::FlagDescriptor flag) override; - void Upsert(std::string& key, + virtual void Init(data_model::SDKDataSet data_set) override; + + void Upsert(std::string const& key, + data_model::FlagDescriptor flag) override; + + void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; - std::string Identity() const override; + + [[nodiscard]] std::string const& Identity() const override; private: data_interfaces::ISerializedDestination& dest_; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp index 1719aef40..b74e84318 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp @@ -3,6 +3,9 @@ namespace launchdarkly::server_side::data_components { +JsonSource::JsonSource(data_interfaces::ISerializedDataPullSource& json_source) + : source_(json_source) {} + template static std::optional> Deserialize( integrations::SerializedItemDescriptor item) { @@ -31,12 +34,13 @@ static std::optional> Deserialize( return std::nullopt; } -data_model::FlagDescriptor JsonSource::GetFlag(std::string& key) const { +data_model::FlagDescriptor JsonSource::GetFlag(std::string const& key) const { // TODO: deserialize then return data_interfaces::ISerializedDataPullSource::GetResult result = source_.Get(kind, key); } -data_model::SegmentDescriptor JsonSource::GetSegment(std::string& key) const { +data_model::SegmentDescriptor JsonSource::GetSegment( + std::string const& key) const { // TODO: deserialize then return data_interfaces::ISerializedDataPullSource::GetResult result = source_.Get(kind, key); @@ -55,7 +59,7 @@ JsonSource::AllSegments() const { source_.All(kind); } -std::string JsonSource::Identity() const { +std::string const& JsonSource::Identity() const { return source_.Identity(); } diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp index af382979e..5098940f2 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp @@ -8,13 +8,21 @@ namespace launchdarkly::server_side::data_components { class JsonSource : public data_interfaces::IPullSource { public: - data_model::FlagDescriptor GetFlag(std::string& key) const override; - data_model::SegmentDescriptor GetSegment(std::string& key) const override; - std::unordered_map AllFlags() - const override; - std::unordered_map AllSegments() - const override; - std::string Identity() const override; + JsonSource(data_interfaces::ISerializedDataPullSource& json_source); + + [[nodiscard]] virtual data_model::FlagDescriptor GetFlag( + std::string const& key) const override; + + [[nodiscard]] virtual data_model::SegmentDescriptor GetSegment( + std::string const& key) const override; + + [[nodiscard]] virtual std::unordered_map + AllFlags() const override; + [[nodiscard]] virtual std::unordered_map + AllSegments() const override; + [[nodiscard]] virtual std::string const& Identity() const override; public: private: diff --git a/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp index 88eee2ea0..3e20cd64e 100644 --- a/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp @@ -1,26 +1,29 @@ #pragma once -#include "../../data_source_status_manager.hpp" -#include "../../interfaces/data_destination.hpp" -#include "../../interfaces/data_push_source.hpp" +#include "../../../data_interfaces/destination/idestination.hpp" +#include "../../../data_interfaces/source/ipush_source.hpp" + +#include "../../status_notifications/data_source_status_manager.hpp" #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_components { -class NullDataSource : public ISynchronizer { +class NullDataSource : public data_interfaces::IPushSource { public: explicit NullDataSource(boost::asio::any_io_executor exec, DataSourceStatusManager& status_manager); void Init(std::optional initial_data, - IDestination& destination) override; + data_interfaces::IDestination& destination) override; void Start() override; void ShutdownAsync(std::function) override; + [[nodiscard]] std::string const& Identity() const override; + private: DataSourceStatusManager& status_manager_; boost::asio::any_io_executor exec_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp index 80a9d4162..806394f5f 100644 --- a/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp @@ -2,6 +2,8 @@ #include "../../../data_interfaces/destination/idestination.hpp" #include "../../../data_interfaces/source/ipush_source.hpp" +#include "../../status_notifications/data_source_status_manager.hpp" + #include #include #include @@ -28,10 +30,12 @@ class PollingDataSource Logger const& logger); void Init(std::optional initial_data, - IDestination& destination) override; + data_interfaces::IDestination& destination) override; void Start() override; void ShutdownAsync(std::function completion) override; + [[nodiscard]] std::string const& Identity() const override; + private: void DoPoll(); void HandlePollResult(network::HttpResult const& res); @@ -48,7 +52,7 @@ class PollingDataSource boost::asio::steady_timer timer_; std::chrono::time_point last_poll_start_; - IDataSourceUpdateSink& update_sink_; + data_interfaces::IDestination& update_sink_; void StartPollingTimer(); }; diff --git a/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp index 71a80dc3f..fe55b89bc 100644 --- a/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp @@ -3,6 +3,8 @@ #include "../../../data_interfaces/destination/idestination.hpp" #include "../../../data_interfaces/source/ipush_source.hpp" +#include "../../status_notifications/data_source_status_manager.hpp" + #include #include #include @@ -30,19 +32,21 @@ class StreamingDataSource final data_source_config, config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, - IDataSourceUpdateSink& handler, + data_interfaces::IDestination& handler, DataSourceStatusManager& status_manager, Logger const& logger); void Init(std::optional initial_data, - IDestination& destination) override; + data_interfaces::IDestination& destination) override; void Start() override; void ShutdownAsync(std::function completion) override; + [[nodiscard]] std::string const& Identity() const override; + private: boost::asio::any_io_executor exec_; DataSourceStatusManager& status_manager_; - DataSourceEventHandler data_source_handler_; + data_interfaces::IDestination& data_source_handler_; std::string streaming_endpoint_; config::shared::built::StreamingConfig diff --git a/libs/server-sdk/src/data_interfaces/destination/idestination.hpp b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp index 00d2e199d..842a54ed5 100644 --- a/libs/server-sdk/src/data_interfaces/destination/idestination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp @@ -8,11 +8,14 @@ namespace launchdarkly::server_side::data_interfaces { class IDestination { public: virtual void Init(data_model::SDKDataSet data_set) = 0; + virtual void Upsert(std::string const& key, data_model::FlagDescriptor flag) = 0; + virtual void Upsert(std::string const& key, data_model::SegmentDescriptor segment) = 0; - virtual std::string const& Identity() const = 0; + + [[nodiscard]] virtual std::string const& Identity() const = 0; IDestination(IDestination const& item) = delete; IDestination(IDestination&& item) = delete; diff --git a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp index af1aa3727..eabe83a57 100644 --- a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp @@ -54,7 +54,7 @@ class ISerializedDestination { std::string const& key, integrations::SerializedItemDescriptor item) = 0; - virtual std::string Identity() const = 0; + virtual std::string const& Identity() const = 0; ISerializedDestination(ISerializedDestination const& item) = delete; ISerializedDestination(ISerializedDestination&& item) = delete; diff --git a/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp index efec2efe9..83cf0676a 100644 --- a/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp @@ -19,7 +19,7 @@ class IPushSource { virtual void Start() = 0; virtual void ShutdownAsync(std::function) = 0; - virtual std::string const& Identity() const = 0; + [[nodiscard]] virtual std::string const& Identity() const = 0; virtual ~IPushSource() = default; IPushSource(IPushSource const& item) = delete; diff --git a/libs/server-sdk/src/data_interfaces/store/istore.hpp b/libs/server-sdk/src/data_interfaces/store/istore.hpp index f4bd8bef0..a50e64a8e 100644 --- a/libs/server-sdk/src/data_interfaces/store/istore.hpp +++ b/libs/server-sdk/src/data_interfaces/store/istore.hpp @@ -7,6 +7,7 @@ namespace launchdarkly::server_side::data_interfaces { class IStore { + public: [[nodiscard]] virtual std::shared_ptr GetFlag( std::string const& key) const = 0; @@ -38,5 +39,14 @@ class IStore { std::string, std::shared_ptr> AllSegments() const = 0; + + virtual ~IStore() = default; + IStore(IStore const& item) = delete; + IStore(IStore&& item) = delete; + IStore& operator=(IStore const&) = delete; + IStore& operator=(IStore&&) = delete; + + protected: + IStore() = default; }; } // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_components/status_notifications/data_source_status.cpp b/libs/server-sdk/src/data_source_status.cpp similarity index 91% rename from libs/server-sdk/src/data_components/status_notifications/data_source_status.cpp rename to libs/server-sdk/src/data_source_status.cpp index aa347b0bb..3cc6ca609 100644 --- a/libs/server-sdk/src/data_components/status_notifications/data_source_status.cpp +++ b/libs/server-sdk/src/data_source_status.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_components { +namespace launchdarkly::server_side { std::ostream& operator<<(std::ostream& out, DataSourceStatus::DataSourceState const& state) { @@ -37,4 +37,4 @@ std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status) { return out; } -} // namespace launchdarkly::server_side::data_components +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index 336ae72ff..4ac3ff2d2 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -19,7 +19,7 @@ std::optional TargetMatchVariation( launchdarkly::Context const& context, Flag::Target const& target); -Evaluator::Evaluator(Logger& logger, data_sources::IDataSource const& source) +Evaluator::Evaluator(Logger& logger, data_interfaces::IStore const& source) : logger_(logger), source_(source), stack_() {} EvaluationDetail Evaluator::Evaluate( diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp index 018f0a080..6601b37c3 100644 --- a/libs/server-sdk/src/evaluation/evaluator.hpp +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -6,19 +6,20 @@ #include #include -#include "../data_sources/data_source_interface.hpp" -#include "../events/event_scope.hpp" #include "bucketing.hpp" #include "detail/evaluation_stack.hpp" #include "evaluation_error.hpp" +#include "../data_interfaces/store/istore.hpp" +#include "../events/event_scope.hpp" + #include namespace launchdarkly::server_side::evaluation { class Evaluator { public: - Evaluator(Logger& logger, data_sources::IDataSource const& source); + Evaluator(Logger& logger, data_interfaces::IStore const& source); /** * Evaluates a flag for a given context. @@ -64,7 +65,7 @@ class Evaluator { void LogError(std::string const& key, Error const& error) const; Logger& logger_; - data_sources::IDataSource const& source_; + data_interfaces::IStore const& source_; detail::EvaluationStack stack_; }; } // namespace launchdarkly::server_side::evaluation From d1e1b8265f2545d4d81c9f1e3f07c759184108f8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 25 Oct 2023 16:50:31 -0700 Subject: [PATCH 071/244] only 8 files not compiling :) --- libs/server-sdk/src/CMakeLists.txt | 10 ++-- libs/server-sdk/src/bindings/c/sdk.cpp | 12 ++-- libs/server-sdk/src/client.cpp | 2 +- libs/server-sdk/src/client_impl.cpp | 31 ++++++----- libs/server-sdk/src/client_impl.hpp | 7 +-- .../change_notifier_destination.cpp | 2 +- .../change_notifier_destination.hpp | 6 +- .../expiration_tracker.cpp | 11 ++-- .../expiration_tracker.hpp | 20 +++---- .../src/data_components/kinds/kinds.hpp | 26 +++++++++ .../memory_store/memory_store.cpp | 2 +- .../memory_store/memory_store.hpp | 32 +++++++---- .../json_pull_source.cpp | 15 ++--- .../json_pull_source.hpp | 5 +- .../sources/noop/null_data_source.hpp | 29 ---------- .../src/data_interfaces/store/istore.hpp | 2 + .../src/data_interfaces/system/isystem.hpp | 7 ++- .../background_sync_system.cpp | 2 +- .../background_sync_system.hpp | 4 +- .../background_sync}/sources/README.md | 0 .../sources/noop/null_data_source.cpp | 11 ++-- .../sources/noop/null_data_source.hpp | 29 ++++++++++ .../sources/polling/polling_data_source.cpp | 11 ++-- .../sources/polling/polling_data_source.hpp | 14 ++--- .../{ => sources/streaming}/event_handler.cpp | 12 ++-- .../{ => sources/streaming}/event_handler.hpp | 52 ++++++++++-------- .../streaming/streaming_data_source.cpp | 13 ++--- .../streaming/streaming_data_source.hpp | 17 +++--- .../lazy_load/lazy_load_system.cpp | 21 +++---- .../lazy_load/lazy_load_system.hpp | 55 ++++++------------- libs/server-sdk/src/evaluation/rules.cpp | 10 ++-- libs/server-sdk/src/evaluation/rules.hpp | 18 +++--- 32 files changed, 257 insertions(+), 231 deletions(-) rename libs/server-sdk/src/{data_systems/lazy_load/expiration => data_components/expiration_tracker}/expiration_tracker.cpp (93%) rename libs/server-sdk/src/{data_systems/lazy_load/expiration => data_components/expiration_tracker}/expiration_tracker.hpp (90%) create mode 100644 libs/server-sdk/src/data_components/kinds/kinds.hpp delete mode 100644 libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp rename libs/server-sdk/src/{data_components => data_systems/background_sync}/sources/README.md (100%) rename libs/server-sdk/src/{data_components => data_systems/background_sync}/sources/noop/null_data_source.cpp (57%) create mode 100644 libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp rename libs/server-sdk/src/{data_components => data_systems/background_sync}/sources/polling/polling_data_source.cpp (96%) rename libs/server-sdk/src/{data_components => data_systems/background_sync}/sources/polling/polling_data_source.hpp (78%) rename libs/server-sdk/src/data_systems/background_sync/{ => sources/streaming}/event_handler.cpp (96%) rename libs/server-sdk/src/data_systems/background_sync/{ => sources/streaming}/event_handler.hpp (65%) rename libs/server-sdk/src/{data_components => data_systems/background_sync}/sources/streaming/streaming_data_source.cpp (94%) rename libs/server-sdk/src/{data_components => data_systems/background_sync}/sources/streaming/streaming_data_source.hpp (75%) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 86ec11b05..5bcaf40c1 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -27,13 +27,13 @@ target_sources(${LIBNAME} data_components/memory_store/memory_store.cpp data_components/serialization_adapters/json_destination.cpp data_components/serialization_adapters/json_pull_source.cpp - data_components/sources/noop/null_data_source.cpp - data_components/sources/streaming/streaming_data_source.cpp - data_components/sources/polling/polling_data_source.cpp + data_components/expiration_tracker/expiration_tracker.cpp + data_systems/background_sync/sources/noop/null_data_source.cpp + data_systems/background_sync/sources/streaming/streaming_data_source.cpp + data_systems/background_sync/sources/polling/polling_data_source.cpp data_systems/background_sync/background_sync_system.cpp - data_systems/background_sync/event_handler.cpp + data_systems/background_sync/sources/streaming/event_handler.cpp data_systems/lazy_load/lazy_load_system.cpp - data_systems/lazy_load/expiration/expiration_tracker.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp index 01536ff94..8a8857673 100644 --- a/libs/server-sdk/src/bindings/c/sdk.cpp +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -355,10 +355,8 @@ LDServerDataSourceStatus_GetLastError(LDServerDataSourceStatus status) { if (!error) { return nullptr; } - return FROM_DATASOURCESTATUS_ERRORINFO( - new data_sources::DataSourceStatus::ErrorInfo( - error->Kind(), error->StatusCode(), error->Message(), - error->Time())); + return FROM_DATASOURCESTATUS_ERRORINFO(new DataSourceStatus::ErrorInfo( + error->Kind(), error->StatusCode(), error->Message(), error->Time())); } LD_EXPORT(time_t) @@ -386,7 +384,7 @@ LDServerSDK_DataSourceStatus_OnStatusChange( if (listener.StatusChanged) { auto connection = TO_SDK(sdk)->DataSourceStatus().OnDataSourceStatusChange( - [listener](data_sources::DataSourceStatus status) { + [listener](DataSourceStatus status) { listener.StatusChanged(FROM_DATASOURCESTATUS(&status), listener.UserData); }); @@ -401,8 +399,8 @@ 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())); + return FROM_DATASOURCESTATUS( + new DataSourceStatus(TO_SDK(sdk)->DataSourceStatus().Status())); } LD_EXPORT(void) LDServerDataSourceStatus_Free(LDServerDataSourceStatus status) { diff --git a/libs/server-sdk/src/client.cpp b/libs/server-sdk/src/client.cpp index ce992de85..11ead4f16 100644 --- a/libs/server-sdk/src/client.cpp +++ b/libs/server-sdk/src/client.cpp @@ -124,7 +124,7 @@ EvaluationDetail Client::JsonVariationDetail(Context const& ctx, return client->JsonVariationDetail(ctx, key, std::move(default_value)); } -data_sources::IDataSourceStatusProvider& Client::DataSourceStatus() { +IDataSourceStatusProvider& Client::DataSourceStatus() { return client->DataSourceStatus(); } diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 047d5cdf4..2e72fcb61 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -1,8 +1,9 @@ #include "client_impl.hpp" #include "all_flags_state/all_flags_state_builder.hpp" -#include "data_retrieval/systems/background_sync/background_sync_system.hpp" -#include "data_sources/push_mode/push_mode_data_source.hpp" +#include "data_systems/background_sync/background_sync_system.hpp" + +#include "data_interfaces/system/isystem.hpp" #include #include @@ -60,11 +61,11 @@ using launchdarkly::config::shared::built::HttpProperties; // logger); // } -static std::unique_ptr MakeDataSystem( +static std::unique_ptr MakeDataSystem( HttpProperties const& http_properties, Config const& config, boost::asio::any_io_executor const& executor, - data_retrieval::DataSourceStatusManager& status_manager, + data_components::DataSourceStatusManager& status_manager, Logger& logger) { auto builder = HttpPropertiesBuilder(http_properties); @@ -73,7 +74,7 @@ static std::unique_ptr MakeDataSystem( // TODO: Check if config is a persistent Store (so, if 'method' is // Persistent). If so, return a data_sources::PullModeSource instead. - return std::make_unique( + return std::make_unique( config.ServiceEndpoints(), config.DataSourceConfig(), data_source_properties, executor, status_manager, logger); } @@ -105,13 +106,13 @@ std::unique_ptr> MakeEventProcessor( } return nullptr; } -R - /** - * Returns true if the flag pointer is valid and the underlying item is - * present. - */ - bool - IsFlagPresent(std::shared_ptr const& flag_desc); + +/** + * 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), @@ -214,7 +215,7 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, EventScope no_events; - for (auto const& [k, v] : data_source_->AllFlags()) { + for (auto const& [k, v] : data_system_->Store().AllFlags()) { if (!v || !v->item) { continue; } @@ -315,7 +316,7 @@ EvaluationDetail ClientImpl::VariationInternal( std::nullopt); } - auto flag_rule = data_source_->GetFlag(key); + auto flag_rule = data_system_->Store().GetFlag(key); bool flag_present = IsFlagPresent(flag_rule); @@ -456,7 +457,7 @@ Value ClientImpl::JsonVariation(Context const& ctx, return *VariationInternal(ctx, key, default_value, events_default_); } -data_sources::IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { +IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { return status_manager_; } diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 23f2f9312..865c658f1 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -101,7 +101,7 @@ class ClientImpl : public IClient { FlagKey const& key, Value default_value) override; - data_interfaces::IDataSourceStatusProvider& DataSourceStatus() override; + IDataSourceStatusProvider& DataSourceStatus() override; ~ClientImpl(); @@ -153,8 +153,7 @@ class ClientImpl : public IClient { std::optional metric_value); std::future StartAsyncInternal( - std::function - predicate); + std::function predicate); void LogVariationCall(std::string const& key, bool flag_present) const; @@ -167,7 +166,7 @@ class ClientImpl : public IClient { boost::asio::executor_work_guard work_; - data_retrieval::DataSourceStatusManager status_manager_; + data_components::DataSourceStatusManager status_manager_; std::unique_ptr data_system_; diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp index d1b1e3684..738a8d451 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp @@ -70,7 +70,7 @@ void DataStoreUpdater::NotifyChanges(DependencySet changes) { } DataStoreUpdater::DataStoreUpdater(IDestination& sink, - data_sources::IDataSource const& source) + data_interfaces::IStore const& source) : sink_(sink), source_(source) {} } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp index b2e4921f4..e58c71bb6 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp @@ -1,7 +1,7 @@ #pragma once #include "../../data_interfaces/destination/idestination.hpp" -#include "../../data_interfaces/source/ipull_source.hpp" +#include "../../data_interfaces/store/istore.hpp" #include "../dependency_tracker/dependency_tracker.hpp" #include @@ -26,7 +26,7 @@ class DataStoreUpdater : public data_interfaces::IDestination, using SharedCollection = std::unordered_map>; - DataStoreUpdater(IDestination& sink, IPullSource const& source); + DataStoreUpdater(IDestination& sink, data_interfaces::IStore const& source); std::unique_ptr OnFlagChange(ChangeHandler handler) override; @@ -104,7 +104,7 @@ class DataStoreUpdater : public data_interfaces::IDestination, void NotifyChanges(DependencySet changes); IDestination& sink_; - data_sources::IDataSource const& source_; + data_interfaces::IStore const& source_; boost::signals2::signal)> signals_; diff --git a/libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.cpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp similarity index 93% rename from libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.cpp rename to libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp index 09e6142ef..fcaf0c541 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.cpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp @@ -1,6 +1,6 @@ #include "expiration_tracker.hpp" -namespace launchdarkly::server_side::data_store::persistent { +namespace launchdarkly::server_side::data_components { void ExpirationTracker::Add(std::string const& key, ExpirationTracker::TimePoint expiration) { @@ -22,19 +22,18 @@ ExpirationTracker::TrackState ExpirationTracker::State( return ExpirationTracker::TrackState::kNotTracked; } -void ExpirationTracker::Add(data_store::DataKind kind, +void ExpirationTracker::Add(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) { +void ExpirationTracker::Remove(DataKind kind, std::string const& key) { scoped_.Remove(kind, key); } ExpirationTracker::TrackState ExpirationTracker::State( - data_store::DataKind kind, + DataKind kind, std::string const& key, ExpirationTracker::TimePoint current_time) const { auto expiration = scoped_.Get(kind, key); @@ -149,4 +148,4 @@ std::ostream& operator<<(std::ostream& out, } return out; } -} // namespace launchdarkly::server_side::data_store::persistent +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.hpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp similarity index 90% rename from libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.hpp rename to libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp index bc57398c0..786b9d453 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/expiration/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp @@ -1,5 +1,10 @@ #pragma once +#include "../dependency_tracker/data_kind.hpp" +#include "../dependency_tracker/tagged_data.hpp" + +#include + #include #include #include @@ -7,12 +12,7 @@ #include #include -#include - -#include "../../data_store/data_kind.hpp" -#include "../tagged_data.hpp" - -namespace launchdarkly::server_side::data_store::persistent { +namespace launchdarkly::server_side::data_components { class ExpirationTracker { public: @@ -68,7 +68,7 @@ class ExpirationTracker { * @param key The key to track. * @param expiration The time that the key expires. */ - void Add(data_store::DataKind kind, + void Add(data_components::DataKind kind, std::string const& key, TimePoint expiration); @@ -78,7 +78,7 @@ class ExpirationTracker { * @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); + void Remove(data_components::DataKind kind, std::string const& key); /** * Check the state of a scoped key. @@ -87,7 +87,7 @@ class ExpirationTracker { * @param key The key to check. * @return The state of the key. */ - TrackState State(data_store::DataKind kind, + TrackState State(data_components::DataKind kind, std::string const& key, TimePoint current_time) const; @@ -142,4 +142,4 @@ class ExpirationTracker { std::ostream& operator<<(std::ostream& out, ExpirationTracker::TrackState const& state); -} // namespace launchdarkly::server_side::data_store::persistent +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/kinds/kinds.hpp b/libs/server-sdk/src/data_components/kinds/kinds.hpp new file mode 100644 index 000000000..4378a1304 --- /dev/null +++ b/libs/server-sdk/src/data_components/kinds/kinds.hpp @@ -0,0 +1,26 @@ +#include + +namespace launchdarkly::server_side::data_components { + +class SegmentKind : public integrations::IPersistentKind { + public: + std::string const& Namespace() const override; + uint64_t Version(std::string const& data) const override; + + ~SegmentKind() override = default; + + private: + static inline std::string const namespace_ = "segments"; +}; + +class FlagKind : public integrations::IPersistentKind { + public: + std::string const& Namespace() const override; + uint64_t Version(std::string const& data) const override; + + ~FlagKind() override = default; + + private: + static inline std::string const namespace_ = "features"; +}; +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index df7c08db2..f38357d95 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -39,7 +39,7 @@ bool MemoryStore::Initialized() const { return initialized_; } -std::string const& MemoryStore::Description() const { +std::string const& MemoryStore::Identity() const { return description_; } diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 3f844b515..16d538dfe 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -1,6 +1,7 @@ #pragma once #include "../../data_interfaces/destination/idestination.hpp" +#include "../../data_interfaces/store/istore.hpp" #include #include @@ -9,26 +10,33 @@ namespace launchdarkly::server_side::data_components { -class MemoryStore : public data_interfaces::IDestination { +class MemoryStore : public data_interfaces::IStore, + public data_interfaces::IDestination { public: - std::shared_ptr GetFlag( - std::string const& key) const; + [[nodiscard]] std::shared_ptr GetFlag( + std::string const& key) const override; - std::shared_ptr GetSegment( - std::string const& key) const; + [[nodiscard]] std::shared_ptr GetSegment( + std::string const& key) const override; - std::unordered_map> - AllFlags() const; - std::unordered_map> - AllSegments() const; + [[nodiscard]] std:: + unordered_map> + AllFlags() const override; - bool Initialized() const; - std::string const& Description() const; + [[nodiscard]] std::unordered_map< + std::string, + std::shared_ptr> + AllSegments() const override; + + [[nodiscard]] bool Initialized() const override; + + [[nodiscard]] std::string const& Identity() const override; void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Upsert(std::string const& key, data_model::FlagDescriptor flag) override; + void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp index b74e84318..c73fa4223 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp @@ -1,10 +1,11 @@ #include "json_pull_source.hpp" + #include namespace launchdarkly::server_side::data_components { JsonSource::JsonSource(data_interfaces::ISerializedDataPullSource& json_source) - : source_(json_source) {} + : flag_kind_(), segment_kind_(), source_(json_source) {} template static std::optional> Deserialize( @@ -37,26 +38,26 @@ static std::optional> Deserialize( data_model::FlagDescriptor JsonSource::GetFlag(std::string const& key) const { // TODO: deserialize then return data_interfaces::ISerializedDataPullSource::GetResult result = - source_.Get(kind, key); + source_.Get(flag_kind_, key); } data_model::SegmentDescriptor JsonSource::GetSegment( std::string const& key) const { // TODO: deserialize then return data_interfaces::ISerializedDataPullSource::GetResult result = - source_.Get(kind, key); + source_.Get(segment_kind_, key); } std::unordered_map JsonSource::AllFlags() const { // TODO: deserialize then return - data_interfaces::ISerializedDataPullSource::GetResult result = - source_.All(kind); + data_interfaces::ISerializedDataPullSource::AllResult result = + source_.All(flag_kind_); } std::unordered_map JsonSource::AllSegments() const { // TODO: deserialize then return - data_interfaces::ISerializedDataPullSource::GetResult result = - source_.All(kind); + data_interfaces::ISerializedDataPullSource::AllResult result = + source_.All(segment_kind_); } std::string const& JsonSource::Identity() const { diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp index 5098940f2..1e38e238a 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp @@ -4,6 +4,8 @@ #include "../../data_interfaces/source/ipull_source.hpp" #include "../../data_interfaces/source/iserialized_pull_source.hpp" +#include "../kinds/kinds.hpp" + namespace launchdarkly::server_side::data_components { class JsonSource : public data_interfaces::IPullSource { @@ -24,8 +26,9 @@ class JsonSource : public data_interfaces::IPullSource { AllSegments() const override; [[nodiscard]] virtual std::string const& Identity() const override; - public: private: + FlagKind const flag_kind_; + FlagKind const segment_kind_; data_interfaces::ISerializedDataPullSource& source_; }; diff --git a/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp deleted file mode 100644 index 3e20cd64e..000000000 --- a/libs/server-sdk/src/data_components/sources/noop/null_data_source.hpp +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include "../../../data_interfaces/destination/idestination.hpp" -#include "../../../data_interfaces/source/ipush_source.hpp" - -#include "../../status_notifications/data_source_status_manager.hpp" - -#include - -namespace launchdarkly::server_side::data_components { - -class NullDataSource : public data_interfaces::IPushSource { - public: - explicit NullDataSource(boost::asio::any_io_executor exec, - DataSourceStatusManager& status_manager); - - void Init(std::optional initial_data, - data_interfaces::IDestination& destination) override; - void Start() override; - void ShutdownAsync(std::function) override; - - [[nodiscard]] std::string const& Identity() const override; - - private: - DataSourceStatusManager& status_manager_; - boost::asio::any_io_executor exec_; -}; - -} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_interfaces/store/istore.hpp b/libs/server-sdk/src/data_interfaces/store/istore.hpp index a50e64a8e..c7ee6c4a9 100644 --- a/libs/server-sdk/src/data_interfaces/store/istore.hpp +++ b/libs/server-sdk/src/data_interfaces/store/istore.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/libs/server-sdk/src/data_interfaces/system/isystem.hpp b/libs/server-sdk/src/data_interfaces/system/isystem.hpp index e9417a90e..5bbf7cc63 100644 --- a/libs/server-sdk/src/data_interfaces/system/isystem.hpp +++ b/libs/server-sdk/src/data_interfaces/system/isystem.hpp @@ -1,10 +1,10 @@ #pragma once +#include "../store/istore.hpp" + #include #include -#include "../store/istore.hpp" - #include #include #include @@ -17,6 +17,9 @@ class ISystem : public IStore { [[nodiscard]] virtual std::string const& Identity() const = 0; virtual void Initialize() = 0; + [[nodiscard]] virtual IStore& Store() = 0; + [[nodiscard]] virtual IStore const& Store() const = 0; + virtual ~ISystem() = default; ISystem(ISystem const& item) = delete; ISystem(ISystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 1e728d655..227d5571f 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -9,7 +9,7 @@ BackgroundSync::BackgroundSync( DataSourceConfig const& data_source_config, HttpProperties http_properties, boost::asio::any_io_executor ioc, - DataSourceStatusManager& status_manager, + data_components::DataSourceStatusManager& status_manager, Logger const& logger) : store_(), synchronizer_(), bootstrapper_() { std::visit( diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 8d9d17ed5..aab37112d 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -31,7 +31,7 @@ class BackgroundSync : public data_interfaces::ISystem { config::shared::ServerSDK> const& data_source_config, config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, - DataSourceStatusManager& status_manager, + data_components::DataSourceStatusManager& status_manager, Logger const& logger); BackgroundSync(BackgroundSync const& item) = delete; @@ -54,6 +54,6 @@ class BackgroundSync : public data_interfaces::ISystem { AllSegments() const override; private: - MemoryStore store_; + data_components::MemoryStore store_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_components/sources/README.md b/libs/server-sdk/src/data_systems/background_sync/sources/README.md similarity index 100% rename from libs/server-sdk/src/data_components/sources/README.md rename to libs/server-sdk/src/data_systems/background_sync/sources/README.md diff --git a/libs/server-sdk/src/data_components/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp similarity index 57% rename from libs/server-sdk/src/data_components/sources/noop/null_data_source.cpp rename to libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp index 7736d6f37..820dd47b5 100644 --- a/libs/server-sdk/src/data_components/sources/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_systems { void NullDataSource::Start() { status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); @@ -13,10 +13,11 @@ void NullDataSource::ShutdownAsync(std::function complete) { } void NullDataSource::Init(std::optional initial_data, - IDestination& destination) {} + data_interfaces::IDestination& destination) {} -NullDataSource::NullDataSource(boost::asio::any_io_executor exec, - DataSourceStatusManager& status_manager) +NullDataSource::NullDataSource( + boost::asio::any_io_executor exec, + data_components::DataSourceStatusManager& status_manager) : status_manager_(status_manager), exec_(exec) {} -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp new file mode 100644 index 000000000..5ed0ad980 --- /dev/null +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "../../../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../../../data_interfaces/destination/idestination.hpp" +#include "../../../../data_interfaces/source/ipush_source.hpp" + +#include + +namespace launchdarkly::server_side::data_systems { + +class NullDataSource : public data_interfaces::IPushSource { + public: + explicit NullDataSource( + boost::asio::any_io_executor exec, + data_components::DataSourceStatusManager& status_manager); + + void Init(std::optional initial_data, + data_interfaces::IDestination& destination) override; + void Start() override; + void ShutdownAsync(std::function) override; + + [[nodiscard]] std::string const& Identity() const override; + + private: + data_components::DataSourceStatusManager& status_manager_; + boost::asio::any_io_executor exec_; +}; + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_components/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp similarity index 96% rename from libs/server-sdk/src/data_components/sources/polling/polling_data_source.cpp rename to libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index c83aa2570..cb912aec4 100644 --- a/libs/server-sdk/src/data_components/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -1,5 +1,4 @@ #include "polling_data_source.hpp" -#include "data_source_update_sink.hpp" #include #include @@ -13,7 +12,7 @@ #include -namespace launchdarkly::server_side::data_components { +namespace launchdarkly::server_side::data_systems { static char const* const kErrorParsingPut = "Could not parse polling payload"; static char const* const kErrorPutInvalid = @@ -47,8 +46,8 @@ PollingDataSource::PollingDataSource( data_source_config, config::shared::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& ioc, - IDataSourceUpdateSink& handler, - DataSourceStatusManager& status_manager, + data_interfaces::IDestination& handler, + data_components::DataSourceStatusManager& status_manager, Logger const& logger) : ioc_(ioc), logger_(logger), @@ -71,7 +70,7 @@ PollingDataSource::PollingDataSource( } void PollingDataSource::Init(std::optional initial_data, - IDestination& destination) { + data_interfaces::IDestination& destination) { // TODO: implement } void PollingDataSource::DoPoll() { @@ -235,4 +234,4 @@ void PollingDataSource::ShutdownAsync(std::function completion) { } } -} // namespace launchdarkly::server_side::data_components +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp similarity index 78% rename from libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp rename to libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp index 806394f5f..6b6404b0c 100644 --- a/libs/server-sdk/src/data_components/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp @@ -1,8 +1,8 @@ #pragma once -#include "../../../data_interfaces/destination/idestination.hpp" -#include "../../../data_interfaces/source/ipush_source.hpp" -#include "../../status_notifications/data_source_status_manager.hpp" +#include "../../../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../../../data_interfaces/destination/idestination.hpp" +#include "../../../../data_interfaces/source/ipush_source.hpp" #include #include @@ -14,7 +14,7 @@ #include -namespace launchdarkly::server_side::data_components { +namespace launchdarkly::server_side::data_systems { class PollingDataSource : public data_interfaces::IPushSource, @@ -26,7 +26,7 @@ class PollingDataSource data_source_config, config::shared::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& ioc, - DataSourceStatusManager& status_manager, + data_components::DataSourceStatusManager& status_manager, Logger const& logger); void Init(std::optional initial_data, @@ -40,7 +40,7 @@ class PollingDataSource void DoPoll(); void HandlePollResult(network::HttpResult const& res); - DataSourceStatusManager& status_manager_; + data_components::DataSourceStatusManager& status_manager_; std::string polling_endpoint_; network::AsioRequester requester_; @@ -57,4 +57,4 @@ class PollingDataSource void StartPollingTimer(); }; -} // namespace launchdarkly::server_side::data_components +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/background_sync/event_handler.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp similarity index 96% rename from libs/server-sdk/src/data_systems/background_sync/event_handler.cpp rename to libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp index dcc0695f2..f38911870 100644 --- a/libs/server-sdk/src/data_systems/background_sync/event_handler.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp @@ -15,7 +15,7 @@ #include "tl/expected.hpp" -namespace launchdarkly::server_side::data { +namespace launchdarkly::server_side::data_systems { static char const* const kErrorParsingPut = "Could not parse PUT message"; static char const* const kErrorPutInvalid = @@ -126,9 +126,9 @@ static tl::expected tag_invoke( } DataSourceEventHandler::DataSourceEventHandler( - IDestination& handler, + data_interfaces::IDestination& handler, Logger const& logger, - DataSourceStatusManager& status_manager) + data_components::DataSourceStatusManager& status_manager) : handler_(handler), logger_(logger), status_manager_(status_manager) {} DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( @@ -212,13 +212,13 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( if (res.has_value()) { switch (res->kind) { - case data_store::DataKind::kFlag: { + case data_components::DataKind::kFlag: { handler_.Upsert(res->key, data_model::FlagDescriptor(res->version)); return DataSourceEventHandler::MessageStatus:: kMessageHandled; } - case data_store::DataKind::kSegment: { + case data_components::DataKind::kSegment: { handler_.Upsert( res->key, data_model::SegmentDescriptor(res->version)); return DataSourceEventHandler::MessageStatus:: @@ -238,4 +238,4 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( return DataSourceEventHandler::MessageStatus::kUnhandledVerb; } -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/background_sync/event_handler.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp similarity index 65% rename from libs/server-sdk/src/data_systems/background_sync/event_handler.hpp rename to libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp index 57494edd7..897e7e94c 100644 --- a/libs/server-sdk/src/data_systems/background_sync/event_handler.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp @@ -1,20 +1,21 @@ #pragma once -#include - -#include - -#include "../data_store/data_kind.hpp" -#include "data_destination_interface.hpp" -#include "data_source_status_manager.hpp" +#include "../../../../data_components/dependency_tracker/data_kind.hpp" +#include "../../../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../../../data_interfaces/destination/idestination.hpp" #include #include #include -#include +#include +#include #include -namespace launchdarkly::server_side::data { +#include + +#include + +namespace launchdarkly::server_side::data_systems { // The FlagsPath and SegmentsPath are made to turn a string literal into a type // for use in a template. @@ -29,10 +30,10 @@ struct SegmentsPath { static constexpr std::string_view path = "/segments/"; }; -template +template class StreamingDataKind { public: - static data_store::DataKind Kind() { return kind; } + static data_components::DataKind Kind() { return kind; } static bool IsKind(std::string const& patch_path) { return patch_path.rfind(TPath::path) == 0; } @@ -42,16 +43,17 @@ class StreamingDataKind { }; struct StreamingDataKinds { - using Flag = StreamingDataKind; + using Flag = StreamingDataKind; using Segment = - StreamingDataKind; + StreamingDataKind; - static std::optional Kind(std::string const& path) { + static std::optional Kind( + std::string const& path) { if (Flag::IsKind(path)) { - return data_store::DataKind::kFlag; + return data_components::DataKind::kFlag; } if (Segment::IsKind(path)) { - return data_store::DataKind::kSegment; + return data_components::DataKind::kSegment; } return std::nullopt; } @@ -93,18 +95,20 @@ class DataSourceEventHandler { struct Patch { std::string key; - std::variant data; + std::variant + data; }; struct Delete { std::string key; - data_store::DataKind kind; + data_components::DataKind kind; uint64_t version; }; - DataSourceEventHandler(IDestination& handler, - Logger const& logger, - DataSourceStatusManager& status_manager); + DataSourceEventHandler( + data_interfaces::IDestination& handler, + Logger const& logger, + data_components::DataSourceStatusManager& status_manager); /** * Handles an event from the LaunchDarkly service. @@ -116,8 +120,8 @@ class DataSourceEventHandler { std::string const& data); private: - IDestination& handler_; + data_interfaces::IDestination& handler_; Logger const& logger_; - DataSourceStatusManager& status_manager_; + data_components::DataSourceStatusManager& status_manager_; }; -} // namespace launchdarkly::server_side::data +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp similarity index 94% rename from libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.cpp rename to libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index 37e129c32..a1f9505a6 100644 --- a/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -9,7 +9,7 @@ #include -namespace launchdarkly::server_side::data_components { +namespace launchdarkly::server_side::data_systems { static char const* const kCouldNotParseEndpoint = "Could not parse streaming endpoint URL"; @@ -34,21 +34,20 @@ StreamingDataSource::StreamingDataSource( data_source_config, config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, - IDataSourceUpdateSink& handler, - DataSourceStatusManager& status_manager, + data_interfaces::IDestination& handler, + data_components::DataSourceStatusManager& status_manager, Logger const& logger) : exec_(std::move(ioc)), logger_(logger), status_manager_(status_manager), - data_source_handler_( - DataSourceEventHandler(handler, logger, status_manager_)), + data_source_handler_(handler, logger, status_manager_), http_config_(std::move(http_properties)), streaming_config_(data_source_config), streaming_endpoint_(endpoints.StreamingBaseUrl()) {} void StreamingDataSource::Init( std::optional initial_data, - IDestination& destination) { + data_interfaces::IDestination& destination) { // TODO: implement } @@ -153,4 +152,4 @@ void StreamingDataSource::ShutdownAsync(std::function completion) { boost::asio::post(exec_, completion); } } -} // namespace launchdarkly::server_side::data_components +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp similarity index 75% rename from libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp rename to libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp index fe55b89bc..869b3a958 100644 --- a/libs/server-sdk/src/data_components/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp @@ -1,9 +1,10 @@ #pragma once -#include "../../../data_interfaces/destination/idestination.hpp" -#include "../../../data_interfaces/source/ipush_source.hpp" +#include "event_handler.hpp" -#include "../../status_notifications/data_source_status_manager.hpp" +#include "../../../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../../../data_interfaces/destination/idestination.hpp" +#include "../../../../data_interfaces/source/ipush_source.hpp" #include #include @@ -20,7 +21,7 @@ using namespace std::chrono_literals; -namespace launchdarkly::server_side::data_components { +namespace launchdarkly::server_side::data_systems { class StreamingDataSource final : public data_interfaces::IPushSource, @@ -33,7 +34,7 @@ class StreamingDataSource final config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_interfaces::IDestination& handler, - DataSourceStatusManager& status_manager, + data_components::DataSourceStatusManager& status_manager, Logger const& logger); void Init(std::optional initial_data, @@ -45,8 +46,8 @@ class StreamingDataSource final private: boost::asio::any_io_executor exec_; - DataSourceStatusManager& status_manager_; - data_interfaces::IDestination& data_source_handler_; + data_components::DataSourceStatusManager& status_manager_; + DataSourceEventHandler data_source_handler_; std::string streaming_endpoint_; config::shared::built::StreamingConfig @@ -57,4 +58,4 @@ class StreamingDataSource final Logger const& logger_; std::shared_ptr client_; }; -} // namespace launchdarkly::server_side::data_components +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 4898d09c7..de68bb2da 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -3,8 +3,6 @@ #include #include -#include "data_version_inspectors.hpp" - namespace launchdarkly::server_side::data_systems { using namespace config::shared::built; @@ -14,13 +12,14 @@ LazyLoad::LazyLoad( DataSourceConfig const& data_source_config, HttpProperties http_properties, boost::asio::any_io_executor ioc, - DataSourceStatusManager& status_manager, + data_components::DataSourceStatusManager& status_manager, Logger const& logger) : store_() {} std::string const& LazyLoad::Identity() const { // TODO: Obtain more specific info static std::string id = "generic lazy-loader"; + return id; } void LazyLoad::Initialize() {} @@ -59,17 +58,19 @@ LazyLoad::AllSegments() const { [this]() { return memory_store_.AllSegments(); }); } -PersistentStore::FlagKind const PersistentStore::Kinds::Flag = FlagKind(); -PersistentStore::SegmentKind const PersistentStore::Kinds::Segment = - SegmentKind(); +data_components::FlagKind const LazyLoad::Kinds::Flag = + data_components::FlagKind(); + +data_components::SegmentKind const LazyLoad::Kinds::Segment = + data_components::SegmentKind(); -bool PersistentStore::Initialized() const { +bool LazyLoad::Initialized() const { auto state = tracker_.State(Keys::kInitialized, time_()); if (initialized_.has_value()) { if (initialized_.value()) { return true; } - if (ExpirationTracker::TrackState::kFresh == state) { + if (data_components::ExpirationTracker::TrackState::kFresh == state) { return initialized_.value(); } } @@ -104,7 +105,7 @@ void LazyLoad::RefreshSegment(std::string const& key) const { } // TODO: Log that we got bogus data? } - tracker_.Add(DataKind::kSegment, key, time_()); + tracker_.Add(data_components::DataKind::kSegment, key, time_()); } // TODO: If there is an actual error, then do we not reset the tracking? } @@ -119,7 +120,7 @@ void LazyLoad::RefreshFlag(std::string const& key) const { } // TODO: Log that we got bogus data? } - tracker_.Add(DataKind::kSegment, key, time_()); + tracker_.Add(data_components::DataKind::kSegment, key, time_()); } // TODO: If there is an actual error, then do we not reset the tracking? } diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 62e120834..e2989ea6e 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -1,5 +1,7 @@ #pragma once +#include "../../data_components/expiration_tracker/expiration_tracker.hpp" +#include "../../data_components/kinds/kinds.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/system/isystem.hpp" @@ -9,6 +11,7 @@ #include #include #include +#include #include @@ -30,7 +33,7 @@ class LazyLoad : public data_interfaces::ISystem { config::shared::ServerSDK> const& data_source_config, config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, - DataSourceStatusManager& status_manager, + data_components::DataSourceStatusManager& status_manager, Logger const& logger); LazyLoad(LazyLoad const& item) = delete; @@ -50,6 +53,8 @@ class LazyLoad : public data_interfaces::ISystem { std::shared_ptr> AllSegments() const override; + bool Initialized() const; + void Initialize() override; private: @@ -59,63 +64,41 @@ class LazyLoad : public data_interfaces::ISystem { void RefreshFlag(std::string const& key) const; void RefreshSegment(std::string const& key) const; - static persistence::SerializedItemDescriptor Serialize( + static integrations::SerializedItemDescriptor Serialize( data_model::FlagDescriptor flag); - static persistence::SerializedItemDescriptor Serialize( + static integrations::SerializedItemDescriptor Serialize( data_model::SegmentDescriptor segment); static std::optional DeserializeFlag( - persistence::SerializedItemDescriptor flag); + integrations::SerializedItemDescriptor flag); static std::optional DeserializeSegment( - persistence::SerializedItemDescriptor segment); + integrations::SerializedItemDescriptor segment); template - static TResult Get(ExpirationTracker::TrackState state, + static TResult Get(data_components::ExpirationTracker::TrackState state, std::function refresh, std::function get) { switch (state) { - case ExpirationTracker::TrackState::kStale: + case data_components::ExpirationTracker::TrackState::kStale: [[fallthrough]]; - case ExpirationTracker::TrackState::kNotTracked: + case data_components::ExpirationTracker::TrackState::kNotTracked: refresh(); [[fallthrough]]; - case ExpirationTracker::TrackState::kFresh: + case data_components::ExpirationTracker::TrackState::kFresh: return get(); } } - mutable MemoryStore memory_store_; + mutable data_components::MemoryStore memory_store_; std::shared_ptr core_; - mutable ExpirationTracker tracker_; + mutable data_components::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; - - ~SegmentKind() override = default; - - private: - static inline std::string const 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 inline std::string const namespace_ = "features"; - }; - struct Kinds { - static FlagKind const Flag; - static SegmentKind const Segment; + static data_components::FlagKind const Flag; + static data_components::SegmentKind const Segment; }; struct Keys { @@ -123,7 +106,5 @@ class LazyLoad : public data_interfaces::ISystem { static inline std::string const kAllSegments = "allSegments"; static inline std::string const kInitialized = "initialized"; }; - - MemoryStore store_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp index ac6359c6c..5c2f9ea44 100644 --- a/libs/server-sdk/src/evaluation/rules.cpp +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -15,7 +15,7 @@ bool MaybeNegate(Clause const& clause, bool value) { tl::expected Match(Flag::Rule const& rule, launchdarkly::Context const& context, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { for (Clause const& clause : rule.clauses) { tl::expected result = Match(clause, context, store, stack); @@ -31,7 +31,7 @@ tl::expected Match(Flag::Rule const& rule, tl::expected Match(Segment::Rule const& rule, Context const& context, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack, std::string const& key, std::string const& salt) { @@ -61,7 +61,7 @@ tl::expected Match(Segment::Rule const& rule, tl::expected Match(Clause const& clause, launchdarkly::Context const& context, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { if (clause.op == Clause::Op::kSegmentMatch) { return MatchSegment(clause, context, store, stack); @@ -71,7 +71,7 @@ tl::expected Match(Clause const& clause, tl::expected MatchSegment(Clause const& clause, launchdarkly::Context const& context, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { for (Value const& value : clause.values) { // A segment key represented as a Value is a string; non-strings are @@ -153,7 +153,7 @@ tl::expected MatchNonSegment( tl::expected Contains(Segment const& segment, Context const& context, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { auto guard = stack.NoticeSegment(segment.key); if (!guard) { diff --git a/libs/server-sdk/src/evaluation/rules.hpp b/libs/server-sdk/src/evaluation/rules.hpp index f282bac28..00f1cf31f 100644 --- a/libs/server-sdk/src/evaluation/rules.hpp +++ b/libs/server-sdk/src/evaluation/rules.hpp @@ -1,13 +1,13 @@ #pragma once +#include "../data_interfaces/store/istore.hpp" +#include "detail/evaluation_stack.hpp" +#include "evaluation_error.hpp" + #include #include #include -#include "../data_retrieval/interfaces/data_store/data_store.hpp" -#include "detail/evaluation_stack.hpp" -#include "evaluation_error.hpp" - #include #include @@ -17,18 +17,18 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Match( data_model::Flag::Rule const&, Context const&, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack); [[nodiscard]] tl::expected Match(data_model::Clause const&, Context const&, - data_retrieval::IDataStore const&, + data_interfaces::IStore const&, detail::EvaluationStack&); [[nodiscard]] tl::expected Match( data_model::Segment::Rule const& rule, Context const& context, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack, std::string const& key, std::string const& salt); @@ -36,7 +36,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected MatchSegment( data_model::Clause const&, Context const&, - data_retrieval::IDataStore const&, + data_interfaces::IStore const&, detail::EvaluationStack& stack); [[nodiscard]] tl::expected MatchNonSegment( @@ -46,7 +46,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Contains( data_model::Segment const&, Context const&, - data_retrieval::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack); [[nodiscard]] bool MaybeNegate(data_model::Clause const& clause, bool value); From 78be652e8434cf991ef6c31bf64fe615093b4a8d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 25 Oct 2023 17:35:58 -0700 Subject: [PATCH 072/244] it compiles --- .../integrations/serialized_descriptors.hpp | 37 ------------- libs/server-sdk/src/CMakeLists.txt | 1 + libs/server-sdk/src/client_impl.cpp | 10 ++-- libs/server-sdk/src/client_impl.hpp | 3 ++ .../change_notifier_destination.cpp | 22 ++++---- .../change_notifier_destination.hpp | 21 +++++--- .../src/data_components/kinds/kinds.cpp | 45 ++++++++++++++++ .../src/data_components/kinds/kinds.hpp | 4 +- .../memory_store/memory_store.hpp | 4 +- .../source/iserialized_pull_source.hpp | 2 + .../src/data_interfaces/system/isystem.hpp | 3 -- .../background_sync_system.cpp | 19 +++---- .../background_sync_system.hpp | 14 +++-- .../sources/polling/polling_data_source.hpp | 1 + .../lazy_load/lazy_load_system.cpp | 24 ++++----- .../lazy_load/lazy_load_system.hpp | 5 +- .../tests/data_store_updater_test.cpp | 54 +++++++++---------- 17 files changed, 148 insertions(+), 121 deletions(-) create mode 100644 libs/server-sdk/src/data_components/kinds/kinds.cpp diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp index e6eb1e81c..ded12cd05 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp @@ -59,41 +59,4 @@ class IPersistentKind { protected: IPersistentKind() = default; }; - -// TODO Find out where to put these - -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 { - return namespace_; -} - -uint64_t PersistentStore::SegmentKind::Version(std::string const& data) const { - return GetVersion(data); -} - -std::string const& PersistentStore::FlagKind::Namespace() const { - return namespace_; -} - -uint64_t PersistentStore::FlagKind::Version(std::string const& data) const { - return GetVersion(data); -} - } // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 5bcaf40c1..54c15a3f6 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -28,6 +28,7 @@ target_sources(${LIBNAME} data_components/serialization_adapters/json_destination.cpp data_components/serialization_adapters/json_pull_source.cpp data_components/expiration_tracker/expiration_tracker.cpp + data_components/kinds/kinds.cpp data_systems/background_sync/sources/noop/null_data_source.cpp data_systems/background_sync/sources/streaming/streaming_data_source.cpp data_systems/background_sync/sources/polling/polling_data_source.cpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 2e72fcb61..ef2fb0e8e 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -126,7 +126,7 @@ ClientImpl::ClientImpl(Config config, std::string const& version) ioc_(kAsioConcurrencyHint), work_(boost::asio::make_work_guard(ioc_)), status_manager_(), - data_source_(MakeDataSource(http_properties_, + data_system_(MakeDataSystem(http_properties_, config_, ioc_.get_executor(), status_manager_, @@ -135,7 +135,7 @@ ClientImpl::ClientImpl(Config config, std::string const& version) ioc_.get_executor(), http_properties_, logger_)), - evaluator_(logger_, *data_source_), + evaluator_(logger_, *data_system_), events_default_(event_processor_.get(), EventFactory::WithoutReasons()), events_with_reasons_(event_processor_.get(), EventFactory::WithReasons()) { @@ -163,7 +163,7 @@ std::future ClientImpl::StartAsyncInternal( auto fut = pr->get_future(); status_manager_.OnDataSourceStatusChangeEx( - [result_predicate, pr](data_sources::DataSourceStatus status) { + [result_predicate, pr](auto status) { auto state = status.State(); if (IsInitialized(state)) { pr->set_value(result_predicate(status.State())); @@ -215,7 +215,7 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, EventScope no_events; - for (auto const& [k, v] : data_system_->Store().AllFlags()) { + for (auto const& [k, v] : data_system_->AllFlags()) { if (!v || !v->item) { continue; } @@ -316,7 +316,7 @@ EvaluationDetail ClientImpl::VariationInternal( std::nullopt); } - auto flag_rule = data_system_->Store().GetFlag(key); + auto flag_rule = data_system_->GetFlag(key); bool flag_present = IsFlagPresent(flag_rule); diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 865c658f1..5f722f25e 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -168,6 +168,9 @@ class ClientImpl : public IClient { data_components::DataSourceStatusManager status_manager_; + // This is the main polymorphic component that constitutes the + // guts of how data is retrieved (polling, streaming, persistent stores, + // etc.) std::unique_ptr data_system_; std::unique_ptr event_processor_; diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp index 738a8d451..290993baa 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp @@ -5,7 +5,7 @@ namespace launchdarkly::server_side::data_components { -std::unique_ptr DataStoreUpdater::OnFlagChange( +std::unique_ptr ChangeNotifierDestination::OnFlagChange( launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { std::lock_guard lock{signal_mutex_}; @@ -13,7 +13,8 @@ std::unique_ptr DataStoreUpdater::OnFlagChange( signals_.connect(handler)); } -void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { +void ChangeNotifierDestination::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. @@ -44,23 +45,23 @@ void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { } } -void DataStoreUpdater::Upsert(std::string const& key, - data_model::FlagDescriptor flag) { +void ChangeNotifierDestination::Upsert(std::string const& key, + data_model::FlagDescriptor flag) { UpsertCommon(DataKind::kFlag, key, source_.GetFlag(key), std::move(flag)); } -void DataStoreUpdater::Upsert(std::string const& key, - data_model::SegmentDescriptor segment) { +void ChangeNotifierDestination::Upsert(std::string const& key, + data_model::SegmentDescriptor segment) { UpsertCommon(DataKind::kSegment, key, source_.GetSegment(key), std::move(segment)); } -bool DataStoreUpdater::HasListeners() const { +bool ChangeNotifierDestination::HasListeners() const { std::lock_guard lock{signal_mutex_}; return !signals_.empty(); } -void DataStoreUpdater::NotifyChanges(DependencySet changes) { +void ChangeNotifierDestination::NotifyChanges(DependencySet changes) { std::lock_guard lock{signal_mutex_}; auto flag_changes = changes.SetForKind(DataKind::kFlag); // Only emit an event if there are changes. @@ -69,8 +70,9 @@ void DataStoreUpdater::NotifyChanges(DependencySet changes) { } } -DataStoreUpdater::DataStoreUpdater(IDestination& sink, - data_interfaces::IStore const& source) +ChangeNotifierDestination::ChangeNotifierDestination( + IDestination& sink, + data_interfaces::IStore const& source) : sink_(sink), source_(source) {} } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp index e58c71bb6..19074bc9f 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.hpp @@ -13,8 +13,8 @@ namespace launchdarkly::server_side::data_components { -class DataStoreUpdater : public data_interfaces::IDestination, - public IChangeNotifier { +class ChangeNotifierDestination : public data_interfaces::IDestination, + public IChangeNotifier { public: template using Collection = data_model::SDKDataSet::Collection; @@ -26,7 +26,8 @@ class DataStoreUpdater : public data_interfaces::IDestination, using SharedCollection = std::unordered_map>; - DataStoreUpdater(IDestination& sink, data_interfaces::IStore const& source); + ChangeNotifierDestination(IDestination& sink, + data_interfaces::IStore const& source); std::unique_ptr OnFlagChange(ChangeHandler handler) override; @@ -35,12 +36,16 @@ class DataStoreUpdater : public data_interfaces::IDestination, data_model::FlagDescriptor flag) override; void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; - ~DataStoreUpdater() override = default; - DataStoreUpdater(DataStoreUpdater const& item) = delete; - DataStoreUpdater(DataStoreUpdater&& item) = delete; - DataStoreUpdater& operator=(DataStoreUpdater const&) = delete; - DataStoreUpdater& operator=(DataStoreUpdater&&) = delete; + [[nodiscard]] std::string const& Identity() const override; + + ~ChangeNotifierDestination() override = default; + + ChangeNotifierDestination(ChangeNotifierDestination const& item) = delete; + ChangeNotifierDestination(ChangeNotifierDestination&& item) = delete; + ChangeNotifierDestination& operator=(ChangeNotifierDestination const&) = + delete; + ChangeNotifierDestination& operator=(ChangeNotifierDestination&&) = delete; private: bool HasListeners() const; diff --git a/libs/server-sdk/src/data_components/kinds/kinds.cpp b/libs/server-sdk/src/data_components/kinds/kinds.cpp new file mode 100644 index 000000000..91234c172 --- /dev/null +++ b/libs/server-sdk/src/data_components/kinds/kinds.cpp @@ -0,0 +1,45 @@ +#include "kinds.hpp" +#include "launchdarkly/serialization/json_errors.hpp" + +#include +#include + +#include + +namespace launchdarkly::server_side::data_components { + +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& SegmentKind::Namespace() const { + return namespace_; +} + +std::uint64_t SegmentKind::Version(std::string const& data) const { + return GetVersion(data); +} + +std::string const& FlagKind::Namespace() const { + return namespace_; +} + +std::uint64_t FlagKind::Version(std::string const& data) const { + return GetVersion(data); +} + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/kinds/kinds.hpp b/libs/server-sdk/src/data_components/kinds/kinds.hpp index 4378a1304..759d5f3ad 100644 --- a/libs/server-sdk/src/data_components/kinds/kinds.hpp +++ b/libs/server-sdk/src/data_components/kinds/kinds.hpp @@ -5,7 +5,7 @@ namespace launchdarkly::server_side::data_components { class SegmentKind : public integrations::IPersistentKind { public: std::string const& Namespace() const override; - uint64_t Version(std::string const& data) const override; + std::uint64_t Version(std::string const& data) const override; ~SegmentKind() override = default; @@ -16,7 +16,7 @@ class SegmentKind : public integrations::IPersistentKind { class FlagKind : public integrations::IPersistentKind { public: std::string const& Namespace() const override; - uint64_t Version(std::string const& data) const override; + std::uint64_t Version(std::string const& data) const override; ~FlagKind() override = default; diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 16d538dfe..a8b733be7 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -28,7 +28,7 @@ class MemoryStore : public data_interfaces::IStore, std::shared_ptr> AllSegments() const override; - [[nodiscard]] bool Initialized() const override; + [[nodiscard]] bool Initialized() const; [[nodiscard]] std::string const& Identity() const override; @@ -36,7 +36,7 @@ class MemoryStore : public data_interfaces::IStore, void Upsert(std::string const& key, data_model::FlagDescriptor flag) override; - + void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; diff --git a/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp b/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp index 2cabcc9ff..286cf3f2b 100644 --- a/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp @@ -72,6 +72,8 @@ class ISerializedDataPullSource { virtual std::string const& Identity() const = 0; + virtual bool Initialized() const = 0; + protected: ISerializedDataPullSource() = default; }; diff --git a/libs/server-sdk/src/data_interfaces/system/isystem.hpp b/libs/server-sdk/src/data_interfaces/system/isystem.hpp index 5bbf7cc63..0a0c86e26 100644 --- a/libs/server-sdk/src/data_interfaces/system/isystem.hpp +++ b/libs/server-sdk/src/data_interfaces/system/isystem.hpp @@ -17,9 +17,6 @@ class ISystem : public IStore { [[nodiscard]] virtual std::string const& Identity() const = 0; virtual void Initialize() = 0; - [[nodiscard]] virtual IStore& Store() = 0; - [[nodiscard]] virtual IStore const& Store() const = 0; - virtual ~ISystem() = default; ISystem(ISystem const& item) = delete; ISystem(ISystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 227d5571f..1c875a71a 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -1,5 +1,8 @@ #include "background_sync_system.hpp" +#include "sources/polling/polling_data_source.hpp" +#include "sources/streaming/streaming_data_source.hpp" + namespace launchdarkly::server_side::data_systems { using namespace config::shared::built; @@ -11,24 +14,22 @@ BackgroundSync::BackgroundSync( boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, Logger const& logger) - : store_(), synchronizer_(), bootstrapper_() { + : store_(), change_notifier_(store_, store_), synchronizer_() { std::visit( [&](auto&& method_config) { using T = std::decay_t; if constexpr (std::is_same_v< T, StreamingConfig>) { - synchronizer_ = - std::make_shared( - endpoints, method_config, http_properties, ioc, store_, - status_manager, logger); + synchronizer_ = std::make_shared( + endpoints, method_config, http_properties, ioc, store_, + status_manager, logger); } else if constexpr (std::is_same_v< T, PollingConfig< config::shared::ServerSDK>>) { - synchronizer_ = - std::make_shared( - endpoints, method_config, http_properties, ioc, store_, - status_manager, logger); + synchronizer_ = std::make_shared( + endpoints, method_config, http_properties, ioc, store_, + status_manager, logger); } }, data_source_config.method); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index aab37112d..4f9d754a8 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -1,7 +1,9 @@ #pragma once +#include "../../data_components/change_notifier/change_notifier_destination.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../data_interfaces/source/ipush_source.hpp" #include "../../data_interfaces/system/isystem.hpp" #include @@ -39,10 +41,6 @@ class BackgroundSync : public data_interfaces::ISystem { BackgroundSync& operator=(BackgroundSync const&) = delete; BackgroundSync& operator=(BackgroundSync&&) = delete; - std::string const& Identity() const override; - - void Initialize() override; - std::shared_ptr GetFlag( std::string const& key) const override; std::shared_ptr GetSegment( @@ -53,7 +51,15 @@ class BackgroundSync : public data_interfaces::ISystem { std::shared_ptr> AllSegments() const override; + std::string const& Identity() const override; + + void Initialize() override; + private: data_components::MemoryStore store_; + data_components::ChangeNotifierDestination change_notifier_; + // Needs to be shared to that the source can keep itself alive through + // async operations. + std::shared_ptr synchronizer_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp index 6b6404b0c..90bb4d225 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp @@ -26,6 +26,7 @@ class PollingDataSource data_source_config, config::shared::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& ioc, + data_interfaces::IDestination& handler, data_components::DataSourceStatusManager& status_manager, Logger const& logger); diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index de68bb2da..034302219 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -14,7 +14,7 @@ LazyLoad::LazyLoad( boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, Logger const& logger) - : store_() {} + : cache_() {} std::string const& LazyLoad::Identity() const { // TODO: Obtain more specific info @@ -29,7 +29,7 @@ std::shared_ptr LazyLoad::GetFlag( auto state = tracker_.State(Keys::kAllSegments, time_()); return Get>( state, [this, &key]() { RefreshFlag(key); }, - [this, &key]() { return memory_store_.GetFlag(key); }); + [this, &key]() { return cache_.GetFlag(key); }); } std::shared_ptr LazyLoad::GetSegment( @@ -37,7 +37,7 @@ std::shared_ptr LazyLoad::GetSegment( auto state = tracker_.State(Keys::kAllSegments, time_()); return Get>( state, [this, &key]() { RefreshSegment(key); }, - [this, &key]() { return memory_store_.GetSegment(key); }); + [this, &key]() { return cache_.GetSegment(key); }); } std::unordered_map> @@ -46,7 +46,7 @@ LazyLoad::AllFlags() const { return Get>>( state, [this]() { RefreshAllFlags(); }, - [this]() { return memory_store_.AllFlags(); }); + [this]() { return cache_.AllFlags(); }); } std::unordered_map> @@ -55,7 +55,7 @@ LazyLoad::AllSegments() const { return Get>>( state, [this]() { RefreshAllSegments(); }, - [this]() { return memory_store_.AllSegments(); }); + [this]() { return cache_.AllSegments(); }); } data_components::FlagKind const LazyLoad::Kinds::Flag = @@ -79,29 +79,29 @@ bool LazyLoad::Initialized() const { } void LazyLoad::RefreshAllFlags() const { - auto res = core_->All(Kinds::Flag); + auto res = source_->All(Kinds::Flag); // TODO: Deserialize and put in store. tracker_.Add(Keys::kAllSegments, time_()); } void LazyLoad::RefreshAllSegments() const { - auto res = core_->All(Kinds::Segment); + auto res = source_->All(Kinds::Segment); // TODO: Deserialize and put in store. tracker_.Add(Keys::kAllFlags, time_()); } void LazyLoad::RefreshInitState() const { - initialized_ = core_->Initialized(); + initialized_ = source_->Initialized(); tracker_.Add(Keys::kInitialized, time_()); } void LazyLoad::RefreshSegment(std::string const& key) const { - auto res = core_->Get(Kinds::Segment, key); + auto res = source_->Get(Kinds::Segment, key); if (res.has_value()) { if (res->has_value()) { auto segment = DeserializeSegment(res->value()); if (segment.has_value()) { - memory_store_.Upsert(key, segment.value()); + cache_.Upsert(key, segment.value()); } // TODO: Log that we got bogus data? } @@ -111,12 +111,12 @@ void LazyLoad::RefreshSegment(std::string const& key) const { } void LazyLoad::RefreshFlag(std::string const& key) const { - auto res = core_->Get(Kinds::Segment, key); + auto res = source_->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()); + cache_.Upsert(key, flag.value()); } // TODO: Log that we got bogus data? } diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index e2989ea6e..1bcce7379 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -4,6 +4,7 @@ #include "../../data_components/kinds/kinds.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" +#include "../../data_interfaces/source/iserialized_pull_source.hpp" #include "../../data_interfaces/system/isystem.hpp" #include @@ -90,8 +91,8 @@ class LazyLoad : public data_interfaces::ISystem { } } - mutable data_components::MemoryStore memory_store_; - std::shared_ptr core_; + mutable data_components::MemoryStore cache_; + std::shared_ptr source_; mutable data_components::ExpirationTracker tracker_; std::function()> time_; mutable std::optional initialized_; diff --git a/libs/server-sdk/tests/data_store_updater_test.cpp b/libs/server-sdk/tests/data_store_updater_test.cpp index 87fc139b9..9764cbf48 100644 --- a/libs/server-sdk/tests/data_store_updater_test.cpp +++ b/libs/server-sdk/tests/data_store_updater_test.cpp @@ -5,7 +5,7 @@ #include "data_store/memory_store.hpp" using launchdarkly::data_model::SDKDataSet; -using launchdarkly::server_side::data_store::DataStoreUpdater; +using launchdarkly::server_side::data_store::ChangeNotifierDestination; using launchdarkly::server_side::data_store::FlagDescriptor; using launchdarkly::server_side::data_store::IDataStore; using launchdarkly::server_side::data_store::MemoryStore; @@ -15,22 +15,22 @@ using launchdarkly::Value; using launchdarkly::data_model::Flag; using launchdarkly::data_model::Segment; -TEST(DataStoreUpdaterTest, DoesNotInitializeStoreUntilInit) { +TEST(ChangeNotifierDestinationTest, DoesNotInitializeStoreUntilInit) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); EXPECT_FALSE(store.Initialized()); } -TEST(DataStoreUpdaterTest, InitializesStore) { +TEST(ChangeNotifierDestinationTest, InitializesStore) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet()); EXPECT_TRUE(store.Initialized()); } -TEST(DataStoreUpdaterTest, InitPropagatesData) { +TEST(ChangeNotifierDestinationTest, InitPropagatesData) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Flag flag; flag.version = 1; flag.key = "flagA"; @@ -65,9 +65,9 @@ TEST(DataStoreUpdaterTest, InitPropagatesData) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { +TEST(ChangeNotifierDestinationTest, SecondInitProducesChanges) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Flag flag_a_v1; flag_a_v1.version = 1; flag_a_v1.key = "flagA"; @@ -144,9 +144,9 @@ TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { EXPECT_TRUE(got_event); } -TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { +TEST(ChangeNotifierDestinationTest, CanUpsertNewFlag) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Flag flag_a; flag_a.version = 1; @@ -166,13 +166,13 @@ TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); } -TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { +TEST(ChangeNotifierDestinationTest, CanUpsertExitingFlag) { Flag flag_a; flag_a.version = 1; flag_a.key = "flagA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -194,14 +194,14 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); } -TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { +TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertFlag) { Flag flag_a; flag_a.version = 2; flag_a.key = "flagA"; flag_a.variations = std::vector{"potato", "ham"}; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -228,13 +228,13 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { EXPECT_EQ(std::string("ham"), fetched_flag->item->variations[1].AsString()); } -TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { +TEST(ChangeNotifierDestinationTest, CanUpsertNewSegment) { Segment segment_a; segment_a.version = 1; segment_a.key = "segmentA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -250,13 +250,13 @@ TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { +TEST(ChangeNotifierDestinationTest, CanUpsertExitingSegment) { Segment segment_a; segment_a.version = 1; segment_a.key = "segmentA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -278,13 +278,13 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { +TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertSegment) { Segment segment_a; segment_a.version = 2; segment_a.key = "segmentA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -306,7 +306,7 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { +TEST(ChangeNotifierDestinationTest, ProducesChangeEventsOnUpsert) { Flag flag_a; Flag flag_b; @@ -319,7 +319,7 @@ TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -349,7 +349,7 @@ TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { EXPECT_EQ(true, got_event); } -TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { +TEST(ChangeNotifierDestinationTest, ProducesNoEventIfNoFlagChanged) { Flag flag_a; Flag flag_b; @@ -362,7 +362,7 @@ TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Segment segment_a; segment_a.version = 1; @@ -392,7 +392,7 @@ TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { EXPECT_EQ(false, got_event); } -TEST(DataStoreUpdaterTest, NoEventOnDiscardedUpsert) { +TEST(ChangeNotifierDestinationTest, NoEventOnDiscardedUpsert) { Flag flag_a; Flag flag_b; @@ -405,7 +405,7 @@ TEST(DataStoreUpdaterTest, NoEventOnDiscardedUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ From 3dfeebfa4d483f98ed65aa376fa61f037d2e62f3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 26 Oct 2023 11:25:58 -0700 Subject: [PATCH 073/244] refactor client's use of IDataSource interface --- libs/client-sdk/src/client_impl.cpp | 16 ++++----- libs/client-sdk/src/client_impl.hpp | 35 +++++++++---------- .../src/data_sources/data_source.hpp | 20 +++++++++++ .../data_source_event_handler.hpp | 1 - .../src/data_sources/null_data_source.hpp | 7 ++-- .../src/data_sources/polling_data_source.hpp | 12 +++---- .../data_sources/streaming_data_source.hpp | 9 +++-- 7 files changed, 58 insertions(+), 42 deletions(-) create mode 100644 libs/client-sdk/src/data_sources/data_source.hpp diff --git a/libs/client-sdk/src/client_impl.cpp b/libs/client-sdk/src/client_impl.cpp index 9d464673a..e77403519 100644 --- a/libs/client-sdk/src/client_impl.cpp +++ b/libs/client-sdk/src/client_impl.cpp @@ -30,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 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); diff --git a/libs/client-sdk/src/client_impl.hpp b/libs/client-sdk/src/client_impl.hpp index c273e2ad1..9577aa4bf 100644 --- a/libs/client-sdk/src/client_impl.hpp +++ b/libs/client-sdk/src/client_impl.hpp @@ -1,17 +1,8 @@ #pragma once -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include +#include "data_sources/data_source.hpp" +#include "data_sources/data_source_status_manager.hpp" +#include "flag_manager/flag_manager.hpp" #include #include @@ -19,15 +10,23 @@ #include #include #include -#include #include +#include #include #include -#include +#include +#include -#include "data_sources/data_source_status_manager.hpp" -#include "flag_manager/flag_manager.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include namespace launchdarkly::client_side { class ClientImpl : public IClient { @@ -130,10 +129,10 @@ class ClientImpl : public IClient { mutable std::shared_mutex context_mutex_; flag_manager::FlagManager flag_manager_; - std::function()> + std::function()> data_source_factory_; - std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; + std::shared_ptr data_source_; std::unique_ptr event_processor_; diff --git a/libs/client-sdk/src/data_sources/data_source.hpp b/libs/client-sdk/src/data_sources/data_source.hpp new file mode 100644 index 000000000..b448032f4 --- /dev/null +++ b/libs/client-sdk/src/data_sources/data_source.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +namespace launchdarkly::client_side::data_sources { + +class IDataSource { + public: + virtual void Start() = 0; + virtual void ShutdownAsync(std::function) = 0; + virtual ~IDataSource() = default; + IDataSource(IDataSource const& item) = delete; + IDataSource(IDataSource&& item) = delete; + IDataSource& operator=(IDataSource const&) = delete; + IDataSource& operator=(IDataSource&&) = delete; + + protected: + IDataSource() = default; +}; + +} // namespace launchdarkly::client_side::data_sources 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 90866d480..13721ad1b 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 @@ -8,7 +8,6 @@ #include #include #include -#include #include 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 e2fa7cc22..7eb82efc2 100644 --- a/libs/client-sdk/src/data_sources/null_data_source.hpp +++ b/libs/client-sdk/src/data_sources/null_data_source.hpp @@ -1,14 +1,13 @@ #pragma once -#include - +#include "data_source.hpp" #include "data_source_status_manager.hpp" -#include +#include namespace launchdarkly::client_side::data_sources { -class NullDataSource : public ::launchdarkly::data_sources::IDataSource { +class NullDataSource : public 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 62874b1f5..ab30e617b 100644 --- a/libs/client-sdk/src/data_sources/polling_data_source.hpp +++ b/libs/client-sdk/src/data_sources/polling_data_source.hpp @@ -1,9 +1,6 @@ #pragma once -#include - -#include - +#include "data_source.hpp" #include "data_source_event_handler.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" @@ -11,14 +8,17 @@ #include #include #include -#include #include #include +#include + +#include + namespace launchdarkly::client_side::data_sources { class PollingDataSource - : public ::launchdarkly::data_sources::IDataSource, + : public 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 430e77888..82c5b3f3a 100644 --- a/libs/client-sdk/src/data_sources/streaming_data_source.hpp +++ b/libs/client-sdk/src/data_sources/streaming_data_source.hpp @@ -1,10 +1,8 @@ #pragma once -#include -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" @@ -15,14 +13,15 @@ using namespace std::chrono_literals; #include #include #include -#include #include #include +#include + namespace launchdarkly::client_side::data_sources { class StreamingDataSource final - : public ::launchdarkly::data_sources::IDataSource, + : public IDataSource, public std::enable_shared_from_this { public: StreamingDataSource( From ab4a1fa1fc35ff17d629d6f06259770973ec3267 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 27 Oct 2023 09:07:26 -0700 Subject: [PATCH 074/244] adding tons of builders --- .../include/launchdarkly/config/server.hpp | 3 +- .../config/shared/builders/config_builder.hpp | 116 +++++++++++++++--- .../shared/builders/data_source_builder.hpp | 9 -- .../shared/builders/data_sources_builder.hpp | 63 ---------- .../data_system/background_sync_builder.hpp | 58 +++++++++ .../data_system/bootstrap_builder.hpp | 27 ++++ .../data_system/data_destination_builder.hpp | 30 +++++ .../data_system/data_systems_builder.hpp | 50 ++++++++ .../data_system/background_sync_config.hpp | 30 +++++ .../built/data_system/bootstrap_config.hpp | 8 ++ .../data_system/data_destination_config.hpp | 16 +++ .../built/data_system/data_system_config.hpp | 27 ++++ .../launchdarkly/config/shared/config.hpp | 21 +++- .../launchdarkly/config/shared/defaults.hpp | 12 ++ libs/common/src/CMakeLists.txt | 6 + libs/common/src/config/config.cpp | 12 +- libs/common/src/config/config_builder.cpp | 108 +++++++++++----- .../common/src/config/data_source_builder.cpp | 6 +- .../data_system/background_sync_builder.cpp | 36 ++++++ .../config/data_system/bootstrap_builder.cpp | 17 +++ .../data_system/data_destination_builder.cpp | 12 ++ .../data_system/data_system_builder.cpp | 18 +++ libs/common/tests/config_builder_test.cpp | 9 +- 23 files changed, 560 insertions(+), 134 deletions(-) delete mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp create mode 100644 libs/common/src/config/data_system/background_sync_builder.cpp create mode 100644 libs/common/src/config/data_system/bootstrap_builder.cpp create mode 100644 libs/common/src/config/data_system/data_destination_builder.cpp create mode 100644 libs/common/src/config/data_system/data_system_builder.cpp diff --git a/libs/common/include/launchdarkly/config/server.hpp b/libs/common/include/launchdarkly/config/server.hpp index d284983f1..9bb41e8b2 100644 --- a/libs/common/include/launchdarkly/config/server.hpp +++ b/libs/common/include/launchdarkly/config/server.hpp @@ -18,9 +18,8 @@ using ConfigBuilder = config::shared::builders::ConfigBuilder; using EventsBuilder = config::shared::builders::EventsBuilder; using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; -using DataSourceBuilder = config::shared::builders::DataSourceBuilder; +using DataSystemBuilder = config::shared::builders::DataSystemBuilder; using LoggingBuilder = config::shared::builders::LoggingBuilder; -using PersistenceBuilder = config::shared::builders::PersistenceBuilder; using Config = config::Config; diff --git a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp index 9345fe7e8..67f83fb09 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include @@ -17,14 +17,18 @@ namespace launchdarkly::config::shared::builders { +template +class ConfigBuilder {}; + /** * ConfigBuilder allows for creation of a Configuration object for use * in a Client. * @tparam SDK Type of SDK. */ -template -class ConfigBuilder { +template <> +class ConfigBuilder { public: + using SDK = ClientSDK; using Result = Config; using EndpointsBuilder = launchdarkly::config::shared::builders::EndpointsBuilder; @@ -32,14 +36,13 @@ class ConfigBuilder { launchdarkly::config::shared::builders::EventsBuilder; using DataSourceBuilder = launchdarkly::config::shared::builders::DataSourceBuilder; - using DataSourcesBuilder = - launchdarkly::config::shared::builders::DataSourcesBuilder; using HttpPropertiesBuilder = launchdarkly::config::shared::builders::HttpPropertiesBuilder; using PersistenceBuilder = launchdarkly::config::shared::builders::PersistenceBuilder; using LoggingBuilder = launchdarkly::config::shared::builders::LoggingBuilder; + /** * A minimal configuration consists of a LaunchDarkly SDK Key. * @param sdk_key SDK Key. @@ -49,7 +52,6 @@ class ConfigBuilder { /** * To customize the ServiceEndpoints the SDK uses for streaming, * polling, and events, pass in an EndpointsBuilder. - * @param builder An EndpointsBuilder. * @return Reference to an EndpointsBuilder. */ EndpointsBuilder& ServiceEndpoints(); @@ -57,7 +59,6 @@ class ConfigBuilder { /** * To include metadata about the application that is utilizing the SDK, * pass in an AppInfoBuilder. - * @param builder An AppInfoBuilder. * @return Reference to an AppInfoBuilder. */ AppInfoBuilder& AppInfo(); @@ -73,39 +74,32 @@ class ConfigBuilder { /** * To tune settings related to event generation and delivery, pass an * EventsBuilder. - * @param builder An EventsBuilder. * @return Reference to an EventsBuilder. */ EventsBuilder& Events(); /** * Sets the configuration of the component that receives feature flag data - * from LaunchDarkly. - * @param builder A DataSourceConfig builder. + * from LaunchDarkly. Client-side SDK only. * @return Reference to a DataSourceBuilder. */ DataSourceBuilder& DataSource(); - DataSourcesBuilder& DataSources(); - /** * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. * The builder has methods for setting individual HTTP-related properties. - * @param builder A HttpPropertiesBuilder builder. * @return Reference to an HttpPropertiesBuilder. */ HttpPropertiesBuilder& HttpProperties(); /** * Sets the logging configuration for the SDK. - * @param builder A Logging builder. * @return Reference to a LoggingBuilder. */ LoggingBuilder& Logging(); /** * Sets the persistence configuration for the SDK. - * @param builder A persistence builder. * @return Reference to a PersistenceBuilder. */ PersistenceBuilder& Persistence(); @@ -124,10 +118,100 @@ class ConfigBuilder { AppInfoBuilder app_info_builder_; EventsBuilder events_builder_; DataSourceBuilder data_source_builder_; - DataSourcesBuilder data_sources_builder_; HttpPropertiesBuilder http_properties_builder_; LoggingBuilder logging_config_builder_; PersistenceBuilder persistence_builder_; }; +template <> +class ConfigBuilder { + public: + using SDK = ServerSDK; + using Result = Config; + using EndpointsBuilder = + launchdarkly::config::shared::builders::EndpointsBuilder; + using EventsBuilder = + launchdarkly::config::shared::builders::EventsBuilder; + using DataSourceBuilder = + launchdarkly::config::shared::builders::DataSourceBuilder; + using HttpPropertiesBuilder = + launchdarkly::config::shared::builders::HttpPropertiesBuilder; + using PersistenceBuilder = + launchdarkly::config::shared::builders::PersistenceBuilder; + using LoggingBuilder = + launchdarkly::config::shared::builders::LoggingBuilder; + using DataSystemBuilder = + launchdarkly::config::shared::builders::DataSystemBuilder; + /** + * A minimal configuration consists of a LaunchDarkly SDK Key. + * @param sdk_key SDK Key. + */ + explicit ConfigBuilder(std::string sdk_key); + + /** + * Configures the data system for the SDK. Server-side SDK only. + * @return Reference to a DataSystemBuilder. + */ + DataSystemBuilder& DataSystem(); + + /** + * To customize the ServiceEndpoints the SDK uses for streaming, + * polling, and events, pass in an EndpointsBuilder. + * @return Reference to an EndpointsBuilder. + */ + EndpointsBuilder& ServiceEndpoints(); + + /** + * To include metadata about the application that is utilizing the SDK, + * pass in an AppInfoBuilder. + * @return Reference to an AppInfoBuilder. + */ + AppInfoBuilder& AppInfo(); + + /** + * Enables or disables "Offline" mode. True means + * Offline mode is enabled. + * @param offline True if the SDK should operate in Offline mode. + * @return Reference to this builder. + */ + ConfigBuilder& Offline(bool offline); + + /** + * To tune settings related to event generation and delivery, pass an + * EventsBuilder. + * @return Reference to an EventsBuilder. + */ + EventsBuilder& Events(); + + /** + * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. + * The builder has methods for setting individual HTTP-related properties. + * @return Reference to an HttpPropertiesBuilder. + */ + HttpPropertiesBuilder& HttpProperties(); + + /** + * Sets the logging configuration for the SDK. + * @return Reference to a LoggingBuilder. + */ + LoggingBuilder& Logging(); + + /** + * Builds a Configuration, suitable for passing into an instance of Client. + * @return + */ + tl::expected Build() const; + + private: + std::string sdk_key_; + std::optional offline_; + + EndpointsBuilder service_endpoints_builder_; + AppInfoBuilder app_info_builder_; + EventsBuilder events_builder_; + HttpPropertiesBuilder http_properties_builder_; + LoggingBuilder logging_config_builder_; + DataSystemBuilder data_system_builder_; +}; + } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp index f94642b32..395008883 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp @@ -198,12 +198,6 @@ class DataSourceBuilder { */ DataSourceBuilder& Method(Polling polling_builder); - DataSourceBuilder& Bootstrap(bool enable_bootstrap); - - DataSourceBuilder& Sync(bool enable_sync); - - DataSourceBuilder& Order(std::uint64_t bootstrap_order); - /** * Build a data source config. This is used internal to the SDK. * @@ -213,9 +207,6 @@ class DataSourceBuilder { private: std::variant method_; - bool enable_bootstrap_; - bool enable_sync_; - std::uint64_t order_; }; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp deleted file mode 100644 index 8fe52aa01..000000000 --- a/libs/common/include/launchdarkly/config/shared/builders/data_sources_builder.hpp +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace launchdarkly::config::shared::builders { - -/** - * Used to construct a DataSourcesConfiguration for the specified SDK type. - * @tparam SDK ClientSDK or ServerSDK. - */ -template -class DataSourcesBuilder; - -template <> -class DataSourcesBuilder { - public: - DataSourcesBuilder() {} -}; - -class BootstrapBuilder { - public: - BootstrapBuilder(); - enum class Order { ConsistentFirst = 0, Assigned = 1, Random = 2 }; - - using SeedType = std::int64_t; - - BootstrapBuilder& Order(Order order); - - BootstrapBuilder& RandomSeed(SeedType seed); - - private: - enum Order order_; - std::optional seed_; -}; - -template <> -class DataSourcesBuilder { - public: - DataSourcesBuilder(); - - DataSourceBuilder& Source(); - - DataSourcesBuilder& Destination(); - - BootstrapBuilder& Bootstrap(); - - [[nodiscard]] built::DataSourceConfig Build() const; - - private: - BootstrapBuilder bootstrap_; - std::vector> sources_; -}; - -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp new file mode 100644 index 000000000..254934322 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::config::shared::builders { + +template +struct BackgroundSyncBuilder {}; + +template <> +struct BackgroundSyncBuilder {}; + +template <> +struct BackgroundSyncBuilder { + using Streaming = DataSourceBuilder::Streaming; + using Polling = DataSourceBuilder::Polling; + + using DataDestinationBuilder = DataDestinationBuilder; + + BackgroundSyncBuilder(); + + BackgroundSyncBuilder& PrimaryBootstrapper(BootstrapBuilder bootstrap); + BackgroundSyncBuilder& FallbackBootstrapper(BootstrapBuilder bootstrap); + BackgroundSyncBuilder& Source(Streaming source); + BackgroundSyncBuilder& Source(Polling source); + BackgroundSyncBuilder& Destination(DataDestinationBuilder destination); + + [[nodiscard]] config::shared::built::BackgroundSyncConfig Build() + const; + + private: + built::BackgroundSyncConfig config_; + + // /* Will be the default LaunchDarkly bootstrapper or a custom one + // provided + // * by the user. */ + // BootstrapBuilder primary_bootstrapper_; + // + // /* Optional, as appropriate fallbacks might not be available or + // wanted. */ std::optional fallback_bootstrapper_; + // + // DataSourceBuilder source_; + // + // /* Optional, as there may be no need to mirror data anywhere. */ + // std::optional destination_; +}; + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp new file mode 100644 index 000000000..df5b279ea --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::config::shared::builders { + +class BootstrapBuilder { + public: + class DefaultBuilder {}; + + BootstrapBuilder(); + + BootstrapBuilder& Default(); + + [[nodiscard]] built::BootstrapConfig Build() const; + + private: + using BootstrapType = std::variant; + BootstrapType bootstrapper_; +}; +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp new file mode 100644 index 000000000..5dbb463b7 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::config::shared::builders { + +template +struct DataDestinationBuilder {}; + +template <> +struct DataDestinationBuilder {}; + +template <> +struct DataDestinationBuilder { + DataDestinationBuilder(); + + [[nodiscard]] config::shared::built::DataDestinationConfig + Build() const; + + private: + built::DataDestinationConfig config_; +}; + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp new file mode 100644 index 000000000..80f9a617a --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::config::shared::builders { + +/** + * Used to construct a DataSourcesConfiguration for the specified SDK type. + * @tparam SDK ClientSDK or ServerSDK. + */ +template +class DataSystemBuilder; + +/** Not used in client-side SDK yet. */ +template <> +class DataSystemBuilder { + public: + DataSystemBuilder() {} +}; + +template <> +class DataSystemBuilder { + public: + using BackgroundSyncBuilder = BackgroundSyncBuilder; + // using LazyLoadBuilder = LazyLoadBuilder; + + DataSystemBuilder(); + + // DataSystemBuilder& LazyLoad(LazyLoadBuilder lazy_load); + + DataSystemBuilder& BackgroundSync(BackgroundSyncBuilder background_sync); + + [[nodiscard]] built::DataSystemConfig Build() const; + + private: + built::DataSystemConfig config_; +}; + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp new file mode 100644 index 000000000..6ea4f588e --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::config::shared::built { + +template +struct BackgroundSyncConfig {}; + +template <> +struct BackgroundSyncConfig {}; + +template <> +struct BackgroundSyncConfig { + BootstrapConfig primary_bootstrapper_; + std::optional fallback_bootstrapper_; + DataSourceConfig source_; + DataDestinationConfig destination_; +}; + +} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp new file mode 100644 index 000000000..8604ddf0c --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace launchdarkly::config::shared::built { + +struct BootstrapConfig {}; +} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp new file mode 100644 index 000000000..74969135d --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace launchdarkly::config::shared::built { + +template +struct DataDestinationConfig {}; + +template <> +struct DataDestinationConfig {}; + +template <> +struct DataDestinationConfig {}; + +} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp new file mode 100644 index 000000000..764a71a1c --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::config::shared::built { + +template +struct DataSystemConfig; + +template <> +struct DataSystemConfig {}; + +template <> +struct DataSystemConfig { + std::variant< + /*LazyLoadConfig, */ BackgroundSyncConfig> + system_; +}; + +} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/config.hpp b/libs/common/include/launchdarkly/config/shared/config.hpp index a9332d41a..7d36b8bdb 100644 --- a/libs/common/include/launchdarkly/config/shared/config.hpp +++ b/libs/common/include/launchdarkly/config/shared/config.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -27,7 +28,8 @@ struct Config { std::optional application_tag, shared::built::DataSourceConfig data_source_config, shared::built::HttpProperties http_properties, - shared::built::Persistence persistence); + shared::built::Persistence persistence, + shared::built::DataSystemConfig data_system_config); [[nodiscard]] std::string const& SdkKey() const; @@ -38,8 +40,19 @@ struct Config { [[nodiscard]] std::optional const& ApplicationTag() const; - config::shared::built::DataSourceConfig const& DataSourceConfig() - const; + /** + * Client-side SDK only. + * @return + */ + [[nodiscard]] config::shared::built::DataSourceConfig const& + DataSourceConfig() const; + + /** + * Server-side SDK only. + * @return + */ + [[nodiscard]] config::shared::built::DataSystemConfig const& + DataSystemConfig() const; [[nodiscard]] shared::built::HttpProperties const& HttpProperties() const; @@ -59,6 +72,6 @@ struct Config { config::shared::built::DataSourceConfig data_source_config_; config::shared::built::HttpProperties http_properties_; shared::built::Persistence persistence_; + shared::built::DataSystemConfig data_system_config_; }; - } // namespace launchdarkly::config diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index b0e4ab2b2..6419f59d3 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -64,6 +65,12 @@ struct Defaults { return {Defaults::StreamingConfig(), false, false}; } + static auto DataSystemConfig() + -> shared::built::DataSystemConfig { + // No usage of DataSystem config yet until next major version. + return {}; + } + static auto PollingConfig() -> shared::built::PollingConfig { return {std::chrono::minutes(5), "/msdk/evalx/contexts", "/msdk/evalx/context", std::chrono::minutes(5)}; @@ -109,6 +116,11 @@ struct Defaults { return {Defaults::StreamingConfig()}; } + static auto DataSystemConfig() + -> shared::built::DataSystemConfig { + return {shared::built::BackgroundSyncConfig{}}; + } + static auto PollingConfig() -> shared::built::PollingConfig { return {std::chrono::seconds{30}, "/sdk/latest-all", std::chrono::seconds{30}}; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 0130ec962..2d10de51b 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -9,6 +9,8 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/config/shared/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/config/shared/builders/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/config/shared/built/*.hpp" + "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/config/shared/builders/data_system/*.hpp" + "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/config/shared/built/data_system/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/data/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/logging/*.hpp" "${LaunchDarklyCommonSdk_SOURCE_DIR}/include/launchdarkly/data_sources/*.hpp" @@ -38,6 +40,10 @@ add_library(${LIBNAME} OBJECT config/http_properties.cpp config/data_source_builder.cpp config/http_properties_builder.cpp + config/data_system/bootstrap_builder.cpp + config/data_system/background_sync_builder.cpp + config/data_system/data_destination_builder.cpp + config/data_system/data_system_builder.cpp bindings/c/value.cpp bindings/c/array_builder.cpp bindings/c/object_builder.cpp diff --git a/libs/common/src/config/config.cpp b/libs/common/src/config/config.cpp index 985f3925e..2ab95f052 100644 --- a/libs/common/src/config/config.cpp +++ b/libs/common/src/config/config.cpp @@ -12,7 +12,8 @@ Config::Config(std::string sdk_key, std::optional application_tag, shared::built::DataSourceConfig data_source_config, shared::built::HttpProperties http_properties, - shared::built::Persistence persistence) + shared::built::Persistence persistence, + shared::built::DataSystemConfig data_system_config) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), offline_(offline), @@ -21,7 +22,8 @@ Config::Config(std::string sdk_key, application_tag_(std::move(application_tag)), data_source_config_(std::move(data_source_config)), http_properties_(std::move(http_properties)), - persistence_(persistence) {} + persistence_(std::move(persistence)), + data_system_config_(std::move(data_system_config)) {} template std::string const& Config::SdkKey() const { @@ -49,6 +51,12 @@ shared::built::DataSourceConfig const& Config::DataSourceConfig() return data_source_config_; } +template +shared::built::DataSystemConfig const& Config::DataSystemConfig() + const { + return data_system_config_; +} + template shared::built::HttpProperties const& Config::HttpProperties() const { return http_properties_; diff --git a/libs/common/src/config/config_builder.cpp b/libs/common/src/config/config_builder.cpp index d7f594a20..22118b68a 100644 --- a/libs/common/src/config/config_builder.cpp +++ b/libs/common/src/config/config_builder.cpp @@ -3,63 +3,79 @@ namespace launchdarkly::config::shared::builders { -template -ConfigBuilder::ConfigBuilder(std::string sdk_key) +ConfigBuilder::ConfigBuilder(std::string sdk_key) : sdk_key_(std::move(sdk_key)) {} -template -typename ConfigBuilder::EndpointsBuilder& -ConfigBuilder::ServiceEndpoints() { +ConfigBuilder::ConfigBuilder(std::string sdk_key) + : sdk_key_(std::move(sdk_key)) {} + +typename ConfigBuilder::EndpointsBuilder& +ConfigBuilder::ServiceEndpoints() { + return service_endpoints_builder_; +} + +typename ConfigBuilder::EndpointsBuilder& +ConfigBuilder::ServiceEndpoints() { return service_endpoints_builder_; } -template -typename ConfigBuilder::EventsBuilder& ConfigBuilder::Events() { +typename ConfigBuilder::EventsBuilder& +ConfigBuilder::Events() { + return events_builder_; +} + +typename ConfigBuilder::EventsBuilder& +ConfigBuilder::Events() { return events_builder_; } -template -AppInfoBuilder& ConfigBuilder::AppInfo() { +AppInfoBuilder& ConfigBuilder::AppInfo() { + return app_info_builder_; +} + +AppInfoBuilder& ConfigBuilder::AppInfo() { return app_info_builder_; } -template -ConfigBuilder& ConfigBuilder::Offline(bool offline) { +ConfigBuilder& ConfigBuilder::Offline(bool offline) { offline_ = offline; return *this; } -template -typename ConfigBuilder::DataSourceBuilder& -ConfigBuilder::DataSource() { +typename ConfigBuilder::DataSourceBuilder& +ConfigBuilder::DataSource() { return data_source_builder_; } -template -typename ConfigBuilder::DataSourcesBuilder& -ConfigBuilder::DataSources() { - return data_sources_builder_; +typename ConfigBuilder::DataSystemBuilder& +ConfigBuilder::DataSystem() { + return data_system_builder_; } -template -typename ConfigBuilder::HttpPropertiesBuilder& -ConfigBuilder::HttpProperties() { +typename ConfigBuilder::HttpPropertiesBuilder& +ConfigBuilder::HttpProperties() { return http_properties_builder_; } -template -LoggingBuilder& ConfigBuilder::Logging() { +typename ConfigBuilder::HttpPropertiesBuilder& +ConfigBuilder::HttpProperties() { + return http_properties_builder_; +} + +LoggingBuilder& ConfigBuilder::Logging() { + return logging_config_builder_; +} + +LoggingBuilder& ConfigBuilder::Logging() { return logging_config_builder_; } -template -PersistenceBuilder& ConfigBuilder::Persistence() { +PersistenceBuilder& ConfigBuilder::Persistence() { return persistence_builder_; } -template -[[nodiscard]] tl::expected::Result, Error> -ConfigBuilder::Build() const { +[[nodiscard]] tl::expected::Result, Error> +ConfigBuilder::Build() const { auto sdk_key = sdk_key_; if (sdk_key.empty()) { return tl::make_unexpected(Error::kConfig_SDKKey_Empty); @@ -96,7 +112,39 @@ ConfigBuilder::Build() const { std::move(persistence)}; } -template class ConfigBuilder; -template class ConfigBuilder; +[[nodiscard]] tl::expected::Result, Error> +ConfigBuilder::Build() const { + auto sdk_key = sdk_key_; + if (sdk_key.empty()) { + return tl::make_unexpected(Error::kConfig_SDKKey_Empty); + } + auto offline = offline_.value_or(Defaults::Offline()); + auto endpoints_config = service_endpoints_builder_.Build(); + if (!endpoints_config) { + return tl::make_unexpected(endpoints_config.error()); + } + auto events_config = events_builder_.Build(); + if (!events_config) { + return tl::make_unexpected(events_config.error()); + } + + std::optional app_tag = app_info_builder_.Build(); + + auto data_system_config = data_system_builder_.Build(); + + auto http_properties = http_properties_builder_.Build(); + + auto logging = logging_config_builder_.Build(); + + return {tl::in_place, + sdk_key, + offline, + logging, + *endpoints_config, + *events_config, + app_tag, + std::move(data_system_config), + std::move(http_properties)}; +} } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 1105d1334..dc9aa08ba 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -66,11 +66,7 @@ built::DataSourceConfig DataSourceBuilder::Build() const { return {method, with_reasons_, use_report_}; } -DataSourceBuilder::DataSourceBuilder() - : method_(Streaming()), - enable_bootstrap_(true), - enable_sync_(false), - order_(0) {} +DataSourceBuilder::DataSourceBuilder() : method_(Streaming()) {} DataSourceBuilder& DataSourceBuilder::Method( StreamingBuilder builder) { diff --git a/libs/common/src/config/data_system/background_sync_builder.cpp b/libs/common/src/config/data_system/background_sync_builder.cpp new file mode 100644 index 000000000..274c7d495 --- /dev/null +++ b/libs/common/src/config/data_system/background_sync_builder.cpp @@ -0,0 +1,36 @@ +#include + +namespace launchdarkly::config::shared::builders { + +BackgroundSyncBuilder::BackgroundSyncBuilder() : config_() {} + +BackgroundSyncBuilder& BackgroundSyncBuilder< + ServerSDK>::PrimaryBootstrapper(BootstrapBuilder bootstrap) { + config_.primary_bootstrapper_ = bootstrap.Build(); + return *this; +} + +BackgroundSyncBuilder& BackgroundSyncBuilder< + ServerSDK>::FallbackBootstrapper(BootstrapBuilder bootstrap) { + config_.fallback_bootstrapper_ = bootstrap.Build(); + return *this; +} + +BackgroundSyncBuilder& BackgroundSyncBuilder::Source( + DataSourceBuilder source) { + config_.source_ = source.Build(); + return *this; +} + +BackgroundSyncBuilder& BackgroundSyncBuilder::Destination( + DataDestinationBuilder destination) { + config_.destination_ = destination.Build(); + return *this; +} + +[[nodiscard]] config::shared::built::BackgroundSyncConfig +BackgroundSyncBuilder::Build() const { + return config_; +} + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/bootstrap_builder.cpp b/libs/common/src/config/data_system/bootstrap_builder.cpp new file mode 100644 index 000000000..75f1f0067 --- /dev/null +++ b/libs/common/src/config/data_system/bootstrap_builder.cpp @@ -0,0 +1,17 @@ +#include + +namespace launchdarkly::config::shared::builders { + +BootstrapBuilder::BootstrapBuilder() : bootstrapper_() {} + +BootstrapBuilder& BootstrapBuilder::Default() { + bootstrapper_ = DefaultBuilder(); + return *this; +} + +built::BootstrapConfig BootstrapBuilder::Build() const { + return { + // TODO + }; +} +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/data_destination_builder.cpp b/libs/common/src/config/data_system/data_destination_builder.cpp new file mode 100644 index 000000000..7979020e5 --- /dev/null +++ b/libs/common/src/config/data_system/data_destination_builder.cpp @@ -0,0 +1,12 @@ +#include + +namespace launchdarkly::config::shared::builders { + +DataDestinationBuilder::DataDestinationBuilder() : config_() {} + +[[nodiscard]] config::shared::built::DataDestinationConfig +DataDestinationBuilder::Build() const { + return config_; +} + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/data_system_builder.cpp b/libs/common/src/config/data_system/data_system_builder.cpp new file mode 100644 index 000000000..73db2b5cf --- /dev/null +++ b/libs/common/src/config/data_system/data_system_builder.cpp @@ -0,0 +1,18 @@ +#include + +namespace launchdarkly::config::shared::builders { + +DataSystemBuilder::DataSystemBuilder() + : config_(Defaults::DataSystemConfig()) {} + +DataSystemBuilder& DataSystemBuilder::BackgroundSync( + BackgroundSyncBuilder builder) { + config_.system_ = builder.Build(); + return *this; +} + +built::DataSystemConfig DataSystemBuilder::Build() const { + return config_; +} + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/tests/config_builder_test.cpp b/libs/common/tests/config_builder_test.cpp index 3f4adeafb..3bdf5b581 100644 --- a/libs/common/tests/config_builder_test.cpp +++ b/libs/common/tests/config_builder_test.cpp @@ -94,11 +94,14 @@ TEST_F(ConfigBuilderTest, TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSource) { using namespace launchdarkly::server_side; + + using Sync = ConfigBuilder::DataSystemBuilder::BackgroundSyncBuilder; + ConfigBuilder builder("sdk-123"); - builder.DataSource().Method( - ConfigBuilder::DataSourceBuilder::Streaming().InitialReconnectDelay( - std::chrono::milliseconds{5000})); + builder.DataSystem().BackgroundSync( + Sync().Source(Sync::Streaming().InitialReconnectDelay( + std::chrono::milliseconds{5000}))); auto cfg = builder.Build(); From c1857d509e110844bd5a914cb340e8f69d798e03 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 3 Nov 2023 10:44:32 -0700 Subject: [PATCH 075/244] checkpoint: before refactoring config to be SDK specific --- .../include/launchdarkly/config/client.hpp | 2 +- .../include/launchdarkly/config/server.hpp | 2 +- .../launchdarkly/config/shared/config.hpp | 71 ++++++++++++++----- libs/common/src/config/config.cpp | 53 ++++++++------ 4 files changed, 88 insertions(+), 40 deletions(-) diff --git a/libs/common/include/launchdarkly/config/client.hpp b/libs/common/include/launchdarkly/config/client.hpp index 516c4b85b..e1828287b 100644 --- a/libs/common/include/launchdarkly/config/client.hpp +++ b/libs/common/include/launchdarkly/config/client.hpp @@ -23,6 +23,6 @@ using DataSourceBuilder = config::shared::builders::DataSourceBuilder; using LoggingBuilder = config::shared::builders::LoggingBuilder; using PersistenceBuilder = config::shared::builders::PersistenceBuilder; -using Config = config::Config; +using Config = config::shared::Config; } // namespace launchdarkly::client_side diff --git a/libs/common/include/launchdarkly/config/server.hpp b/libs/common/include/launchdarkly/config/server.hpp index 9bb41e8b2..daaffa67a 100644 --- a/libs/common/include/launchdarkly/config/server.hpp +++ b/libs/common/include/launchdarkly/config/server.hpp @@ -21,6 +21,6 @@ using HttpPropertiesBuilder = using DataSystemBuilder = config::shared::builders::DataSystemBuilder; using LoggingBuilder = config::shared::builders::LoggingBuilder; -using Config = config::Config; +using Config = config::shared::Config; } // namespace launchdarkly::server_side diff --git a/libs/common/include/launchdarkly/config/shared/config.hpp b/libs/common/include/launchdarkly/config/shared/config.hpp index 7d36b8bdb..fe96c1d40 100644 --- a/libs/common/include/launchdarkly/config/shared/config.hpp +++ b/libs/common/include/launchdarkly/config/shared/config.hpp @@ -9,17 +9,22 @@ #include #include #include +#include -namespace launchdarkly::config { +namespace launchdarkly::config::shared { /** * Config represents the configuration for a LaunchDarkly C++ SDK. - * It should be passed into an instance of Client. + * It should be passed into the SDK's constructor. * @tparam SDK Type of SDK. */ + template -struct Config { +struct Config {}; +template <> +struct Config { public: + using SDK = shared::ClientSDK; Config(std::string sdk_key, bool offline, shared::built::Logging logging, @@ -28,8 +33,7 @@ struct Config { std::optional application_tag, shared::built::DataSourceConfig data_source_config, shared::built::HttpProperties http_properties, - shared::built::Persistence persistence, - shared::built::DataSystemConfig data_system_config); + shared::built::Persistence persistence); [[nodiscard]] std::string const& SdkKey() const; @@ -40,20 +44,9 @@ struct Config { [[nodiscard]] std::optional const& ApplicationTag() const; - /** - * Client-side SDK only. - * @return - */ [[nodiscard]] config::shared::built::DataSourceConfig const& DataSourceConfig() const; - /** - * Server-side SDK only. - * @return - */ - [[nodiscard]] config::shared::built::DataSystemConfig const& - DataSystemConfig() const; - [[nodiscard]] shared::built::HttpProperties const& HttpProperties() const; [[nodiscard]] bool Offline() const; @@ -72,6 +65,50 @@ struct Config { config::shared::built::DataSourceConfig data_source_config_; config::shared::built::HttpProperties http_properties_; shared::built::Persistence persistence_; +}; + +template <> +struct Config { + public: + using SDK = shared::ServerSDK; + Config(std::string sdk_key, + bool offline, + shared::built::Logging logging, + shared::built::ServiceEndpoints endpoints, + shared::built::Events events, + std::optional application_tag, + shared::built::DataSystemConfig data_system_config, + shared::built::HttpProperties http_properties); + + [[nodiscard]] std::string const& SdkKey() const; + + [[nodiscard]] shared::built::ServiceEndpoints const& ServiceEndpoints() + const; + + [[nodiscard]] shared::built::Events const& Events() const; + + [[nodiscard]] std::optional const& ApplicationTag() const; + + [[nodiscard]] config::shared::built::DataSystemConfig const& + DataSystemConfig() const; + + [[nodiscard]] shared::built::HttpProperties const& HttpProperties() const; + + [[nodiscard]] bool Offline() const; + + [[nodiscard]] shared::built::Logging const& Logging() const; + + [[nodiscard]] shared::built::Persistence const& Persistence() const; + + private: + std::string sdk_key_; + bool offline_; + config::shared::built::Logging logging_; + config::shared::built::ServiceEndpoints service_endpoints_; + std::optional application_tag_; + config::shared::built::Events events_; shared::built::DataSystemConfig data_system_config_; + config::shared::built::HttpProperties http_properties_; }; -} // namespace launchdarkly::config + +} // namespace launchdarkly::config::shared diff --git a/libs/common/src/config/config.cpp b/libs/common/src/config/config.cpp index 2ab95f052..4900871c0 100644 --- a/libs/common/src/config/config.cpp +++ b/libs/common/src/config/config.cpp @@ -2,18 +2,19 @@ #include -namespace launchdarkly::config { -template -Config::Config(std::string sdk_key, - bool offline, - shared::built::Logging logging, - shared::built::ServiceEndpoints service_endpoints, - shared::built::Events events, - std::optional application_tag, - shared::built::DataSourceConfig data_source_config, - shared::built::HttpProperties http_properties, - shared::built::Persistence persistence, - shared::built::DataSystemConfig data_system_config) +namespace launchdarkly::config::shared { + +Config::Config( + std::string sdk_key, + bool offline, + shared::built::Logging logging, + shared::built::ServiceEndpoints service_endpoints, + shared::built::Events events, + std::optional application_tag, + shared::built::DataSourceConfig data_source_config, + shared::built::HttpProperties http_properties, + shared::built::Persistence persistence, + shared::built::DataSystemConfig data_system_config) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), offline_(offline), @@ -22,21 +23,31 @@ Config::Config(std::string sdk_key, application_tag_(std::move(application_tag)), data_source_config_(std::move(data_source_config)), http_properties_(std::move(http_properties)), - persistence_(std::move(persistence)), - data_system_config_(std::move(data_system_config)) {} + persistence_(std::move(persistence)) {} -template -std::string const& Config::SdkKey() const { +std::string const& Config::SdkKey() const { return sdk_key_; } -template -shared::built::ServiceEndpoints const& Config::ServiceEndpoints() const { +std::string const& Config::SdkKey() const { + return sdk_key_; +} + +shared::built::ServiceEndpoints const& +Config::ServiceEndpoints() const { return service_endpoints_; } -template -shared::built::Events const& Config::Events() const { +shared::built::ServiceEndpoints const& +Config::ServiceEndpoints() const { + return service_endpoints_; +} + +shared::built::Events const& Config::Events() const { + return events_; +} + +shared::built::Events const& Config::Events() const { return events_; } @@ -80,4 +91,4 @@ shared::built::Persistence const& Config::Persistence() const { template class Config; template class Config; -} // namespace launchdarkly::config +} // namespace launchdarkly::config::shared From 271d1b59cefdde6166f390264c46762d26a05e2d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 3 Nov 2023 12:05:38 -0700 Subject: [PATCH 076/244] feat: refactor server side config Server-side config is no longer templated and now lives in the local server_side include directories. --- examples/hello-cpp-server/main.cpp | 1 + .../include/launchdarkly/config/server.hpp | 27 ----- libs/common/src/config/config_builder.cpp | 1 - .../launchdarkly/server_side/client.hpp | 2 +- .../server_side/config/config.hpp | 57 ++++++++++ .../server_side/config/config_builder.hpp | 101 ++++++++++++++++++ libs/server-sdk/src/CMakeLists.txt | 2 + libs/server-sdk/src/bindings/c/builder.cpp | 2 +- libs/server-sdk/src/bindings/c/config.cpp | 2 +- libs/server-sdk/src/client_impl.cpp | 4 +- libs/server-sdk/src/config/config.cpp | 56 ++++++++++ libs/server-sdk/src/config/config_builder.cpp | 72 +++++++++++++ 12 files changed, 294 insertions(+), 33 deletions(-) delete mode 100644 libs/common/include/launchdarkly/config/server.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/config.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp create mode 100644 libs/server-sdk/src/config/config.cpp create mode 100644 libs/server-sdk/src/config/config_builder.cpp diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index 1f975ded5..ce2191b4d 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include diff --git a/libs/common/include/launchdarkly/config/server.hpp b/libs/common/include/launchdarkly/config/server.hpp deleted file mode 100644 index d284983f1..000000000 --- a/libs/common/include/launchdarkly/config/server.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace launchdarkly::server_side { - -using SDK = config::shared::ServerSDK; - -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; -using DataSourceBuilder = config::shared::builders::DataSourceBuilder; -using LoggingBuilder = config::shared::builders::LoggingBuilder; -using PersistenceBuilder = config::shared::builders::PersistenceBuilder; - -using Config = config::Config; - -} // namespace launchdarkly::server_side diff --git a/libs/common/src/config/config_builder.cpp b/libs/common/src/config/config_builder.cpp index 5e58e3b89..0c306cb1f 100644 --- a/libs/common/src/config/config_builder.cpp +++ b/libs/common/src/config/config_builder.cpp @@ -91,6 +91,5 @@ ConfigBuilder::Build() const { } template class ConfigBuilder; -template class ConfigBuilder; } // namespace launchdarkly::config::shared::builders diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index e7f1b8056..2dfcaaf2e 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -1,8 +1,8 @@ #pragma once -#include #include #include +#include #include #include diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp new file mode 100644 index 000000000..70f19b19a --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +using SDK = config::shared::ServerSDK; + +struct Config { + public: + Config(std::string sdk_key, + bool offline, + config::shared::built::Logging logging, + config::shared::built::ServiceEndpoints endpoints, + config::shared::built::Events events, + std::optional application_tag, + config::shared::built::DataSourceConfig data_source_config, + config::shared::built::HttpProperties http_properties); + + [[nodiscard]] std::string const& SdkKey() const; + + [[nodiscard]] config::shared::built::ServiceEndpoints const& + ServiceEndpoints() const; + + [[nodiscard]] config::shared::built::Events const& Events() const; + + [[nodiscard]] std::optional const& ApplicationTag() const; + + config::shared::built::DataSourceConfig const& DataSourceConfig() + const; + + [[nodiscard]] config::shared::built::HttpProperties const& HttpProperties() + const; + + [[nodiscard]] bool Offline() const; + + [[nodiscard]] config::shared::built::Logging const& Logging() const; + + private: + std::string sdk_key_; + bool offline_; + config::shared::built::Logging logging_; + config::shared::built::ServiceEndpoints service_endpoints_; + std::optional application_tag_; + config::shared::built::Events events_; + config::shared::built::DataSourceConfig data_source_config_; + config::shared::built::HttpProperties http_properties_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp new file mode 100644 index 000000000..b9fc16648 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -0,0 +1,101 @@ +#pragma once +#include +#include "launchdarkly/config/shared/builders/app_info_builder.hpp" +#include "launchdarkly/config/shared/builders/data_source_builder.hpp" +#include "launchdarkly/config/shared/builders/http_properties_builder.hpp" +#include "launchdarkly/config/shared/builders/logging_builder.hpp" +#include "launchdarkly/config/shared/defaults.hpp" + +namespace launchdarkly::server_side { + +using Defaults = config::shared::Defaults; +using AppInfoBuilder = config::shared::builders::AppInfoBuilder; +using EndpointsBuilder = config::shared::builders::EndpointsBuilder; +using EventsBuilder = config::shared::builders::EventsBuilder; +using HttpPropertiesBuilder = + config::shared::builders::HttpPropertiesBuilder; +using DataSourceBuilder = config::shared::builders::DataSourceBuilder; +using LoggingBuilder = config::shared::builders::LoggingBuilder; + +class ConfigBuilder { + public: + using Result = Config; + /** + * A minimal configuration consists of a LaunchDarkly SDK Key. + * @param sdk_key SDK Key. + */ + explicit ConfigBuilder(std::string sdk_key); + + /** + * To customize the ServiceEndpoints the SDK uses for streaming, + * polling, and events, pass in an EndpointsBuilder. + * @param builder An EndpointsBuilder. + * @return Reference to an EndpointsBuilder. + */ + EndpointsBuilder& ServiceEndpoints(); + + /** + * To include metadata about the application that is utilizing the SDK, + * pass in an AppInfoBuilder. + * @param builder An AppInfoBuilder. + * @return Reference to an AppInfoBuilder. + */ + AppInfoBuilder& AppInfo(); + + /** + * Enables or disables "Offline" mode. True means + * Offline mode is enabled. + * @param offline True if the SDK should operate in Offline mode. + * @return Reference to this builder. + */ + ConfigBuilder& Offline(bool offline); + + /** + * To tune settings related to event generation and delivery, pass an + * EventsBuilder. + * @param builder An EventsBuilder. + * @return Reference to an EventsBuilder. + */ + EventsBuilder& Events(); + + /** + * Sets the configuration of the component that receives feature flag data + * from LaunchDarkly. + * @param builder A DataSourceConfig builder. + * @return Reference to a DataSourceBuilder. + */ + DataSourceBuilder& DataSource(); + + /** + * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. + * The builder has methods for setting individual HTTP-related properties. + * @param builder A HttpPropertiesBuilder builder. + * @return Reference to an HttpPropertiesBuilder. + */ + HttpPropertiesBuilder& HttpProperties(); + + /** + * Sets the logging configuration for the SDK. + * @param builder A Logging builder. + * @return Reference to a LoggingBuilder. + */ + LoggingBuilder& Logging(); + + /** + * Builds a Configuration, suitable for passing into an instance of Client. + * @return + */ + tl::expected Build() const; + + private: + std::string sdk_key_; + std::optional offline_; + + EndpointsBuilder service_endpoints_builder_; + AppInfoBuilder app_info_builder_; + EventsBuilder events_builder_; + DataSourceBuilder data_source_builder_; + HttpPropertiesBuilder http_properties_builder_; + LoggingBuilder logging_config_builder_; +}; +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 00569be24..5f234d148 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -19,6 +19,8 @@ target_sources(${LIBNAME} boost.cpp client.cpp client_impl.cpp + config/config.cpp + config/config_builder.cpp all_flags_state/all_flags_state.cpp all_flags_state/json_all_flags_state.cpp all_flags_state/all_flags_state_builder.cpp diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index 461bcf204..fe8037413 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -3,8 +3,8 @@ #include -#include #include +#include using namespace launchdarkly::server_side; diff --git a/libs/server-sdk/src/bindings/c/config.cpp b/libs/server-sdk/src/bindings/c/config.cpp index 72b23bace..27618baa6 100644 --- a/libs/server-sdk/src/bindings/c/config.cpp +++ b/libs/server-sdk/src/bindings/c/config.cpp @@ -2,7 +2,7 @@ // NOLINTBEGIN OCInconsistentNamingInspection #include -#include +#include #define TO_CONFIG(ptr) (reinterpret_cast(ptr)) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 5dfc5b56b..0845f4f6b 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -4,9 +4,8 @@ #include #include -#include "client_impl.hpp" - #include "all_flags_state/all_flags_state_builder.hpp" +#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" @@ -17,6 +16,7 @@ #include #include #include +#include namespace launchdarkly::server_side { diff --git a/libs/server-sdk/src/config/config.cpp b/libs/server-sdk/src/config/config.cpp new file mode 100644 index 000000000..0427d39b3 --- /dev/null +++ b/libs/server-sdk/src/config/config.cpp @@ -0,0 +1,56 @@ +#include + +namespace launchdarkly::server_side { + +Config::Config(std::string sdk_key, + bool offline, + config::shared::built::Logging logging, + config::shared::built::ServiceEndpoints service_endpoints, + config::shared::built::Events events, + std::optional application_tag, + config::shared::built::DataSourceConfig data_source_config, + config::shared::built::HttpProperties http_properties) + : sdk_key_(std::move(sdk_key)), + logging_(std::move(logging)), + offline_(offline), + service_endpoints_(std::move(service_endpoints)), + events_(std::move(events)), + application_tag_(std::move(application_tag)), + data_source_config_(std::move(data_source_config)), + http_properties_(std::move(http_properties)) {} + +std::string const& Config::SdkKey() const { + return sdk_key_; +} + +config::shared::built::ServiceEndpoints const& Config::ServiceEndpoints() + const { + return service_endpoints_; +} + +config::shared::built::Events const& Config::Events() const { + return events_; +} + +std::optional const& Config::ApplicationTag() const { + return application_tag_; +} + +config::shared::built::DataSourceConfig const& +Config::DataSourceConfig() const { + return data_source_config_; +} + +config::shared::built::HttpProperties const& Config::HttpProperties() const { + return http_properties_; +} + +bool Config::Offline() const { + return offline_; +} + +config::shared::built::Logging const& Config::Logging() const { + return logging_; +} + +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp new file mode 100644 index 000000000..ad6603b94 --- /dev/null +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -0,0 +1,72 @@ +#include +#include "launchdarkly/config/shared/defaults.hpp" + +namespace launchdarkly::server_side { + +ConfigBuilder::ConfigBuilder(std::string sdk_key) + : sdk_key_(std::move(sdk_key)) {} + +EndpointsBuilder& ConfigBuilder::ServiceEndpoints() { + return service_endpoints_builder_; +} + +EventsBuilder& ConfigBuilder::Events() { + return events_builder_; +} + +AppInfoBuilder& ConfigBuilder::AppInfo() { + return app_info_builder_; +} + +ConfigBuilder& ConfigBuilder::Offline(bool offline) { + offline_ = offline; + return *this; +} + +DataSourceBuilder& ConfigBuilder::DataSource() { + return data_source_builder_; +} + +HttpPropertiesBuilder& ConfigBuilder::HttpProperties() { + return http_properties_builder_; +} + +LoggingBuilder& ConfigBuilder::Logging() { + return logging_config_builder_; +} + +tl::expected ConfigBuilder::Build() const { + auto sdk_key = sdk_key_; + if (sdk_key.empty()) { + return tl::make_unexpected(Error::kConfig_SDKKey_Empty); + } + auto offline = offline_.value_or(Defaults::Offline()); + auto endpoints_config = service_endpoints_builder_.Build(); + if (!endpoints_config) { + return tl::make_unexpected(endpoints_config.error()); + } + auto events_config = events_builder_.Build(); + if (!events_config) { + return tl::make_unexpected(events_config.error()); + } + + std::optional app_tag = app_info_builder_.Build(); + + auto data_source_config = data_source_builder_.Build(); + + auto http_properties = http_properties_builder_.Build(); + + auto logging = logging_config_builder_.Build(); + + return {tl::in_place, + sdk_key, + offline, + logging, + *endpoints_config, + *events_config, + app_tag, + std::move(data_source_config), + std::move(http_properties)}; +} + +} // namespace launchdarkly::server_side From bee9be3e30ad19fb26bb0daa8769ed11d034c45c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 3 Nov 2023 14:53:57 -0700 Subject: [PATCH 077/244] move unit tests from common to lib/server --- libs/common/tests/config_builder_test.cpp | 48 -------------- libs/server-sdk/tests/client_test.cpp | 11 ++-- libs/server-sdk/tests/config_builder_test.cpp | 65 +++++++++++++++++++ 3 files changed, 71 insertions(+), 53 deletions(-) create mode 100644 libs/server-sdk/tests/config_builder_test.cpp diff --git a/libs/common/tests/config_builder_test.cpp b/libs/common/tests/config_builder_test.cpp index 3f4adeafb..19cf7129d 100644 --- a/libs/common/tests/config_builder_test.cpp +++ b/libs/common/tests/config_builder_test.cpp @@ -1,7 +1,6 @@ #include #include -#include #include class ConfigBuilderTest @@ -20,14 +19,6 @@ TEST_F(ConfigBuilderTest, DefaultConstruction_ClientConfig) { ASSERT_EQ(cfg->SdkKey(), "sdk-123"); } -TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig) { - using namespace launchdarkly::server_side; - ConfigBuilder builder("sdk-123"); - auto cfg = builder.Build(); - ASSERT_TRUE(cfg); - ASSERT_EQ(cfg->SdkKey(), "sdk-123"); -} - TEST_F(ConfigBuilderTest, DefaultConstruction_UsesDefaultEndpointsIfNotSupplied) { using namespace launchdarkly::client_side; @@ -78,37 +69,6 @@ TEST_F(ConfigBuilderTest, .initial_reconnect_delay); } -TEST_F(ConfigBuilderTest, - DefaultConstruction_ServerConfig_UsesDefaulDataSourceConfig) { - using namespace launchdarkly::server_side; - ConfigBuilder builder("sdk-123"); - auto cfg = builder.Build(); - - // Should be streaming with a 1 second initial retry; - EXPECT_EQ(std::chrono::milliseconds{1000}, - std::get>( - cfg->DataSourceConfig().method) - .initial_reconnect_delay); -} - -TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSource) { - using namespace launchdarkly::server_side; - ConfigBuilder builder("sdk-123"); - - builder.DataSource().Method( - ConfigBuilder::DataSourceBuilder::Streaming().InitialReconnectDelay( - std::chrono::milliseconds{5000})); - - auto cfg = builder.Build(); - - EXPECT_EQ(std::chrono::milliseconds{5000}, - std::get>( - cfg->DataSourceConfig().method) - .initial_reconnect_delay); -} - TEST_F(ConfigBuilderTest, ClientConfig_CanSetDataSource) { using namespace launchdarkly::client_side; ConfigBuilder builder("sdk-123"); @@ -141,14 +101,6 @@ TEST_F(ConfigBuilderTest, ASSERT_EQ(cfg->HttpProperties(), Defaults::HttpProperties()); } -TEST_F(ConfigBuilderTest, - DefaultConstruction_ServerConfig_UsesDefaultHttpProperties) { - using namespace launchdarkly::server_side; - ConfigBuilder builder("sdk-123"); - auto cfg = builder.Build(); - ASSERT_EQ(cfg->HttpProperties(), Defaults::HttpProperties()); -} - TEST_F(ConfigBuilderTest, DefaultConstruction_CanSetHttpProperties) { using namespace launchdarkly::client_side; ConfigBuilder builder("sdk-123"); diff --git a/libs/server-sdk/tests/client_test.cpp b/libs/server-sdk/tests/client_test.cpp index 73d0c0dd7..28d4b82da 100644 --- a/libs/server-sdk/tests/client_test.cpp +++ b/libs/server-sdk/tests/client_test.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include using namespace launchdarkly; @@ -23,7 +24,7 @@ TEST_F(ClientTest, ClientConstructedWithMinimalConfigAndContextT) { } TEST_F(ClientTest, BoolVariationDefaultPassesThrough) { - const std::string flag = "extra-cat-food"; + std::string const flag = "extra-cat-food"; std::vector values = {true, false}; for (auto const& v : values) { ASSERT_EQ(client_.BoolVariation(context_, flag, v), v); @@ -32,7 +33,7 @@ TEST_F(ClientTest, BoolVariationDefaultPassesThrough) { } TEST_F(ClientTest, StringVariationDefaultPassesThrough) { - const std::string flag = "treat"; + std::string const flag = "treat"; std::vector values = {"chicken", "fish", "cat-grass"}; for (auto const& v : values) { ASSERT_EQ(client_.StringVariation(context_, flag, v), v); @@ -41,7 +42,7 @@ TEST_F(ClientTest, StringVariationDefaultPassesThrough) { } TEST_F(ClientTest, IntVariationDefaultPassesThrough) { - const std::string flag = "weight"; + std::string const flag = "weight"; std::vector values = {0, 12, 13, 24, 1000}; for (auto const& v : values) { ASSERT_EQ(client_.IntVariation(context_, flag, v), v); @@ -50,7 +51,7 @@ TEST_F(ClientTest, IntVariationDefaultPassesThrough) { } TEST_F(ClientTest, DoubleVariationDefaultPassesThrough) { - const std::string flag = "weight"; + std::string const 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); @@ -59,7 +60,7 @@ TEST_F(ClientTest, DoubleVariationDefaultPassesThrough) { } TEST_F(ClientTest, JsonVariationDefaultPassesThrough) { - const std::string flag = "assorted-values"; + std::string const flag = "assorted-values"; std::vector values = { Value({"running", "jumping"}), Value(3), Value(1.0), Value(true), Value(std::map{{"weight", 20}})}; diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp new file mode 100644 index 000000000..c88990163 --- /dev/null +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include + +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +class ConfigBuilderTest + : public ::testing:: + Test { // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) + protected: + launchdarkly::Logger logger; + ConfigBuilderTest() : logger(launchdarkly::logging::NullLogger()) {} +}; + +TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig) { + using namespace launchdarkly::server_side; + ConfigBuilder builder("sdk-123"); + auto cfg = builder.Build(); + ASSERT_TRUE(cfg); + ASSERT_EQ(cfg->SdkKey(), "sdk-123"); +} + +TEST_F(ConfigBuilderTest, + DefaultConstruction_ServerConfig_UsesDefaulDataSourceConfig) { + using namespace launchdarkly::server_side; + ConfigBuilder builder("sdk-123"); + auto cfg = builder.Build(); + + // Should be streaming with a 1 second initial retry; + EXPECT_EQ(std::chrono::milliseconds{1000}, + std::get>( + cfg->DataSourceConfig().method) + .initial_reconnect_delay); +} + +TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSource) { + using namespace launchdarkly::server_side; + ConfigBuilder builder("sdk-123"); + + builder.DataSource().Method( + DataSourceBuilder::Streaming().InitialReconnectDelay( + std::chrono::milliseconds{5000})); + + auto cfg = builder.Build(); + + EXPECT_EQ(std::chrono::milliseconds{5000}, + std::get>( + cfg->DataSourceConfig().method) + .initial_reconnect_delay); +} + +TEST_F(ConfigBuilderTest, + DefaultConstruction_ServerConfig_UsesDefaultHttpProperties) { + using namespace launchdarkly::server_side; + ConfigBuilder builder("sdk-123"); + auto cfg = builder.Build(); + ASSERT_EQ(cfg->HttpProperties(), Defaults::HttpProperties()); +} From 771fe74ff6a158e684928ffd51d8b7bb81129c2e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 3 Nov 2023 15:10:42 -0700 Subject: [PATCH 078/244] fix tests --- .../launchdarkly/config/server_builders.hpp | 24 +++++++++++++++++++ .../common/tests/data_source_builder_test.cpp | 2 +- libs/common/tests/service_endpoints_test.cpp | 2 +- .../server_side/config/config_builder.hpp | 10 +------- 4 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 libs/common/include/launchdarkly/config/server_builders.hpp diff --git a/libs/common/include/launchdarkly/config/server_builders.hpp b/libs/common/include/launchdarkly/config/server_builders.hpp new file mode 100644 index 000000000..453199bf8 --- /dev/null +++ b/libs/common/include/launchdarkly/config/server_builders.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side { + +using SDK = config::shared::ServerSDK; + +using Defaults = config::shared::Defaults; +using AppInfoBuilder = config::shared::builders::AppInfoBuilder; +using EndpointsBuilder = config::shared::builders::EndpointsBuilder; +using EventsBuilder = config::shared::builders::EventsBuilder; +using HttpPropertiesBuilder = + config::shared::builders::HttpPropertiesBuilder; +using DataSourceBuilder = config::shared::builders::DataSourceBuilder; +using LoggingBuilder = config::shared::builders::LoggingBuilder; + +} // namespace launchdarkly::server_side diff --git a/libs/common/tests/data_source_builder_test.cpp b/libs/common/tests/data_source_builder_test.cpp index 1c5f896c9..795a6f937 100644 --- a/libs/common/tests/data_source_builder_test.cpp +++ b/libs/common/tests/data_source_builder_test.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include #include diff --git a/libs/common/tests/service_endpoints_test.cpp b/libs/common/tests/service_endpoints_test.cpp index 080070c0a..bd593352e 100644 --- a/libs/common/tests/service_endpoints_test.cpp +++ b/libs/common/tests/service_endpoints_test.cpp @@ -1,7 +1,7 @@ #include #include -#include +#include #include class ServiceEndpointTest : public testing::Test {}; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index b9fc16648..8ed1d66d2 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include "launchdarkly/config/shared/builders/app_info_builder.hpp" #include "launchdarkly/config/shared/builders/data_source_builder.hpp" @@ -8,15 +9,6 @@ namespace launchdarkly::server_side { -using Defaults = config::shared::Defaults; -using AppInfoBuilder = config::shared::builders::AppInfoBuilder; -using EndpointsBuilder = config::shared::builders::EndpointsBuilder; -using EventsBuilder = config::shared::builders::EventsBuilder; -using HttpPropertiesBuilder = - config::shared::builders::HttpPropertiesBuilder; -using DataSourceBuilder = config::shared::builders::DataSourceBuilder; -using LoggingBuilder = config::shared::builders::LoggingBuilder; - class ConfigBuilder { public: using Result = Config; From 54ecdce826ed1566e0e131cdedae6026481757b2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 3 Nov 2023 15:28:30 -0700 Subject: [PATCH 079/244] contract tests --- contract-tests/server-contract-tests/src/entity_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index 8e8235036..c7db150fb 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -1,9 +1,9 @@ #include "entity_manager.hpp" #include -#include #include #include +#include using launchdarkly::LogLevel; using namespace launchdarkly::server_side; From 7ccd3c9a487f1b7e909f9c29b4ebafb690447aa7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 10:55:59 -0800 Subject: [PATCH 080/244] swap DataSourceBuilder with DataSystemBuilder in server config --- .../server_side/config/config.hpp | 3 -- .../server_side/config/config_builder.hpp | 21 ++++----- libs/server-sdk/src/bindings/c/builder.cpp | 46 +++++++++---------- 3 files changed, 33 insertions(+), 37 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp index 70f19b19a..ff2e402d5 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -1,12 +1,9 @@ #pragma once -#include -#include #include #include #include #include -#include #include #include diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 8ed1d66d2..0f5932ce3 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -1,11 +1,10 @@ #pragma once #include +#include +#include +#include +#include #include -#include "launchdarkly/config/shared/builders/app_info_builder.hpp" -#include "launchdarkly/config/shared/builders/data_source_builder.hpp" -#include "launchdarkly/config/shared/builders/http_properties_builder.hpp" -#include "launchdarkly/config/shared/builders/logging_builder.hpp" -#include "launchdarkly/config/shared/defaults.hpp" namespace launchdarkly::server_side { @@ -51,12 +50,12 @@ class ConfigBuilder { EventsBuilder& Events(); /** - * Sets the configuration of the component that receives feature flag data - * from LaunchDarkly. - * @param builder A DataSourceConfig builder. - * @return Reference to a DataSourceBuilder. + * Sets the configuration of the component that receives and stores feature + * flag data from LaunchDarkly. + * @param builder A DataSystemBuilder. + * @return Reference to a DataSystemBuilder. */ - DataSourceBuilder& DataSource(); + DataSystemBuilder& DataSource(); /** * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. @@ -86,7 +85,7 @@ class ConfigBuilder { EndpointsBuilder service_endpoints_builder_; AppInfoBuilder app_info_builder_; EventsBuilder events_builder_; - DataSourceBuilder data_source_builder_; + DataSystemBuilder data_system_builder_; HttpPropertiesBuilder http_properties_builder_; LoggingBuilder logging_config_builder_; }; diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index fe8037413..3412b80d8 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -164,29 +164,29 @@ LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder 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(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() { From 8d6417798768f01077be22b6c88cfba09eeac0a3 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 11:07:03 -0800 Subject: [PATCH 081/244] revert changes to common config --- .../launchdarkly/config/server_builders.hpp | 1 + .../config/shared/builders/config_builder.hpp | 109 ++---------------- .../server_side/config/config_builder.hpp | 2 +- libs/server-sdk/src/config/config_builder.cpp | 4 +- 4 files changed, 14 insertions(+), 102 deletions(-) diff --git a/libs/common/include/launchdarkly/config/server_builders.hpp b/libs/common/include/launchdarkly/config/server_builders.hpp index 31094de58..bb03ea675 100644 --- a/libs/common/include/launchdarkly/config/server_builders.hpp +++ b/libs/common/include/launchdarkly/config/server_builders.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include diff --git a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp index 67f83fb09..db81c24e2 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/config_builder.hpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -17,18 +16,14 @@ namespace launchdarkly::config::shared::builders { -template -class ConfigBuilder {}; - /** * ConfigBuilder allows for creation of a Configuration object for use * in a Client. * @tparam SDK Type of SDK. */ -template <> -class ConfigBuilder { +template +class ConfigBuilder { public: - using SDK = ClientSDK; using Result = Config; using EndpointsBuilder = launchdarkly::config::shared::builders::EndpointsBuilder; @@ -52,6 +47,7 @@ class ConfigBuilder { /** * To customize the ServiceEndpoints the SDK uses for streaming, * polling, and events, pass in an EndpointsBuilder. + * @param builder An EndpointsBuilder. * @return Reference to an EndpointsBuilder. */ EndpointsBuilder& ServiceEndpoints(); @@ -59,6 +55,7 @@ class ConfigBuilder { /** * To include metadata about the application that is utilizing the SDK, * pass in an AppInfoBuilder. + * @param builder An AppInfoBuilder. * @return Reference to an AppInfoBuilder. */ AppInfoBuilder& AppInfo(); @@ -74,13 +71,15 @@ class ConfigBuilder { /** * To tune settings related to event generation and delivery, pass an * EventsBuilder. + * @param builder An EventsBuilder. * @return Reference to an EventsBuilder. */ EventsBuilder& Events(); /** * Sets the configuration of the component that receives feature flag data - * from LaunchDarkly. Client-side SDK only. + * from LaunchDarkly. + * @param builder A DataSourceConfig builder. * @return Reference to a DataSourceBuilder. */ DataSourceBuilder& DataSource(); @@ -88,18 +87,21 @@ class ConfigBuilder { /** * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. * The builder has methods for setting individual HTTP-related properties. + * @param builder A HttpPropertiesBuilder builder. * @return Reference to an HttpPropertiesBuilder. */ HttpPropertiesBuilder& HttpProperties(); /** * Sets the logging configuration for the SDK. + * @param builder A Logging builder. * @return Reference to a LoggingBuilder. */ LoggingBuilder& Logging(); /** * Sets the persistence configuration for the SDK. + * @param builder A persistence builder. * @return Reference to a PersistenceBuilder. */ PersistenceBuilder& Persistence(); @@ -123,95 +125,4 @@ class ConfigBuilder { PersistenceBuilder persistence_builder_; }; -template <> -class ConfigBuilder { - public: - using SDK = ServerSDK; - using Result = Config; - using EndpointsBuilder = - launchdarkly::config::shared::builders::EndpointsBuilder; - using EventsBuilder = - launchdarkly::config::shared::builders::EventsBuilder; - using DataSourceBuilder = - launchdarkly::config::shared::builders::DataSourceBuilder; - using HttpPropertiesBuilder = - launchdarkly::config::shared::builders::HttpPropertiesBuilder; - using PersistenceBuilder = - launchdarkly::config::shared::builders::PersistenceBuilder; - using LoggingBuilder = - launchdarkly::config::shared::builders::LoggingBuilder; - using DataSystemBuilder = - launchdarkly::config::shared::builders::DataSystemBuilder; - /** - * A minimal configuration consists of a LaunchDarkly SDK Key. - * @param sdk_key SDK Key. - */ - explicit ConfigBuilder(std::string sdk_key); - - /** - * Configures the data system for the SDK. Server-side SDK only. - * @return Reference to a DataSystemBuilder. - */ - DataSystemBuilder& DataSystem(); - - /** - * To customize the ServiceEndpoints the SDK uses for streaming, - * polling, and events, pass in an EndpointsBuilder. - * @return Reference to an EndpointsBuilder. - */ - EndpointsBuilder& ServiceEndpoints(); - - /** - * To include metadata about the application that is utilizing the SDK, - * pass in an AppInfoBuilder. - * @return Reference to an AppInfoBuilder. - */ - AppInfoBuilder& AppInfo(); - - /** - * Enables or disables "Offline" mode. True means - * Offline mode is enabled. - * @param offline True if the SDK should operate in Offline mode. - * @return Reference to this builder. - */ - ConfigBuilder& Offline(bool offline); - - /** - * To tune settings related to event generation and delivery, pass an - * EventsBuilder. - * @return Reference to an EventsBuilder. - */ - EventsBuilder& Events(); - - /** - * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. - * The builder has methods for setting individual HTTP-related properties. - * @return Reference to an HttpPropertiesBuilder. - */ - HttpPropertiesBuilder& HttpProperties(); - - /** - * Sets the logging configuration for the SDK. - * @return Reference to a LoggingBuilder. - */ - LoggingBuilder& Logging(); - - /** - * Builds a Configuration, suitable for passing into an instance of Client. - * @return - */ - tl::expected Build() const; - - private: - std::string sdk_key_; - std::optional offline_; - - EndpointsBuilder service_endpoints_builder_; - AppInfoBuilder app_info_builder_; - EventsBuilder events_builder_; - HttpPropertiesBuilder http_properties_builder_; - LoggingBuilder logging_config_builder_; - DataSystemBuilder data_system_builder_; -}; - } // namespace launchdarkly::config::shared::builders diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 0f5932ce3..4aba9eae1 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -55,7 +55,7 @@ class ConfigBuilder { * @param builder A DataSystemBuilder. * @return Reference to a DataSystemBuilder. */ - DataSystemBuilder& DataSource(); + DataSystemBuilder& DataSystem(); /** * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index ad6603b94..57902d2b9 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -23,8 +23,8 @@ ConfigBuilder& ConfigBuilder::Offline(bool offline) { return *this; } -DataSourceBuilder& ConfigBuilder::DataSource() { - return data_source_builder_; +DataSystemBuilder& ConfigBuilder::DataSystem() { + return data_system_builder_; } HttpPropertiesBuilder& ConfigBuilder::HttpProperties() { From d4130fe6de595b7b2d98b9363d79f75281962405 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 11:17:19 -0800 Subject: [PATCH 082/244] fixing the bad merge.. --- .../launchdarkly/config/shared/config.hpp | 62 ++----------------- libs/common/src/config/config.cpp | 57 ++++++----------- 2 files changed, 25 insertions(+), 94 deletions(-) diff --git a/libs/common/include/launchdarkly/config/shared/config.hpp b/libs/common/include/launchdarkly/config/shared/config.hpp index fe96c1d40..a9332d41a 100644 --- a/libs/common/include/launchdarkly/config/shared/config.hpp +++ b/libs/common/include/launchdarkly/config/shared/config.hpp @@ -3,28 +3,22 @@ #include #include #include -#include #include #include #include #include #include -#include -namespace launchdarkly::config::shared { +namespace launchdarkly::config { /** * Config represents the configuration for a LaunchDarkly C++ SDK. - * It should be passed into the SDK's constructor. + * It should be passed into an instance of Client. * @tparam SDK Type of SDK. */ - template -struct Config {}; -template <> -struct Config { +struct Config { public: - using SDK = shared::ClientSDK; Config(std::string sdk_key, bool offline, shared::built::Logging logging, @@ -44,8 +38,8 @@ struct Config { [[nodiscard]] std::optional const& ApplicationTag() const; - [[nodiscard]] config::shared::built::DataSourceConfig const& - DataSourceConfig() const; + config::shared::built::DataSourceConfig const& DataSourceConfig() + const; [[nodiscard]] shared::built::HttpProperties const& HttpProperties() const; @@ -67,48 +61,4 @@ struct Config { shared::built::Persistence persistence_; }; -template <> -struct Config { - public: - using SDK = shared::ServerSDK; - Config(std::string sdk_key, - bool offline, - shared::built::Logging logging, - shared::built::ServiceEndpoints endpoints, - shared::built::Events events, - std::optional application_tag, - shared::built::DataSystemConfig data_system_config, - shared::built::HttpProperties http_properties); - - [[nodiscard]] std::string const& SdkKey() const; - - [[nodiscard]] shared::built::ServiceEndpoints const& ServiceEndpoints() - const; - - [[nodiscard]] shared::built::Events const& Events() const; - - [[nodiscard]] std::optional const& ApplicationTag() const; - - [[nodiscard]] config::shared::built::DataSystemConfig const& - DataSystemConfig() const; - - [[nodiscard]] shared::built::HttpProperties const& HttpProperties() const; - - [[nodiscard]] bool Offline() const; - - [[nodiscard]] shared::built::Logging const& Logging() const; - - [[nodiscard]] shared::built::Persistence const& Persistence() const; - - private: - std::string sdk_key_; - bool offline_; - config::shared::built::Logging logging_; - config::shared::built::ServiceEndpoints service_endpoints_; - std::optional application_tag_; - config::shared::built::Events events_; - shared::built::DataSystemConfig data_system_config_; - config::shared::built::HttpProperties http_properties_; -}; - -} // namespace launchdarkly::config::shared +} // namespace launchdarkly::config diff --git a/libs/common/src/config/config.cpp b/libs/common/src/config/config.cpp index 4900871c0..985f3925e 100644 --- a/libs/common/src/config/config.cpp +++ b/libs/common/src/config/config.cpp @@ -2,19 +2,17 @@ #include -namespace launchdarkly::config::shared { - -Config::Config( - std::string sdk_key, - bool offline, - shared::built::Logging logging, - shared::built::ServiceEndpoints service_endpoints, - shared::built::Events events, - std::optional application_tag, - shared::built::DataSourceConfig data_source_config, - shared::built::HttpProperties http_properties, - shared::built::Persistence persistence, - shared::built::DataSystemConfig data_system_config) +namespace launchdarkly::config { +template +Config::Config(std::string sdk_key, + bool offline, + shared::built::Logging logging, + shared::built::ServiceEndpoints service_endpoints, + shared::built::Events events, + std::optional application_tag, + shared::built::DataSourceConfig data_source_config, + shared::built::HttpProperties http_properties, + shared::built::Persistence persistence) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), offline_(offline), @@ -23,31 +21,20 @@ Config::Config( application_tag_(std::move(application_tag)), data_source_config_(std::move(data_source_config)), http_properties_(std::move(http_properties)), - persistence_(std::move(persistence)) {} - -std::string const& Config::SdkKey() const { - return sdk_key_; -} + persistence_(persistence) {} -std::string const& Config::SdkKey() const { +template +std::string const& Config::SdkKey() const { return sdk_key_; } -shared::built::ServiceEndpoints const& -Config::ServiceEndpoints() const { - return service_endpoints_; -} - -shared::built::ServiceEndpoints const& -Config::ServiceEndpoints() const { +template +shared::built::ServiceEndpoints const& Config::ServiceEndpoints() const { return service_endpoints_; } -shared::built::Events const& Config::Events() const { - return events_; -} - -shared::built::Events const& Config::Events() const { +template +shared::built::Events const& Config::Events() const { return events_; } @@ -62,12 +49,6 @@ shared::built::DataSourceConfig const& Config::DataSourceConfig() return data_source_config_; } -template -shared::built::DataSystemConfig const& Config::DataSystemConfig() - const { - return data_system_config_; -} - template shared::built::HttpProperties const& Config::HttpProperties() const { return http_properties_; @@ -91,4 +72,4 @@ shared::built::Persistence const& Config::Persistence() const { template class Config; template class Config; -} // namespace launchdarkly::config::shared +} // namespace launchdarkly::config From b82e93cdd57a1223fbb0641541b6b4682150ead5 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 11:19:12 -0800 Subject: [PATCH 083/244] updating server config tests to try out data system --- libs/server-sdk/tests/config_builder_test.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index c88990163..139041b88 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -4,6 +4,7 @@ #include #include +#include #include using namespace launchdarkly; @@ -39,13 +40,21 @@ TEST_F(ConfigBuilderTest, .initial_reconnect_delay); } -TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSource) { +TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSystem) { using namespace launchdarkly::server_side; ConfigBuilder builder("sdk-123"); - builder.DataSource().Method( - DataSourceBuilder::Streaming().InitialReconnectDelay( - std::chrono::milliseconds{5000})); + auto const streaming = + DataSystemBuilder::BackgroundSyncBuilder::Streaming() + .InitialReconnectDelay(std::chrono::milliseconds{5000}); + + auto const mirror = + DataSystemBuilder::BackgroundSyncBuilder::DataDestinationBuilder(); + + builder.DataSystem().BackgroundSync( + DataSystemBuilder::BackgroundSyncBuilder() + .Source(streaming) + .Destination(mirror)); auto cfg = builder.Build(); From 88cd7954a8acdac479a8e87dee53ce2256ab346f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 11:29:21 -0800 Subject: [PATCH 084/244] fix data system config test --- .../data_system/background_sync_builder.cpp | 10 ++++++++-- .../launchdarkly/server_side/config/config.hpp | 8 ++++---- libs/server-sdk/src/config/config.cpp | 10 +++++----- libs/server-sdk/src/config/config_builder.cpp | 4 ++-- libs/server-sdk/tests/config_builder_test.cpp | 17 ++++++++++++----- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/libs/common/src/config/data_system/background_sync_builder.cpp b/libs/common/src/config/data_system/background_sync_builder.cpp index 274c7d495..b2fd302c0 100644 --- a/libs/common/src/config/data_system/background_sync_builder.cpp +++ b/libs/common/src/config/data_system/background_sync_builder.cpp @@ -17,8 +17,14 @@ BackgroundSyncBuilder& BackgroundSyncBuilder< } BackgroundSyncBuilder& BackgroundSyncBuilder::Source( - DataSourceBuilder source) { - config_.source_ = source.Build(); + Streaming source) { + config_.source_.method = source.Build(); + return *this; +} + +BackgroundSyncBuilder& BackgroundSyncBuilder::Source( + Polling source) { + config_.source_.method = source.Build(); return *this; } diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp index ff2e402d5..d51b8d36f 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -19,7 +19,7 @@ struct Config { config::shared::built::ServiceEndpoints endpoints, config::shared::built::Events events, std::optional application_tag, - config::shared::built::DataSourceConfig data_source_config, + config::shared::built::DataSystemConfig data_system_config, config::shared::built::HttpProperties http_properties); [[nodiscard]] std::string const& SdkKey() const; @@ -31,7 +31,7 @@ struct Config { [[nodiscard]] std::optional const& ApplicationTag() const; - config::shared::built::DataSourceConfig const& DataSourceConfig() + config::shared::built::DataSystemConfig const& DataSystemConfig() const; [[nodiscard]] config::shared::built::HttpProperties const& HttpProperties() @@ -48,7 +48,7 @@ struct Config { config::shared::built::ServiceEndpoints service_endpoints_; std::optional application_tag_; config::shared::built::Events events_; - config::shared::built::DataSourceConfig data_source_config_; + config::shared::built::DataSystemConfig data_system_config_; config::shared::built::HttpProperties http_properties_; }; } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/config/config.cpp b/libs/server-sdk/src/config/config.cpp index 0427d39b3..b22b0f7a0 100644 --- a/libs/server-sdk/src/config/config.cpp +++ b/libs/server-sdk/src/config/config.cpp @@ -8,7 +8,7 @@ Config::Config(std::string sdk_key, config::shared::built::ServiceEndpoints service_endpoints, config::shared::built::Events events, std::optional application_tag, - config::shared::built::DataSourceConfig data_source_config, + config::shared::built::DataSystemConfig data_system_config, config::shared::built::HttpProperties http_properties) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), @@ -16,7 +16,7 @@ Config::Config(std::string sdk_key, service_endpoints_(std::move(service_endpoints)), events_(std::move(events)), application_tag_(std::move(application_tag)), - data_source_config_(std::move(data_source_config)), + data_system_config_(std::move(data_system_config)), http_properties_(std::move(http_properties)) {} std::string const& Config::SdkKey() const { @@ -36,9 +36,9 @@ std::optional const& Config::ApplicationTag() const { return application_tag_; } -config::shared::built::DataSourceConfig const& -Config::DataSourceConfig() const { - return data_source_config_; +config::shared::built::DataSystemConfig const& +Config::DataSystemConfig() const { + return data_system_config_; } config::shared::built::HttpProperties const& Config::HttpProperties() const { diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 57902d2b9..e6abad016 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -52,7 +52,7 @@ tl::expected ConfigBuilder::Build() const { std::optional app_tag = app_info_builder_.Build(); - auto data_source_config = data_source_builder_.Build(); + auto data_system_config = data_system_builder_.Build(); auto http_properties = http_properties_builder_.Build(); @@ -65,7 +65,7 @@ tl::expected ConfigBuilder::Build() const { *endpoints_config, *events_config, app_tag, - std::move(data_source_config), + std::move(data_system_config), std::move(http_properties)}; } diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 139041b88..aca60a1d8 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -27,17 +27,24 @@ TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig) { } TEST_F(ConfigBuilderTest, - DefaultConstruction_ServerConfig_UsesDefaulDataSourceConfig) { + DefaultConstruction_ServerConfig_UsesDefaulDataSystemConfig) { using namespace launchdarkly::server_side; ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); + auto background_sync_config = + std::get>( + cfg->DataSystemConfig().system_); + + auto streaming_config = + std::get >( + background_sync_config.source_.method); + // Should be streaming with a 1 second initial retry; EXPECT_EQ(std::chrono::milliseconds{1000}, - std::get>( - cfg->DataSourceConfig().method) - .initial_reconnect_delay); + streaming_config.initial_reconnect_delay); } TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSystem) { From 60eb56795bda7e2789c510187a9095fc3f5016fa Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 12:24:18 -0800 Subject: [PATCH 085/244] update namespaces for server tests --- .../include/launchdarkly/config/client.hpp | 2 +- .../launchdarkly/config/server_builders.hpp | 1 + .../shared/built/data_source_config.hpp | 3 -- .../data_system/background_sync_config.hpp | 4 +- .../launchdarkly/config/shared/defaults.hpp | 39 ++++++++++----- libs/server-sdk/src/CMakeLists.txt | 1 + libs/server-sdk/src/client_impl.cpp | 7 ++- .../change_notifier_destination.cpp | 6 +++ .../memory_store/memory_store.hpp | 4 +- .../data_interfaces/source/ipush_source.hpp | 3 +- .../background_sync_system.cpp | 11 ++++- .../background_sync_system.hpp | 5 +- .../sources/noop/null_data_source.cpp | 8 ++- .../sources/noop/null_data_source.hpp | 3 +- .../sources/polling/polling_data_source.cpp | 9 +++- .../sources/polling/polling_data_source.hpp | 3 +- .../streaming/streaming_data_source.cpp | 8 ++- .../streaming/streaming_data_source.hpp | 3 +- libs/server-sdk/tests/config_builder_test.cpp | 34 +++++++------ .../tests/data_source_event_handler_test.cpp | 11 +++-- .../tests/data_store_updater_test.cpp | 18 +++---- .../tests/dependency_tracker_test.cpp | 19 ++----- libs/server-sdk/tests/evaluator_tests.cpp | 4 +- .../tests/expiration_tracker_test.cpp | 5 +- libs/server-sdk/tests/memory_store_test.cpp | 16 ++---- .../tests/persistent_data_store_test.cpp | 49 ------------------- .../tests/server_c_bindings_test.cpp | 6 +-- libs/server-sdk/tests/test_store.cpp | 20 ++++---- libs/server-sdk/tests/test_store.hpp | 12 ++--- 29 files changed, 144 insertions(+), 170 deletions(-) delete mode 100644 libs/server-sdk/tests/persistent_data_store_test.cpp diff --git a/libs/common/include/launchdarkly/config/client.hpp b/libs/common/include/launchdarkly/config/client.hpp index e1828287b..516c4b85b 100644 --- a/libs/common/include/launchdarkly/config/client.hpp +++ b/libs/common/include/launchdarkly/config/client.hpp @@ -23,6 +23,6 @@ using DataSourceBuilder = config::shared::builders::DataSourceBuilder; using LoggingBuilder = config::shared::builders::LoggingBuilder; using PersistenceBuilder = config::shared::builders::PersistenceBuilder; -using Config = config::shared::Config; +using Config = config::Config; } // namespace launchdarkly::client_side diff --git a/libs/common/include/launchdarkly/config/server_builders.hpp b/libs/common/include/launchdarkly/config/server_builders.hpp index bb03ea675..ecda9664f 100644 --- a/libs/common/include/launchdarkly/config/server_builders.hpp +++ b/libs/common/include/launchdarkly/config/server_builders.hpp @@ -18,6 +18,7 @@ using EndpointsBuilder = config::shared::builders::EndpointsBuilder; using EventsBuilder = config::shared::builders::EventsBuilder; using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; +using DataSourceBuilder = config::shared::builders::DataSourceBuilder; using DataSystemBuilder = config::shared::builders::DataSystemBuilder; using LoggingBuilder = config::shared::builders::LoggingBuilder; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index a26356353..2d3615041 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -57,9 +57,6 @@ struct DataSourceConfig { template <> struct DataSourceConfig { std::variant, PollingConfig> method; - bool enable_bootstrap; - bool enable_sync; - std::uint64_t order; }; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp index 6ea4f588e..41f005a30 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp @@ -21,10 +21,10 @@ struct BackgroundSyncConfig {}; template <> struct BackgroundSyncConfig { - BootstrapConfig primary_bootstrapper_; + std::optional primary_bootstrapper_; std::optional fallback_bootstrapper_; DataSourceConfig source_; - DataDestinationConfig destination_; + std::optional> destination_; }; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 6419f59d3..891e2c4ef 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -83,13 +83,13 @@ template <> struct Defaults { static bool Offline() { return Defaults::Offline(); } - static auto ServiceEndpoints() -> shared::built::ServiceEndpoints { + static auto ServiceEndpoints() -> built::ServiceEndpoints { return {"https://sdk.launchdarkly.com", "https://stream.launchdarkly.com", "https://events.launchdarkly.com"}; } - static auto Events() -> shared::built::Events { + static auto Events() -> built::Events { return {true, 10000, std::chrono::seconds(5), @@ -101,32 +101,49 @@ struct Defaults { 1000}; } - static auto HttpProperties() -> shared::built::HttpProperties { + static auto HttpProperties() -> built::HttpProperties { return {std::chrono::seconds{10}, std::chrono::seconds{10}, std::chrono::seconds{10}, std::chrono::seconds{10}, std::map()}; } - static auto StreamingConfig() -> shared::built::StreamingConfig { + static auto StreamingConfig() -> built::StreamingConfig { return {std::chrono::seconds{1}, "/all"}; } - static auto DataSourceConfig() - -> shared::built::DataSourceConfig { - return {Defaults::StreamingConfig()}; + static auto DataSourceConfig() -> built::DataSourceConfig { + return {StreamingConfig()}; + } + + // No bootstrap phase yet in server-sdk; instead full + // sync is done when polling/streaming source initializes. + static auto PrimaryBootstrapConfig() + -> std::optional { + return std::nullopt; + } + + // Data isn't mirrored anywhere by default. + static auto DataDestinationConfig() + -> std::optional> { + return std::nullopt; } static auto DataSystemConfig() - -> shared::built::DataSystemConfig { - return {shared::built::BackgroundSyncConfig{}}; + -> built::DataSystemConfig { + return {shared::built::BackgroundSyncConfig{ + PrimaryBootstrapConfig(), + std::nullopt, + DataSourceConfig(), + DataDestinationConfig(), + }}; } - static auto PollingConfig() -> shared::built::PollingConfig { + static auto PollingConfig() -> built::PollingConfig { return {std::chrono::seconds{30}, "/sdk/latest-all", std::chrono::seconds{30}}; } - static auto PersistenceConfig() -> shared::built::Persistence { + static auto PersistenceConfig() -> built::Persistence { return {}; } }; diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 5f208d7f0..a2317c655 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -37,6 +37,7 @@ target_sources(${LIBNAME} data_systems/background_sync/background_sync_system.cpp data_systems/background_sync/sources/streaming/event_handler.cpp data_systems/lazy_load/lazy_load_system.cpp + data_source_status.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index b72ea8168..435a0bd4d 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -75,9 +75,12 @@ static std::unique_ptr MakeDataSystem( // TODO: Check if config is a persistent Store (so, if 'method' is // Persistent). If so, return a data_sources::PullModeSource instead. + auto const bg_sync_config = std::get>( + config.DataSystemConfig().system_); + return std::make_unique( - config.ServiceEndpoints(), config.DataSourceConfig(), - data_source_properties, executor, status_manager, logger); + config.ServiceEndpoints(), bg_sync_config, data_source_properties, + executor, status_manager, logger); } static Logger MakeLogger(config::shared::built::Logging const& config) { diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp index 290993baa..bdec46a77 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier_destination.cpp @@ -75,4 +75,10 @@ ChangeNotifierDestination::ChangeNotifierDestination( data_interfaces::IStore const& source) : sink_(sink), source_(source) {} +std::string const& ChangeNotifierDestination::Identity() const { + static std::string const identity = + "change notifier for " + sink_.Identity(); + return identity; +} + } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index a8b733be7..485ad13e7 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -10,8 +10,8 @@ namespace launchdarkly::server_side::data_components { -class MemoryStore : public data_interfaces::IStore, - public data_interfaces::IDestination { +class MemoryStore final : public data_interfaces::IStore, + public data_interfaces::IDestination { public: [[nodiscard]] std::shared_ptr GetFlag( std::string const& key) const override; diff --git a/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp index 83cf0676a..e54ddaa71 100644 --- a/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp @@ -14,8 +14,7 @@ namespace launchdarkly::server_side::data_interfaces { class IPushSource { public: - virtual void Init(std::optional initial_data, - IDestination& destination) = 0; + virtual void Init(std::optional initial_data) = 0; virtual void Start() = 0; virtual void ShutdownAsync(std::function) = 0; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 1c875a71a..ac5fccec3 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -9,7 +9,8 @@ using namespace config::shared::built; BackgroundSync::BackgroundSync( ServiceEndpoints const& endpoints, - DataSourceConfig const& data_source_config, + BackgroundSyncConfig const& + background_sync_config, HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, @@ -32,7 +33,13 @@ BackgroundSync::BackgroundSync( status_manager, logger); } }, - data_source_config.method); + background_sync_config.source_.method); +} + +void BackgroundSync::Initialize() { + // TODO: if there was any data from bootstrapping, then add it: + // synchronizer_->Init(data); + synchronizer_->Start(); } std::string const& BackgroundSync::Identity() const { diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 4f9d754a8..ad7d6162a 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -7,6 +7,7 @@ #include "../../data_interfaces/system/isystem.hpp" #include +#include #include #include #include @@ -29,8 +30,8 @@ namespace launchdarkly::server_side::data_systems { class BackgroundSync : public data_interfaces::ISystem { public: BackgroundSync(config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::DataSourceConfig< - config::shared::ServerSDK> const& data_source_config, + config::shared::built::BackgroundSyncConfig< + config::shared::ServerSDK> const& background_sync_config, config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp index 820dd47b5..11679b8b9 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp @@ -12,8 +12,12 @@ void NullDataSource::ShutdownAsync(std::function complete) { boost::asio::post(exec_, complete); } -void NullDataSource::Init(std::optional initial_data, - data_interfaces::IDestination& destination) {} +void NullDataSource::Init(std::optional initial_data) {} + +std::string const& NullDataSource::Identity() const { + static std::string const identity = "no-op data source"; + return identity; +} NullDataSource::NullDataSource( boost::asio::any_io_executor exec, diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp index 5ed0ad980..cdd3b63ad 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp @@ -14,8 +14,7 @@ class NullDataSource : public data_interfaces::IPushSource { boost::asio::any_io_executor exec, data_components::DataSourceStatusManager& status_manager); - void Init(std::optional initial_data, - data_interfaces::IDestination& destination) override; + void Init(std::optional initial_data) override; void Start() override; void ShutdownAsync(std::function) override; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index cb912aec4..d56146c81 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -40,6 +40,11 @@ static network::HttpRequest MakeRequest( return {url.value_or(""), method, builder.Build(), body}; } +std::string const& PollingDataSource::Identity() const { + static std::string const identity = "polling data source"; + return identity; +} + PollingDataSource::PollingDataSource( config::shared::built::ServiceEndpoints const& endpoints, config::shared::built::PollingConfig const& @@ -69,8 +74,8 @@ PollingDataSource::PollingDataSource( } } -void PollingDataSource::Init(std::optional initial_data, - data_interfaces::IDestination& destination) { +void PollingDataSource::Init( + std::optional initial_data) { // TODO: implement } void PollingDataSource::DoPoll() { diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp index 90bb4d225..1022fb157 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp @@ -30,8 +30,7 @@ class PollingDataSource data_components::DataSourceStatusManager& status_manager, Logger const& logger); - void Init(std::optional initial_data, - data_interfaces::IDestination& destination) override; + void Init(std::optional initial_data) override; void Start() override; void ShutdownAsync(std::function completion) override; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index a1f9505a6..b8209d00b 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -28,6 +28,11 @@ static char const* DataSourceErrorToString(launchdarkly::sse::Error error) { } } +std::string const& StreamingDataSource::Identity() const { + static std::string const identity = "streaming data source"; + return identity; +} + StreamingDataSource::StreamingDataSource( config::shared::built::ServiceEndpoints const& endpoints, config::shared::built::StreamingConfig const& @@ -46,8 +51,7 @@ StreamingDataSource::StreamingDataSource( streaming_endpoint_(endpoints.StreamingBaseUrl()) {} void StreamingDataSource::Init( - std::optional initial_data, - data_interfaces::IDestination& destination) { + std::optional initial_data) { // TODO: implement } diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp index 869b3a958..e48abb079 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp @@ -37,8 +37,7 @@ class StreamingDataSource final data_components::DataSourceStatusManager& status_manager, Logger const& logger); - void Init(std::optional initial_data, - data_interfaces::IDestination& destination) override; + void Init(std::optional initial_data) override; void Start() override; void ShutdownAsync(std::function completion) override; diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index aca60a1d8..2d8b29936 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -38,38 +38,40 @@ TEST_F(ConfigBuilderTest, cfg->DataSystemConfig().system_); auto streaming_config = - std::get >( - background_sync_config.source_.method); + std::get>( + background_sync_config.source_.method); - // Should be streaming with a 1 second initial retry; + // Should be streaming with a 1 second initial delay. EXPECT_EQ(std::chrono::milliseconds{1000}, streaming_config.initial_reconnect_delay); } -TEST_F(ConfigBuilderTest, ServerConfig_CanSetDataSystem) { +TEST_F(ConfigBuilderTest, ServerConfig_CanModifyStreamReconnectDelay) { using namespace launchdarkly::server_side; ConfigBuilder builder("sdk-123"); + auto const delay = std::chrono::seconds{5}; + auto const streaming = DataSystemBuilder::BackgroundSyncBuilder::Streaming() - .InitialReconnectDelay(std::chrono::milliseconds{5000}); - - auto const mirror = - DataSystemBuilder::BackgroundSyncBuilder::DataDestinationBuilder(); + .InitialReconnectDelay(delay); builder.DataSystem().BackgroundSync( DataSystemBuilder::BackgroundSyncBuilder() - .Source(streaming) - .Destination(mirror)); + .Source(streaming)); auto cfg = builder.Build(); - EXPECT_EQ(std::chrono::milliseconds{5000}, - std::get>( - cfg->DataSourceConfig().method) - .initial_reconnect_delay); + EXPECT_EQ( + delay, + std::get>( + std::get>( + cfg->DataSystemConfig().system_) + .source_.method) + .initial_reconnect_delay); } TEST_F(ConfigBuilderTest, diff --git a/libs/server-sdk/tests/data_source_event_handler_test.cpp b/libs/server-sdk/tests/data_source_event_handler_test.cpp index b19c92b6f..cdddb8e66 100644 --- a/libs/server-sdk/tests/data_source_event_handler_test.cpp +++ b/libs/server-sdk/tests/data_source_event_handler_test.cpp @@ -1,17 +1,18 @@ #include #include -#include "data_sources/data_source_event_handler.hpp" -#include "data_store/memory_store.hpp" + +#include +#include using namespace launchdarkly; using namespace launchdarkly::server_side; -using namespace launchdarkly::server_side::data; -using namespace launchdarkly::server_side::data_store; +using namespace server_side::data_components; +using namespace server_side::data_systems; TEST(DataSourceEventHandlerTests, HandlesEmptyPutMessage) { auto logger = launchdarkly::logging::NullLogger(); - auto store = std::make_shared(); + auto store = std::make_shared(); DataSourceStatusManager manager; DataSourceEventHandler event_handler(*store, logger, manager); diff --git a/libs/server-sdk/tests/data_store_updater_test.cpp b/libs/server-sdk/tests/data_store_updater_test.cpp index 9764cbf48..ea957f4ef 100644 --- a/libs/server-sdk/tests/data_store_updater_test.cpp +++ b/libs/server-sdk/tests/data_store_updater_test.cpp @@ -1,19 +1,13 @@ #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::ChangeNotifierDestination; -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; +#include +#include +#include using launchdarkly::Value; -using launchdarkly::data_model::Flag; -using launchdarkly::data_model::Segment; + +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::data_components; TEST(ChangeNotifierDestinationTest, DoesNotInitializeStoreUntilInit) { MemoryStore store; diff --git a/libs/server-sdk/tests/dependency_tracker_test.cpp b/libs/server-sdk/tests/dependency_tracker_test.cpp index bbffbc09b..797e3a45e 100644 --- a/libs/server-sdk/tests/dependency_tracker_test.cpp +++ b/libs/server-sdk/tests/dependency_tracker_test.cpp @@ -1,22 +1,13 @@ #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; +#include +#include 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; + +using namespace launchdarkly::server_side::data_components; +using namespace launchdarkly::data_model; TEST(ScopedSetTest, CanAddItem) { DependencySet set; diff --git a/libs/server-sdk/tests/evaluator_tests.cpp b/libs/server-sdk/tests/evaluator_tests.cpp index 85807a848..93d5d8314 100644 --- a/libs/server-sdk/tests/evaluator_tests.cpp +++ b/libs/server-sdk/tests/evaluator_tests.cpp @@ -25,7 +25,7 @@ class EvaluatorTests : public ::testing::Test { Logger logger_; protected: - std::unique_ptr store_; + std::unique_ptr store_; evaluation::Evaluator eval_; }; @@ -48,7 +48,7 @@ class EvaluatorTestsWithLogs : public ::testing::Test { Logger logger_; protected: - std::unique_ptr store_; + std::unique_ptr store_; evaluation::Evaluator eval_; }; diff --git a/libs/server-sdk/tests/expiration_tracker_test.cpp b/libs/server-sdk/tests/expiration_tracker_test.cpp index a8d17f99c..15b6163ef 100644 --- a/libs/server-sdk/tests/expiration_tracker_test.cpp +++ b/libs/server-sdk/tests/expiration_tracker_test.cpp @@ -1,9 +1,8 @@ #include -#include "data_store/persistent/expiration_tracker.hpp" +#include -using launchdarkly::server_side::data_store::DataKind; -using launchdarkly::server_side::data_store::persistent::ExpirationTracker; +using namespace launchdarkly::server_side::data_components; ExpirationTracker::TimePoint Second(uint64_t second) { return std::chrono::steady_clock::time_point{std::chrono::seconds{second}}; diff --git a/libs/server-sdk/tests/memory_store_test.cpp b/libs/server-sdk/tests/memory_store_test.cpp index 852d112dc..896fabd9a 100644 --- a/libs/server-sdk/tests/memory_store_test.cpp +++ b/libs/server-sdk/tests/memory_store_test.cpp @@ -1,17 +1,11 @@ #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; +#include using launchdarkly::Value; -using launchdarkly::data_model::Flag; -using launchdarkly::data_model::Segment; + +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::data_components; TEST(MemoryStoreTest, StartsUninitialized) { MemoryStore store; @@ -26,7 +20,7 @@ TEST(MemoryStoreTest, IsInitializedAfterInit) { TEST(MemoryStoreTest, HasDescription) { MemoryStore store; - EXPECT_EQ(std::string("memory"), store.Description()); + EXPECT_EQ(std::string("memory"), store.Identity()); } TEST(MemoryStoreTest, CanGetFlag) { diff --git a/libs/server-sdk/tests/persistent_data_store_test.cpp b/libs/server-sdk/tests/persistent_data_store_test.cpp deleted file mode 100644 index 4a47b6439..000000000 --- a/libs/server-sdk/tests/persistent_data_store_test.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#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); -} diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 3c047f9dc..7a5485891 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -1,16 +1,16 @@ #include +#include #include #include +#include #include -#include - #include #include -using launchdarkly::server_side::data::DataSourceStatus; +using namespace launchdarkly::server_side; TEST(ClientBindings, MinimalInstantiation) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); diff --git a/libs/server-sdk/tests/test_store.cpp b/libs/server-sdk/tests/test_store.cpp index 128eb2b8d..2f79e7af5 100644 --- a/libs/server-sdk/tests/test_store.cpp +++ b/libs/server-sdk/tests/test_store.cpp @@ -1,38 +1,38 @@ #include "test_store.hpp" -#include "data_store/memory_store.hpp" - +#include +#include #include #include namespace launchdarkly::server_side::test_store { -std::unique_ptr Empty() { - auto store = std::make_unique(); +std::unique_ptr Empty() { + auto store = std::make_unique(); store->Init({}); return store; } -data_store::FlagDescriptor Flag(char const* json) { +data_model::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()}; + return data_model::FlagDescriptor{val.value().value()}; } -data_store::SegmentDescriptor Segment(char const* json) { +data_model::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()}; + return data_model::SegmentDescriptor{val.value().value()}; } -std::unique_ptr TestData() { - auto store = std::make_unique(); +std::unique_ptr TestData() { + auto store = std::make_unique(); store->Init({}); store->Upsert("segmentWithNoRules", Segment(R"({ diff --git a/libs/server-sdk/tests/test_store.hpp b/libs/server-sdk/tests/test_store.hpp index bfccc008e..e0bd2fdc5 100644 --- a/libs/server-sdk/tests/test_store.hpp +++ b/libs/server-sdk/tests/test_store.hpp @@ -1,7 +1,7 @@ #pragma once -#include "data_store/data_store.hpp" - +#include +#include #include namespace launchdarkly::server_side::test_store { @@ -9,23 +9,23 @@ namespace launchdarkly::server_side::test_store { /** * @return A data store preloaded with flags/segments for unit tests. */ -std::unique_ptr TestData(); +std::unique_ptr TestData(); /** * @return An initialized, but empty, data store. */ -std::unique_ptr Empty(); +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); +data_model::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); +data_model::SegmentDescriptor Segment(char const* json); } // namespace launchdarkly::server_side::test_store From 12bd0337f298f09d204e3f4260b421d1cb7b861c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 12:30:39 -0800 Subject: [PATCH 086/244] fix hello app --- examples/hello-cpp-server/main.cpp | 22 ++++++++----------- .../launchdarkly/config/server_builders.hpp | 2 ++ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index bdfb2237d..eac3b638e 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -25,19 +25,15 @@ int main() { } auto cfg_builder = server_side::ConfigBuilder(SDK_KEY); - cfg_builder.DataSources() - .Source() - .Method( - server_side::DataSourceBuilder::Streaming().InitialReconnectDelay( - std::chrono::seconds(1))) - .Bootstrap(true) - .Order(3); - - cfg_builder.DataSources() - .Bootstrap() - .Order(launchdarkly::config::shared::builders::BootstrapBuilder::Order:: - Random) - .RandomSeed(1234); + + auto const streaming_source = + server_side::DataSourceBuilder::Streaming().InitialReconnectDelay( + std::chrono::seconds(1)); + + auto const background_sync = + server_side::BackgroundSyncBuilder().Source(streaming_source); + + cfg_builder.DataSystem().BackgroundSync(background_sync); auto config = cfg_builder.Build(); if (!config) { diff --git a/libs/common/include/launchdarkly/config/server_builders.hpp b/libs/common/include/launchdarkly/config/server_builders.hpp index ecda9664f..ab8a83f0c 100644 --- a/libs/common/include/launchdarkly/config/server_builders.hpp +++ b/libs/common/include/launchdarkly/config/server_builders.hpp @@ -20,6 +20,8 @@ using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; using DataSourceBuilder = config::shared::builders::DataSourceBuilder; using DataSystemBuilder = config::shared::builders::DataSystemBuilder; +using BackgroundSyncBuilder = DataSystemBuilder::BackgroundSyncBuilder; + using LoggingBuilder = config::shared::builders::LoggingBuilder; } // namespace launchdarkly::server_side From ae02047bf5a45852b88ecd4e898341ff3b162de0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 15:36:19 -0800 Subject: [PATCH 087/244] setup data system config in tests --- examples/hello-cpp-server/main.cpp | 16 +++++----- .../launchdarkly/config/server_builders.hpp | 1 - .../data_system/background_sync_builder.hpp | 13 ++++---- .../data_system/bootstrap_builder.hpp | 15 ++-------- .../data_system/data_systems_builder.hpp | 9 ++---- .../data_system/background_sync_config.hpp | 7 +---- .../built/data_system/bootstrap_config.hpp | 4 ++- .../launchdarkly/config/shared/defaults.hpp | 9 ++---- .../data_system/background_sync_builder.cpp | 30 ++++++++----------- .../config/data_system/bootstrap_builder.cpp | 16 ++++------ .../data_system/data_system_builder.cpp | 6 ++-- .../server_side/bindings/c/config/builder.h | 9 ------ .../server_side/config/config_builder.hpp | 8 ----- libs/server-sdk/src/bindings/c/builder.cpp | 7 ----- libs/server-sdk/src/config/config_builder.cpp | 5 ---- libs/server-sdk/tests/config_builder_test.cpp | 15 +++++----- .../tests/server_c_bindings_test.cpp | 7 +++-- 17 files changed, 64 insertions(+), 113 deletions(-) diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index eac3b638e..ea9cb06c2 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -16,6 +16,8 @@ #define INIT_TIMEOUT_MILLISECONDS 3000 using namespace launchdarkly; +using namespace launchdarkly::server_side; + int main() { if (!strlen(SDK_KEY)) { printf( @@ -24,16 +26,16 @@ int main() { return 1; } - auto cfg_builder = server_side::ConfigBuilder(SDK_KEY); + auto cfg_builder = ConfigBuilder(SDK_KEY); - auto const streaming_source = - server_side::DataSourceBuilder::Streaming().InitialReconnectDelay( + auto const streaming_connection = + DataSourceBuilder::Streaming().InitialReconnectDelay( std::chrono::seconds(1)); - auto const background_sync = - server_side::BackgroundSyncBuilder().Source(streaming_source); + auto system = + DataSystemBuilder::BackgroundSync().Synchronizer(streaming_connection); - cfg_builder.DataSystem().BackgroundSync(background_sync); + cfg_builder.DataSystem().Method(system); auto config = cfg_builder.Build(); if (!config) { @@ -41,7 +43,7 @@ int main() { return 1; } - auto client = server_side::Client(std::move(*config)); + auto client = Client(std::move(*config)); auto start_result = client.StartAsync(); auto status = start_result.wait_for( diff --git a/libs/common/include/launchdarkly/config/server_builders.hpp b/libs/common/include/launchdarkly/config/server_builders.hpp index ab8a83f0c..82af98d3a 100644 --- a/libs/common/include/launchdarkly/config/server_builders.hpp +++ b/libs/common/include/launchdarkly/config/server_builders.hpp @@ -20,7 +20,6 @@ using HttpPropertiesBuilder = config::shared::builders::HttpPropertiesBuilder; using DataSourceBuilder = config::shared::builders::DataSourceBuilder; using DataSystemBuilder = config::shared::builders::DataSystemBuilder; -using BackgroundSyncBuilder = DataSystemBuilder::BackgroundSyncBuilder; using LoggingBuilder = config::shared::builders::LoggingBuilder; diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp index 254934322..6f2c48997 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp @@ -29,16 +29,17 @@ struct BackgroundSyncBuilder { BackgroundSyncBuilder(); - BackgroundSyncBuilder& PrimaryBootstrapper(BootstrapBuilder bootstrap); - BackgroundSyncBuilder& FallbackBootstrapper(BootstrapBuilder bootstrap); - BackgroundSyncBuilder& Source(Streaming source); - BackgroundSyncBuilder& Source(Polling source); + BootstrapBuilder& Bootstrapper(); + + BackgroundSyncBuilder& Synchronizer(Streaming source); + BackgroundSyncBuilder& Synchronizer(Polling source); + BackgroundSyncBuilder& Destination(DataDestinationBuilder destination); - [[nodiscard]] config::shared::built::BackgroundSyncConfig Build() - const; + [[nodiscard]] built::BackgroundSyncConfig Build() const; private: + BootstrapBuilder bootstrap_builder_; built::BackgroundSyncConfig config_; // /* Will be the default LaunchDarkly bootstrapper or a custom one diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp index df5b279ea..7d42b45dd 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp @@ -1,27 +1,18 @@ #pragma once -#include -#include +#include -#include #include -#include -#include namespace launchdarkly::config::shared::builders { class BootstrapBuilder { public: - class DefaultBuilder {}; - BootstrapBuilder(); - BootstrapBuilder& Default(); - - [[nodiscard]] built::BootstrapConfig Build() const; + [[nodiscard]] std::optional Build() const; private: - using BootstrapType = std::variant; - BootstrapType bootstrapper_; + std::optional config_; }; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp index 80f9a617a..8ca7ca7a4 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp @@ -32,18 +32,15 @@ class DataSystemBuilder { template <> class DataSystemBuilder { public: - using BackgroundSyncBuilder = BackgroundSyncBuilder; - // using LazyLoadBuilder = LazyLoadBuilder; - DataSystemBuilder(); + using BackgroundSync = BackgroundSyncBuilder; - // DataSystemBuilder& LazyLoad(LazyLoadBuilder lazy_load); - - DataSystemBuilder& BackgroundSync(BackgroundSyncBuilder background_sync); + DataSystemBuilder& Method(BackgroundSync bg_sync); [[nodiscard]] built::DataSystemConfig Build() const; private: + built::DataSystemConfig config_; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp index 41f005a30..c6c51792d 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp @@ -5,11 +5,7 @@ #include #include -#include #include -#include -#include -#include namespace launchdarkly::config::shared::built { @@ -21,8 +17,7 @@ struct BackgroundSyncConfig {}; template <> struct BackgroundSyncConfig { - std::optional primary_bootstrapper_; - std::optional fallback_bootstrapper_; + std::optional bootstrap_; DataSourceConfig source_; std::optional> destination_; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp index 8604ddf0c..d2b356f0b 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp @@ -4,5 +4,7 @@ namespace launchdarkly::config::shared::built { -struct BootstrapConfig {}; +struct BootstrapConfig { + +}; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 891e2c4ef..28ae87006 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -117,8 +117,7 @@ struct Defaults { // No bootstrap phase yet in server-sdk; instead full // sync is done when polling/streaming source initializes. - static auto PrimaryBootstrapConfig() - -> std::optional { + static auto BootstrapConfig() -> std::optional { return std::nullopt; } @@ -128,11 +127,9 @@ struct Defaults { return std::nullopt; } - static auto DataSystemConfig() - -> built::DataSystemConfig { + static auto DataSystemConfig() -> built::DataSystemConfig { return {shared::built::BackgroundSyncConfig{ - PrimaryBootstrapConfig(), - std::nullopt, + BootstrapConfig(), DataSourceConfig(), DataDestinationConfig(), }}; diff --git a/libs/common/src/config/data_system/background_sync_builder.cpp b/libs/common/src/config/data_system/background_sync_builder.cpp index b2fd302c0..a77ee27ff 100644 --- a/libs/common/src/config/data_system/background_sync_builder.cpp +++ b/libs/common/src/config/data_system/background_sync_builder.cpp @@ -2,28 +2,21 @@ namespace launchdarkly::config::shared::builders { -BackgroundSyncBuilder::BackgroundSyncBuilder() : config_() {} +BackgroundSyncBuilder::BackgroundSyncBuilder() + : bootstrap_builder_(), config_() {} -BackgroundSyncBuilder& BackgroundSyncBuilder< - ServerSDK>::PrimaryBootstrapper(BootstrapBuilder bootstrap) { - config_.primary_bootstrapper_ = bootstrap.Build(); - return *this; -} - -BackgroundSyncBuilder& BackgroundSyncBuilder< - ServerSDK>::FallbackBootstrapper(BootstrapBuilder bootstrap) { - config_.fallback_bootstrapper_ = bootstrap.Build(); - return *this; +BootstrapBuilder& BackgroundSyncBuilder::Bootstrapper() { + return bootstrap_builder_; } -BackgroundSyncBuilder& BackgroundSyncBuilder::Source( - Streaming source) { +BackgroundSyncBuilder& +BackgroundSyncBuilder::Synchronizer(Streaming source) { config_.source_.method = source.Build(); return *this; } -BackgroundSyncBuilder& BackgroundSyncBuilder::Source( - Polling source) { +BackgroundSyncBuilder& +BackgroundSyncBuilder::Synchronizer(Polling source) { config_.source_.method = source.Build(); return *this; } @@ -34,9 +27,12 @@ BackgroundSyncBuilder& BackgroundSyncBuilder::Destination( return *this; } -[[nodiscard]] config::shared::built::BackgroundSyncConfig +[[nodiscard]] built::BackgroundSyncConfig BackgroundSyncBuilder::Build() const { - return config_; + auto const bootstrap_cfg = bootstrap_builder_.Build(); + auto copy = config_; + copy.bootstrap_ = bootstrap_cfg; + return copy; } } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/bootstrap_builder.cpp b/libs/common/src/config/data_system/bootstrap_builder.cpp index 75f1f0067..277db045c 100644 --- a/libs/common/src/config/data_system/bootstrap_builder.cpp +++ b/libs/common/src/config/data_system/bootstrap_builder.cpp @@ -1,17 +1,13 @@ #include -namespace launchdarkly::config::shared::builders { +#include "launchdarkly/config/shared/defaults.hpp" -BootstrapBuilder::BootstrapBuilder() : bootstrapper_() {} +namespace launchdarkly::config::shared::builders { -BootstrapBuilder& BootstrapBuilder::Default() { - bootstrapper_ = DefaultBuilder(); - return *this; -} +BootstrapBuilder::BootstrapBuilder() + : config_(Defaults::BootstrapConfig()) {} -built::BootstrapConfig BootstrapBuilder::Build() const { - return { - // TODO - }; +std::optional BootstrapBuilder::Build() const { + return config_; } } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/data_system_builder.cpp b/libs/common/src/config/data_system/data_system_builder.cpp index 73db2b5cf..fa373d0d1 100644 --- a/libs/common/src/config/data_system/data_system_builder.cpp +++ b/libs/common/src/config/data_system/data_system_builder.cpp @@ -5,9 +5,9 @@ namespace launchdarkly::config::shared::builders { DataSystemBuilder::DataSystemBuilder() : config_(Defaults::DataSystemConfig()) {} -DataSystemBuilder& DataSystemBuilder::BackgroundSync( - BackgroundSyncBuilder builder) { - config_.system_ = builder.Build(); +DataSystemBuilder& DataSystemBuilder::Method( + BackgroundSync bg_sync) { + config_.system_ = bg_sync.Build(); return *this; } 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 index 6e7b3e8ca..7c601e929 100644 --- 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 @@ -83,15 +83,6 @@ 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. diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 4aba9eae1..feaaa2f5b 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -33,14 +33,6 @@ class ConfigBuilder { */ AppInfoBuilder& AppInfo(); - /** - * Enables or disables "Offline" mode. True means - * Offline mode is enabled. - * @param offline True if the SDK should operate in Offline mode. - * @return Reference to this builder. - */ - ConfigBuilder& Offline(bool offline); - /** * To tune settings related to event generation and delivery, pass an * EventsBuilder. diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index 3412b80d8..c4b4a000f 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -117,13 +117,6 @@ LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, 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); diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index e6abad016..91ef5242f 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -18,11 +18,6 @@ AppInfoBuilder& ConfigBuilder::AppInfo() { return app_info_builder_; } -ConfigBuilder& ConfigBuilder::Offline(bool offline) { - offline_ = offline; - return *this; -} - DataSystemBuilder& ConfigBuilder::DataSystem() { return data_system_builder_; } diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 2d8b29936..33cbb2f91 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -53,15 +53,16 @@ TEST_F(ConfigBuilderTest, ServerConfig_CanModifyStreamReconnectDelay) { auto const delay = std::chrono::seconds{5}; - auto const streaming = - DataSystemBuilder::BackgroundSyncBuilder::Streaming() - .InitialReconnectDelay(delay); + auto const streaming_connection = + DataSystemBuilder::BackgroundSync::Streaming().InitialReconnectDelay( + delay); - builder.DataSystem().BackgroundSync( - DataSystemBuilder::BackgroundSyncBuilder() - .Source(streaming)); + auto const background_sync = + DataSystemBuilder::BackgroundSync().Synchronizer(streaming_connection); - auto cfg = builder.Build(); + builder.DataSystem().Method(background_sync); + + const auto cfg = builder.Build(); EXPECT_EQ( delay, diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 7a5485891..b25440199 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -37,7 +37,9 @@ void StatusListenerFunction(LDServerDataSourceStatus status, void* user_data) { // 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); + + // TODO: Disable the default datasource for the test, otherwise this will + // try to make a net conneciton. LDServerConfig config; LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); @@ -66,7 +68,8 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { TEST(ClientBindings, GetStatusOfOfflineClient) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); - LDServerConfigBuilder_Offline(cfg_builder, true); + // TODO: Disable the default datasource for the test, otherwise this will + // try to make a net conneciton. LDServerConfig config; LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); From d83cc4f4ce0d42b4f6235f65d6e16c3285a786db Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 16:11:53 -0800 Subject: [PATCH 088/244] add support for disabling background sync --- examples/hello-cpp-server/main.cpp | 4 +++- .../config/data_system/data_system_builder.h | 0 .../launchdarkly/config/server_builders.hpp | 2 +- ...ms_builder.hpp => data_system_builder.hpp} | 3 ++- .../built/data_system/data_system_config.hpp | 1 + .../launchdarkly/config/shared/defaults.hpp | 10 +++++----- .../data_system/data_system_builder.cpp | 8 +++++++- .../server_side/config/config_builder.hpp | 2 +- libs/server-sdk/src/client_impl.cpp | 16 ++++++++++------ .../background_sync_system.cpp | 8 ++++++++ .../background_sync_system.hpp | 6 ++++++ .../sources/noop/null_data_source.hpp | 1 - libs/server-sdk/tests/config_builder_test.cpp | 19 +++++++++++++++---- 13 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 libs/common/include/launchdarkly/bindings/c/config/data_system/data_system_builder.h rename libs/common/include/launchdarkly/config/shared/builders/data_system/{data_systems_builder.hpp => data_system_builder.hpp} (96%) diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index ea9cb06c2..9d46fdd54 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -37,7 +37,9 @@ int main() { cfg_builder.DataSystem().Method(system); - auto config = cfg_builder.Build(); + cfg_builder. + + auto config = cfg_builder.Build(); if (!config) { std::cout << "error: config is invalid: " << config.error() << '\n'; return 1; diff --git a/libs/common/include/launchdarkly/bindings/c/config/data_system/data_system_builder.h b/libs/common/include/launchdarkly/bindings/c/config/data_system/data_system_builder.h new file mode 100644 index 000000000..e69de29bb diff --git a/libs/common/include/launchdarkly/config/server_builders.hpp b/libs/common/include/launchdarkly/config/server_builders.hpp index 82af98d3a..ddbbc42fd 100644 --- a/libs/common/include/launchdarkly/config/server_builders.hpp +++ b/libs/common/include/launchdarkly/config/server_builders.hpp @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp similarity index 96% rename from libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp rename to libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp index 8ca7ca7a4..98d19cb0d 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_systems_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp @@ -35,12 +35,13 @@ class DataSystemBuilder { DataSystemBuilder(); using BackgroundSync = BackgroundSyncBuilder; + DataSystemBuilder& Disabled(bool disabled); + DataSystemBuilder& Method(BackgroundSync bg_sync); [[nodiscard]] built::DataSystemConfig Build() const; private: - built::DataSystemConfig config_; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp index 764a71a1c..d9d9edf5f 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp @@ -19,6 +19,7 @@ struct DataSystemConfig {}; template <> struct DataSystemConfig { + bool disabled; std::variant< /*LazyLoadConfig, */ BackgroundSyncConfig> system_; diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 28ae87006..4fc8ae7ed 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -128,11 +128,11 @@ struct Defaults { } static auto DataSystemConfig() -> built::DataSystemConfig { - return {shared::built::BackgroundSyncConfig{ - BootstrapConfig(), - DataSourceConfig(), - DataDestinationConfig(), - }}; + return {false, shared::built::BackgroundSyncConfig{ + BootstrapConfig(), + DataSourceConfig(), + DataDestinationConfig(), + }}; } static auto PollingConfig() -> built::PollingConfig { diff --git a/libs/common/src/config/data_system/data_system_builder.cpp b/libs/common/src/config/data_system/data_system_builder.cpp index fa373d0d1..56c6c2cbc 100644 --- a/libs/common/src/config/data_system/data_system_builder.cpp +++ b/libs/common/src/config/data_system/data_system_builder.cpp @@ -1,4 +1,4 @@ -#include +#include namespace launchdarkly::config::shared::builders { @@ -11,6 +11,12 @@ DataSystemBuilder& DataSystemBuilder::Method( return *this; } +DataSystemBuilder& DataSystemBuilder::Disabled( + bool const disabled) { + config_.disabled = disabled; + return *this; +} + built::DataSystemConfig DataSystemBuilder::Build() const { return config_; } diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index feaaa2f5b..1dc3e7ca4 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -1,7 +1,7 @@ #pragma once #include #include -#include +#include #include #include #include diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 435a0bd4d..2377b3546 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -68,15 +68,19 @@ static std::unique_ptr MakeDataSystem( boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, Logger& logger) { - auto builder = HttpPropertiesBuilder(http_properties); - auto data_source_properties = builder.Build(); + if (config.DataSystemConfig().disabled) { + return std::make_unique(executor, + status_manager); + } - // TODO: Check if config is a persistent Store (so, if 'method' is - // Persistent). If so, return a data_sources::PullModeSource instead. + auto const builder = HttpPropertiesBuilder(http_properties); + + auto data_source_properties = builder.Build(); - auto const bg_sync_config = std::get>( - config.DataSystemConfig().system_); + auto const bg_sync_config = + std::get>( + config.DataSystemConfig().system_); return std::make_unique( config.ServiceEndpoints(), bg_sync_config, data_source_properties, diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index ac5fccec3..dc631eb7e 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -1,5 +1,6 @@ #include "background_sync_system.hpp" +#include "sources/noop/null_data_source.hpp" #include "sources/polling/polling_data_source.hpp" #include "sources/streaming/streaming_data_source.hpp" @@ -36,6 +37,13 @@ BackgroundSync::BackgroundSync( background_sync_config.source_.method); } +BackgroundSync::BackgroundSync( + boost::asio::any_io_executor ioc, + data_components::DataSourceStatusManager& status_manager) + : store_(), + change_notifier_(store_, store_), + synchronizer_(std::make_shared(ioc, status_manager)) {} + void BackgroundSync::Initialize() { // TODO: if there was any data from bootstrapping, then add it: // synchronizer_->Init(data); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index ad7d6162a..b2656d87f 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -37,6 +37,12 @@ class BackgroundSync : public data_interfaces::ISystem { data_components::DataSourceStatusManager& status_manager, Logger const& logger); + /** + * @brief Constructs a BackgroundSync without a data source. + */ + BackgroundSync(boost::asio::any_io_executor ioc, + data_components::DataSourceStatusManager& status_manager); + BackgroundSync(BackgroundSync const& item) = delete; BackgroundSync(BackgroundSync&& item) = delete; BackgroundSync& operator=(BackgroundSync const&) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp index cdd3b63ad..64c8032d4 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp @@ -1,7 +1,6 @@ #pragma once #include "../../../../data_components/status_notifications/data_source_status_manager.hpp" -#include "../../../../data_interfaces/destination/idestination.hpp" #include "../../../../data_interfaces/source/ipush_source.hpp" #include diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 33cbb2f91..49c812f38 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -5,7 +5,6 @@ #include #include -#include using namespace launchdarkly; using namespace launchdarkly::server_side; @@ -14,8 +13,8 @@ class ConfigBuilderTest : public ::testing:: Test { // NOLINT(cppcoreguidelines-non-private-member-variables-in-classes) protected: - launchdarkly::Logger logger; - ConfigBuilderTest() : logger(launchdarkly::logging::NullLogger()) {} + Logger logger; + ConfigBuilderTest() : logger(logging::NullLogger()) {} }; TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig) { @@ -62,7 +61,7 @@ TEST_F(ConfigBuilderTest, ServerConfig_CanModifyStreamReconnectDelay) { builder.DataSystem().Method(background_sync); - const auto cfg = builder.Build(); + auto const cfg = builder.Build(); EXPECT_EQ( delay, @@ -75,6 +74,18 @@ TEST_F(ConfigBuilderTest, ServerConfig_CanModifyStreamReconnectDelay) { .initial_reconnect_delay); } +TEST_F(ConfigBuilderTest, CanDisableDataSystem) { + using namespace launchdarkly::server_side; + ConfigBuilder builder("sdk-123"); + + auto const cfg1 = builder.Build(); + EXPECT_FALSE(cfg1->DataSystemConfig().disabled); + + builder.DataSystem().Disabled(true); + auto const cfg2 = builder.Build(); + EXPECT_TRUE(cfg2->DataSystemConfig().disabled); +} + TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig_UsesDefaultHttpProperties) { using namespace launchdarkly::server_side; From 97f554fda8c4ebaf5d6a20fe0c411e5cfa5bd918 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 16:25:03 -0800 Subject: [PATCH 089/244] fix contract test build --- .../server-contract-tests/include/client_entity.hpp | 3 +++ .../server-contract-tests/src/entity_manager.cpp | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contract-tests/server-contract-tests/include/client_entity.hpp b/contract-tests/server-contract-tests/include/client_entity.hpp index 39b4a3be3..2b58d6631 100644 --- a/contract-tests/server-contract-tests/include/client_entity.hpp +++ b/contract-tests/server-contract-tests/include/client_entity.hpp @@ -2,8 +2,11 @@ #include #include + #include +#include + class ClientEntity { public: explicit ClientEntity( diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index c7db150fb..afccd9a50 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -38,7 +38,7 @@ std::optional EntityManager::create(ConfigParams const& in) { } } - auto& datasource = config_builder.DataSource(); + auto datasystem = DataSystemBuilder::BackgroundSync(); if (in.streaming) { if (in.streaming->baseUri) { @@ -48,7 +48,7 @@ std::optional EntityManager::create(ConfigParams const& in) { auto streaming = DataSourceBuilder::Streaming(); streaming.InitialReconnectDelay( std::chrono::milliseconds(*in.streaming->initialRetryDelayMs)); - datasource.Method(std::move(streaming)); + datasystem.Synchronizer(std::move(streaming)); } } @@ -64,10 +64,12 @@ std::optional EntityManager::create(ConfigParams const& in) { std::chrono::milliseconds( *in.polling->pollIntervalMs))); } - datasource.Method(std::move(method)); + datasystem.Synchronizer(std::move(method)); } } + config_builder.DataSystem().Method(std::move(datasystem)); + auto& event_config = config_builder.Events(); if (in.events) { From 6d0f831c6654327bcdc063dd2cd08ffc667d3f47 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 16:34:35 -0800 Subject: [PATCH 090/244] wire up data system identity --- examples/hello-cpp-server/main.cpp | 6 ++---- libs/server-sdk/src/client_impl.cpp | 3 ++- .../data_systems/background_sync/background_sync_system.cpp | 3 +-- .../src/data_systems/lazy_load/lazy_load_system.cpp | 3 +-- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index 9d46fdd54..06c8d2aee 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -6,7 +6,7 @@ #include // Set SDK_KEY to your LaunchDarkly SDK key. -#define SDK_KEY "" +#define SDK_KEY "foo" // Set FEATURE_FLAG_KEY to the feature flag key you want to evaluate. #define FEATURE_FLAG_KEY "my-boolean-flag" @@ -37,9 +37,7 @@ int main() { cfg_builder.DataSystem().Method(system); - cfg_builder. - - auto config = cfg_builder.Build(); + auto config = cfg_builder.Build(); if (!config) { std::cout << "error: config is invalid: " << config.error() << '\n'; return 1; diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 2377b3546..03ff7be18 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -68,7 +68,6 @@ static std::unique_ptr MakeDataSystem( boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, Logger& logger) { - if (config.DataSystemConfig().disabled) { return std::make_unique(executor, status_manager); @@ -147,6 +146,8 @@ ClientImpl::ClientImpl(Config config, std::string const& version) events_default_(event_processor_.get(), EventFactory::WithoutReasons()), events_with_reasons_(event_processor_.get(), EventFactory::WithReasons()) { + LD_LOG(logger_, LogLevel::kDebug) + << "data system: " << data_system_->Identity(); run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); } diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index dc631eb7e..c49c19e5d 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -51,8 +51,7 @@ void BackgroundSync::Initialize() { } std::string const& BackgroundSync::Identity() const { - // TODO: Obtain more specific info - static std::string id = "generic background-sync"; + static std::string id = "background sync via " + synchronizer_->Identity(); return id; } diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 034302219..e521f3185 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -17,8 +17,7 @@ LazyLoad::LazyLoad( : cache_() {} std::string const& LazyLoad::Identity() const { - // TODO: Obtain more specific info - static std::string id = "generic lazy-loader"; + static std::string id = "lazy load via " + source_->Identity(); return id; } From 2475f7b6280c8b2d4c89e5ff0da2c32c78c6a2e6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 14 Nov 2023 11:46:56 -0800 Subject: [PATCH 091/244] fix client-side contract test build --- contract-tests/client-contract-tests/src/entity_manager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contract-tests/client-contract-tests/src/entity_manager.cpp b/contract-tests/client-contract-tests/src/entity_manager.cpp index 5a4d57c80..9993b932b 100644 --- a/contract-tests/client-contract-tests/src/entity_manager.cpp +++ b/contract-tests/client-contract-tests/src/entity_manager.cpp @@ -1,10 +1,11 @@ #include "entity_manager.hpp" -#include #include #include #include +#include + using launchdarkly::LogLevel; using namespace launchdarkly::client_side; From 71be6fe04e246a050902bb458c6e5e3a1874fce1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 13 Nov 2023 16:58:41 -0800 Subject: [PATCH 092/244] fetch redis++ using FetchContent --- CMakeLists.txt | 7 +++++++ cmake/redis-plus-plus.cmake | 10 ++++++++++ libs/server-sdk/src/CMakeLists.txt | 7 +++++++ .../lazy_load/sources/redis/redis_source.cpp | 5 +++++ .../lazy_load/sources/redis/redis_source.hpp | 19 +++++++++++++++++++ 5 files changed, 48 insertions(+) create mode 100644 cmake/redis-plus-plus.cmake create mode 100644 libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp create mode 100644 libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e9f9e29b0..8aafc43b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,8 @@ option(LD_DYNAMIC_LINK_OPENSSL option(LD_BUILD_EXAMPLES "Build hello-world examples." ON) +option(LD_BUILD_REDIS_SUPPORT "Build redis support." ON) + # If using 'make' as the build system, CMake causes the 'install' target to have a dependency on 'all', meaning # it will cause a full build. This disables that, allowing us to build piecemeal instead. This is useful # so that we only need to build the client or server for a given release (if only the client or server were affected.) @@ -129,6 +131,11 @@ add_subdirectory(libs/common) add_subdirectory(libs/internal) add_subdirectory(libs/server-sent-events) +if (LD_BUILD_REDIS_SUPPORT) + message(STATUS "LaunchDarkly: building redis support") + include(${CMAKE_FILES}/redis-plus-plus.cmake) +endif () + # Built as static or shared depending on LD_BUILD_SHARED_LIBS variable. # This target "links" in common, internal, and sse as object libraries. add_subdirectory(libs/client-sdk) diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake new file mode 100644 index 000000000..5595d34d9 --- /dev/null +++ b/cmake/redis-plus-plus.cmake @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.11) + +include(FetchContent) + +FetchContent_Declare(redis-plus-plus + GIT_REPOSITORY https://github.com/sewenew/redis-plus-plus.git + GIT_TAG 8b9ce389099608cf9bae617d79d257d2cc05e12f +) + +FetchContent_MakeAvailable(redis-plus-plus) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index a2317c655..af9a0d8d4 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -53,6 +53,13 @@ target_sources(${LIBNAME} bindings/c/all_flags_state/all_flags_state.cpp ) +if (LD_BUILD_REDIS_SUPPORT) + target_sources(${LIBNAME} + PRIVATE + data_systems/lazy_load/sources/redis/redis_source.cpp) + target_link_libraries(${LIBNAME} PRIVATE redis++::redis++_static) +endif () + if (MSVC OR (NOT LD_BUILD_SHARED_LIBS)) target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp new file mode 100644 index 000000000..e19a0e351 --- /dev/null +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -0,0 +1,5 @@ +#include "redis_source.hpp" + +namespace launchdarkly::server_side::data_systems { + +} diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp new file mode 100644 index 000000000..4d6ca2e32 --- /dev/null +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "../../../../data_interfaces/source/iserialized_pull_source.hpp" + +namespace launchdarkly::server_side::data_systems { + +class RedisDataSource final + : public data_interfaces::ISerializedDataPullSource { + public: + RedisDataSource(); + [[nodiscard]] GetResult Get(integrations::IPersistentKind const& kind, + std::string const& itemKey) const override; + [[nodiscard]] AllResult All( + integrations::IPersistentKind const& kind) const override; + [[nodiscard]] std::string const& Identity() const override; + [[nodiscard]] bool Initialized() const override; +}; + +} // namespace launchdarkly::server_side::data_systems From feaa919aadb1a653b2570e178e9befc1a22e87ce Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 14 Nov 2023 11:39:29 -0800 Subject: [PATCH 093/244] adding hiredis --- cmake/redis-plus-plus.cmake | 8 ++++ .../src/entity_manager.cpp | 3 +- libs/server-sdk/src/CMakeLists.txt | 2 +- .../lazy_load/sources/redis/redis_source.cpp | 42 +++++++++++++++++++ .../lazy_load/sources/redis/redis_source.hpp | 12 +++++- libs/server-sdk/tests/CMakeLists.txt | 4 +- libs/server-sdk/tests/config_builder_test.cpp | 2 +- libs/server-sdk/tests/redis_source_test.cpp | 8 ++++ 8 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 libs/server-sdk/tests/redis_source_test.cpp diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 5595d34d9..bd6f77756 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -2,6 +2,14 @@ cmake_minimum_required(VERSION 3.11) include(FetchContent) + +FetchContent_Declare(hiredis + GIT_REPOSITORY https://github.com/redis/hiredis.git + GIT_TAG 60e5075d4ac77424809f855ba3e398df7aacefe8 +) + +FetchContent_MakeAvailable(hiredis) + FetchContent_Declare(redis-plus-plus GIT_REPOSITORY https://github.com/sewenew/redis-plus-plus.git GIT_TAG 8b9ce389099608cf9bae617d79d257d2cc05e12f diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index afccd9a50..559ea3bab 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -17,8 +17,7 @@ std::optional EntityManager::create(ConfigParams const& in) { auto config_builder = ConfigBuilder(in.credential); - auto default_endpoints = - launchdarkly::server_side::Defaults::ServiceEndpoints(); + auto default_endpoints = Defaults::ServiceEndpoints(); auto& endpoints = config_builder.ServiceEndpoints() diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index af9a0d8d4..040c5d47e 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -57,7 +57,7 @@ if (LD_BUILD_REDIS_SUPPORT) target_sources(${LIBNAME} PRIVATE data_systems/lazy_load/sources/redis/redis_source.cpp) - target_link_libraries(${LIBNAME} PRIVATE redis++::redis++_static) + target_link_libraries(${LIBNAME} PRIVATE hiredis::hiredis redis++::redis++_static) endif () if (MSVC OR (NOT LD_BUILD_SHARED_LIBS)) diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp index e19a0e351..52e1b7958 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -2,4 +2,46 @@ namespace launchdarkly::server_side::data_systems { +std::string RedisDataSource::key_for_kind( + integrations::IPersistentKind const& kind) const { + return prefix_ + ":" + kind.Namespace(); } + +RedisDataSource::RedisDataSource(std::string uri, std::string prefix) + : prefix_(std::move(prefix)), + inited_key_(prefix_ + ":$inited"), + redis_(std::move(uri)) {} + +data_interfaces::ISerializedDataPullSource::GetResult RedisDataSource::Get( + integrations::IPersistentKind const& kind, + std::string const& itemKey) const { + if (auto maybe_item = redis_.hget(key_for_kind(kind), itemKey)) { + return integrations::SerializedItemDescriptor{0, false, + maybe_item.value()}; + } + return tl::make_unexpected(Error{"not found"}); +} + +data_interfaces::ISerializedDataPullSource::AllResult RedisDataSource::All( + integrations::IPersistentKind const& kind) const { + std::unordered_map raw_items; + AllResult::value_type items; + redis_.hgetall(key_for_kind(kind), + std::inserter(raw_items, raw_items.begin())); + for (auto const& [key, val] : raw_items) { + items.emplace(key, + integrations::SerializedItemDescriptor{0, false, val}); + } + return items; +} + +std::string const& RedisDataSource::Identity() const { + static std::string const identity = "redis source"; + return identity; +} + +bool RedisDataSource::Initialized() const { + return redis_.exists(inited_key_) == 1; +} + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp index 4d6ca2e32..2dab0f3d9 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp @@ -1,19 +1,29 @@ #pragma once +#include + #include "../../../../data_interfaces/source/iserialized_pull_source.hpp" +#include + namespace launchdarkly::server_side::data_systems { class RedisDataSource final : public data_interfaces::ISerializedDataPullSource { public: - RedisDataSource(); + RedisDataSource(std::string uri, std::string prefix); [[nodiscard]] GetResult Get(integrations::IPersistentKind const& kind, std::string const& itemKey) const override; [[nodiscard]] AllResult All( integrations::IPersistentKind const& kind) const override; [[nodiscard]] std::string const& Identity() const override; [[nodiscard]] bool Initialized() const override; + + private: + std::string const prefix_; + std::string const inited_key_; + std::string key_for_kind(integrations::IPersistentKind const& kind) const; + mutable sw::redis::Redis redis_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 707abb839..f45425dd5 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -14,7 +14,7 @@ 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 timestamp GTest::gtest_main) +) +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp redis++::redis++_static GTest::gtest_main) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 49c812f38..44b519211 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -4,7 +4,7 @@ #include #include -#include +#include "data_systems/background_sync/sources/streaming/streaming_data_source.hpp" using namespace launchdarkly; using namespace launchdarkly::server_side; diff --git a/libs/server-sdk/tests/redis_source_test.cpp b/libs/server-sdk/tests/redis_source_test.cpp new file mode 100644 index 000000000..9174fe3d0 --- /dev/null +++ b/libs/server-sdk/tests/redis_source_test.cpp @@ -0,0 +1,8 @@ +#include + +#include "data_systems/lazy_load/sources/redis/redis_source.hpp" + +using namespace launchdarkly::server_side::data_systems; +TEST(RedisTests, ConnectToRedis) { + RedisDataSource source("tcp://localhost:6379", "test"); +} From a37623e1829e28dbf39bd8a91adb6cf7e1e80ad6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 14 Nov 2023 14:22:01 -0800 Subject: [PATCH 094/244] add some sanity tests for redis --- .../data_system/data_system_builder.hpp | 4 + .../data_system/lazy_load_builder.hpp | 63 +++++++++++++++ .../built/data_system/lazy_load_config.hpp | 19 +++++ .../src/data_components/kinds/kinds.hpp | 6 +- .../json_pull_source.cpp | 26 ++++--- .../json_pull_source.hpp | 56 +++++++++++--- .../data_interfaces/source/ipull_source.hpp | 9 ++- .../source/iserialized_pull_source.hpp | 3 +- .../lazy_load/lazy_load_system.cpp | 76 +++++++++---------- .../lazy_load/lazy_load_system.hpp | 20 ++--- .../tests/lazy_load_system_test.cpp | 12 +++ libs/server-sdk/tests/redis_source_test.cpp | 8 ++ 12 files changed, 226 insertions(+), 76 deletions(-) create mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp create mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp create mode 100644 libs/server-sdk/tests/lazy_load_system_test.cpp diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp index 98d19cb0d..7636b7da9 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp @@ -1,6 +1,8 @@ #pragma once #include +#include + #include #include @@ -34,10 +36,12 @@ class DataSystemBuilder { public: DataSystemBuilder(); using BackgroundSync = BackgroundSyncBuilder; + using LazyLoad = LazyLoadBuilder; DataSystemBuilder& Disabled(bool disabled); DataSystemBuilder& Method(BackgroundSync bg_sync); + DataSystemBuilder& Method(LazyLoad lazy_load); [[nodiscard]] built::DataSystemConfig Build() const; diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp new file mode 100644 index 000000000..a8d844a65 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include + +namespace launchdarkly::config::shared::builders { + +/** + * \brief LazyLoadBuilder allows for specifying the configuration of + * the Lazy Load data system, which is appropriate when a LaunchDarkly + * environment should be stored external to the SDK (such as in Redis.) + * + * In the Lazy Load system, flag and segment data is fetched on-demand from the + * database and stored in an in-memory cache for a specific duration. This + * allows the SDK to maintain a working set of data that may be a specific + * subset of the entire environment. + * + * The database is read-only from the perspective of the SDK. To populate the + * database with flag and segment data, an external process (e.g. Relay Proxy or + * another SDK) is necessary. + */ +struct LazyLoadBuilder { + using Redis = DataSourceBuilder::RedisPull; + using EvictionPolicy = built::LazyLoadConfig::EvictionPolicy; + /** + * \brief Constructs a new LazyLoadBuilder. + */ + LazyLoadBuilder(); + + /** + * \brief Specify the source of the data. Currently, only Redis is + * supported. + * \param source The Redis configuration. + * \return Reference to this. + */ + LazyLoadBuilder& Source(Redis source); + + /** + * \brief + * \param ttl Specify the duration data items should be live in-memory + * before being refreshed from the database. The chosen \ref EvictionPolicy + * affects usage of this TTL. \return Reference to this. + */ + LazyLoadBuilder& CacheTTL(std::chrono::milliseconds ttl); + + /** + * \brief Specify the eviction policy when a data item's TTL expires. + * At this time, only EvictionPolicy::Disabled is supported (the default), + * which leaves stale items in the cache until they can be refreshed. \param + * policy The EvictionPolicy. \return Reference to this. + */ + LazyLoadBuilder& CacheEviction(EvictionPolicy policy); + + [[nodiscard]] built::LazyLoadConfig Build() const; + + private: + built::LazyLoadConfig config_; +}; + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp new file mode 100644 index 000000000..2bc41b160 --- /dev/null +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace launchdarkly::config::shared::built { + +struct LazyLoadConfig { + /** + * \brief Specifies the action taken when a data item's TTL expires. + */ + enum class EvictionPolicy { + /* No action taken; eviction is disabled. Stale items will be used + * in evaluations if they cannot be refreshed. */ + Disabled = 0 + }; + + EvictionPolicy eviction_policy; +}; +} // namespace launchdarkly::config::shared::built diff --git a/libs/server-sdk/src/data_components/kinds/kinds.hpp b/libs/server-sdk/src/data_components/kinds/kinds.hpp index 759d5f3ad..21f07212e 100644 --- a/libs/server-sdk/src/data_components/kinds/kinds.hpp +++ b/libs/server-sdk/src/data_components/kinds/kinds.hpp @@ -1,8 +1,10 @@ +#pragma once + #include namespace launchdarkly::server_side::data_components { -class SegmentKind : public integrations::IPersistentKind { +class SegmentKind final : public integrations::IPersistentKind { public: std::string const& Namespace() const override; std::uint64_t Version(std::string const& data) const override; @@ -13,7 +15,7 @@ class SegmentKind : public integrations::IPersistentKind { static inline std::string const namespace_ = "segments"; }; -class FlagKind : public integrations::IPersistentKind { +class FlagKind final : public integrations::IPersistentKind { public: std::string const& Namespace() const override; std::uint64_t Version(std::string const& data) const override; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp index c73fa4223..33d49a641 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp @@ -1,7 +1,12 @@ #include "json_pull_source.hpp" +#include +#include + #include +#include + namespace launchdarkly::server_side::data_components { JsonSource::JsonSource(data_interfaces::ISerializedDataPullSource& json_source) @@ -35,17 +40,16 @@ static std::optional> Deserialize( return std::nullopt; } -data_model::FlagDescriptor JsonSource::GetFlag(std::string const& key) const { - // TODO: deserialize then return - data_interfaces::ISerializedDataPullSource::GetResult result = - source_.Get(flag_kind_, key); +data_interfaces::IPullSource::ItemResult +JsonSource::GetFlag(std::string const& key) const { + return Deserialize(flag_kind_, key); } -data_model::SegmentDescriptor JsonSource::GetSegment( - std::string const& key) const { - // TODO: deserialize then return - data_interfaces::ISerializedDataPullSource::GetResult result = - source_.Get(segment_kind_, key); + +data_interfaces::IPullSource::ItemResult +JsonSource::GetSegment(std::string const& key) const { + return Deserialize(segment_kind_, key); } + std::unordered_map JsonSource::AllFlags() const { // TODO: deserialize then return @@ -64,4 +68,8 @@ std::string const& JsonSource::Identity() const { return source_.Identity(); } +bool JsonSource::Initialized() const { + return source_.Initialized(); +} + } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp index 1e38e238a..8222cee81 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp @@ -8,25 +8,63 @@ namespace launchdarkly::server_side::data_components { -class JsonSource : public data_interfaces::IPullSource { +class JsonSource final : public data_interfaces::IPullSource { public: - JsonSource(data_interfaces::ISerializedDataPullSource& json_source); + explicit JsonSource( + data_interfaces::ISerializedDataPullSource& json_source); - [[nodiscard]] virtual data_model::FlagDescriptor GetFlag( + [[nodiscard]] ItemResult GetFlag( std::string const& key) const override; - [[nodiscard]] virtual data_model::SegmentDescriptor GetSegment( + [[nodiscard]] ItemResult GetSegment( std::string const& key) const override; - [[nodiscard]] virtual std::unordered_map + [[nodiscard]] std::unordered_map AllFlags() const override; - [[nodiscard]] virtual std::unordered_map + [[nodiscard]] std::unordered_map AllSegments() const override; - [[nodiscard]] virtual std::string const& Identity() const override; + [[nodiscard]] std::string const& Identity() const override; + + [[nodiscard]] bool Initialized() const override; private: + template + ItemResult> Deserialize( + DataKind const& kind, + std::string const& key) const { + auto result = source_.Get(kind, key); + + if (!result) { + /* the actual fetch failed */ + return tl::make_unexpected(result.error().message); + } + + if (!result->serializedItem) { + /* the fetch succeeded, but the item wasn't found */ + return std::nullopt; + } + + auto const boost_json_val = boost::json::parse(*result->serializedItem); + auto flag = boost::json::value_to< + tl::expected, JsonError>>(boost_json_val); + + if (!flag) { + /* flag couldn't be deserialized from the JSON string */ + return tl::make_unexpected(ErrorToString(flag.error())); + } + + std::optional maybe_flag = flag->value(); + + if (!maybe_flag) { + /* JSON was valid, but the value is 'null' + * TODO: will this ever happen? + */ + return tl::make_unexpected("data was null"); + } + + return data_model::ItemDescriptor(std::move(*maybe_flag)); + } + FlagKind const flag_kind_; FlagKind const segment_kind_; data_interfaces::ISerializedDataPullSource& source_; diff --git a/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp index 933f1622e..b18c9a283 100644 --- a/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp @@ -12,7 +12,10 @@ namespace launchdarkly::server_side::data_interfaces { class IPullSource { public: - [[nodiscard]] virtual data_model::FlagDescriptor GetFlag( + template + using ItemResult = tl::expected, std::string>; + + [[nodiscard]] virtual ItemResult GetFlag( std::string const& key) const = 0; /** @@ -22,7 +25,7 @@ class IPullSource { * @return Returns a shared_ptr to the SegmentDescriptor, or a nullptr if * there is no such segment, or the segment was deleted. */ - [[nodiscard]] virtual data_model::SegmentDescriptor GetSegment( + [[nodiscard]] virtual ItemResult GetSegment( std::string const& key) const = 0; /** @@ -45,6 +48,8 @@ class IPullSource { [[nodiscard]] virtual std::string const& Identity() const = 0; + [[nodiscard]] virtual bool Initialized() const = 0; + virtual ~IPullSource() = default; IPullSource(IPullSource const& item) = delete; IPullSource(IPullSource&& item) = delete; diff --git a/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp b/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp index 286cf3f2b..7d7280fd4 100644 --- a/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp @@ -40,8 +40,7 @@ class ISerializedDataPullSource { }; using GetResult = - tl::expected, - Error>; + tl::expected; using AllResult = tl::expected< std::unordered_map, diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index e521f3185..c64ce3f00 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -3,21 +3,29 @@ #include #include +#include "sources/redis/redis_source.hpp" + namespace launchdarkly::server_side::data_systems { using namespace config::shared::built; -LazyLoad::LazyLoad( - ServiceEndpoints const& endpoints, - DataSourceConfig const& data_source_config, - HttpProperties http_properties, - boost::asio::any_io_executor ioc, - data_components::DataSourceStatusManager& status_manager, - Logger const& logger) - : cache_() {} +/* +*DataSourceConfig const& data_source_config, +HttpProperties http_properties, +boost::asio::any_io_executor ioc, +data_components::DataSourceStatusManager& status_manager, +*/ +LazyLoad::LazyLoad() + : cache_(), + raw_source_( + std::make_shared("tcp://localhost:6379", "test")), + source_(*raw_source_.get()), + tracker_(), + time_([]() { return std::chrono::steady_clock::now(); }), + initialized_() {} std::string const& LazyLoad::Identity() const { - static std::string id = "lazy load via " + source_->Identity(); + static std::string id = "lazy load via " + source_.Identity(); return id; } @@ -25,7 +33,7 @@ void LazyLoad::Initialize() {} std::shared_ptr LazyLoad::GetFlag( std::string const& key) const { - auto state = tracker_.State(Keys::kAllSegments, time_()); + auto const state = tracker_.State(Keys::kAllSegments, time_()); return Get>( state, [this, &key]() { RefreshFlag(key); }, [this, &key]() { return cache_.GetFlag(key); }); @@ -33,7 +41,7 @@ std::shared_ptr LazyLoad::GetFlag( std::shared_ptr LazyLoad::GetSegment( std::string const& key) const { - auto state = tracker_.State(Keys::kAllSegments, time_()); + auto const state = tracker_.State(Keys::kAllSegments, time_()); return Get>( state, [this, &key]() { RefreshSegment(key); }, [this, &key]() { return cache_.GetSegment(key); }); @@ -41,7 +49,7 @@ std::shared_ptr LazyLoad::GetSegment( std::unordered_map> LazyLoad::AllFlags() const { - auto state = tracker_.State(Keys::kAllFlags, time_()); + auto const state = tracker_.State(Keys::kAllFlags, time_()); return Get>>( state, [this]() { RefreshAllFlags(); }, @@ -50,7 +58,7 @@ LazyLoad::AllFlags() const { std::unordered_map> LazyLoad::AllSegments() const { - auto state = tracker_.State(Keys::kAllSegments, time_()); + auto const state = tracker_.State(Keys::kAllSegments, time_()); return Get>>( state, [this]() { RefreshAllSegments(); }, @@ -64,7 +72,7 @@ data_components::SegmentKind const LazyLoad::Kinds::Segment = data_components::SegmentKind(); bool LazyLoad::Initialized() const { - auto state = tracker_.State(Keys::kInitialized, time_()); + auto const state = tracker_.State(Keys::kInitialized, time_()); if (initialized_.has_value()) { if (initialized_.value()) { return true; @@ -78,31 +86,28 @@ bool LazyLoad::Initialized() const { } void LazyLoad::RefreshAllFlags() const { - auto res = source_->All(Kinds::Flag); - // TODO: Deserialize and put in store. - tracker_.Add(Keys::kAllSegments, time_()); + for (auto const [flag_key, flag_descriptor] : source_.AllFlags()) { + cache_.Upsert(flag_key, std::move(flag_descriptor)); + } + tracker_.Add(Keys::kAllFlags, time_()); } void LazyLoad::RefreshAllSegments() const { - auto res = source_->All(Kinds::Segment); - // TODO: Deserialize and put in store. - tracker_.Add(Keys::kAllFlags, time_()); + for (auto const [seg_key, seg_descriptor] : source_.AllSegments()) { + cache_.Upsert(seg_key, std::move(seg_descriptor)); + } + tracker_.Add(Keys::kAllSegments, time_()); } void LazyLoad::RefreshInitState() const { - initialized_ = source_->Initialized(); + initialized_ = source_.Initialized(); tracker_.Add(Keys::kInitialized, time_()); } void LazyLoad::RefreshSegment(std::string const& key) const { - auto res = source_->Get(Kinds::Segment, key); - if (res.has_value()) { - if (res->has_value()) { - auto segment = DeserializeSegment(res->value()); - if (segment.has_value()) { - cache_.Upsert(key, segment.value()); - } - // TODO: Log that we got bogus data? + if (auto segment_result = source_.GetSegment(key)) { + if (auto optional_segment = *segment_result) { + cache_.Upsert(key, std::move(*optional_segment)); } tracker_.Add(data_components::DataKind::kSegment, key, time_()); } @@ -110,16 +115,11 @@ void LazyLoad::RefreshSegment(std::string const& key) const { } void LazyLoad::RefreshFlag(std::string const& key) const { - auto res = source_->Get(Kinds::Segment, key); - if (res.has_value()) { - if (res->has_value()) { - auto flag = DeserializeFlag(res->value()); - if (flag.has_value()) { - cache_.Upsert(key, flag.value()); - } - // TODO: Log that we got bogus data? + if (auto flag_result = source_.GetFlag(key)) { + if (auto optional_flag = *flag_result) { + cache_.Upsert(key, std::move(*optional_flag)); } - tracker_.Add(data_components::DataKind::kSegment, key, time_()); + tracker_.Add(data_components::DataKind::kFlag, key, time_()); } // TODO: If there is an actual error, then do we not reset the tracking? } diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 1bcce7379..fea8321e8 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -3,8 +3,10 @@ #include "../../data_components/expiration_tracker/expiration_tracker.hpp" #include "../../data_components/kinds/kinds.hpp" #include "../../data_components/memory_store/memory_store.hpp" +#include "../../data_components/serialization_adapters/json_pull_source.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/source/iserialized_pull_source.hpp" + #include "../../data_interfaces/system/isystem.hpp" #include @@ -29,13 +31,7 @@ namespace launchdarkly::server_side::data_systems { */ class LazyLoad : public data_interfaces::ISystem { public: - LazyLoad(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, - data_components::DataSourceStatusManager& status_manager, - Logger const& logger); + LazyLoad(); LazyLoad(LazyLoad const& item) = delete; LazyLoad(LazyLoad&& item) = delete; @@ -70,12 +66,6 @@ class LazyLoad : public data_interfaces::ISystem { static integrations::SerializedItemDescriptor Serialize( data_model::SegmentDescriptor segment); - static std::optional DeserializeFlag( - integrations::SerializedItemDescriptor flag); - - static std::optional DeserializeSegment( - integrations::SerializedItemDescriptor segment); - template static TResult Get(data_components::ExpirationTracker::TrackState state, std::function refresh, @@ -92,7 +82,9 @@ class LazyLoad : public data_interfaces::ISystem { } mutable data_components::MemoryStore cache_; - std::shared_ptr source_; + std::shared_ptr raw_source_; + data_components::JsonSource source_; + mutable data_components::ExpirationTracker tracker_; std::function()> time_; mutable std::optional initialized_; diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp new file mode 100644 index 000000000..281c460bf --- /dev/null +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -0,0 +1,12 @@ +#include + +#include "data_systems/lazy_load/lazy_load_system.hpp" + +using namespace launchdarkly::server_side::data_systems; + +class LazyLoadTest : public ::testing::Test {}; + +TEST_F(LazyLoadTest, Thing) { + LazyLoad system; + ASSERT_FALSE(system.GetFlag("foo")); +} diff --git a/libs/server-sdk/tests/redis_source_test.cpp b/libs/server-sdk/tests/redis_source_test.cpp index 9174fe3d0..a31ee482d 100644 --- a/libs/server-sdk/tests/redis_source_test.cpp +++ b/libs/server-sdk/tests/redis_source_test.cpp @@ -1,8 +1,16 @@ #include +#include "data_components/kinds/kinds.hpp" #include "data_systems/lazy_load/sources/redis/redis_source.hpp" using namespace launchdarkly::server_side::data_systems; +using namespace launchdarkly::server_side::data_components; + TEST(RedisTests, ConnectToRedis) { RedisDataSource source("tcp://localhost:6379", "test"); + ASSERT_FALSE(source.Initialized()); + + auto all_flags = source.All(FlagKind{}); + ASSERT_TRUE(all_flags.has_value()); + ASSERT_EQ(all_flags->size(), 0); } From 2fbceb0119cca17b55b0f8ba6f79c08d969932e2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 14 Nov 2023 17:00:42 -0800 Subject: [PATCH 095/244] adding data system error codes --- .../shared/builders/data_source_builder.hpp | 101 +++++++--------- .../data_system/background_sync_builder.hpp | 4 +- .../data_system/data_system_builder.hpp | 12 +- .../data_system/lazy_load_builder.hpp | 6 +- .../shared/built/data_source_config.hpp | 19 ++- .../built/data_system/data_system_config.hpp | 9 +- .../built/data_system/lazy_load_config.hpp | 2 + .../launchdarkly/config/shared/defaults.hpp | 21 +++- libs/common/include/launchdarkly/error.hpp | 3 + libs/common/src/CMakeLists.txt | 1 + .../common/src/config/data_source_builder.cpp | 112 +++++++++++++----- .../data_system/data_system_builder.cpp | 37 +++++- .../config/data_system/lazy_load_builder.cpp | 34 ++++++ libs/common/src/error.cpp | 6 + libs/server-sdk/src/bindings/c/builder.cpp | 9 +- libs/server-sdk/src/config/config_builder.cpp | 5 +- libs/server-sdk/tests/config_builder_test.cpp | 19 ++- 17 files changed, 275 insertions(+), 125 deletions(-) create mode 100644 libs/common/src/config/data_system/lazy_load_builder.cpp diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp index 395008883..c49e87dca 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp @@ -1,14 +1,16 @@ #pragma once -#include -#include -#include -#include - #include #include #include +#include + +#include + +#include +#include + namespace launchdarkly::config::shared::builders { /** @@ -77,21 +79,41 @@ class PollingBuilder { }; /** - * The method visitor is only needed inside this file + * \brief Represents a Redis data source capable of pulling data on-demand. */ -namespace { -template -struct MethodVisitor { - std::variant, built::PollingConfig> - operator()(StreamingBuilder streaming) { - return streaming.Build(); - } - std::variant, built::PollingConfig> - operator()(PollingBuilder polling) { - return polling.Build(); - } +class RedisPullBuilder { + public: + /** + * \brief Inividual connection options, including host, port, password, and + * db. + */ + using ConnOpts = built::RedisPullConfig::ConnectionOpts; + /** + * \brief Connection string. + */ + using ConnURI = built::RedisPullConfig::URI; + + RedisPullBuilder(); + + /** + * \brief Connect to Redis using explicit connection options. + * \param opts The options. + * \return Reference to this. + */ + RedisPullBuilder& Connection(ConnOpts opts); + + /** + * \brief Connect to Redis using a URI. + * \param uri The URI. + * \return Reference to this. + */ + RedisPullBuilder& Connection(ConnURI uri); + + [[nodiscard]] tl::expected Build() const; + + private: + built::RedisPullConfig config_; }; -} // namespace template <> class DataSourceBuilder { @@ -166,47 +188,4 @@ class DataSourceBuilder { bool use_report_; }; -template <> -class DataSourceBuilder { - public: - using Streaming = StreamingBuilder; - using Polling = PollingBuilder; - - DataSourceBuilder(); - - /** - * 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 stream_builder The streaming builder. - * @return Reference to this builder. - */ - DataSourceBuilder& Method(Streaming 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 polling_builder The polling builder. - * @return Reference to this builder. - */ - DataSourceBuilder& Method(Polling polling_builder); - - /** - * Build a data source config. This is used internal to the SDK. - * - * @return The built config. - */ - [[nodiscard]] built::DataSourceConfig Build() const; - - private: - std::variant method_; -}; - } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp index 6f2c48997..982c7e13f 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp @@ -22,8 +22,8 @@ struct BackgroundSyncBuilder {}; template <> struct BackgroundSyncBuilder { - using Streaming = DataSourceBuilder::Streaming; - using Polling = DataSourceBuilder::Polling; + using Streaming = StreamingBuilder; + using Polling = PollingBuilder; using DataDestinationBuilder = DataDestinationBuilder; diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp index 7636b7da9..f0fee5c73 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp @@ -4,16 +4,10 @@ #include #include - -#include - #include #include -#include -#include -#include -#include +#include namespace launchdarkly::config::shared::builders { @@ -43,9 +37,11 @@ class DataSystemBuilder { DataSystemBuilder& Method(BackgroundSync bg_sync); DataSystemBuilder& Method(LazyLoad lazy_load); - [[nodiscard]] built::DataSystemConfig Build() const; + [[nodiscard]] tl::expected, Error> + Build() const; private: + std::optional> method_builder_; built::DataSystemConfig config_; }; diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp index a8d844a65..2d7318679 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include @@ -23,7 +24,7 @@ namespace launchdarkly::config::shared::builders { * another SDK) is necessary. */ struct LazyLoadBuilder { - using Redis = DataSourceBuilder::RedisPull; + using Redis = RedisPullBuilder; using EvictionPolicy = built::LazyLoadConfig::EvictionPolicy; /** * \brief Constructs a new LazyLoadBuilder. @@ -54,9 +55,10 @@ struct LazyLoadBuilder { */ LazyLoadBuilder& CacheEviction(EvictionPolicy policy); - [[nodiscard]] built::LazyLoadConfig Build() const; + [[nodiscard]] tl::expected Build() const; private: + RedisPullBuilder redis_builder_; built::LazyLoadConfig config_; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 2d3615041..128851ce4 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -43,6 +43,20 @@ struct PollingConfig { std::chrono::seconds min_polling_interval; }; +struct RedisPullConfig { + using URI = std::string; + + struct ConnectionOpts { + std::string host; + std::uint16_t port; + std::string password; + std::uint64_t db; + ConnectionOpts() : host(), port(0), password(), db(0) {} + }; + + std::variant connection_; +}; + template struct DataSourceConfig; @@ -56,7 +70,10 @@ struct DataSourceConfig { template <> struct DataSourceConfig { - std::variant, PollingConfig> method; + std::variant, + PollingConfig, + RedisPullConfig> + method; }; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp index d9d9edf5f..f75823edf 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp @@ -1,12 +1,9 @@ #pragma once #include +#include #include -#include -#include -#include -#include #include namespace launchdarkly::config::shared::built { @@ -20,9 +17,7 @@ struct DataSystemConfig {}; template <> struct DataSystemConfig { bool disabled; - std::variant< - /*LazyLoadConfig, */ BackgroundSyncConfig> - system_; + std::variant> system_; }; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp index 2bc41b160..90e53d032 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp @@ -15,5 +15,7 @@ struct LazyLoadConfig { }; EvictionPolicy eviction_policy; + std::chrono::milliseconds eviction_ttl; + DataSourceConfig source; }; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 4fc8ae7ed..a13af7066 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -127,12 +128,22 @@ struct Defaults { return std::nullopt; } + static auto BackgroundSyncConfig() + -> built::BackgroundSyncConfig { + return {BootstrapConfig(), DataSourceConfig(), DataDestinationConfig()}; + } + + static auto RedisPullConfig() -> built::RedisPullConfig { + return {"tcp://localhost:6379"}; + } + + static auto LazyLoadConfig() -> built::LazyLoadConfig { + return {built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::minutes{5}, RedisPullConfig()}; + } + static auto DataSystemConfig() -> built::DataSystemConfig { - return {false, shared::built::BackgroundSyncConfig{ - BootstrapConfig(), - DataSourceConfig(), - DataDestinationConfig(), - }}; + return {false, BackgroundSyncConfig()}; } static auto PollingConfig() -> built::PollingConfig { diff --git a/libs/common/include/launchdarkly/error.hpp b/libs/common/include/launchdarkly/error.hpp index a50afe433..b3226db2b 100644 --- a/libs/common/include/launchdarkly/error.hpp +++ b/libs/common/include/launchdarkly/error.hpp @@ -23,6 +23,9 @@ enum class Error : std::uint32_t { kConfig_SDKKey_Empty = 400, /* Client-side errors: 10000-19999 */ /* Server-side errors: 20000-29999 */ + kConfig_DataSource_RedisPull_EmptyURI = 20000, + kConfig_DataSource_RedisPull_EmptyHost = 20001, + kConfig_DataSource_RedisPull_MissingPort = 2002, kMax = std::numeric_limits::max() }; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 2d10de51b..645d6412f 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -44,6 +44,7 @@ add_library(${LIBNAME} OBJECT config/data_system/background_sync_builder.cpp config/data_system/data_destination_builder.cpp config/data_system/data_system_builder.cpp + config/data_system/lazy_load_builder.cpp bindings/c/value.cpp bindings/c/array_builder.cpp bindings/c/object_builder.cpp diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index dc9aa08ba..b42fc9501 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -2,6 +2,49 @@ namespace launchdarkly::config::shared::builders { +template +struct MethodVisitor {}; + +template <> +struct MethodVisitor { + using SDK = ClientSDK; + using Result = + std::variant, built::PollingConfig>; + + Result operator()(StreamingBuilder const& streaming) const { + return streaming.Build(); + } + + Result operator()(PollingBuilder const& polling) const { + return polling.Build(); + } +}; + +template <> +struct MethodVisitor { + using SDK = ServerSDK; + using Result = tl::expected, + built::PollingConfig, + built::RedisPullConfig>, + Error>; + + Result operator()(StreamingBuilder const& streaming) const { + return streaming.Build(); + } + + Result operator()(PollingBuilder const& polling) const { + return polling.Build(); + } + + Result operator()(RedisPullBuilder const& redis_pull) const { + return redis_pull.Build().map([](auto&& config) { + return std::variant, + built::PollingConfig, + built::RedisPullConfig>{std::move(config)}; + }); + } +}; + template StreamingBuilder::StreamingBuilder() : config_(Defaults::StreamingConfig()) {} @@ -34,6 +77,40 @@ built::PollingConfig PollingBuilder::Build() const { return config_; } +RedisPullBuilder::RedisPullBuilder() + : config_(Defaults::RedisPullConfig()) {} + +RedisPullBuilder& RedisPullBuilder::Connection(ConnOpts opts) { + config_.connection_ = opts; + return *this; +} + +RedisPullBuilder& RedisPullBuilder::Connection(ConnURI uri) { + config_.connection_ = uri; + return *this; +} + +tl::expected RedisPullBuilder::Build() const { + if (std::holds_alternative(config_.connection_)) { + auto const& opts = std::get(config_.connection_); + if (opts.host.empty()) { + return tl::make_unexpected( + Error::kConfig_DataSource_RedisPull_EmptyHost); + } + if (opts.port == 0) { + return tl::make_unexpected( + Error::kConfig_DataSource_RedisPull_MissingPort); + } + } else { + auto const& uri = std::get(config_.connection_); + if (uri.empty()) { + return tl::make_unexpected( + Error::kConfig_DataSource_RedisPull_EmptyURI); + } + } + return config_; +} + DataSourceBuilder::DataSourceBuilder() : with_reasons_(false), use_report_(false), method_(Streaming()) {} @@ -51,44 +128,25 @@ DataSourceBuilder& DataSourceBuilder::UseReport( DataSourceBuilder& DataSourceBuilder::Method( StreamingBuilder builder) { - method_ = builder; + method_ = std::move(builder); return *this; } DataSourceBuilder& DataSourceBuilder::Method( PollingBuilder builder) { - method_ = builder; + method_ = std::move(builder); return *this; } built::DataSourceConfig DataSourceBuilder::Build() const { - auto method = std::visit(MethodVisitor(), method_); - return {method, with_reasons_, use_report_}; -} - -DataSourceBuilder::DataSourceBuilder() : method_(Streaming()) {} - -DataSourceBuilder& DataSourceBuilder::Method( - StreamingBuilder builder) { - method_ = builder; - return *this; -} - -DataSourceBuilder& DataSourceBuilder::Method( - PollingBuilder builder) { - method_ = builder; - return *this; -} - -built::DataSourceConfig DataSourceBuilder::Build() const { - auto method = std::visit(MethodVisitor(), method_); - return {method}; + return {std::visit(MethodVisitor(), method_), with_reasons_, + use_report_}; } -template class PollingBuilder; -template class PollingBuilder; +template class PollingBuilder; +template class PollingBuilder; -template class StreamingBuilder; -template class StreamingBuilder; +template class StreamingBuilder; +template class StreamingBuilder; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/data_system_builder.cpp b/libs/common/src/config/data_system/data_system_builder.cpp index 56c6c2cbc..554dfe8ac 100644 --- a/libs/common/src/config/data_system/data_system_builder.cpp +++ b/libs/common/src/config/data_system/data_system_builder.cpp @@ -3,11 +3,18 @@ namespace launchdarkly::config::shared::builders { DataSystemBuilder::DataSystemBuilder() - : config_(Defaults::DataSystemConfig()) {} + : method_builder_(std::nullopt), + config_(Defaults::DataSystemConfig()) {} DataSystemBuilder& DataSystemBuilder::Method( BackgroundSync bg_sync) { - config_.system_ = bg_sync.Build(); + method_builder_ = std::move(bg_sync); + return *this; +} + +DataSystemBuilder& DataSystemBuilder::Method( + LazyLoad lazy_load) { + method_builder_ = std::move(lazy_load); return *this; } @@ -17,7 +24,31 @@ DataSystemBuilder& DataSystemBuilder::Disabled( return *this; } -built::DataSystemConfig DataSystemBuilder::Build() const { +tl::expected, Error> +DataSystemBuilder::Build() const { + if (method_builder_) { + auto lazy_or_background_cfg = std::visit( + [](auto&& arg) + -> tl::expected< + std::variant>, + Error> { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return arg.Build(); // -> built::BackgroundSyncConfig + } else if constexpr (std::is_same_v) { + return arg + .Build(); // -> tl::expected + } + }, + *method_builder_); + if (!lazy_or_background_cfg) { + return tl::make_unexpected(lazy_or_background_cfg.error()); + } + return built::DataSystemConfig{ + config_.disabled, std::move(*lazy_or_background_cfg)}; + } return config_; } diff --git a/libs/common/src/config/data_system/lazy_load_builder.cpp b/libs/common/src/config/data_system/lazy_load_builder.cpp new file mode 100644 index 000000000..382fc3509 --- /dev/null +++ b/libs/common/src/config/data_system/lazy_load_builder.cpp @@ -0,0 +1,34 @@ +#include + +namespace launchdarkly::config::shared::builders { + +LazyLoadBuilder::LazyLoadBuilder() + : redis_builder_(), config_(Defaults::LazyLoadConfig()) {} + +LazyLoadBuilder& LazyLoadBuilder::CacheTTL( + std::chrono::milliseconds const ttl) { + config_.eviction_ttl = ttl; + return *this; +} + +LazyLoadBuilder& LazyLoadBuilder::CacheEviction(EvictionPolicy const policy) { + config_.eviction_policy = policy; + return *this; +} + +LazyLoadBuilder& LazyLoadBuilder::Source(Redis source) { + redis_builder_ = std::move(source); + return *this; +} + +tl::expected LazyLoadBuilder::Build() const { + auto redis_config = redis_builder_.Build(); + if (!redis_config) { + return tl::make_unexpected(redis_config.error()); + } + auto copy = config_; + copy.source = {*redis_config}; + return copy; +} + +} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/error.cpp b/libs/common/src/error.cpp index d584d0aa6..14576c7b9 100644 --- a/libs/common/src/error.cpp +++ b/libs/common/src/error.cpp @@ -30,6 +30,12 @@ char const* ErrorToString(Error err) { return "events: capacity must be non-zero"; case Error::kConfig_SDKKey_Empty: return "sdk key: cannot be empty"; + case Error::kConfig_DataSource_RedisPull_EmptyHost: + return "data system: Redis host cannot be empty"; + case Error::kConfig_DataSource_RedisPull_EmptyURI: + return "data system: Redis URI cannot be empty"; + case Error::kConfig_DataSource_RedisPull_MissingPort: + return "data system: Redis port must be specified"; case Error::kMax: break; } diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index c4b4a000f..c9559cfe8 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -12,13 +12,13 @@ using namespace launchdarkly::server_side; #define FROM_BUILDER(ptr) (reinterpret_cast(ptr)) #define TO_STREAM_BUILDER(ptr) \ - (reinterpret_cast(ptr)) + (reinterpret_cast(ptr)) #define FROM_STREAM_BUILDER(ptr) \ (reinterpret_cast(ptr)) #define TO_POLL_BUILDER(ptr) \ - (reinterpret_cast(ptr)) + (reinterpret_cast(ptr)) #define FROM_POLL_BUILDER(ptr) \ (reinterpret_cast(ptr)) @@ -183,7 +183,8 @@ LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, LD_EXPORT(LDServerDataSourceStreamBuilder) LDServerDataSourceStreamBuilder_New() { - return FROM_STREAM_BUILDER(new DataSourceBuilder::Streaming()); + return FROM_STREAM_BUILDER( + new DataSystemBuilder::BackgroundSync::Streaming()); } LD_EXPORT(void) @@ -202,7 +203,7 @@ LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b) { } LD_EXPORT(LDServerDataSourcePollBuilder) LDServerDataSourcePollBuilder_New() { - return FROM_POLL_BUILDER(new DataSourceBuilder::Polling()); + return FROM_POLL_BUILDER(new DataSystemBuilder::BackgroundSync::Polling()); } LD_EXPORT(void) diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 91ef5242f..493b767bc 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -48,6 +48,9 @@ tl::expected ConfigBuilder::Build() const { std::optional app_tag = app_info_builder_.Build(); auto data_system_config = data_system_builder_.Build(); + if (!data_system_config) { + return tl::make_unexpected(data_system_config.error()); + } auto http_properties = http_properties_builder_.Build(); @@ -60,7 +63,7 @@ tl::expected ConfigBuilder::Build() const { *endpoints_config, *events_config, app_tag, - std::move(data_system_config), + std::move(*data_system_config), std::move(http_properties)}; } diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 44b519211..7fc0ea598 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -27,7 +27,6 @@ TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig) { TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig_UsesDefaulDataSystemConfig) { - using namespace launchdarkly::server_side; ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); @@ -47,7 +46,6 @@ TEST_F(ConfigBuilderTest, } TEST_F(ConfigBuilderTest, ServerConfig_CanModifyStreamReconnectDelay) { - using namespace launchdarkly::server_side; ConfigBuilder builder("sdk-123"); auto const delay = std::chrono::seconds{5}; @@ -75,7 +73,6 @@ TEST_F(ConfigBuilderTest, ServerConfig_CanModifyStreamReconnectDelay) { } TEST_F(ConfigBuilderTest, CanDisableDataSystem) { - using namespace launchdarkly::server_side; ConfigBuilder builder("sdk-123"); auto const cfg1 = builder.Build(); @@ -88,8 +85,22 @@ TEST_F(ConfigBuilderTest, CanDisableDataSystem) { TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig_UsesDefaultHttpProperties) { - using namespace launchdarkly::server_side; ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); ASSERT_EQ(cfg->HttpProperties(), Defaults::HttpProperties()); } + +TEST_F(ConfigBuilderTest, RedisConfiguration) { + ConfigBuilder builder("sdk-123"); + + using LazyLoad = DataSystemBuilder::LazyLoad; + + builder.DataSystem().Method( + LazyLoad() + .Source(LazyLoad::Redis().Connection("tcp://localhost:1234")) + .CacheEviction(LazyLoad::EvictionPolicy::Disabled) + .CacheTTL(std::chrono::seconds(5))); + + auto cfg = builder.Build(); + ASSERT_TRUE(cfg); +} From 286250d0d621e1d975cb246a243bb2f6c147dacf Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 15 Nov 2023 09:58:18 -0800 Subject: [PATCH 096/244] encapsulate redis connection opts --- .../data_system/data_system_builder.hpp | 5 +++ .../shared/built/data_source_config.hpp | 24 +++++++++---- libs/common/src/CMakeLists.txt | 1 + .../common/src/config/data_source_builder.cpp | 4 +-- libs/common/src/config/data_source_config.cpp | 33 +++++++++++++++++ .../data_system/data_system_builder.cpp | 36 +++++++------------ libs/server-sdk/tests/config_builder_test.cpp | 17 ++++++++- 7 files changed, 88 insertions(+), 32 deletions(-) create mode 100644 libs/common/src/config/data_source_config.cpp diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp index f0fee5c73..6270bc744 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp @@ -41,6 +41,11 @@ class DataSystemBuilder { Build() const; private: + std::optional< + tl::expected>, + Error>> + method_config_; std::optional> method_builder_; built::DataSystemConfig config_; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 128851ce4..31c40a98e 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -46,12 +46,24 @@ struct PollingConfig { struct RedisPullConfig { using URI = std::string; - struct ConnectionOpts { - std::string host; - std::uint16_t port; - std::string password; - std::uint64_t db; - ConnectionOpts() : host(), port(0), password(), db(0) {} + class ConnectionOpts { + public: + ConnectionOpts(); + ConnectionOpts(std::string host, + std::uint16_t port, + std::string password, + std::uint64_t db); + + [[nodiscard]] std::string const& Host() const noexcept; + [[nodiscard]] std::uint16_t Port() const noexcept; + [[nodiscard]] std::string const& Password() const noexcept; + [[nodiscard]] std::uint64_t DB() const noexcept; + + private: + std::string host_; + std::uint16_t port_; + std::string password_; + std::uint64_t db_; }; std::variant connection_; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 645d6412f..674ef6540 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -45,6 +45,7 @@ add_library(${LIBNAME} OBJECT config/data_system/data_destination_builder.cpp config/data_system/data_system_builder.cpp config/data_system/lazy_load_builder.cpp + config/data_source_config.cpp bindings/c/value.cpp bindings/c/array_builder.cpp bindings/c/object_builder.cpp diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index b42fc9501..02a053590 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -93,11 +93,11 @@ RedisPullBuilder& RedisPullBuilder::Connection(ConnURI uri) { tl::expected RedisPullBuilder::Build() const { if (std::holds_alternative(config_.connection_)) { auto const& opts = std::get(config_.connection_); - if (opts.host.empty()) { + if (opts.Host().empty()) { return tl::make_unexpected( Error::kConfig_DataSource_RedisPull_EmptyHost); } - if (opts.port == 0) { + if (opts.Port() == 0) { return tl::make_unexpected( Error::kConfig_DataSource_RedisPull_MissingPort); } diff --git a/libs/common/src/config/data_source_config.cpp b/libs/common/src/config/data_source_config.cpp new file mode 100644 index 000000000..499cbe2de --- /dev/null +++ b/libs/common/src/config/data_source_config.cpp @@ -0,0 +1,33 @@ +#include + +namespace launchdarkly::config::shared::built { + +RedisPullConfig::ConnectionOpts::ConnectionOpts() + : host_(), port_(0), password_(), db_(0) {} + +RedisPullConfig::ConnectionOpts::ConnectionOpts(std::string host, + std::uint16_t port, + std::string password, + std::uint64_t db) + : host_(std::move(host)), + port_(port), + password_(std::move(password)), + db_(db) {} + +std::string const& RedisPullConfig::ConnectionOpts::Host() const noexcept { + return host_; +} + +std::uint16_t RedisPullConfig::ConnectionOpts::Port() const noexcept { + return port_; +} + +std::string const& RedisPullConfig::ConnectionOpts::Password() const noexcept { + return password_; +} + +std::uint64_t RedisPullConfig::ConnectionOpts::DB() const noexcept { + return db_; +} + +} // namespace launchdarkly::config::shared::built diff --git a/libs/common/src/config/data_system/data_system_builder.cpp b/libs/common/src/config/data_system/data_system_builder.cpp index 554dfe8ac..f92660fb4 100644 --- a/libs/common/src/config/data_system/data_system_builder.cpp +++ b/libs/common/src/config/data_system/data_system_builder.cpp @@ -8,13 +8,13 @@ DataSystemBuilder::DataSystemBuilder() DataSystemBuilder& DataSystemBuilder::Method( BackgroundSync bg_sync) { - method_builder_ = std::move(bg_sync); + method_config_ = bg_sync.Build(); return *this; } DataSystemBuilder& DataSystemBuilder::Method( LazyLoad lazy_load) { - method_builder_ = std::move(lazy_load); + method_config_ = lazy_load.Build(); return *this; } @@ -26,29 +26,19 @@ DataSystemBuilder& DataSystemBuilder::Disabled( tl::expected, Error> DataSystemBuilder::Build() const { - if (method_builder_) { - auto lazy_or_background_cfg = std::visit( - [](auto&& arg) - -> tl::expected< - std::variant>, - Error> { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return arg.Build(); // -> built::BackgroundSyncConfig - } else if constexpr (std::is_same_v) { - return arg - .Build(); // -> tl::expected - } - }, - *method_builder_); - if (!lazy_or_background_cfg) { - return tl::make_unexpected(lazy_or_background_cfg.error()); + // We could also store the builders and do a std::visit here. Instead, + // we're building immediately in the Method setters to reduce the visitor + // boilerplate. + + if (method_config_) { + auto maybe_built = *method_config_; + if (!maybe_built) { + return tl::make_unexpected(maybe_built.error()); } - return built::DataSystemConfig{ - config_.disabled, std::move(*lazy_or_background_cfg)}; + return built::DataSystemConfig{config_.disabled, + *maybe_built}; } + return config_; } diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 7fc0ea598..0c6a44766 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -90,7 +90,7 @@ TEST_F(ConfigBuilderTest, ASSERT_EQ(cfg->HttpProperties(), Defaults::HttpProperties()); } -TEST_F(ConfigBuilderTest, RedisConfiguration) { +TEST_F(ConfigBuilderTest, ValidRedisConfiguration) { ConfigBuilder builder("sdk-123"); using LazyLoad = DataSystemBuilder::LazyLoad; @@ -104,3 +104,18 @@ TEST_F(ConfigBuilderTest, RedisConfiguration) { auto cfg = builder.Build(); ASSERT_TRUE(cfg); } + +TEST_F(ConfigBuilderTest, InvalidRedisConfiguration) { + ConfigBuilder builder("sdk-123"); + + using LazyLoad = DataSystemBuilder::LazyLoad; + + builder.DataSystem().Method( + LazyLoad().Source(LazyLoad::Redis().Connection(""))); + + auto cfg = builder.Build(); + ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_RedisPull_EmptyURI); + + builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( + LazyLoad::Redis::ConnOpts("tcp://localhost", 1233, "password", 2)))); +} From 866a2c026f9b2e16ea28943524a69dfb3e8351e5 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 15 Nov 2023 10:05:02 -0800 Subject: [PATCH 097/244] rename redis errors --- libs/common/include/launchdarkly/error.hpp | 6 +++--- libs/common/src/config/data_source_builder.cpp | 6 +++--- libs/common/src/error.cpp | 6 +++--- libs/server-sdk/tests/config_builder_test.cpp | 10 ++++++++-- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/libs/common/include/launchdarkly/error.hpp b/libs/common/include/launchdarkly/error.hpp index b3226db2b..e82482583 100644 --- a/libs/common/include/launchdarkly/error.hpp +++ b/libs/common/include/launchdarkly/error.hpp @@ -23,9 +23,9 @@ enum class Error : std::uint32_t { kConfig_SDKKey_Empty = 400, /* Client-side errors: 10000-19999 */ /* Server-side errors: 20000-29999 */ - kConfig_DataSource_RedisPull_EmptyURI = 20000, - kConfig_DataSource_RedisPull_EmptyHost = 20001, - kConfig_DataSource_RedisPull_MissingPort = 2002, + kConfig_DataSource_Redis_EmptyURI = 20000, + kConfig_DataSource_Redis_EmptyHost = 20001, + kConfig_DataSource_Redis_MissingPort = 2002, kMax = std::numeric_limits::max() }; diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 02a053590..7907e2559 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -95,17 +95,17 @@ tl::expected RedisPullBuilder::Build() const { auto const& opts = std::get(config_.connection_); if (opts.Host().empty()) { return tl::make_unexpected( - Error::kConfig_DataSource_RedisPull_EmptyHost); + Error::kConfig_DataSource_Redis_EmptyHost); } if (opts.Port() == 0) { return tl::make_unexpected( - Error::kConfig_DataSource_RedisPull_MissingPort); + Error::kConfig_DataSource_Redis_MissingPort); } } else { auto const& uri = std::get(config_.connection_); if (uri.empty()) { return tl::make_unexpected( - Error::kConfig_DataSource_RedisPull_EmptyURI); + Error::kConfig_DataSource_Redis_EmptyURI); } } return config_; diff --git a/libs/common/src/error.cpp b/libs/common/src/error.cpp index 14576c7b9..c62834d18 100644 --- a/libs/common/src/error.cpp +++ b/libs/common/src/error.cpp @@ -30,11 +30,11 @@ char const* ErrorToString(Error err) { return "events: capacity must be non-zero"; case Error::kConfig_SDKKey_Empty: return "sdk key: cannot be empty"; - case Error::kConfig_DataSource_RedisPull_EmptyHost: + case Error::kConfig_DataSource_Redis_EmptyHost: return "data system: Redis host cannot be empty"; - case Error::kConfig_DataSource_RedisPull_EmptyURI: + case Error::kConfig_DataSource_Redis_EmptyURI: return "data system: Redis URI cannot be empty"; - case Error::kConfig_DataSource_RedisPull_MissingPort: + case Error::kConfig_DataSource_Redis_MissingPort: return "data system: Redis port must be specified"; case Error::kMax: break; diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 0c6a44766..7b3fdbea2 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -114,8 +114,14 @@ TEST_F(ConfigBuilderTest, InvalidRedisConfiguration) { LazyLoad().Source(LazyLoad::Redis().Connection(""))); auto cfg = builder.Build(); - ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_RedisPull_EmptyURI); + ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_EmptyURI); builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( - LazyLoad::Redis::ConnOpts("tcp://localhost", 1233, "password", 2)))); + LazyLoad::Redis::ConnOpts("", 1233, "password", 2)))); + + cfg = builder.Build(); + ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_EmptyHost); + + builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( + LazyLoad::Redis::ConnOpts("tcp://localhost", 0, "password", 2)))); } From 30f8a2c25da0ce455cbf0171db975442a56f1f88 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 15 Nov 2023 10:06:05 -0800 Subject: [PATCH 098/244] Revert "encapsulate redis connection opts" This reverts commit 286250d0d621e1d975cb246a243bb2f6c147dacf. --- .../data_system/data_system_builder.hpp | 5 --- .../shared/built/data_source_config.hpp | 24 ++++--------- libs/common/src/CMakeLists.txt | 1 - .../common/src/config/data_source_builder.cpp | 4 +-- libs/common/src/config/data_source_config.cpp | 33 ----------------- .../data_system/data_system_builder.cpp | 36 ++++++++++++------- 6 files changed, 31 insertions(+), 72 deletions(-) delete mode 100644 libs/common/src/config/data_source_config.cpp diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp index 6270bc744..f0fee5c73 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp @@ -41,11 +41,6 @@ class DataSystemBuilder { Build() const; private: - std::optional< - tl::expected>, - Error>> - method_config_; std::optional> method_builder_; built::DataSystemConfig config_; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 31c40a98e..128851ce4 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -46,24 +46,12 @@ struct PollingConfig { struct RedisPullConfig { using URI = std::string; - class ConnectionOpts { - public: - ConnectionOpts(); - ConnectionOpts(std::string host, - std::uint16_t port, - std::string password, - std::uint64_t db); - - [[nodiscard]] std::string const& Host() const noexcept; - [[nodiscard]] std::uint16_t Port() const noexcept; - [[nodiscard]] std::string const& Password() const noexcept; - [[nodiscard]] std::uint64_t DB() const noexcept; - - private: - std::string host_; - std::uint16_t port_; - std::string password_; - std::uint64_t db_; + struct ConnectionOpts { + std::string host; + std::uint16_t port; + std::string password; + std::uint64_t db; + ConnectionOpts() : host(), port(0), password(), db(0) {} }; std::variant connection_; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 674ef6540..645d6412f 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -45,7 +45,6 @@ add_library(${LIBNAME} OBJECT config/data_system/data_destination_builder.cpp config/data_system/data_system_builder.cpp config/data_system/lazy_load_builder.cpp - config/data_source_config.cpp bindings/c/value.cpp bindings/c/array_builder.cpp bindings/c/object_builder.cpp diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 7907e2559..26fe5780b 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -93,11 +93,11 @@ RedisPullBuilder& RedisPullBuilder::Connection(ConnURI uri) { tl::expected RedisPullBuilder::Build() const { if (std::holds_alternative(config_.connection_)) { auto const& opts = std::get(config_.connection_); - if (opts.Host().empty()) { + if (opts.host.empty()) { return tl::make_unexpected( Error::kConfig_DataSource_Redis_EmptyHost); } - if (opts.Port() == 0) { + if (opts.port == 0) { return tl::make_unexpected( Error::kConfig_DataSource_Redis_MissingPort); } diff --git a/libs/common/src/config/data_source_config.cpp b/libs/common/src/config/data_source_config.cpp deleted file mode 100644 index 499cbe2de..000000000 --- a/libs/common/src/config/data_source_config.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include - -namespace launchdarkly::config::shared::built { - -RedisPullConfig::ConnectionOpts::ConnectionOpts() - : host_(), port_(0), password_(), db_(0) {} - -RedisPullConfig::ConnectionOpts::ConnectionOpts(std::string host, - std::uint16_t port, - std::string password, - std::uint64_t db) - : host_(std::move(host)), - port_(port), - password_(std::move(password)), - db_(db) {} - -std::string const& RedisPullConfig::ConnectionOpts::Host() const noexcept { - return host_; -} - -std::uint16_t RedisPullConfig::ConnectionOpts::Port() const noexcept { - return port_; -} - -std::string const& RedisPullConfig::ConnectionOpts::Password() const noexcept { - return password_; -} - -std::uint64_t RedisPullConfig::ConnectionOpts::DB() const noexcept { - return db_; -} - -} // namespace launchdarkly::config::shared::built diff --git a/libs/common/src/config/data_system/data_system_builder.cpp b/libs/common/src/config/data_system/data_system_builder.cpp index f92660fb4..554dfe8ac 100644 --- a/libs/common/src/config/data_system/data_system_builder.cpp +++ b/libs/common/src/config/data_system/data_system_builder.cpp @@ -8,13 +8,13 @@ DataSystemBuilder::DataSystemBuilder() DataSystemBuilder& DataSystemBuilder::Method( BackgroundSync bg_sync) { - method_config_ = bg_sync.Build(); + method_builder_ = std::move(bg_sync); return *this; } DataSystemBuilder& DataSystemBuilder::Method( LazyLoad lazy_load) { - method_config_ = lazy_load.Build(); + method_builder_ = std::move(lazy_load); return *this; } @@ -26,19 +26,29 @@ DataSystemBuilder& DataSystemBuilder::Disabled( tl::expected, Error> DataSystemBuilder::Build() const { - // We could also store the builders and do a std::visit here. Instead, - // we're building immediately in the Method setters to reduce the visitor - // boilerplate. - - if (method_config_) { - auto maybe_built = *method_config_; - if (!maybe_built) { - return tl::make_unexpected(maybe_built.error()); + if (method_builder_) { + auto lazy_or_background_cfg = std::visit( + [](auto&& arg) + -> tl::expected< + std::variant>, + Error> { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return arg.Build(); // -> built::BackgroundSyncConfig + } else if constexpr (std::is_same_v) { + return arg + .Build(); // -> tl::expected + } + }, + *method_builder_); + if (!lazy_or_background_cfg) { + return tl::make_unexpected(lazy_or_background_cfg.error()); } - return built::DataSystemConfig{config_.disabled, - *maybe_built}; + return built::DataSystemConfig{ + config_.disabled, std::move(*lazy_or_background_cfg)}; } - return config_; } From 31124994bb7adae8209a8f871357c64d76c7eda8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 15 Nov 2023 10:13:18 -0800 Subject: [PATCH 099/244] update tests --- .../config/shared/built/data_source_config.hpp | 1 - libs/server-sdk/tests/config_builder_test.cpp | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 128851ce4..146478286 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -51,7 +51,6 @@ struct RedisPullConfig { std::uint16_t port; std::string password; std::uint64_t db; - ConnectionOpts() : host(), port(0), password(), db(0) {} }; std::variant connection_; diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 7b3fdbea2..1f144ab10 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -117,11 +117,14 @@ TEST_F(ConfigBuilderTest, InvalidRedisConfiguration) { ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_EmptyURI); builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( - LazyLoad::Redis::ConnOpts("", 1233, "password", 2)))); + LazyLoad::Redis::ConnOpts{"", 1233, "password", 2}))); cfg = builder.Build(); ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_EmptyHost); builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( - LazyLoad::Redis::ConnOpts("tcp://localhost", 0, "password", 2)))); + LazyLoad::Redis::ConnOpts{"tcp://localhost"}))); + + cfg = builder.Build(); + ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingPort); } From b00b7ca052e6af791faa0eabd23d4cc506a85223 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 15 Nov 2023 10:46:59 -0800 Subject: [PATCH 100/244] docs and update tests --- .../shared/built/data_source_config.hpp | 25 +++++- .../launchdarkly/config/shared/defaults.hpp | 2 - libs/common/include/launchdarkly/error.hpp | 4 +- .../common/src/config/data_source_builder.cpp | 6 +- libs/common/src/error.cpp | 4 +- .../server_side/config/config.hpp | 3 - libs/server-sdk/src/client_impl.cpp | 6 +- libs/server-sdk/src/config/config.cpp | 6 -- libs/server-sdk/src/config/config_builder.cpp | 3 +- libs/server-sdk/tests/config_builder_test.cpp | 90 +++++++++---------- 10 files changed, 73 insertions(+), 76 deletions(-) diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 146478286..2470f9ce0 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -5,7 +5,6 @@ #include #include #include -#include #include namespace launchdarkly::config::shared::built { @@ -25,6 +24,12 @@ struct StreamingConfig { std::string streaming_path; }; +inline bool operator==(StreamingConfig const& lhs, + StreamingConfig const& rhs) { + return lhs.initial_reconnect_delay == rhs.initial_reconnect_delay && + lhs.streaming_path == rhs.streaming_path; +} + template struct PollingConfig; @@ -47,10 +52,22 @@ struct RedisPullConfig { using URI = std::string; struct ConnectionOpts { + /** + * \brief Redis host. Required; cannot be empty string. + */ std::string host; - std::uint16_t port; - std::string password; - std::uint64_t db; + /** + * \brief Redis port. Required. + */ + std::optional port; + /** + * \brief Redis password. Optional. + */ + std::optional password; + /** + * \brief Redis db. Optional. + */ + std::optional db; }; std::variant connection_; diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index a13af7066..10f9392a6 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -82,8 +82,6 @@ struct Defaults { template <> struct Defaults { - static bool Offline() { return Defaults::Offline(); } - static auto ServiceEndpoints() -> built::ServiceEndpoints { return {"https://sdk.launchdarkly.com", "https://stream.launchdarkly.com", diff --git a/libs/common/include/launchdarkly/error.hpp b/libs/common/include/launchdarkly/error.hpp index e82482583..e4402cdce 100644 --- a/libs/common/include/launchdarkly/error.hpp +++ b/libs/common/include/launchdarkly/error.hpp @@ -23,8 +23,8 @@ enum class Error : std::uint32_t { kConfig_SDKKey_Empty = 400, /* Client-side errors: 10000-19999 */ /* Server-side errors: 20000-29999 */ - kConfig_DataSource_Redis_EmptyURI = 20000, - kConfig_DataSource_Redis_EmptyHost = 20001, + kConfig_DataSource_Redis_MissingURI = 20000, + kConfig_DataSource_Redis_MissingHost = 20001, kConfig_DataSource_Redis_MissingPort = 2002, kMax = std::numeric_limits::max() diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 26fe5780b..10c6e7d1d 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -95,9 +95,9 @@ tl::expected RedisPullBuilder::Build() const { auto const& opts = std::get(config_.connection_); if (opts.host.empty()) { return tl::make_unexpected( - Error::kConfig_DataSource_Redis_EmptyHost); + Error::kConfig_DataSource_Redis_MissingHost); } - if (opts.port == 0) { + if (!opts.port) { return tl::make_unexpected( Error::kConfig_DataSource_Redis_MissingPort); } @@ -105,7 +105,7 @@ tl::expected RedisPullBuilder::Build() const { auto const& uri = std::get(config_.connection_); if (uri.empty()) { return tl::make_unexpected( - Error::kConfig_DataSource_Redis_EmptyURI); + Error::kConfig_DataSource_Redis_MissingURI); } } return config_; diff --git a/libs/common/src/error.cpp b/libs/common/src/error.cpp index c62834d18..454608bdd 100644 --- a/libs/common/src/error.cpp +++ b/libs/common/src/error.cpp @@ -30,9 +30,9 @@ char const* ErrorToString(Error err) { return "events: capacity must be non-zero"; case Error::kConfig_SDKKey_Empty: return "sdk key: cannot be empty"; - case Error::kConfig_DataSource_Redis_EmptyHost: + case Error::kConfig_DataSource_Redis_MissingHost: return "data system: Redis host cannot be empty"; - case Error::kConfig_DataSource_Redis_EmptyURI: + case Error::kConfig_DataSource_Redis_MissingURI: return "data system: Redis URI cannot be empty"; case Error::kConfig_DataSource_Redis_MissingPort: return "data system: Redis port must be specified"; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp index d51b8d36f..ba516b908 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -14,7 +14,6 @@ using SDK = config::shared::ServerSDK; struct Config { public: Config(std::string sdk_key, - bool offline, config::shared::built::Logging logging, config::shared::built::ServiceEndpoints endpoints, config::shared::built::Events events, @@ -37,8 +36,6 @@ struct Config { [[nodiscard]] config::shared::built::HttpProperties const& HttpProperties() const; - [[nodiscard]] bool Offline() const; - [[nodiscard]] config::shared::built::Logging const& Logging() const; private: diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 03ff7be18..e7be1cc1d 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -97,16 +97,12 @@ 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)) { + if (config.Events().Enabled()) { return std::make_unique>( exec, config.ServiceEndpoints(), config.Events(), http_properties, logger); diff --git a/libs/server-sdk/src/config/config.cpp b/libs/server-sdk/src/config/config.cpp index b22b0f7a0..a49ec3b0c 100644 --- a/libs/server-sdk/src/config/config.cpp +++ b/libs/server-sdk/src/config/config.cpp @@ -3,7 +3,6 @@ namespace launchdarkly::server_side { Config::Config(std::string sdk_key, - bool offline, config::shared::built::Logging logging, config::shared::built::ServiceEndpoints service_endpoints, config::shared::built::Events events, @@ -12,7 +11,6 @@ Config::Config(std::string sdk_key, config::shared::built::HttpProperties http_properties) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), - offline_(offline), service_endpoints_(std::move(service_endpoints)), events_(std::move(events)), application_tag_(std::move(application_tag)), @@ -45,10 +43,6 @@ config::shared::built::HttpProperties const& Config::HttpProperties() const { return http_properties_; } -bool Config::Offline() const { - return offline_; -} - config::shared::built::Logging const& Config::Logging() const { return logging_; } diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 493b767bc..41095a1dd 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -35,7 +35,7 @@ tl::expected ConfigBuilder::Build() const { if (sdk_key.empty()) { return tl::make_unexpected(Error::kConfig_SDKKey_Empty); } - auto offline = offline_.value_or(Defaults::Offline()); + auto endpoints_config = service_endpoints_builder_.Build(); if (!endpoints_config) { return tl::make_unexpected(endpoints_config.error()); @@ -58,7 +58,6 @@ tl::expected ConfigBuilder::Build() const { return {tl::in_place, sdk_key, - offline, logging, *endpoints_config, *events_config, diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 1f144ab10..27206c27b 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -8,6 +8,7 @@ using namespace launchdarkly; using namespace launchdarkly::server_side; +using namespace launchdarkly::config::shared; class ConfigBuilderTest : public ::testing:: @@ -17,64 +18,61 @@ class ConfigBuilderTest ConfigBuilderTest() : logger(logging::NullLogger()) {} }; -TEST_F(ConfigBuilderTest, DefaultConstruction_ServerConfig) { - using namespace launchdarkly::server_side; +TEST_F(ConfigBuilderTest, DefaultConstruction_Succeeds) { + // It's important that the SDK's configuration can be constructed with + // nothing more than an SDK key. ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); ASSERT_TRUE(cfg); ASSERT_EQ(cfg->SdkKey(), "sdk-123"); } -TEST_F(ConfigBuilderTest, - DefaultConstruction_ServerConfig_UsesDefaulDataSystemConfig) { +TEST_F(ConfigBuilderTest, DefaultConstruction_StreamingDefaultsAreUsed) { + // Sanity check that the default server-side config uses + // the streaming data source. + ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); - auto background_sync_config = - std::get>( + ASSERT_TRUE(std::holds_alternative>( + cfg->DataSystemConfig().system_)); + + auto const bg_sync_config = + std::get>( cfg->DataSystemConfig().system_); - auto streaming_config = - std::get>( - background_sync_config.source_.method); + ASSERT_TRUE(std::holds_alternative>( + bg_sync_config.source_.method)); + + auto const streaming_config = std::get>( + bg_sync_config.source_.method); - // Should be streaming with a 1 second initial delay. - EXPECT_EQ(std::chrono::milliseconds{1000}, - streaming_config.initial_reconnect_delay); + EXPECT_EQ(streaming_config, server_side::Defaults::StreamingConfig()); } -TEST_F(ConfigBuilderTest, ServerConfig_CanModifyStreamReconnectDelay) { +TEST_F(ConfigBuilderTest, DefaultConstruction_HttpPropertyDefaultsAreUsed) { ConfigBuilder builder("sdk-123"); + auto cfg = builder.Build(); + ASSERT_EQ(cfg->HttpProperties(), server_side::Defaults::HttpProperties()); +} - auto const delay = std::chrono::seconds{5}; - - auto const streaming_connection = - DataSystemBuilder::BackgroundSync::Streaming().InitialReconnectDelay( - delay); - - auto const background_sync = - DataSystemBuilder::BackgroundSync().Synchronizer(streaming_connection); - - builder.DataSystem().Method(background_sync); - - auto const cfg = builder.Build(); +TEST_F(ConfigBuilderTest, DefaultConstruction_ServiceEndpointDefaultsAreUsed) { + ConfigBuilder builder("sdk-123"); + auto cfg = builder.Build(); + ASSERT_EQ(cfg->ServiceEndpoints(), + server_side::Defaults::ServiceEndpoints()); +} - EXPECT_EQ( - delay, - std::get>( - std::get>( - cfg->DataSystemConfig().system_) - .source_.method) - .initial_reconnect_delay); +TEST_F(ConfigBuilderTest, DefaultConstruction_EventDefaultsAreUsed) { + ConfigBuilder builder("sdk-123"); + auto cfg = builder.Build(); + ASSERT_EQ(cfg->Events(), server_side::Defaults::Events()); } TEST_F(ConfigBuilderTest, CanDisableDataSystem) { ConfigBuilder builder("sdk-123"); + // First establish that the data system is enabled. auto const cfg1 = builder.Build(); EXPECT_FALSE(cfg1->DataSystemConfig().disabled); @@ -83,14 +81,7 @@ TEST_F(ConfigBuilderTest, CanDisableDataSystem) { EXPECT_TRUE(cfg2->DataSystemConfig().disabled); } -TEST_F(ConfigBuilderTest, - DefaultConstruction_ServerConfig_UsesDefaultHttpProperties) { - ConfigBuilder builder("sdk-123"); - auto cfg = builder.Build(); - ASSERT_EQ(cfg->HttpProperties(), Defaults::HttpProperties()); -} - -TEST_F(ConfigBuilderTest, ValidRedisConfiguration) { +TEST_F(ConfigBuilderTest, CanConstructValidRedisConfig) { ConfigBuilder builder("sdk-123"); using LazyLoad = DataSystemBuilder::LazyLoad; @@ -105,23 +96,28 @@ TEST_F(ConfigBuilderTest, ValidRedisConfiguration) { ASSERT_TRUE(cfg); } -TEST_F(ConfigBuilderTest, InvalidRedisConfiguration) { +TEST_F(ConfigBuilderTest, InvalidRedisConfigurationDetected) { ConfigBuilder builder("sdk-123"); using LazyLoad = DataSystemBuilder::LazyLoad; + // An empty URI string should be rejected before it is passed deeper + // into the redis client. builder.DataSystem().Method( LazyLoad().Source(LazyLoad::Redis().Connection(""))); auto cfg = builder.Build(); - ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_EmptyURI); + ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingURI); + // If using ConnOpts instead of a URI string, the host should be rejected + // for same reason as above. builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( LazyLoad::Redis::ConnOpts{"", 1233, "password", 2}))); cfg = builder.Build(); - ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_EmptyHost); + ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingHost); + // If the port isn't set, it'll be default-constructed as std::nullopt. builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( LazyLoad::Redis::ConnOpts{"tcp://localhost"}))); From 04f615f5b30f05a0af441a0656b470622a2da88f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 15 Nov 2023 11:01:03 -0800 Subject: [PATCH 101/244] fix tests --- .../src/entity_manager.cpp | 7 +++-- examples/hello-cpp-server/main.cpp | 11 ++++--- .../common/tests/data_source_builder_test.cpp | 30 ------------------- .../src/data_components/kinds/kinds.cpp | 12 ++++---- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index 559ea3bab..2ec56bd01 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -37,14 +37,15 @@ std::optional EntityManager::create(ConfigParams const& in) { } } - auto datasystem = DataSystemBuilder::BackgroundSync(); + using BackgroundSync = DataSystemBuilder::BackgroundSync; + auto datasystem = BackgroundSync(); if (in.streaming) { if (in.streaming->baseUri) { endpoints.StreamingBaseUrl(*in.streaming->baseUri); } if (in.streaming->initialRetryDelayMs) { - auto streaming = DataSourceBuilder::Streaming(); + auto streaming = BackgroundSync::Streaming(); streaming.InitialReconnectDelay( std::chrono::milliseconds(*in.streaming->initialRetryDelayMs)); datasystem.Synchronizer(std::move(streaming)); @@ -56,7 +57,7 @@ std::optional EntityManager::create(ConfigParams const& in) { endpoints.PollingBaseUrl(*in.polling->baseUri); } if (!in.streaming) { - auto method = DataSourceBuilder::Polling(); + auto method = BackgroundSync::Polling(); if (in.polling->pollIntervalMs) { method.PollInterval( std::chrono::duration_cast( diff --git a/examples/hello-cpp-server/main.cpp b/examples/hello-cpp-server/main.cpp index 06c8d2aee..64be85022 100644 --- a/examples/hello-cpp-server/main.cpp +++ b/examples/hello-cpp-server/main.cpp @@ -28,14 +28,13 @@ int main() { auto cfg_builder = ConfigBuilder(SDK_KEY); - auto const streaming_connection = - DataSourceBuilder::Streaming().InitialReconnectDelay( - std::chrono::seconds(1)); + using BackgroundSync = DataSystemBuilder::BackgroundSync; - auto system = - DataSystemBuilder::BackgroundSync().Synchronizer(streaming_connection); + auto data_system = BackgroundSync().Synchronizer( + BackgroundSync::Streaming().InitialReconnectDelay( + std::chrono::seconds(1))); - cfg_builder.DataSystem().Method(system); + cfg_builder.DataSystem().Method(data_system); auto config = cfg_builder.Build(); if (!config) { diff --git a/libs/common/tests/data_source_builder_test.cpp b/libs/common/tests/data_source_builder_test.cpp index 795a6f937..729bd949d 100644 --- a/libs/common/tests/data_source_builder_test.cpp +++ b/libs/common/tests/data_source_builder_test.cpp @@ -46,33 +46,3 @@ TEST(DataSourceBuilderTests, CanCreatePollingClientConfig) { client_config.method) .poll_interval); } - -TEST(DataSourceBuilderTests, CanCreateStreamingServerConfig) { - auto server_config = - server_side::DataSourceBuilder() - .Method(server_side::DataSourceBuilder::Streaming() - .InitialReconnectDelay(std::chrono::milliseconds{1500})) - .Build(); - - EXPECT_EQ( - std::chrono::milliseconds{1500}, - std::get< - config::shared::built::StreamingConfig>( - server_config.method) - .initial_reconnect_delay); -} - -TEST(DataSourceBuilderTests, CanCreatePollingServerConfig) { - auto server_config = - server_side::DataSourceBuilder() - .Method(server_side::DataSourceBuilder::Polling().PollInterval( - std::chrono::seconds{30000})) - .Build(); - - EXPECT_EQ( - std::chrono::seconds{30000}, - std::get< - config::shared::built::PollingConfig>( - server_config.method) - .poll_interval); -} diff --git a/libs/server-sdk/src/data_components/kinds/kinds.cpp b/libs/server-sdk/src/data_components/kinds/kinds.cpp index 91234c172..eaaea2c35 100644 --- a/libs/server-sdk/src/data_components/kinds/kinds.cpp +++ b/libs/server-sdk/src/data_components/kinds/kinds.cpp @@ -1,17 +1,17 @@ #include "kinds.hpp" -#include "launchdarkly/serialization/json_errors.hpp" -#include -#include +#include +#include +#include -#include +#include namespace launchdarkly::server_side::data_components { template -static uint64_t GetVersion(std::string data) { +static uint64_t GetVersion(std::string const& data) { boost::json::error_code error_code; - auto parsed = boost::json::parse(data, error_code); + auto const parsed = boost::json::parse(data, error_code); if (error_code) { return 0; From bc636e42cbbaa8519b225900b4d069f895051098 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 09:59:36 -0800 Subject: [PATCH 102/244] moving a bunch of files from common to server sdk --- .../launchdarkly/config/server_builders.hpp | 26 ------- .../shared/builders/data_source_builder.hpp | 16 ++-- .../data_system/background_sync_builder.hpp | 59 --------------- .../data_system/data_destination_builder.hpp | 30 -------- .../data_system/data_system_builder.hpp | 48 ------------ .../shared/builders/persistence_builder.hpp | 75 ------------------- .../shared/built/data_source_config.hpp | 8 +- .../data_system/background_sync_config.hpp | 25 ------- .../built/data_system/bootstrap_config.hpp | 10 --- .../data_system/data_destination_config.hpp | 16 ---- .../built/data_system/data_system_config.hpp | 23 ------ .../launchdarkly/config/shared/defaults.hpp | 42 ----------- libs/common/include/launchdarkly/error.hpp | 4 +- libs/common/src/CMakeLists.txt | 5 -- .../common/src/config/data_source_builder.cpp | 42 +---------- .../data_system/background_sync_builder.cpp | 38 ---------- .../config/data_system/bootstrap_builder.cpp | 13 ---- .../data_system/data_destination_builder.cpp | 12 --- .../data_system/data_system_builder.cpp | 55 -------------- .../config/data_system/lazy_load_builder.cpp | 34 --------- libs/common/src/error.cpp | 8 +- .../data_system/background_sync_builder.hpp | 33 ++++++++ .../data_system/bootstrap_builder.hpp | 6 +- .../data_system/data_destination_builder.hpp | 16 ++++ .../data_system/data_system_builder.hpp | 29 +++++++ .../data_system/lazy_load_builder.hpp | 22 +++--- .../data_system/background_sync_config.hpp | 20 +++++ .../built/data_system/bootstrap_config.hpp | 8 ++ .../data_system/data_destination_config.hpp | 9 +++ .../built/data_system/data_system_config.hpp | 15 ++++ .../built/data_system/lazy_load_config.hpp | 12 ++- .../server_side/config/config.hpp | 41 +++++----- .../server_side/config/config_builder.hpp | 7 +- .../sources}/iserialized_pull_source.hpp | 0 .../integrations}/redis/redis_source.hpp | 4 +- libs/server-sdk/src/CMakeLists.txt | 7 +- .../data_system/background_sync_builder.cpp | 35 +++++++++ .../data_system/bootstrap_builder.cpp | 12 +++ .../data_system/data_destination_builder.cpp | 12 +++ .../data_system/data_system_builder.cpp | 50 +++++++++++++ .../config/builders/data_system/defaults.hpp | 35 +++++++++ .../data_system/lazy_load_builder.cpp | 33 ++++++++ libs/server-sdk/src/config/config.cpp | 24 +++--- .../json_pull_source.cpp | 42 ++++------- .../json_pull_source.hpp | 17 +++-- .../data_interfaces/source/ipull_source.hpp | 14 ++-- .../redis/redis_source.cpp | 4 +- libs/server-sdk/tests/CMakeLists.txt | 12 ++- .../tests/{ => redis}/redis_source_test.cpp | 2 +- 49 files changed, 437 insertions(+), 673 deletions(-) delete mode 100644 libs/common/include/launchdarkly/config/server_builders.hpp delete mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp delete mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp delete mode 100644 libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp delete mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp delete mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp delete mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp delete mode 100644 libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp delete mode 100644 libs/common/src/config/data_system/background_sync_builder.cpp delete mode 100644 libs/common/src/config/data_system/bootstrap_builder.cpp delete mode 100644 libs/common/src/config/data_system/data_destination_builder.cpp delete mode 100644 libs/common/src/config/data_system/data_system_builder.cpp delete mode 100644 libs/common/src/config/data_system/lazy_load_builder.cpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/background_sync_builder.hpp rename libs/{common/include/launchdarkly/config/shared => server-sdk/include/launchdarkly/server_side/config}/builders/data_system/bootstrap_builder.hpp (54%) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_destination_builder.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp rename libs/{common/include/launchdarkly/config/shared => server-sdk/include/launchdarkly/server_side/config}/builders/data_system/lazy_load_builder.hpp (76%) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/bootstrap_config.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_destination_config.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp rename libs/{common/include/launchdarkly/config/shared => server-sdk/include/launchdarkly/server_side/config}/built/data_system/lazy_load_config.hpp (55%) rename libs/server-sdk/{src/data_interfaces/source => include/launchdarkly/server_side/data_interfaces/sources}/iserialized_pull_source.hpp (100%) rename libs/server-sdk/{src/data_systems/lazy_load/sources => include/launchdarkly/server_side/integrations}/redis/redis_source.hpp (89%) create mode 100644 libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp create mode 100644 libs/server-sdk/src/config/builders/data_system/bootstrap_builder.cpp create mode 100644 libs/server-sdk/src/config/builders/data_system/data_destination_builder.cpp create mode 100644 libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp create mode 100644 libs/server-sdk/src/config/builders/data_system/defaults.hpp create mode 100644 libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp rename libs/server-sdk/src/{data_systems/lazy_load/sources => integrations}/redis/redis_source.cpp (91%) rename libs/server-sdk/tests/{ => redis}/redis_source_test.cpp (86%) diff --git a/libs/common/include/launchdarkly/config/server_builders.hpp b/libs/common/include/launchdarkly/config/server_builders.hpp deleted file mode 100644 index ddbbc42fd..000000000 --- a/libs/common/include/launchdarkly/config/server_builders.hpp +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -namespace launchdarkly::server_side { - -using SDK = config::shared::ServerSDK; - -using Defaults = config::shared::Defaults; -using AppInfoBuilder = config::shared::builders::AppInfoBuilder; -using EndpointsBuilder = config::shared::builders::EndpointsBuilder; -using EventsBuilder = config::shared::builders::EventsBuilder; -using HttpPropertiesBuilder = - config::shared::builders::HttpPropertiesBuilder; -using DataSourceBuilder = config::shared::builders::DataSourceBuilder; -using DataSystemBuilder = config::shared::builders::DataSystemBuilder; - -using LoggingBuilder = config::shared::builders::LoggingBuilder; - -} // namespace launchdarkly::server_side diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp index c49e87dca..f4125287e 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp @@ -81,38 +81,38 @@ class PollingBuilder { /** * \brief Represents a Redis data source capable of pulling data on-demand. */ -class RedisPullBuilder { +class RedisBuilder { public: /** * \brief Inividual connection options, including host, port, password, and * db. */ - using ConnOpts = built::RedisPullConfig::ConnectionOpts; + using ConnOpts = built::RedisConfig::Standard; /** * \brief Connection string. */ - using ConnURI = built::RedisPullConfig::URI; + using ConnURI = built::RedisConfig::URI; - RedisPullBuilder(); + RedisBuilder(); /** * \brief Connect to Redis using explicit connection options. * \param opts The options. * \return Reference to this. */ - RedisPullBuilder& Connection(ConnOpts opts); + RedisBuilder& Connection(ConnOpts opts); /** * \brief Connect to Redis using a URI. * \param uri The URI. * \return Reference to this. */ - RedisPullBuilder& Connection(ConnURI uri); + RedisBuilder& Connection(ConnURI uri); - [[nodiscard]] tl::expected Build() const; + [[nodiscard]] tl::expected Build() const; private: - built::RedisPullConfig config_; + built::RedisConfig config_; }; template <> diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp deleted file mode 100644 index 982c7e13f..000000000 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/background_sync_builder.hpp +++ /dev/null @@ -1,59 +0,0 @@ -#pragma once - -#include -#include -#include - -#include -#include - -#include -#include -#include -#include - -namespace launchdarkly::config::shared::builders { - -template -struct BackgroundSyncBuilder {}; - -template <> -struct BackgroundSyncBuilder {}; - -template <> -struct BackgroundSyncBuilder { - using Streaming = StreamingBuilder; - using Polling = PollingBuilder; - - using DataDestinationBuilder = DataDestinationBuilder; - - BackgroundSyncBuilder(); - - BootstrapBuilder& Bootstrapper(); - - BackgroundSyncBuilder& Synchronizer(Streaming source); - BackgroundSyncBuilder& Synchronizer(Polling source); - - BackgroundSyncBuilder& Destination(DataDestinationBuilder destination); - - [[nodiscard]] built::BackgroundSyncConfig Build() const; - - private: - BootstrapBuilder bootstrap_builder_; - built::BackgroundSyncConfig config_; - - // /* Will be the default LaunchDarkly bootstrapper or a custom one - // provided - // * by the user. */ - // BootstrapBuilder primary_bootstrapper_; - // - // /* Optional, as appropriate fallbacks might not be available or - // wanted. */ std::optional fallback_bootstrapper_; - // - // DataSourceBuilder source_; - // - // /* Optional, as there may be no need to mirror data anywhere. */ - // std::optional destination_; -}; - -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp deleted file mode 100644 index 5dbb463b7..000000000 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_destination_builder.hpp +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include -#include - -namespace launchdarkly::config::shared::builders { - -template -struct DataDestinationBuilder {}; - -template <> -struct DataDestinationBuilder {}; - -template <> -struct DataDestinationBuilder { - DataDestinationBuilder(); - - [[nodiscard]] config::shared::built::DataDestinationConfig - Build() const; - - private: - built::DataDestinationConfig config_; -}; - -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp deleted file mode 100644 index f0fee5c73..000000000 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/data_system_builder.hpp +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -#include - -namespace launchdarkly::config::shared::builders { - -/** - * Used to construct a DataSourcesConfiguration for the specified SDK type. - * @tparam SDK ClientSDK or ServerSDK. - */ -template -class DataSystemBuilder; - -/** Not used in client-side SDK yet. */ -template <> -class DataSystemBuilder { - public: - DataSystemBuilder() {} -}; - -template <> -class DataSystemBuilder { - public: - DataSystemBuilder(); - using BackgroundSync = BackgroundSyncBuilder; - using LazyLoad = LazyLoadBuilder; - - DataSystemBuilder& Disabled(bool disabled); - - DataSystemBuilder& Method(BackgroundSync bg_sync); - DataSystemBuilder& Method(LazyLoad lazy_load); - - [[nodiscard]] tl::expected, Error> - Build() const; - - private: - std::optional> method_builder_; - built::DataSystemConfig config_; -}; - -} // namespace launchdarkly::config::shared::builders 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 8ee88f97a..8722e5d6c 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/persistence_builder.hpp @@ -81,79 +81,4 @@ class PersistenceBuilder { std::size_t max_contexts_; }; -template <> -class PersistenceBuilder { - public: - PersistenceBuilder() - : persistence_(Defaults::PersistenceConfig()) {} - - // /** - // * 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 The time, in seconds, cached data remains - // * fresh. - // * @return A reference to this builder. - // */ - // PersistenceBuilder& CacheRefreshTime( - // std::chrono::seconds cache_refresh_time) { - // persistence_.cache_refresh_time = cache_refresh_time; - // return *this; - // } - // - // /** - // * 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; - // return *this; - // } - // - // /** - // * 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) { - // persistence_.eviction_interval = eviction_interval; - // return *this; - // } - - [[nodiscard]] built::Persistence Build() const { - return persistence_; - } - - private: - built::Persistence persistence_; -}; - } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 2470f9ce0..04673c6a4 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -48,10 +48,10 @@ struct PollingConfig { std::chrono::seconds min_polling_interval; }; -struct RedisPullConfig { +struct RedisConfig { using URI = std::string; - struct ConnectionOpts { + struct Standard { /** * \brief Redis host. Required; cannot be empty string. */ @@ -70,7 +70,7 @@ struct RedisPullConfig { std::optional db; }; - std::variant connection_; + std::variant connection_; }; template @@ -88,7 +88,7 @@ template <> struct DataSourceConfig { std::variant, PollingConfig, - RedisPullConfig> + RedisConfig> method; }; diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp deleted file mode 100644 index c6c51792d..000000000 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/background_sync_config.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include - -namespace launchdarkly::config::shared::built { - -template -struct BackgroundSyncConfig {}; - -template <> -struct BackgroundSyncConfig {}; - -template <> -struct BackgroundSyncConfig { - std::optional bootstrap_; - DataSourceConfig source_; - std::optional> destination_; -}; - -} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp deleted file mode 100644 index d2b356f0b..000000000 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/bootstrap_config.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include - -namespace launchdarkly::config::shared::built { - -struct BootstrapConfig { - -}; -} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp deleted file mode 100644 index 74969135d..000000000 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/data_destination_config.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -namespace launchdarkly::config::shared::built { - -template -struct DataDestinationConfig {}; - -template <> -struct DataDestinationConfig {}; - -template <> -struct DataDestinationConfig {}; - -} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp deleted file mode 100644 index f75823edf..000000000 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/data_system_config.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include -#include -#include - -#include - -namespace launchdarkly::config::shared::built { - -template -struct DataSystemConfig; - -template <> -struct DataSystemConfig {}; - -template <> -struct DataSystemConfig { - bool disabled; - std::variant> system_; -}; - -} // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index 10f9392a6..affad00dc 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -1,8 +1,6 @@ #pragma once #include -#include -#include #include #include #include @@ -66,12 +64,6 @@ struct Defaults { return {Defaults::StreamingConfig(), false, false}; } - static auto DataSystemConfig() - -> shared::built::DataSystemConfig { - // No usage of DataSystem config yet until next major version. - return {}; - } - static auto PollingConfig() -> shared::built::PollingConfig { return {std::chrono::minutes(5), "/msdk/evalx/contexts", "/msdk/evalx/context", std::chrono::minutes(5)}; @@ -114,44 +106,10 @@ struct Defaults { return {StreamingConfig()}; } - // No bootstrap phase yet in server-sdk; instead full - // sync is done when polling/streaming source initializes. - static auto BootstrapConfig() -> std::optional { - return std::nullopt; - } - - // Data isn't mirrored anywhere by default. - static auto DataDestinationConfig() - -> std::optional> { - return std::nullopt; - } - - static auto BackgroundSyncConfig() - -> built::BackgroundSyncConfig { - return {BootstrapConfig(), DataSourceConfig(), DataDestinationConfig()}; - } - - static auto RedisPullConfig() -> built::RedisPullConfig { - return {"tcp://localhost:6379"}; - } - - static auto LazyLoadConfig() -> built::LazyLoadConfig { - return {built::LazyLoadConfig::EvictionPolicy::Disabled, - std::chrono::minutes{5}, RedisPullConfig()}; - } - - static auto DataSystemConfig() -> built::DataSystemConfig { - return {false, BackgroundSyncConfig()}; - } - static auto PollingConfig() -> built::PollingConfig { return {std::chrono::seconds{30}, "/sdk/latest-all", std::chrono::seconds{30}}; } - - static auto PersistenceConfig() -> built::Persistence { - return {}; - } }; } // namespace launchdarkly::config::shared diff --git a/libs/common/include/launchdarkly/error.hpp b/libs/common/include/launchdarkly/error.hpp index e4402cdce..64d3f2194 100644 --- a/libs/common/include/launchdarkly/error.hpp +++ b/libs/common/include/launchdarkly/error.hpp @@ -23,9 +23,7 @@ enum class Error : std::uint32_t { kConfig_SDKKey_Empty = 400, /* Client-side errors: 10000-19999 */ /* Server-side errors: 20000-29999 */ - kConfig_DataSource_Redis_MissingURI = 20000, - kConfig_DataSource_Redis_MissingHost = 20001, - kConfig_DataSource_Redis_MissingPort = 2002, + kConfig_DataSystem_LazyLoad_MissingSource = 20000, kMax = std::numeric_limits::max() }; diff --git a/libs/common/src/CMakeLists.txt b/libs/common/src/CMakeLists.txt index 645d6412f..8ba969cb4 100644 --- a/libs/common/src/CMakeLists.txt +++ b/libs/common/src/CMakeLists.txt @@ -40,11 +40,6 @@ add_library(${LIBNAME} OBJECT config/http_properties.cpp config/data_source_builder.cpp config/http_properties_builder.cpp - config/data_system/bootstrap_builder.cpp - config/data_system/background_sync_builder.cpp - config/data_system/data_destination_builder.cpp - config/data_system/data_system_builder.cpp - config/data_system/lazy_load_builder.cpp bindings/c/value.cpp bindings/c/array_builder.cpp bindings/c/object_builder.cpp diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 10c6e7d1d..91a32b81c 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -25,7 +25,7 @@ struct MethodVisitor { using SDK = ServerSDK; using Result = tl::expected, built::PollingConfig, - built::RedisPullConfig>, + built::RedisConfig>, Error>; Result operator()(StreamingBuilder const& streaming) const { @@ -36,11 +36,11 @@ struct MethodVisitor { return polling.Build(); } - Result operator()(RedisPullBuilder const& redis_pull) const { + Result operator()(RedisBuilder const& redis_pull) const { return redis_pull.Build().map([](auto&& config) { return std::variant, - built::PollingConfig, - built::RedisPullConfig>{std::move(config)}; + built::PollingConfig, built::RedisConfig>{ + std::move(config)}; }); } }; @@ -77,40 +77,6 @@ built::PollingConfig PollingBuilder::Build() const { return config_; } -RedisPullBuilder::RedisPullBuilder() - : config_(Defaults::RedisPullConfig()) {} - -RedisPullBuilder& RedisPullBuilder::Connection(ConnOpts opts) { - config_.connection_ = opts; - return *this; -} - -RedisPullBuilder& RedisPullBuilder::Connection(ConnURI uri) { - config_.connection_ = uri; - return *this; -} - -tl::expected RedisPullBuilder::Build() const { - if (std::holds_alternative(config_.connection_)) { - auto const& opts = std::get(config_.connection_); - if (opts.host.empty()) { - return tl::make_unexpected( - Error::kConfig_DataSource_Redis_MissingHost); - } - if (!opts.port) { - return tl::make_unexpected( - Error::kConfig_DataSource_Redis_MissingPort); - } - } else { - auto const& uri = std::get(config_.connection_); - if (uri.empty()) { - return tl::make_unexpected( - Error::kConfig_DataSource_Redis_MissingURI); - } - } - return config_; -} - DataSourceBuilder::DataSourceBuilder() : with_reasons_(false), use_report_(false), method_(Streaming()) {} diff --git a/libs/common/src/config/data_system/background_sync_builder.cpp b/libs/common/src/config/data_system/background_sync_builder.cpp deleted file mode 100644 index a77ee27ff..000000000 --- a/libs/common/src/config/data_system/background_sync_builder.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include - -namespace launchdarkly::config::shared::builders { - -BackgroundSyncBuilder::BackgroundSyncBuilder() - : bootstrap_builder_(), config_() {} - -BootstrapBuilder& BackgroundSyncBuilder::Bootstrapper() { - return bootstrap_builder_; -} - -BackgroundSyncBuilder& -BackgroundSyncBuilder::Synchronizer(Streaming source) { - config_.source_.method = source.Build(); - return *this; -} - -BackgroundSyncBuilder& -BackgroundSyncBuilder::Synchronizer(Polling source) { - config_.source_.method = source.Build(); - return *this; -} - -BackgroundSyncBuilder& BackgroundSyncBuilder::Destination( - DataDestinationBuilder destination) { - config_.destination_ = destination.Build(); - return *this; -} - -[[nodiscard]] built::BackgroundSyncConfig -BackgroundSyncBuilder::Build() const { - auto const bootstrap_cfg = bootstrap_builder_.Build(); - auto copy = config_; - copy.bootstrap_ = bootstrap_cfg; - return copy; -} - -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/bootstrap_builder.cpp b/libs/common/src/config/data_system/bootstrap_builder.cpp deleted file mode 100644 index 277db045c..000000000 --- a/libs/common/src/config/data_system/bootstrap_builder.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include - -#include "launchdarkly/config/shared/defaults.hpp" - -namespace launchdarkly::config::shared::builders { - -BootstrapBuilder::BootstrapBuilder() - : config_(Defaults::BootstrapConfig()) {} - -std::optional BootstrapBuilder::Build() const { - return config_; -} -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/data_destination_builder.cpp b/libs/common/src/config/data_system/data_destination_builder.cpp deleted file mode 100644 index 7979020e5..000000000 --- a/libs/common/src/config/data_system/data_destination_builder.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include - -namespace launchdarkly::config::shared::builders { - -DataDestinationBuilder::DataDestinationBuilder() : config_() {} - -[[nodiscard]] config::shared::built::DataDestinationConfig -DataDestinationBuilder::Build() const { - return config_; -} - -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/data_system_builder.cpp b/libs/common/src/config/data_system/data_system_builder.cpp deleted file mode 100644 index 554dfe8ac..000000000 --- a/libs/common/src/config/data_system/data_system_builder.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include - -namespace launchdarkly::config::shared::builders { - -DataSystemBuilder::DataSystemBuilder() - : method_builder_(std::nullopt), - config_(Defaults::DataSystemConfig()) {} - -DataSystemBuilder& DataSystemBuilder::Method( - BackgroundSync bg_sync) { - method_builder_ = std::move(bg_sync); - return *this; -} - -DataSystemBuilder& DataSystemBuilder::Method( - LazyLoad lazy_load) { - method_builder_ = std::move(lazy_load); - return *this; -} - -DataSystemBuilder& DataSystemBuilder::Disabled( - bool const disabled) { - config_.disabled = disabled; - return *this; -} - -tl::expected, Error> -DataSystemBuilder::Build() const { - if (method_builder_) { - auto lazy_or_background_cfg = std::visit( - [](auto&& arg) - -> tl::expected< - std::variant>, - Error> { - using T = std::decay_t; - if constexpr (std::is_same_v) { - return arg.Build(); // -> built::BackgroundSyncConfig - } else if constexpr (std::is_same_v) { - return arg - .Build(); // -> tl::expected - } - }, - *method_builder_); - if (!lazy_or_background_cfg) { - return tl::make_unexpected(lazy_or_background_cfg.error()); - } - return built::DataSystemConfig{ - config_.disabled, std::move(*lazy_or_background_cfg)}; - } - return config_; -} - -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/config/data_system/lazy_load_builder.cpp b/libs/common/src/config/data_system/lazy_load_builder.cpp deleted file mode 100644 index 382fc3509..000000000 --- a/libs/common/src/config/data_system/lazy_load_builder.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include - -namespace launchdarkly::config::shared::builders { - -LazyLoadBuilder::LazyLoadBuilder() - : redis_builder_(), config_(Defaults::LazyLoadConfig()) {} - -LazyLoadBuilder& LazyLoadBuilder::CacheTTL( - std::chrono::milliseconds const ttl) { - config_.eviction_ttl = ttl; - return *this; -} - -LazyLoadBuilder& LazyLoadBuilder::CacheEviction(EvictionPolicy const policy) { - config_.eviction_policy = policy; - return *this; -} - -LazyLoadBuilder& LazyLoadBuilder::Source(Redis source) { - redis_builder_ = std::move(source); - return *this; -} - -tl::expected LazyLoadBuilder::Build() const { - auto redis_config = redis_builder_.Build(); - if (!redis_config) { - return tl::make_unexpected(redis_config.error()); - } - auto copy = config_; - copy.source = {*redis_config}; - return copy; -} - -} // namespace launchdarkly::config::shared::builders diff --git a/libs/common/src/error.cpp b/libs/common/src/error.cpp index 454608bdd..2f2eef527 100644 --- a/libs/common/src/error.cpp +++ b/libs/common/src/error.cpp @@ -30,12 +30,8 @@ char const* ErrorToString(Error err) { return "events: capacity must be non-zero"; case Error::kConfig_SDKKey_Empty: return "sdk key: cannot be empty"; - case Error::kConfig_DataSource_Redis_MissingHost: - return "data system: Redis host cannot be empty"; - case Error::kConfig_DataSource_Redis_MissingURI: - return "data system: Redis URI cannot be empty"; - case Error::kConfig_DataSource_Redis_MissingPort: - return "data system: Redis port must be specified"; + case Error::kConfig_DataSystem_LazyLoad_MissingSource: + return "data system: lazy load config requires a source"; case Error::kMax: break; } diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/background_sync_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/background_sync_builder.hpp new file mode 100644 index 000000000..4ceabcea7 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/background_sync_builder.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include +#include + +namespace launchdarkly::server_side::config::builders { + +struct BackgroundSyncBuilder { + using Streaming = + launchdarkly::config::shared::builders::StreamingBuilder; + using Polling = launchdarkly::config::shared::builders::PollingBuilder; + + BackgroundSyncBuilder(); + + BootstrapBuilder& Bootstrapper(); + + BackgroundSyncBuilder& Synchronizer(Streaming source); + BackgroundSyncBuilder& Synchronizer(Polling source); + + BackgroundSyncBuilder& Destination(DataDestinationBuilder destination); + + [[nodiscard]] built::BackgroundSyncConfig Build() const; + + private: + BootstrapBuilder bootstrap_builder_; + built::BackgroundSyncConfig config_; +}; + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/bootstrap_builder.hpp similarity index 54% rename from libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp rename to libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/bootstrap_builder.hpp index 7d42b45dd..2eca9f28d 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/bootstrap_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/bootstrap_builder.hpp @@ -1,10 +1,10 @@ #pragma once -#include +#include #include -namespace launchdarkly::config::shared::builders { +namespace launchdarkly::server_side::config::builders { class BootstrapBuilder { public: @@ -15,4 +15,4 @@ class BootstrapBuilder { private: std::optional config_; }; -} // namespace launchdarkly::config::shared::builders +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_destination_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_destination_builder.hpp new file mode 100644 index 000000000..0c3d2ccdb --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_destination_builder.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::config::builders { + +struct DataDestinationBuilder { + DataDestinationBuilder(); + + [[nodiscard]] built::DataDestinationConfig Build() const; + + private: + built::DataDestinationConfig config_; +}; + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp new file mode 100644 index 000000000..deb89c75a --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +#include + +namespace launchdarkly::server_side::config::builders { + +class DataSystemBuilder { + public: + DataSystemBuilder(); + using BackgroundSync = BackgroundSyncBuilder; + using LazyLoad = LazyLoadBuilder; + + DataSystemBuilder& Disabled(bool disabled); + + DataSystemBuilder& Method(BackgroundSync bg_sync); + DataSystemBuilder& Method(LazyLoad lazy_load); + + [[nodiscard]] tl::expected Build() const; + + private: + std::optional> method_builder_; + built::DataSystemConfig config_; +}; + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp similarity index 76% rename from libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp rename to libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp index 2d7318679..abe68288c 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_system/lazy_load_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp @@ -1,13 +1,14 @@ #pragma once -#include -#include -#include +#include +#include + #include #include +#include -namespace launchdarkly::config::shared::builders { +namespace launchdarkly::server_side::config::builders { /** * \brief LazyLoadBuilder allows for specifying the configuration of @@ -24,7 +25,8 @@ namespace launchdarkly::config::shared::builders { * another SDK) is necessary. */ struct LazyLoadBuilder { - using Redis = RedisPullBuilder; + using SourcePtr = + std::shared_ptr; using EvictionPolicy = built::LazyLoadConfig::EvictionPolicy; /** * \brief Constructs a new LazyLoadBuilder. @@ -32,12 +34,11 @@ struct LazyLoadBuilder { LazyLoadBuilder(); /** - * \brief Specify the source of the data. Currently, only Redis is - * supported. - * \param source The Redis configuration. + * \brief Specify the source of the data. + * \param source Component implementing ISerializedDataPullSource. * \return Reference to this. */ - LazyLoadBuilder& Source(Redis source); + LazyLoadBuilder& Source(SourcePtr source); /** * \brief @@ -58,8 +59,7 @@ struct LazyLoadBuilder { [[nodiscard]] tl::expected Build() const; private: - RedisPullBuilder redis_builder_; built::LazyLoadConfig config_; }; -} // namespace launchdarkly::config::shared::builders +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp new file mode 100644 index 000000000..a8edd9f09 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace launchdarkly::server_side::config::built { + +struct BackgroundSyncConfig { + std::optional bootstrap_; + launchdarkly::config::shared::built::DataSourceConfig< + launchdarkly::config::shared::ServerSDK> + source_; + std::optional destination_; +}; + +} // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/bootstrap_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/bootstrap_config.hpp new file mode 100644 index 000000000..763941d33 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/bootstrap_config.hpp @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::config::built { + +struct BootstrapConfig {}; +} // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_destination_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_destination_config.hpp new file mode 100644 index 000000000..74fdf2bd3 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_destination_config.hpp @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::config::built { + +struct DataDestinationConfig {}; + +} // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp new file mode 100644 index 000000000..76dd478ed --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/data_system_config.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly::server_side::config::built { + +struct DataSystemConfig { + bool disabled; + std::variant system_; +}; + +} // namespace launchdarkly::server_side::config::built diff --git a/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp similarity index 55% rename from libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp rename to libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp index 90e53d032..e4517def4 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_system/lazy_load_config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp @@ -1,8 +1,11 @@ #pragma once -#include +#include -namespace launchdarkly::config::shared::built { +#include +#include + +namespace launchdarkly::server_side::config::built { struct LazyLoadConfig { /** @@ -16,6 +19,7 @@ struct LazyLoadConfig { EvictionPolicy eviction_policy; std::chrono::milliseconds eviction_ttl; - DataSourceConfig source; + std::shared_ptr + source; }; -} // namespace launchdarkly::config::shared::built +} // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp index ba516b908..424bff1f9 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -1,51 +1,50 @@ #pragma once -#include #include #include #include #include -#include -namespace launchdarkly::server_side { +#include -using SDK = config::shared::ServerSDK; +namespace launchdarkly::server_side { struct Config { public: Config(std::string sdk_key, - config::shared::built::Logging logging, - config::shared::built::ServiceEndpoints endpoints, - config::shared::built::Events events, + launchdarkly::config::shared::built::Logging logging, + launchdarkly::config::shared::built::ServiceEndpoints endpoints, + launchdarkly::config::shared::built::Events events, std::optional application_tag, - config::shared::built::DataSystemConfig data_system_config, - config::shared::built::HttpProperties http_properties); + config::built::DataSystemConfig data_system_config, + launchdarkly::config::shared::built::HttpProperties http_properties); [[nodiscard]] std::string const& SdkKey() const; - [[nodiscard]] config::shared::built::ServiceEndpoints const& + [[nodiscard]] launchdarkly::config::shared::built::ServiceEndpoints const& ServiceEndpoints() const; - [[nodiscard]] config::shared::built::Events const& Events() const; + [[nodiscard]] launchdarkly::config::shared::built::Events const& Events() + const; [[nodiscard]] std::optional const& ApplicationTag() const; - config::shared::built::DataSystemConfig const& DataSystemConfig() - const; + config::built::DataSystemConfig const& DataSystemConfig() const; - [[nodiscard]] config::shared::built::HttpProperties const& HttpProperties() - const; + [[nodiscard]] launchdarkly::config::shared::built::HttpProperties const& + HttpProperties() const; - [[nodiscard]] config::shared::built::Logging const& Logging() const; + [[nodiscard]] launchdarkly::config::shared::built::Logging const& Logging() + const; private: std::string sdk_key_; bool offline_; - config::shared::built::Logging logging_; - config::shared::built::ServiceEndpoints service_endpoints_; + launchdarkly::config::shared::built::Logging logging_; + launchdarkly::config::shared::built::ServiceEndpoints service_endpoints_; std::optional application_tag_; - config::shared::built::Events events_; - config::shared::built::DataSystemConfig data_system_config_; - config::shared::built::HttpProperties http_properties_; + launchdarkly::config::shared::built::Events events_; + config::built::DataSystemConfig data_system_config_; + launchdarkly::config::shared::built::HttpProperties http_properties_; }; } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 1dc3e7ca4..4c5e03a0c 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -1,13 +1,16 @@ #pragma once -#include #include -#include #include #include +#include + +#include #include namespace launchdarkly::server_side { +using DataSystemBuilder = config::builders::DataSystemBuilder; + class ConfigBuilder { public: using Result = Config; diff --git a/libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_pull_source.hpp similarity index 100% rename from libs/server-sdk/src/data_interfaces/source/iserialized_pull_source.hpp rename to libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_pull_source.hpp diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp similarity index 89% rename from libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp rename to libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index 2dab0f3d9..c895fb976 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -1,8 +1,6 @@ #pragma once -#include - -#include "../../../../data_interfaces/source/iserialized_pull_source.hpp" +#include #include diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 040c5d47e..811137913 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -37,6 +37,11 @@ target_sources(${LIBNAME} data_systems/background_sync/background_sync_system.cpp data_systems/background_sync/sources/streaming/event_handler.cpp data_systems/lazy_load/lazy_load_system.cpp + config/builders/data_system/background_sync_builder.cpp + config/builders/data_system/bootstrap_builder.cpp + config/builders/data_system/data_destination_builder.cpp + config/builders/data_system/data_system_builder.cpp + config/builders/data_system/lazy_load_builder.cpp data_source_status.cpp evaluation/evaluator.cpp evaluation/rules.cpp @@ -56,7 +61,7 @@ target_sources(${LIBNAME} if (LD_BUILD_REDIS_SUPPORT) target_sources(${LIBNAME} PRIVATE - data_systems/lazy_load/sources/redis/redis_source.cpp) + integrations/redis/redis_source.cpp) target_link_libraries(${LIBNAME} PRIVATE hiredis::hiredis redis++::redis++_static) endif () diff --git a/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp b/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp new file mode 100644 index 000000000..c05d7622b --- /dev/null +++ b/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp @@ -0,0 +1,35 @@ +#include + +namespace launchdarkly::server_side::config::builders { + +BackgroundSyncBuilder::BackgroundSyncBuilder() + : bootstrap_builder_(), config_() {} + +BootstrapBuilder& BackgroundSyncBuilder::Bootstrapper() { + return bootstrap_builder_; +} + +BackgroundSyncBuilder& BackgroundSyncBuilder::Synchronizer(Streaming source) { + config_.source_.method = source.Build(); + return *this; +} + +BackgroundSyncBuilder& BackgroundSyncBuilder::Synchronizer(Polling source) { + config_.source_.method = source.Build(); + return *this; +} + +BackgroundSyncBuilder& BackgroundSyncBuilder::Destination( + DataDestinationBuilder destination) { + config_.destination_ = destination.Build(); + return *this; +} + +[[nodiscard]] built::BackgroundSyncConfig BackgroundSyncBuilder::Build() const { + auto const bootstrap_cfg = bootstrap_builder_.Build(); + auto copy = config_; + copy.bootstrap_ = bootstrap_cfg; + return copy; +} + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/config/builders/data_system/bootstrap_builder.cpp b/libs/server-sdk/src/config/builders/data_system/bootstrap_builder.cpp new file mode 100644 index 000000000..f33d7cf6a --- /dev/null +++ b/libs/server-sdk/src/config/builders/data_system/bootstrap_builder.cpp @@ -0,0 +1,12 @@ +#include + +#include "defaults.hpp" + +namespace launchdarkly::server_side::config::builders { + +BootstrapBuilder::BootstrapBuilder() : config_(Defaults::BootstrapConfig()) {} + +std::optional BootstrapBuilder::Build() const { + return config_; +} +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/config/builders/data_system/data_destination_builder.cpp b/libs/server-sdk/src/config/builders/data_system/data_destination_builder.cpp new file mode 100644 index 000000000..7a703d103 --- /dev/null +++ b/libs/server-sdk/src/config/builders/data_system/data_destination_builder.cpp @@ -0,0 +1,12 @@ +#include + +namespace launchdarkly::server_side::config::builders { + +DataDestinationBuilder::DataDestinationBuilder() : config_() {} + +[[nodiscard]] built::DataDestinationConfig DataDestinationBuilder::Build() + const { + return config_; +} + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp b/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp new file mode 100644 index 000000000..5816ae2ea --- /dev/null +++ b/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp @@ -0,0 +1,50 @@ +#include +#include "defaults.hpp" + +namespace launchdarkly::server_side::config::builders { + +DataSystemBuilder::DataSystemBuilder() + : method_builder_(std::nullopt), config_(Defaults::DataSystemConfig()) {} + +DataSystemBuilder& DataSystemBuilder::Method(BackgroundSync bg_sync) { + method_builder_ = std::move(bg_sync); + return *this; +} + +DataSystemBuilder& DataSystemBuilder::Method(LazyLoad lazy_load) { + method_builder_ = std::move(lazy_load); + return *this; +} + +DataSystemBuilder& DataSystemBuilder::Disabled(bool const disabled) { + config_.disabled = disabled; + return *this; +} + +tl::expected DataSystemBuilder::Build() const { + if (method_builder_) { + auto lazy_or_background_cfg = std::visit( + [](auto&& arg) + -> tl::expected, + Error> { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return arg.Build(); // -> built::BackgroundSyncConfig + } else if constexpr (std::is_same_v) { + return arg + .Build(); // -> tl::expected + } + }, + *method_builder_); + if (!lazy_or_background_cfg) { + return tl::make_unexpected(lazy_or_background_cfg.error()); + } + return built::DataSystemConfig{config_.disabled, + std::move(*lazy_or_background_cfg)}; + } + return config_; +} + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/config/builders/data_system/defaults.hpp b/libs/server-sdk/src/config/builders/data_system/defaults.hpp new file mode 100644 index 000000000..43c6f734f --- /dev/null +++ b/libs/server-sdk/src/config/builders/data_system/defaults.hpp @@ -0,0 +1,35 @@ +#pragma once +#include +#include +#include +#include + +namespace launchdarkly::server_side::config { + +struct Defaults { + // No bootstrap phase yet in server-sdk; instead full + // sync is done when polling/streaming source initializes. + static auto BootstrapConfig() -> std::optional { + return std::nullopt; + } + + // Data isn't mirrored anywhere by default. + static auto DataDestinationConfig() + -> std::optional { + return std::nullopt; + } + + static auto BackgroundSyncConfig() -> built::BackgroundSyncConfig { + return {BootstrapConfig(), DataSourceConfig(), DataDestinationConfig()}; + } + + static auto LazyLoadConfig() -> built::LazyLoadConfig { + return {built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::minutes{5}, nullptr}; + } + + static auto DataSystemConfig() -> built::DataSystemConfig { + return {false, BackgroundSyncConfig()}; + } +}; +} // namespace launchdarkly::server_side::config diff --git a/libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp b/libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp new file mode 100644 index 000000000..06cef97f2 --- /dev/null +++ b/libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp @@ -0,0 +1,33 @@ +#include + +#include "defaults.hpp" + +namespace launchdarkly::server_side::config::builders { + +LazyLoadBuilder::LazyLoadBuilder() : config_(Defaults::LazyLoadConfig()) {} + +LazyLoadBuilder& LazyLoadBuilder::CacheTTL( + std::chrono::milliseconds const ttl) { + config_.eviction_ttl = ttl; + return *this; +} + +LazyLoadBuilder& LazyLoadBuilder::CacheEviction(EvictionPolicy const policy) { + config_.eviction_policy = policy; + return *this; +} + +LazyLoadBuilder& LazyLoadBuilder::Source(SourcePtr source) { + config_.source = source; + return *this; +} + +tl::expected LazyLoadBuilder::Build() const { + if (!config_.source) { + return tl::make_unexpected( + Error::kConfig_DataSystem_LazyLoad_MissingSource); + } + return config_; +} + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/src/config/config.cpp b/libs/server-sdk/src/config/config.cpp index a49ec3b0c..0e2e2dba7 100644 --- a/libs/server-sdk/src/config/config.cpp +++ b/libs/server-sdk/src/config/config.cpp @@ -2,13 +2,16 @@ namespace launchdarkly::server_side { +using namespace launchdarkly::config::shared; + Config::Config(std::string sdk_key, - config::shared::built::Logging logging, - config::shared::built::ServiceEndpoints service_endpoints, - config::shared::built::Events events, + built::Logging logging, + built::ServiceEndpoints service_endpoints, + built::Events events, std::optional application_tag, - config::shared::built::DataSystemConfig data_system_config, - config::shared::built::HttpProperties http_properties) + launchdarkly::server_side::config::built::DataSystemConfig + data_system_config, + built::HttpProperties http_properties) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), service_endpoints_(std::move(service_endpoints)), @@ -21,12 +24,11 @@ std::string const& Config::SdkKey() const { return sdk_key_; } -config::shared::built::ServiceEndpoints const& Config::ServiceEndpoints() - const { +built::ServiceEndpoints const& Config::ServiceEndpoints() const { return service_endpoints_; } -config::shared::built::Events const& Config::Events() const { +built::Events const& Config::Events() const { return events_; } @@ -34,16 +36,16 @@ std::optional const& Config::ApplicationTag() const { return application_tag_; } -config::shared::built::DataSystemConfig const& +launchdarkly::server_side::config::built::DataSystemConfig const& Config::DataSystemConfig() const { return data_system_config_; } -config::shared::built::HttpProperties const& Config::HttpProperties() const { +built::HttpProperties const& Config::HttpProperties() const { return http_properties_; } -config::shared::built::Logging const& Config::Logging() const { +built::Logging const& Config::Logging() const { return logging_; } diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp index 33d49a641..d1dbb3ed7 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp @@ -12,33 +12,6 @@ namespace launchdarkly::server_side::data_components { JsonSource::JsonSource(data_interfaces::ISerializedDataPullSource& json_source) : flag_kind_(), segment_kind_(), source_(json_source) {} -template -static std::optional> Deserialize( - integrations::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; -} data_interfaces::IPullSource::ItemResult JsonSource::GetFlag(std::string const& key) const { @@ -50,13 +23,24 @@ JsonSource::GetSegment(std::string const& key) const { return Deserialize(segment_kind_, key); } -std::unordered_map +data_interfaces::IPullSource::AllResult JsonSource::AllFlags() const { // TODO: deserialize then return data_interfaces::ISerializedDataPullSource::AllResult result = source_.All(flag_kind_); + if (!result) { + return tl::make_unexpected(result.error().message); + } + + AllResult flags; + for (auto [key, val] : *result) { + auto deserialized = Deserialize(std::move(val)); + + } + return flags; } -std::unordered_map + +data_interfaces::IPullSource::AllResult JsonSource::AllSegments() const { // TODO: deserialize then return diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp index 8222cee81..34100e05c 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.hpp @@ -1,11 +1,10 @@ -#include "../../data_interfaces/destination/idestination.hpp" -#include "../../data_interfaces/destination/iserialized_destination.hpp" +#pragma once #include "../../data_interfaces/source/ipull_source.hpp" -#include "../../data_interfaces/source/iserialized_pull_source.hpp" - #include "../kinds/kinds.hpp" +#include + namespace launchdarkly::server_side::data_components { class JsonSource final : public data_interfaces::IPullSource { @@ -19,10 +18,12 @@ class JsonSource final : public data_interfaces::IPullSource { [[nodiscard]] ItemResult GetSegment( std::string const& key) const override; - [[nodiscard]] std::unordered_map - AllFlags() const override; - [[nodiscard]] std::unordered_map - AllSegments() const override; + [[nodiscard]] AllResult AllFlags() + const override; + + [[nodiscard]] AllResult AllSegments() + const override; + [[nodiscard]] std::string const& Identity() const override; [[nodiscard]] bool Initialized() const override; diff --git a/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp index b18c9a283..e0cfdff0d 100644 --- a/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp @@ -15,6 +15,10 @@ class IPullSource { template using ItemResult = tl::expected, std::string>; + template + using AllResult = + tl::expected, std::string>; + [[nodiscard]] virtual ItemResult GetFlag( std::string const& key) const = 0; @@ -33,18 +37,16 @@ class IPullSource { * * @return Returns an unordered map of FlagDescriptors. */ - [[nodiscard]] virtual std::unordered_map - AllFlags() const = 0; + [[nodiscard]] virtual AllResult AllFlags() + const = 0; /** * Get all of the segments. * * @return Returns an unordered map of SegmentDescriptors. */ - [[nodiscard]] virtual std::unordered_map - AllSegments() const = 0; + [[nodiscard]] virtual AllResult AllSegments() + const = 0; [[nodiscard]] virtual std::string const& Identity() const = 0; diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/integrations/redis/redis_source.cpp similarity index 91% rename from libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp rename to libs/server-sdk/src/integrations/redis/redis_source.cpp index 52e1b7958..5d013f025 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/integrations/redis/redis_source.cpp @@ -1,4 +1,4 @@ -#include "redis_source.hpp" +#include namespace launchdarkly::server_side::data_systems { @@ -19,7 +19,7 @@ data_interfaces::ISerializedDataPullSource::GetResult RedisDataSource::Get( return integrations::SerializedItemDescriptor{0, false, maybe_item.value()}; } - return tl::make_unexpected(Error{"not found"}); + sw::redis::SentinelOptions return tl::make_unexpected(Error{"not found"}); } data_interfaces::ISerializedDataPullSource::AllResult RedisDataSource::All( diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index f45425dd5..0d788e7db 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -6,6 +6,11 @@ include_directories("${PROJECT_SOURCE_DIR}/src") file(GLOB tests "${PROJECT_SOURCE_DIR}/tests/*.cpp") +if (LD_BUILD_REDIS_SUPPORT) + file(GLOB redis_tests "${PROJECT_SOURCE_DIR}/tests/redis/*.cpp") + list(APPEND tests ${redis_tests}) +endif () + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # Get things in the same directory on windows. @@ -15,6 +20,11 @@ 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 timestamp redis++::redis++_static GTest::gtest_main) + +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main) + +if (LD_BUILD_REDIS_SUPPORT) + target_link_libraries(gtest_${LIBNAME} redis++::redis++_static) +endif () gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/redis_source_test.cpp b/libs/server-sdk/tests/redis/redis_source_test.cpp similarity index 86% rename from libs/server-sdk/tests/redis_source_test.cpp rename to libs/server-sdk/tests/redis/redis_source_test.cpp index a31ee482d..8ccb17d7a 100644 --- a/libs/server-sdk/tests/redis_source_test.cpp +++ b/libs/server-sdk/tests/redis/redis_source_test.cpp @@ -1,7 +1,7 @@ #include +#include #include "data_components/kinds/kinds.hpp" -#include "data_systems/lazy_load/sources/redis/redis_source.hpp" using namespace launchdarkly::server_side::data_systems; using namespace launchdarkly::server_side::data_components; From 8cc183d5d2b113e95dae62934d2546d5eded0c3b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 13:32:38 -0800 Subject: [PATCH 103/244] feat: add data system interfaces --- .../launchdarkly/data_model/descriptors.hpp | 11 + .../integrations/iserialized_item_kind.hpp | 43 ++++ .../integrations/persistent_store_core.hpp | 214 ------------------ .../serialized_item_descriptor.hpp | 28 +++ libs/server-sdk/src/CMakeLists.txt | 3 +- libs/server-sdk/src/data_interfaces.cpp | 9 + .../destination/idestination.hpp | 52 +++++ .../destination/iserialized_destination.hpp | 95 ++++++++ .../data_interfaces/source/ipull_source.hpp | 80 +++++++ .../data_interfaces/source/ipush_source.hpp | 54 +++++ .../src/data_interfaces/store/istore.hpp | 59 +++++ .../src/data_interfaces/system/isystem.hpp | 34 +++ .../persistent/persistent_data_store.cpp | 3 - .../persistent/persistent_data_store.hpp | 51 ----- 14 files changed, 466 insertions(+), 270 deletions(-) create mode 100644 libs/internal/include/launchdarkly/data_model/descriptors.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp delete mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/persistent_store_core.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp create mode 100644 libs/server-sdk/src/data_interfaces.cpp create mode 100644 libs/server-sdk/src/data_interfaces/destination/idestination.hpp create mode 100644 libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp create mode 100644 libs/server-sdk/src/data_interfaces/source/ipull_source.hpp create mode 100644 libs/server-sdk/src/data_interfaces/source/ipush_source.hpp create mode 100644 libs/server-sdk/src/data_interfaces/store/istore.hpp create mode 100644 libs/server-sdk/src/data_interfaces/system/isystem.hpp delete mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp delete mode 100644 libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp diff --git a/libs/internal/include/launchdarkly/data_model/descriptors.hpp b/libs/internal/include/launchdarkly/data_model/descriptors.hpp new file mode 100644 index 000000000..db33d579b --- /dev/null +++ b/libs/internal/include/launchdarkly/data_model/descriptors.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly::data_model { +using FlagDescriptor = ItemDescriptor; +using SegmentDescriptor = ItemDescriptor; +} // namespace launchdarkly::data_model diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp new file mode 100644 index 000000000..6d4fb4a68 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::integrations { + +/** + * \brief Represents the kind of a serialized item. The purpose of this + * interface is to allow for determining a serialized item's version without + * leaking the details of the serialization format to the calling code. + */ +class ISerializedItemKind { + public: + /** + * The namespace for the data. + */ + [[nodiscard]] virtual std::string const& Namespace() const = 0; + + /** + * 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. + * + * 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 std::uint64_t Version( + std::string const& data) const = 0; + + ISerializedItemKind(ISerializedItemKind const& item) = delete; + ISerializedItemKind(ISerializedItemKind&& item) = delete; + ISerializedItemKind& operator=(ISerializedItemKind const&) = delete; + ISerializedItemKind& operator=(ISerializedItemKind&&) = delete; + virtual ~ISerializedItemKind() = default; + + protected: + ISerializedItemKind() = default; +}; +} // namespace launchdarkly::server_side::integrations 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/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp new file mode 100644 index 000000000..0c6869c0a --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +namespace launchdarkly::server_side::integrations { +/** + * A versioned item which can be stored or loaded from a persistent store. + */ +struct SerializedItemDescriptor { + std::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; +}; + +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 5f234d148..161002267 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -41,6 +41,7 @@ target_sources(${LIBNAME} data_sources/streaming_data_source.hpp data_sources/streaming_data_source.cpp data_sources/null_data_source.cpp + data_interfaces.cpp evaluation/evaluator.cpp evaluation/rules.cpp evaluation/bucketing.cpp @@ -49,9 +50,7 @@ target_sources(${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 events/event_factory.cpp bindings/c/sdk.cpp diff --git a/libs/server-sdk/src/data_interfaces.cpp b/libs/server-sdk/src/data_interfaces.cpp new file mode 100644 index 000000000..bd5a40015 --- /dev/null +++ b/libs/server-sdk/src/data_interfaces.cpp @@ -0,0 +1,9 @@ +#include "data_interfaces/destination/idestination.hpp" +#include "data_interfaces/destination/iserialized_destination.hpp" +#include "data_interfaces/source/ipull_source.hpp" +#include "data_interfaces/source/ipush_source.hpp" +#include "data_interfaces/store/istore.hpp" +#include "data_interfaces/system/isystem.hpp" + +#include +#include diff --git a/libs/server-sdk/src/data_interfaces/destination/idestination.hpp b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp new file mode 100644 index 000000000..342acf49c --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/destination/idestination.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include + +#include + +namespace launchdarkly::server_side::data_interfaces { + +/** + * \brief IDestination represents a sink for data received by the + * SDK. A destination may be a database, local file, etc. + */ +class IDestination { + public: + /** + * \brief Initialize the destination with a base set of data. + * \param data_set The initial data received by the SDK. + */ + virtual void Init(data_model::SDKDataSet data_set) = 0; + + /** + * \brief Upsert a flag named by key. + * \param key Flag key. + * \param flag Flag descriptor. + */ + virtual void Upsert(std::string const& key, + data_model::FlagDescriptor flag) = 0; + + /** + * \brief Upsert a segment named by key. + * \param key Segment key. + * \param segment Segment descriptor. + */ + virtual void Upsert(std::string const& key, + data_model::SegmentDescriptor segment) = 0; + + /** + * \return Identity of the destination. Used in logs. + */ + [[nodiscard]] virtual std::string const& Identity() const = 0; + + IDestination(IDestination const& item) = delete; + IDestination(IDestination&& item) = delete; + IDestination& operator=(IDestination const&) = delete; + IDestination& operator=(IDestination&&) = delete; + virtual ~IDestination() = default; + + protected: + IDestination() = default; +}; +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp new file mode 100644 index 000000000..5953fe7db --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp @@ -0,0 +1,95 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_interfaces { + +class ISerializedDestination { + 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 + }; + + using Key = std::string; + + template + using Keyed = std::pair; + + using OrderedNamepace = + std::vector>; + + using ItemCollection = + std::pair; + + /** + * \brief Initializes the destination with data. + * \param sdk_data_set A series of collections, where each collection is + * named by an ISerializedItemKind and contains a list of key/value pairs + * representing the key of the item and the serialized form of the item. + * \return InitResult::kSuccess if all + * data items were stored, or InitResult::kError if any error occoured. + */ + virtual InitResult Init(std::vector sdk_data_set) = 0; + + /** + * \brief Upserts a single item (update if exist, insert if not.) + * \param kind The item kind. + * \param key The item key. + * \param item Serialized form of the item. The item should be deleted if + * the SerializedItem's 'deleted' bool is true. \return + * UpsertResult::kSuccess if the operation was successful. + * UpsertResult::kError if an error occured. Otherwise, + * UpsertResult::kNotUpdated if the existing item version was greater than + * the version passed in. + */ + virtual UpsertResult Upsert( + std::string const& kind, + std::string const& key, + integrations::SerializedItemDescriptor item) = 0; + + /** + * \return Identity of the destination. Used in logs. + */ + virtual std::string const& Identity() const = 0; + + ISerializedDestination(ISerializedDestination const& item) = delete; + ISerializedDestination(ISerializedDestination&& item) = delete; + ISerializedDestination& operator=(ISerializedDestination const&) = delete; + ISerializedDestination& operator=(ISerializedDestination&&) = delete; + virtual ~ISerializedDestination() = default; + + protected: + ISerializedDestination() = default; +}; +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp new file mode 100644 index 000000000..d6fd142c6 --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/source/ipull_source.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_interfaces { + +/** + * \brief IPullSource obtains data on-demand. Calls to obtain data may fail, so + * the getter methods use tl::expected in order to propagate error codes. + * + * The IPullSource does not perform caching, so parent components must be + * careful to avoid repeatedly fetching data (i.e. use a cache.) + * + */ +class IPullSource { + public: + using Error = std::string; + + template + using Single = tl::expected, Error>; + + template + using Collection = tl::expected, Error>; + + /** + * \brief Attempts to get a flag named by key. + * \param key Key of the flag. + * \return On success, an optional FlagDescriptor (std::nullopt means the + * flag doesn't exist.) On failure, an error string. + */ + [[nodiscard]] virtual Single GetFlag( + std::string const& key) const = 0; + + /** + * \brief Attempts to get a segment named by key. + * \param key Key of the segment. + * \return On success, an optional SegmentDescriptor (std::nullopt means the + * segment doesn't exist.) On failure, an error string. + */ + [[nodiscard]] virtual Single GetSegment( + std::string const& key) const = 0; + + /** + * \brief Attempts to get a collection of all flags. + * \return On success, a collection of FlagDescriptors. On failure, an error + * string. + */ + [[nodiscard]] virtual Collection AllFlags() + const = 0; + + /** + * \brief Attempts to get a collection of all segments. + * \return On success, a collection of SegmentDescriptors. On failure, an + * error string. + */ + [[nodiscard]] virtual Collection + AllSegments() const = 0; + + /** + * \return Identity of the source. Used in logs. + */ + [[nodiscard]] virtual std::string const& Identity() const = 0; + + virtual ~IPullSource() = default; + IPullSource(IPullSource const& item) = delete; + IPullSource(IPullSource&& item) = delete; + IPullSource& operator=(IPullSource const&) = delete; + IPullSource& operator=(IPullSource&&) = delete; + + protected: + IPullSource() = default; +}; + +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp new file mode 100644 index 000000000..fd24538c5 --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_interfaces { + +/** + * \brief IPushSource obtains data via a push synchronization mechanism, + * updating a local cache whenever changes are made upstream. + */ +class IPushSource { + public: + /** + * \brief Initialize the source, optionally with an initial data set. Init + * will be called before Start. + * \param initial_data Initial set of SDK data. + */ + virtual void Init(std::optional initial_data) = 0; + + /** + * \brief Starts the synchronization mechanism. Start will be called only + * once after Init; the source is responsible for maintaining a persistent + * connection. Start should not block. + */ + virtual void StartAsync() = 0; + + /** + * \brief Stops the synchronization mechanism. Stop will be called only once + * after StartAsync. Stop should not block, but should invoke the completion + * function once shutdown. + * \param complete A callback to be invoked on completion. + */ + virtual void ShutdownAsync(std::function complete) = 0; + + /** + * \return Identity of the source. Used in logs. + */ + [[nodiscard]] virtual std::string const& Identity() const = 0; + + virtual ~IPushSource() = default; + IPushSource(IPushSource const& item) = delete; + IPushSource(IPushSource&& item) = delete; + IPushSource& operator=(IPushSource const&) = delete; + IPushSource& operator=(IPushSource&&) = delete; + + protected: + IPushSource() = default; +}; + +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/store/istore.hpp b/libs/server-sdk/src/data_interfaces/store/istore.hpp new file mode 100644 index 000000000..1b5fd8bcc --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/store/istore.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include + +#include +#include +#include + +namespace launchdarkly::server_side::data_interfaces { + +/** + * \brief IStore provides shared ownership of flag and segment domain + * objects. + */ +class IStore { + public: + /** + * \brief Get the flag named by key. Returns nullptr if no such flag exists. + * \param key Key of the flag. + * \return Shared pointer to the flag. + */ + [[nodiscard]] virtual std::shared_ptr GetFlag( + std::string const& key) const = 0; + + /** + * \brief Get the segment named by key. Returns nullptr if no such flag + * exists. \param key Key of the segment. \return Shared pointer to the + * segment. + */ + [[nodiscard]] virtual std::shared_ptr + GetSegment(std::string const& key) const = 0; + + /** + * \brief Get a map of all flags. + * \return Map of shared pointers to flags. + */ + [[nodiscard]] virtual std:: + unordered_map> + AllFlags() const = 0; + + /** + * \brief Get a map of all segments. + * \return Map of shared pointers to segments. + */ + [[nodiscard]] virtual std::unordered_map< + std::string, + std::shared_ptr> + AllSegments() const = 0; + + virtual ~IStore() = default; + IStore(IStore const& item) = delete; + IStore(IStore&& item) = delete; + IStore& operator=(IStore const&) = delete; + IStore& operator=(IStore&&) = delete; + + protected: + IStore() = default; +}; +} // namespace launchdarkly::server_side::data_interfaces diff --git a/libs/server-sdk/src/data_interfaces/system/isystem.hpp b/libs/server-sdk/src/data_interfaces/system/isystem.hpp new file mode 100644 index 000000000..ad88b279f --- /dev/null +++ b/libs/server-sdk/src/data_interfaces/system/isystem.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "../store/istore.hpp" + +namespace launchdarkly::server_side::data_interfaces { + +/** + * \brief ISystem obtains data used for flag evaluations and makes it available + * to other components. + */ +class ISystem : public IStore { + public: + /** + * \return Identity of the system. Used in logs. + */ + [[nodiscard]] virtual std::string const& Identity() const = 0; + + /** + * \brief Initializes the system. This method will be called before any of + * the IStore methods are called. + */ + virtual void Initialize() = 0; + + virtual ~ISystem() = default; + ISystem(ISystem const& item) = delete; + ISystem(ISystem&& item) = delete; + ISystem& operator=(ISystem const&) = delete; + ISystem& operator=(ISystem&&) = delete; + + protected: + ISystem() = default; +}; + +} // namespace launchdarkly::server_side::data_interfaces 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 deleted file mode 100644 index 706f78796..000000000 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.cpp +++ /dev/null @@ -1,3 +0,0 @@ -#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 deleted file mode 100644 index 523e09889..000000000 --- a/libs/server-sdk/src/data_store/persistent/persistent_data_store.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#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 From c684a00e7aaf66c93a891cb2c3e13da6794747e6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 13:47:04 -0800 Subject: [PATCH 104/244] lints --- .../server_side/integrations/iserialized_item_kind.hpp | 1 + .../integrations/serialized_item_descriptor.hpp | 1 + .../destination/iserialized_destination.hpp | 7 ++++--- libs/server-sdk/src/data_interfaces/system/isystem.hpp | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp index 6d4fb4a68..81e2c0060 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include namespace launchdarkly::server_side::integrations { diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp index 0c6869c0a..4f92e9904 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp index 5953fe7db..37f936cda 100644 --- a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp @@ -60,7 +60,8 @@ class ISerializedDestination { * \return InitResult::kSuccess if all * data items were stored, or InitResult::kError if any error occoured. */ - virtual InitResult Init(std::vector sdk_data_set) = 0; + [[nodiscard]] virtual InitResult Init( + std::vector sdk_data_set) = 0; /** * \brief Upserts a single item (update if exist, insert if not.) @@ -73,7 +74,7 @@ class ISerializedDestination { * UpsertResult::kNotUpdated if the existing item version was greater than * the version passed in. */ - virtual UpsertResult Upsert( + [[nodiscard]] virtual UpsertResult Upsert( std::string const& kind, std::string const& key, integrations::SerializedItemDescriptor item) = 0; @@ -81,7 +82,7 @@ class ISerializedDestination { /** * \return Identity of the destination. Used in logs. */ - virtual std::string const& Identity() const = 0; + [[nodiscard]] virtual std::string const& Identity() const = 0; ISerializedDestination(ISerializedDestination const& item) = delete; ISerializedDestination(ISerializedDestination&& item) = delete; diff --git a/libs/server-sdk/src/data_interfaces/system/isystem.hpp b/libs/server-sdk/src/data_interfaces/system/isystem.hpp index ad88b279f..7a1a389e0 100644 --- a/libs/server-sdk/src/data_interfaces/system/isystem.hpp +++ b/libs/server-sdk/src/data_interfaces/system/isystem.hpp @@ -21,7 +21,7 @@ class ISystem : public IStore { */ virtual void Initialize() = 0; - virtual ~ISystem() = default; + virtual ~ISystem() override = default; ISystem(ISystem const& item) = delete; ISystem(ISystem&& item) = delete; ISystem& operator=(ISystem const&) = delete; From 3dc9dcbc1a6a5a4d1426fdb2c8dbf41e6daeb9ee Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 14:36:04 -0800 Subject: [PATCH 105/244] rename data_store_updater -> change_notifier --- libs/server-sdk/src/CMakeLists.txt | 4 +- .../change_notifier/change_notifier.cpp} | 42 +++++++++++-------- .../change_notifier/change_notifier.hpp} | 41 ++++++++++-------- 3 files changed, 49 insertions(+), 38 deletions(-) rename libs/server-sdk/src/{data_store/data_store_updater.cpp => data_components/change_notifier/change_notifier.cpp} (58%) rename libs/server-sdk/src/{data_store/data_store_updater.hpp => data_components/change_notifier/change_notifier.hpp} (74%) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 161002267..0528939bd 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -26,8 +26,6 @@ target_sources(${LIBNAME} 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 - data_store/data_store_updater.cpp data_store/memory_store.cpp data_store/dependency_tracker.hpp data_store/dependency_tracker.cpp @@ -41,6 +39,8 @@ target_sources(${LIBNAME} data_sources/streaming_data_source.hpp data_sources/streaming_data_source.cpp data_sources/null_data_source.cpp + data_components/change_notifier/change_notifier.hpp + data_components/change_notifier/change_notifier.cpp data_interfaces.cpp evaluation/evaluator.cpp evaluation/rules.cpp diff --git a/libs/server-sdk/src/data_store/data_store_updater.cpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp similarity index 58% rename from libs/server-sdk/src/data_store/data_store_updater.cpp rename to libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp index 973a7d585..c18e8eda2 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.cpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier.cpp @@ -1,11 +1,11 @@ -#include "data_store_updater.hpp" +#include "change_notifier.hpp" #include #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_components { -std::unique_ptr DataStoreUpdater::OnFlagChange( +std::unique_ptr ChangeNotifier::OnFlagChange( launchdarkly::server_side::IChangeNotifier::ChangeHandler handler) { std::lock_guard lock{signal_mutex_}; @@ -13,7 +13,7 @@ std::unique_ptr DataStoreUpdater::OnFlagChange( signals_.connect(handler)); } -void DataStoreUpdater::Init(launchdarkly::data_model::SDKDataSet data_set) { +void ChangeNotifier::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. @@ -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, source_.AllFlags(), data_set.flags, updated_items); - CalculateChanges(DataKind::kSegment, store_.AllSegments(), + CalculateChanges(DataKind::kSegment, source_.AllSegments(), data_set.segments, updated_items); change_notifications = updated_items; } @@ -44,23 +44,23 @@ 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)); +void ChangeNotifier::Upsert(std::string const& key, + data_model::FlagDescriptor flag) { + UpsertCommon(DataKind::kFlag, key, source_.GetFlag(key), std::move(flag)); } -void DataStoreUpdater::Upsert(std::string const& key, - data_store::SegmentDescriptor segment) { - UpsertCommon(DataKind::kSegment, key, store_.GetSegment(key), +void ChangeNotifier::Upsert(std::string const& key, + data_model::SegmentDescriptor segment) { + UpsertCommon(DataKind::kSegment, key, source_.GetSegment(key), std::move(segment)); } -bool DataStoreUpdater::HasListeners() const { +bool ChangeNotifier::HasListeners() const { std::lock_guard lock{signal_mutex_}; return !signals_.empty(); } -void DataStoreUpdater::NotifyChanges(DependencySet changes) { +void ChangeNotifier::NotifyChanges(DependencySet changes) { std::lock_guard lock{signal_mutex_}; auto flag_changes = changes.SetForKind(DataKind::kFlag); // Only emit an event if there are changes. @@ -69,8 +69,14 @@ void DataStoreUpdater::NotifyChanges(DependencySet changes) { } } -DataStoreUpdater::DataStoreUpdater(IDataSourceUpdateSink& sink, - IDataStore const& store) - : sink_(sink), store_(store) {} +ChangeNotifier::ChangeNotifier(IDestination& sink, + data_interfaces::IStore const& source) + : sink_(sink), source_(source) {} -} // namespace launchdarkly::server_side::data_store +std::string const& ChangeNotifier::Identity() const { + static std::string const identity = + "change notifier for " + sink_.Identity(); + return identity; +} + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_store/data_store_updater.hpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp similarity index 74% rename from libs/server-sdk/src/data_store/data_store_updater.hpp rename to libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp index 0b5a2e153..cb265298d 100644 --- a/libs/server-sdk/src/data_store/data_store_updater.hpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp @@ -1,20 +1,20 @@ #pragma once -#include "../data_sources/data_source_update_sink.hpp" -#include "data_store.hpp" -#include "dependency_tracker.hpp" +#include "../../data_interfaces/destination/idestination.hpp" +#include "../../data_interfaces/store/istore.hpp" +#include "../dependency_tracker/dependency_tracker.hpp" +#include #include #include #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_components { -class DataStoreUpdater - : public launchdarkly::server_side::data_sources::IDataSourceUpdateSink, - public launchdarkly::server_side::IChangeNotifier { +class ChangeNotifier : public data_interfaces::IDestination, + public IChangeNotifier { public: template using Collection = data_model::SDKDataSet::Collection; @@ -26,19 +26,24 @@ class DataStoreUpdater using SharedCollection = std::unordered_map>; - DataStoreUpdater(IDataSourceUpdateSink& sink, IDataStore const& store); + ChangeNotifier(IDestination& sink, data_interfaces::IStore const& source); 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; + void Upsert(std::string const& key, + data_model::FlagDescriptor flag) override; + void Upsert(std::string const& key, + data_model::SegmentDescriptor segment) override; - DataStoreUpdater(DataStoreUpdater const& item) = delete; - DataStoreUpdater(DataStoreUpdater&& item) = delete; - DataStoreUpdater& operator=(DataStoreUpdater const&) = delete; - DataStoreUpdater& operator=(DataStoreUpdater&&) = delete; + [[nodiscard]] std::string const& Identity() const override; + + ~ChangeNotifier() override = default; + + ChangeNotifier(ChangeNotifier const& item) = delete; + ChangeNotifier(ChangeNotifier&& item) = delete; + ChangeNotifier& operator=(ChangeNotifier const&) = delete; + ChangeNotifier& operator=(ChangeNotifier&&) = delete; private: bool HasListeners() const; @@ -101,8 +106,8 @@ class DataStoreUpdater void NotifyChanges(DependencySet changes); - IDataSourceUpdateSink& sink_; - IDataStore const& store_; + IDestination& sink_; + data_interfaces::IStore const& source_; boost::signals2::signal)> signals_; @@ -118,4 +123,4 @@ class DataStoreUpdater DependencyTracker dependency_tracker_; }; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_components From a54c76e674170d0dd869ce3c80efee0c7487bc05 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 14:43:55 -0800 Subject: [PATCH 106/244] move dependency tracker --- libs/server-sdk/src/CMakeLists.txt | 4 +- .../dependency_tracker/data_kind.hpp | 7 ++++ .../dependency_tracker.cpp | 0 .../dependency_tracker.hpp | 15 ++++---- .../dependency_tracker/tagged_data.hpp | 26 +++++++++++++ libs/server-sdk/src/data_store/data_kind.hpp | 7 ---- .../server-sdk/src/data_store/tagged_data.hpp | 37 ------------------- 7 files changed, 42 insertions(+), 54 deletions(-) create mode 100644 libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp rename libs/server-sdk/src/{data_store => data_components/dependency_tracker}/dependency_tracker.cpp (100%) rename libs/server-sdk/src/{data_store => data_components/dependency_tracker}/dependency_tracker.hpp (97%) create mode 100644 libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp delete mode 100644 libs/server-sdk/src/data_store/data_kind.hpp delete mode 100644 libs/server-sdk/src/data_store/tagged_data.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 0528939bd..2cdcbcecc 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -27,8 +27,6 @@ target_sources(${LIBNAME} data_sources/data_source_update_sink.hpp data_store/data_store.hpp data_store/memory_store.cpp - data_store/dependency_tracker.hpp - data_store/dependency_tracker.cpp data_store/descriptors.hpp data_sources/data_source_event_handler.cpp data_sources/data_source_event_handler.hpp @@ -41,6 +39,8 @@ target_sources(${LIBNAME} data_sources/null_data_source.cpp data_components/change_notifier/change_notifier.hpp data_components/change_notifier/change_notifier.cpp + data_components/dependency_tracker/dependency_tracker.hpp + data_components/dependency_tracker/dependency_tracker.cpp data_interfaces.cpp evaluation/evaluator.cpp evaluation/rules.cpp diff --git a/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp b/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp new file mode 100644 index 000000000..4f231b6fa --- /dev/null +++ b/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace launchdarkly::server_side::data_components { +enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_store/dependency_tracker.cpp b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp similarity index 100% rename from libs/server-sdk/src/data_store/dependency_tracker.cpp rename to libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp diff --git a/libs/server-sdk/src/data_store/dependency_tracker.hpp b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp similarity index 97% rename from libs/server-sdk/src/data_store/dependency_tracker.hpp rename to libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp index d62ba2c7c..5bb1d0df9 100644 --- a/libs/server-sdk/src/data_store/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp @@ -1,19 +1,18 @@ #pragma once -#include -#include -#include -#include +#include "data_kind.hpp" +#include "tagged_data.hpp" #include #include #include #include -#include "data_kind.hpp" -#include "tagged_data.hpp" +#include +#include +#include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_components { /** * Class used to maintain a set of dependencies. Each dependency may be either @@ -153,4 +152,4 @@ class DependencyTracker { std::vector const& clauses); }; -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp b/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp new file mode 100644 index 000000000..28308b9ad --- /dev/null +++ b/libs/server-sdk/src/data_components/dependency_tracker/tagged_data.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "data_kind.hpp" + +namespace launchdarkly::server_side::data_components { +/** + * 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 const 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_; +}; + +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_store/data_kind.hpp b/libs/server-sdk/src/data_store/data_kind.hpp deleted file mode 100644 index 17ead105b..000000000 --- a/libs/server-sdk/src/data_store/data_kind.hpp +++ /dev/null @@ -1,7 +0,0 @@ -#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/tagged_data.hpp b/libs/server-sdk/src/data_store/tagged_data.hpp deleted file mode 100644 index 320146721..000000000 --- a/libs/server-sdk/src/data_store/tagged_data.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#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_; -}; - -} // namespace launchdarkly::server_side::data_store From 683e65b47e9083220e8704103c769007ee35f1e1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 14:56:57 -0800 Subject: [PATCH 107/244] move expiration tracker + update unit tests --- libs/server-sdk/src/CMakeLists.txt | 4 +- libs/server-sdk/src/client_impl.hpp | 4 +- .../change_notifier/change_notifier.hpp | 15 ++-- .../dependency_tracker/dependency_tracker.cpp | 11 +-- .../expiration_tracker.cpp | 13 ++-- .../expiration_tracker.hpp | 20 +++--- .../data_source_event_handler.hpp | 19 ++--- ...ater_test.cpp => change_notifier_test.cpp} | 70 +++++++++---------- .../tests/dependency_tracker_test.cpp | 19 ++--- .../tests/expiration_tracker_test.cpp | 5 +- 10 files changed, 83 insertions(+), 97 deletions(-) rename libs/server-sdk/src/{data_store/persistent => data_components/expiration_tracker}/expiration_tracker.cpp (93%) rename libs/server-sdk/src/{data_store/persistent => data_components/expiration_tracker}/expiration_tracker.hpp (90%) rename libs/server-sdk/tests/{data_store_updater_test.cpp => change_notifier_test.cpp} (86%) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 2cdcbcecc..d675a436d 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -41,6 +41,8 @@ target_sources(${LIBNAME} data_components/change_notifier/change_notifier.cpp data_components/dependency_tracker/dependency_tracker.hpp data_components/dependency_tracker/dependency_tracker.cpp + data_components/expiration_tracker/expiration_tracker.hpp + data_components/expiration_tracker/expiration_tracker.cpp data_interfaces.cpp evaluation/evaluator.cpp evaluation/rules.cpp @@ -50,8 +52,6 @@ target_sources(${LIBNAME} evaluation/detail/evaluation_stack.cpp evaluation/detail/semver_operations.cpp evaluation/detail/timestamp_operations.cpp - data_store/persistent/expiration_tracker.hpp - data_store/persistent/expiration_tracker.cpp events/event_factory.cpp bindings/c/sdk.cpp bindings/c/builder.cpp diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index d6c77ecee..f3940d648 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -13,7 +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_components/change_notifier/change_notifier.hpp" #include "data_store/memory_store.hpp" #include "evaluation/evaluator.hpp" @@ -176,7 +176,7 @@ class ClientImpl : public IClient { data_store::MemoryStore memory_store_; data_sources::DataSourceStatusManager status_manager_; - data_store::DataStoreUpdater data_store_updater_; + data_components::ChangeNotifier data_store_updater_; std::shared_ptr<::launchdarkly::data_sources::IDataSource> data_source_; diff --git a/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp index cb265298d..87518eabf 100644 --- a/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp +++ b/libs/server-sdk/src/data_components/change_notifier/change_notifier.hpp @@ -13,8 +13,8 @@ namespace launchdarkly::server_side::data_components { -class ChangeNotifier : public data_interfaces::IDestination, - public IChangeNotifier { +class ChangeNotifier final : public data_interfaces::IDestination, + public IChangeNotifier { public: template using Collection = data_model::SDKDataSet::Collection; @@ -30,7 +30,7 @@ class ChangeNotifier : public data_interfaces::IDestination, std::unique_ptr OnFlagChange(ChangeHandler handler) override; - void Init(launchdarkly::data_model::SDKDataSet data_set) override; + void Init(data_model::SDKDataSet data_set) override; void Upsert(std::string const& key, data_model::FlagDescriptor flag) override; void Upsert(std::string const& key, @@ -49,11 +49,10 @@ class ChangeNotifier : public data_interfaces::IDestination, bool HasListeners() const; template - void UpsertCommon( - DataKind kind, - std::string key, - SharedItem existing, - launchdarkly::data_model::ItemDescriptor updated) { + void UpsertCommon(DataKind kind, + std::string key, + SharedItem existing, + data_model::ItemDescriptor updated) { if (existing && (updated.version <= existing->version)) { // Out of order update, ignore it. return; diff --git a/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp index d401fe327..202fcdef8 100644 --- a/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp @@ -3,7 +3,7 @@ #include -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_components { DependencySet::DependencySet() : data_{ @@ -11,15 +11,16 @@ DependencySet::DependencySet() TaggedData>(DataKind::kSegment), } {} -void DependencySet::Set(DataKind kind, std::string key) { +void DependencySet::Set(DataKind const kind, std::string key) { Data(kind).emplace(std::move(key)); } -void DependencySet::Remove(DataKind kind, std::string const& key) { +void DependencySet::Remove(DataKind const kind, std::string const& key) { Data(kind).erase(key); } -bool DependencySet::Contains(DataKind kind, std::string const& key) const { +bool DependencySet::Contains(DataKind const kind, + std::string const& key) const { return Data(kind).count(key) != 0; } @@ -194,4 +195,4 @@ void DependencyTracker::Clear() { dependencies_from_.Clear(); } -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp similarity index 93% rename from libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp rename to libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp index 09e6142ef..d36c7f947 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.cpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp @@ -1,6 +1,8 @@ #include "expiration_tracker.hpp" -namespace launchdarkly::server_side::data_store::persistent { +#include + +namespace launchdarkly::server_side::data_components { void ExpirationTracker::Add(std::string const& key, ExpirationTracker::TimePoint expiration) { @@ -22,19 +24,18 @@ ExpirationTracker::TrackState ExpirationTracker::State( return ExpirationTracker::TrackState::kNotTracked; } -void ExpirationTracker::Add(data_store::DataKind kind, +void ExpirationTracker::Add(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) { +void ExpirationTracker::Remove(DataKind kind, std::string const& key) { scoped_.Remove(kind, key); } ExpirationTracker::TrackState ExpirationTracker::State( - data_store::DataKind kind, + DataKind kind, std::string const& key, ExpirationTracker::TimePoint current_time) const { auto expiration = scoped_.Get(kind, key); @@ -149,4 +150,4 @@ std::ostream& operator<<(std::ostream& out, } return out; } -} // namespace launchdarkly::server_side::data_store::persistent +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp similarity index 90% rename from libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp rename to libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp index bc57398c0..786b9d453 100644 --- a/libs/server-sdk/src/data_store/persistent/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp @@ -1,5 +1,10 @@ #pragma once +#include "../dependency_tracker/data_kind.hpp" +#include "../dependency_tracker/tagged_data.hpp" + +#include + #include #include #include @@ -7,12 +12,7 @@ #include #include -#include - -#include "../../data_store/data_kind.hpp" -#include "../tagged_data.hpp" - -namespace launchdarkly::server_side::data_store::persistent { +namespace launchdarkly::server_side::data_components { class ExpirationTracker { public: @@ -68,7 +68,7 @@ class ExpirationTracker { * @param key The key to track. * @param expiration The time that the key expires. */ - void Add(data_store::DataKind kind, + void Add(data_components::DataKind kind, std::string const& key, TimePoint expiration); @@ -78,7 +78,7 @@ class ExpirationTracker { * @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); + void Remove(data_components::DataKind kind, std::string const& key); /** * Check the state of a scoped key. @@ -87,7 +87,7 @@ class ExpirationTracker { * @param key The key to check. * @return The state of the key. */ - TrackState State(data_store::DataKind kind, + TrackState State(data_components::DataKind kind, std::string const& key, TimePoint current_time) const; @@ -142,4 +142,4 @@ class ExpirationTracker { std::ostream& operator<<(std::ostream& out, ExpirationTracker::TrackState const& state); -} // namespace launchdarkly::server_side::data_store::persistent +} // namespace launchdarkly::server_side::data_components 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 index 798796506..c0309da5b 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp @@ -4,7 +4,7 @@ #include -#include "../data_store/data_kind.hpp" +#include "../data_components/dependency_tracker/data_kind.hpp" #include "data_source_status_manager.hpp" #include "data_source_update_sink.hpp" @@ -29,10 +29,10 @@ struct SegmentsPath { static constexpr std::string_view path = "/segments/"; }; -template +template class StreamingDataKind { public: - static data_store::DataKind Kind() { return kind; } + static data_components::DataKind Kind() { return kind; } static bool IsKind(std::string const& patch_path) { return patch_path.rfind(TPath::path) == 0; } @@ -42,16 +42,17 @@ class StreamingDataKind { }; struct StreamingDataKinds { - using Flag = StreamingDataKind; + using Flag = StreamingDataKind; using Segment = - StreamingDataKind; + StreamingDataKind; - static std::optional Kind(std::string const& path) { + static std::optional Kind( + std::string const& path) { if (Flag::IsKind(path)) { - return data_store::DataKind::kFlag; + return data_components::DataKind::kFlag; } if (Segment::IsKind(path)) { - return data_store::DataKind::kSegment; + return data_components::DataKind::kSegment; } return std::nullopt; } @@ -99,7 +100,7 @@ class DataSourceEventHandler { struct Delete { std::string key; - data_store::DataKind kind; + data_components::DataKind kind; uint64_t version; }; diff --git a/libs/server-sdk/tests/data_store_updater_test.cpp b/libs/server-sdk/tests/change_notifier_test.cpp similarity index 86% rename from libs/server-sdk/tests/data_store_updater_test.cpp rename to libs/server-sdk/tests/change_notifier_test.cpp index 87fc139b9..1740c35a8 100644 --- a/libs/server-sdk/tests/data_store_updater_test.cpp +++ b/libs/server-sdk/tests/change_notifier_test.cpp @@ -1,36 +1,30 @@ #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; +#include +#include +#include using launchdarkly::Value; -using launchdarkly::data_model::Flag; -using launchdarkly::data_model::Segment; -TEST(DataStoreUpdaterTest, DoesNotInitializeStoreUntilInit) { +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::data_components; + +TEST(ChangeNotifierDestinationTest, DoesNotInitializeStoreUntilInit) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); EXPECT_FALSE(store.Initialized()); } -TEST(DataStoreUpdaterTest, InitializesStore) { +TEST(ChangeNotifierDestinationTest, InitializesStore) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet()); EXPECT_TRUE(store.Initialized()); } -TEST(DataStoreUpdaterTest, InitPropagatesData) { +TEST(ChangeNotifierDestinationTest, InitPropagatesData) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Flag flag; flag.version = 1; flag.key = "flagA"; @@ -65,9 +59,9 @@ TEST(DataStoreUpdaterTest, InitPropagatesData) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { +TEST(ChangeNotifierDestinationTest, SecondInitProducesChanges) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Flag flag_a_v1; flag_a_v1.version = 1; flag_a_v1.key = "flagA"; @@ -144,9 +138,9 @@ TEST(DataStoreUpdaterTest, SecondInitProducesChanges) { EXPECT_TRUE(got_event); } -TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { +TEST(ChangeNotifierDestinationTest, CanUpsertNewFlag) { MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Flag flag_a; flag_a.version = 1; @@ -166,13 +160,13 @@ TEST(DataStoreUpdaterTest, CanUpsertNewFlag) { EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); } -TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { +TEST(ChangeNotifierDestinationTest, CanUpsertExitingFlag) { Flag flag_a; flag_a.version = 1; flag_a.key = "flagA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -194,14 +188,14 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingFlag) { EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); } -TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { +TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertFlag) { Flag flag_a; flag_a.version = 2; flag_a.key = "flagA"; flag_a.variations = std::vector{"potato", "ham"}; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -228,13 +222,13 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertFlag) { EXPECT_EQ(std::string("ham"), fetched_flag->item->variations[1].AsString()); } -TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { +TEST(ChangeNotifierDestinationTest, CanUpsertNewSegment) { Segment segment_a; segment_a.version = 1; segment_a.key = "segmentA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -250,13 +244,13 @@ TEST(DataStoreUpdaterTest, CanUpsertNewSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { +TEST(ChangeNotifierDestinationTest, CanUpsertExitingSegment) { Segment segment_a; segment_a.version = 1; segment_a.key = "segmentA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -278,13 +272,13 @@ TEST(DataStoreUpdaterTest, CanUpsertExitingSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { +TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertSegment) { Segment segment_a; segment_a.version = 2; segment_a.key = "segmentA"; MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -306,7 +300,7 @@ TEST(DataStoreUpdaterTest, OldVersionIsDiscardedOnUpsertSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { +TEST(ChangeNotifierDestinationTest, ProducesChangeEventsOnUpsert) { Flag flag_a; Flag flag_b; @@ -319,7 +313,7 @@ TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -349,7 +343,7 @@ TEST(DataStoreUpdaterTest, ProducesChangeEventsOnUpsert) { EXPECT_EQ(true, got_event); } -TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { +TEST(ChangeNotifierDestinationTest, ProducesNoEventIfNoFlagChanged) { Flag flag_a; Flag flag_b; @@ -362,7 +356,7 @@ TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); Segment segment_a; segment_a.version = 1; @@ -392,7 +386,7 @@ TEST(DataStoreUpdaterTest, ProducesNoEventIfNoFlagChanged) { EXPECT_EQ(false, got_event); } -TEST(DataStoreUpdaterTest, NoEventOnDiscardedUpsert) { +TEST(ChangeNotifierDestinationTest, NoEventOnDiscardedUpsert) { Flag flag_a; Flag flag_b; @@ -405,7 +399,7 @@ TEST(DataStoreUpdaterTest, NoEventOnDiscardedUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - DataStoreUpdater updater(store, store); + ChangeNotifierDestination updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ diff --git a/libs/server-sdk/tests/dependency_tracker_test.cpp b/libs/server-sdk/tests/dependency_tracker_test.cpp index bbffbc09b..797e3a45e 100644 --- a/libs/server-sdk/tests/dependency_tracker_test.cpp +++ b/libs/server-sdk/tests/dependency_tracker_test.cpp @@ -1,22 +1,13 @@ #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; +#include +#include 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; + +using namespace launchdarkly::server_side::data_components; +using namespace launchdarkly::data_model; TEST(ScopedSetTest, CanAddItem) { DependencySet set; diff --git a/libs/server-sdk/tests/expiration_tracker_test.cpp b/libs/server-sdk/tests/expiration_tracker_test.cpp index a8d17f99c..15b6163ef 100644 --- a/libs/server-sdk/tests/expiration_tracker_test.cpp +++ b/libs/server-sdk/tests/expiration_tracker_test.cpp @@ -1,9 +1,8 @@ #include -#include "data_store/persistent/expiration_tracker.hpp" +#include -using launchdarkly::server_side::data_store::DataKind; -using launchdarkly::server_side::data_store::persistent::ExpirationTracker; +using namespace launchdarkly::server_side::data_components; ExpirationTracker::TimePoint Second(uint64_t second) { return std::chrono::steady_clock::time_point{std::chrono::seconds{second}}; From cfdbfe57957ec184a0a439e83b4d60cc3fcccca6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 15:19:59 -0800 Subject: [PATCH 108/244] adding memory store --- libs/server-sdk/src/CMakeLists.txt | 5 +- .../all_flags_state_builder.cpp | 2 + .../all_flags_state_builder.hpp | 10 ++- libs/server-sdk/src/client_impl.cpp | 2 +- libs/server-sdk/src/client_impl.hpp | 4 +- .../memory_store}/memory_store.cpp | 32 ++++---- .../memory_store/memory_store.hpp | 62 ++++++++++++++ .../data_source_event_handler.hpp | 6 +- .../data_sources/data_source_update_sink.hpp | 12 +-- libs/server-sdk/src/data_store/data_store.hpp | 81 ------------------- .../server-sdk/src/data_store/descriptors.hpp | 12 --- .../src/data_store/memory_store.hpp | 50 ------------ libs/server-sdk/src/evaluation/evaluator.cpp | 12 +-- libs/server-sdk/src/evaluation/evaluator.hpp | 18 ++--- libs/server-sdk/src/evaluation/rules.cpp | 12 +-- libs/server-sdk/src/evaluation/rules.hpp | 18 ++--- .../server-sdk/tests/all_flags_state_test.cpp | 6 +- .../server-sdk/tests/change_notifier_test.cpp | 52 ++++++------ .../tests/data_source_event_handler_test.cpp | 4 +- libs/server-sdk/tests/evaluator_tests.cpp | 4 +- libs/server-sdk/tests/memory_store_test.cpp | 16 ++-- libs/server-sdk/tests/test_store.cpp | 20 ++--- libs/server-sdk/tests/test_store.hpp | 12 +-- 23 files changed, 183 insertions(+), 269 deletions(-) rename libs/server-sdk/src/{data_store => data_components/memory_store}/memory_store.cpp (55%) create mode 100644 libs/server-sdk/src/data_components/memory_store/memory_store.hpp delete mode 100644 libs/server-sdk/src/data_store/data_store.hpp delete mode 100644 libs/server-sdk/src/data_store/descriptors.hpp delete mode 100644 libs/server-sdk/src/data_store/memory_store.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index d675a436d..97dea1f3a 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -25,9 +25,6 @@ target_sources(${LIBNAME} 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/memory_store.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 @@ -43,6 +40,8 @@ target_sources(${LIBNAME} data_components/dependency_tracker/dependency_tracker.cpp data_components/expiration_tracker/expiration_tracker.hpp data_components/expiration_tracker/expiration_tracker.cpp + data_components/memory_store/memory_store.hpp + data_components/memory_store/memory_store.cpp data_interfaces.cpp evaluation/evaluator.cpp evaluation/rules.cpp 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 index efa4314b6..8e8a43bcf 100644 --- 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 @@ -1,5 +1,7 @@ #include "all_flags_state_builder.hpp" +#include + namespace launchdarkly::server_side { bool IsDebuggingEnabled(std::optional debug_events_until); 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 34e1a839b..351d62399 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 @@ -2,8 +2,10 @@ #include -#include "../data_store/data_store.hpp" -#include "../evaluation/evaluator.hpp" +#include + +#include +#include namespace launchdarkly::server_side { @@ -16,7 +18,7 @@ class AllFlagsStateBuilder { * Constructs a builder capable of generating a AllFlagsState structure. * @param options Options affecting the behavior of the builder. */ - AllFlagsStateBuilder(AllFlagsState::Options options); + explicit AllFlagsStateBuilder(AllFlagsState::Options options); /** * Adds a flag, including its evaluation result and additional state. @@ -36,7 +38,7 @@ class AllFlagsStateBuilder { [[nodiscard]] AllFlagsState Build(); private: - enum AllFlagsState::Options options_; + AllFlagsState::Options options_; std::unordered_map flags_state_; std::unordered_map evaluations_; }; diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 0845f4f6b..61ed8070d 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -6,10 +6,10 @@ #include "all_flags_state/all_flags_state_builder.hpp" #include "client_impl.hpp" +#include "data_components/memory_store/memory_store.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 diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index f3940d648..79fe879f7 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -14,7 +14,7 @@ #include "data_sources/data_source_update_sink.hpp" #include "data_components/change_notifier/change_notifier.hpp" -#include "data_store/memory_store.hpp" +#include "data_components/memory_store/memory_store.hpp" #include "evaluation/evaluator.hpp" @@ -173,7 +173,7 @@ class ClientImpl : public IClient { boost::asio::executor_work_guard work_; - data_store::MemoryStore memory_store_; + data_components::MemoryStore memory_store_; data_sources::DataSourceStatusManager status_manager_; data_components::ChangeNotifier data_store_updater_; diff --git a/libs/server-sdk/src/data_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp similarity index 55% rename from libs/server-sdk/src/data_store/memory_store.cpp rename to libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 321c6249d..f38357d95 100644 --- a/libs/server-sdk/src/data_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -1,10 +1,8 @@ - - #include "memory_store.hpp" -namespace launchdarkly::server_side::data_store { +namespace launchdarkly::server_side::data_components { -std::shared_ptr MemoryStore::GetFlag( +std::shared_ptr MemoryStore::GetFlag( std::string const& key) const { std::lock_guard lock{data_mutex_}; auto found = flags_.find(key); @@ -14,7 +12,7 @@ std::shared_ptr MemoryStore::GetFlag( return nullptr; } -std::shared_ptr MemoryStore::GetSegment( +std::shared_ptr MemoryStore::GetSegment( std::string const& key) const { std::lock_guard lock{data_mutex_}; auto found = segments_.find(key); @@ -24,13 +22,13 @@ std::shared_ptr MemoryStore::GetSegment( return nullptr; } -std::unordered_map> +std::unordered_map> MemoryStore::AllFlags() const { std::lock_guard lock{data_mutex_}; return {flags_}; } -std::unordered_map> +std::unordered_map> MemoryStore::AllSegments() const { std::lock_guard lock{data_mutex_}; return {segments_}; @@ -41,7 +39,7 @@ bool MemoryStore::Initialized() const { return initialized_; } -std::string const& MemoryStore::Description() const { +std::string const& MemoryStore::Identity() const { return description_; } @@ -51,25 +49,27 @@ void MemoryStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { flags_.clear(); segments_.clear(); for (auto flag : dataSet.flags) { - flags_.emplace(flag.first, std::make_shared( + 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))); + segments_.emplace(segment.first, + std::make_shared( + std::move(segment.second))); } } void MemoryStore::Upsert(std::string const& key, - data_store::FlagDescriptor flag) { + data_model::FlagDescriptor flag) { std::lock_guard lock{data_mutex_}; - flags_[key] = std::make_shared(std::move(flag)); + flags_[key] = std::make_shared(std::move(flag)); } void MemoryStore::Upsert(std::string const& key, - data_store::SegmentDescriptor segment) { + data_model::SegmentDescriptor segment) { std::lock_guard lock{data_mutex_}; - segments_[key] = std::make_shared(std::move(segment)); + segments_[key] = + std::make_shared(std::move(segment)); } -} // namespace launchdarkly::server_side::data_store +} // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp new file mode 100644 index 000000000..485ad13e7 --- /dev/null +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -0,0 +1,62 @@ +#pragma once + +#include "../../data_interfaces/destination/idestination.hpp" +#include "../../data_interfaces/store/istore.hpp" + +#include +#include +#include +#include + +namespace launchdarkly::server_side::data_components { + +class MemoryStore final : public data_interfaces::IStore, + public data_interfaces::IDestination { + public: + [[nodiscard]] std::shared_ptr GetFlag( + std::string const& key) const override; + + [[nodiscard]] std::shared_ptr GetSegment( + std::string const& key) const override; + + [[nodiscard]] std:: + unordered_map> + AllFlags() const override; + + [[nodiscard]] std::unordered_map< + std::string, + std::shared_ptr> + AllSegments() const override; + + [[nodiscard]] bool Initialized() const; + + [[nodiscard]] std::string const& Identity() const override; + + void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + + void Upsert(std::string const& key, + data_model::FlagDescriptor flag) override; + + void Upsert(std::string const& key, + data_model::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 const description_ = "memory"; + std::unordered_map> + flags_; + std::unordered_map> + segments_; + bool initialized_ = false; + mutable std::mutex data_mutex_; +}; + +} // namespace launchdarkly::server_side::data_components 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 index c0309da5b..3a094ed7d 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp @@ -4,14 +4,14 @@ #include +#include + #include "../data_components/dependency_tracker/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 { @@ -94,7 +94,7 @@ class DataSourceEventHandler { struct Patch { std::string key; - std::variant + std::variant data; }; 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 294f82ee7..a22b7dae8 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 @@ -1,11 +1,7 @@ #pragma once -#include -#include +#include #include -#include - -#include "../data_store/descriptors.hpp" namespace launchdarkly::server_side::data_sources { /** @@ -13,11 +9,11 @@ namespace launchdarkly::server_side::data_sources { */ class IDataSourceUpdateSink { public: - virtual void Init(launchdarkly::data_model::SDKDataSet data_set) = 0; + virtual void Init(data_model::SDKDataSet data_set) = 0; virtual void Upsert(std::string const& key, - data_store::FlagDescriptor flag) = 0; + data_model::FlagDescriptor flag) = 0; virtual void Upsert(std::string const& key, - data_store::SegmentDescriptor segment) = 0; + data_model::SegmentDescriptor segment) = 0; IDataSourceUpdateSink(IDataSourceUpdateSink const& item) = delete; IDataSourceUpdateSink(IDataSourceUpdateSink&& item) = delete; diff --git a/libs/server-sdk/src/data_store/data_store.hpp b/libs/server-sdk/src/data_store/data_store.hpp deleted file mode 100644 index 3ae0184e0..000000000 --- a/libs/server-sdk/src/data_store/data_store.hpp +++ /dev/null @@ -1,81 +0,0 @@ -#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/descriptors.hpp b/libs/server-sdk/src/data_store/descriptors.hpp deleted file mode 100644 index 075d3a31d..000000000 --- a/libs/server-sdk/src/data_store/descriptors.hpp +++ /dev/null @@ -1,12 +0,0 @@ -#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.hpp b/libs/server-sdk/src/data_store/memory_store.hpp deleted file mode 100644 index f8758e4a8..000000000 --- a/libs/server-sdk/src/data_store/memory_store.hpp +++ /dev/null @@ -1,50 +0,0 @@ -#pragma once - -#include "../data_sources/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_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; - - 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 const 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/src/evaluation/evaluator.cpp b/libs/server-sdk/src/evaluation/evaluator.cpp index 97057fbd2..4ac3ff2d2 100644 --- a/libs/server-sdk/src/evaluation/evaluator.cpp +++ b/libs/server-sdk/src/evaluation/evaluator.cpp @@ -19,8 +19,8 @@ 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_() {} +Evaluator::Evaluator(Logger& logger, data_interfaces::IStore const& source) + : logger_(logger), source_(source), stack_() {} EvaluationDetail Evaluator::Evaluate( data_model::Flag const& flag, @@ -46,15 +46,15 @@ EvaluationDetail Evaluator::Evaluate( } for (Flag::Prerequisite const& p : flag.prerequisites) { - std::shared_ptr maybe_flag = - store_.GetFlag(p.key); + std::shared_ptr maybe_flag = + source_.GetFlag(p.key); if (!maybe_flag) { return OffValue(flag, EvaluationReason::PrerequisiteFailed(p.key)); } - data_store::FlagDescriptor const& descriptor = *maybe_flag; + data_model::FlagDescriptor const& descriptor = *maybe_flag; if (!descriptor.item) { // This flag existed at some point, but has since been deleted. @@ -106,7 +106,7 @@ EvaluationDetail Evaluator::Evaluate( auto const& rule = flag.rules[rule_index]; tl::expected rule_match = - Match(rule, context, store_, stack_); + Match(rule, context, source_, stack_); if (!rule_match) { LogError(flag.key, rule_match.error()); diff --git a/libs/server-sdk/src/evaluation/evaluator.hpp b/libs/server-sdk/src/evaluation/evaluator.hpp index cf7a15468..f85d396f8 100644 --- a/libs/server-sdk/src/evaluation/evaluator.hpp +++ b/libs/server-sdk/src/evaluation/evaluator.hpp @@ -6,19 +6,20 @@ #include #include -#include "../data_store/data_store.hpp" -#include "../events/event_scope.hpp" #include "bucketing.hpp" #include "detail/evaluation_stack.hpp" #include "evaluation_error.hpp" +#include "../data_interfaces/store/istore.hpp" +#include "../events/event_scope.hpp" + #include namespace launchdarkly::server_side::evaluation { class Evaluator { public: - Evaluator(Logger& logger, data_store::IDataStore const& store); + Evaluator(Logger& logger, data_interfaces::IStore const& source); /** * Evaluates a flag for a given context. @@ -31,7 +32,7 @@ class Evaluator { */ [[nodiscard]] EvaluationDetail Evaluate( data_model::Flag const& flag, - launchdarkly::Context const& context, + Context const& context, EventScope const& event_scope); /** @@ -41,15 +42,14 @@ class Evaluator { * @param flag The flag to evaluate. * @param context The context to evaluate the flag against. */ - [[nodiscard]] EvaluationDetail Evaluate( - data_model::Flag const& flag, - launchdarkly::Context const& context); + [[nodiscard]] EvaluationDetail Evaluate(data_model::Flag const& flag, + Context const& context); private: [[nodiscard]] EvaluationDetail Evaluate( std::optional parent_key, data_model::Flag const& flag, - launchdarkly::Context const& context, + Context const& context, EventScope const& event_scope); [[nodiscard]] EvaluationDetail FlagVariation( @@ -64,7 +64,7 @@ class Evaluator { void LogError(std::string const& key, Error const& error) const; Logger& logger_; - data_store::IDataStore const& store_; + data_interfaces::IStore const& source_; detail::EvaluationStack stack_; }; } // namespace launchdarkly::server_side::evaluation diff --git a/libs/server-sdk/src/evaluation/rules.cpp b/libs/server-sdk/src/evaluation/rules.cpp index 313cc44b1..5c2f9ea44 100644 --- a/libs/server-sdk/src/evaluation/rules.cpp +++ b/libs/server-sdk/src/evaluation/rules.cpp @@ -15,7 +15,7 @@ bool MaybeNegate(Clause const& clause, bool value) { tl::expected Match(Flag::Rule const& rule, launchdarkly::Context const& context, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { for (Clause const& clause : rule.clauses) { tl::expected result = Match(clause, context, store, stack); @@ -31,7 +31,7 @@ tl::expected Match(Flag::Rule const& rule, tl::expected Match(Segment::Rule const& rule, Context const& context, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack, std::string const& key, std::string const& salt) { @@ -61,7 +61,7 @@ tl::expected Match(Segment::Rule const& rule, tl::expected Match(Clause const& clause, launchdarkly::Context const& context, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { if (clause.op == Clause::Op::kSegmentMatch) { return MatchSegment(clause, context, store, stack); @@ -71,7 +71,7 @@ tl::expected Match(Clause const& clause, tl::expected MatchSegment(Clause const& clause, launchdarkly::Context const& context, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { for (Value const& value : clause.values) { // A segment key represented as a Value is a string; non-strings are @@ -82,7 +82,7 @@ tl::expected MatchSegment(Clause const& clause, std::string const& segment_key = value.AsString(); - std::shared_ptr segment_ptr = + std::shared_ptr segment_ptr = store.GetSegment(segment_key); if (!segment_ptr || !segment_ptr->item) { @@ -153,7 +153,7 @@ tl::expected MatchNonSegment( tl::expected Contains(Segment const& segment, Context const& context, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack) { auto guard = stack.NoticeSegment(segment.key); if (!guard) { diff --git a/libs/server-sdk/src/evaluation/rules.hpp b/libs/server-sdk/src/evaluation/rules.hpp index 5ca9a974e..00f1cf31f 100644 --- a/libs/server-sdk/src/evaluation/rules.hpp +++ b/libs/server-sdk/src/evaluation/rules.hpp @@ -1,13 +1,13 @@ #pragma once +#include "../data_interfaces/store/istore.hpp" +#include "detail/evaluation_stack.hpp" +#include "evaluation_error.hpp" + #include #include #include -#include "../data_store/data_store.hpp" -#include "detail/evaluation_stack.hpp" -#include "evaluation_error.hpp" - #include #include @@ -17,18 +17,18 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Match( data_model::Flag::Rule const&, Context const&, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack); [[nodiscard]] tl::expected Match(data_model::Clause const&, Context const&, - data_store::IDataStore const&, + data_interfaces::IStore const&, detail::EvaluationStack&); [[nodiscard]] tl::expected Match( data_model::Segment::Rule const& rule, Context const& context, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack, std::string const& key, std::string const& salt); @@ -36,7 +36,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected MatchSegment( data_model::Clause const&, Context const&, - data_store::IDataStore const&, + data_interfaces::IStore const&, detail::EvaluationStack& stack); [[nodiscard]] tl::expected MatchNonSegment( @@ -46,7 +46,7 @@ namespace launchdarkly::server_side::evaluation { [[nodiscard]] tl::expected Contains( data_model::Segment const&, Context const&, - data_store::IDataStore const& store, + data_interfaces::IStore const& store, detail::EvaluationStack& stack); [[nodiscard]] bool MaybeNegate(data_model::Clause const& clause, bool value); diff --git a/libs/server-sdk/tests/all_flags_state_test.cpp b/libs/server-sdk/tests/all_flags_state_test.cpp index c08a4fe54..96dbc4f8f 100644 --- a/libs/server-sdk/tests/all_flags_state_test.cpp +++ b/libs/server-sdk/tests/all_flags_state_test.cpp @@ -4,6 +4,8 @@ #include +#include + using namespace launchdarkly; using namespace launchdarkly::server_side; @@ -109,7 +111,7 @@ TEST(AllFlagsTest, IncludeReasons) { TEST(AllFlagsTest, FlagValues) { AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; - const std::size_t kNumFlags = 10; + std::size_t const kNumFlags = 10; for (std::size_t i = 0; i < kNumFlags; i++) { builder.AddFlag("myFlag" + std::to_string(i), "value", @@ -131,7 +133,7 @@ TEST(AllFlagsTest, FlagValues) { TEST(AllFlagsTest, FlagState) { AllFlagsStateBuilder builder{AllFlagsState::Options::Default}; - const std::size_t kNumFlags = 10; + std::size_t const kNumFlags = 10; AllFlagsState::State state{42, 1, std::nullopt, false, false, std::nullopt}; for (std::size_t i = 0; i < kNumFlags; i++) { diff --git a/libs/server-sdk/tests/change_notifier_test.cpp b/libs/server-sdk/tests/change_notifier_test.cpp index 1740c35a8..eb51f5748 100644 --- a/libs/server-sdk/tests/change_notifier_test.cpp +++ b/libs/server-sdk/tests/change_notifier_test.cpp @@ -9,22 +9,22 @@ using launchdarkly::Value; using namespace launchdarkly::data_model; using namespace launchdarkly::server_side::data_components; -TEST(ChangeNotifierDestinationTest, DoesNotInitializeStoreUntilInit) { +TEST(ChangeNotifierTest, DoesNotInitializeStoreUntilInit) { MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); EXPECT_FALSE(store.Initialized()); } -TEST(ChangeNotifierDestinationTest, InitializesStore) { +TEST(ChangeNotifierTest, InitializesStore) { MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet()); EXPECT_TRUE(store.Initialized()); } -TEST(ChangeNotifierDestinationTest, InitPropagatesData) { +TEST(ChangeNotifierTest, InitPropagatesData) { MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); Flag flag; flag.version = 1; flag.key = "flagA"; @@ -59,9 +59,9 @@ TEST(ChangeNotifierDestinationTest, InitPropagatesData) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(ChangeNotifierDestinationTest, SecondInitProducesChanges) { +TEST(ChangeNotifierTest, SecondInitProducesChanges) { MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); Flag flag_a_v1; flag_a_v1.version = 1; flag_a_v1.key = "flagA"; @@ -138,9 +138,9 @@ TEST(ChangeNotifierDestinationTest, SecondInitProducesChanges) { EXPECT_TRUE(got_event); } -TEST(ChangeNotifierDestinationTest, CanUpsertNewFlag) { +TEST(ChangeNotifierTest, CanUpsertNewFlag) { MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); Flag flag_a; flag_a.version = 1; @@ -160,13 +160,13 @@ TEST(ChangeNotifierDestinationTest, CanUpsertNewFlag) { EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); } -TEST(ChangeNotifierDestinationTest, CanUpsertExitingFlag) { +TEST(ChangeNotifierTest, CanUpsertExitingFlag) { Flag flag_a; flag_a.version = 1; flag_a.key = "flagA"; MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -188,14 +188,14 @@ TEST(ChangeNotifierDestinationTest, CanUpsertExitingFlag) { EXPECT_EQ(fetched_flag->version, fetched_flag->item->version); } -TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertFlag) { +TEST(ChangeNotifierTest, OldVersionIsDiscardedOnUpsertFlag) { Flag flag_a; flag_a.version = 2; flag_a.key = "flagA"; flag_a.variations = std::vector{"potato", "ham"}; MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -222,13 +222,13 @@ TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertFlag) { EXPECT_EQ(std::string("ham"), fetched_flag->item->variations[1].AsString()); } -TEST(ChangeNotifierDestinationTest, CanUpsertNewSegment) { +TEST(ChangeNotifierTest, CanUpsertNewSegment) { Segment segment_a; segment_a.version = 1; segment_a.key = "segmentA"; MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -244,13 +244,13 @@ TEST(ChangeNotifierDestinationTest, CanUpsertNewSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(ChangeNotifierDestinationTest, CanUpsertExitingSegment) { +TEST(ChangeNotifierTest, CanUpsertExitingSegment) { Segment segment_a; segment_a.version = 1; segment_a.key = "segmentA"; MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -272,13 +272,13 @@ TEST(ChangeNotifierDestinationTest, CanUpsertExitingSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertSegment) { +TEST(ChangeNotifierTest, OldVersionIsDiscardedOnUpsertSegment) { Segment segment_a; segment_a.version = 2; segment_a.key = "segmentA"; MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet{ std::unordered_map(), @@ -300,7 +300,7 @@ TEST(ChangeNotifierDestinationTest, OldVersionIsDiscardedOnUpsertSegment) { EXPECT_EQ(fetched_segment->version, fetched_segment->item->version); } -TEST(ChangeNotifierDestinationTest, ProducesChangeEventsOnUpsert) { +TEST(ChangeNotifierTest, ProducesChangeEventsOnUpsert) { Flag flag_a; Flag flag_b; @@ -313,7 +313,7 @@ TEST(ChangeNotifierDestinationTest, ProducesChangeEventsOnUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ @@ -343,7 +343,7 @@ TEST(ChangeNotifierDestinationTest, ProducesChangeEventsOnUpsert) { EXPECT_EQ(true, got_event); } -TEST(ChangeNotifierDestinationTest, ProducesNoEventIfNoFlagChanged) { +TEST(ChangeNotifierTest, ProducesNoEventIfNoFlagChanged) { Flag flag_a; Flag flag_b; @@ -356,7 +356,7 @@ TEST(ChangeNotifierDestinationTest, ProducesNoEventIfNoFlagChanged) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); Segment segment_a; segment_a.version = 1; @@ -386,7 +386,7 @@ TEST(ChangeNotifierDestinationTest, ProducesNoEventIfNoFlagChanged) { EXPECT_EQ(false, got_event); } -TEST(ChangeNotifierDestinationTest, NoEventOnDiscardedUpsert) { +TEST(ChangeNotifierTest, NoEventOnDiscardedUpsert) { Flag flag_a; Flag flag_b; @@ -399,7 +399,7 @@ TEST(ChangeNotifierDestinationTest, NoEventOnDiscardedUpsert) { flag_b.prerequisites.push_back(Flag::Prerequisite{"flagA", 0}); MemoryStore store; - ChangeNotifierDestination updater(store, store); + ChangeNotifier updater(store, store); updater.Init(SDKDataSet{ std::unordered_map{ diff --git a/libs/server-sdk/tests/data_source_event_handler_test.cpp b/libs/server-sdk/tests/data_source_event_handler_test.cpp index 4092c78a2..3ec6d77f3 100644 --- a/libs/server-sdk/tests/data_source_event_handler_test.cpp +++ b/libs/server-sdk/tests/data_source_event_handler_test.cpp @@ -1,13 +1,13 @@ #include #include +#include "data_components/memory_store/memory_store.hpp" #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; +using namespace launchdarkly::server_side::data_components; TEST(DataSourceEventHandlerTests, HandlesEmptyPutMessage) { auto logger = launchdarkly::logging::NullLogger(); diff --git a/libs/server-sdk/tests/evaluator_tests.cpp b/libs/server-sdk/tests/evaluator_tests.cpp index 85807a848..93d5d8314 100644 --- a/libs/server-sdk/tests/evaluator_tests.cpp +++ b/libs/server-sdk/tests/evaluator_tests.cpp @@ -25,7 +25,7 @@ class EvaluatorTests : public ::testing::Test { Logger logger_; protected: - std::unique_ptr store_; + std::unique_ptr store_; evaluation::Evaluator eval_; }; @@ -48,7 +48,7 @@ class EvaluatorTestsWithLogs : public ::testing::Test { Logger logger_; protected: - std::unique_ptr store_; + std::unique_ptr store_; evaluation::Evaluator eval_; }; diff --git a/libs/server-sdk/tests/memory_store_test.cpp b/libs/server-sdk/tests/memory_store_test.cpp index 852d112dc..896fabd9a 100644 --- a/libs/server-sdk/tests/memory_store_test.cpp +++ b/libs/server-sdk/tests/memory_store_test.cpp @@ -1,17 +1,11 @@ #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; +#include using launchdarkly::Value; -using launchdarkly::data_model::Flag; -using launchdarkly::data_model::Segment; + +using namespace launchdarkly::data_model; +using namespace launchdarkly::server_side::data_components; TEST(MemoryStoreTest, StartsUninitialized) { MemoryStore store; @@ -26,7 +20,7 @@ TEST(MemoryStoreTest, IsInitializedAfterInit) { TEST(MemoryStoreTest, HasDescription) { MemoryStore store; - EXPECT_EQ(std::string("memory"), store.Description()); + EXPECT_EQ(std::string("memory"), store.Identity()); } TEST(MemoryStoreTest, CanGetFlag) { diff --git a/libs/server-sdk/tests/test_store.cpp b/libs/server-sdk/tests/test_store.cpp index 128eb2b8d..2f79e7af5 100644 --- a/libs/server-sdk/tests/test_store.cpp +++ b/libs/server-sdk/tests/test_store.cpp @@ -1,38 +1,38 @@ #include "test_store.hpp" -#include "data_store/memory_store.hpp" - +#include +#include #include #include namespace launchdarkly::server_side::test_store { -std::unique_ptr Empty() { - auto store = std::make_unique(); +std::unique_ptr Empty() { + auto store = std::make_unique(); store->Init({}); return store; } -data_store::FlagDescriptor Flag(char const* json) { +data_model::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()}; + return data_model::FlagDescriptor{val.value().value()}; } -data_store::SegmentDescriptor Segment(char const* json) { +data_model::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()}; + return data_model::SegmentDescriptor{val.value().value()}; } -std::unique_ptr TestData() { - auto store = std::make_unique(); +std::unique_ptr TestData() { + auto store = std::make_unique(); store->Init({}); store->Upsert("segmentWithNoRules", Segment(R"({ diff --git a/libs/server-sdk/tests/test_store.hpp b/libs/server-sdk/tests/test_store.hpp index bfccc008e..e0bd2fdc5 100644 --- a/libs/server-sdk/tests/test_store.hpp +++ b/libs/server-sdk/tests/test_store.hpp @@ -1,7 +1,7 @@ #pragma once -#include "data_store/data_store.hpp" - +#include +#include #include namespace launchdarkly::server_side::test_store { @@ -9,23 +9,23 @@ namespace launchdarkly::server_side::test_store { /** * @return A data store preloaded with flags/segments for unit tests. */ -std::unique_ptr TestData(); +std::unique_ptr TestData(); /** * @return An initialized, but empty, data store. */ -std::unique_ptr Empty(); +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); +data_model::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); +data_model::SegmentDescriptor Segment(char const* json); } // namespace launchdarkly::server_side::test_store From 7323be1166170191db0030f6fe28aee2e4bf3a94 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 16 Nov 2023 15:26:09 -0800 Subject: [PATCH 109/244] update data source status --- .../server_side/data_source_status.hpp | 12 ++++-------- libs/server-sdk/src/client_impl.hpp | 2 +- .../data_source_status_manager.hpp | 17 ++++++++--------- .../data_sources/data_source_event_handler.hpp | 11 ++++++----- .../src/data_sources/null_data_source.hpp | 11 ++++++----- 5 files changed, 25 insertions(+), 28 deletions(-) rename libs/server-sdk/src/{data_sources => data_components/status_notifications}/data_source_status_manager.hpp (87%) 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 c6f16f7ea..76c3a6565 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 @@ -1,19 +1,15 @@ #pragma once -#pragma once - -#include #include #include #include -#include #include #include #include #include -namespace launchdarkly::server_side::data_sources { +namespace launchdarkly::server_side { /** * Enumeration of possible data source states. @@ -85,7 +81,7 @@ class IDataSourceStatusProvider { * @return A IConnection which can be used to stop listening to the status. */ virtual std::unique_ptr OnDataSourceStatusChange( - std::function handler) = 0; + std::function handler) = 0; /** * Listen to changes to the data source status, with ability for listener @@ -96,7 +92,7 @@ class IDataSourceStatusProvider { * @return A IConnection which can be used to stop listening to the status. */ virtual std::unique_ptr OnDataSourceStatusChangeEx( - std::function handler) = 0; + std::function handler) = 0; virtual ~IDataSourceStatusProvider() = default; IDataSourceStatusProvider(IDataSourceStatusProvider const& item) = delete; @@ -114,4 +110,4 @@ std::ostream& operator<<(std::ostream& out, std::ostream& operator<<(std::ostream& out, DataSourceStatus const& status); -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 79fe879f7..5717f640c 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -10,11 +10,11 @@ #include #include -#include "data_sources/data_source_status_manager.hpp" #include "data_sources/data_source_update_sink.hpp" #include "data_components/change_notifier/change_notifier.hpp" #include "data_components/memory_store/memory_store.hpp" +#include "data_components/status_notifications/data_source_status_manager.hpp" #include "evaluation/evaluator.hpp" diff --git a/libs/server-sdk/src/data_sources/data_source_status_manager.hpp b/libs/server-sdk/src/data_components/status_notifications/data_source_status_manager.hpp similarity index 87% rename from libs/server-sdk/src/data_sources/data_source_status_manager.hpp rename to libs/server-sdk/src/data_components/status_notifications/data_source_status_manager.hpp index d19040ed8..220b666c1 100644 --- a/libs/server-sdk/src/data_sources/data_source_status_manager.hpp +++ b/libs/server-sdk/src/data_components/status_notifications/data_source_status_manager.hpp @@ -1,21 +1,20 @@ #pragma once - -#include -#include - -#include - #include #include #include -namespace launchdarkly::server_side::data_sources { +#include + +#include +#include + +namespace launchdarkly::server_side::data_components { class DataSourceStatusManager : public internal::data_sources::DataSourceStatusManagerBase< DataSourceStatus, IDataSourceStatusProvider> { - public: +public: DataSourceStatusManager() = default; ~DataSourceStatusManager() override = default; @@ -25,4 +24,4 @@ class DataSourceStatusManager DataSourceStatusManager& operator=(DataSourceStatusManager&&) = delete; }; -} // namespace launchdarkly::server_side::data_sources +} // namespace launchdarkly::server_side::data_components 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 index 3a094ed7d..b28e10d54 100644 --- a/libs/server-sdk/src/data_sources/data_source_event_handler.hpp +++ b/libs/server-sdk/src/data_sources/data_source_event_handler.hpp @@ -7,7 +7,7 @@ #include #include "../data_components/dependency_tracker/data_kind.hpp" -#include "data_source_status_manager.hpp" +#include "../data_components/status_notifications/data_source_status_manager.hpp" #include "data_source_update_sink.hpp" #include @@ -104,9 +104,10 @@ class DataSourceEventHandler { uint64_t version; }; - DataSourceEventHandler(IDataSourceUpdateSink& handler, - Logger const& logger, - DataSourceStatusManager& status_manager); + DataSourceEventHandler( + IDataSourceUpdateSink& handler, + Logger const& logger, + data_components::DataSourceStatusManager& status_manager); /** * Handles an event from the LaunchDarkly service. @@ -120,6 +121,6 @@ class DataSourceEventHandler { private: IDataSourceUpdateSink& handler_; Logger const& logger_; - DataSourceStatusManager& status_manager_; + data_components::DataSourceStatusManager& status_manager_; }; } // 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 index 7a1102f49..d3e24592d 100644 --- a/libs/server-sdk/src/data_sources/null_data_source.hpp +++ b/libs/server-sdk/src/data_sources/null_data_source.hpp @@ -1,6 +1,6 @@ #pragma once -#include "data_source_status_manager.hpp" +#include "../data_components/status_notifications/data_source_status_manager.hpp" #include @@ -8,15 +8,16 @@ namespace launchdarkly::server_side::data_sources { -class NullDataSource : public ::launchdarkly::data_sources::IDataSource { +class NullDataSource final : public ::launchdarkly::data_sources::IDataSource { public: - explicit NullDataSource(boost::asio::any_io_executor exec, - DataSourceStatusManager& status_manager); + explicit NullDataSource( + boost::asio::any_io_executor exec, + data_components::DataSourceStatusManager& status_manager); void Start() override; void ShutdownAsync(std::function) override; private: - DataSourceStatusManager& status_manager_; + data_components::DataSourceStatusManager& status_manager_; boost::asio::any_io_executor exec_; }; From 3d9d841244a1f68ba128f0609f2eafcbe904e5fd Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 17 Nov 2023 12:28:37 -0800 Subject: [PATCH 110/244] it compiles --- .../shared/built/data_source_config.hpp | 8 +- .../launchdarkly/config/shared/defaults.hpp | 4 - .../config/builders/all_builders.hpp | 27 +++++ .../server_side/config/built/all_built.hpp | 21 ++++ .../data_system/background_sync_config.hpp | 10 +- .../server_side/config/config.hpp | 35 +++--- .../server_side/config/config_builder.hpp | 42 +++---- .../server_side/config/defaults.hpp | 10 ++ libs/server-sdk/src/bindings/c/builder.cpp | 1 + libs/server-sdk/src/client_impl.cpp | 34 +++--- libs/server-sdk/src/client_impl.hpp | 2 +- libs/server-sdk/src/config/config_builder.cpp | 13 +-- .../dependency_tracker/dependency_tracker.cpp | 4 +- .../dependency_tracker/dependency_tracker.hpp | 10 +- .../background_sync_system.cpp | 30 +++-- .../background_sync_system.hpp | 25 ++--- .../sources/noop/null_data_source.cpp | 2 +- .../sources/polling/polling_data_source.cpp | 28 ++--- .../sources/polling/polling_data_source.hpp | 24 ++-- .../sources/streaming/event_handler.cpp | 2 +- .../sources/streaming/event_handler.hpp | 13 +-- .../streaming/streaming_data_source.cpp | 2 +- .../streaming/streaming_data_source.hpp | 15 ++- .../lazy_load/lazy_load_system.cpp | 25 +++-- .../lazy_load/sources/redis/redis_source.cpp | 2 +- libs/server-sdk/tests/config_builder_test.cpp | 103 +++++++++++------- 26 files changed, 269 insertions(+), 223 deletions(-) create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/built/all_built.hpp create mode 100644 libs/server-sdk/include/launchdarkly/server_side/config/defaults.hpp diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index 04673c6a4..bee8aee2f 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -85,11 +85,5 @@ struct DataSourceConfig { }; template <> -struct DataSourceConfig { - std::variant, - PollingConfig, - RedisConfig> - method; -}; - +struct DataSourceConfig {}; } // namespace launchdarkly::config::shared::built diff --git a/libs/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index affad00dc..9b7814360 100644 --- a/libs/common/include/launchdarkly/config/shared/defaults.hpp +++ b/libs/common/include/launchdarkly/config/shared/defaults.hpp @@ -102,10 +102,6 @@ struct Defaults { return {std::chrono::seconds{1}, "/all"}; } - static auto DataSourceConfig() -> built::DataSourceConfig { - return {StreamingConfig()}; - } - static auto PollingConfig() -> built::PollingConfig { return {std::chrono::seconds{30}, "/sdk/latest-all", std::chrono::seconds{30}}; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp new file mode 100644 index 000000000..b767cc9b7 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/all_builders.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::config::builders { + +using SDK = launchdarkly::config::shared::ServerSDK; +using EndpointsBuilder = + launchdarkly::config::shared::builders::EndpointsBuilder; +using HttpPropertiesBuilder = + launchdarkly::config::shared::builders::HttpPropertiesBuilder; +using AppInfoBuilder = launchdarkly::config::shared::builders::AppInfoBuilder; +using EventsBuilder = + launchdarkly::config::shared::builders::EventsBuilder; +using LoggingBuilder = launchdarkly::config::shared::builders::LoggingBuilder; + +} // namespace launchdarkly::server_side::config::builders diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/all_built.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/all_built.hpp new file mode 100644 index 000000000..96e829b49 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/all_built.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::config::built { + +using Events = launchdarkly::config::shared::built::Events; +using HttpProperties = launchdarkly::config::shared::built::HttpProperties; +using Logging = launchdarkly::config::shared::built::Logging; +using ServiceEndpoints = launchdarkly::config::shared::built::ServiceEndpoints; + +} // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp index a8edd9f09..17e5af4dd 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/background_sync_config.hpp @@ -6,14 +6,18 @@ #include #include +#include namespace launchdarkly::server_side::config::built { + struct BackgroundSyncConfig { + using StreamingConfig = launchdarkly::config::shared::built::StreamingConfig; + using PollingConfig = launchdarkly::config::shared::built::PollingConfig; + + std::optional bootstrap_; - launchdarkly::config::shared::built::DataSourceConfig< - launchdarkly::config::shared::ServerSDK> - source_; + std::variant synchronizer_; std::optional destination_; }; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp index 424bff1f9..5fbdbea8f 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -1,10 +1,6 @@ #pragma once -#include -#include -#include -#include - +#include #include namespace launchdarkly::server_side { @@ -12,39 +8,36 @@ namespace launchdarkly::server_side { struct Config { public: Config(std::string sdk_key, - launchdarkly::config::shared::built::Logging logging, - launchdarkly::config::shared::built::ServiceEndpoints endpoints, - launchdarkly::config::shared::built::Events events, + config::built::Logging logging, + config::built::ServiceEndpoints endpoints, + config::built::Events events, std::optional application_tag, config::built::DataSystemConfig data_system_config, - launchdarkly::config::shared::built::HttpProperties http_properties); + config::built::HttpProperties http_properties); [[nodiscard]] std::string const& SdkKey() const; - [[nodiscard]] launchdarkly::config::shared::built::ServiceEndpoints const& - ServiceEndpoints() const; - - [[nodiscard]] launchdarkly::config::shared::built::Events const& Events() + [[nodiscard]] config::built::ServiceEndpoints const& ServiceEndpoints() const; + [[nodiscard]] config::built::Events const& Events() const; + [[nodiscard]] std::optional const& ApplicationTag() const; config::built::DataSystemConfig const& DataSystemConfig() const; - [[nodiscard]] launchdarkly::config::shared::built::HttpProperties const& - HttpProperties() const; + [[nodiscard]] config::built::HttpProperties const& HttpProperties() const; - [[nodiscard]] launchdarkly::config::shared::built::Logging const& Logging() - const; + [[nodiscard]] config::built::Logging const& Logging() const; private: std::string sdk_key_; bool offline_; - launchdarkly::config::shared::built::Logging logging_; - launchdarkly::config::shared::built::ServiceEndpoints service_endpoints_; + config::built::Logging logging_; + config::built::ServiceEndpoints service_endpoints_; std::optional application_tag_; - launchdarkly::config::shared::built::Events events_; + config::built::Events events_; config::built::DataSystemConfig data_system_config_; - launchdarkly::config::shared::built::HttpProperties http_properties_; + config::built::HttpProperties http_properties_; }; } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index b8c590f79..0ddd6e895 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -1,26 +1,10 @@ #pragma once -#include -#include -#include -#include -#include -#include +#include #include namespace launchdarkly::server_side { -using SDK = launchdarkly::config::shared::ServerSDK; -using DataSystemBuilder = config::builders::DataSystemBuilder; -using EndpointsBuilder = - launchdarkly::config::shared::builders::EndpointsBuilder; -using HttpPropertiesBuilder = - launchdarkly::config::shared::builders::HttpPropertiesBuilder; -using AppInfoBuilder = launchdarkly::config::shared::builders::AppInfoBuilder; -using EventsBuilder = - launchdarkly::config::shared::builders::EventsBuilder; -using LoggingBuilder = launchdarkly::config::shared::builders::LoggingBuilder; - class ConfigBuilder { public: using Result = Config; @@ -36,7 +20,7 @@ class ConfigBuilder { * @param builder An EndpointsBuilder. * @return Reference to an EndpointsBuilder. */ - EndpointsBuilder& ServiceEndpoints(); + config::builders::EndpointsBuilder& ServiceEndpoints(); /** * To include metadata about the application that is utilizing the SDK, @@ -44,7 +28,7 @@ class ConfigBuilder { * @param builder An AppInfoBuilder. * @return Reference to an AppInfoBuilder. */ - AppInfoBuilder& AppInfo(); + config::builders::AppInfoBuilder& AppInfo(); /** * To tune settings related to event generation and delivery, pass an @@ -52,7 +36,7 @@ class ConfigBuilder { * @param builder An EventsBuilder. * @return Reference to an EventsBuilder. */ - EventsBuilder& Events(); + config::builders::EventsBuilder& Events(); /** * Sets the configuration of the component that receives and stores feature @@ -60,7 +44,7 @@ class ConfigBuilder { * @param builder A DataSystemBuilder. * @return Reference to a DataSystemBuilder. */ - DataSystemBuilder& DataSystem(); + config::builders::DataSystemBuilder& DataSystem(); /** * Sets the SDK's networking configuration, using an HttpPropertiesBuilder. @@ -68,14 +52,14 @@ class ConfigBuilder { * @param builder A HttpPropertiesBuilder builder. * @return Reference to an HttpPropertiesBuilder. */ - HttpPropertiesBuilder& HttpProperties(); + config::builders::HttpPropertiesBuilder& HttpProperties(); /** * Sets the logging configuration for the SDK. * @param builder A Logging builder. * @return Reference to a LoggingBuilder. */ - LoggingBuilder& Logging(); + config::builders::LoggingBuilder& Logging(); /** * Builds a Configuration, suitable for passing into an instance of Client. @@ -87,11 +71,11 @@ class ConfigBuilder { std::string sdk_key_; std::optional offline_; - EndpointsBuilder service_endpoints_builder_; - AppInfoBuilder app_info_builder_; - EventsBuilder events_builder_; - DataSystemBuilder data_system_builder_; - HttpPropertiesBuilder http_properties_builder_; - LoggingBuilder logging_config_builder_; + config::builders::EndpointsBuilder service_endpoints_builder_; + config::builders::AppInfoBuilder app_info_builder_; + config::builders::EventsBuilder events_builder_; + config::builders::DataSystemBuilder data_system_builder_; + config::builders::HttpPropertiesBuilder http_properties_builder_; + config::builders::LoggingBuilder logging_config_builder_; }; } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/defaults.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/defaults.hpp new file mode 100644 index 000000000..fc61c3d50 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/config/defaults.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace launchdarkly::server_side::config { + +struct Defaults { + + +}; + +} // namespace launchdarkly::server_side::config diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index c9559cfe8..313cc6d63 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -7,6 +7,7 @@ #include using namespace launchdarkly::server_side; +using namespace launchdarkly::server_side::config::builders; #define TO_BUILDER(ptr) (reinterpret_cast(ptr)) #define FROM_BUILDER(ptr) (reinterpret_cast(ptr)) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index e7be1cc1d..63eec1a15 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -10,7 +10,9 @@ #include #include #include -#include + +#include +#include #include #include @@ -18,6 +20,8 @@ namespace launchdarkly::server_side { +using EventProcessor = events::AsioEventProcessor; + // The ASIO implementation assumes that the io_context will be run from a // single thread, and applies several optimisations based on this // assumption. @@ -27,10 +31,6 @@ auto const kAsioConcurrencyHint = 1; // 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; - // static std::shared_ptr<::launchdarkly::data_sources::IDataSource> // MakeDataSource(HttpProperties const& http_properties, // Config const& config, @@ -63,7 +63,7 @@ using launchdarkly::config::shared::built::HttpProperties; // } static std::unique_ptr MakeDataSystem( - HttpProperties const& http_properties, + config::built::HttpProperties const& http_properties, Config const& config, boost::asio::any_io_executor const& executor, data_components::DataSourceStatusManager& status_manager, @@ -73,20 +73,20 @@ static std::unique_ptr MakeDataSystem( status_manager); } - auto const builder = HttpPropertiesBuilder(http_properties); + auto const builder = + config::builders::HttpPropertiesBuilder(http_properties); auto data_source_properties = builder.Build(); - auto const bg_sync_config = - std::get>( - config.DataSystemConfig().system_); + auto const bg_sync_config = std::get( + config.DataSystemConfig().system_); return std::make_unique( config.ServiceEndpoints(), bg_sync_config, data_source_properties, executor, status_manager, logger); } -static Logger MakeLogger(config::shared::built::Logging const& config) { +static Logger MakeLogger(config::built::Logging const& config) { if (config.disable_logging) { return {std::make_shared()}; } @@ -97,15 +97,15 @@ static Logger MakeLogger(config::shared::built::Logging const& config) { std::make_shared(config.level, config.tag)}; } -std::unique_ptr> MakeEventProcessor( +std::unique_ptr MakeEventProcessor( Config const& config, boost::asio::any_io_executor const& exec, - HttpProperties const& http_properties, + config::built::HttpProperties const& http_properties, Logger& logger) { if (config.Events().Enabled()) { - return std::make_unique>( - exec, config.ServiceEndpoints(), config.Events(), http_properties, - logger); + return std::make_unique(exec, config.ServiceEndpoints(), + config.Events(), + http_properties, logger); } return nullptr; } @@ -120,7 +120,7 @@ bool IsFlagPresent( ClientImpl::ClientImpl(Config config, std::string const& version) : config_(config), http_properties_( - HttpPropertiesBuilder(config.HttpProperties()) + config::builders::HttpPropertiesBuilder(config.HttpProperties()) .Header("user-agent", "CPPClient/" + version) .Header("authorization", config.SdkKey()) .Header("x-launchdarkly-tags", config.ApplicationTag()) diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 20023d5fa..5f722f25e 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -101,7 +101,7 @@ class ClientImpl : public IClient { FlagKey const& key, Value default_value) override; - data_sources::IDataSourceStatusProvider& DataSourceStatus() override; + IDataSourceStatusProvider& DataSourceStatus() override; ~ClientImpl(); diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 41095a1dd..5a425f03e 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -1,32 +1,31 @@ #include -#include "launchdarkly/config/shared/defaults.hpp" namespace launchdarkly::server_side { ConfigBuilder::ConfigBuilder(std::string sdk_key) : sdk_key_(std::move(sdk_key)) {} -EndpointsBuilder& ConfigBuilder::ServiceEndpoints() { +config::builders::EndpointsBuilder& ConfigBuilder::ServiceEndpoints() { return service_endpoints_builder_; } -EventsBuilder& ConfigBuilder::Events() { +config::builders::EventsBuilder& ConfigBuilder::Events() { return events_builder_; } -AppInfoBuilder& ConfigBuilder::AppInfo() { +config::builders::AppInfoBuilder& ConfigBuilder::AppInfo() { return app_info_builder_; } -DataSystemBuilder& ConfigBuilder::DataSystem() { +config::builders::DataSystemBuilder& ConfigBuilder::DataSystem() { return data_system_builder_; } -HttpPropertiesBuilder& ConfigBuilder::HttpProperties() { +config::builders::HttpPropertiesBuilder& ConfigBuilder::HttpProperties() { return http_properties_builder_; } -LoggingBuilder& ConfigBuilder::Logging() { +config::builders::LoggingBuilder& ConfigBuilder::Logging() { return logging_config_builder_; } diff --git a/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp index 202fcdef8..0a3338ac6 100644 --- a/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.cpp @@ -98,7 +98,7 @@ DependencyMap::end() const { void DependencyTracker::UpdateDependencies( std::string const& key, - DependencyTracker::FlagDescriptor const& flag) { + data_model::FlagDescriptor const& flag) { DependencySet dependencies; if (flag.item) { for (auto const& prereq : flag.item->prerequisites) { @@ -114,7 +114,7 @@ void DependencyTracker::UpdateDependencies( void DependencyTracker::UpdateDependencies( std::string const& key, - DependencyTracker::SegmentDescriptor const& segment) { + data_model::SegmentDescriptor const& segment) { DependencySet dependencies; if (segment.item) { for (auto const& rule : segment.item->rules) { diff --git a/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp index 5bb1d0df9..27d98025c 100644 --- a/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/dependency_tracker.hpp @@ -3,8 +3,8 @@ #include "data_kind.hpp" #include "tagged_data.hpp" +#include #include -#include #include #include @@ -93,16 +93,14 @@ class DependencyMap { */ 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); + void UpdateDependencies(std::string const& key, + data_model::FlagDescriptor const& flag); /** * Update the dependency tracker with a new or updated segment. @@ -111,7 +109,7 @@ class DependencyTracker { * @param flag A descriptor for the segment. */ void UpdateDependencies(std::string const& key, - SegmentDescriptor const& segment); + data_model::SegmentDescriptor const& segment); /** * Given the current dependencies, determine what flags or segments may be diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index c49c19e5d..ae2682a63 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -6,13 +6,10 @@ namespace launchdarkly::server_side::data_systems { -using namespace config::shared::built; - BackgroundSync::BackgroundSync( - ServiceEndpoints const& endpoints, - BackgroundSyncConfig const& - background_sync_config, - HttpProperties http_properties, + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig const& background_sync_config, + config::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, Logger const& logger) @@ -20,21 +17,22 @@ BackgroundSync::BackgroundSync( std::visit( [&](auto&& method_config) { using T = std::decay_t; - if constexpr (std::is_same_v< - T, StreamingConfig>) { + if constexpr (std::is_same_v) { synchronizer_ = std::make_shared( - endpoints, method_config, http_properties, ioc, store_, - status_manager, logger); + endpoints, method_config, http_properties, ioc, + change_notifier_, status_manager, logger); } else if constexpr (std::is_same_v< - T, PollingConfig< - config::shared::ServerSDK>>) { + T, config::built::BackgroundSyncConfig:: + PollingConfig>) { synchronizer_ = std::make_shared( - endpoints, method_config, http_properties, ioc, store_, - status_manager, logger); + endpoints, method_config, http_properties, ioc, + change_notifier_, status_manager, logger); } }, - background_sync_config.source_.method); + background_sync_config.synchronizer_); } BackgroundSync::BackgroundSync( @@ -47,7 +45,7 @@ BackgroundSync::BackgroundSync( void BackgroundSync::Initialize() { // TODO: if there was any data from bootstrapping, then add it: // synchronizer_->Init(data); - synchronizer_->Start(); + synchronizer_->StartAsync(); } std::string const& BackgroundSync::Identity() const { diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index b2656d87f..d7b032237 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -1,17 +1,14 @@ #pragma once -#include "../../data_components/change_notifier/change_notifier_destination.hpp" +#include "../../data_components/change_notifier/change_notifier.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/source/ipush_source.hpp" #include "../../data_interfaces/system/isystem.hpp" -#include -#include -#include -#include #include #include +#include #include @@ -27,15 +24,15 @@ namespace launchdarkly::server_side::data_systems { * too large to fit in memory, or a direct connection to LaunchDarkly isn't * desired, necessitating use of the alternate LazyLoad system. */ -class BackgroundSync : public data_interfaces::ISystem { +class BackgroundSync final : public data_interfaces::ISystem { public: - BackgroundSync(config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::BackgroundSyncConfig< - config::shared::ServerSDK> const& background_sync_config, - config::shared::built::HttpProperties http_properties, - boost::asio::any_io_executor ioc, - data_components::DataSourceStatusManager& status_manager, - Logger const& logger); + BackgroundSync( + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig const& background_sync_config, + config::built::HttpProperties http_properties, + boost::asio::any_io_executor ioc, + data_components::DataSourceStatusManager& status_manager, + Logger const& logger); /** * @brief Constructs a BackgroundSync without a data source. @@ -64,7 +61,7 @@ class BackgroundSync : public data_interfaces::ISystem { private: data_components::MemoryStore store_; - data_components::ChangeNotifierDestination change_notifier_; + data_components::ChangeNotifier change_notifier_; // Needs to be shared to that the source can keep itself alive through // async operations. std::shared_ptr synchronizer_; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp index 11679b8b9..28f07ab5c 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp @@ -4,7 +4,7 @@ namespace launchdarkly::server_side::data_systems { -void NullDataSource::Start() { +void NullDataSource::StartAsync() { status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); } diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index d56146c81..0f0876d48 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -1,15 +1,15 @@ #include "polling_data_source.hpp" -#include -#include #include #include + #include #include -#include #include #include +#include + #include namespace launchdarkly::server_side::data_systems { @@ -22,10 +22,9 @@ static char const* const kCouldNotParseEndpoint = "Could not parse polling endpoint URL"; static network::HttpRequest MakeRequest( - config::shared::built::PollingConfig const& - polling_config, - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::HttpProperties const& http_properties) { + config::built::BackgroundSyncConfig::PollingConfig const& polling_config, + config::built::ServiceEndpoints const& endpoints, + config::built::HttpProperties const& http_properties) { auto url = std::make_optional(endpoints.PollingBaseUrl()); url = network::AppendUrl(url, polling_config.polling_get_path); @@ -33,8 +32,7 @@ static network::HttpRequest MakeRequest( network::HttpRequest::BodyType body; network::HttpMethod method = network::HttpMethod::kGet; - config::shared::builders::HttpPropertiesBuilder - builder(http_properties); + config::builders::HttpPropertiesBuilder const builder(http_properties); // If no URL is set, then we will fail the request. return {url.value_or(""), method, builder.Build(), body}; @@ -46,10 +44,10 @@ std::string const& PollingDataSource::Identity() const { } PollingDataSource::PollingDataSource( - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::PollingConfig const& + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig::PollingConfig const& data_source_config, - config::shared::built::HttpProperties const& http_properties, + config::built::HttpProperties const& http_properties, boost::asio::any_io_executor const& ioc, data_interfaces::IDestination& handler, data_components::DataSourceStatusManager& status_manager, @@ -106,9 +104,7 @@ void PollingDataSource::HandlePollResult(network::HttpResult const& res) { } if (has_etag) { - config::shared::builders::HttpPropertiesBuilder< - config::shared::ServerSDK> - builder(request_.Properties()); + config::builders::HttpPropertiesBuilder builder(request_.Properties()); builder.Header("If-None-Match", header_etag->second); request_ = network::HttpRequest(request_, builder.Build()); @@ -215,7 +211,7 @@ void PollingDataSource::StartPollingTimer() { }); } -void PollingDataSource::Start() { +void PollingDataSource::StartAsync() { status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); if (!request_.Valid()) { LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp index 1022fb157..0b87706ae 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp @@ -4,9 +4,8 @@ #include "../../../../data_interfaces/destination/idestination.hpp" #include "../../../../data_interfaces/source/ipush_source.hpp" -#include -#include -#include +#include + #include #include @@ -20,18 +19,17 @@ class PollingDataSource : public data_interfaces::IPushSource, public std::enable_shared_from_this { public: - PollingDataSource( - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::PollingConfig const& - data_source_config, - config::shared::built::HttpProperties const& http_properties, - boost::asio::any_io_executor const& ioc, - data_interfaces::IDestination& handler, - data_components::DataSourceStatusManager& status_manager, - Logger const& logger); + PollingDataSource(config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig::PollingConfig const& + data_source_config, + config::built::HttpProperties const& http_properties, + boost::asio::any_io_executor const& ioc, + data_interfaces::IDestination& handler, + data_components::DataSourceStatusManager& status_manager, + Logger const& logger); void Init(std::optional initial_data) override; - void Start() override; + void StartAsync() override; void ShutdownAsync(std::function completion) override; [[nodiscard]] std::string const& Identity() const override; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp index f38911870..47888c48e 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp @@ -13,7 +13,7 @@ #include #include -#include "tl/expected.hpp" +#include namespace launchdarkly::server_side::data_systems { diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp index e7b5a1564..b4e580630 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp @@ -1,19 +1,18 @@ #pragma once -#include - -#include - -#include - #include "../../../../data_components/dependency_tracker/data_kind.hpp" #include "../../../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../../../data_interfaces/destination/idestination.hpp" #include +#include #include -namespace launchdarkly::server_side::data_sources { +#include + +#include + +namespace launchdarkly::server_side::data_systems { // The FlagsPath and SegmentsPath are made to turn a string literal into a type // for use in a template. diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index b8209d00b..efe93b7cc 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -55,7 +55,7 @@ void StreamingDataSource::Init( // TODO: implement } -void StreamingDataSource::Start() { +void StreamingDataSource::StartAsync() { status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); auto updated_url = network::AppendUrl(streaming_endpoint_, diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp index e48abb079..1a01eebff 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp @@ -28,17 +28,19 @@ class StreamingDataSource final public std::enable_shared_from_this { public: StreamingDataSource( - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::StreamingConfig const& + ::launchdarkly::config::shared::built::ServiceEndpoints const& + endpoints, + ::launchdarkly::config::shared::built::StreamingConfig< + ::launchdarkly::config::shared::ServerSDK> const& data_source_config, - config::shared::built::HttpProperties http_properties, + launchdarkly::config::shared::built::HttpProperties http_properties, boost::asio::any_io_executor ioc, data_interfaces::IDestination& handler, data_components::DataSourceStatusManager& status_manager, Logger const& logger); void Init(std::optional initial_data) override; - void Start() override; + void StartAsync() override; void ShutdownAsync(std::function completion) override; [[nodiscard]] std::string const& Identity() const override; @@ -49,10 +51,11 @@ class StreamingDataSource final DataSourceEventHandler data_source_handler_; std::string streaming_endpoint_; - config::shared::built::StreamingConfig + ::launchdarkly::config::shared::built::StreamingConfig< + ::launchdarkly::config::shared::ServerSDK> streaming_config_; - config::shared::built::HttpProperties http_config_; + ::launchdarkly::config::shared::built::HttpProperties http_config_; Logger const& logger_; std::shared_ptr client_; diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index c64ce3f00..e2efd24cd 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -3,7 +3,7 @@ #include #include -#include "sources/redis/redis_source.hpp" +#include namespace launchdarkly::server_side::data_systems { @@ -86,21 +86,30 @@ bool LazyLoad::Initialized() const { } void LazyLoad::RefreshAllFlags() const { - for (auto const [flag_key, flag_descriptor] : source_.AllFlags()) { - cache_.Upsert(flag_key, std::move(flag_descriptor)); + auto maybe_flags = source_.AllFlags(); + // TODO: log failure? + if (maybe_flags) { + for (auto const [flag_key, flag_descriptor] : *maybe_flags) { + cache_.Upsert(flag_key, std::move(flag_descriptor)); + } + tracker_.Add(Keys::kAllFlags, time_()); } - tracker_.Add(Keys::kAllFlags, time_()); } void LazyLoad::RefreshAllSegments() const { - for (auto const [seg_key, seg_descriptor] : source_.AllSegments()) { - cache_.Upsert(seg_key, std::move(seg_descriptor)); + auto maybe_segments = source_.AllSegments(); + // TODO: log failure? + if (maybe_segments) { + for (auto const [seg_key, seg_descriptor] : *maybe_segments) { + cache_.Upsert(seg_key, std::move(seg_descriptor)); + } + tracker_.Add(Keys::kAllSegments, time_()); } - tracker_.Add(Keys::kAllSegments, time_()); } void LazyLoad::RefreshInitState() const { - initialized_ = source_.Initialized(); + // TODO: what does this matter? + // initialized_ = source_.Initialized(); tracker_.Add(Keys::kInitialized, time_()); } diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp index 5d013f025..64c55c156 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -19,7 +19,7 @@ data_interfaces::ISerializedDataPullSource::GetResult RedisDataSource::Get( return integrations::SerializedItemDescriptor{0, false, maybe_item.value()}; } - sw::redis::SentinelOptions return tl::make_unexpected(Error{"not found"}); + return tl::make_unexpected(Error{"not found"}); } data_interfaces::ISerializedDataPullSource::AllResult RedisDataSource::All( diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 27206c27b..746105049 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -3,12 +3,13 @@ #include #include #include +#include #include "data_systems/background_sync/sources/streaming/streaming_data_source.hpp" using namespace launchdarkly; using namespace launchdarkly::server_side; -using namespace launchdarkly::config::shared; +using namespace launchdarkly::server_side::config; class ConfigBuilderTest : public ::testing:: @@ -34,39 +35,51 @@ TEST_F(ConfigBuilderTest, DefaultConstruction_StreamingDefaultsAreUsed) { ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); - ASSERT_TRUE(std::holds_alternative>( + ASSERT_TRUE(std::holds_alternative( cfg->DataSystemConfig().system_)); auto const bg_sync_config = - std::get>( - cfg->DataSystemConfig().system_); + std::get(cfg->DataSystemConfig().system_); - ASSERT_TRUE(std::holds_alternative>( - bg_sync_config.source_.method)); + ASSERT_TRUE(std::holds_alternative< + ::launchdarkly::config::shared::built::StreamingConfig< + launchdarkly::config::shared::ServerSDK>>( + bg_sync_config.synchronizer_)); - auto const streaming_config = std::get>( - bg_sync_config.source_.method); + auto const streaming_config = + std::get<::launchdarkly::config::shared::built::StreamingConfig< + launchdarkly::config::shared::ServerSDK>>( + bg_sync_config.synchronizer_); - EXPECT_EQ(streaming_config, server_side::Defaults::StreamingConfig()); + EXPECT_EQ(streaming_config, + ::launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ServerSDK>::StreamingConfig()); } TEST_F(ConfigBuilderTest, DefaultConstruction_HttpPropertyDefaultsAreUsed) { ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); - ASSERT_EQ(cfg->HttpProperties(), server_side::Defaults::HttpProperties()); + ASSERT_EQ(cfg->HttpProperties(), + ::launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ServerSDK>::Defaults:: + HttpProperties()); } TEST_F(ConfigBuilderTest, DefaultConstruction_ServiceEndpointDefaultsAreUsed) { ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); ASSERT_EQ(cfg->ServiceEndpoints(), - server_side::Defaults::ServiceEndpoints()); + ::launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ServerSDK>::Defaults:: + ServiceEndpoints()); } TEST_F(ConfigBuilderTest, DefaultConstruction_EventDefaultsAreUsed) { ConfigBuilder builder("sdk-123"); auto cfg = builder.Build(); - ASSERT_EQ(cfg->Events(), server_side::Defaults::Events()); + ASSERT_EQ(cfg->Events(), + ::launchdarkly::config::shared::Defaults< + launchdarkly::config::shared::ServerSDK>::Defaults::Events()); } TEST_F(ConfigBuilderTest, CanDisableDataSystem) { @@ -84,11 +97,14 @@ TEST_F(ConfigBuilderTest, CanDisableDataSystem) { TEST_F(ConfigBuilderTest, CanConstructValidRedisConfig) { ConfigBuilder builder("sdk-123"); - using LazyLoad = DataSystemBuilder::LazyLoad; + using LazyLoad = builders::DataSystemBuilder::LazyLoad; + + auto redis = std::make_shared( + "tcp://foo.bar:1234", "test"); builder.DataSystem().Method( LazyLoad() - .Source(LazyLoad::Redis().Connection("tcp://localhost:1234")) + .Source(redis) .CacheEviction(LazyLoad::EvictionPolicy::Disabled) .CacheTTL(std::chrono::seconds(5))); @@ -96,31 +112,34 @@ TEST_F(ConfigBuilderTest, CanConstructValidRedisConfig) { ASSERT_TRUE(cfg); } -TEST_F(ConfigBuilderTest, InvalidRedisConfigurationDetected) { - ConfigBuilder builder("sdk-123"); - - using LazyLoad = DataSystemBuilder::LazyLoad; - - // An empty URI string should be rejected before it is passed deeper - // into the redis client. - builder.DataSystem().Method( - LazyLoad().Source(LazyLoad::Redis().Connection(""))); - - auto cfg = builder.Build(); - ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingURI); - - // If using ConnOpts instead of a URI string, the host should be rejected - // for same reason as above. - builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( - LazyLoad::Redis::ConnOpts{"", 1233, "password", 2}))); - - cfg = builder.Build(); - ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingHost); - - // If the port isn't set, it'll be default-constructed as std::nullopt. - builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( - LazyLoad::Redis::ConnOpts{"tcp://localhost"}))); - - cfg = builder.Build(); - ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingPort); -} +// TEST_F(ConfigBuilderTest, InvalidRedisConfigurationDetected) { +// ConfigBuilder builder("sdk-123"); +// +// using LazyLoad = DataSystemBuilder::LazyLoad; +// +// auto redis = std::make_shared( +// "tcp://foo.bar:1234", "test"); +// +// // An empty URI string should be rejected before it is passed deeper +// // into the redis client. +// builder.DataSystem().Method( +// LazyLoad().Source(redis); +// +// auto cfg = builder.Build(); +// ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingURI); +// +// // If using ConnOpts instead of a URI string, the host should be rejected +// // for same reason as above. +// builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( +// LazyLoad::Redis::ConnOpts{"", 1233, "password", 2}))); +// +// cfg = builder.Build(); +// ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingHost); +// +// // If the port isn't set, it'll be default-constructed as std::nullopt. +// builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( +// LazyLoad::Redis::ConnOpts{"tcp://localhost"}))); +// +// cfg = builder.Build(); +// ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingPort); +// } From 59b8e7bc43b0cb8d48719b2d8a489df0f7baa91e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 10:40:22 -0800 Subject: [PATCH 111/244] remove redundant namespace qualifiers in expiration_tracker.cpp --- .../expiration_tracker/expiration_tracker.cpp | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp index d36c7f947..e78c8a412 100644 --- a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp @@ -4,8 +4,7 @@ namespace launchdarkly::server_side::data_components { -void ExpirationTracker::Add(std::string const& key, - ExpirationTracker::TimePoint expiration) { +void ExpirationTracker::Add(std::string const& key, TimePoint expiration) { unscoped_.insert({key, expiration}); } @@ -15,7 +14,7 @@ void ExpirationTracker::Remove(std::string const& key) { ExpirationTracker::TrackState ExpirationTracker::State( std::string const& key, - ExpirationTracker::TimePoint current_time) const { + TimePoint current_time) const { auto item = unscoped_.find(key); if (item != unscoped_.end()) { return State(item->second, current_time); @@ -26,7 +25,7 @@ ExpirationTracker::TrackState ExpirationTracker::State( void ExpirationTracker::Add(DataKind kind, std::string const& key, - ExpirationTracker::TimePoint expiration) { + TimePoint expiration) { scoped_.Set(kind, key, expiration); } @@ -37,7 +36,7 @@ void ExpirationTracker::Remove(DataKind kind, std::string const& key) { ExpirationTracker::TrackState ExpirationTracker::State( DataKind kind, std::string const& key, - ExpirationTracker::TimePoint current_time) const { + TimePoint current_time) const { auto expiration = scoped_.Get(kind, key); if (expiration.has_value()) { return State(expiration.value(), current_time); @@ -50,7 +49,7 @@ void ExpirationTracker::Clear() { unscoped_.clear(); } std::vector, std::string>> -ExpirationTracker::Prune(ExpirationTracker::TimePoint current_time) { +ExpirationTracker::Prune(TimePoint current_time) { std::vector, std::string>> pruned; // Determine everything to be pruned. @@ -79,19 +78,17 @@ ExpirationTracker::Prune(ExpirationTracker::TimePoint current_time) { } return pruned; } -ExpirationTracker::TrackState ExpirationTracker::State( - ExpirationTracker::TimePoint expiration, - ExpirationTracker::TimePoint current_time) { +ExpirationTracker::TrackState ExpirationTracker::State(TimePoint expiration, + TimePoint current_time) { if (expiration > current_time) { - return ExpirationTracker::TrackState::kFresh; + return TrackState::kFresh; } - return ExpirationTracker::TrackState::kStale; + return TrackState::kStale; } -void ExpirationTracker::ScopedTtls::Set( - DataKind kind, - std::string const& key, - ExpirationTracker::TimePoint expiration) { +void ExpirationTracker::ScopedTtls::Set(DataKind kind, + std::string const& key, + TimePoint expiration) { data_[static_cast>(kind)].Data().insert( {key, expiration}); } @@ -108,7 +105,7 @@ void ExpirationTracker::ScopedTtls::Clear() { } } -std::optional ExpirationTracker::ScopedTtls::Get( +std::optional ExpirationTracker::ScopedTtls::Get( DataKind kind, std::string const& key) const { auto const& scope = From d81c08580d05112e48bc4a54dd1ba764b4ae6474 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 10:46:47 -0800 Subject: [PATCH 112/244] add missing configs to CMakeLists.txt --- libs/server-sdk/src/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 6a84a5c14..953f4af4f 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -21,6 +21,10 @@ target_sources(${LIBNAME} client_impl.cpp config/config.cpp config/config_builder.cpp + config/builders/data_system/background_sync_builder.cpp + config/builders/data_system/bootstrap_builder.cpp + config/builders/data_system/data_system_builder.cpp + config/builders/data_system/lazy_load_builder.cpp all_flags_state/all_flags_state.cpp all_flags_state/json_all_flags_state.cpp all_flags_state/all_flags_state_builder.cpp From 6ad3d55a33c65ef574106faaa9e6281f776c72cc Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 10:47:11 -0800 Subject: [PATCH 113/244] fix default configs --- .../config/builders/data_system/background_sync_builder.cpp | 4 ++-- libs/server-sdk/src/config/builders/data_system/defaults.hpp | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp b/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp index c05d7622b..c7411cf1b 100644 --- a/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp +++ b/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp @@ -10,12 +10,12 @@ BootstrapBuilder& BackgroundSyncBuilder::Bootstrapper() { } BackgroundSyncBuilder& BackgroundSyncBuilder::Synchronizer(Streaming source) { - config_.source_.method = source.Build(); + config_.synchronizer_ = source.Build(); return *this; } BackgroundSyncBuilder& BackgroundSyncBuilder::Synchronizer(Polling source) { - config_.source_.method = source.Build(); + config_.synchronizer_ = source.Build(); return *this; } diff --git a/libs/server-sdk/src/config/builders/data_system/defaults.hpp b/libs/server-sdk/src/config/builders/data_system/defaults.hpp index 43c6f734f..a5e182326 100644 --- a/libs/server-sdk/src/config/builders/data_system/defaults.hpp +++ b/libs/server-sdk/src/config/builders/data_system/defaults.hpp @@ -19,6 +19,11 @@ struct Defaults { return std::nullopt; } + static auto DataSourceConfig() + -> built::BackgroundSyncConfig::StreamingConfig { + return {std::chrono::seconds(1), "/all"}; + } + static auto BackgroundSyncConfig() -> built::BackgroundSyncConfig { return {BootstrapConfig(), DataSourceConfig(), DataDestinationConfig()}; } From 6c291a57d21c61ca9f286cf831b64faab9ed89c2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 10:47:21 -0800 Subject: [PATCH 114/244] fix expiration tracker compilation --- .../data_components/expiration_tracker/expiration_tracker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp index e78c8a412..d296d3a7e 100644 --- a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp @@ -105,7 +105,7 @@ void ExpirationTracker::ScopedTtls::Clear() { } } -std::optional ExpirationTracker::ScopedTtls::Get( +std::optional ExpirationTracker::ScopedTtls::Get( DataKind kind, std::string const& key) const { auto const& scope = From 5f3cdca36bfa98a60d20010f3dc33909fa8ac4b8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 10:53:30 -0800 Subject: [PATCH 115/244] add missing data_destination_builder to CMakeLists.txt --- libs/server-sdk/src/CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 953f4af4f..540d1bf91 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -25,6 +25,7 @@ target_sources(${LIBNAME} config/builders/data_system/bootstrap_builder.cpp config/builders/data_system/data_system_builder.cpp config/builders/data_system/lazy_load_builder.cpp + config/builders/data_system/data_destination_builder.cpp all_flags_state/all_flags_state.cpp all_flags_state/json_all_flags_state.cpp all_flags_state/all_flags_state_builder.cpp @@ -36,6 +37,10 @@ target_sources(${LIBNAME} data_components/expiration_tracker/expiration_tracker.cpp data_components/memory_store/memory_store.hpp data_components/memory_store/memory_store.cpp + data_components/serialization_adapters/json_pull_source.cpp + data_components/serialization_adapters/json_pull_source.hpp + data_components/serialization_adapters/json_destination.cpp + data_components/serialization_adapters/json_destination.hpp data_systems/background_sync/sources/noop/null_data_source.hpp data_systems/background_sync/sources/noop/null_data_source.cpp data_systems/background_sync/sources/polling/polling_data_source.hpp From fb44d55ccd3b50291b37476e95500e21dbb28607 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 10:53:48 -0800 Subject: [PATCH 116/244] json pull source compilation --- .../json_pull_source.cpp | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp index d1dbb3ed7..8c056a47b 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp @@ -12,18 +12,17 @@ namespace launchdarkly::server_side::data_components { JsonSource::JsonSource(data_interfaces::ISerializedDataPullSource& json_source) : flag_kind_(), segment_kind_(), source_(json_source) {} - -data_interfaces::IPullSource::ItemResult +data_interfaces::IPullSource::Single JsonSource::GetFlag(std::string const& key) const { return Deserialize(flag_kind_, key); } -data_interfaces::IPullSource::ItemResult +data_interfaces::IPullSource::Single JsonSource::GetSegment(std::string const& key) const { return Deserialize(segment_kind_, key); } -data_interfaces::IPullSource::AllResult +data_interfaces::IPullSource::Collection JsonSource::AllFlags() const { // TODO: deserialize then return data_interfaces::ISerializedDataPullSource::AllResult result = @@ -32,15 +31,14 @@ JsonSource::AllFlags() const { return tl::make_unexpected(result.error().message); } - AllResult flags; + Collection flags; for (auto [key, val] : *result) { - auto deserialized = Deserialize(std::move(val)); - + // TODO } return flags; } -data_interfaces::IPullSource::AllResult +data_interfaces::IPullSource::Collection JsonSource::AllSegments() const { // TODO: deserialize then return @@ -52,8 +50,4 @@ std::string const& JsonSource::Identity() const { return source_.Identity(); } -bool JsonSource::Initialized() const { - return source_.Initialized(); -} - } // namespace launchdarkly::server_side::data_components From 74069638f8764ce0c105eb29bff488e36e54885d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 10:59:57 -0800 Subject: [PATCH 117/244] add missing kinds.cpp to CMakeLists.txt --- libs/server-sdk/src/CMakeLists.txt | 2 ++ .../src/data_systems/lazy_load/lazy_load_system.cpp | 2 -- .../src/data_systems/lazy_load/lazy_load_system.hpp | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 540d1bf91..ec1778551 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -41,6 +41,8 @@ target_sources(${LIBNAME} data_components/serialization_adapters/json_pull_source.hpp data_components/serialization_adapters/json_destination.cpp data_components/serialization_adapters/json_destination.hpp + data_components/kinds/kinds.hpp + data_components/kinds/kinds.cpp data_systems/background_sync/sources/noop/null_data_source.hpp data_systems/background_sync/sources/noop/null_data_source.cpp data_systems/background_sync/sources/polling/polling_data_source.hpp diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index e2efd24cd..6a6d3186f 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -7,8 +7,6 @@ namespace launchdarkly::server_side::data_systems { -using namespace config::shared::built; - /* *DataSourceConfig const& data_source_config, HttpProperties http_properties, diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 00089824d..05750a753 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -4,18 +4,12 @@ #include "../../data_components/kinds/kinds.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/serialization_adapters/json_pull_source.hpp" -#include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/system/isystem.hpp" -#include -#include -#include #include #include #include -#include - namespace launchdarkly::server_side::data_systems { /** From 07edf2940eda2a39f5286008ecb2f4c49d7e205f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 11:01:16 -0800 Subject: [PATCH 118/244] add missing data_source_status.cpp; compiles --- libs/server-sdk/src/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index ec1778551..4c99db659 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -19,6 +19,7 @@ target_sources(${LIBNAME} boost.cpp client.cpp client_impl.cpp + data_source_status.cpp config/config.cpp config/config_builder.cpp config/builders/data_system/background_sync_builder.cpp From c6f9953fa5218384690b8f9f80b5a7823b775f67 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 11:51:16 -0800 Subject: [PATCH 119/244] unit tests for lazy load system --- .../data_system/lazy_load_builder.hpp | 2 +- .../built/data_system/lazy_load_config.hpp | 2 +- .../data_system/lazy_load_builder.cpp | 4 +- .../expiration_tracker/expiration_tracker.cpp | 10 ++- .../expiration_tracker/expiration_tracker.hpp | 12 ++-- .../lazy_load/lazy_load_system.cpp | 5 +- .../lazy_load/lazy_load_system.hpp | 6 +- libs/server-sdk/tests/config_builder_test.cpp | 2 +- .../tests/lazy_load_system_test.cpp | 63 +++++++++++++++++-- 9 files changed, 78 insertions(+), 28 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp index abe68288c..5010a883f 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp @@ -46,7 +46,7 @@ struct LazyLoadBuilder { * before being refreshed from the database. The chosen \ref EvictionPolicy * affects usage of this TTL. \return Reference to this. */ - LazyLoadBuilder& CacheTTL(std::chrono::milliseconds ttl); + LazyLoadBuilder& CacheRefresh(std::chrono::milliseconds ttl); /** * \brief Specify the eviction policy when a data item's TTL expires. diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp index e4517def4..aed530ad7 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp @@ -18,7 +18,7 @@ struct LazyLoadConfig { }; EvictionPolicy eviction_policy; - std::chrono::milliseconds eviction_ttl; + std::chrono::milliseconds refresh_ttl; std::shared_ptr source; }; diff --git a/libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp b/libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp index 06cef97f2..12fc0597b 100644 --- a/libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp +++ b/libs/server-sdk/src/config/builders/data_system/lazy_load_builder.cpp @@ -6,9 +6,9 @@ namespace launchdarkly::server_side::config::builders { LazyLoadBuilder::LazyLoadBuilder() : config_(Defaults::LazyLoadConfig()) {} -LazyLoadBuilder& LazyLoadBuilder::CacheTTL( +LazyLoadBuilder& LazyLoadBuilder::CacheRefresh( std::chrono::milliseconds const ttl) { - config_.eviction_ttl = ttl; + config_.refresh_ttl = ttl; return *this; } diff --git a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp index d296d3a7e..8ce76e4d7 100644 --- a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp @@ -20,7 +20,7 @@ ExpirationTracker::TrackState ExpirationTracker::State( return State(item->second, current_time); } - return ExpirationTracker::TrackState::kNotTracked; + return TrackState::kNotTracked; } void ExpirationTracker::Add(DataKind kind, @@ -41,7 +41,7 @@ ExpirationTracker::TrackState ExpirationTracker::State( if (expiration.has_value()) { return State(expiration.value(), current_time); } - return ExpirationTracker::TrackState::kNotTracked; + return TrackState::kNotTracked; } void ExpirationTracker::Clear() { @@ -54,15 +54,13 @@ ExpirationTracker::Prune(TimePoint current_time) { // Determine everything to be pruned. for (auto const& item : unscoped_) { - if (State(item.second, current_time) == - ExpirationTracker::TrackState::kStale) { + if (State(item.second, current_time) == 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) { + if (State(item.second, current_time) == TrackState::kStale) { pruned.emplace_back(scope.Kind(), item.first); } } diff --git a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp index 786b9d453..c99421e11 100644 --- a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp @@ -68,9 +68,7 @@ class ExpirationTracker { * @param key The key to track. * @param expiration The time that the key expires. */ - void Add(data_components::DataKind kind, - std::string const& key, - TimePoint expiration); + void Add(DataKind kind, std::string const& key, TimePoint expiration); /** * Remove a scoped key from the tracker. @@ -78,7 +76,7 @@ class ExpirationTracker { * @param kind The scope (kind) of the key. * @param key The key to stop tracking. */ - void Remove(data_components::DataKind kind, std::string const& key); + void Remove(DataKind kind, std::string const& key); /** * Check the state of a scoped key. @@ -87,7 +85,7 @@ class ExpirationTracker { * @param key The key to check. * @return The state of the key. */ - TrackState State(data_components::DataKind kind, + TrackState State(DataKind kind, std::string const& key, TimePoint current_time) const; @@ -110,9 +108,7 @@ class ExpirationTracker { TtlMap unscoped_; - static ExpirationTracker::TrackState State( - ExpirationTracker::TimePoint expiration, - ExpirationTracker::TimePoint current_time); + static TrackState State(TimePoint expiration, TimePoint current_time); class ScopedTtls { public: diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 6a6d3186f..2778f89fa 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -13,10 +13,9 @@ HttpProperties http_properties, boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, */ -LazyLoad::LazyLoad() +LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg) : cache_(), - raw_source_( - std::make_shared("tcp://localhost:6379", "test")), + raw_source_(std::move(cfg.source)), source_(*raw_source_.get()), tracker_(), time_([]() { return std::chrono::steady_clock::now(); }), diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 05750a753..2c71043f8 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -6,6 +6,8 @@ #include "../../data_components/serialization_adapters/json_pull_source.hpp" #include "../../data_interfaces/system/isystem.hpp" +#include + #include #include #include @@ -21,9 +23,9 @@ namespace launchdarkly::server_side::data_systems { * LazyLoad is able to remain efficient because it caches responses from the * store. Over time, data becomes stale causing the system to refresh data. */ -class LazyLoad : public data_interfaces::ISystem { +class LazyLoad final : public data_interfaces::ISystem { public: - LazyLoad(); + explicit LazyLoad(config::built::LazyLoadConfig cfg); LazyLoad(LazyLoad const& item) = delete; LazyLoad(LazyLoad&& item) = delete; diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index 746105049..ec33dedbe 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -106,7 +106,7 @@ TEST_F(ConfigBuilderTest, CanConstructValidRedisConfig) { LazyLoad() .Source(redis) .CacheEviction(LazyLoad::EvictionPolicy::Disabled) - .CacheTTL(std::chrono::seconds(5))); + .CacheRefresh(std::chrono::seconds(5))); auto cfg = builder.Build(); ASSERT_TRUE(cfg); diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index 281c460bf..12605c567 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -2,11 +2,66 @@ #include "data_systems/lazy_load/lazy_load_system.hpp" -using namespace launchdarkly::server_side::data_systems; +#include +#include + +using namespace launchdarkly::server_side; +using namespace launchdarkly::server_side::config; class LazyLoadTest : public ::testing::Test {}; -TEST_F(LazyLoadTest, Thing) { - LazyLoad system; - ASSERT_FALSE(system.GetFlag("foo")); +class FakeDataSource : public launchdarkly::server_side::data_interfaces:: + ISerializedDataPullSource { + public: + FakeDataSource(std::string name) + : name(std::move(name)), items_requested() {} + GetResult Get(integrations::IPersistentKind const& kind, + std::string const& itemKey) const override { + items_requested[kind.Namespace()].push_back(itemKey); + return tl::make_unexpected( + ISerializedDataPullSource::Error{"no such flag"}); + } + AllResult All(integrations::IPersistentKind const& kind) const override {} + + std::string const& Identity() const override { return name; } + bool Initialized() const override { return true; } + + std::string name; + mutable std::unordered_map> + items_requested; +}; + +TEST_F(LazyLoadTest, ItentityIncludesSourceIdentity) { + std::string const name = "fake source"; + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::milliseconds(100), std::make_shared(name)}; + + data_systems::LazyLoad const lazy_load(config); + + ASSERT_EQ(lazy_load.Identity(), "lazy load via " + name); +} + +TEST_F(LazyLoadTest, SourceIsAccessedRepeatedlyIfFetchingFails) { + auto source = std::make_shared("fake source"); + + // We want the cache refresh logic to not play a role in this unit test, so + // set the refresh large enough that it's highly unlikely to be triggered. + auto refresh = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; + + data_systems::LazyLoad const lazy_load(config); + + constexpr std::size_t kNumCalls = 10; + + // Each call will need to hit the source because the FakeDataSource always + // responds with an error. + for (std::size_t i = 0; i < kNumCalls; i++) { + ASSERT_FALSE(lazy_load.GetFlag("foo")); + } + + ASSERT_EQ(source->items_requested["features"].size(), kNumCalls); } From ee63fa5fe0348c94fa1bba603f258b197d3e3b4c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 14:13:18 -0800 Subject: [PATCH 120/244] implementing more of lazy load tracker and unit tests --- .../lazy_load/lazy_load_system.cpp | 12 +- .../lazy_load/lazy_load_system.hpp | 6 +- .../tests/lazy_load_system_test.cpp | 128 ++++++++++++++++-- 3 files changed, 127 insertions(+), 19 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 2778f89fa..75d0c2b64 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -14,11 +14,15 @@ boost::asio::any_io_executor ioc, data_components::DataSourceStatusManager& status_manager, */ LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg) + : LazyLoad(std::move(cfg), + []() { return std::chrono::steady_clock::now(); }) {} + +LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg, TimeFn time) : cache_(), raw_source_(std::move(cfg.source)), source_(*raw_source_.get()), tracker_(), - time_([]() { return std::chrono::steady_clock::now(); }), + time_(std::move(time)), initialized_() {} std::string const& LazyLoad::Identity() const { @@ -30,7 +34,8 @@ void LazyLoad::Initialize() {} std::shared_ptr LazyLoad::GetFlag( std::string const& key) const { - auto const state = tracker_.State(Keys::kAllSegments, time_()); + auto const state = + tracker_.State(data_components::DataKind::kFlag, key, time_()); return Get>( state, [this, &key]() { RefreshFlag(key); }, [this, &key]() { return cache_.GetFlag(key); }); @@ -38,7 +43,8 @@ std::shared_ptr LazyLoad::GetFlag( std::shared_ptr LazyLoad::GetSegment( std::string const& key) const { - auto const state = tracker_.State(Keys::kAllSegments, time_()); + auto const state = + tracker_.State(data_components::DataKind::kSegment, key, time_()); return Get>( state, [this, &key]() { RefreshSegment(key); }, [this, &key]() { return cache_.GetSegment(key); }); diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 2c71043f8..2e0216966 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -25,7 +25,11 @@ namespace launchdarkly::server_side::data_systems { */ class LazyLoad final : public data_interfaces::ISystem { public: + using TimeFn = + std::function()>; + explicit LazyLoad(config::built::LazyLoadConfig cfg); + LazyLoad(config::built::LazyLoadConfig cfg, TimeFn time); LazyLoad(LazyLoad const& item) = delete; LazyLoad(LazyLoad&& item) = delete; @@ -80,7 +84,7 @@ class LazyLoad final : public data_interfaces::ISystem { data_components::JsonSource source_; mutable data_components::ExpirationTracker tracker_; - std::function()> time_; + TimeFn time_; mutable std::optional initialized_; struct Kinds { diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index 12605c567..de3608d04 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -13,22 +13,54 @@ class LazyLoadTest : public ::testing::Test {}; class FakeDataSource : public launchdarkly::server_side::data_interfaces:: ISerializedDataPullSource { public: - FakeDataSource(std::string name) - : name(std::move(name)), items_requested() {} + using GetFn = std::function; + + using AllFn = + std::function; + + FakeDataSource(std::string name, GetFn items, AllFn all) + : name(std::move(name)), + item_getter([](integrations::IPersistentKind const& kind, + std::string const& key) { + return tl::make_unexpected(Error{key + "not found"}); + }), + all_getter([](integrations::IPersistentKind const& kind) { + return tl::make_unexpected(Error{kind.Namespace() + "not found"}); + }), + items_requested() { + if (items) { + item_getter = std::move(items); + } + if (all) { + all_getter = std::move(all); + } + } + + explicit FakeDataSource(std::string name) + : FakeDataSource(std::move(name), nullptr, nullptr) {} + + FakeDataSource() : FakeDataSource("fake source") {} + GetResult Get(integrations::IPersistentKind const& kind, std::string const& itemKey) const override { items_requested[kind.Namespace()].push_back(itemKey); - return tl::make_unexpected( - ISerializedDataPullSource::Error{"no such flag"}); + return item_getter(kind, itemKey); + } + AllResult All(integrations::IPersistentKind const& kind) const override { + all_requested.push_back(kind.Namespace()); + return all_getter(kind); } - AllResult All(integrations::IPersistentKind const& kind) const override {} std::string const& Identity() const override { return name; } bool Initialized() const override { return true; } std::string name; + GetFn item_getter; + AllFn all_getter; mutable std::unordered_map> items_requested; + mutable std::vector all_requested; }; TEST_F(LazyLoadTest, ItentityIncludesSourceIdentity) { @@ -43,11 +75,9 @@ TEST_F(LazyLoadTest, ItentityIncludesSourceIdentity) { ASSERT_EQ(lazy_load.Identity(), "lazy load via " + name); } -TEST_F(LazyLoadTest, SourceIsAccessedRepeatedlyIfFetchingFails) { - auto source = std::make_shared("fake source"); +TEST_F(LazyLoadTest, SourceIsNotAccessedIfFetchFails) { + auto source = std::make_shared(); - // We want the cache refresh logic to not play a role in this unit test, so - // set the refresh large enough that it's highly unlikely to be triggered. auto refresh = std::chrono::seconds(10); built::LazyLoadConfig const config{ @@ -55,13 +85,81 @@ TEST_F(LazyLoadTest, SourceIsAccessedRepeatedlyIfFetchingFails) { data_systems::LazyLoad const lazy_load(config); - constexpr std::size_t kNumCalls = 10; - - // Each call will need to hit the source because the FakeDataSource always - // responds with an error. - for (std::size_t i = 0; i < kNumCalls; i++) { + // Although we ask for 'foo' 10 times, the underlying source should only + // receive one call because the refresh is 10 seconds. Only after 10 seconds + // elapse would the source be queried again. + for (std::size_t i = 0; i < 10; i++) { ASSERT_FALSE(lazy_load.GetFlag("foo")); } - ASSERT_EQ(source->items_requested["features"].size(), kNumCalls); + ASSERT_EQ(source->items_requested["features"].size(), 1); +} + +TEST_F(LazyLoadTest, FetchingAllSegmentsRefreshesIndividualSegments) { + auto source = std::make_shared(); + + auto refresh = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; + + data_systems::LazyLoad const lazy_load(config); + + ASSERT_FALSE(lazy_load.AllSegments().empty()); + ASSERT_TRUE(lazy_load.GetSegment("foo")); + + // Since all segments were requested, then.. + ASSERT_EQ(source->all_requested, std::vector{"segments"}); + // There should be no individual request for a segment. + ASSERT_TRUE(source->items_requested["segments"].empty()); +} + +TEST_F(LazyLoadTest, FetchingAllFlagsRefreshesIndividualFlags) { + auto source = std::make_shared(); + + auto refresh = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; + + data_systems::LazyLoad const lazy_load(config); + + ASSERT_FALSE(lazy_load.AllFlags().empty()); + ASSERT_TRUE(lazy_load.GetFlag("foo")); + + // Since all flags were requested, then.. + ASSERT_EQ(source->all_requested, std::vector{"features"}); + // There should be no individual request for a segment. + ASSERT_TRUE(source->items_requested["features"].empty()); +} + +TEST_F(LazyLoadTest, ItemIsRefreshedAfterDelay) { + auto source = std::make_shared(); + + auto refresh = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; + + std::chrono::time_point now( + std::chrono::seconds(0)); + + data_systems::LazyLoad const lazy_load(config, [&]() { return now; }); + + // Simulate time moving forward for 9 seconds. Each time, the flag should be + // served from the cache rather than quering the store. + for (std::size_t i = 0; i < 9; i++) { + now = std::chrono::time_point( + std::chrono::seconds(i)); + ASSERT_TRUE(lazy_load.GetFlag("foo")); + ASSERT_EQ(source->items_requested["features"], + std::vector{"foo"}); + } + + // Advance past the refresh time. Now the flag should be queried again. + now = std::chrono::time_point(refresh); + + ASSERT_TRUE(lazy_load.GetFlag("foo")); + auto expected = std::vector{"foo", "foo"}; + ASSERT_EQ(source->items_requested["features"], expected); } From 0f6b4681b37f59f4215b49bb4c7c1e16bcc6b684 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 15:34:32 -0800 Subject: [PATCH 121/244] refactor IPushSource to take IDestination as method argument instead of hidden in constructors --- .../data_interfaces/source/ipush_source.hpp | 31 ++++++++----- .../src/data_interfaces/system/isystem.hpp | 8 +++- .../background_sync_system.cpp | 19 +++++--- .../background_sync_system.hpp | 2 + .../sources/noop/null_data_source.cpp | 5 +- .../sources/noop/null_data_source.hpp | 4 +- .../sources/polling/polling_data_source.cpp | 33 +++++++------ .../sources/polling/polling_data_source.hpp | 40 +++++++++++----- .../sources/streaming/event_handler.cpp | 10 ++-- .../sources/streaming/event_handler.hpp | 2 +- .../streaming/streaming_data_source.cpp | 41 ++++++++--------- .../streaming/streaming_data_source.hpp | 46 +++++++------------ .../lazy_load/lazy_load_system.cpp | 2 + .../lazy_load/lazy_load_system.hpp | 2 + 14 files changed, 134 insertions(+), 111 deletions(-) diff --git a/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp index fd24538c5..2df780a72 100644 --- a/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp +++ b/libs/server-sdk/src/data_interfaces/source/ipush_source.hpp @@ -6,6 +6,8 @@ #include #include +#include "../destination/idestination.hpp" + namespace launchdarkly::server_side::data_interfaces { /** @@ -15,18 +17,25 @@ namespace launchdarkly::server_side::data_interfaces { class IPushSource { public: /** - * \brief Initialize the source, optionally with an initial data set. Init - * will be called before Start. - * \param initial_data Initial set of SDK data. - */ - virtual void Init(std::optional initial_data) = 0; - - /** - * \brief Starts the synchronization mechanism. Start will be called only - * once after Init; the source is responsible for maintaining a persistent - * connection. Start should not block. + * @brief Starts synchronizing data into the given IDestination. + * + * + * The second parameter, boostrap_data, may be nullptr meaning no bootstrap + * data is present in the SDK and a full synchronization must be initiated. + * + * If bootstrap_data is not nullptr, then it contains data obtained by the + * SDK out-of-band from the source's mechanism. The pointer is valid only + * for this call. + * + * The data may be used to optimize the synchronization process, e.g. by + * obtaining a diff rather than a full dataset. + * @param destination The destination to synchronize data into. Pointer is + * invalid after the ShutdownAsync completion handler is called. + * @param bootstrap_data Optional bootstrap data. + * Pointer is valid only for this call. */ - virtual void StartAsync() = 0; + virtual void StartAsync(IDestination* destination, + data_model::SDKDataSet const* bootstrap_data) = 0; /** * \brief Stops the synchronization mechanism. Stop will be called only once diff --git a/libs/server-sdk/src/data_interfaces/system/isystem.hpp b/libs/server-sdk/src/data_interfaces/system/isystem.hpp index 7a1a389e0..89faf3dd3 100644 --- a/libs/server-sdk/src/data_interfaces/system/isystem.hpp +++ b/libs/server-sdk/src/data_interfaces/system/isystem.hpp @@ -16,11 +16,17 @@ class ISystem : public IStore { [[nodiscard]] virtual std::string const& Identity() const = 0; /** - * \brief Initializes the system. This method will be called before any of + * @brief Initializes the system. This method will be called before any of * the IStore methods are called. */ virtual void Initialize() = 0; + /** + * @brief Shuts down the system. This method will be called at some point + * after Initialize, after which no methods from IStore will be called. + */ + virtual void Shutdown() = 0; + virtual ~ISystem() override = default; ISystem(ISystem const& item) = delete; ISystem(ISystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index ae2682a63..d545b0421 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -21,15 +21,15 @@ BackgroundSync::BackgroundSync( config::built::BackgroundSyncConfig:: StreamingConfig>) { synchronizer_ = std::make_shared( - endpoints, method_config, http_properties, ioc, - change_notifier_, status_manager, logger); + ioc, logger, status_manager, endpoints, method_config, + http_properties); } else if constexpr (std::is_same_v< T, config::built::BackgroundSyncConfig:: PollingConfig>) { synchronizer_ = std::make_shared( - endpoints, method_config, http_properties, ioc, - change_notifier_, status_manager, logger); + ioc, logger, status_manager, endpoints, method_config, + http_properties); } }, background_sync_config.synchronizer_); @@ -43,9 +43,14 @@ BackgroundSync::BackgroundSync( synchronizer_(std::make_shared(ioc, status_manager)) {} void BackgroundSync::Initialize() { - // TODO: if there was any data from bootstrapping, then add it: - // synchronizer_->Init(data); - synchronizer_->StartAsync(); + synchronizer_->StartAsync(&change_notifier_, nullptr); +} + +void BackgroundSync::Shutdown() { + auto promise = std::make_shared>(); + auto const did_shutdown = promise->get_future(); + synchronizer_->ShutdownAsync([promise]() { promise->set_value(); }); + did_shutdown.wait(); } std::string const& BackgroundSync::Identity() const { diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index d7b032237..d8b7ab981 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -59,6 +59,8 @@ class BackgroundSync final : public data_interfaces::ISystem { void Initialize() override; + void Shutdown() override; + private: data_components::MemoryStore store_; data_components::ChangeNotifier change_notifier_; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp index 28f07ab5c..a6b70f2c7 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp @@ -4,7 +4,8 @@ namespace launchdarkly::server_side::data_systems { -void NullDataSource::StartAsync() { +void NullDataSource::StartAsync(data_interfaces::IDestination* destination, + data_model::SDKDataSet const* initial_data) { status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); } @@ -12,8 +13,6 @@ void NullDataSource::ShutdownAsync(std::function complete) { boost::asio::post(exec_, complete); } -void NullDataSource::Init(std::optional initial_data) {} - std::string const& NullDataSource::Identity() const { static std::string const identity = "no-op data source"; return identity; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp index cd4619928..762d0a4b0 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp @@ -13,8 +13,8 @@ class NullDataSource : public data_interfaces::IPushSource { boost::asio::any_io_executor exec, data_components::DataSourceStatusManager& status_manager); - void Init(std::optional initial_data) override; - void StartAsync() override; + void StartAsync(data_interfaces::IDestination* destination, + data_model::SDKDataSet const* initial_data) override; void ShutdownAsync(std::function) override; [[nodiscard]] std::string const& Identity() const override; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp index 0f0876d48..f88eb5cc9 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.cpp @@ -13,7 +13,6 @@ #include namespace launchdarkly::server_side::data_systems { - static char const* const kErrorParsingPut = "Could not parse polling payload"; static char const* const kErrorPutInvalid = "Polling payload contained invalid data"; @@ -44,22 +43,20 @@ std::string const& PollingDataSource::Identity() const { } PollingDataSource::PollingDataSource( + boost::asio::any_io_executor const& ioc, + Logger const& logger, + data_components::DataSourceStatusManager& status_manager, config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::PollingConfig const& data_source_config, - config::built::HttpProperties const& http_properties, - boost::asio::any_io_executor const& ioc, - data_interfaces::IDestination& handler, - data_components::DataSourceStatusManager& status_manager, - Logger const& logger) - : ioc_(ioc), - logger_(logger), + config::built::HttpProperties const& http_properties) + : logger_(logger), status_manager_(status_manager), - update_sink_(handler), requester_(ioc), - timer_(ioc), polling_interval_(data_source_config.poll_interval), - request_(MakeRequest(data_source_config, endpoints, http_properties)) { + request_(MakeRequest(data_source_config, endpoints, http_properties)), + timer_(ioc), + sink_(nullptr) { if (polling_interval_ < data_source_config.min_polling_interval) { LD_LOG(logger_, LogLevel::kWarn) << "Polling interval too frequent, defaulting to " @@ -72,10 +69,6 @@ PollingDataSource::PollingDataSource( } } -void PollingDataSource::Init( - std::optional initial_data) { - // TODO: implement -} void PollingDataSource::DoPoll() { last_poll_start_ = std::chrono::system_clock::now(); @@ -136,7 +129,7 @@ void PollingDataSource::HandlePollResult(network::HttpResult const& res) { tl::expected>(parsed); if (poll_result.has_value()) { - update_sink_.Init(std::move(*poll_result)); + sink_->Init(std::move(*poll_result)); status_manager_.SetState( DataSourceStatus::DataSourceState::kValid); return; @@ -211,7 +204,13 @@ void PollingDataSource::StartPollingTimer() { }); } -void PollingDataSource::StartAsync() { +void PollingDataSource::StartAsync( + data_interfaces::IDestination* dest, + data_model::SDKDataSet const* bootstrap_data) { + boost::ignore_unused(bootstrap_data); + + sink_ = dest; + status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); if (!request_.Valid()) { LD_LOG(logger_, LogLevel::kError) << kCouldNotParseEndpoint; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp index 0b87706ae..8f2b1dd7c 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/polling/polling_data_source.hpp @@ -19,17 +19,17 @@ class PollingDataSource : public data_interfaces::IPushSource, public std::enable_shared_from_this { public: - PollingDataSource(config::built::ServiceEndpoints const& endpoints, + PollingDataSource(boost::asio::any_io_executor const& ioc, + Logger const& logger, + data_components::DataSourceStatusManager& status_manager, + config::built::ServiceEndpoints const& endpoints, config::built::BackgroundSyncConfig::PollingConfig const& data_source_config, - config::built::HttpProperties const& http_properties, - boost::asio::any_io_executor const& ioc, - data_interfaces::IDestination& handler, - data_components::DataSourceStatusManager& status_manager, - Logger const& logger); + config::built::HttpProperties const& http_properties); + + void StartAsync(data_interfaces::IDestination* dest, + data_model::SDKDataSet const* bootstrap_data) override; - void Init(std::optional initial_data) override; - void StartAsync() override; void ShutdownAsync(std::function completion) override; [[nodiscard]] std::string const& Identity() const override; @@ -38,19 +38,35 @@ class PollingDataSource void DoPoll(); void HandlePollResult(network::HttpResult const& res); + Logger const& logger_; + + // Status manager is used to report the status of the data source. It must + // outlive the source. This source performs asynchronous + // operations, so a completion handler might invoke the status manager after + // the it has been destroyed. data_components::DataSourceStatusManager& status_manager_; - std::string polling_endpoint_; + // Responsible for performing HTTP requests using boost::asio. network::AsioRequester requester_; - Logger const& logger_; - boost::asio::any_io_executor ioc_; + + // How long to wait beteween individual polling requests. std::chrono::seconds polling_interval_; + + // Cached request arguments used in the polling request. network::HttpRequest request_; + + // Etag can be sent in HTTP request to save bandwidth if the server knows + // the response is unchanged. std::optional etag_; + // Used with polling_interval to schedule polling requests. boost::asio::steady_timer timer_; + + // The last time the polling HTTP request is initiated. std::chrono::time_point last_poll_start_; - data_interfaces::IDestination& update_sink_; + + // Destination for all data obtained via polling. + data_interfaces::IDestination* sink_; void StartPollingTimer(); }; diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp index 47888c48e..90c2bc6d5 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.cpp @@ -142,7 +142,7 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorParsingPut); - return DataSourceEventHandler::MessageStatus::kInvalidMessage; + return MessageStatus::kInvalidMessage; } auto res = boost::json::value_to, JsonError>>( @@ -153,14 +153,14 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorPutInvalid); - return DataSourceEventHandler::MessageStatus::kInvalidMessage; + return 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 MessageStatus::kMessageHandled; } return DataSourceEventHandler::MessageStatus::kMessageHandled; } @@ -232,10 +232,10 @@ DataSourceEventHandler::MessageStatus DataSourceEventHandler::HandleMessage( status_manager_.SetError( DataSourceStatus::ErrorInfo::ErrorKind::kInvalidData, kErrorDeleteInvalid); - return DataSourceEventHandler::MessageStatus::kInvalidMessage; + return MessageStatus::kInvalidMessage; } - return DataSourceEventHandler::MessageStatus::kUnhandledVerb; + return MessageStatus::kUnhandledVerb; } } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp index b4e580630..48401ce4a 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/event_handler.hpp @@ -68,7 +68,7 @@ struct StreamingDataKinds { /** * This class handles LaunchDarkly events, parses them, and then uses - * a IDataSourceUpdateSink to process the parsed events. + * a IDestination 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 diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index efe93b7cc..2bbcd84c4 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -34,28 +34,26 @@ std::string const& StreamingDataSource::Identity() const { } StreamingDataSource::StreamingDataSource( - config::shared::built::ServiceEndpoints const& endpoints, - config::shared::built::StreamingConfig const& - data_source_config, - config::shared::built::HttpProperties http_properties, - boost::asio::any_io_executor ioc, - data_interfaces::IDestination& handler, + boost::asio::any_io_executor io, + Logger const& logger, data_components::DataSourceStatusManager& status_manager, - Logger const& logger) - : exec_(std::move(ioc)), + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig::StreamingConfig const& streaming, + config::built::HttpProperties const& http_properties) + : io_(std::move(io)), logger_(logger), status_manager_(status_manager), - data_source_handler_(handler, logger, status_manager_), - http_config_(std::move(http_properties)), - streaming_config_(data_source_config), + http_config_(http_properties), + streaming_config_(streaming), streaming_endpoint_(endpoints.StreamingBaseUrl()) {} -void StreamingDataSource::Init( - std::optional initial_data) { - // TODO: implement -} +void StreamingDataSource::StartAsync( + data_interfaces::IDestination* dest, + data_model::SDKDataSet const* bootstrap_data) { + boost::movelib::ignore(bootstrap_data); + + event_handler_.emplace(*dest, logger_, status_manager_); -void StreamingDataSource::StartAsync() { status_manager_.SetState(DataSourceStatus::DataSourceState::kInitializing); auto updated_url = network::AppendUrl(streaming_endpoint_, @@ -85,7 +83,7 @@ void StreamingDataSource::StartAsync() { boost::urls::url url = uri_components.value(); - auto client_builder = launchdarkly::sse::Builder(exec_, url.buffer()); + auto client_builder = launchdarkly::sse::Builder(io_, url.buffer()); client_builder.method(boost::beast::http::verb::get); @@ -101,16 +99,15 @@ void StreamingDataSource::StartAsync() { client_builder.initial_reconnect_delay( streaming_config_.initial_reconnect_delay); - for (auto const& header : http_config_.BaseHeaders()) { - client_builder.header(header.first, header.second); + for (auto const& [key, value] : http_config_.BaseHeaders()) { + client_builder.header(key, value); } 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()); + self->event_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 } @@ -153,7 +150,7 @@ void StreamingDataSource::ShutdownAsync(std::function completion) { return client_->async_shutdown(std::move(completion)); } if (completion) { - boost::asio::post(exec_, completion); + boost::asio::post(io_, completion); } } } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp index 1a01eebff..289fdaab8 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.hpp @@ -6,21 +6,12 @@ #include "../../../../data_interfaces/destination/idestination.hpp" #include "../../../../data_interfaces/source/ipush_source.hpp" -#include -#include -#include -#include -#include -#include #include +#include #include #include -#include - -using namespace std::chrono_literals; - namespace launchdarkly::server_side::data_systems { class StreamingDataSource final @@ -28,36 +19,31 @@ class StreamingDataSource final public std::enable_shared_from_this { public: StreamingDataSource( - ::launchdarkly::config::shared::built::ServiceEndpoints const& - endpoints, - ::launchdarkly::config::shared::built::StreamingConfig< - ::launchdarkly::config::shared::ServerSDK> const& - data_source_config, - launchdarkly::config::shared::built::HttpProperties http_properties, - boost::asio::any_io_executor ioc, - data_interfaces::IDestination& handler, + boost::asio::any_io_executor io, + Logger const& logger, data_components::DataSourceStatusManager& status_manager, - Logger const& logger); + config::built::ServiceEndpoints const& endpoints, + config::built::BackgroundSyncConfig::StreamingConfig const& streaming, + config::built::HttpProperties const& http_properties); - void Init(std::optional initial_data) override; - void StartAsync() override; + void StartAsync(data_interfaces::IDestination* dest, + data_model::SDKDataSet const* bootstrap_data) override; void ShutdownAsync(std::function completion) override; [[nodiscard]] std::string const& Identity() const override; private: - boost::asio::any_io_executor exec_; + boost::asio::any_io_executor io_; + Logger const& logger_; + data_components::DataSourceStatusManager& status_manager_; - DataSourceEventHandler data_source_handler_; - std::string streaming_endpoint_; + config::built::HttpProperties http_config_; - ::launchdarkly::config::shared::built::StreamingConfig< - ::launchdarkly::config::shared::ServerSDK> - streaming_config_; + std::optional event_handler_; + std::string streaming_endpoint_; - ::launchdarkly::config::shared::built::HttpProperties http_config_; + config::built::BackgroundSyncConfig::StreamingConfig streaming_config_; - Logger const& logger_; - std::shared_ptr client_; + std::shared_ptr client_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 75d0c2b64..0ab6ffe12 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -32,6 +32,8 @@ std::string const& LazyLoad::Identity() const { void LazyLoad::Initialize() {} +void LazyLoad::Shutdown() {} + std::shared_ptr LazyLoad::GetFlag( std::string const& key) const { auto const state = diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 2e0216966..87d21dcf2 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -52,6 +52,8 @@ class LazyLoad final : public data_interfaces::ISystem { void Initialize() override; + void Shutdown() override; + private: void RefreshAllFlags() const; void RefreshAllSegments() const; From abed9758e40c21a1f5ec02f2803864dc51b40d6b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 16:35:01 -0800 Subject: [PATCH 122/244] add back offline mode --- .../data_system/data_system_builder.hpp | 31 +++++++++++++++++-- .../server_side/config/config_builder.hpp | 8 +++++ .../data_system/data_system_builder.cpp | 8 +++-- libs/server-sdk/src/config/config.cpp | 6 ++-- libs/server-sdk/src/config/config_builder.cpp | 20 ++++++++++-- 5 files changed, 62 insertions(+), 11 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp index deb89c75a..795d79114 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/data_system_builder.hpp @@ -14,9 +14,36 @@ class DataSystemBuilder { using BackgroundSync = BackgroundSyncBuilder; using LazyLoad = LazyLoadBuilder; - DataSystemBuilder& Disabled(bool disabled); - + /** + * @brief Alias for Enabled(false). + * @return Reference to this. + */ + DataSystemBuilder& Disable(); + + /** + * @brief Specifies if the data system is enabled or disabled. + * If disabled, the configured method won't be used. Defaults to true. + * @param enabled If the data system is enabled. + * @return Reference to this. + */ + DataSystemBuilder& Enabled(bool enabled); + + /** + * @brief Configures the Background Sync data system. In this system, + * the SDK periodically receives updates from LaunchDarkly servers and + * stores them in an in-memory cache. This is the default data system. + * @param bg_sync Background Sync configuration. + * @return Reference to this. + */ DataSystemBuilder& Method(BackgroundSync bg_sync); + + /** + * @brief Configures the Lazy Load data system. In this system, the SDK + * pulls data on demand from a configured source, caching responses in + * memory for a configurable duration. + * @param lazy_load Lazy Load configuration. + * @return Reference to this. + */ DataSystemBuilder& Method(LazyLoad lazy_load); [[nodiscard]] tl::expected Build() const; diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 0ddd6e895..89e960fbf 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -61,6 +61,14 @@ class ConfigBuilder { */ config::builders::LoggingBuilder& Logging(); + /** + * @brief Equivalent to setting Events().Disable() and + * DataSystem().Disable(). The effect is that all evaluations will return + * application-provided default values, and no network calls will be made. + * @return Reference to this. + */ + ConfigBuilder& Offline(); + /** * Builds a Configuration, suitable for passing into an instance of Client. * @return diff --git a/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp b/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp index 5816ae2ea..fbf29b846 100644 --- a/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp +++ b/libs/server-sdk/src/config/builders/data_system/data_system_builder.cpp @@ -16,11 +16,15 @@ DataSystemBuilder& DataSystemBuilder::Method(LazyLoad lazy_load) { return *this; } -DataSystemBuilder& DataSystemBuilder::Disabled(bool const disabled) { - config_.disabled = disabled; +DataSystemBuilder& DataSystemBuilder::Enabled(bool const enabled) { + config_.disabled = !enabled; return *this; } +DataSystemBuilder& DataSystemBuilder::Disable() { + return Enabled(false); +} + tl::expected DataSystemBuilder::Build() const { if (method_builder_) { auto lazy_or_background_cfg = std::visit( diff --git a/libs/server-sdk/src/config/config.cpp b/libs/server-sdk/src/config/config.cpp index 0e2e2dba7..ff863d36b 100644 --- a/libs/server-sdk/src/config/config.cpp +++ b/libs/server-sdk/src/config/config.cpp @@ -9,8 +9,7 @@ Config::Config(std::string sdk_key, built::ServiceEndpoints service_endpoints, built::Events events, std::optional application_tag, - launchdarkly::server_side::config::built::DataSystemConfig - data_system_config, + config::built::DataSystemConfig data_system_config, built::HttpProperties http_properties) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), @@ -36,8 +35,7 @@ std::optional const& Config::ApplicationTag() const { return application_tag_; } -launchdarkly::server_side::config::built::DataSystemConfig const& -Config::DataSystemConfig() const { +config::built::DataSystemConfig const& Config::DataSystemConfig() const { return data_system_config_; } diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 5a425f03e..346cca216 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -3,7 +3,7 @@ namespace launchdarkly::server_side { ConfigBuilder::ConfigBuilder(std::string sdk_key) - : sdk_key_(std::move(sdk_key)) {} + : sdk_key_(std::move(sdk_key)), offline_(false) {} config::builders::EndpointsBuilder& ConfigBuilder::ServiceEndpoints() { return service_endpoints_builder_; @@ -29,6 +29,11 @@ config::builders::LoggingBuilder& ConfigBuilder::Logging() { return logging_config_builder_; } +ConfigBuilder& ConfigBuilder::Offline() { + offline_ = true; + return *this; +} + tl::expected ConfigBuilder::Build() const { auto sdk_key = sdk_key_; if (sdk_key.empty()) { @@ -39,14 +44,23 @@ tl::expected ConfigBuilder::Build() const { if (!endpoints_config) { return tl::make_unexpected(endpoints_config.error()); } - auto events_config = events_builder_.Build(); + + auto events_builder = events_builder_; + if (offline_) { + events_builder.Disable(); + } + auto events_config = events_builder.Build(); if (!events_config) { return tl::make_unexpected(events_config.error()); } std::optional app_tag = app_info_builder_.Build(); - auto data_system_config = data_system_builder_.Build(); + auto data_system_builder = data_system_builder_; + if (offline_) { + data_system_builder.Disable(); + } + auto data_system_config = data_system_builder.Build(); if (!data_system_config) { return tl::make_unexpected(data_system_config.error()); } From 58f563ebf6c18f8f845b8fd076b80314154e7ef4 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 20 Nov 2023 17:50:19 -0800 Subject: [PATCH 123/244] partially implement JsonDestination --- .../sources/iserialized_pull_source.hpp | 8 ++- .../integrations/redis/redis_source.hpp | 6 +- .../integrations/serialized_descriptors.hpp | 62 ---------------- .../serialized_item_descriptor.hpp | 11 +++ .../src/data_components/kinds/kinds.hpp | 6 +- .../json_destination.cpp | 49 ++++++++++++- .../json_destination.hpp | 12 +++- .../json_pull_source.cpp | 2 +- .../destination/iserialized_destination.hpp | 70 ++++++++++++++++--- .../lazy_load/lazy_load_system.hpp | 1 - .../lazy_load/sources/redis/redis_source.cpp | 6 +- .../tests/lazy_load_system_test.cpp | 12 ++-- 12 files changed, 148 insertions(+), 97 deletions(-) delete mode 100644 libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp diff --git a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_pull_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_pull_source.hpp index 7d7280fd4..1849e2d24 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_pull_source.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_pull_source.hpp @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include @@ -55,7 +56,7 @@ class ISerializedDataPullSource { * 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(integrations::IPersistentKind const& kind, + virtual GetResult Get(integrations::ISerializedItemKind const& kind, std::string const& itemKey) const = 0; /** @@ -67,7 +68,8 @@ class ISerializedDataPullSource { * @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(integrations::IPersistentKind const& kind) const = 0; + virtual AllResult All( + integrations::ISerializedItemKind const& kind) const = 0; virtual std::string const& Identity() const = 0; diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index c895fb976..7bb93515c 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -10,17 +10,17 @@ class RedisDataSource final : public data_interfaces::ISerializedDataPullSource { public: RedisDataSource(std::string uri, std::string prefix); - [[nodiscard]] GetResult Get(integrations::IPersistentKind const& kind, + [[nodiscard]] GetResult Get(integrations::ISerializedItemKind const& kind, std::string const& itemKey) const override; [[nodiscard]] AllResult All( - integrations::IPersistentKind const& kind) const override; + integrations::ISerializedItemKind const& kind) const override; [[nodiscard]] std::string const& Identity() const override; [[nodiscard]] bool Initialized() const override; private: std::string const prefix_; std::string const inited_key_; - std::string key_for_kind(integrations::IPersistentKind const& kind) const; + std::string key_for_kind(integrations::ISerializedItemKind const& kind) const; mutable sw::redis::Redis redis_; }; diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp deleted file mode 100644 index ded12cd05..000000000 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_descriptors.hpp +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include -#include - -namespace launchdarkly::server_side::integrations { - -/** - * A versioned item which can be stored in a persistent store. - */ -struct SerializedItemDescriptor { - std::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() const = 0; - - /** - * 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. - * - * 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 = 0; - - IPersistentKind(IPersistentKind const& item) = delete; - IPersistentKind(IPersistentKind&& item) = delete; - IPersistentKind& operator=(IPersistentKind const&) = delete; - IPersistentKind& operator=(IPersistentKind&&) = delete; - virtual ~IPersistentKind() = default; - - protected: - IPersistentKind() = default; -}; -} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp index 4f92e9904..2259ce946 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp @@ -24,6 +24,17 @@ struct SerializedItemDescriptor { * std::nullopt for deleted items. */ std::optional serializedItem; + + static SerializedItemDescriptor Present(std::uint64_t version, + std::string data) { + return SerializedItemDescriptor{version, false, std::move(data)}; + } + + static SerializedItemDescriptor Absent(std::uint64_t const version, + std::string tombstone_rep) { + return SerializedItemDescriptor{version, true, + std::move(tombstone_rep)}; + } }; } // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/data_components/kinds/kinds.hpp b/libs/server-sdk/src/data_components/kinds/kinds.hpp index 21f07212e..bb2a0d22e 100644 --- a/libs/server-sdk/src/data_components/kinds/kinds.hpp +++ b/libs/server-sdk/src/data_components/kinds/kinds.hpp @@ -1,10 +1,10 @@ #pragma once -#include +#include namespace launchdarkly::server_side::data_components { -class SegmentKind final : public integrations::IPersistentKind { +class SegmentKind final : public integrations::ISerializedItemKind { public: std::string const& Namespace() const override; std::uint64_t Version(std::string const& data) const override; @@ -15,7 +15,7 @@ class SegmentKind final : public integrations::IPersistentKind { static inline std::string const namespace_ = "segments"; }; -class FlagKind final : public integrations::IPersistentKind { +class FlagKind final : public integrations::ISerializedItemKind { public: std::string const& Namespace() const override; std::uint64_t Version(std::string const& data) const override; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 9cb5f7dd1..578fc4310 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -1,23 +1,66 @@ #include "json_destination.hpp" +#include +#include + namespace launchdarkly::server_side::data_components { +using integrations::SerializedItemDescriptor; + +FlagKind const JsonDestination::Kinds::Flag = FlagKind(); +SegmentKind const JsonDestination::Kinds::Segment = SegmentKind(); + JsonDestination::JsonDestination( data_interfaces::ISerializedDestination& destination) : dest_(destination) {} void JsonDestination::Init(data_model::SDKDataSet data_set) { - // TODO: serialize and forward to dest_.Init + // TODO: implement } void JsonDestination::Upsert(std::string const& key, data_model::FlagDescriptor flag) { - // TODO: serialize and forward to dest_.Upsert + SerializedItemDescriptor descriptor; + + if (!flag.item) { + boost::json::object tombstone; + tombstone.emplace("deleted", true); + tombstone.emplace("key", key); + tombstone.emplace("version", flag.version); + + descriptor = SerializedItemDescriptor::Absent( + flag.version, boost::json::serialize(tombstone)); + } else { + descriptor = SerializedItemDescriptor::Present( + flag.version, + boost::json::serialize(boost::json::value_from(*flag.item))); + } + + // TOOD: Log upsert errors? + + auto _ = dest_.Upsert(Kinds::Flag, key, std::move(descriptor)); } void JsonDestination::Upsert(std::string const& key, data_model::SegmentDescriptor segment) { - // TODO: serialize and forward to dest_.Upsert + SerializedItemDescriptor descriptor; + + if (!segment.item) { + boost::json::object tombstone; + tombstone.emplace("deleted", true); + tombstone.emplace("key", key); + tombstone.emplace("version", segment.version); + + descriptor = SerializedItemDescriptor::Absent( + segment.version, boost::json::serialize(tombstone)); + } else { + descriptor = SerializedItemDescriptor::Present( + segment.version, + boost::json::serialize(boost::json::value_from(*segment.item))); + } + // TOOD: Log upsert errors? + + auto _ = dest_.Upsert(Kinds::Segment, key, std::move(descriptor)); } std::string const& JsonDestination::Identity() const { diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 88f6c8cb9..76378cd57 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -1,11 +1,15 @@ +#pragma once + +#include "../../data_components/kinds/kinds.hpp" #include "../../data_interfaces/destination/idestination.hpp" #include "../../data_interfaces/destination/iserialized_destination.hpp" namespace launchdarkly::server_side::data_components { -class JsonDestination : public data_interfaces::IDestination { +class JsonDestination final : public data_interfaces::IDestination { public: - JsonDestination(data_interfaces::ISerializedDestination& destination); + explicit JsonDestination( + data_interfaces::ISerializedDestination& destination); virtual void Init(data_model::SDKDataSet data_set) override; @@ -19,6 +23,10 @@ class JsonDestination : public data_interfaces::IDestination { private: data_interfaces::ISerializedDestination& dest_; + struct Kinds { + static FlagKind const Flag; + static SegmentKind const Segment; + }; }; } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp index 8c056a47b..2a8a1e6cf 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include diff --git a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp index 37f936cda..af99c100a 100644 --- a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp @@ -9,6 +9,31 @@ namespace launchdarkly::server_side::data_interfaces { +/** + * @brief This interface is used for persisting data to databases, or any other + * component that can store feature flag / segment data. + * + * The SDK automatically converts between its in-memory data model and a + * serialized string form, which is what this interface interacts with. + * + * Each item in the store is conceptually a SerializedItemDescriptor containing + * a version and the serialized form. The serialized form might represent a + * flag/segment, or a "tombstone" representing the (absence) of an item. + * + * It's possible to satisfy the interface in two ways: + * + * 1. The Destination can store the version number, deleted state, and item + * separately. This is preferred because it avoids the need to deserialize the + * entire item just to inspect the version/deleted state when performing an + * Upsert operation. If implementing this strategy, the Destination may ignore + * deleted SerializeItemDescriptor's serializedItem members on Upserts. + * + * 2. If there's no way to store the version number, deleted state, and item + * separately in an efficient way, then the store may instead persist the + * serializedItem as-is during an Upsert. The item will contain a "tombstone" + * representation which the SDK will later use to determine if the item is + * deleted or not. + */ class ISerializedDestination { public: enum class InitResult { @@ -53,34 +78,59 @@ class ISerializedDestination { std::pair; /** - * \brief Initializes the destination with data. - * \param sdk_data_set A series of collections, where each collection is + * @brief Overwrites the Destination's contents with a set of items for each + * collection. All previous data should be disgraded regardless of + * versioning. + * + * The update should be done atomically. If that's not possible, 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 sdk_data_set A series of collections, where each collection is * named by an ISerializedItemKind and contains a list of key/value pairs * representing the key of the item and the serialized form of the item. - * \return InitResult::kSuccess if all + * @return InitResult::kSuccess if all * data items were stored, or InitResult::kError if any error occoured. */ [[nodiscard]] virtual InitResult Init( std::vector sdk_data_set) = 0; /** - * \brief Upserts a single item (update if exist, insert if not.) - * \param kind The item kind. - * \param key The item key. - * \param item Serialized form of the item. The item should be deleted if - * the SerializedItem's 'deleted' bool is true. \return + * @brief Upserts a single item (update if exist, insert if not.) + * + * If the given key already exists in the collection named by kind, + * then the Destination must check the version number corresponding to that + * key. Note that the item corresponding to that key may be a tombstone + * representing an absent item. + * + * If the version of the existing item is >= the version of the new item, + * return UpsertResult::kNotUpdated. If the Destination can't determine the + * version number of the existing item without full deserialization, then it + * may call integrations::ISerializedItemKind::Version on the data to obtain + * it. + * + * If the given item's deleted flag is true, the Destination must persist + * this fact. It can either store a tombstone (value of serializedItem), or + * if deletion state is stored separate from the item, it can use that + * mechanism. In any case, it should not delete/forget about the item. + * + * @param kind The item kind. + * @param key The item key. + * @param item Serialized form of the item. + * @return * UpsertResult::kSuccess if the operation was successful. * UpsertResult::kError if an error occured. Otherwise, * UpsertResult::kNotUpdated if the existing item version was greater than * the version passed in. */ [[nodiscard]] virtual UpsertResult Upsert( - std::string const& kind, + integrations::ISerializedItemKind const& kind, std::string const& key, integrations::SerializedItemDescriptor item) = 0; /** - * \return Identity of the destination. Used in logs. + * @return Identity of the destination. Used in logs. */ [[nodiscard]] virtual std::string const& Identity() const = 0; diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 28eadd5e8..7b4e17ce3 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -10,7 +10,6 @@ #include #include -#include namespace launchdarkly::server_side::data_systems { diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp index 64c55c156..8c3ac137b 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -3,7 +3,7 @@ namespace launchdarkly::server_side::data_systems { std::string RedisDataSource::key_for_kind( - integrations::IPersistentKind const& kind) const { + integrations::ISerializedItemKind const& kind) const { return prefix_ + ":" + kind.Namespace(); } @@ -13,7 +13,7 @@ RedisDataSource::RedisDataSource(std::string uri, std::string prefix) redis_(std::move(uri)) {} data_interfaces::ISerializedDataPullSource::GetResult RedisDataSource::Get( - integrations::IPersistentKind const& kind, + integrations::ISerializedItemKind const& kind, std::string const& itemKey) const { if (auto maybe_item = redis_.hget(key_for_kind(kind), itemKey)) { return integrations::SerializedItemDescriptor{0, false, @@ -23,7 +23,7 @@ data_interfaces::ISerializedDataPullSource::GetResult RedisDataSource::Get( } data_interfaces::ISerializedDataPullSource::AllResult RedisDataSource::All( - integrations::IPersistentKind const& kind) const { + integrations::ISerializedItemKind const& kind) const { std::unordered_map raw_items; AllResult::value_type items; redis_.hgetall(key_for_kind(kind), diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index de3608d04..f81a2d02a 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -13,19 +13,19 @@ class LazyLoadTest : public ::testing::Test {}; class FakeDataSource : public launchdarkly::server_side::data_interfaces:: ISerializedDataPullSource { public: - using GetFn = std::function; using AllFn = - std::function; + std::function; FakeDataSource(std::string name, GetFn items, AllFn all) : name(std::move(name)), - item_getter([](integrations::IPersistentKind const& kind, + item_getter([](integrations::ISerializedItemKind const& kind, std::string const& key) { return tl::make_unexpected(Error{key + "not found"}); }), - all_getter([](integrations::IPersistentKind const& kind) { + all_getter([](integrations::ISerializedItemKind const& kind) { return tl::make_unexpected(Error{kind.Namespace() + "not found"}); }), items_requested() { @@ -42,12 +42,12 @@ class FakeDataSource : public launchdarkly::server_side::data_interfaces:: FakeDataSource() : FakeDataSource("fake source") {} - GetResult Get(integrations::IPersistentKind const& kind, + GetResult Get(integrations::ISerializedItemKind const& kind, std::string const& itemKey) const override { items_requested[kind.Namespace()].push_back(itemKey); return item_getter(kind, itemKey); } - AllResult All(integrations::IPersistentKind const& kind) const override { + AllResult All(integrations::ISerializedItemKind const& kind) const override { all_requested.push_back(kind.Namespace()); return all_getter(kind); } From a233eff6632c0ec30c61205e4e448b38d5f4a574 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 08:29:03 -0800 Subject: [PATCH 124/244] add back in missing streaming/polling C binding builders --- .../common/src/config/data_source_builder.cpp | 25 ----- .../server_side/bindings/c/config/builder.h | 104 ++++++++++-------- .../server_side/config/config_builder.hpp | 4 +- libs/server-sdk/src/bindings/c/builder.cpp | 81 ++++++++------ libs/server-sdk/src/config/config_builder.cpp | 4 +- 5 files changed, 108 insertions(+), 110 deletions(-) diff --git a/libs/common/src/config/data_source_builder.cpp b/libs/common/src/config/data_source_builder.cpp index 91a32b81c..e19a9417e 100644 --- a/libs/common/src/config/data_source_builder.cpp +++ b/libs/common/src/config/data_source_builder.cpp @@ -20,31 +20,6 @@ struct MethodVisitor { } }; -template <> -struct MethodVisitor { - using SDK = ServerSDK; - using Result = tl::expected, - built::PollingConfig, - built::RedisConfig>, - Error>; - - Result operator()(StreamingBuilder const& streaming) const { - return streaming.Build(); - } - - Result operator()(PollingBuilder const& polling) const { - return polling.Build(); - } - - Result operator()(RedisBuilder const& redis_pull) const { - return redis_pull.Build().map([](auto&& config) { - return std::variant, - built::PollingConfig, built::RedisConfig>{ - std::move(config)}; - }); - } -}; - template StreamingBuilder::StreamingBuilder() : config_(Defaults::StreamingConfig()) {} 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 index 7c601e929..982b0c518 100644 --- 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 @@ -18,9 +18,8 @@ extern "C" { // only need to export C interface if #endif typedef struct _LDServerConfigBuilder* LDServerConfigBuilder; -typedef struct _LDServerDataSourceStreamBuilder* - LDServerDataSourceStreamBuilder; -typedef struct _LDServerDataSourcePollBuilder* LDServerDataSourcePollBuilder; +typedef struct _LDServerStreamingSyncBuilder* LDServerStreamingSyncBuilder; +typedef struct _LDServerPollingSyncBuilder* LDServerPollingSyncBuilder; /** * Constructs a client-side config builder. @@ -29,7 +28,7 @@ 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 b Config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -37,7 +36,7 @@ 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 b Config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -45,7 +44,7 @@ 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 b Config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -54,7 +53,7 @@ LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(LDServerConfigBuilder b, /** * 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 b Config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -64,7 +63,7 @@ LDServerConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( /** * Sets an identifier for the application. - * @param b Client config builder. Must not be NULL. + * @param b Config builder. Must not be NULL. * @param app_id Non-empty string. Must be <= 64 chars. Must be alphanumeric, * '-', '.', or '_'. Must not be NULL. */ @@ -74,7 +73,7 @@ LDServerConfigBuilder_AppInfo_Identifier(LDServerConfigBuilder b, /** * Sets a version for the application. - * @param b Client config builder. Must not be NULL. + * @param b Config builder. Must not be NULL. * @param app_version Non-empty string. Must be <= 64 chars. Must be * alphanumeric, * '-', '.', or '_'. Must not be NULL. @@ -83,10 +82,19 @@ 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 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 b Config builder. Must not be NULL. * @param enabled True to enable event-sending. */ LD_EXPORT(void) @@ -96,7 +104,7 @@ 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 b Config builder. Must not be NULL. * @param capacity Event queue capacity. */ LD_EXPORT(void) @@ -106,7 +114,7 @@ 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 b Config builder. Must not be NULL. * @param milliseconds Interval between automatic flushes. */ LD_EXPORT(void) @@ -132,7 +140,7 @@ LDServerConfigBuilder_Events_FlushIntervalMs(LDServerConfigBuilder b, * 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 b Config builder. Must not be NULL. * @param all_attributes_private True for behavior of (1), false for default * behavior of (2) or (3). */ @@ -143,7 +151,7 @@ LDServerConfigBuilder_Events_AllAttributesPrivate(LDServerConfigBuilder b, /** * Specifies a single private attribute. May be called multiple times * with additional private attributes. - * @param b Client config builder. Must not be NULL. + * @param b Config builder. Must not be NULL. * @param attribute_reference Attribute to mark private. */ LD_EXPORT(void) @@ -151,47 +159,51 @@ LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, char const* attribute_reference); /** - * Set the streaming configuration for the builder. + * Configures the Background Sync data system with a Streaming synchronizer. + * + * This is the default data system for the SDK. * - * 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. + * In this mode, the SDK maintains a persistent, streaming data connection + * with LaunchDarkly. Flag data is received automatically in the background, + * meaning there are no network costs associated with evaluating flags. * - * @param b Client config builder. Must not be NULL. + * @param b 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_DataSystem_BackgroundSync_Streaming( LDServerConfigBuilder b, - LDServerDataSourceStreamBuilder stream_builder); + LDServerStreamingSyncBuilder stream_builder); /** - * Set the polling configuration for the builder. + * Configures the Background Sync data system with a Polling synchronizer. + * + * This synchronizer may be chosen to override the default Streaming mode. * - * 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. + * In this mode, the SDK makes periodic network requests to LaunchDarkly. + * Between requests, flag data may be stale to some degree. This mode may be + * advantageous if a streaming connection cannot be maintained. * - * @param b Client config builder. Must not be NULL. + * @param b 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_DataSystem_BackgroundSync_Polling( LDServerConfigBuilder b, - LDServerDataSourcePollBuilder poll_builder); + LDServerPollingSyncBuilder 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. + * builder, must be manually freed with LDServerStreamingSyncBuilder_Free. * * @return New builder for Streaming method. */ -LD_EXPORT(LDServerDataSourceStreamBuilder) -LDServerDataSourceStreamBuilder_New(); +LD_EXPORT(LDServerStreamingSyncBuilder) +LDServerStreamingSyncBuilder_New(); /** * Sets the initial reconnect delay for the streaming connection. @@ -205,8 +217,8 @@ LDServerDataSourceStreamBuilder_New(); * @param milliseconds Initial delay for a reconnection attempt. */ LD_EXPORT(void) -LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( - LDServerDataSourceStreamBuilder b, +LDServerStreamingSyncBuilder_InitialReconnectDelayMs( + LDServerStreamingSyncBuilder b, unsigned int milliseconds); /** @@ -216,19 +228,19 @@ LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( * @param b Builder to free. */ LD_EXPORT(void) -LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b); +LDServerStreamingSyncBuilder_Free(LDServerStreamingSyncBuilder b); /** * Creates a new DataSource builder for the Polling method. * * If not passed into the config - * builder, must be manually freed with LDServerDataSourcePollBuilder_Free. + * builder, must be manually freed with LDServerPollingSyncBuilder_Free. * * @return New builder for Polling method. */ -LD_EXPORT(LDServerDataSourcePollBuilder) -LDServerDataSourcePollBuilder_New(); +LD_EXPORT(LDServerPollingSyncBuilder) +LDServerPollingSyncBuilder_New(); /** * Sets the interval at which the SDK will poll for feature flag updates. @@ -236,8 +248,8 @@ LDServerDataSourcePollBuilder_New(); * @param milliseconds Polling interval. */ LD_EXPORT(void) -LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, - unsigned int seconds); +LDServerPollingSyncBuilder_IntervalS(LDServerPollingSyncBuilder b, + unsigned int seconds); /** * Frees a Polling method builder. Do not call if the builder was consumed by @@ -246,13 +258,13 @@ LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, * @param b Builder to free. */ LD_EXPORT(void) -LDServerDataSourcePollBuilder_Free(LDServerDataSourcePollBuilder b); +LDServerPollingSyncBuilder_Free(LDServerPollingSyncBuilder 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 b Config builder. Must not be NULL. * @param wrapper_name Name of the wrapper. */ LD_EXPORT(void) @@ -263,7 +275,7 @@ LDServerConfigBuilder_HttpProperties_WrapperName(LDServerConfigBuilder b, * 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 b Config builder. Must not be NULL. * @param wrapper_version Version of the wrapper. */ LD_EXPORT(void) @@ -275,7 +287,7 @@ LDServerConfigBuilder_HttpProperties_WrapperVersion( * Set a custom header value. May be called more than once with additional * headers. * - * @param b Client config builder. Must not be NULL. + * @param b 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. */ @@ -286,14 +298,14 @@ LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, /** * Disables the default SDK logging. - * @param b Client config builder. Must not be NULL. + * @param b 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 b Config builder. Must not be NULL. * @param basic_builder The basic logging builder. Must not be NULL. */ LD_EXPORT(void) @@ -302,7 +314,7 @@ LDServerConfigBuilder_Logging_Basic(LDServerConfigBuilder b, /** * Configures the SDK with custom logging. - * @param b Client config builder. Must not be NULL. + * @param b Config builder. Must not be NULL. * @param custom_builder The custom logging builder. Must not be NULL. */ LD_EXPORT(void) diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 89e960fbf..1c46d3f1e 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -62,12 +62,12 @@ class ConfigBuilder { config::builders::LoggingBuilder& Logging(); /** - * @brief Equivalent to setting Events().Disable() and + * @brief If true, equivalent to setting Events().Disable() and * DataSystem().Disable(). The effect is that all evaluations will return * application-provided default values, and no network calls will be made. * @return Reference to this. */ - ConfigBuilder& Offline(); + ConfigBuilder& Offline(bool offline); /** * Builds a Configuration, suitable for passing into an instance of Client. diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index 313cc6d63..a85ed2bf8 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -6,6 +6,8 @@ #include #include +#include "../../data_systems/lazy_load/lazy_load_system.hpp" + using namespace launchdarkly::server_side; using namespace launchdarkly::server_side::config::builders; @@ -16,13 +18,13 @@ using namespace launchdarkly::server_side::config::builders; (reinterpret_cast(ptr)) #define FROM_STREAM_BUILDER(ptr) \ - (reinterpret_cast(ptr)) + (reinterpret_cast(ptr)) #define TO_POLL_BUILDER(ptr) \ (reinterpret_cast(ptr)) #define FROM_POLL_BUILDER(ptr) \ - (reinterpret_cast(ptr)) + (reinterpret_cast(ptr)) #define TO_BASIC_LOGGING_BUILDER(ptr) \ (reinterpret_cast(ptr)) @@ -118,6 +120,13 @@ LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, TO_BUILDER(b)->AppInfo().Version(app_version); } +LD_EXPORT(void) +LDServerConfigBuilder_Offline(LDServerConfigBuilder b, bool const 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); @@ -158,39 +167,41 @@ LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder 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() { +LD_EXPORT(void) +LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming( + LDServerConfigBuilder b, + LDServerStreamingSyncBuilder stream_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(stream_builder); + + BackgroundSyncBuilder::Streaming* sb = TO_STREAM_BUILDER(stream_builder); + TO_BUILDER(b)->DataSystem().Method( + BackgroundSyncBuilder().Synchronizer(*sb)); + LDServerStreamingSyncBuilder_Free(stream_builder); +} + +LD_EXPORT(void) +LDServerConfigBuilder_DataSystem_BackgroudSync_Polling( + LDServerConfigBuilder b, + LDServerPollingSyncBuilder poll_builder) { + LD_ASSERT_NOT_NULL(b); + LD_ASSERT_NOT_NULL(poll_builder); + + BackgroundSyncBuilder::Polling* pb = TO_POLL_BUILDER(poll_builder); + TO_BUILDER(b)->DataSystem().Method( + BackgroundSyncBuilder().Synchronizer(*pb)); + LDServerPollingSyncBuilder_Free(poll_builder); +} + +LD_EXPORT(LDServerStreamingSyncBuilder) +LDServerStreamingSyncBuilder_New() { return FROM_STREAM_BUILDER( new DataSystemBuilder::BackgroundSync::Streaming()); } LD_EXPORT(void) -LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( - LDServerDataSourceStreamBuilder b, +LDServerStreamingSyncBuilder_InitialReconnectDelayMs( + LDServerStreamingSyncBuilder b, unsigned int milliseconds) { LD_ASSERT_NOT_NULL(b); @@ -199,24 +210,24 @@ LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( } LD_EXPORT(void) -LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b) { +LDServerStreamingSyncBuilder_Free(LDServerStreamingSyncBuilder b) { delete TO_STREAM_BUILDER(b); } -LD_EXPORT(LDServerDataSourcePollBuilder) LDServerDataSourcePollBuilder_New() { +LD_EXPORT(LDServerPollingSyncBuilder) LDServerPollingSyncBuilder_New() { return FROM_POLL_BUILDER(new DataSystemBuilder::BackgroundSync::Polling()); } LD_EXPORT(void) -LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, - unsigned int seconds) { +LDServerPollingSyncBuilder_IntervalS(LDServerPollingSyncBuilder 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) { +LDServerPollingSyncBuilder_Free(LDServerPollingSyncBuilder b) { delete TO_POLL_BUILDER(b); } diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 346cca216..647c9429d 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -29,8 +29,8 @@ config::builders::LoggingBuilder& ConfigBuilder::Logging() { return logging_config_builder_; } -ConfigBuilder& ConfigBuilder::Offline() { - offline_ = true; +ConfigBuilder& ConfigBuilder::Offline(bool const offline) { + offline_ = offline; return *this; } From 90306281e5312ece7f0630cf9ec961f08b78e329 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 08:37:28 -0800 Subject: [PATCH 125/244] fix offline default config --- .../launchdarkly/server_side/config/config_builder.hpp | 2 +- libs/server-sdk/src/client_impl.cpp | 1 + .../config/builders/data_system/background_sync_builder.cpp | 4 +++- .../server-sdk/src/config/builders/data_system/defaults.hpp | 5 +++-- .../src/data_systems/lazy_load/lazy_load_system.cpp | 6 ------ 5 files changed, 8 insertions(+), 10 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 1c46d3f1e..8bb36e7d1 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -77,7 +77,7 @@ class ConfigBuilder { private: std::string sdk_key_; - std::optional offline_; + bool offline_; config::builders::EndpointsBuilder service_endpoints_builder_; config::builders::AppInfoBuilder app_info_builder_; diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index c870aca37..8b31421c0 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -440,6 +440,7 @@ IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { // } ClientImpl::~ClientImpl() { + data_system_->Shutdown(); ioc_.stop(); // TODO(SC-219101) run_thread_.join(); diff --git a/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp b/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp index c7411cf1b..7e573455b 100644 --- a/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp +++ b/libs/server-sdk/src/config/builders/data_system/background_sync_builder.cpp @@ -1,9 +1,11 @@ #include +#include "defaults.hpp" + namespace launchdarkly::server_side::config::builders { BackgroundSyncBuilder::BackgroundSyncBuilder() - : bootstrap_builder_(), config_() {} + : bootstrap_builder_(), config_(Defaults::BackgroundSyncConfig()) {} BootstrapBuilder& BackgroundSyncBuilder::Bootstrapper() { return bootstrap_builder_; diff --git a/libs/server-sdk/src/config/builders/data_system/defaults.hpp b/libs/server-sdk/src/config/builders/data_system/defaults.hpp index a5e182326..c8a6c5112 100644 --- a/libs/server-sdk/src/config/builders/data_system/defaults.hpp +++ b/libs/server-sdk/src/config/builders/data_system/defaults.hpp @@ -19,13 +19,14 @@ struct Defaults { return std::nullopt; } - static auto DataSourceConfig() + static auto SynchronizerConfig() -> built::BackgroundSyncConfig::StreamingConfig { return {std::chrono::seconds(1), "/all"}; } static auto BackgroundSyncConfig() -> built::BackgroundSyncConfig { - return {BootstrapConfig(), DataSourceConfig(), DataDestinationConfig()}; + return {BootstrapConfig(), SynchronizerConfig(), + DataDestinationConfig()}; } static auto LazyLoadConfig() -> built::LazyLoadConfig { diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 0ab6ffe12..3c6c09b6c 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -7,12 +7,6 @@ namespace launchdarkly::server_side::data_systems { -/* -*DataSourceConfig const& data_source_config, -HttpProperties http_properties, -boost::asio::any_io_executor ioc, -data_components::DataSourceStatusManager& status_manager, -*/ LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg) : LazyLoad(std::move(cfg), []() { return std::chrono::steady_clock::now(); }) {} From 44d87ab1b70132b8938b8d11be13041b835f5d28 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 08:49:54 -0800 Subject: [PATCH 126/244] add Enabled c binding for data system builder --- .../config/data_system/data_system_builder.h | 0 .../server_side/bindings/c/config/builder.h | 22 ++++++- .../server_side/config/config_builder.hpp | 4 ++ libs/server-sdk/src/bindings/c/builder.cpp | 9 ++- libs/server-sdk/tests/config_builder_test.cpp | 57 ++----------------- .../tests/server_c_bindings_test.cpp | 9 +-- 6 files changed, 41 insertions(+), 60 deletions(-) delete mode 100644 libs/common/include/launchdarkly/bindings/c/config/data_system/data_system_builder.h diff --git a/libs/common/include/launchdarkly/bindings/c/config/data_system/data_system_builder.h b/libs/common/include/launchdarkly/bindings/c/config/data_system/data_system_builder.h deleted file mode 100644 index e69de29bb..000000000 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 index 982b0c518..ba8451306 100644 --- 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 @@ -83,8 +83,14 @@ LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, char const* app_version); /** - * Enables or disables "Offline" mode. True means - * Offline mode is enabled. + * If true, equivalent to setting LDServerConfigBuilder_Events_Enabled(false) + * and LDServerConfigBuilder_DataSystem_Enabled(false). + * + * The effect is that all evaluations will return + * application-provided default values, and no network calls will be made. + * + * This overrides specific configuration of events and/or data system, if + * present. * @param b Config builder. Must not be NULL. * @param offline True if offline. */ @@ -194,6 +200,18 @@ LDServerConfigBuilder_DataSystem_BackgroundSync_Polling( LDServerConfigBuilder b, LDServerPollingSyncBuilder poll_builder); +/** + * Specify if the SDK's data system should be enabled or not. + * + * By default, the SDK uses the Background Sync data system with a Streaming + * connection. + * + * @param b Config builder. Must not be NULL. + * @param enabled True to enable the data system, false to disable it. + */ +LD_EXPORT(void) +LDServerConfigBuilder_DataSystem_Enabled(LDServerConfigBuilder b, bool enabled); + /** * Creates a new DataSource builder for the Streaming method. * diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 8bb36e7d1..563578d42 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -65,6 +65,10 @@ class ConfigBuilder { * @brief If true, equivalent to setting Events().Disable() and * DataSystem().Disable(). The effect is that all evaluations will return * application-provided default values, and no network calls will be made. + * + * This overrides specific configuration of events and/or data system, if + * present. + * * @return Reference to this. */ ConfigBuilder& Offline(bool offline); diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index a85ed2bf8..012ceee40 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -181,7 +181,7 @@ LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming( } LD_EXPORT(void) -LDServerConfigBuilder_DataSystem_BackgroudSync_Polling( +LDServerConfigBuilder_DataSystem_BackgroundSync_Polling( LDServerConfigBuilder b, LDServerPollingSyncBuilder poll_builder) { LD_ASSERT_NOT_NULL(b); @@ -193,6 +193,13 @@ LDServerConfigBuilder_DataSystem_BackgroudSync_Polling( LDServerPollingSyncBuilder_Free(poll_builder); } +LD_EXPORT(void) +LDServerConfigBuilder_DataSystem_Enabled(LDServerConfigBuilder b, + bool const enabled) { + LD_ASSERT_NOT_NULL(b); + TO_BUILDER(b)->DataSystem().Enabled(enabled); +} + LD_EXPORT(LDServerStreamingSyncBuilder) LDServerStreamingSyncBuilder_New() { return FROM_STREAM_BUILDER( diff --git a/libs/server-sdk/tests/config_builder_test.cpp b/libs/server-sdk/tests/config_builder_test.cpp index ec33dedbe..5dfdc5748 100644 --- a/libs/server-sdk/tests/config_builder_test.cpp +++ b/libs/server-sdk/tests/config_builder_test.cpp @@ -85,61 +85,16 @@ TEST_F(ConfigBuilderTest, DefaultConstruction_EventDefaultsAreUsed) { TEST_F(ConfigBuilderTest, CanDisableDataSystem) { ConfigBuilder builder("sdk-123"); - // First establish that the data system is enabled. + // First establish that the data system is enabled, + // to detect if defaults are misconfigured. auto const cfg1 = builder.Build(); EXPECT_FALSE(cfg1->DataSystemConfig().disabled); - builder.DataSystem().Disabled(true); + builder.DataSystem().Disable(); auto const cfg2 = builder.Build(); EXPECT_TRUE(cfg2->DataSystemConfig().disabled); -} - -TEST_F(ConfigBuilderTest, CanConstructValidRedisConfig) { - ConfigBuilder builder("sdk-123"); - - using LazyLoad = builders::DataSystemBuilder::LazyLoad; - - auto redis = std::make_shared( - "tcp://foo.bar:1234", "test"); - builder.DataSystem().Method( - LazyLoad() - .Source(redis) - .CacheEviction(LazyLoad::EvictionPolicy::Disabled) - .CacheRefresh(std::chrono::seconds(5))); - - auto cfg = builder.Build(); - ASSERT_TRUE(cfg); + builder.DataSystem().Enabled(true); + auto const cfg3 = builder.Build(); + EXPECT_FALSE(cfg3->DataSystemConfig().disabled); } - -// TEST_F(ConfigBuilderTest, InvalidRedisConfigurationDetected) { -// ConfigBuilder builder("sdk-123"); -// -// using LazyLoad = DataSystemBuilder::LazyLoad; -// -// auto redis = std::make_shared( -// "tcp://foo.bar:1234", "test"); -// -// // An empty URI string should be rejected before it is passed deeper -// // into the redis client. -// builder.DataSystem().Method( -// LazyLoad().Source(redis); -// -// auto cfg = builder.Build(); -// ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingURI); -// -// // If using ConnOpts instead of a URI string, the host should be rejected -// // for same reason as above. -// builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( -// LazyLoad::Redis::ConnOpts{"", 1233, "password", 2}))); -// -// cfg = builder.Build(); -// ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingHost); -// -// // If the port isn't set, it'll be default-constructed as std::nullopt. -// builder.DataSystem().Method(LazyLoad().Source(LazyLoad::Redis().Connection( -// LazyLoad::Redis::ConnOpts{"tcp://localhost"}))); -// -// cfg = builder.Build(); -// ASSERT_EQ(cfg.error(), Error::kConfig_DataSource_Redis_MissingPort); -// } diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index b25440199..66f980913 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -37,9 +37,7 @@ void StatusListenerFunction(LDServerDataSourceStatus status, void* user_data) { // will at least ensure 1.) Compilation, and 2.) Allow sanitizers to run. TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); - - // TODO: Disable the default datasource for the test, otherwise this will - // try to make a net conneciton. + LDServerConfigBuilder_Offline(cfg_builder, true); LDServerConfig config; LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); @@ -47,7 +45,7 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { LDServerSDK sdk = LDServerSDK_New(config); - struct LDServerDataSourceStatusListener listener {}; + LDServerDataSourceStatusListener listener{}; LDServerDataSourceStatusListener_Init(&listener); listener.UserData = const_cast("Potato"); @@ -68,8 +66,7 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { TEST(ClientBindings, GetStatusOfOfflineClient) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); - // TODO: Disable the default datasource for the test, otherwise this will - // try to make a net conneciton. + LDServerConfigBuilder_Offline(cfg_builder, true); LDServerConfig config; LDStatus status = LDServerConfigBuilder_Build(cfg_builder, &config); From 1a8162f0b80681b3d79ed0a8d8645a75c3754c5c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 09:39:52 -0800 Subject: [PATCH 127/244] fix contract test compilation; unit tests work --- .../src/entity_manager.cpp | 19 ++-- .../common/tests/data_source_builder_test.cpp | 1 - libs/common/tests/service_endpoints_test.cpp | 10 --- .../tests/service_endpoint_builders_test.cpp | 88 +++++++++++++++++++ 4 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 libs/server-sdk/tests/service_endpoint_builders_test.cpp diff --git a/contract-tests/server-contract-tests/src/entity_manager.cpp b/contract-tests/server-contract-tests/src/entity_manager.cpp index 2ec56bd01..cf9e7de93 100644 --- a/contract-tests/server-contract-tests/src/entity_manager.cpp +++ b/contract-tests/server-contract-tests/src/entity_manager.cpp @@ -17,7 +17,16 @@ std::optional EntityManager::create(ConfigParams const& in) { auto config_builder = ConfigBuilder(in.credential); - auto default_endpoints = Defaults::ServiceEndpoints(); + // The contract test service sets endpoints in a way that is disallowed + // for users. Specifically, it may set just 1 of the 3 endpoints, whereas + // we require all 3 to be set. + // + // To avoid that error being detected, we must configure the Endpoints + // builder with the 3 default URLs, which we can fetch by just calling Build + // on a new builder. That way when the contract tests set just 1 URL, + // the others have already been "set" so no error occurs. + auto const default_endpoints = + *config::builders::EndpointsBuilder().Build(); auto& endpoints = config_builder.ServiceEndpoints() @@ -36,16 +45,14 @@ std::optional EntityManager::create(ConfigParams const& in) { endpoints.EventsBaseUrl(*in.serviceEndpoints->events); } } - - using BackgroundSync = DataSystemBuilder::BackgroundSync; - auto datasystem = BackgroundSync(); + auto datasystem = config::builders::DataSystemBuilder::BackgroundSync(); if (in.streaming) { if (in.streaming->baseUri) { endpoints.StreamingBaseUrl(*in.streaming->baseUri); } if (in.streaming->initialRetryDelayMs) { - auto streaming = BackgroundSync::Streaming(); + auto streaming = decltype(datasystem)::Streaming(); streaming.InitialReconnectDelay( std::chrono::milliseconds(*in.streaming->initialRetryDelayMs)); datasystem.Synchronizer(std::move(streaming)); @@ -57,7 +64,7 @@ std::optional EntityManager::create(ConfigParams const& in) { endpoints.PollingBaseUrl(*in.polling->baseUri); } if (!in.streaming) { - auto method = BackgroundSync::Polling(); + auto method = decltype(datasystem)::Polling(); if (in.polling->pollIntervalMs) { method.PollInterval( std::chrono::duration_cast( diff --git a/libs/common/tests/data_source_builder_test.cpp b/libs/common/tests/data_source_builder_test.cpp index 729bd949d..8f16edc94 100644 --- a/libs/common/tests/data_source_builder_test.cpp +++ b/libs/common/tests/data_source_builder_test.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include diff --git a/libs/common/tests/service_endpoints_test.cpp b/libs/common/tests/service_endpoints_test.cpp index bd593352e..d9a729a9a 100644 --- a/libs/common/tests/service_endpoints_test.cpp +++ b/libs/common/tests/service_endpoints_test.cpp @@ -1,7 +1,6 @@ #include #include -#include #include class ServiceEndpointTest : public testing::Test {}; @@ -18,15 +17,6 @@ TEST(ServiceEndpointTest, DefaultClientBuilderURLs) { ASSERT_EQ(eps->EventsBaseUrl(), "https://mobile.launchdarkly.com"); } -TEST(ServiceEndpointTest, DefaultServerBuilderURLs) { - server_side::EndpointsBuilder builder; - auto eps = builder.Build(); - ASSERT_TRUE(eps); - ASSERT_EQ(eps->PollingBaseUrl(), "https://sdk.launchdarkly.com"); - ASSERT_EQ(eps->StreamingBaseUrl(), "https://stream.launchdarkly.com"); - ASSERT_EQ(eps->EventsBaseUrl(), "https://events.launchdarkly.com"); -} - TEST(ServiceEndpointTest, ModifySingleURLCausesError) { auto result = client_side::EndpointsBuilder().PollingBaseUrl("foo").Build(); ASSERT_FALSE(result); diff --git a/libs/server-sdk/tests/service_endpoint_builders_test.cpp b/libs/server-sdk/tests/service_endpoint_builders_test.cpp new file mode 100644 index 000000000..0e4ec3bf1 --- /dev/null +++ b/libs/server-sdk/tests/service_endpoint_builders_test.cpp @@ -0,0 +1,88 @@ +#include + +#include +#include + +using namespace launchdarkly::server_side::config::builders; +using Error = launchdarkly::Error; + +TEST(ServiceEndpointTest, DefaultServerBuilderURLs) { + EndpointsBuilder builder; + auto eps = builder.Build(); + ASSERT_TRUE(eps); + ASSERT_EQ(eps->PollingBaseUrl(), "https://sdk.launchdarkly.com"); + ASSERT_EQ(eps->StreamingBaseUrl(), "https://stream.launchdarkly.com"); + ASSERT_EQ(eps->EventsBaseUrl(), "https://events.launchdarkly.com"); +} + +TEST(ServiceEndpointTest, ModifySingleURLCausesError) { + auto result = EndpointsBuilder().PollingBaseUrl("foo").Build(); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), Error::kConfig_Endpoints_AllURLsMustBeSet); + + result = EndpointsBuilder().StreamingBaseUrl("foo").Build(); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), Error::kConfig_Endpoints_AllURLsMustBeSet); + + result = EndpointsBuilder().EventsBaseUrl("foo").Build(); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), Error::kConfig_Endpoints_AllURLsMustBeSet); +} + +TEST(ServiceEndpointsTest, RelaySetsAllURLS) { + auto eps = EndpointsBuilder().RelayProxyBaseURL("foo").Build(); + ASSERT_TRUE(eps); + ASSERT_EQ(eps->StreamingBaseUrl(), "foo"); + ASSERT_EQ(eps->PollingBaseUrl(), "foo"); + ASSERT_EQ(eps->EventsBaseUrl(), "foo"); +} + +TEST(ServiceEndpointsTest, TrimsTrailingSlashes) { + { + auto eps = EndpointsBuilder().RelayProxyBaseURL("foo/").Build(); + ASSERT_TRUE(eps); + ASSERT_EQ(eps->StreamingBaseUrl(), "foo"); + } + + { + auto eps = EndpointsBuilder().RelayProxyBaseURL("foo////////").Build(); + ASSERT_TRUE(eps); + ASSERT_EQ(eps->StreamingBaseUrl(), "foo"); + } + + { + auto eps = EndpointsBuilder().RelayProxyBaseURL("/").Build(); + ASSERT_TRUE(eps); + ASSERT_EQ(eps->StreamingBaseUrl(), ""); + } +} + +TEST(ServiceEndpointsTest, EmptyURLsAreInvalid) { + auto result = EndpointsBuilder().RelayProxyBaseURL("").Build(); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), Error::kConfig_Endpoints_EmptyURL); + + result = EndpointsBuilder() + .StreamingBaseUrl("") + .EventsBaseUrl("foo") + .PollingBaseUrl("bar") + .Build(); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), Error::kConfig_Endpoints_EmptyURL); + + result = EndpointsBuilder() + .StreamingBaseUrl("foo") + .EventsBaseUrl("") + .PollingBaseUrl("bar") + .Build(); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), Error::kConfig_Endpoints_EmptyURL); + + result = EndpointsBuilder() + .StreamingBaseUrl("foo") + .EventsBaseUrl("bar") + .PollingBaseUrl("") + .Build(); + ASSERT_FALSE(result); + ASSERT_EQ(result.error(), Error::kConfig_Endpoints_EmptyURL); +} From cfa10bdf68ad33918dc9951d2972712f497b3dc8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 12:10:20 -0800 Subject: [PATCH 128/244] integrate merged changes --- .../shared/builders/data_source_builder.hpp | 37 ------------------- .../shared/built/data_source_config.hpp | 25 ------------- libs/common/include/launchdarkly/error.hpp | 1 - .../integrations/redis/redis_source.hpp | 8 ++-- .../lazy_load/sources/redis/redis_source.cpp | 4 +- .../tests/lazy_load_system_test.cpp | 11 +++--- 6 files changed, 12 insertions(+), 74 deletions(-) diff --git a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp index f4125287e..e3275e0be 100644 --- a/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp +++ b/libs/common/include/launchdarkly/config/shared/builders/data_source_builder.hpp @@ -78,43 +78,6 @@ class PollingBuilder { built::PollingConfig config_; }; -/** - * \brief Represents a Redis data source capable of pulling data on-demand. - */ -class RedisBuilder { - public: - /** - * \brief Inividual connection options, including host, port, password, and - * db. - */ - using ConnOpts = built::RedisConfig::Standard; - /** - * \brief Connection string. - */ - using ConnURI = built::RedisConfig::URI; - - RedisBuilder(); - - /** - * \brief Connect to Redis using explicit connection options. - * \param opts The options. - * \return Reference to this. - */ - RedisBuilder& Connection(ConnOpts opts); - - /** - * \brief Connect to Redis using a URI. - * \param uri The URI. - * \return Reference to this. - */ - RedisBuilder& Connection(ConnURI uri); - - [[nodiscard]] tl::expected Build() const; - - private: - built::RedisConfig config_; -}; - template <> class DataSourceBuilder { public: diff --git a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp index bee8aee2f..8b6a13055 100644 --- a/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/data_source_config.hpp @@ -48,31 +48,6 @@ struct PollingConfig { std::chrono::seconds min_polling_interval; }; -struct RedisConfig { - using URI = std::string; - - struct Standard { - /** - * \brief Redis host. Required; cannot be empty string. - */ - std::string host; - /** - * \brief Redis port. Required. - */ - std::optional port; - /** - * \brief Redis password. Optional. - */ - std::optional password; - /** - * \brief Redis db. Optional. - */ - std::optional db; - }; - - std::variant connection_; -}; - template struct DataSourceConfig; diff --git a/libs/common/include/launchdarkly/error.hpp b/libs/common/include/launchdarkly/error.hpp index 64d3f2194..baa35bfd4 100644 --- a/libs/common/include/launchdarkly/error.hpp +++ b/libs/common/include/launchdarkly/error.hpp @@ -24,7 +24,6 @@ enum class Error : std::uint32_t { /* Client-side errors: 10000-19999 */ /* Server-side errors: 20000-29999 */ kConfig_DataSystem_LazyLoad_MissingSource = 20000, - kMax = std::numeric_limits::max() }; diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index 7bb93515c..0c4502db9 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -1,13 +1,12 @@ #pragma once -#include +#include #include namespace launchdarkly::server_side::data_systems { -class RedisDataSource final - : public data_interfaces::ISerializedDataPullSource { +class RedisDataSource final : public data_interfaces::ISerializedDataReader { public: RedisDataSource(std::string uri, std::string prefix); [[nodiscard]] GetResult Get(integrations::ISerializedItemKind const& kind, @@ -20,7 +19,8 @@ class RedisDataSource final private: std::string const prefix_; std::string const inited_key_; - std::string key_for_kind(integrations::ISerializedItemKind const& kind) const; + std::string key_for_kind( + integrations::ISerializedItemKind const& kind) const; mutable sw::redis::Redis redis_; }; diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp index 8c3ac137b..231152ef5 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -12,7 +12,7 @@ RedisDataSource::RedisDataSource(std::string uri, std::string prefix) inited_key_(prefix_ + ":$inited"), redis_(std::move(uri)) {} -data_interfaces::ISerializedDataPullSource::GetResult RedisDataSource::Get( +data_interfaces::ISerializedDataReader::GetResult RedisDataSource::Get( integrations::ISerializedItemKind const& kind, std::string const& itemKey) const { if (auto maybe_item = redis_.hget(key_for_kind(kind), itemKey)) { @@ -22,7 +22,7 @@ data_interfaces::ISerializedDataPullSource::GetResult RedisDataSource::Get( return tl::make_unexpected(Error{"not found"}); } -data_interfaces::ISerializedDataPullSource::AllResult RedisDataSource::All( +data_interfaces::ISerializedDataReader::AllResult RedisDataSource::All( integrations::ISerializedItemKind const& kind) const { std::unordered_map raw_items; AllResult::value_type items; diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index f81a2d02a..d5d0147c3 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -10,11 +10,11 @@ using namespace launchdarkly::server_side::config; class LazyLoadTest : public ::testing::Test {}; -class FakeDataSource : public launchdarkly::server_side::data_interfaces:: - ISerializedDataPullSource { +class FakeDataSource final : public data_interfaces::ISerializedDataReader { public: - using GetFn = std::function; + using GetFn = + std::function; using AllFn = std::function; @@ -47,7 +47,8 @@ class FakeDataSource : public launchdarkly::server_side::data_interfaces:: items_requested[kind.Namespace()].push_back(itemKey); return item_getter(kind, itemKey); } - AllResult All(integrations::ISerializedItemKind const& kind) const override { + AllResult All( + integrations::ISerializedItemKind const& kind) const override { all_requested.push_back(kind.Namespace()); return all_getter(kind); } From e21d30de895676ae6377298e4de14d0c7a7e0403 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 15:08:30 -0800 Subject: [PATCH 129/244] remove obsolete file --- .../src/config/data_sources_builder.cpp | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 libs/common/src/config/data_sources_builder.cpp diff --git a/libs/common/src/config/data_sources_builder.cpp b/libs/common/src/config/data_sources_builder.cpp deleted file mode 100644 index 788f1d18b..000000000 --- a/libs/common/src/config/data_sources_builder.cpp +++ /dev/null @@ -1,35 +0,0 @@ - -#include - -namespace launchdarkly::config::shared::builders { - -BootstrapBuilder::BootstrapBuilder() - : order_(Order::ConsistentFirst), seed_(std::nullopt) {} - -BootstrapBuilder& BootstrapBuilder::Order(enum BootstrapBuilder::Order order) { - order_ = order; - return *this; -} - -BootstrapBuilder& BootstrapBuilder::RandomSeed( - BootstrapBuilder::SeedType seed) { - seed_ = seed; - return *this; -} - -DataSourcesBuilder::DataSourcesBuilder() - : bootstrap_(), sources_() {} - -DataSourceBuilder& DataSourcesBuilder::Source() { - return sources_.emplace_back(); -} - -DataSourcesBuilder& DataSourcesBuilder::Destination() { - return *this; -} - -BootstrapBuilder& DataSourcesBuilder::Bootstrap() { - return bootstrap_; -} - -} // namespace launchdarkly::config::shared::builders From 57a28c40af232d8d3041eed7a1de2f601375d65b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 16:00:57 -0800 Subject: [PATCH 130/244] revert changes to C binding names for polling/streaming builders --- .../server_side/bindings/c/config/builder.h | 32 +++++++++---------- libs/server-sdk/src/bindings/c/builder.cpp | 28 ++++++++-------- 2 files changed, 30 insertions(+), 30 deletions(-) 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 index ba8451306..411b4a345 100644 --- 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 @@ -18,8 +18,8 @@ extern "C" { // only need to export C interface if #endif typedef struct _LDServerConfigBuilder* LDServerConfigBuilder; -typedef struct _LDServerStreamingSyncBuilder* LDServerStreamingSyncBuilder; -typedef struct _LDServerPollingSyncBuilder* LDServerPollingSyncBuilder; +typedef struct _LDServerDataSourceStreamBuilder* LDServerDataSourceStreamBuilder; +typedef struct _LDServerDataSourcePollBuilder* LDServerDataSourcePollBuilder; /** * Constructs a client-side config builder. @@ -180,7 +180,7 @@ LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, LD_EXPORT(void) LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming( LDServerConfigBuilder b, - LDServerStreamingSyncBuilder stream_builder); + LDServerDataSourceStreamBuilder stream_builder); /** * Configures the Background Sync data system with a Polling synchronizer. @@ -198,7 +198,7 @@ LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming( LD_EXPORT(void) LDServerConfigBuilder_DataSystem_BackgroundSync_Polling( LDServerConfigBuilder b, - LDServerPollingSyncBuilder poll_builder); + LDServerDataSourcePollBuilder poll_builder); /** * Specify if the SDK's data system should be enabled or not. @@ -216,12 +216,12 @@ LDServerConfigBuilder_DataSystem_Enabled(LDServerConfigBuilder b, bool enabled); * Creates a new DataSource builder for the Streaming method. * * If not passed into the config - * builder, must be manually freed with LDServerStreamingSyncBuilder_Free. + * builder, must be manually freed with LDServerDataSourceStreamBuilder_Free. * * @return New builder for Streaming method. */ -LD_EXPORT(LDServerStreamingSyncBuilder) -LDServerStreamingSyncBuilder_New(); +LD_EXPORT(LDServerDataSourceStreamBuilder) +LDServerDataSourceStreamBuilder_New(); /** * Sets the initial reconnect delay for the streaming connection. @@ -235,8 +235,8 @@ LDServerStreamingSyncBuilder_New(); * @param milliseconds Initial delay for a reconnection attempt. */ LD_EXPORT(void) -LDServerStreamingSyncBuilder_InitialReconnectDelayMs( - LDServerStreamingSyncBuilder b, +LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( + LDServerDataSourceStreamBuilder b, unsigned int milliseconds); /** @@ -246,19 +246,19 @@ LDServerStreamingSyncBuilder_InitialReconnectDelayMs( * @param b Builder to free. */ LD_EXPORT(void) -LDServerStreamingSyncBuilder_Free(LDServerStreamingSyncBuilder b); +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 LDServerPollingSyncBuilder_Free. + * builder, must be manually freed with LDServerDataSourcePollBuilder_Free. * * @return New builder for Polling method. */ -LD_EXPORT(LDServerPollingSyncBuilder) -LDServerPollingSyncBuilder_New(); +LD_EXPORT(LDServerDataSourcePollBuilder) +LDServerDataSourcePollBuilder_New(); /** * Sets the interval at which the SDK will poll for feature flag updates. @@ -266,8 +266,8 @@ LDServerPollingSyncBuilder_New(); * @param milliseconds Polling interval. */ LD_EXPORT(void) -LDServerPollingSyncBuilder_IntervalS(LDServerPollingSyncBuilder b, - unsigned int seconds); +LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, + unsigned int seconds); /** * Frees a Polling method builder. Do not call if the builder was consumed by @@ -276,7 +276,7 @@ LDServerPollingSyncBuilder_IntervalS(LDServerPollingSyncBuilder b, * @param b Builder to free. */ LD_EXPORT(void) -LDServerPollingSyncBuilder_Free(LDServerPollingSyncBuilder b); +LDServerDataSourcePollBuilder_Free(LDServerDataSourcePollBuilder b); /** * This should be used for wrapper SDKs to set the wrapper name. diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index 6ce061f02..9576b1cc6 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -16,13 +16,13 @@ using namespace launchdarkly::server_side::config::builders; (reinterpret_cast(ptr)) #define FROM_STREAM_BUILDER(ptr) \ - (reinterpret_cast(ptr)) + (reinterpret_cast(ptr)) #define TO_POLL_BUILDER(ptr) \ (reinterpret_cast(ptr)) #define FROM_POLL_BUILDER(ptr) \ - (reinterpret_cast(ptr)) + (reinterpret_cast(ptr)) #define TO_BASIC_LOGGING_BUILDER(ptr) \ (reinterpret_cast(ptr)) @@ -168,27 +168,27 @@ LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, LD_EXPORT(void) LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming( LDServerConfigBuilder b, - LDServerStreamingSyncBuilder stream_builder) { + LDServerDataSourceStreamBuilder stream_builder) { LD_ASSERT_NOT_NULL(b); LD_ASSERT_NOT_NULL(stream_builder); BackgroundSyncBuilder::Streaming* sb = TO_STREAM_BUILDER(stream_builder); TO_BUILDER(b)->DataSystem().Method( BackgroundSyncBuilder().Synchronizer(*sb)); - LDServerStreamingSyncBuilder_Free(stream_builder); + LDServerDataSourceStreamBuilder_Free(stream_builder); } LD_EXPORT(void) LDServerConfigBuilder_DataSystem_BackgroundSync_Polling( LDServerConfigBuilder b, - LDServerPollingSyncBuilder poll_builder) { + LDServerDataSourcePollBuilder poll_builder) { LD_ASSERT_NOT_NULL(b); LD_ASSERT_NOT_NULL(poll_builder); BackgroundSyncBuilder::Polling* pb = TO_POLL_BUILDER(poll_builder); TO_BUILDER(b)->DataSystem().Method( BackgroundSyncBuilder().Synchronizer(*pb)); - LDServerPollingSyncBuilder_Free(poll_builder); + LDServerDataSourcePollBuilder_Free(poll_builder); } LD_EXPORT(void) @@ -198,15 +198,15 @@ LDServerConfigBuilder_DataSystem_Enabled(LDServerConfigBuilder b, TO_BUILDER(b)->DataSystem().Enabled(enabled); } -LD_EXPORT(LDServerStreamingSyncBuilder) -LDServerStreamingSyncBuilder_New() { +LD_EXPORT(LDServerDataSourceStreamBuilder) +LDServerDataSourceStreamBuilder_New() { return FROM_STREAM_BUILDER( new DataSystemBuilder::BackgroundSync::Streaming()); } LD_EXPORT(void) -LDServerStreamingSyncBuilder_InitialReconnectDelayMs( - LDServerStreamingSyncBuilder b, +LDServerDataSourceStreamBuilder_InitialReconnectDelayMs( + LDServerDataSourceStreamBuilder b, unsigned int milliseconds) { LD_ASSERT_NOT_NULL(b); @@ -215,16 +215,16 @@ LDServerStreamingSyncBuilder_InitialReconnectDelayMs( } LD_EXPORT(void) -LDServerStreamingSyncBuilder_Free(LDServerStreamingSyncBuilder b) { +LDServerDataSourceStreamBuilder_Free(LDServerDataSourceStreamBuilder b) { delete TO_STREAM_BUILDER(b); } -LD_EXPORT(LDServerPollingSyncBuilder) LDServerPollingSyncBuilder_New() { +LD_EXPORT(LDServerDataSourcePollBuilder) LDServerDataSourcePollBuilder_New() { return FROM_POLL_BUILDER(new DataSystemBuilder::BackgroundSync::Polling()); } LD_EXPORT(void) -LDServerPollingSyncBuilder_IntervalS(LDServerPollingSyncBuilder b, +LDServerDataSourcePollBuilder_IntervalS(LDServerDataSourcePollBuilder b, unsigned int seconds) { LD_ASSERT_NOT_NULL(b); @@ -232,7 +232,7 @@ LDServerPollingSyncBuilder_IntervalS(LDServerPollingSyncBuilder b, } LD_EXPORT(void) -LDServerPollingSyncBuilder_Free(LDServerPollingSyncBuilder b) { +LDServerDataSourcePollBuilder_Free(LDServerDataSourcePollBuilder b) { delete TO_POLL_BUILDER(b); } From 741cd8d187db9c7e9c36b5faf25e4bbb798ee3a9 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 21 Nov 2023 16:02:08 -0800 Subject: [PATCH 131/244] update wording on c binding builders --- .../server_side/bindings/c/config/builder.h | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) 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 index 411b4a345..6ec557262 100644 --- 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 @@ -28,7 +28,7 @@ LD_EXPORT(LDServerConfigBuilder) LDServerConfigBuilder_New(char const* sdk_key); /** * Sets a custom URL for the polling service. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -36,7 +36,7 @@ LDServerConfigBuilder_ServiceEndpoints_PollingBaseURL(LDServerConfigBuilder b, char const* url); /** * Sets a custom URL for the streaming service. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -44,7 +44,7 @@ LDServerConfigBuilder_ServiceEndpoints_StreamingBaseURL(LDServerConfigBuilder b, char const* url); /** * Sets a custom URL for the events service. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -53,7 +53,7 @@ LDServerConfigBuilder_ServiceEndpoints_EventsBaseURL(LDServerConfigBuilder b, /** * Sets a custom URL for a Relay Proxy instance. The streaming, * polling, and events URLs are set automatically. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param url Target URL. Must not be NULL. */ LD_EXPORT(void) @@ -63,7 +63,7 @@ LDServerConfigBuilder_ServiceEndpoints_RelayProxyBaseURL( /** * Sets an identifier for the application. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param app_id Non-empty string. Must be <= 64 chars. Must be alphanumeric, * '-', '.', or '_'. Must not be NULL. */ @@ -73,7 +73,7 @@ LDServerConfigBuilder_AppInfo_Identifier(LDServerConfigBuilder b, /** * Sets a version for the application. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param app_version Non-empty string. Must be <= 64 chars. Must be * alphanumeric, * '-', '.', or '_'. Must not be NULL. @@ -91,7 +91,7 @@ LDServerConfigBuilder_AppInfo_Version(LDServerConfigBuilder b, * * This overrides specific configuration of events and/or data system, if * present. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param offline True if offline. */ LD_EXPORT(void) @@ -100,7 +100,7 @@ LDServerConfigBuilder_Offline(LDServerConfigBuilder b, bool offline); /** * Specify if event-sending should be enabled or not. By default, * events are enabled. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param enabled True to enable event-sending. */ LD_EXPORT(void) @@ -110,7 +110,7 @@ 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 Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param capacity Event queue capacity. */ LD_EXPORT(void) @@ -120,7 +120,7 @@ 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 Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param milliseconds Interval between automatic flushes. */ LD_EXPORT(void) @@ -146,7 +146,7 @@ LDServerConfigBuilder_Events_FlushIntervalMs(LDServerConfigBuilder b, * necessary to call either of these methods, as the default behavior is to * treat all attributes as non-private unless otherwise specified. * - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param all_attributes_private True for behavior of (1), false for default * behavior of (2) or (3). */ @@ -157,7 +157,7 @@ LDServerConfigBuilder_Events_AllAttributesPrivate(LDServerConfigBuilder b, /** * Specifies a single private attribute. May be called multiple times * with additional private attributes. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param attribute_reference Attribute to mark private. */ LD_EXPORT(void) @@ -173,7 +173,7 @@ LDServerConfigBuilder_Events_PrivateAttribute(LDServerConfigBuilder b, * with LaunchDarkly. Flag data is received automatically in the background, * meaning there are no network costs associated with evaluating flags. * - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param stream_builder The streaming builder. The builder is consumed; do not * free it. */ @@ -191,7 +191,7 @@ LDServerConfigBuilder_DataSystem_BackgroundSync_Streaming( * Between requests, flag data may be stale to some degree. This mode may be * advantageous if a streaming connection cannot be maintained. * - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param poll_builder The polling builder. The builder is consumed; do not free * it. */ @@ -206,7 +206,7 @@ LDServerConfigBuilder_DataSystem_BackgroundSync_Polling( * By default, the SDK uses the Background Sync data system with a Streaming * connection. * - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param enabled True to enable the data system, false to disable it. */ LD_EXPORT(void) @@ -282,7 +282,7 @@ 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 Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param wrapper_name Name of the wrapper. */ LD_EXPORT(void) @@ -293,7 +293,7 @@ LDServerConfigBuilder_HttpProperties_WrapperName(LDServerConfigBuilder b, * This should be used for wrapper SDKs to set the wrapper version. * * Wrapper information will be included in request headers. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param wrapper_version Version of the wrapper. */ LD_EXPORT(void) @@ -305,7 +305,7 @@ LDServerConfigBuilder_HttpProperties_WrapperVersion( * Set a custom header value. May be called more than once with additional * headers. * - * @param b Config builder. Must not be NULL. + * @param b Server 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. */ @@ -316,14 +316,14 @@ LDServerConfigBuilder_HttpProperties_Header(LDServerConfigBuilder b, /** * Disables the default SDK logging. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. */ LD_EXPORT(void) LDServerConfigBuilder_Logging_Disable(LDServerConfigBuilder b); /** * Configures the SDK with basic logging. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param basic_builder The basic logging builder. Must not be NULL. */ LD_EXPORT(void) @@ -332,7 +332,7 @@ LDServerConfigBuilder_Logging_Basic(LDServerConfigBuilder b, /** * Configures the SDK with custom logging. - * @param b Config builder. Must not be NULL. + * @param b Server config builder. Must not be NULL. * @param custom_builder The custom logging builder. Must not be NULL. */ LD_EXPORT(void) From 8a14a1d9c887d44759e9346295e4bb089740d12b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 07:34:24 -0800 Subject: [PATCH 132/244] fix formatting in builder.h --- .../launchdarkly/server_side/bindings/c/config/builder.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 8d1a71579..d64d89235 100644 --- 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 @@ -18,7 +18,8 @@ extern "C" { // only need to export C interface if #endif typedef struct _LDServerConfigBuilder* LDServerConfigBuilder; -typedef struct _LDServerDataSourceStreamBuilder* LDServerDataSourceStreamBuilder; +typedef struct _LDServerDataSourceStreamBuilder* + LDServerDataSourceStreamBuilder; typedef struct _LDServerDataSourcePollBuilder* LDServerDataSourcePollBuilder; /** From a788d8177cc0a609d660683e7ead6da3ad53b2a8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 08:04:23 -0800 Subject: [PATCH 133/244] revert changes to server_c_bindings_test.cpp --- libs/server-sdk/tests/server_c_bindings_test.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 4ef32eb1d..5a684fdf3 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -1,16 +1,15 @@ #include -#include #include #include #include +#include + #include #include -using namespace launchdarkly::server_side; - TEST(ClientBindings, MinimalInstantiation) { LDServerConfigBuilder cfg_builder = LDServerConfigBuilder_New("sdk-123"); @@ -44,7 +43,7 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { LDServerSDK sdk = LDServerSDK_New(config); - LDServerDataSourceStatusListener listener{}; + struct LDServerDataSourceStatusListener listener {}; LDServerDataSourceStatusListener_Init(&listener); listener.UserData = const_cast("Potato"); From e693d859b20d05a56ac045013aee3eb9117bf8d7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 09:12:33 -0800 Subject: [PATCH 134/244] back out irrelevant change to client_entity --- contract-tests/server-contract-tests/include/client_entity.hpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/contract-tests/server-contract-tests/include/client_entity.hpp b/contract-tests/server-contract-tests/include/client_entity.hpp index 2b58d6631..39b4a3be3 100644 --- a/contract-tests/server-contract-tests/include/client_entity.hpp +++ b/contract-tests/server-contract-tests/include/client_entity.hpp @@ -2,11 +2,8 @@ #include #include - #include -#include - class ClientEntity { public: explicit ClientEntity( From 706fd852980b8908ac7f9154079977ec64112ad7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 09:22:46 -0800 Subject: [PATCH 135/244] remove obsolete json_pull_source.cpp --- .../json_pull_source.cpp | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp deleted file mode 100644 index 2a8a1e6cf..000000000 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_pull_source.cpp +++ /dev/null @@ -1,53 +0,0 @@ -#include "json_pull_source.hpp" - -#include -#include - -#include - -#include - -namespace launchdarkly::server_side::data_components { - -JsonSource::JsonSource(data_interfaces::ISerializedDataPullSource& json_source) - : flag_kind_(), segment_kind_(), source_(json_source) {} - -data_interfaces::IPullSource::Single -JsonSource::GetFlag(std::string const& key) const { - return Deserialize(flag_kind_, key); -} - -data_interfaces::IPullSource::Single -JsonSource::GetSegment(std::string const& key) const { - return Deserialize(segment_kind_, key); -} - -data_interfaces::IPullSource::Collection -JsonSource::AllFlags() const { - // TODO: deserialize then return - data_interfaces::ISerializedDataPullSource::AllResult result = - source_.All(flag_kind_); - if (!result) { - return tl::make_unexpected(result.error().message); - } - - Collection flags; - for (auto [key, val] : *result) { - // TODO - } - return flags; -} - -data_interfaces::IPullSource::Collection -JsonSource::AllSegments() const { - // TODO: deserialize then return - - data_interfaces::ISerializedDataPullSource::AllResult result = - source_.All(segment_kind_); -} - -std::string const& JsonSource::Identity() const { - return source_.Identity(); -} - -} // namespace launchdarkly::server_side::data_components From c4169643bcdd83f4e1011100f4dc3cc3469d4e0c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 11:57:32 -0800 Subject: [PATCH 136/244] feat: implement JsonDestination --- libs/server-sdk/src/CMakeLists.txt | 2 + .../json_destination.cpp | 49 +++++++++++++++++-- .../json_destination.hpp | 12 ++++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index f4c8be542..b9e311e99 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -40,6 +40,8 @@ target_sources(${LIBNAME} data_components/memory_store/memory_store.cpp data_components/serialization_adapters/json_deserializer.hpp data_components/serialization_adapters/json_deserializer.cpp + data_components/serialization_adapters/json_destination.hpp + data_components/serialization_adapters/json_destination.cpp data_systems/background_sync/sources/noop/null_data_source.hpp data_systems/background_sync/sources/noop/null_data_source.cpp data_systems/background_sync/sources/polling/polling_data_source.hpp diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 9cb5f7dd1..578fc4310 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -1,23 +1,66 @@ #include "json_destination.hpp" +#include +#include + namespace launchdarkly::server_side::data_components { +using integrations::SerializedItemDescriptor; + +FlagKind const JsonDestination::Kinds::Flag = FlagKind(); +SegmentKind const JsonDestination::Kinds::Segment = SegmentKind(); + JsonDestination::JsonDestination( data_interfaces::ISerializedDestination& destination) : dest_(destination) {} void JsonDestination::Init(data_model::SDKDataSet data_set) { - // TODO: serialize and forward to dest_.Init + // TODO: implement } void JsonDestination::Upsert(std::string const& key, data_model::FlagDescriptor flag) { - // TODO: serialize and forward to dest_.Upsert + SerializedItemDescriptor descriptor; + + if (!flag.item) { + boost::json::object tombstone; + tombstone.emplace("deleted", true); + tombstone.emplace("key", key); + tombstone.emplace("version", flag.version); + + descriptor = SerializedItemDescriptor::Absent( + flag.version, boost::json::serialize(tombstone)); + } else { + descriptor = SerializedItemDescriptor::Present( + flag.version, + boost::json::serialize(boost::json::value_from(*flag.item))); + } + + // TOOD: Log upsert errors? + + auto _ = dest_.Upsert(Kinds::Flag, key, std::move(descriptor)); } void JsonDestination::Upsert(std::string const& key, data_model::SegmentDescriptor segment) { - // TODO: serialize and forward to dest_.Upsert + SerializedItemDescriptor descriptor; + + if (!segment.item) { + boost::json::object tombstone; + tombstone.emplace("deleted", true); + tombstone.emplace("key", key); + tombstone.emplace("version", segment.version); + + descriptor = SerializedItemDescriptor::Absent( + segment.version, boost::json::serialize(tombstone)); + } else { + descriptor = SerializedItemDescriptor::Present( + segment.version, + boost::json::serialize(boost::json::value_from(*segment.item))); + } + // TOOD: Log upsert errors? + + auto _ = dest_.Upsert(Kinds::Segment, key, std::move(descriptor)); } std::string const& JsonDestination::Identity() const { diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 88f6c8cb9..76378cd57 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -1,11 +1,15 @@ +#pragma once + +#include "../../data_components/kinds/kinds.hpp" #include "../../data_interfaces/destination/idestination.hpp" #include "../../data_interfaces/destination/iserialized_destination.hpp" namespace launchdarkly::server_side::data_components { -class JsonDestination : public data_interfaces::IDestination { +class JsonDestination final : public data_interfaces::IDestination { public: - JsonDestination(data_interfaces::ISerializedDestination& destination); + explicit JsonDestination( + data_interfaces::ISerializedDestination& destination); virtual void Init(data_model::SDKDataSet data_set) override; @@ -19,6 +23,10 @@ class JsonDestination : public data_interfaces::IDestination { private: data_interfaces::ISerializedDestination& dest_; + struct Kinds { + static FlagKind const Flag; + static SegmentKind const Segment; + }; }; } // namespace launchdarkly::server_side::data_components From 5711b7e9c2a43820dec1fb1b00c4ffa3ab31b958 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 12:34:37 -0800 Subject: [PATCH 137/244] update SerializedItemDescriptor --- .../serialized_item_descriptor.hpp | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp index 4f92e9904..73f97964a 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp @@ -24,6 +24,37 @@ struct SerializedItemDescriptor { * std::nullopt for deleted items. */ std::optional serializedItem; + + /** + * @brief Constructs a SerializedItemDescriptor from a version and a + * serialized item. + * @param version Version of item. + * @param data Serialized item. + * @return SerializedItemDescriptor. + */ + static SerializedItemDescriptor Present(std::uint64_t version, + std::string data) { + return SerializedItemDescriptor{version, false, std::move(data)}; + } + + /** + * @brief Constructs a SerializedItemDescriptor from a version and a + * tombstone. + * + * This is used when an item is deleted: the tombstone can be stored in + * place of the item, and the version checked in the future. Without the + * tombstone, out-of-order data updates could "resurrect" a deleted item. + * + * @param version Version of the item. + * @param tombstone_rep Serialized tombstone representation of the item. + * @return SerializedItemDescriptor. + */ + static SerializedItemDescriptor Absent(std::uint64_t const version, + std::string tombstone_rep) { + return SerializedItemDescriptor{version, true, + std::move(tombstone_rep)}; + } +} }; } // namespace launchdarkly::server_side::integrations From c3fb9bebc8306f70dff868f6fa54655f6c8e4673 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 12:35:08 -0800 Subject: [PATCH 138/244] implementing JSON destination --- .../json_destination.cpp | 110 +++++++++++------- .../json_destination.hpp | 42 ++++++- 2 files changed, 111 insertions(+), 41 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 578fc4310..5cfe57fa0 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -5,65 +5,95 @@ namespace launchdarkly::server_side::data_components { +using data_interfaces::ISerializedDestination; using integrations::SerializedItemDescriptor; FlagKind const JsonDestination::Kinds::Flag = FlagKind(); SegmentKind const JsonDestination::Kinds::Segment = SegmentKind(); -JsonDestination::JsonDestination( - data_interfaces::ISerializedDestination& destination) +/** + * @brief Creates a boost::json::value representing a tombstone for a given + * ItemDescriptor. The tombstone includes 'deleted', 'key', and 'version' + * fields. + * + * @param T Type of descriptor. + * @param key Key of item. + * @param desc The descriptor. + * @return Tombstone suitable for serialization. + */ +template +boost::json::value Tombstone(std::string const& key, + data_model::ItemDescriptor const& desc) { + boost::json::object tombstone; + tombstone.emplace("deleted", true); + tombstone.emplace("key", key); + tombstone.emplace("version", desc.version); + return tombstone; +} + +/** + * @brief Creates a SerializedItemDescriptor for a given ItemDescriptor. The + * SerializedItemDescriptor either represents an item that is present, or one + * that is absent. + * + * @param T Type of descriptor. + * @param key Key of item. + * @param desc The descriptor. + * @return SerializedItemDescriptor suitable for forwarding to an + * ISerializedDestination. + */ +template +SerializedItemDescriptor Serialize(std::string const& key, + data_model::ItemDescriptor const& desc) { + return desc.item + ? SerializedItemDescriptor::Present( + desc.version, boost::json::serialize( + boost::json::value_from(*desc.item))) + : SerializedItemDescriptor::Absent( + desc.version, + boost::json::serialize(Tombstone(key, desc))); +} + +JsonDestination::JsonDestination(ISerializedDestination& destination) : dest_(destination) {} void JsonDestination::Init(data_model::SDKDataSet data_set) { // TODO: implement -} -void JsonDestination::Upsert(std::string const& key, - data_model::FlagDescriptor flag) { - SerializedItemDescriptor descriptor; - - if (!flag.item) { - boost::json::object tombstone; - tombstone.emplace("deleted", true); - tombstone.emplace("key", key); - tombstone.emplace("version", flag.version); - - descriptor = SerializedItemDescriptor::Absent( - flag.version, boost::json::serialize(tombstone)); - } else { - descriptor = SerializedItemDescriptor::Present( - flag.version, - boost::json::serialize(boost::json::value_from(*flag.item))); + std::vector items; + + ISerializedDestination::OrderedNamepace flags; + for (auto const& [key, descriptor] : data_set.flags) { + flags.emplace_back(key, Serialize(key, descriptor)); } + std::sort(flags.begin(), flags.end()); + items.emplace_back(Kinds::Flag, std::move(flags)); - // TOOD: Log upsert errors? + ISerializedDestination::OrderedNamepace segments; + for (auto const& [key, descriptor] : data_set.segments) { + segments.emplace_back(key, Serialize(key, descriptor)); + } + std::sort(segments.begin(), segments.end()); + items.emplace_back(Kinds::Segment, std::move(segments)); - auto _ = dest_.Upsert(Kinds::Flag, key, std::move(descriptor)); + dest_.Init(std::move(items)); } void JsonDestination::Upsert(std::string const& key, - data_model::SegmentDescriptor segment) { - SerializedItemDescriptor descriptor; - - if (!segment.item) { - boost::json::object tombstone; - tombstone.emplace("deleted", true); - tombstone.emplace("key", key); - tombstone.emplace("version", segment.version); - - descriptor = SerializedItemDescriptor::Absent( - segment.version, boost::json::serialize(tombstone)); - } else { - descriptor = SerializedItemDescriptor::Present( - segment.version, - boost::json::serialize(boost::json::value_from(*segment.item))); - } - // TOOD: Log upsert errors? + data_model::FlagDescriptor flag) { + // TODO: how to handle errors? + dest_.Upsert(Kinds::Flag, key, Serialize(key, flag)); +} - auto _ = dest_.Upsert(Kinds::Segment, key, std::move(descriptor)); +void JsonDestination::Upsert(std::string const& key, + data_model::SegmentDescriptor segment) { + // TODO: how to handle errors? + dest_.Upsert(Kinds::Segment, key, Serialize(key, segment)); } std::string const& JsonDestination::Identity() const { - return dest_.Identity(); + static std::string const identity = dest_.Identity() + "(JSON)"; + return identity; } + } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 76378cd57..9752948d8 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -6,19 +6,59 @@ namespace launchdarkly::server_side::data_components { +/** + * @brief JsonDestination is responsible for converting flag and segment + * models into serialized data suitable for storage in an + * ISerializedDestination. + * + * It's purpose is to encapsulate the details of serialization in a reusable + * adapter. + * + */ class JsonDestination final : public data_interfaces::IDestination { public: + /** + * @brief Construct the JsonDestination with the given destination. + * Calls to Upsert will trigger serialization and storage in the + * destination. + * @param destination Where data should be forwarded. + */ explicit JsonDestination( data_interfaces::ISerializedDestination& destination); - virtual void Init(data_model::SDKDataSet data_set) override; + /** + * @brief Initialize the destination with an SDK data set. + * @param data_set The initial data. + */ + void Init(data_model::SDKDataSet data_set) override; + /** + * @brief Upsert data for the flag named by key. + * + * If the descriptor represents a deleted item, a tombstone will + * be forwarded to the ISerializedDestination. + * + * @param key Key of flag. + * @param flag Descriptor of flag. + */ void Upsert(std::string const& key, data_model::FlagDescriptor flag) override; + /** + * @brief Upsert data for the segment named by key. + * + * If the descriptor represents a deleted item, a tombstone will + * be forwarded to the ISerializedDestination. + * + * @param key Key of segment. + * @param segment Descriptor of segment. + */ void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + /** + * @return Identity of this destination. Used in logs. + */ [[nodiscard]] std::string const& Identity() const override; private: From 623e174a82c11b8d5daa39a32a044944a208d824 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 12:35:25 -0800 Subject: [PATCH 139/244] update comments on ISerializedDestination --- .../destination/iserialized_destination.hpp | 70 ++++++++++++++++--- 1 file changed, 60 insertions(+), 10 deletions(-) diff --git a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp index 37f936cda..af99c100a 100644 --- a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp @@ -9,6 +9,31 @@ namespace launchdarkly::server_side::data_interfaces { +/** + * @brief This interface is used for persisting data to databases, or any other + * component that can store feature flag / segment data. + * + * The SDK automatically converts between its in-memory data model and a + * serialized string form, which is what this interface interacts with. + * + * Each item in the store is conceptually a SerializedItemDescriptor containing + * a version and the serialized form. The serialized form might represent a + * flag/segment, or a "tombstone" representing the (absence) of an item. + * + * It's possible to satisfy the interface in two ways: + * + * 1. The Destination can store the version number, deleted state, and item + * separately. This is preferred because it avoids the need to deserialize the + * entire item just to inspect the version/deleted state when performing an + * Upsert operation. If implementing this strategy, the Destination may ignore + * deleted SerializeItemDescriptor's serializedItem members on Upserts. + * + * 2. If there's no way to store the version number, deleted state, and item + * separately in an efficient way, then the store may instead persist the + * serializedItem as-is during an Upsert. The item will contain a "tombstone" + * representation which the SDK will later use to determine if the item is + * deleted or not. + */ class ISerializedDestination { public: enum class InitResult { @@ -53,34 +78,59 @@ class ISerializedDestination { std::pair; /** - * \brief Initializes the destination with data. - * \param sdk_data_set A series of collections, where each collection is + * @brief Overwrites the Destination's contents with a set of items for each + * collection. All previous data should be disgraded regardless of + * versioning. + * + * The update should be done atomically. If that's not possible, 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 sdk_data_set A series of collections, where each collection is * named by an ISerializedItemKind and contains a list of key/value pairs * representing the key of the item and the serialized form of the item. - * \return InitResult::kSuccess if all + * @return InitResult::kSuccess if all * data items were stored, or InitResult::kError if any error occoured. */ [[nodiscard]] virtual InitResult Init( std::vector sdk_data_set) = 0; /** - * \brief Upserts a single item (update if exist, insert if not.) - * \param kind The item kind. - * \param key The item key. - * \param item Serialized form of the item. The item should be deleted if - * the SerializedItem's 'deleted' bool is true. \return + * @brief Upserts a single item (update if exist, insert if not.) + * + * If the given key already exists in the collection named by kind, + * then the Destination must check the version number corresponding to that + * key. Note that the item corresponding to that key may be a tombstone + * representing an absent item. + * + * If the version of the existing item is >= the version of the new item, + * return UpsertResult::kNotUpdated. If the Destination can't determine the + * version number of the existing item without full deserialization, then it + * may call integrations::ISerializedItemKind::Version on the data to obtain + * it. + * + * If the given item's deleted flag is true, the Destination must persist + * this fact. It can either store a tombstone (value of serializedItem), or + * if deletion state is stored separate from the item, it can use that + * mechanism. In any case, it should not delete/forget about the item. + * + * @param kind The item kind. + * @param key The item key. + * @param item Serialized form of the item. + * @return * UpsertResult::kSuccess if the operation was successful. * UpsertResult::kError if an error occured. Otherwise, * UpsertResult::kNotUpdated if the existing item version was greater than * the version passed in. */ [[nodiscard]] virtual UpsertResult Upsert( - std::string const& kind, + integrations::ISerializedItemKind const& kind, std::string const& key, integrations::SerializedItemDescriptor item) = 0; /** - * \return Identity of the destination. Used in logs. + * @return Identity of the destination. Used in logs. */ [[nodiscard]] virtual std::string const& Identity() const = 0; From 533d3a633f4fef5c3985dfb4b5330927aa7a5d82 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 22 Nov 2023 12:51:27 -0800 Subject: [PATCH 140/244] update comments --- .../serialization_adapters/json_destination.cpp | 5 +---- .../serialization_adapters/json_destination.hpp | 13 ++++++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 5cfe57fa0..b08980c9a 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -58,24 +58,21 @@ JsonDestination::JsonDestination(ISerializedDestination& destination) : dest_(destination) {} void JsonDestination::Init(data_model::SDKDataSet data_set) { - // TODO: implement - std::vector items; ISerializedDestination::OrderedNamepace flags; for (auto const& [key, descriptor] : data_set.flags) { flags.emplace_back(key, Serialize(key, descriptor)); } - std::sort(flags.begin(), flags.end()); items.emplace_back(Kinds::Flag, std::move(flags)); ISerializedDestination::OrderedNamepace segments; for (auto const& [key, descriptor] : data_set.segments) { segments.emplace_back(key, Serialize(key, descriptor)); } - std::sort(segments.begin(), segments.end()); items.emplace_back(Kinds::Segment, std::move(segments)); + // TODO: how to handle errors? dest_.Init(std::move(items)); } diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 9752948d8..c96ceb2db 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -14,8 +14,19 @@ namespace launchdarkly::server_side::data_components { * It's purpose is to encapsulate the details of serialization in a reusable * adapter. * + * JsonDestination does not initialize ISerializedDestination with a + * flag-dependency-order data layout, which is required for some stores (e.g. + * DynamoDB). + * + * Since DynamoDB is not supported at the moment this is acceptable. When the + * capability is needed, a new class should be derived from JsonDestination + * overriding Init to provide the correct data layout. + * + * Alternatively, JsonDestination can be made to order the data so that it works + * for any store. + * */ -class JsonDestination final : public data_interfaces::IDestination { +class JsonDestination : public data_interfaces::IDestination { public: /** * @brief Construct the JsonDestination with the given destination. From 4ca20a94bfbaa22e3637e2d79bd34b2d6b7c9e40 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 12:24:20 -0800 Subject: [PATCH 141/244] add WriteMinimal for std::vector, use in flag/segment serialization --- .../serialization/value_mapping.hpp | 9 ++++++++ libs/internal/src/serialization/json_flag.cpp | 23 ++++++++++++------- .../src/serialization/json_segment.cpp | 12 ++++------ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index 09d90b4e2..db584f493 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -166,6 +166,15 @@ void WriteMinimal(boost::json::object& obj, } } +template +void WriteMinimal(boost::json::object& obj, + std::string const& key, + std::vector const& val) { + if (!val.empty()) { + obj.emplace(key, boost::json::value_from(val)); + } +} + void WriteMinimal(boost::json::object& obj, std::string const& key, // No copy when not used. bool val); diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index 1d040c1fa..007184538 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -349,19 +349,26 @@ void tag_invoke(boost::json::value_from_tag const& unused, 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)); + WriteMinimal(obj, "variations", flag.variations); + WriteMinimal(obj, "rules", flag.rules); + WriteMinimal(obj, "prerequisites", flag.prerequisites); + if (auto fallthrough = boost::json::value_from(flag.fallthrough); + !fallthrough.as_object().empty()) { + obj.emplace("fallthrough", fallthrough); + } + if (flag.clientSideAvailability.usingEnvironmentId || + flag.clientSideAvailability.usingMobileKey) { + obj.emplace("clientSideAvailability", + boost::json::value_from(flag.clientSideAvailability)); + } + WriteMinimal(obj, "contextTargets", 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)); + + WriteMinimal(obj, "targets", user_targets); } } // namespace data_model diff --git a/libs/internal/src/serialization/json_segment.cpp b/libs/internal/src/serialization/json_segment.cpp index fa927b57a..89e2de150 100644 --- a/libs/internal/src/serialization/json_segment.cpp +++ b/libs/internal/src/serialization/json_segment.cpp @@ -114,13 +114,11 @@ void tag_invoke(boost::json::value_from_tag const& unused, 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)); + WriteMinimal(obj, "rules", segment.rules); + WriteMinimal(obj, "excluded", segment.excluded); + WriteMinimal(obj, "excludedContexts", segment.excludedContexts); + WriteMinimal(obj, "included", segment.included); + WriteMinimal(obj, "includedContexts", segment.includedContexts); } void tag_invoke(boost::json::value_from_tag const& unused, From d49e37b8d55d0ec4088554bcbe16752634a5ce93 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 12:24:41 -0800 Subject: [PATCH 142/244] add bool perator for ISerializedItemKind and descriptor --- .../server_side/integrations/iserialized_item_kind.hpp | 10 ++++++++++ .../integrations/serialized_item_descriptor.hpp | 7 ++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp index 81e2c0060..7dd11ecec 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp @@ -41,4 +41,14 @@ class ISerializedItemKind { protected: ISerializedItemKind() = default; }; + +/** + * @brief Used in test assertions; two ISerializedItemKinds are regarded as + * the same if they have the same namespace. + */ +inline bool operator==(ISerializedItemKind const& lhs, + ISerializedItemKind const& rhs) { + return lhs.Namespace() == rhs.Namespace(); +} + } // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp index 73f97964a..1d1bf408b 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp @@ -54,7 +54,12 @@ struct SerializedItemDescriptor { return SerializedItemDescriptor{version, true, std::move(tombstone_rep)}; } -} }; +inline bool operator==(SerializedItemDescriptor const& lhs, + SerializedItemDescriptor const& rhs) { + return lhs.version == rhs.version && lhs.deleted == rhs.deleted && + lhs.serializedItem == rhs.serializedItem; +} + } // namespace launchdarkly::server_side::integrations From d54ae6240ff36f6d5e87035780daa95154df1f9a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 12:24:56 -0800 Subject: [PATCH 143/244] add kinds.cpp to CMakeLists.txt --- libs/server-sdk/src/CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index b9e311e99..7050c1213 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -42,6 +42,8 @@ target_sources(${LIBNAME} data_components/serialization_adapters/json_deserializer.cpp data_components/serialization_adapters/json_destination.hpp data_components/serialization_adapters/json_destination.cpp + data_components/kinds/kinds.hpp + data_components/kinds/kinds.cpp data_systems/background_sync/sources/noop/null_data_source.hpp data_systems/background_sync/sources/noop/null_data_source.cpp data_systems/background_sync/sources/polling/polling_data_source.hpp From e5321339c58aa270d3f3d65bc3fe9d0c3f9a8197 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 12:25:28 -0800 Subject: [PATCH 144/244] add tests for JsonDestination --- .../json_destination.cpp | 56 ++++-- .../json_destination.hpp | 28 ++- libs/server-sdk/tests/CMakeLists.txt | 4 +- .../tests/json_destination_test.cpp | 189 ++++++++++++++++++ 4 files changed, 255 insertions(+), 22 deletions(-) create mode 100644 libs/server-sdk/tests/json_destination_test.cpp diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index b08980c9a..016e81cb3 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -54,8 +54,14 @@ SerializedItemDescriptor Serialize(std::string const& key, boost::json::serialize(Tombstone(key, desc))); } -JsonDestination::JsonDestination(ISerializedDestination& destination) - : dest_(destination) {} +JsonDestination::JsonDestination(Logger const& logger, + ISerializedDestination& destination) + : logger_(logger), dest_(destination) {} + +std::string const& JsonDestination::Identity() const { + static std::string const identity = dest_.Identity() + " (JSON)"; + return identity; +} void JsonDestination::Init(data_model::SDKDataSet data_set) { std::vector items; @@ -64,33 +70,57 @@ void JsonDestination::Init(data_model::SDKDataSet data_set) { for (auto const& [key, descriptor] : data_set.flags) { flags.emplace_back(key, Serialize(key, descriptor)); } + std::sort(flags.begin(), flags.end(), [](auto const& lhs, auto const& rhs) { + return lhs.first < rhs.first; + }); items.emplace_back(Kinds::Flag, std::move(flags)); ISerializedDestination::OrderedNamepace segments; for (auto const& [key, descriptor] : data_set.segments) { segments.emplace_back(key, Serialize(key, descriptor)); } + std::sort( + segments.begin(), segments.end(), + [](auto const& lhs, auto const& rhs) { return lhs.first < rhs.first; }); items.emplace_back(Kinds::Segment, std::move(segments)); - // TODO: how to handle errors? - dest_.Init(std::move(items)); + if (auto const result = dest_.Init(std::move(items)); + result != ISerializedDestination::InitResult::kSuccess) { + LD_LOG(logger_, LogLevel::kError) + << dest_.Identity() << ": failed to store initial SDK data"; + } } void JsonDestination::Upsert(std::string const& key, - data_model::FlagDescriptor flag) { - // TODO: how to handle errors? - dest_.Upsert(Kinds::Flag, key, Serialize(key, flag)); + data_model::FlagDescriptor const flag) { + LogUpsertResult(key, "flag", + dest_.Upsert(Kinds::Flag, key, Serialize(key, flag))); } void JsonDestination::Upsert(std::string const& key, - data_model::SegmentDescriptor segment) { - // TODO: how to handle errors? - dest_.Upsert(Kinds::Segment, key, Serialize(key, segment)); + data_model::SegmentDescriptor const segment) { + LogUpsertResult(key, "segment", + dest_.Upsert(Kinds::Segment, key, Serialize(key, segment))); } -std::string const& JsonDestination::Identity() const { - static std::string const identity = dest_.Identity() + "(JSON)"; - return identity; +void JsonDestination::LogUpsertResult( + std::string const& key, + std::string const& data_type, + ISerializedDestination::UpsertResult const& result) const { + switch (result) { + case ISerializedDestination::UpsertResult::kSuccess: + break; + case ISerializedDestination::UpsertResult::kError: + LD_LOG(logger_, LogLevel::kError) + << dest_.Identity() << ": failed to update " << data_type << " " + << key; + break; + case ISerializedDestination::UpsertResult::kNotUpdated: + LD_LOG(logger_, LogLevel::kDebug) + << dest_.Identity() << ": " << data_type << " " << key + << " not updated; data was stale"; + break; + } } } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index c96ceb2db..ac55e0dc0 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -4,6 +4,8 @@ #include "../../data_interfaces/destination/idestination.hpp" #include "../../data_interfaces/destination/iserialized_destination.hpp" +#include + namespace launchdarkly::server_side::data_components { /** @@ -15,12 +17,14 @@ namespace launchdarkly::server_side::data_components { * adapter. * * JsonDestination does not initialize ISerializedDestination with a - * flag-dependency-order data layout, which is required for some stores (e.g. - * DynamoDB). + * deterministic flag-dependency-order data layout, which is required for some + * stores (e.g. DynamoDB). Instead, it sorts items within a collection using + * '<', to have enough determinism for testing purposes. * - * Since DynamoDB is not supported at the moment this is acceptable. When the - * capability is needed, a new class should be derived from JsonDestination - * overriding Init to provide the correct data layout. + * Since DynamoDB is not supported at the moment this sorting is acceptable. + * When the support for a store requiring a specific ordering is needed, a new + * class should be derived from JsonDestination overriding Init to provide the + * correct data layout. * * Alternatively, JsonDestination can be made to order the data so that it works * for any store. @@ -32,9 +36,11 @@ class JsonDestination : public data_interfaces::IDestination { * @brief Construct the JsonDestination with the given destination. * Calls to Upsert will trigger serialization and storage in the * destination. + * @param logger Used for logging storage errors. * @param destination Where data should be forwarded. */ explicit JsonDestination( + Logger const& logger, data_interfaces::ISerializedDestination& destination); /** @@ -72,12 +78,20 @@ class JsonDestination : public data_interfaces::IDestination { */ [[nodiscard]] std::string const& Identity() const override; - private: - data_interfaces::ISerializedDestination& dest_; struct Kinds { static FlagKind const Flag; static SegmentKind const Segment; }; + + private: + void LogUpsertResult( + std::string const& key, + std::string const& data_type, + data_interfaces::ISerializedDestination::UpsertResult const& result) + const; + + Logger const& logger_; + data_interfaces::ISerializedDestination& dest_; }; } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 707abb839..cadf72f23 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -14,7 +14,7 @@ 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 timestamp GTest::gtest_main) +) +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp new file mode 100644 index 000000000..5bb79b80e --- /dev/null +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -0,0 +1,189 @@ +#include +#include + +#include "data_components/serialization_adapters/json_destination.hpp" + +#include + +#include "spy_logger.hpp" + +using namespace launchdarkly; +using namespace launchdarkly::server_side::data_components; +using namespace launchdarkly::server_side::data_interfaces; +using namespace launchdarkly::server_side::integrations; + +using ::testing::Eq; +using ::testing::NiceMock; +using ::testing::Pair; +using ::testing::Return; + +class MockSerializedDestination : public ISerializedDestination { + public: + MOCK_METHOD(InitResult, Init, (std::vector), (override)); + MOCK_METHOD(UpsertResult, + Upsert, + (ISerializedItemKind const&, + std::string const&, + SerializedItemDescriptor), + (override)); + MOCK_METHOD(std::string const&, Identity, (), (const, override)); +}; + +TEST(JsonDestination, WrapsUnderlyingDestinationIdentity) { + auto logger = logging::NullLogger(); + NiceMock mock_dest; + JsonDestination const destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, Identity()).WillOnce([]() { + return "FooCorp Database"; + }); + + ASSERT_EQ(destination.Identity(), "FooCorp Database (JSON)"); +} + +TEST(JsonDestination, InitErrorGeneratesLogMessage) { + auto spy_logger = std::make_shared(); + Logger logger(spy_logger); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, Identity()).WillOnce([] { + return "FooCorp Database"; + }); + + EXPECT_CALL(mock_dest, Init) + .WillOnce(Return(ISerializedDestination::InitResult::kError)); + + destination.Init(data_model::SDKDataSet{}); + + ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, "failed")); +} + +// The SerializedDestination need only be concerned with inserting items +// exactly as specified in its Init argument. This test verifies that the +// transformation from SDKDataSet into that argument is done correctly. The +// transformation consists of two parts; first, serializing items to JSON, and +// second, ordering those items by comparing keys with '<'. +TEST(JsonDestination, InitProperlyTransformsSDKDataSet) { + auto logger = logging::NullLogger(); + MockSerializedDestination mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL( + mock_dest, + Init(Eq(std::vector{ + {{JsonDestination::Kinds::Flag, + std::vector< + ISerializedDestination::Keyed>{ + {"flag_alpha", + SerializedItemDescriptor::Present( + 1, "{\"key\":\"flag_alpha\",\"version\":1}")}, + {"flag_beta", + SerializedItemDescriptor::Present( + 2, "{\"key\":\"flag_beta\",\"version\":2}")}}}, + {JsonDestination::Kinds::Segment, + std::vector< + ISerializedDestination::Keyed>{ + {"segment_alpha", + SerializedItemDescriptor::Present( + 1, "{\"key\":\"segment_alpha\",\"version\":1}")}, + {"segment_beta", + SerializedItemDescriptor::Present( + 2, "{\"key\":\"segment_beta\",\"version\":2}")}}}}}))) + .WillOnce(Return(ISerializedDestination::InitResult::kSuccess)); + + // Note: flag/segments are out of alphabetical order here to help verify + // they are sorted. + destination.Init(data_model::SDKDataSet{ + std::unordered_map{ + {"flag_beta", + data_model::FlagDescriptor(data_model::Flag{"flag_beta", 2})}, + {"flag_alpha", + data_model::FlagDescriptor(data_model::Flag{"flag_alpha", 1})}}, + std::unordered_map{ + {"segment_beta", data_model::SegmentDescriptor( + data_model::Segment{"segment_beta", 2})}, + {"segment_alpha", data_model::SegmentDescriptor( + data_model::Segment{"segment_alpha", 1})}}}); +} + +TEST(JsonDestination, UpsertFlagErrorGeneratesErrorMessage) { + auto spy_logger = std::make_shared(); + Logger logger(spy_logger); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, Identity()).WillOnce([] { + return "FooCorp Database"; + }); + + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); + + destination.Upsert("foo", data_model::FlagDescriptor(1)); + + ASSERT_TRUE( + spy_logger->Contains(0, LogLevel::kError, "failed to update flag foo")); +} + +TEST(JsonDestination, UpsertSegmentErrorGeneratesErrorMessage) { + auto spy_logger = std::make_shared(); + Logger logger(spy_logger); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, Identity()).WillOnce([] { + return "FooCorp Database"; + }); + + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); + + destination.Upsert("foo", data_model::SegmentDescriptor(1)); + + ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, + "failed to update segment foo")); +} + +TEST(JsonDestination, UpsertStaleFlagGeneratesDebugMessage) { + auto spy_logger = std::make_shared(); + Logger logger(spy_logger); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, Identity()).WillOnce([] { + return "FooCorp Database"; + }); + + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); + + destination.Upsert("foo", data_model::FlagDescriptor(1)); + + ASSERT_TRUE( + spy_logger->Contains(0, LogLevel::kDebug, "flag foo not updated")); +} + +TEST(JsonDestination, UpsertStaleSegmentGeneratesDebugMessage) { + auto spy_logger = std::make_shared(); + Logger logger(spy_logger); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, Identity()).WillOnce([] { + return "FooCorp Database"; + }); + + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); + + destination.Upsert("foo", data_model::SegmentDescriptor(1)); + + ASSERT_TRUE( + spy_logger->Contains(0, LogLevel::kDebug, "segment foo not updated")); +} From e2e4d409381b323f39347248a8da6c0545baceb5 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 13:19:30 -0800 Subject: [PATCH 145/244] more tests --- .../json_destination.cpp | 4 +- .../json_destination.hpp | 18 ++++----- .../tests/json_destination_test.cpp | 37 ++++++++++++++++++- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 016e81cb3..8fd0197bc 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -25,9 +25,9 @@ template boost::json::value Tombstone(std::string const& key, data_model::ItemDescriptor const& desc) { boost::json::object tombstone; - tombstone.emplace("deleted", true); tombstone.emplace("key", key); tombstone.emplace("version", desc.version); + tombstone.emplace("deleted", true); return tombstone; } @@ -64,6 +64,8 @@ std::string const& JsonDestination::Identity() const { } void JsonDestination::Init(data_model::SDKDataSet data_set) { + // TODO(sc-225327): Topographical sort of flag dependencies + std::vector items; ISerializedDestination::OrderedNamepace flags; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index ac55e0dc0..9aa3ba554 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -16,21 +16,17 @@ namespace launchdarkly::server_side::data_components { * It's purpose is to encapsulate the details of serialization in a reusable * adapter. * - * JsonDestination does not initialize ISerializedDestination with a - * deterministic flag-dependency-order data layout, which is required for some - * stores (e.g. DynamoDB). Instead, it sorts items within a collection using - * '<', to have enough determinism for testing purposes. + * JsonDestination does not currently initialize ISerializedDestination with a + * deterministic flag-dependency-order data order, which is required for some + * stores without atomic transactions (e.g. DynamoDB). * - * Since DynamoDB is not supported at the moment this sorting is acceptable. - * When the support for a store requiring a specific ordering is needed, a new - * class should be derived from JsonDestination overriding Init to provide the - * correct data layout. + * Instead, it sorts items within a collection using '<' on item keys, giving + * enough determinism for testing purposes. * - * Alternatively, JsonDestination can be made to order the data so that it works - * for any store. + * TODO(sc-225327): Implement topographic sort as prerequisite for DynamoDB. * */ -class JsonDestination : public data_interfaces::IDestination { +class JsonDestination final : public data_interfaces::IDestination { public: /** * @brief Construct the JsonDestination with the given destination. diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index 5bb79b80e..aafef12b3 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -15,6 +15,7 @@ using namespace launchdarkly::server_side::integrations; using ::testing::Eq; using ::testing::NiceMock; using ::testing::Pair; +using ::testing::Ref; using ::testing::Return; class MockSerializedDestination : public ISerializedDestination { @@ -67,7 +68,7 @@ TEST(JsonDestination, InitErrorGeneratesLogMessage) { // second, ordering those items by comparing keys with '<'. TEST(JsonDestination, InitProperlyTransformsSDKDataSet) { auto logger = logging::NullLogger(); - MockSerializedDestination mock_dest; + NiceMock mock_dest; JsonDestination destination{logger, mock_dest}; EXPECT_CALL( @@ -187,3 +188,37 @@ TEST(JsonDestination, UpsertStaleSegmentGeneratesDebugMessage) { ASSERT_TRUE( spy_logger->Contains(0, LogLevel::kDebug, "segment foo not updated")); } + +TEST(JsonDestination, UpsertDeletedFlagCreatesTombstone) { + auto spy_logger = std::make_shared(); + Logger logger(spy_logger); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL( + mock_dest, + Upsert(Ref(JsonDestination::Kinds::Flag), "flag", + SerializedItemDescriptor::Absent( + 2, "{\"key\":\"flag\",\"version\":2,\"deleted\":true}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + destination.Upsert("flag", data_model::FlagDescriptor(2)); +} + +TEST(JsonDestination, UpsertDeletedSegmentCreatesTombstone) { + auto spy_logger = std::make_shared(); + Logger logger(spy_logger); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL( + mock_dest, + Upsert(Ref(JsonDestination::Kinds::Segment), "segment", + SerializedItemDescriptor::Absent( + 2, "{\"key\":\"segment\",\"version\":2,\"deleted\":true}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + destination.Upsert("segment", data_model::SegmentDescriptor(2)); +} From fb6971c49ad2de10e283ac75fa781c54981572e6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 13:35:47 -0800 Subject: [PATCH 146/244] more tests --- .../serialized_item_descriptor.hpp | 6 +++ .../tests/json_destination_test.cpp | 47 ++++++++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp index 1d1bf408b..71f48b986 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace launchdarkly::server_side::integrations { @@ -62,4 +63,9 @@ inline bool operator==(SerializedItemDescriptor const& lhs, lhs.serializedItem == rhs.serializedItem; } +inline void PrintTo(SerializedItemDescriptor const& item, std::ostream* os) { + *os << "{version=" << item.version << ", deleted=" << item.deleted + << ", item=" << item.serializedItem.value_or("nullopt") << "}"; +} + } // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index aafef12b3..0f29ce092 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -31,7 +31,8 @@ class MockSerializedDestination : public ISerializedDestination { }; TEST(JsonDestination, WrapsUnderlyingDestinationIdentity) { - auto logger = logging::NullLogger(); + Logger const logger = logging::NullLogger(); + NiceMock mock_dest; JsonDestination const destination{logger, mock_dest}; @@ -67,7 +68,8 @@ TEST(JsonDestination, InitErrorGeneratesLogMessage) { // transformation consists of two parts; first, serializing items to JSON, and // second, ordering those items by comparing keys with '<'. TEST(JsonDestination, InitProperlyTransformsSDKDataSet) { - auto logger = logging::NullLogger(); + Logger const logger = logging::NullLogger(); + NiceMock mock_dest; JsonDestination destination{logger, mock_dest}; @@ -190,8 +192,7 @@ TEST(JsonDestination, UpsertStaleSegmentGeneratesDebugMessage) { } TEST(JsonDestination, UpsertDeletedFlagCreatesTombstone) { - auto spy_logger = std::make_shared(); - Logger logger(spy_logger); + Logger const logger = logging::NullLogger(); NiceMock mock_dest; JsonDestination destination{logger, mock_dest}; @@ -207,8 +208,7 @@ TEST(JsonDestination, UpsertDeletedFlagCreatesTombstone) { } TEST(JsonDestination, UpsertDeletedSegmentCreatesTombstone) { - auto spy_logger = std::make_shared(); - Logger logger(spy_logger); + Logger const logger = logging::NullLogger(); NiceMock mock_dest; JsonDestination destination{logger, mock_dest}; @@ -222,3 +222,38 @@ TEST(JsonDestination, UpsertDeletedSegmentCreatesTombstone) { destination.Upsert("segment", data_model::SegmentDescriptor(2)); } + +TEST(JsonDestination, UpserFlagCreatesSerializedItem) { + Logger const logger = logging::NullLogger(); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, + Upsert(Ref(JsonDestination::Kinds::Flag), "flag", + SerializedItemDescriptor::Present( + 2, "{\"on\":true,\"key\":\"flag\",\"version\":2}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + destination.Upsert( + "flag", data_model::FlagDescriptor(data_model::Flag{"flag", 2, true})); +} + +TEST(JsonDestination, UpserSegmentCreatesSerializedItem) { + Logger const logger = logging::NullLogger(); + + NiceMock mock_dest; + JsonDestination destination{logger, mock_dest}; + + EXPECT_CALL(mock_dest, + Upsert(Ref(JsonDestination::Kinds::Segment), "segment", + SerializedItemDescriptor::Present( + 2, + "{\"key\":\"segment\",\"version\":2,\"excluded\":" + "[\"bar\"],\"included\":[\"foo\"]}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + destination.Upsert( + "segment", data_model::SegmentDescriptor( + data_model::Segment{"segment", 2, {"foo"}, {"bar"}})); +} From d904a77a901d4274bf33d3319d2b1f8f46eed34c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 13:55:13 -0800 Subject: [PATCH 147/244] add test fixture --- .../json_destination.hpp | 19 ++- .../tests/json_destination_test.cpp | 150 +++++------------- 2 files changed, 52 insertions(+), 117 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 9aa3ba554..6233d9895 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -13,15 +13,15 @@ namespace launchdarkly::server_side::data_components { * models into serialized data suitable for storage in an * ISerializedDestination. * - * It's purpose is to encapsulate the details of serialization in a reusable - * adapter. + * By encapsulating the serialization logic here, different adapters can be + * swapped in if our serialization format ever changes. * * JsonDestination does not currently initialize ISerializedDestination with a - * deterministic flag-dependency-order data order, which is required for some + * flag-dependency-order payload, which is required to minimize bugs in * stores without atomic transactions (e.g. DynamoDB). * - * Instead, it sorts items within a collection using '<' on item keys, giving - * enough determinism for testing purposes. + * Instead, it sorts items within a collection using 'operator<' on their keys, + * giving which is enough determinism for testing purposes. * * TODO(sc-225327): Implement topographic sort as prerequisite for DynamoDB. * @@ -29,9 +29,9 @@ namespace launchdarkly::server_side::data_components { class JsonDestination final : public data_interfaces::IDestination { public: /** - * @brief Construct the JsonDestination with the given destination. - * Calls to Upsert will trigger serialization and storage in the - * destination. + * @brief Construct the JsonDestination with the given + * ISerializedDestination. Calls to Upsert will trigger serialization and + * store to the destination. * @param logger Used for logging storage errors. * @param destination Where data should be forwarded. */ @@ -74,6 +74,9 @@ class JsonDestination final : public data_interfaces::IDestination { */ [[nodiscard]] std::string const& Identity() const override; + /** + * @brief These are public so they can be referenced in tests. + */ struct Kinds { static FlagKind const Flag; static SegmentKind const Segment; diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index 0f29ce092..ed2b26bce 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -30,34 +30,32 @@ class MockSerializedDestination : public ISerializedDestination { MOCK_METHOD(std::string const&, Identity, (), (const, override)); }; -TEST(JsonDestination, WrapsUnderlyingDestinationIdentity) { - Logger const logger = logging::NullLogger(); - +class JsonDestinationTest : public ::testing::Test { + public: + std::shared_ptr spy_logger; + Logger const logger; NiceMock mock_dest; - JsonDestination const destination{logger, mock_dest}; - - EXPECT_CALL(mock_dest, Identity()).WillOnce([]() { - return "FooCorp Database"; - }); + JsonDestination dest; + JsonDestinationTest() + : spy_logger(std::make_shared()), + logger(spy_logger), + dest(logger, mock_dest) { + ON_CALL(mock_dest, Identity()).WillByDefault([]() { + static const std::string name = "FooCorp Database"; + return name; + }); + } +}; - ASSERT_EQ(destination.Identity(), "FooCorp Database (JSON)"); +TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { + ASSERT_EQ(dest.Identity(), "FooCorp Database (JSON)"); } -TEST(JsonDestination, InitErrorGeneratesLogMessage) { - auto spy_logger = std::make_shared(); - Logger logger(spy_logger); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - - EXPECT_CALL(mock_dest, Identity()).WillOnce([] { - return "FooCorp Database"; - }); - +TEST_F(JsonDestinationTest, InitErrorGeneratesLogMessage) { EXPECT_CALL(mock_dest, Init) .WillOnce(Return(ISerializedDestination::InitResult::kError)); - destination.Init(data_model::SDKDataSet{}); + dest.Init(data_model::SDKDataSet{}); ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, "failed")); } @@ -67,12 +65,7 @@ TEST(JsonDestination, InitErrorGeneratesLogMessage) { // transformation from SDKDataSet into that argument is done correctly. The // transformation consists of two parts; first, serializing items to JSON, and // second, ordering those items by comparing keys with '<'. -TEST(JsonDestination, InitProperlyTransformsSDKDataSet) { - Logger const logger = logging::NullLogger(); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - +TEST_F(JsonDestinationTest, InitProperlyTransformsSDKDataSet) { EXPECT_CALL( mock_dest, Init(Eq(std::vector{ @@ -96,9 +89,9 @@ TEST(JsonDestination, InitProperlyTransformsSDKDataSet) { 2, "{\"key\":\"segment_beta\",\"version\":2}")}}}}}))) .WillOnce(Return(ISerializedDestination::InitResult::kSuccess)); - // Note: flag/segments are out of alphabetical order here to help verify - // they are sorted. - destination.Init(data_model::SDKDataSet{ + // Note: flag/segments are deliberately not in alphabetical order here, so + // that the implementation must sort them to pass the test. + dest.Init(data_model::SDKDataSet{ std::unordered_map{ {"flag_beta", data_model::FlagDescriptor(data_model::Flag{"flag_beta", 2})}, @@ -111,92 +104,47 @@ TEST(JsonDestination, InitProperlyTransformsSDKDataSet) { data_model::Segment{"segment_alpha", 1})}}}); } -TEST(JsonDestination, UpsertFlagErrorGeneratesErrorMessage) { - auto spy_logger = std::make_shared(); - Logger logger(spy_logger); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - - EXPECT_CALL(mock_dest, Identity()).WillOnce([] { - return "FooCorp Database"; - }); - +TEST_F(JsonDestinationTest, UpsertFlagErrorGeneratesErrorMessage) { EXPECT_CALL(mock_dest, Upsert) .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); - destination.Upsert("foo", data_model::FlagDescriptor(1)); + dest.Upsert("foo", data_model::FlagDescriptor(1)); ASSERT_TRUE( spy_logger->Contains(0, LogLevel::kError, "failed to update flag foo")); } -TEST(JsonDestination, UpsertSegmentErrorGeneratesErrorMessage) { - auto spy_logger = std::make_shared(); - Logger logger(spy_logger); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - - EXPECT_CALL(mock_dest, Identity()).WillOnce([] { - return "FooCorp Database"; - }); - +TEST_F(JsonDestinationTest, UpsertSegmentErrorGeneratesErrorMessage) { EXPECT_CALL(mock_dest, Upsert) .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); - destination.Upsert("foo", data_model::SegmentDescriptor(1)); + dest.Upsert("foo", data_model::SegmentDescriptor(1)); ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, "failed to update segment foo")); } -TEST(JsonDestination, UpsertStaleFlagGeneratesDebugMessage) { - auto spy_logger = std::make_shared(); - Logger logger(spy_logger); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - - EXPECT_CALL(mock_dest, Identity()).WillOnce([] { - return "FooCorp Database"; - }); - +TEST_F(JsonDestinationTest, UpsertStaleFlagGeneratesDebugMessage) { EXPECT_CALL(mock_dest, Upsert) .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); - destination.Upsert("foo", data_model::FlagDescriptor(1)); + dest.Upsert("foo", data_model::FlagDescriptor(1)); ASSERT_TRUE( spy_logger->Contains(0, LogLevel::kDebug, "flag foo not updated")); } -TEST(JsonDestination, UpsertStaleSegmentGeneratesDebugMessage) { - auto spy_logger = std::make_shared(); - Logger logger(spy_logger); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - - EXPECT_CALL(mock_dest, Identity()).WillOnce([] { - return "FooCorp Database"; - }); - +TEST_F(JsonDestinationTest, UpsertStaleSegmentGeneratesDebugMessage) { EXPECT_CALL(mock_dest, Upsert) .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); - destination.Upsert("foo", data_model::SegmentDescriptor(1)); + dest.Upsert("foo", data_model::SegmentDescriptor(1)); ASSERT_TRUE( spy_logger->Contains(0, LogLevel::kDebug, "segment foo not updated")); } -TEST(JsonDestination, UpsertDeletedFlagCreatesTombstone) { - Logger const logger = logging::NullLogger(); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - +TEST_F(JsonDestinationTest, UpsertDeletedFlagCreatesTombstone) { EXPECT_CALL( mock_dest, Upsert(Ref(JsonDestination::Kinds::Flag), "flag", @@ -204,15 +152,10 @@ TEST(JsonDestination, UpsertDeletedFlagCreatesTombstone) { 2, "{\"key\":\"flag\",\"version\":2,\"deleted\":true}"))) .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - destination.Upsert("flag", data_model::FlagDescriptor(2)); + dest.Upsert("flag", data_model::FlagDescriptor(2)); } -TEST(JsonDestination, UpsertDeletedSegmentCreatesTombstone) { - Logger const logger = logging::NullLogger(); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - +TEST_F(JsonDestinationTest, UpsertDeletedSegmentCreatesTombstone) { EXPECT_CALL( mock_dest, Upsert(Ref(JsonDestination::Kinds::Segment), "segment", @@ -220,31 +163,21 @@ TEST(JsonDestination, UpsertDeletedSegmentCreatesTombstone) { 2, "{\"key\":\"segment\",\"version\":2,\"deleted\":true}"))) .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - destination.Upsert("segment", data_model::SegmentDescriptor(2)); + dest.Upsert("segment", data_model::SegmentDescriptor(2)); } -TEST(JsonDestination, UpserFlagCreatesSerializedItem) { - Logger const logger = logging::NullLogger(); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - +TEST_F(JsonDestinationTest, UpserFlagCreatesSerializedItem) { EXPECT_CALL(mock_dest, Upsert(Ref(JsonDestination::Kinds::Flag), "flag", SerializedItemDescriptor::Present( 2, "{\"on\":true,\"key\":\"flag\",\"version\":2}"))) .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - destination.Upsert( - "flag", data_model::FlagDescriptor(data_model::Flag{"flag", 2, true})); + dest.Upsert("flag", + data_model::FlagDescriptor(data_model::Flag{"flag", 2, true})); } -TEST(JsonDestination, UpserSegmentCreatesSerializedItem) { - Logger const logger = logging::NullLogger(); - - NiceMock mock_dest; - JsonDestination destination{logger, mock_dest}; - +TEST_F(JsonDestinationTest, UpserSegmentCreatesSerializedItem) { EXPECT_CALL(mock_dest, Upsert(Ref(JsonDestination::Kinds::Segment), "segment", SerializedItemDescriptor::Present( @@ -253,7 +186,6 @@ TEST(JsonDestination, UpserSegmentCreatesSerializedItem) { "[\"bar\"],\"included\":[\"foo\"]}"))) .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - destination.Upsert( - "segment", data_model::SegmentDescriptor( - data_model::Segment{"segment", 2, {"foo"}, {"bar"}})); + dest.Upsert("segment", data_model::SegmentDescriptor(data_model::Segment{ + "segment", 2, {"foo"}, {"bar"}})); } From 055f40aabf833d51cb23b368b29df91dad757d91 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 13:56:17 -0800 Subject: [PATCH 148/244] typo --- libs/server-sdk/tests/json_destination_test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index ed2b26bce..ae5aa3f56 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -166,7 +166,7 @@ TEST_F(JsonDestinationTest, UpsertDeletedSegmentCreatesTombstone) { dest.Upsert("segment", data_model::SegmentDescriptor(2)); } -TEST_F(JsonDestinationTest, UpserFlagCreatesSerializedItem) { +TEST_F(JsonDestinationTest, UpsertFlagCreatesSerializedItem) { EXPECT_CALL(mock_dest, Upsert(Ref(JsonDestination::Kinds::Flag), "flag", SerializedItemDescriptor::Present( @@ -177,7 +177,7 @@ TEST_F(JsonDestinationTest, UpserFlagCreatesSerializedItem) { data_model::FlagDescriptor(data_model::Flag{"flag", 2, true})); } -TEST_F(JsonDestinationTest, UpserSegmentCreatesSerializedItem) { +TEST_F(JsonDestinationTest, UpsertSegmentCreatesSerializedItem) { EXPECT_CALL(mock_dest, Upsert(Ref(JsonDestination::Kinds::Segment), "segment", SerializedItemDescriptor::Present( From 4b8867323a0815f018c011c56f9af682b8b401a7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 14:24:11 -0800 Subject: [PATCH 149/244] update flag serialization test in internal --- .../serialization/value_mapping.hpp | 10 +++++++ libs/internal/src/serialization/json_flag.cpp | 27 ++++++++++++------- .../src/serialization/value_mapping.cpp | 1 + .../tests/data_model_serialization_test.cpp | 12 ++++++--- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp index db584f493..9f26369b6 100644 --- a/libs/internal/include/launchdarkly/serialization/value_mapping.hpp +++ b/libs/internal/include/launchdarkly/serialization/value_mapping.hpp @@ -175,6 +175,16 @@ void WriteMinimal(boost::json::object& obj, } } +template +void WriteMinimal(boost::json::object& obj, + std::string const& key, + T const& val, + std::function const& predicate) { + if (predicate()) { + obj.emplace(key, boost::json::value_from(val)); + } +} + void WriteMinimal(boost::json::object& obj, std::string const& key, // No copy when not used. bool val); diff --git a/libs/internal/src/serialization/json_flag.cpp b/libs/internal/src/serialization/json_flag.cpp index 007184538..f654abdbf 100644 --- a/libs/internal/src/serialization/json_flag.cpp +++ b/libs/internal/src/serialization/json_flag.cpp @@ -352,15 +352,24 @@ void tag_invoke(boost::json::value_from_tag const& unused, WriteMinimal(obj, "variations", flag.variations); WriteMinimal(obj, "rules", flag.rules); WriteMinimal(obj, "prerequisites", flag.prerequisites); - if (auto fallthrough = boost::json::value_from(flag.fallthrough); - !fallthrough.as_object().empty()) { - obj.emplace("fallthrough", fallthrough); - } - if (flag.clientSideAvailability.usingEnvironmentId || - flag.clientSideAvailability.usingMobileKey) { - obj.emplace("clientSideAvailability", - boost::json::value_from(flag.clientSideAvailability)); - } + WriteMinimal(obj, "fallthrough", flag.fallthrough, [&]() { + return std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v>) { + return arg.has_value(); + } else if constexpr (std::is_same_v) { + return true; + } + }, + flag.fallthrough); + }); + WriteMinimal(obj, "clientSideAvailability", flag.clientSideAvailability, + [&]() { + return flag.clientSideAvailability.usingEnvironmentId || + flag.clientSideAvailability.usingMobileKey; + }); WriteMinimal(obj, "contextTargets", flag.contextTargets); std::vector user_targets; diff --git a/libs/internal/src/serialization/value_mapping.cpp b/libs/internal/src/serialization/value_mapping.cpp index 7142d8bf7..bcf385879 100644 --- a/libs/internal/src/serialization/value_mapping.cpp +++ b/libs/internal/src/serialization/value_mapping.cpp @@ -75,4 +75,5 @@ void WriteMinimal(boost::json::object& obj, std::string const& key, bool 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 c8965e16d..48d47b50c 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -600,9 +600,13 @@ TEST(FlagTests, SerializeAll) { }, 24, ContextKind("bob")}}, // contextTargets - {}, // rules - 84, // offVariation - true, // clientSide + {data_model::Flag::Rule{{data_model::Clause{data_model::Clause::Op::kIn, + {"bob"}, + false, + ContextKind{"user"}, + "name"}}}}, // rules + 84, // offVariation + true, // clientSide data_model::Flag::ClientSideAvailability{true, true}, "4242", // salt true, // trackEvents @@ -623,7 +627,7 @@ TEST(FlagTests, SerializeAll) { "key":"the-key", "version":21, "variations":["a","b"], - "rules":[], + "rules":[{"clauses":[{"op":"in","values":["bob"],"contextKind":"user","attribute":"name"}]}], "prerequisites":[{"key":"prereqA","variation":2}, {"key":"prereqB","variation":3}], "fallthrough":{"variation":42}, From f00bf074ec8687265474b2870252b0e3b9dafd82 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 14:34:05 -0800 Subject: [PATCH 150/244] update unbounded segment serialization test --- libs/internal/tests/data_model_serialization_test.cpp | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libs/internal/tests/data_model_serialization_test.cpp b/libs/internal/tests/data_model_serialization_test.cpp index 48d47b50c..416786409 100644 --- a/libs/internal/tests/data_model_serialization_test.cpp +++ b/libs/internal/tests/data_model_serialization_test.cpp @@ -743,12 +743,7 @@ TEST(SegmentTests, SerializeUnbounded) { R"({ "key": "my-segment", "version": 87, - "included": [], - "excluded": [], - "includedContexts": [], - "excludedContexts": [], "salt": "salty", - "rules":[], "unbounded": true, "unboundedContextKind": "company", "generation": 12 From 3ec583972b471a16c918d7893fe4b1336a5ef0e4 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 15:30:59 -0800 Subject: [PATCH 151/244] add lazy load system unit tests --- .../lazy_load/lazy_load_system.cpp | 18 ++- .../lazy_load/lazy_load_system.hpp | 9 +- .../tests/lazy_load_system_test.cpp | 139 ++++++++++++++++++ 3 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 libs/server-sdk/tests/lazy_load_system_test.cpp diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 4aa53fb23..fe65858da 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -4,10 +4,14 @@ namespace launchdarkly::server_side::data_systems { -LazyLoad::LazyLoad() - : reader_(std::make_unique( - *serialized_reader_)), - time_([]() { return std::chrono::steady_clock::now(); }) {} +LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg) + : LazyLoad(std::move(cfg), + []() { return std::chrono::steady_clock::now(); }) {} + +LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg, TimeFn time) + : serialized_reader_(std::move(cfg.source)), + reader_(std::make_unique(*serialized_reader_)), + time_(std::move(time)) {} std::string const& LazyLoad::Identity() const { static std::string id = "lazy load via " + reader_->Identity(); @@ -20,7 +24,8 @@ void LazyLoad::Shutdown() {} std::shared_ptr LazyLoad::GetFlag( std::string const& key) const { - auto const state = tracker_.State(Keys::kAllSegments, time_()); + auto const state = + tracker_.State(data_components::DataKind::kFlag, key, time_()); return Get>( state, [this, &key]() { RefreshFlag(key); }, [this, &key]() { return cache_.GetFlag(key); }); @@ -28,7 +33,8 @@ std::shared_ptr LazyLoad::GetFlag( std::shared_ptr LazyLoad::GetSegment( std::string const& key) const { - auto const state = tracker_.State(Keys::kAllSegments, time_()); + auto const state = + tracker_.State(data_components::DataKind::kSegment, key, time_()); return Get>( state, [this, &key]() { RefreshSegment(key); }, [this, &key]() { return cache_.GetSegment(key); }); diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 825a210ad..c54f22b0c 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -6,6 +6,7 @@ #include "../../data_interfaces/source/idata_reader.hpp" #include "../../data_interfaces/system/idata_system.hpp" +#include #include #include @@ -26,7 +27,11 @@ namespace launchdarkly::server_side::data_systems { */ class LazyLoad final : public data_interfaces::IDataSystem { public: - LazyLoad(); + using TimeFn = + std::function()>; + + explicit LazyLoad(config::built::LazyLoadConfig cfg); + LazyLoad(config::built::LazyLoadConfig cfg, TimeFn time); std::string const& Identity() const override; @@ -79,7 +84,7 @@ class LazyLoad final : public data_interfaces::IDataSystem { std::unique_ptr reader_; mutable data_components::ExpirationTracker tracker_; - std::function()> time_; + TimeFn time_; mutable std::optional initialized_; struct Kinds { diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp new file mode 100644 index 000000000..eb88ebc2b --- /dev/null +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -0,0 +1,139 @@ +#include +#include + +#include "data_systems/lazy_load/lazy_load_system.hpp" + +#include +#include + +using namespace launchdarkly::server_side; +using namespace launchdarkly::server_side::config; + +using ::testing::NiceMock; + +class MockDataReader : public data_interfaces::ISerializedDataReader { + public: + MOCK_METHOD(GetResult, + Get, + (integrations::ISerializedItemKind const& kind, + std::string const& itemKey), + (override, const)); + MOCK_METHOD(AllResult, + All, + (integrations::ISerializedItemKind const& kind), + (override, const)); + MOCK_METHOD(std::string const&, Identity, (), (override, const)); + MOCK_METHOD(bool, Initialized, (), (override, const)); +}; + +class LazyLoadTest : public ::testing::Test { + public: + std::string mock_reader_name; + std::shared_ptr> mock_reader; + LazyLoadTest() + : mock_reader_name("fake reader"), + mock_reader(std::make_shared>()) { + ON_CALL(*mock_reader, Identity()).WillByDefault([&]() { + return mock_reader_name; + }); + } +}; + +TEST_F(LazyLoadTest, ItentityWrapsReaderIdentity) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::milliseconds(100), mock_reader}; + + data_systems::LazyLoad const lazy_load(config); + + ASSERT_EQ(lazy_load.Identity(), "lazy load via " + mock_reader_name); +} + +TEST_F(LazyLoadTest, SourceIsNotAccessedIfFetchFails) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + data_systems::LazyLoad const lazy_load(config); + + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 1, false, "{\"key\":\"foo\",\"version\":1}"})); + + // Although we ask for 'foo' 10 times, the underlying source should only + // receive one call because the refresh is 10 seconds. Only after 10 seconds + // elapse would the source be queried again. + for (std::size_t i = 0; i < 10; i++) { + ASSERT_FALSE(lazy_load.GetFlag("foo")); + } +} + +// TEST_F(LazyLoadTest, FetchingAllSegmentsRefreshesIndividualSegments) { +// auto source = std::make_shared(); +// +// auto refresh = std::chrono::seconds(10); +// +// built::LazyLoadConfig const config{ +// built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; +// +// data_systems::LazyLoad const lazy_load(config); +// +// ASSERT_FALSE(lazy_load.AllSegments().empty()); +// ASSERT_TRUE(lazy_load.GetSegment("foo")); +// +// // Since all segments were requested, then.. +// ASSERT_EQ(source->all_requested, std::vector{"segments"}); +// // There should be no individual request for a segment. +// ASSERT_TRUE(source->items_requested["segments"].empty()); +// } +// +// TEST_F(LazyLoadTest, FetchingAllFlagsRefreshesIndividualFlags) { +// auto source = std::make_shared(); +// +// auto refresh = std::chrono::seconds(10); +// +// built::LazyLoadConfig const config{ +// built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; +// +// data_systems::LazyLoad const lazy_load(config); +// +// ASSERT_FALSE(lazy_load.AllFlags().empty()); +// ASSERT_TRUE(lazy_load.GetFlag("foo")); +// +// // Since all flags were requested, then.. +// ASSERT_EQ(source->all_requested, std::vector{"features"}); +// // There should be no individual request for a segment. +// ASSERT_TRUE(source->items_requested["features"].empty()); +// } +// +// TEST_F(LazyLoadTest, ItemIsRefreshedAfterDelay) { +// auto source = std::make_shared(); +// +// auto refresh = std::chrono::seconds(10); +// +// built::LazyLoadConfig const config{ +// built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; +// +// std::chrono::time_point now( +// std::chrono::seconds(0)); +// +// data_systems::LazyLoad const lazy_load(config, [&]() { return now; }); +// +// // Simulate time moving forward for 9 seconds. Each time, the flag should +// be +// // served from the cache rather than quering the store. +// for (std::size_t i = 0; i < 9; i++) { +// now = std::chrono::time_point( +// std::chrono::seconds(i)); +// ASSERT_TRUE(lazy_load.GetFlag("foo")); +// ASSERT_EQ(source->items_requested["features"], +// std::vector{"foo"}); +// } +// +// // Advance past the refresh time. Now the flag should be queried again. +// now = std::chrono::time_point(refresh); +// +// ASSERT_TRUE(lazy_load.GetFlag("foo")); +// auto expected = std::vector{"foo", "foo"}; +// ASSERT_EQ(source->items_requested["features"], expected); +// } From 85991f4636fb39634ae2eb6e5970cdec2b8f0261 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 19:05:10 -0800 Subject: [PATCH 152/244] fix: allow items in expiration tracker to be updated --- .../expiration_tracker/expiration_tracker.cpp | 7 ++++--- libs/server-sdk/tests/expiration_tracker_test.cpp | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp index 8ce76e4d7..64ff189fe 100644 --- a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.cpp @@ -5,7 +5,7 @@ namespace launchdarkly::server_side::data_components { void ExpirationTracker::Add(std::string const& key, TimePoint expiration) { - unscoped_.insert({key, expiration}); + unscoped_.insert_or_assign(key, expiration); } void ExpirationTracker::Remove(std::string const& key) { @@ -87,8 +87,9 @@ ExpirationTracker::TrackState ExpirationTracker::State(TimePoint expiration, void ExpirationTracker::ScopedTtls::Set(DataKind kind, std::string const& key, TimePoint expiration) { - data_[static_cast>(kind)].Data().insert( - {key, expiration}); + data_[static_cast>(kind)] + .Data() + .insert_or_assign(key, expiration); } void ExpirationTracker::ScopedTtls::Remove(DataKind kind, diff --git a/libs/server-sdk/tests/expiration_tracker_test.cpp b/libs/server-sdk/tests/expiration_tracker_test.cpp index 15b6163ef..92c4f6071 100644 --- a/libs/server-sdk/tests/expiration_tracker_test.cpp +++ b/libs/server-sdk/tests/expiration_tracker_test.cpp @@ -119,3 +119,17 @@ TEST(ExpirationTrackerTest, CanPrune) { EXPECT_EQ(ExpirationTracker::TrackState::kFresh, tracker.State(DataKind::kSegment, "freshSegment", Second(80))); } + +TEST(ExpirationTrackerTest, CanUpdateExistingExpiry) { + ExpirationTracker tracker; + + for (std::size_t seconds = 1; seconds < 10; seconds++) { + auto const now = Second(seconds - 1); + auto const expiry = Second(seconds); + + tracker.Add("Potato", expiry); + EXPECT_EQ(ExpirationTracker::TrackState::kFresh, + tracker.State("Potato", now)); + ASSERT_TRUE(tracker.Prune(now).empty()); + } +} From d39703a02515f113df7689ab1376ffd492ae0cc6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 19:06:24 -0800 Subject: [PATCH 153/244] add back Initialize to IDataReader --- .../sources/iserialized_data_reader.hpp | 10 +++++++ .../json_deserializer.cpp | 10 +++++-- .../json_deserializer.hpp | 11 +++++-- .../data_interfaces/source/idata_reader.hpp | 29 +++++++++++-------- 4 files changed, 42 insertions(+), 18 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp b/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp index 8b193ded8..eefdea900 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp @@ -71,8 +71,18 @@ class ISerializedDataReader { virtual AllResult All( integrations::ISerializedItemKind const& kind) const = 0; + /** + * @return Identity of the reader. Used in logs. + */ virtual std::string const& Identity() const = 0; + /** + * @return True if the reader has data that can be queried. The reader + * should derive this state externally; that is, it should be an attribute + * of the underlying source of data (not in memory.) A possible + * implementation would be to store a special data key that is only set + * after initial SDK data is stored. + */ virtual bool Initialized() const = 0; protected: diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp index 0c548aaf7..e3fbb49a6 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp @@ -10,8 +10,8 @@ namespace launchdarkly::server_side::data_components { JsonDeserializer::JsonDeserializer( - data_interfaces::ISerializedDataReader& reader) - : flag_kind_(), segment_kind_(), reader_(reader) {} + std::shared_ptr reader) + : flag_kind_(), segment_kind_(), reader_(std::move(reader)) {} data_interfaces::IDataReader::Single JsonDeserializer::GetFlag(std::string const& key) const { @@ -36,7 +36,11 @@ JsonDeserializer::AllSegments() const { } std::string const& JsonDeserializer::Identity() const { - return reader_.Identity(); + return reader_->Identity(); +} + +bool JsonDeserializer::Initialized() const { + return reader_->Initialized(); } } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index 82f1a81c0..be723c0cc 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -5,11 +5,14 @@ #include +#include + namespace launchdarkly::server_side::data_components { class JsonDeserializer final : public data_interfaces::IDataReader { public: - explicit JsonDeserializer(data_interfaces::ISerializedDataReader& reader); + explicit JsonDeserializer( + std::shared_ptr reader); [[nodiscard]] Single GetFlag( std::string const& key) const override; @@ -25,12 +28,14 @@ class JsonDeserializer final : public data_interfaces::IDataReader { [[nodiscard]] std::string const& Identity() const override; + [[nodiscard]] bool Initialized() const override; + private: template Single> Deserialize( DataKind const& kind, std::string const& key) const { - auto result = reader_.Get(kind, key); + auto result = reader_->Get(kind, key); if (!result) { /* the actual fetch failed */ @@ -65,7 +70,7 @@ class JsonDeserializer final : public data_interfaces::IDataReader { FlagKind const flag_kind_; FlagKind const segment_kind_; - data_interfaces::ISerializedDataReader& reader_; + std::shared_ptr reader_; }; } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp b/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp index 8829daddb..adcc7e38f 100644 --- a/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp +++ b/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp @@ -11,7 +11,7 @@ namespace launchdarkly::server_side::data_interfaces { /** - * \brief IDataReader obtains data on-demand. Calls to obtain data may fail, so + * @brief IDataReader obtains data on-demand. Calls to obtain data may fail, so * the getter methods use tl::expected in order to propagate error codes. * * The IDataReader does not perform caching, so parent components must be @@ -29,44 +29,49 @@ class IDataReader { using Collection = tl::expected, Error>; /** - * \brief Attempts to get a flag named by key. - * \param key Key of the flag. - * \return On success, an optional FlagDescriptor (std::nullopt means the + * @brief Attempts to get a flag named by key. + * @param key Key of the flag. + * @return On success, an optional FlagDescriptor (std::nullopt means the * flag doesn't exist.) On failure, an error string. */ [[nodiscard]] virtual Single GetFlag( std::string const& key) const = 0; /** - * \brief Attempts to get a segment named by key. - * \param key Key of the segment. - * \return On success, an optional SegmentDescriptor (std::nullopt means the + * @brief Attempts to get a segment named by key. + * @param key Key of the segment. + * @return On success, an optional SegmentDescriptor (std::nullopt means the * segment doesn't exist.) On failure, an error string. */ [[nodiscard]] virtual Single GetSegment( std::string const& key) const = 0; /** - * \brief Attempts to get a collection of all flags. - * \return On success, a collection of FlagDescriptors. On failure, an error + * @brief Attempts to get a collection of all flags. + * @return On success, a collection of FlagDescriptors. On failure, an error * string. */ [[nodiscard]] virtual Collection AllFlags() const = 0; /** - * \brief Attempts to get a collection of all segments. - * \return On success, a collection of SegmentDescriptors. On failure, an + * @brief Attempts to get a collection of all segments. + * @return On success, a collection of SegmentDescriptors. On failure, an * error string. */ [[nodiscard]] virtual Collection AllSegments() const = 0; /** - * \return Identity of the source. Used in logs. + * @return Identity of the reader. Used in logs. */ [[nodiscard]] virtual std::string const& Identity() const = 0; + /** + * @return Whether the reader is initialized. + */ + [[nodiscard]] virtual bool Initialized() const = 0; + virtual ~IDataReader() = default; IDataReader(IDataReader const& item) = delete; IDataReader(IDataReader&& item) = delete; From 5f16bfeb7a1d7672e175ab4bf9fd18793553d953 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 27 Nov 2023 19:06:36 -0800 Subject: [PATCH 154/244] update lazy load tests --- .../lazy_load/lazy_load_system.cpp | 91 +++++-- .../lazy_load/lazy_load_system.hpp | 17 +- .../tests/lazy_load_system_test.cpp | 252 ++++++++++++------ 3 files changed, 252 insertions(+), 108 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index fe65858da..f9ad3726b 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -4,14 +4,18 @@ namespace launchdarkly::server_side::data_systems { -LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg) - : LazyLoad(std::move(cfg), - []() { return std::chrono::steady_clock::now(); }) {} - -LazyLoad::LazyLoad(config::built::LazyLoadConfig cfg, TimeFn time) - : serialized_reader_(std::move(cfg.source)), - reader_(std::make_unique(*serialized_reader_)), - time_(std::move(time)) {} +LazyLoad::LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg) + : LazyLoad(logger, std::move(cfg), []() { + return std::chrono::steady_clock::now(); + }) {} + +LazyLoad::LazyLoad(Logger const& logger, + config::built::LazyLoadConfig cfg, + TimeFn time) + : logger_(logger), + reader_(std::make_unique(cfg.source)), + time_(std::move(time)), + fresh_duration_(cfg.refresh_ttl) {} std::string const& LazyLoad::Identity() const { static std::string id = "lazy load via " + reader_->Identity(); @@ -67,11 +71,14 @@ data_components::SegmentKind const LazyLoad::Kinds::Segment = bool LazyLoad::Initialized() const { auto const state = tracker_.State(Keys::kInitialized, time_()); if (initialized_.has_value()) { + /* Once initialized, we can always return true. */ if (initialized_.value()) { return true; } + /* If not yet initialized, then we can return false only if the state is + * fresh - otherwise we should make an attempt to refresh. */ if (data_components::ExpirationTracker::TrackState::kFresh == state) { - return initialized_.value(); + return false; } } RefreshInitState(); @@ -79,51 +86,85 @@ bool LazyLoad::Initialized() const { } void LazyLoad::RefreshAllFlags() const { - auto maybe_flags = reader_->AllFlags(); - // TODO: log failure? - if (maybe_flags) { - for (auto flag : *maybe_flags) { + auto const updated_expiry = ExpiryTime(); + if (auto all_flags = reader_->AllFlags()) { + for (auto flag : *all_flags) { cache_.Upsert(flag.first, std::move(flag.second)); + tracker_.Add(data_components::DataKind::kSegment, flag.first, + updated_expiry); } - tracker_.Add(Keys::kAllFlags, time_()); + } else { + LD_LOG(logger_, LogLevel::kError) + << "failed to refresh all flags via " << reader_->Identity() + << all_flags.error(); } + tracker_.Add(Keys::kAllFlags, updated_expiry); } void LazyLoad::RefreshAllSegments() const { - auto maybe_segments = reader_->AllSegments(); - // TODO: log failure? - if (maybe_segments) { - for (auto seg : *maybe_segments) { - cache_.Upsert(seg.first, std::move(seg.second)); + auto const updated_expiry = ExpiryTime(); + if (auto all_segments = reader_->AllSegments()) { + for (auto segment : *all_segments) { + cache_.Upsert(segment.first, std::move(segment.second)); + tracker_.Add(data_components::DataKind::kSegment, segment.first, + updated_expiry); } - tracker_.Add(Keys::kAllSegments, time_()); + } else { + LD_LOG(logger_, LogLevel::kError) + << "failed to refresh all segments via " << reader_->Identity() + << all_segments.error(); } + tracker_.Add(Keys::kAllSegments, updated_expiry); } void LazyLoad::RefreshInitState() const { - // TODO: what does this matter? - // initialized_ = source_.Initialized(); - tracker_.Add(Keys::kInitialized, time_()); + initialized_ = reader_->Initialized(); + tracker_.Add(Keys::kInitialized, ExpiryTime()); } void LazyLoad::RefreshSegment(std::string const& key) const { if (auto segment_result = reader_->GetSegment(key)) { if (auto optional_segment = *segment_result) { cache_.Upsert(key, std::move(*optional_segment)); + } else { + LD_LOG(logger_, LogLevel::kDebug) + << "segment " << key << " requested but not found via " + << reader_->Identity() + << ", this is probably a temporary issue"; } - tracker_.Add(data_components::DataKind::kSegment, key, time_()); + } else { + LD_LOG(logger_, LogLevel::kError) + << "failed to refresh segment " << key << " via " + << reader_->Identity() << ": " << segment_result.error(); } // TODO: If there is an actual error, then do we not reset the tracking? + tracker_.Add(data_components::DataKind::kSegment, key, ExpiryTime()); } void LazyLoad::RefreshFlag(std::string const& key) const { if (auto flag_result = reader_->GetFlag(key)) { if (auto optional_flag = *flag_result) { cache_.Upsert(key, std::move(*optional_flag)); + } else { + LD_LOG(logger_, LogLevel::kDebug) + << "flag " << key << " requested but not found via " + << reader_->Identity() + << ", this is probably a temporary issue"; } - tracker_.Add(data_components::DataKind::kFlag, key, time_()); + } else { + LD_LOG(logger_, LogLevel::kError) + << "failed to refresh flag " << key << " via " + << reader_->Identity() << ": " << flag_result.error(); } // TODO: If there is an actual error, then do we not reset the tracking? + tracker_.Add(data_components::DataKind::kFlag, key, ExpiryTime()); +} + +std::chrono::time_point LazyLoad::ExpiryTime() + const { + return time_() + + std::chrono::duration_cast( + fresh_duration_); } } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index c54f22b0c..dc4cd2132 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -27,11 +27,13 @@ namespace launchdarkly::server_side::data_systems { */ class LazyLoad final : public data_interfaces::IDataSystem { public: - using TimeFn = - std::function()>; + using ClockType = std::chrono::steady_clock; + using TimeFn = std::function()>; - explicit LazyLoad(config::built::LazyLoadConfig cfg); - LazyLoad(config::built::LazyLoadConfig cfg, TimeFn time); + explicit LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg); + LazyLoad(Logger const& logger, + config::built::LazyLoadConfig cfg, + TimeFn time); std::string const& Identity() const override; @@ -79,14 +81,19 @@ class LazyLoad final : public data_interfaces::IDataSystem { detail::unreachable(); } + ClockType::time_point ExpiryTime() const; + + Logger const& logger_; + mutable data_components::MemoryStore cache_; - std::shared_ptr serialized_reader_; std::unique_ptr reader_; mutable data_components::ExpirationTracker tracker_; TimeFn time_; mutable std::optional initialized_; + ClockType::duration fresh_duration_; + struct Kinds { static data_components::FlagKind const Flag; static data_components::SegmentKind const Segment; diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index eb88ebc2b..e8cc2c387 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -6,9 +6,13 @@ #include #include +#include "spy_logger.hpp" + +using namespace launchdarkly; using namespace launchdarkly::server_side; using namespace launchdarkly::server_side::config; +using ::testing::InSequence; using ::testing::NiceMock; class MockDataReader : public data_interfaces::ISerializedDataReader { @@ -30,110 +34,202 @@ class LazyLoadTest : public ::testing::Test { public: std::string mock_reader_name; std::shared_ptr> mock_reader; + std::shared_ptr spy_logger_backend; + Logger const logger; LazyLoadTest() : mock_reader_name("fake reader"), - mock_reader(std::make_shared>()) { + mock_reader(std::make_shared>()), + spy_logger_backend(std::make_shared()), + logger(spy_logger_backend) { ON_CALL(*mock_reader, Identity()).WillByDefault([&]() { return mock_reader_name; }); } }; -TEST_F(LazyLoadTest, ItentityWrapsReaderIdentity) { +TEST_F(LazyLoadTest, IdentityWrapsReaderIdentity) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::milliseconds(100), mock_reader}; - data_systems::LazyLoad const lazy_load(config); + data_systems::LazyLoad const lazy_load(logger, config); ASSERT_EQ(lazy_load.Identity(), "lazy load via " + mock_reader_name); } -TEST_F(LazyLoadTest, SourceIsNotAccessedIfFetchFails) { +TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfFlagIsCached) { built::LazyLoadConfig const config{ built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - data_systems::LazyLoad const lazy_load(config); + data_systems::LazyLoad const lazy_load(logger, config); EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); - // Although we ask for 'foo' 10 times, the underlying source should only - // receive one call because the refresh is 10 seconds. Only after 10 seconds - // elapse would the source be queried again. - for (std::size_t i = 0; i < 10; i++) { + for (std::size_t i = 0; i < 20; i++) { + ASSERT_TRUE(lazy_load.GetFlag("foo")); + } +} + +TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfSegmentIsCached) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + data_systems::LazyLoad const lazy_load(logger, config); + + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 1, false, "{\"key\":\"foo\",\"version\":1}"})); + + for (std::size_t i = 0; i < 20; i++) { + ASSERT_TRUE(lazy_load.GetSegment("foo")); + } +} + +TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfFlagCannotBeFetched) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + data_systems::LazyLoad const lazy_load(logger, config); + + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(tl::make_unexpected( + data_interfaces::ISerializedDataReader::Error{"oops"}))); + + for (std::size_t i = 0; i < 20; i++) { ASSERT_FALSE(lazy_load.GetFlag("foo")); + }; + + ASSERT_TRUE(spy_logger_backend->Count(1)); + ASSERT_TRUE(spy_logger_backend->Contains(0, LogLevel::kError, "oops")); +} + +TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfSegmentCannotBeFetched) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + data_systems::LazyLoad const lazy_load(logger, config); + + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(tl::make_unexpected( + data_interfaces::ISerializedDataReader::Error{"oops"}))); + + for (std::size_t i = 0; i < 20; i++) { + ASSERT_FALSE(lazy_load.GetSegment("foo")); + }; + + ASSERT_TRUE(spy_logger_backend->Count(1)); + ASSERT_TRUE(spy_logger_backend->Contains(0, LogLevel::kError, "oops")); +} + +TEST_F(LazyLoadTest, RefreshesFlagIfStale) { + using TimePoint = data_systems::LazyLoad::ClockType::time_point; + + constexpr auto refresh_ttl = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh_ttl, + mock_reader}; + + TimePoint now{std::chrono::seconds(0)}; + + data_systems::LazyLoad const lazy_load(logger, config, + [&]() { return now; }); + + { + InSequence s; + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 1, false, "{\"key\":\"foo\",\"version\":1}"})); + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 2, false, "{\"key\":\"foo\",\"version\":2}"})); + } + + for (std::size_t i = 0; i < 10; i++) { + auto flag = lazy_load.GetFlag("foo"); + ASSERT_TRUE(flag); + ASSERT_EQ(flag->version, 1); + } + + now = TimePoint{refresh_ttl + std::chrono::seconds(1)}; + + for (std::size_t i = 0; i < 10; i++) { + auto flag = lazy_load.GetFlag("foo"); + ASSERT_TRUE(flag); + ASSERT_EQ(flag->version, 2); } } -// TEST_F(LazyLoadTest, FetchingAllSegmentsRefreshesIndividualSegments) { -// auto source = std::make_shared(); -// -// auto refresh = std::chrono::seconds(10); -// -// built::LazyLoadConfig const config{ -// built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; -// -// data_systems::LazyLoad const lazy_load(config); -// -// ASSERT_FALSE(lazy_load.AllSegments().empty()); -// ASSERT_TRUE(lazy_load.GetSegment("foo")); -// -// // Since all segments were requested, then.. -// ASSERT_EQ(source->all_requested, std::vector{"segments"}); -// // There should be no individual request for a segment. -// ASSERT_TRUE(source->items_requested["segments"].empty()); -// } -// -// TEST_F(LazyLoadTest, FetchingAllFlagsRefreshesIndividualFlags) { -// auto source = std::make_shared(); -// -// auto refresh = std::chrono::seconds(10); -// -// built::LazyLoadConfig const config{ -// built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; -// -// data_systems::LazyLoad const lazy_load(config); -// -// ASSERT_FALSE(lazy_load.AllFlags().empty()); -// ASSERT_TRUE(lazy_load.GetFlag("foo")); -// -// // Since all flags were requested, then.. -// ASSERT_EQ(source->all_requested, std::vector{"features"}); -// // There should be no individual request for a segment. -// ASSERT_TRUE(source->items_requested["features"].empty()); -// } -// -// TEST_F(LazyLoadTest, ItemIsRefreshedAfterDelay) { -// auto source = std::make_shared(); -// -// auto refresh = std::chrono::seconds(10); -// -// built::LazyLoadConfig const config{ -// built::LazyLoadConfig::EvictionPolicy::Disabled, refresh, source}; -// -// std::chrono::time_point now( -// std::chrono::seconds(0)); -// -// data_systems::LazyLoad const lazy_load(config, [&]() { return now; }); -// -// // Simulate time moving forward for 9 seconds. Each time, the flag should -// be -// // served from the cache rather than quering the store. -// for (std::size_t i = 0; i < 9; i++) { -// now = std::chrono::time_point( -// std::chrono::seconds(i)); -// ASSERT_TRUE(lazy_load.GetFlag("foo")); -// ASSERT_EQ(source->items_requested["features"], -// std::vector{"foo"}); -// } -// -// // Advance past the refresh time. Now the flag should be queried again. -// now = std::chrono::time_point(refresh); -// -// ASSERT_TRUE(lazy_load.GetFlag("foo")); -// auto expected = std::vector{"foo", "foo"}; -// ASSERT_EQ(source->items_requested["features"], expected); -// } +TEST_F(LazyLoadTest, RefreshesSegmentIfStale) { + using TimePoint = data_systems::LazyLoad::ClockType::time_point; + + constexpr auto refresh_ttl = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh_ttl, + mock_reader}; + + TimePoint now{std::chrono::seconds(0)}; + + data_systems::LazyLoad const lazy_load(logger, config, + [&]() { return now; }); + + { + InSequence s; + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 1, false, "{\"key\":\"foo\",\"version\":1}"})); + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 2, false, "{\"key\":\"foo\",\"version\":2}"})); + } + + for (std::size_t i = 0; i < 10; i++) { + auto segment = lazy_load.GetSegment("foo"); + ASSERT_TRUE(segment); + ASSERT_EQ(segment->version, 1); + } + + now = TimePoint{refresh_ttl + std::chrono::seconds(1)}; + + for (std::size_t i = 0; i < 10; i++) { + auto segment = lazy_load.GetSegment("foo"); + ASSERT_TRUE(segment); + ASSERT_EQ(segment->version, 2); + } +} + +TEST_F(LazyLoadTest, AllFlagsRefreshesIndividualFlagExpiration) { + using TimePoint = data_systems::LazyLoad::ClockType::time_point; + + constexpr auto refresh_ttl = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh_ttl, + mock_reader}; + + // We want to demonstrate that the individual TTL of a flag will be + // refreshed not just when we grab that single flag, but also if + // we call AllFlags. So we'll have a situation where a single + // flag is fetched, and then is about to expire. We'll then call AllFlags + // and step the time forward, and ensure the flag is updated in memory - without + // actually calling GetFlag on the reader. + + { + InSequence s; + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 1, false, "{\"key\":\"foo\",\"version\":1}"})); + EXPECT_CALL(*mock_reader, All(testing::_)) + .WillOnce(testing::Return(std::unordered_map{ + {"foo", {2, false, "{\"key\":\"foo\",\"version\":2}"}}})); + } + + +} From 4549146c1cb4d021365fb9c82d213f76d861aaff Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 09:40:42 -0800 Subject: [PATCH 155/244] update lazy load behavior based on code review --- .../lazy_load/lazy_load_system.cpp | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index f9ad3726b..4a1fb6dab 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -90,12 +90,12 @@ void LazyLoad::RefreshAllFlags() const { if (auto all_flags = reader_->AllFlags()) { for (auto flag : *all_flags) { cache_.Upsert(flag.first, std::move(flag.second)); - tracker_.Add(data_components::DataKind::kSegment, flag.first, + tracker_.Add(data_components::DataKind::kFlag, flag.first, updated_expiry); } } else { LD_LOG(logger_, LogLevel::kError) - << "failed to refresh all flags via " << reader_->Identity() + << "failed to refresh all flags via " << reader_->Identity() << ": " << all_flags.error(); } tracker_.Add(Keys::kAllFlags, updated_expiry); @@ -112,7 +112,7 @@ void LazyLoad::RefreshAllSegments() const { } else { LD_LOG(logger_, LogLevel::kError) << "failed to refresh all segments via " << reader_->Identity() - << all_segments.error(); + << ": " << all_segments.error(); } tracker_.Add(Keys::kAllSegments, updated_expiry); } @@ -123,41 +123,41 @@ void LazyLoad::RefreshInitState() const { } void LazyLoad::RefreshSegment(std::string const& key) const { + // Rate limit in all cases to protect the underlying store. + tracker_.Add(data_components::DataKind::kSegment, key, ExpiryTime()); + if (auto segment_result = reader_->GetSegment(key)) { if (auto optional_segment = *segment_result) { cache_.Upsert(key, std::move(*optional_segment)); } else { LD_LOG(logger_, LogLevel::kDebug) << "segment " << key << " requested but not found via " - << reader_->Identity() - << ", this is probably a temporary issue"; + << reader_->Identity(); } } else { LD_LOG(logger_, LogLevel::kError) << "failed to refresh segment " << key << " via " << reader_->Identity() << ": " << segment_result.error(); } - // TODO: If there is an actual error, then do we not reset the tracking? - tracker_.Add(data_components::DataKind::kSegment, key, ExpiryTime()); } void LazyLoad::RefreshFlag(std::string const& key) const { + // Rate limit in all cases to protect the underlying store. + tracker_.Add(data_components::DataKind::kFlag, key, ExpiryTime()); + if (auto flag_result = reader_->GetFlag(key)) { if (auto optional_flag = *flag_result) { cache_.Upsert(key, std::move(*optional_flag)); } else { LD_LOG(logger_, LogLevel::kDebug) << "flag " << key << " requested but not found via " - << reader_->Identity() - << ", this is probably a temporary issue"; + << reader_->Identity(); } } else { LD_LOG(logger_, LogLevel::kError) << "failed to refresh flag " << key << " via " << reader_->Identity() << ": " << flag_result.error(); } - // TODO: If there is an actual error, then do we not reset the tracking? - tracker_.Add(data_components::DataKind::kFlag, key, ExpiryTime()); } std::chrono::time_point LazyLoad::ExpiryTime() From 0ca8b5df631d7e6a74c705a59e04e46bdbf823e6 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 11:06:49 -0800 Subject: [PATCH 156/244] try to fix gmock segfault --- libs/server-sdk/tests/json_destination_test.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index ae5aa3f56..a294236f7 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -36,14 +36,13 @@ class JsonDestinationTest : public ::testing::Test { Logger const logger; NiceMock mock_dest; JsonDestination dest; + std::string name; JsonDestinationTest() : spy_logger(std::make_shared()), logger(spy_logger), - dest(logger, mock_dest) { - ON_CALL(mock_dest, Identity()).WillByDefault([]() { - static const std::string name = "FooCorp Database"; - return name; - }); + dest(logger, mock_dest), + name("FooCorp Database") { + ON_CALL(mock_dest, Identity()).WillByDefault([&]() { return name; }); } }; From 99e4fdcd589b85859dddd90b831e379c54866870 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 11:14:12 -0800 Subject: [PATCH 157/244] add RemoveSegment/RemoveFlag to MemoryStore --- .../memory_store/memory_store.cpp | 10 +++++ .../memory_store/memory_store.hpp | 4 ++ libs/server-sdk/tests/memory_store_test.cpp | 41 +++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index f38357d95..46612e16c 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -72,4 +72,14 @@ void MemoryStore::Upsert(std::string const& key, std::make_shared(std::move(segment)); } +bool MemoryStore::RemoveFlag(std::string const& key) { + std::lock_guard lock{data_mutex_}; + return flags_.erase(key) == 1; +} + +bool MemoryStore::RemoveSegment(std::string const& key) { + std::lock_guard lock{data_mutex_}; + return segments_.erase(key) == 1; +} + } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 485ad13e7..33a663ef6 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -40,6 +40,10 @@ class MemoryStore final : public data_interfaces::IStore, void Upsert(std::string const& key, data_model::SegmentDescriptor segment) override; + bool RemoveFlag(std::string const& key); + + bool RemoveSegment(std::string const& key); + MemoryStore() = default; ~MemoryStore() override = default; diff --git a/libs/server-sdk/tests/memory_store_test.cpp b/libs/server-sdk/tests/memory_store_test.cpp index 896fabd9a..901282ea2 100644 --- a/libs/server-sdk/tests/memory_store_test.cpp +++ b/libs/server-sdk/tests/memory_store_test.cpp @@ -278,3 +278,44 @@ TEST(MemoryStoreTest, OriginalFlagValidAfterUpsertOfFlag) { EXPECT_EQ(std::string("potato"), fetched_flag_after->item->variations[0].AsString()); } + +TEST(MemoryStoreTest, CanDeleteExistingSegment) { + MemoryStore store; + + Segment segment_a; + segment_a.version = 1; + segment_a.key = "segmentA"; + + store.Init(SDKDataSet{ + std::unordered_map{}, + std::unordered_map{ + {"segmentA", SegmentDescriptor(segment_a)}}, + }); + + ASSERT_TRUE(store.GetSegment("segmentA")); + ASSERT_TRUE(store.RemoveSegment("segmentA")); + + ASSERT_FALSE(store.GetSegment("segmentA")); + ASSERT_FALSE(store.RemoveSegment("segmentA")); +} + +TEST(MemoryStoreTest, CanDeleteExistingFlag) { + MemoryStore store; + + Flag flag_a; + flag_a.version = 1; + flag_a.key = "flagA"; + flag_a.variations = std::vector{"potato", "ham"}; + + store.Init(SDKDataSet{ + std::unordered_map{ + {"flagA", FlagDescriptor(flag_a)}}, + std::unordered_map(), + }); + + ASSERT_TRUE(store.GetFlag("flagA")); + ASSERT_TRUE(store.RemoveFlag("flagA")); + + ASSERT_FALSE(store.GetFlag("flagA")); + ASSERT_FALSE(store.RemoveFlag("flagA")); +} From e62648f132c7a15416bb82d37d6133a115f83a2e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 13:41:48 -0800 Subject: [PATCH 158/244] implement json_deserializer All methods --- .../integrations/iserialized_item_kind.hpp | 5 ++ .../memory_store/memory_store.cpp | 2 +- .../json_deserializer.cpp | 9 ++- .../json_deserializer.hpp | 58 ++++++++++++++++--- .../lazy_load/lazy_load_system.cpp | 54 ++++++++++++----- .../lazy_load/lazy_load_system.hpp | 15 +++-- .../tests/lazy_load_system_test.cpp | 42 +++++++++----- 7 files changed, 136 insertions(+), 49 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp index 7dd11ecec..c11b739ef 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace launchdarkly::server_side::integrations { @@ -51,4 +52,8 @@ inline bool operator==(ISerializedItemKind const& lhs, return lhs.Namespace() == rhs.Namespace(); } +inline void PrintTo(ISerializedItemKind const& kind, std::ostream* os) { + *os << kind.Namespace(); +} + } // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp index 46612e16c..95cac8748 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.cpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.cpp @@ -43,7 +43,7 @@ std::string const& MemoryStore::Identity() const { return description_; } -void MemoryStore::Init(launchdarkly::data_model::SDKDataSet dataSet) { +void MemoryStore::Init(data_model::SDKDataSet dataSet) { std::lock_guard lock{data_mutex_}; initialized_ = true; flags_.clear(); diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp index e3fbb49a6..5146a9803 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp @@ -25,18 +25,17 @@ JsonDeserializer::GetSegment(std::string const& key) const { data_interfaces::IDataReader::Collection JsonDeserializer::AllFlags() const { - // TODO: deserialize then return - return tl::make_unexpected("Not implemented"); + return DeserializeAll(flag_kind_); } data_interfaces::IDataReader::Collection JsonDeserializer::AllSegments() const { - // TODO: deserialize then return - return tl::make_unexpected("Not implemented"); + return DeserializeAll(segment_kind_); } std::string const& JsonDeserializer::Identity() const { - return reader_->Identity(); + static std::string const name = reader_->Identity() + " (JSON)"; + return name; } bool JsonDeserializer::Initialized() const { diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index be723c0cc..20d8a38a6 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -48,24 +48,68 @@ class JsonDeserializer final : public data_interfaces::IDataReader { } auto const boost_json_val = boost::json::parse(*result->serializedItem); - auto flag = boost::json::value_to< + auto item = boost::json::value_to< tl::expected, JsonError>>(boost_json_val); - if (!flag) { - /* flag couldn't be deserialized from the JSON string */ - return tl::make_unexpected(ErrorToString(flag.error())); + if (!item) { + /* item couldn't be deserialized from the JSON string */ + return tl::make_unexpected(ErrorToString(item.error())); } - std::optional maybe_flag = flag->value(); + std::optional maybe_item = item->value(); - if (!maybe_flag) { + if (!maybe_item) { /* JSON was valid, but the value is 'null' * TODO: will this ever happen? */ return tl::make_unexpected("data was null"); } - return data_model::ItemDescriptor(std::move(*maybe_flag)); + return data_model::ItemDescriptor(std::move(*maybe_item)); + } + + template + Collection> DeserializeAll( + DataKind const& kind) const { + auto result = reader_->All(kind); + + if (!result) { + /* the actual fetch failed */ + return tl::make_unexpected(result.error().message); + } + + std::unordered_map> + items; + + for (auto const& [key, descriptor] : *result) { + if (!descriptor.serializedItem) { + // Item deleted + continue; + } + + auto const boost_json_val = + boost::json::parse(*descriptor.serializedItem); + auto item = boost::json::value_to< + tl::expected, JsonError>>( + boost_json_val); + + if (!item) { + // TODO: log parse error + continue; + } + + std::optional maybe_item = item->value(); + + if (!maybe_item) { + /* JSON was valid, but the value is 'null', so skip it. TODO: + * log it?*/ + continue; + } + + items.emplace(key, data_model::ItemDescriptor( + std::move(*maybe_item))); + } + return items; } FlagKind const flag_kind_; diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 4a1fb6dab..9d6753bf5 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -4,6 +4,12 @@ namespace launchdarkly::server_side::data_systems { +data_components::FlagKind const LazyLoad::Kinds::Flag = + data_components::FlagKind(); + +data_components::SegmentKind const LazyLoad::Kinds::Segment = + data_components::SegmentKind(); + LazyLoad::LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg) : LazyLoad(logger, std::move(cfg), []() { return std::chrono::steady_clock::now(); @@ -44,30 +50,46 @@ std::shared_ptr LazyLoad::GetSegment( [this, &key]() { return cache_.GetSegment(key); }); } +// In the normal course of SDK operation, flags and segments are loaded +// on-demand, are resident in memory for a TTL, and then are refreshed. +// This results in a working set that is eventually consistent. Load on the +// underlying source is spread out relative to the pattern of flag evaluations +// performed by an application. +// +// However, AllFlags is a special case. Here the SDK is asking for all flags +// in order to perform a mass-evaluation for a single context. This could +// theoretically generate thousands of individual refresh calls for both flags +// and segments, which could overwhelm the source. +// +// To optimize this, two calls are made to grab all flags and all segments. +// As long as the TTL is longer than the actual time it takes to evaluate all of +// the flags, no additional calls will need to be made to the source. +// +// To guard against overloading the source when all flags are being constantly +// evaluated, the "all flags" operation itself is assigned a TTL (the same as a +// flag/segment.) std::unordered_map> LazyLoad::AllFlags() const { auto const state = tracker_.State(Keys::kAllFlags, time_()); return Get>>( - state, [this]() { RefreshAllFlags(); }, - [this]() { return cache_.AllFlags(); }); + state, + [this]() { + RefreshAllFlags(); + RefreshAllSegments(); + }, + [this]() { + return cache_ + .AllFlags(); // segments will be accessed as-needed by the + // evaluation algorithm + }); } std::unordered_map> LazyLoad::AllSegments() const { - auto const state = tracker_.State(Keys::kAllSegments, time_()); - return Get>>( - state, [this]() { RefreshAllSegments(); }, - [this]() { return cache_.AllSegments(); }); + return cache_.AllSegments(); } -data_components::FlagKind const LazyLoad::Kinds::Flag = - data_components::FlagKind(); - -data_components::SegmentKind const LazyLoad::Kinds::Segment = - data_components::SegmentKind(); - bool LazyLoad::Initialized() const { auto const state = tracker_.State(Keys::kInitialized, time_()); if (initialized_.has_value()) { @@ -87,6 +109,8 @@ bool LazyLoad::Initialized() const { void LazyLoad::RefreshAllFlags() const { auto const updated_expiry = ExpiryTime(); + tracker_.Add(Keys::kAllFlags, updated_expiry); + if (auto all_flags = reader_->AllFlags()) { for (auto flag : *all_flags) { cache_.Upsert(flag.first, std::move(flag.second)); @@ -98,7 +122,6 @@ void LazyLoad::RefreshAllFlags() const { << "failed to refresh all flags via " << reader_->Identity() << ": " << all_flags.error(); } - tracker_.Add(Keys::kAllFlags, updated_expiry); } void LazyLoad::RefreshAllSegments() const { @@ -114,7 +137,6 @@ void LazyLoad::RefreshAllSegments() const { << "failed to refresh all segments via " << reader_->Identity() << ": " << all_segments.error(); } - tracker_.Add(Keys::kAllSegments, updated_expiry); } void LazyLoad::RefreshInitState() const { @@ -133,6 +155,7 @@ void LazyLoad::RefreshSegment(std::string const& key) const { LD_LOG(logger_, LogLevel::kDebug) << "segment " << key << " requested but not found via " << reader_->Identity(); + cache_.RemoveSegment(key); } } else { LD_LOG(logger_, LogLevel::kError) @@ -152,6 +175,7 @@ void LazyLoad::RefreshFlag(std::string const& key) const { LD_LOG(logger_, LogLevel::kDebug) << "flag " << key << " requested but not found via " << reader_->Identity(); + cache_.RemoveFlag(key); } } else { LD_LOG(logger_, LogLevel::kError) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index dc4cd2132..d8f7efe96 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -39,10 +39,13 @@ class LazyLoad final : public data_interfaces::IDataSystem { 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; @@ -53,6 +56,12 @@ class LazyLoad final : public data_interfaces::IDataSystem { void Shutdown() override; + // Public for usage in tests. + struct Kinds { + static data_components::FlagKind const Flag; + static data_components::SegmentKind const Segment; + }; + private: void RefreshAllFlags() const; void RefreshAllSegments() const; @@ -94,14 +103,8 @@ class LazyLoad final : public data_interfaces::IDataSystem { ClockType::duration fresh_duration_; - struct Kinds { - static data_components::FlagKind const Flag; - static data_components::SegmentKind const Segment; - }; - struct Keys { static inline std::string const kAllFlags = "allFlags"; - static inline std::string const kAllSegments = "allSegments"; static inline std::string const kInitialized = "initialized"; }; }; diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index e8cc2c387..dea6306f2 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -6,6 +6,7 @@ #include #include +#include "data_components/serialization_adapters/json_deserializer.hpp" #include "spy_logger.hpp" using namespace launchdarkly; @@ -205,31 +206,42 @@ TEST_F(LazyLoadTest, RefreshesSegmentIfStale) { } } -TEST_F(LazyLoadTest, AllFlagsRefreshesIndividualFlagExpiration) { - using TimePoint = data_systems::LazyLoad::ClockType::time_point; - - constexpr auto refresh_ttl = std::chrono::seconds(10); - +TEST_F(LazyLoadTest, AllFlagsRefreshesIndividualFlag) { built::LazyLoadConfig const config{ - built::LazyLoadConfig::EvictionPolicy::Disabled, refresh_ttl, - mock_reader}; + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; - // We want to demonstrate that the individual TTL of a flag will be + // We want to demonstrate that an individual flag will be // refreshed not just when we grab that single flag, but also if - // we call AllFlags. So we'll have a situation where a single - // flag is fetched, and then is about to expire. We'll then call AllFlags - // and step the time forward, and ensure the flag is updated in memory - without - // actually calling GetFlag on the reader. + // we call AllFlags. { InSequence s; EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); - EXPECT_CALL(*mock_reader, All(testing::_)) - .WillOnce(testing::Return(std::unordered_map{ - {"foo", {2, false, "{\"key\":\"foo\",\"version\":2}"}}})); + EXPECT_CALL(*mock_reader, All(testing::_)) // first call for flags + .WillOnce(testing::Return( + std::unordered_map{ + {"foo", {2, false, "{\"key\":\"foo\",\"version\":2}"}}})); + EXPECT_CALL(*mock_reader, All(testing::_)) // second call for segments + .WillOnce(testing::Return( + std::unordered_map{})); } + data_systems::LazyLoad const lazy_load(logger, config); + + auto const flag1 = lazy_load.GetFlag("foo"); + ASSERT_TRUE(flag1); + ASSERT_EQ(flag1->version, 1); + + auto const all_flags = lazy_load.AllFlags(); + ASSERT_EQ(all_flags.size(), 1); + ASSERT_EQ(all_flags.at("foo")->version, 2); + auto const flag2 = lazy_load.GetFlag("foo"); + ASSERT_TRUE(flag2); + ASSERT_EQ(flag2->version, 2); } From d49690daa3dc0c006eb75ff9533e6aeaecedbf65 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 13:50:13 -0800 Subject: [PATCH 159/244] another attempt --- libs/server-sdk/tests/json_destination_test.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index a294236f7..9d57c61d0 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -28,6 +28,11 @@ class MockSerializedDestination : public ISerializedDestination { SerializedItemDescriptor), (override)); MOCK_METHOD(std::string const&, Identity, (), (const, override)); + MockSerializedDestination() { + ON_CALL(*this, Identity).WillByDefault([]() { + return "FooCorp Database"; + }); + } }; class JsonDestinationTest : public ::testing::Test { @@ -36,14 +41,10 @@ class JsonDestinationTest : public ::testing::Test { Logger const logger; NiceMock mock_dest; JsonDestination dest; - std::string name; JsonDestinationTest() : spy_logger(std::make_shared()), logger(spy_logger), - dest(logger, mock_dest), - name("FooCorp Database") { - ON_CALL(mock_dest, Identity()).WillByDefault([&]() { return name; }); - } + dest(logger, mock_dest) {} }; TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { From 1cb9cb65391551f2a85531e2f4310a427fe2dc8d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 14:06:27 -0800 Subject: [PATCH 160/244] backout some lazy load changes --- .../lazy_load/lazy_load_system.cpp | 19 +++++++++---------- .../lazy_load/lazy_load_system.hpp | 6 +----- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 9d6753bf5..78c177ec8 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -73,20 +73,17 @@ LazyLoad::AllFlags() const { auto const state = tracker_.State(Keys::kAllFlags, time_()); return Get>>( - state, - [this]() { - RefreshAllFlags(); - RefreshAllSegments(); - }, - [this]() { - return cache_ - .AllFlags(); // segments will be accessed as-needed by the - // evaluation algorithm - }); + state, [this]() { RefreshAllFlags(); }, + [this]() { return cache_.AllFlags(); }); } std::unordered_map> LazyLoad::AllSegments() const { + auto const state = tracker_.State(Keys::kAllSegments, time_()); + return Get>>( + state, [this]() { RefreshAllSegments(); }, + [this]() { return cache_.AllSegments(); }); return cache_.AllSegments(); } @@ -126,6 +123,8 @@ void LazyLoad::RefreshAllFlags() const { void LazyLoad::RefreshAllSegments() const { auto const updated_expiry = ExpiryTime(); + tracker_.Add(Keys::kAllSegments, updated_expiry); + if (auto all_segments = reader_->AllSegments()) { for (auto segment : *all_segments) { cache_.Upsert(segment.first, std::move(segment.second)); diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index d8f7efe96..7e7fcc534 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -69,11 +69,6 @@ class LazyLoad final : public data_interfaces::IDataSystem { void RefreshFlag(std::string const& key) const; void RefreshSegment(std::string const& key) const; - static integrations::SerializedItemDescriptor Serialize( - data_model::FlagDescriptor flag); - static integrations::SerializedItemDescriptor Serialize( - data_model::SegmentDescriptor segment); - template static TResult Get(data_components::ExpirationTracker::TrackState state, std::function const& refresh, @@ -105,6 +100,7 @@ class LazyLoad final : public data_interfaces::IDataSystem { struct Keys { static inline std::string const kAllFlags = "allFlags"; + static inline std::string const kAllSegments = "allFlags"; static inline std::string const kInitialized = "initialized"; }; }; From ff5fd5a2dc58f690b33251f70ca111118aa44d47 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 14:14:03 -0800 Subject: [PATCH 161/244] try to not have a sigsegv --- .../data_components/serialization_adapters/json_destination.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 8fd0197bc..0c9b31460 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -59,7 +59,7 @@ JsonDestination::JsonDestination(Logger const& logger, : logger_(logger), dest_(destination) {} std::string const& JsonDestination::Identity() const { - static std::string const identity = dest_.Identity() + " (JSON)"; + static std::string const identity = "(JSON)"; return identity; } From 2d483118cd4335d62fe999b7c9b749977686f4fb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 14:15:34 -0800 Subject: [PATCH 162/244] one more attempt --- .../serialization_adapters/json_destination.cpp | 7 ++++--- .../serialization_adapters/json_destination.hpp | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 0c9b31460..52aa626d4 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -56,11 +56,12 @@ SerializedItemDescriptor Serialize(std::string const& key, JsonDestination::JsonDestination(Logger const& logger, ISerializedDestination& destination) - : logger_(logger), dest_(destination) {} + : logger_(logger), + dest_(destination), + ident_(dest_.Identity() + " (JSON)") {} std::string const& JsonDestination::Identity() const { - static std::string const identity = "(JSON)"; - return identity; + return ident_; } void JsonDestination::Init(data_model::SDKDataSet data_set) { diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 6233d9895..3c15cbad9 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -91,6 +91,7 @@ class JsonDestination final : public data_interfaces::IDestination { Logger const& logger_; data_interfaces::ISerializedDestination& dest_; + std::string const ident_; }; } // namespace launchdarkly::server_side::data_components From 753eb8e459c3bb062c281644579f0430f82800db Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 14:17:43 -0800 Subject: [PATCH 163/244] think I was using gmock wrong, use testing::ReturnRef --- libs/server-sdk/tests/json_destination_test.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index 9d57c61d0..ec826ee92 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -29,9 +29,8 @@ class MockSerializedDestination : public ISerializedDestination { (override)); MOCK_METHOD(std::string const&, Identity, (), (const, override)); MockSerializedDestination() { - ON_CALL(*this, Identity).WillByDefault([]() { - return "FooCorp Database"; - }); + ON_CALL(*this, Identity) + .WillByDefault(testing::ReturnRef("FooCorp Database")); } }; From f3afd59beff1552367401b19bc0f8705a061f05c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 14:31:55 -0800 Subject: [PATCH 164/244] refactor json_deserializer.hpp --- .../json_deserializer.cpp | 8 +-- .../json_deserializer.hpp | 63 +++++++++---------- .../tests/lazy_load_system_test.cpp | 3 +- 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp index 5146a9803..c8734f58d 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp @@ -15,22 +15,22 @@ JsonDeserializer::JsonDeserializer( data_interfaces::IDataReader::Single JsonDeserializer::GetFlag(std::string const& key) const { - return Deserialize(flag_kind_, key); + return DeserializeSingle(flag_kind_, key); } data_interfaces::IDataReader::Single JsonDeserializer::GetSegment(std::string const& key) const { - return Deserialize(segment_kind_, key); + return DeserializeSingle(segment_kind_, key); } data_interfaces::IDataReader::Collection JsonDeserializer::AllFlags() const { - return DeserializeAll(flag_kind_); + return DeserializeCollection(flag_kind_); } data_interfaces::IDataReader::Collection JsonDeserializer::AllSegments() const { - return DeserializeAll(segment_kind_); + return DeserializeCollection(segment_kind_); } std::string const& JsonDeserializer::Identity() const { diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index 20d8a38a6..b2c7d3d34 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -32,22 +32,9 @@ class JsonDeserializer final : public data_interfaces::IDataReader { private: template - Single> Deserialize( - DataKind const& kind, - std::string const& key) const { - auto result = reader_->Get(kind, key); - - if (!result) { - /* the actual fetch failed */ - return tl::make_unexpected(result.error().message); - } - - if (!result->serializedItem) { - /* the fetch succeeded, but the item wasn't found */ - return std::nullopt; - } - - auto const boost_json_val = boost::json::parse(*result->serializedItem); + tl::expected, std::string> + DeserializeItem(std::string const& serialized_item) const { + auto const boost_json_val = boost::json::parse(serialized_item); auto item = boost::json::value_to< tl::expected, JsonError>>(boost_json_val); @@ -60,7 +47,7 @@ class JsonDeserializer final : public data_interfaces::IDataReader { if (!maybe_item) { /* JSON was valid, but the value is 'null' - * TODO: will this ever happen? + * TODO: log an error? Can this happen? */ return tl::make_unexpected("data was null"); } @@ -69,7 +56,26 @@ class JsonDeserializer final : public data_interfaces::IDataReader { } template - Collection> DeserializeAll( + Single> DeserializeSingle( + DataKind const& kind, + std::string const& key) const { + auto result = reader_->Get(kind, key); + + if (!result) { + /* the actual fetch failed */ + return tl::make_unexpected(result.error().message); + } + + if (!result->serializedItem) { + /* the fetch succeeded, but the item wasn't found */ + return std::nullopt; + } + + return DeserializeItem(*result->serializedItem); + } + + template + Collection> DeserializeCollection( DataKind const& kind) const { auto result = reader_->All(kind); @@ -87,27 +93,14 @@ class JsonDeserializer final : public data_interfaces::IDataReader { continue; } - auto const boost_json_val = - boost::json::parse(*descriptor.serializedItem); - auto item = boost::json::value_to< - tl::expected, JsonError>>( - boost_json_val); - - if (!item) { - // TODO: log parse error - continue; - } - - std::optional maybe_item = item->value(); - + auto maybe_item = DeserializeItem( + *descriptor.serializedItem); if (!maybe_item) { - /* JSON was valid, but the value is 'null', so skip it. TODO: - * log it?*/ + // TODO: Log the error? continue; } - items.emplace(key, data_model::ItemDescriptor( - std::move(*maybe_item))); + items.emplace(key, *maybe_item); } return items; } diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index dea6306f2..e6d14cb8c 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -55,7 +55,8 @@ TEST_F(LazyLoadTest, IdentityWrapsReaderIdentity) { data_systems::LazyLoad const lazy_load(logger, config); - ASSERT_EQ(lazy_load.Identity(), "lazy load via " + mock_reader_name); + ASSERT_EQ(lazy_load.Identity(), + "lazy load via " + mock_reader_name + " (JSON)"); } TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfFlagIsCached) { From af56baa93e1b4dba0e18740888e42a59c50d772a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 14:32:17 -0800 Subject: [PATCH 165/244] remove identity test --- libs/server-sdk/tests/json_destination_test.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index ec826ee92..fe0eacfe6 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -46,9 +46,9 @@ class JsonDestinationTest : public ::testing::Test { dest(logger, mock_dest) {} }; -TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { - ASSERT_EQ(dest.Identity(), "FooCorp Database (JSON)"); -} +// TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { +// ASSERT_EQ(dest.Identity(), "FooCorp Database (JSON)"); +// } TEST_F(JsonDestinationTest, InitErrorGeneratesLogMessage) { EXPECT_CALL(mock_dest, Init) From 54e69537cab3782f79a9c2103b6fb6c1d74ef9fb Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 15:05:04 -0800 Subject: [PATCH 166/244] try only instantiating the test fixture --- .../json_destination.hpp | 7 +- .../tests/json_destination_test.cpp | 293 +++++++++--------- 2 files changed, 156 insertions(+), 144 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 3c15cbad9..37c4d7289 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -6,6 +6,8 @@ #include +#include + namespace launchdarkly::server_side::data_components { /** @@ -35,9 +37,8 @@ class JsonDestination final : public data_interfaces::IDestination { * @param logger Used for logging storage errors. * @param destination Where data should be forwarded. */ - explicit JsonDestination( - Logger const& logger, - data_interfaces::ISerializedDestination& destination); + JsonDestination(Logger const& logger, + data_interfaces::ISerializedDestination& destination); /** * @brief Initialize the destination with an SDK data set. diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index fe0eacfe6..30da3e0b6 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -43,148 +43,159 @@ class JsonDestinationTest : public ::testing::Test { JsonDestinationTest() : spy_logger(std::make_shared()), logger(spy_logger), + mock_dest(), dest(logger, mock_dest) {} }; -// TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { -// ASSERT_EQ(dest.Identity(), "FooCorp Database (JSON)"); +TEST_F(JsonDestinationTest, Instantiation) {} +// +// // TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { +// // ASSERT_EQ(dest.Identity(), "FooCorp Database (JSON)"); +// // } +// +// TEST_F(JsonDestinationTest, InitErrorGeneratesLogMessage) { +// EXPECT_CALL(mock_dest, Init) +// .WillOnce(Return(ISerializedDestination::InitResult::kError)); +// +// dest.Init(data_model::SDKDataSet{}); +// +// ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, "failed")); +// } +// +// // The SerializedDestination need only be concerned with inserting items +// // exactly as specified in its Init argument. This test verifies that the +// // transformation from SDKDataSet into that argument is done correctly. The +// // transformation consists of two parts; first, serializing items to JSON, +// and +// // second, ordering those items by comparing keys with '<'. +// TEST_F(JsonDestinationTest, InitProperlyTransformsSDKDataSet) { +// EXPECT_CALL( +// mock_dest, +// Init(Eq(std::vector{ +// {{JsonDestination::Kinds::Flag, +// std::vector< +// ISerializedDestination::Keyed>{ +// {"flag_alpha", +// SerializedItemDescriptor::Present( +// 1, "{\"key\":\"flag_alpha\",\"version\":1}")}, +// {"flag_beta", +// SerializedItemDescriptor::Present( +// 2, "{\"key\":\"flag_beta\",\"version\":2}")}}}, +// {JsonDestination::Kinds::Segment, +// std::vector< +// ISerializedDestination::Keyed>{ +// {"segment_alpha", +// SerializedItemDescriptor::Present( +// 1, "{\"key\":\"segment_alpha\",\"version\":1}")}, +// {"segment_beta", +// SerializedItemDescriptor::Present( +// 2, "{\"key\":\"segment_beta\",\"version\":2}")}}}}}))) +// .WillOnce(Return(ISerializedDestination::InitResult::kSuccess)); +// +// // Note: flag/segments are deliberately not in alphabetical order here, +// so +// // that the implementation must sort them to pass the test. +// dest.Init(data_model::SDKDataSet{ +// std::unordered_map{ +// {"flag_beta", +// data_model::FlagDescriptor(data_model::Flag{"flag_beta", 2})}, +// {"flag_alpha", +// data_model::FlagDescriptor(data_model::Flag{"flag_alpha", 1})}}, +// std::unordered_map{ +// {"segment_beta", data_model::SegmentDescriptor( +// data_model::Segment{"segment_beta", 2})}, +// {"segment_alpha", data_model::SegmentDescriptor( +// data_model::Segment{"segment_alpha", +// 1})}}}); +// } +// +// TEST_F(JsonDestinationTest, UpsertFlagErrorGeneratesErrorMessage) { +// EXPECT_CALL(mock_dest, Upsert) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); +// +// dest.Upsert("foo", data_model::FlagDescriptor(1)); +// +// ASSERT_TRUE( +// spy_logger->Contains(0, LogLevel::kError, "failed to update flag +// foo")); +// } +// +// TEST_F(JsonDestinationTest, UpsertSegmentErrorGeneratesErrorMessage) { +// EXPECT_CALL(mock_dest, Upsert) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); +// +// dest.Upsert("foo", data_model::SegmentDescriptor(1)); +// +// ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, +// "failed to update segment foo")); +// } +// +// TEST_F(JsonDestinationTest, UpsertStaleFlagGeneratesDebugMessage) { +// EXPECT_CALL(mock_dest, Upsert) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); +// +// dest.Upsert("foo", data_model::FlagDescriptor(1)); +// +// ASSERT_TRUE( +// spy_logger->Contains(0, LogLevel::kDebug, "flag foo not updated")); +// } +// +// TEST_F(JsonDestinationTest, UpsertStaleSegmentGeneratesDebugMessage) { +// EXPECT_CALL(mock_dest, Upsert) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); +// +// dest.Upsert("foo", data_model::SegmentDescriptor(1)); +// +// ASSERT_TRUE( +// spy_logger->Contains(0, LogLevel::kDebug, "segment foo not +// updated")); +// } +// +// TEST_F(JsonDestinationTest, UpsertDeletedFlagCreatesTombstone) { +// EXPECT_CALL( +// mock_dest, +// Upsert(Ref(JsonDestination::Kinds::Flag), "flag", +// SerializedItemDescriptor::Absent( +// 2, "{\"key\":\"flag\",\"version\":2,\"deleted\":true}"))) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); +// +// dest.Upsert("flag", data_model::FlagDescriptor(2)); +// } +// +// TEST_F(JsonDestinationTest, UpsertDeletedSegmentCreatesTombstone) { +// EXPECT_CALL( +// mock_dest, +// Upsert(Ref(JsonDestination::Kinds::Segment), "segment", +// SerializedItemDescriptor::Absent( +// 2, +// "{\"key\":\"segment\",\"version\":2,\"deleted\":true}"))) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); +// +// dest.Upsert("segment", data_model::SegmentDescriptor(2)); +// } +// +// TEST_F(JsonDestinationTest, UpsertFlagCreatesSerializedItem) { +// EXPECT_CALL(mock_dest, +// Upsert(Ref(JsonDestination::Kinds::Flag), "flag", +// SerializedItemDescriptor::Present( +// 2, +// "{\"on\":true,\"key\":\"flag\",\"version\":2}"))) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); +// +// dest.Upsert("flag", +// data_model::FlagDescriptor(data_model::Flag{"flag", 2, +// true})); +// } +// +// TEST_F(JsonDestinationTest, UpsertSegmentCreatesSerializedItem) { +// EXPECT_CALL(mock_dest, +// Upsert(Ref(JsonDestination::Kinds::Segment), "segment", +// SerializedItemDescriptor::Present( +// 2, +// "{\"key\":\"segment\",\"version\":2,\"excluded\":" +// "[\"bar\"],\"included\":[\"foo\"]}"))) +// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); +// +// dest.Upsert("segment", data_model::SegmentDescriptor(data_model::Segment{ +// "segment", 2, {"foo"}, {"bar"}})); // } - -TEST_F(JsonDestinationTest, InitErrorGeneratesLogMessage) { - EXPECT_CALL(mock_dest, Init) - .WillOnce(Return(ISerializedDestination::InitResult::kError)); - - dest.Init(data_model::SDKDataSet{}); - - ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, "failed")); -} - -// The SerializedDestination need only be concerned with inserting items -// exactly as specified in its Init argument. This test verifies that the -// transformation from SDKDataSet into that argument is done correctly. The -// transformation consists of two parts; first, serializing items to JSON, and -// second, ordering those items by comparing keys with '<'. -TEST_F(JsonDestinationTest, InitProperlyTransformsSDKDataSet) { - EXPECT_CALL( - mock_dest, - Init(Eq(std::vector{ - {{JsonDestination::Kinds::Flag, - std::vector< - ISerializedDestination::Keyed>{ - {"flag_alpha", - SerializedItemDescriptor::Present( - 1, "{\"key\":\"flag_alpha\",\"version\":1}")}, - {"flag_beta", - SerializedItemDescriptor::Present( - 2, "{\"key\":\"flag_beta\",\"version\":2}")}}}, - {JsonDestination::Kinds::Segment, - std::vector< - ISerializedDestination::Keyed>{ - {"segment_alpha", - SerializedItemDescriptor::Present( - 1, "{\"key\":\"segment_alpha\",\"version\":1}")}, - {"segment_beta", - SerializedItemDescriptor::Present( - 2, "{\"key\":\"segment_beta\",\"version\":2}")}}}}}))) - .WillOnce(Return(ISerializedDestination::InitResult::kSuccess)); - - // Note: flag/segments are deliberately not in alphabetical order here, so - // that the implementation must sort them to pass the test. - dest.Init(data_model::SDKDataSet{ - std::unordered_map{ - {"flag_beta", - data_model::FlagDescriptor(data_model::Flag{"flag_beta", 2})}, - {"flag_alpha", - data_model::FlagDescriptor(data_model::Flag{"flag_alpha", 1})}}, - std::unordered_map{ - {"segment_beta", data_model::SegmentDescriptor( - data_model::Segment{"segment_beta", 2})}, - {"segment_alpha", data_model::SegmentDescriptor( - data_model::Segment{"segment_alpha", 1})}}}); -} - -TEST_F(JsonDestinationTest, UpsertFlagErrorGeneratesErrorMessage) { - EXPECT_CALL(mock_dest, Upsert) - .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); - - dest.Upsert("foo", data_model::FlagDescriptor(1)); - - ASSERT_TRUE( - spy_logger->Contains(0, LogLevel::kError, "failed to update flag foo")); -} - -TEST_F(JsonDestinationTest, UpsertSegmentErrorGeneratesErrorMessage) { - EXPECT_CALL(mock_dest, Upsert) - .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); - - dest.Upsert("foo", data_model::SegmentDescriptor(1)); - - ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, - "failed to update segment foo")); -} - -TEST_F(JsonDestinationTest, UpsertStaleFlagGeneratesDebugMessage) { - EXPECT_CALL(mock_dest, Upsert) - .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); - - dest.Upsert("foo", data_model::FlagDescriptor(1)); - - ASSERT_TRUE( - spy_logger->Contains(0, LogLevel::kDebug, "flag foo not updated")); -} - -TEST_F(JsonDestinationTest, UpsertStaleSegmentGeneratesDebugMessage) { - EXPECT_CALL(mock_dest, Upsert) - .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); - - dest.Upsert("foo", data_model::SegmentDescriptor(1)); - - ASSERT_TRUE( - spy_logger->Contains(0, LogLevel::kDebug, "segment foo not updated")); -} - -TEST_F(JsonDestinationTest, UpsertDeletedFlagCreatesTombstone) { - EXPECT_CALL( - mock_dest, - Upsert(Ref(JsonDestination::Kinds::Flag), "flag", - SerializedItemDescriptor::Absent( - 2, "{\"key\":\"flag\",\"version\":2,\"deleted\":true}"))) - .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - - dest.Upsert("flag", data_model::FlagDescriptor(2)); -} - -TEST_F(JsonDestinationTest, UpsertDeletedSegmentCreatesTombstone) { - EXPECT_CALL( - mock_dest, - Upsert(Ref(JsonDestination::Kinds::Segment), "segment", - SerializedItemDescriptor::Absent( - 2, "{\"key\":\"segment\",\"version\":2,\"deleted\":true}"))) - .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - - dest.Upsert("segment", data_model::SegmentDescriptor(2)); -} - -TEST_F(JsonDestinationTest, UpsertFlagCreatesSerializedItem) { - EXPECT_CALL(mock_dest, - Upsert(Ref(JsonDestination::Kinds::Flag), "flag", - SerializedItemDescriptor::Present( - 2, "{\"on\":true,\"key\":\"flag\",\"version\":2}"))) - .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - - dest.Upsert("flag", - data_model::FlagDescriptor(data_model::Flag{"flag", 2, true})); -} - -TEST_F(JsonDestinationTest, UpsertSegmentCreatesSerializedItem) { - EXPECT_CALL(mock_dest, - Upsert(Ref(JsonDestination::Kinds::Segment), "segment", - SerializedItemDescriptor::Present( - 2, - "{\"key\":\"segment\",\"version\":2,\"excluded\":" - "[\"bar\"],\"included\":[\"foo\"]}"))) - .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); - - dest.Upsert("segment", data_model::SegmentDescriptor(data_model::Segment{ - "segment", 2, {"foo"}, {"bar"}})); -} From 8f9135b642a309ddbbc0793d610e0a78b24cb6e8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 15:11:59 -0800 Subject: [PATCH 167/244] 29th try --- libs/server-sdk/tests/json_destination_test.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index 30da3e0b6..dc35daf8a 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -29,9 +29,11 @@ class MockSerializedDestination : public ISerializedDestination { (override)); MOCK_METHOD(std::string const&, Identity, (), (const, override)); MockSerializedDestination() { - ON_CALL(*this, Identity) - .WillByDefault(testing::ReturnRef("FooCorp Database")); + ON_CALL(*this, Identity).WillByDefault(testing::ReturnRef(name)); } + + private: + std::string const name = "FooCorp Database"; }; class JsonDestinationTest : public ::testing::Test { From 58122a2374b00f306db3db5908576ad8da0f186e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 16:10:35 -0800 Subject: [PATCH 168/244] fix some includes --- .../expiration_tracker/expiration_tracker.hpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp index c99421e11..ba790290a 100644 --- a/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp +++ b/libs/server-sdk/src/data_components/expiration_tracker/expiration_tracker.hpp @@ -3,14 +3,13 @@ #include "../dependency_tracker/data_kind.hpp" #include "../dependency_tracker/tagged_data.hpp" -#include - #include #include -#include -#include #include +#include #include +#include +#include namespace launchdarkly::server_side::data_components { From c4bd414896c8c7a71a48b70799ef4a99e79ce8f1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 16:29:34 -0800 Subject: [PATCH 169/244] add back missing tests --- .../tests/json_destination_test.cpp | 298 +++++++++--------- 1 file changed, 146 insertions(+), 152 deletions(-) diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index dc35daf8a..3f8fdf2ea 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -49,155 +49,149 @@ class JsonDestinationTest : public ::testing::Test { dest(logger, mock_dest) {} }; -TEST_F(JsonDestinationTest, Instantiation) {} -// -// // TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { -// // ASSERT_EQ(dest.Identity(), "FooCorp Database (JSON)"); -// // } -// -// TEST_F(JsonDestinationTest, InitErrorGeneratesLogMessage) { -// EXPECT_CALL(mock_dest, Init) -// .WillOnce(Return(ISerializedDestination::InitResult::kError)); -// -// dest.Init(data_model::SDKDataSet{}); -// -// ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, "failed")); -// } -// -// // The SerializedDestination need only be concerned with inserting items -// // exactly as specified in its Init argument. This test verifies that the -// // transformation from SDKDataSet into that argument is done correctly. The -// // transformation consists of two parts; first, serializing items to JSON, -// and -// // second, ordering those items by comparing keys with '<'. -// TEST_F(JsonDestinationTest, InitProperlyTransformsSDKDataSet) { -// EXPECT_CALL( -// mock_dest, -// Init(Eq(std::vector{ -// {{JsonDestination::Kinds::Flag, -// std::vector< -// ISerializedDestination::Keyed>{ -// {"flag_alpha", -// SerializedItemDescriptor::Present( -// 1, "{\"key\":\"flag_alpha\",\"version\":1}")}, -// {"flag_beta", -// SerializedItemDescriptor::Present( -// 2, "{\"key\":\"flag_beta\",\"version\":2}")}}}, -// {JsonDestination::Kinds::Segment, -// std::vector< -// ISerializedDestination::Keyed>{ -// {"segment_alpha", -// SerializedItemDescriptor::Present( -// 1, "{\"key\":\"segment_alpha\",\"version\":1}")}, -// {"segment_beta", -// SerializedItemDescriptor::Present( -// 2, "{\"key\":\"segment_beta\",\"version\":2}")}}}}}))) -// .WillOnce(Return(ISerializedDestination::InitResult::kSuccess)); -// -// // Note: flag/segments are deliberately not in alphabetical order here, -// so -// // that the implementation must sort them to pass the test. -// dest.Init(data_model::SDKDataSet{ -// std::unordered_map{ -// {"flag_beta", -// data_model::FlagDescriptor(data_model::Flag{"flag_beta", 2})}, -// {"flag_alpha", -// data_model::FlagDescriptor(data_model::Flag{"flag_alpha", 1})}}, -// std::unordered_map{ -// {"segment_beta", data_model::SegmentDescriptor( -// data_model::Segment{"segment_beta", 2})}, -// {"segment_alpha", data_model::SegmentDescriptor( -// data_model::Segment{"segment_alpha", -// 1})}}}); -// } -// -// TEST_F(JsonDestinationTest, UpsertFlagErrorGeneratesErrorMessage) { -// EXPECT_CALL(mock_dest, Upsert) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); -// -// dest.Upsert("foo", data_model::FlagDescriptor(1)); -// -// ASSERT_TRUE( -// spy_logger->Contains(0, LogLevel::kError, "failed to update flag -// foo")); -// } -// -// TEST_F(JsonDestinationTest, UpsertSegmentErrorGeneratesErrorMessage) { -// EXPECT_CALL(mock_dest, Upsert) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); -// -// dest.Upsert("foo", data_model::SegmentDescriptor(1)); -// -// ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, -// "failed to update segment foo")); -// } -// -// TEST_F(JsonDestinationTest, UpsertStaleFlagGeneratesDebugMessage) { -// EXPECT_CALL(mock_dest, Upsert) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); -// -// dest.Upsert("foo", data_model::FlagDescriptor(1)); -// -// ASSERT_TRUE( -// spy_logger->Contains(0, LogLevel::kDebug, "flag foo not updated")); -// } -// -// TEST_F(JsonDestinationTest, UpsertStaleSegmentGeneratesDebugMessage) { -// EXPECT_CALL(mock_dest, Upsert) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); -// -// dest.Upsert("foo", data_model::SegmentDescriptor(1)); -// -// ASSERT_TRUE( -// spy_logger->Contains(0, LogLevel::kDebug, "segment foo not -// updated")); -// } -// -// TEST_F(JsonDestinationTest, UpsertDeletedFlagCreatesTombstone) { -// EXPECT_CALL( -// mock_dest, -// Upsert(Ref(JsonDestination::Kinds::Flag), "flag", -// SerializedItemDescriptor::Absent( -// 2, "{\"key\":\"flag\",\"version\":2,\"deleted\":true}"))) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); -// -// dest.Upsert("flag", data_model::FlagDescriptor(2)); -// } -// -// TEST_F(JsonDestinationTest, UpsertDeletedSegmentCreatesTombstone) { -// EXPECT_CALL( -// mock_dest, -// Upsert(Ref(JsonDestination::Kinds::Segment), "segment", -// SerializedItemDescriptor::Absent( -// 2, -// "{\"key\":\"segment\",\"version\":2,\"deleted\":true}"))) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); -// -// dest.Upsert("segment", data_model::SegmentDescriptor(2)); -// } -// -// TEST_F(JsonDestinationTest, UpsertFlagCreatesSerializedItem) { -// EXPECT_CALL(mock_dest, -// Upsert(Ref(JsonDestination::Kinds::Flag), "flag", -// SerializedItemDescriptor::Present( -// 2, -// "{\"on\":true,\"key\":\"flag\",\"version\":2}"))) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); -// -// dest.Upsert("flag", -// data_model::FlagDescriptor(data_model::Flag{"flag", 2, -// true})); -// } -// -// TEST_F(JsonDestinationTest, UpsertSegmentCreatesSerializedItem) { -// EXPECT_CALL(mock_dest, -// Upsert(Ref(JsonDestination::Kinds::Segment), "segment", -// SerializedItemDescriptor::Present( -// 2, -// "{\"key\":\"segment\",\"version\":2,\"excluded\":" -// "[\"bar\"],\"included\":[\"foo\"]}"))) -// .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); -// -// dest.Upsert("segment", data_model::SegmentDescriptor(data_model::Segment{ -// "segment", 2, {"foo"}, {"bar"}})); -// } +TEST_F(JsonDestinationTest, WrapsUnderlyingDestinationIdentity) { + ASSERT_EQ(dest.Identity(), "FooCorp Database (JSON)"); +} + +TEST_F(JsonDestinationTest, InitErrorGeneratesLogMessage) { + EXPECT_CALL(mock_dest, Init) + .WillOnce(Return(ISerializedDestination::InitResult::kError)); + + dest.Init(data_model::SDKDataSet{}); + + ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, "failed")); +} + +// The SerializedDestination need only be concerned with inserting items +// exactly as specified in its Init argument. This test verifies that the +// transformation from SDKDataSet into that argument is done correctly. The +// transformation consists of two parts; first, serializing items to JSON, and +// second, ordering those items by comparing keys with '<'. +TEST_F(JsonDestinationTest, InitProperlyTransformsSDKDataSet) { + EXPECT_CALL( + mock_dest, + Init(Eq(std::vector{ + {{JsonDestination::Kinds::Flag, + std::vector< + ISerializedDestination::Keyed>{ + {"flag_alpha", + SerializedItemDescriptor::Present( + 1, "{\"key\":\"flag_alpha\",\"version\":1}")}, + {"flag_beta", + SerializedItemDescriptor::Present( + 2, "{\"key\":\"flag_beta\",\"version\":2}")}}}, + {JsonDestination::Kinds::Segment, + std::vector< + ISerializedDestination::Keyed>{ + {"segment_alpha", + SerializedItemDescriptor::Present( + 1, "{\"key\":\"segment_alpha\",\"version\":1}")}, + {"segment_beta", + SerializedItemDescriptor::Present( + 2, "{\"key\":\"segment_beta\",\"version\":2}")}}}}}))) + .WillOnce(Return(ISerializedDestination::InitResult::kSuccess)); + + // Note: flag/segments are deliberately not in alphabetical order here, + so + // that the implementation must sort them to pass the test. + dest.Init(data_model::SDKDataSet{ + std::unordered_map{ + {"flag_beta", + data_model::FlagDescriptor(data_model::Flag{"flag_beta", 2})}, + {"flag_alpha", data_model::FlagDescriptor( + data_model::Flag{"flag_alpha", 1})}}, + std::unordered_map{ + {"segment_beta", data_model::SegmentDescriptor( + data_model::Segment{"segment_beta", 2})}, + {"segment_alpha", + data_model::SegmentDescriptor( + data_model::Segment{"segment_alpha", 1})}}}); +} + +TEST_F(JsonDestinationTest, UpsertFlagErrorGeneratesErrorMessage) { + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); + + dest.Upsert("foo", data_model::FlagDescriptor(1)); + + ASSERT_TRUE( + spy_logger->Contains(0, LogLevel::kError, "failed to update flag + foo")); +} + +TEST_F(JsonDestinationTest, UpsertSegmentErrorGeneratesErrorMessage) { + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kError)); + + dest.Upsert("foo", data_model::SegmentDescriptor(1)); + + ASSERT_TRUE(spy_logger->Contains(0, LogLevel::kError, + "failed to update segment foo")); +} + +TEST_F(JsonDestinationTest, UpsertStaleFlagGeneratesDebugMessage) { + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); + + dest.Upsert("foo", data_model::FlagDescriptor(1)); + + ASSERT_TRUE( + spy_logger->Contains(0, LogLevel::kDebug, "flag foo not updated")); +} + +TEST_F(JsonDestinationTest, UpsertStaleSegmentGeneratesDebugMessage) { + EXPECT_CALL(mock_dest, Upsert) + .WillOnce(Return(ISerializedDestination::UpsertResult::kNotUpdated)); + + dest.Upsert("foo", data_model::SegmentDescriptor(1)); + + ASSERT_TRUE( + spy_logger->Contains(0, LogLevel::kDebug, "segment foo not + updated")); +} + +TEST_F(JsonDestinationTest, UpsertDeletedFlagCreatesTombstone) { + EXPECT_CALL( + mock_dest, + Upsert(Ref(JsonDestination::Kinds::Flag), "flag", + SerializedItemDescriptor::Absent( + 2, "{\"key\":\"flag\",\"version\":2,\"deleted\":true}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + dest.Upsert("flag", data_model::FlagDescriptor(2)); +} + +TEST_F(JsonDestinationTest, UpsertDeletedSegmentCreatesTombstone) { + EXPECT_CALL( + mock_dest, + Upsert(Ref(JsonDestination::Kinds::Segment), "segment", + SerializedItemDescriptor::Absent( + 2, "{\"key\":\"segment\",\"version\":2,\"deleted\":true}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + dest.Upsert("segment", data_model::SegmentDescriptor(2)); +} + +TEST_F(JsonDestinationTest, UpsertFlagCreatesSerializedItem) { + EXPECT_CALL(mock_dest, + Upsert(Ref(JsonDestination::Kinds::Flag), "flag", + SerializedItemDescriptor::Present( + 2, "{\"on\":true,\"key\":\"flag\",\"version\":2}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + dest.Upsert("flag", + data_model::FlagDescriptor(data_model::Flag{"flag", 2, true})); +} + +TEST_F(JsonDestinationTest, UpsertSegmentCreatesSerializedItem) { + EXPECT_CALL(mock_dest, + Upsert(Ref(JsonDestination::Kinds::Segment), "segment", + SerializedItemDescriptor::Present( + 2, + "{\"key\":\"segment\",\"version\":2,\"excluded\":" + "[\"bar\"],\"included\":[\"foo\"]}"))) + .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); + + dest.Upsert("segment", data_model::SegmentDescriptor(data_model::Segment{ + "segment", 2, {"foo"}, {"bar"}})); +} From ab98d992920265106c1d53dde0c57cc6b3a91d61 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Tue, 28 Nov 2023 17:49:19 -0800 Subject: [PATCH 170/244] refactoring lazy_load to use templated getters --- .../launchdarkly/detail/unreachable.hpp | 2 + .../dependency_tracker/data_kind.hpp | 19 +++++ .../lazy_load/lazy_load_system.cpp | 79 ++++--------------- .../lazy_load/lazy_load_system.hpp | 60 ++++++++++++++ 4 files changed, 95 insertions(+), 65 deletions(-) diff --git a/libs/common/include/launchdarkly/detail/unreachable.hpp b/libs/common/include/launchdarkly/detail/unreachable.hpp index f0dff1993..264065b83 100644 --- a/libs/common/include/launchdarkly/detail/unreachable.hpp +++ b/libs/common/include/launchdarkly/detail/unreachable.hpp @@ -1,3 +1,5 @@ +#pragma once + namespace launchdarkly::detail { // This may be replaced with a standard routine when C++23 is available. diff --git a/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp b/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp index 4f231b6fa..88f5efefb 100644 --- a/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp +++ b/libs/server-sdk/src/data_components/dependency_tracker/data_kind.hpp @@ -1,7 +1,26 @@ #pragma once +#include + #include +#include namespace launchdarkly::server_side::data_components { enum class DataKind : std::size_t { kFlag = 0, kSegment = 1, kKindCount = 2 }; + +inline std::ostream& operator<<(std::ostream& out, DataKind const& kind) { + switch (kind) { + case DataKind::kFlag: + out << "flag"; + return out; + case DataKind::kSegment: + out << "segment"; + return out; + case DataKind::kKindCount: + out << "kind_count"; + return out; + } + detail::unreachable(); +} + } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 78c177ec8..1a25e35d2 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -84,7 +84,6 @@ LazyLoad::AllSegments() const { std::string, std::shared_ptr>>( state, [this]() { RefreshAllSegments(); }, [this]() { return cache_.AllSegments(); }); - return cache_.AllSegments(); } bool LazyLoad::Initialized() const { @@ -105,37 +104,13 @@ bool LazyLoad::Initialized() const { } void LazyLoad::RefreshAllFlags() const { - auto const updated_expiry = ExpiryTime(); - tracker_.Add(Keys::kAllFlags, updated_expiry); - - if (auto all_flags = reader_->AllFlags()) { - for (auto flag : *all_flags) { - cache_.Upsert(flag.first, std::move(flag.second)); - tracker_.Add(data_components::DataKind::kFlag, flag.first, - updated_expiry); - } - } else { - LD_LOG(logger_, LogLevel::kError) - << "failed to refresh all flags via " << reader_->Identity() << ": " - << all_flags.error(); - } + RefreshAll(Keys::kAllFlags, data_components::DataKind::kFlag, + [this]() { return reader_->AllFlags(); }); } void LazyLoad::RefreshAllSegments() const { - auto const updated_expiry = ExpiryTime(); - tracker_.Add(Keys::kAllSegments, updated_expiry); - - if (auto all_segments = reader_->AllSegments()) { - for (auto segment : *all_segments) { - cache_.Upsert(segment.first, std::move(segment.second)); - tracker_.Add(data_components::DataKind::kSegment, segment.first, - updated_expiry); - } - } else { - LD_LOG(logger_, LogLevel::kError) - << "failed to refresh all segments via " << reader_->Identity() - << ": " << all_segments.error(); - } + RefreshAll(Keys::kAllSegments, data_components::DataKind::kSegment, + [this]() { return reader_->AllSegments(); }); } void LazyLoad::RefreshInitState() const { @@ -143,44 +118,18 @@ void LazyLoad::RefreshInitState() const { tracker_.Add(Keys::kInitialized, ExpiryTime()); } -void LazyLoad::RefreshSegment(std::string const& key) const { - // Rate limit in all cases to protect the underlying store. - tracker_.Add(data_components::DataKind::kSegment, key, ExpiryTime()); - - if (auto segment_result = reader_->GetSegment(key)) { - if (auto optional_segment = *segment_result) { - cache_.Upsert(key, std::move(*optional_segment)); - } else { - LD_LOG(logger_, LogLevel::kDebug) - << "segment " << key << " requested but not found via " - << reader_->Identity(); - cache_.RemoveSegment(key); - } - } else { - LD_LOG(logger_, LogLevel::kError) - << "failed to refresh segment " << key << " via " - << reader_->Identity() << ": " << segment_result.error(); - } +void LazyLoad::RefreshSegment(std::string const& segment_key) const { + RefreshItem( + data_components::DataKind::kSegment, segment_key, + [this](std::string const& key) { return reader_->GetSegment(key); }, + [this](std::string const& key) { return cache_.RemoveSegment(key); }); } -void LazyLoad::RefreshFlag(std::string const& key) const { - // Rate limit in all cases to protect the underlying store. - tracker_.Add(data_components::DataKind::kFlag, key, ExpiryTime()); - - if (auto flag_result = reader_->GetFlag(key)) { - if (auto optional_flag = *flag_result) { - cache_.Upsert(key, std::move(*optional_flag)); - } else { - LD_LOG(logger_, LogLevel::kDebug) - << "flag " << key << " requested but not found via " - << reader_->Identity(); - cache_.RemoveFlag(key); - } - } else { - LD_LOG(logger_, LogLevel::kError) - << "failed to refresh flag " << key << " via " - << reader_->Identity() << ": " << flag_result.error(); - } +void LazyLoad::RefreshFlag(std::string const& flag_key) const { + RefreshItem( + data_components::DataKind::kFlag, flag_key, + [this](std::string const& key) { return reader_->GetFlag(key); }, + [this](std::string const& key) { return cache_.RemoveFlag(key); }); } std::chrono::time_point LazyLoad::ExpiryTime() diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 7e7fcc534..e5d087e01 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -85,6 +85,66 @@ class LazyLoad final : public data_interfaces::IDataSystem { detail::unreachable(); } + template + void RefreshItem(data_components::DataKind const kind, + std::string const& key, + Getter&& getter, + Evictor&& evictor) const { + // Refreshing this item is always rate limited, even + // if the refresh has an error. + tracker_.Add(kind, key, ExpiryTime()); + + if (auto expected_item = getter(key)) { + if (auto optional_item = *expected_item) { + cache_.Upsert(key, std::move(*optional_item)); + } else { + // If the item is actually *missing* - not just a deleted + // tombstone representation - it implies that the source + // was re-initialized. In this case, the correct thing to do + // is evict it from the memory cache + LD_LOG(logger_, LogLevel::kDebug) + << kind << key << " requested but not found via " + << reader_->Identity(); + if (evictor(key)) { + LD_LOG(logger_, LogLevel::kDebug) + << "removed " << kind << " " << key << " from cache"; + } + } + } else { + // If there's a persistent error, it will be logged at the refresh + // interval. + LD_LOG(logger_, LogLevel::kError) + << "failed to refresh " << kind << " " << key << " via " + << reader_->Identity() << ": " << expected_item.error(); + } + } + + template + void RefreshAll(std::string const& all_item_key, + data_components::DataKind const item_kind, + Getter&& getter) const { + // Storing an expiry time so that the 'all' key and the individual + // item keys will expire at the same time. + auto const updated_expiry = ExpiryTime(); + + // Refreshing 'all' for this item is always rate limited, even if + // the refresh has an error. + tracker_.Add(all_item_key, updated_expiry); + + if (auto all_items = getter()) { + for (auto item : *all_items) { + cache_.Upsert(item.first, std::move(item.second)); + tracker_.Add(item_kind, item.first, updated_expiry); + } + } else { + // If there's a persistent error, it will be logged at the + // refresh interval. + LD_LOG(logger_, LogLevel::kError) + << "failed to refresh all " << item_kind << "s via " + << reader_->Identity() << ": " << all_items.error(); + } + } + ClockType::time_point ExpiryTime() const; Logger const& logger_; From 4b7984127c79c5e2cb89800efbcb38c8cfeea899 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 09:02:58 -0800 Subject: [PATCH 171/244] add design comment to Lazy Load system --- .../lazy_load/lazy_load_system.cpp | 59 +++++++++++++------ .../lazy_load/lazy_load_system.hpp | 1 - 2 files changed, 41 insertions(+), 19 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 1a25e35d2..a24e1a0b4 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -1,3 +1,44 @@ +// The Lazy Load system is responsible for loading flags and segments from +// an underlying source when requested by the SDK's evaluation algorithm. +// +// This is fundamentally different than Background Sync, where all items +// are loaded into memory at initialization, and then updated asynchronously +// when changes arrive from LaunchDarkly. + +// In this system, items are updated through a cache refresh process which +// depends on a TTL. +// +// This TTL is configured by the user, and represents a tradeoff between +// freshness (and consistency), and load/traffic to the underyling source. +// +// In the normal course of SDK operation, individual flags and segments loaded +// over time depending on the patterns of SDK usage present in an application. +// This generally spreads out the load on the source over time. +// +// A different usage pattern is caused by the SDK's "AllFlags" API, which +// evaluates all flags for a given context. Because this usage is potentially +// extremely costly (if there are thousands of flags or segments to be loaded), +// the Lazy Load system optimizes by performing a special "GetAll" operation +// on the underlying source. This operation fetches the entire set of a data kind +// (either flags or segments) in one trip, and then stores them in the cache. +// +// It's important that the "GetAll" operation also be bound by a TTL, so that +// repeated calls to "AllFlags" don't cause repeated calls to the source. The TTL +// for this operation is identical to the indivudal-item TTL configurable by +// the user. +// +// An implication of the current implementation is that if Lazy Load is being +// used to handle a "sparse" environment - that is, the environment is too big +// to load into memory, and so loading on demand is desirable - calling "AllFlags" +// will destroy that property because items are not actively evicted from +// the cache. On the other hand, that property could be used to prime the SDK's +// memory cache, preventing the need to individually load flags or segments. +// +// The current design does not perform active eviction when an item is stale because +// it is generally better to serve stale data than none at all. If the source +// is unavailable, the SDK will be able to indefinitely serve the last known +// values. Active eviction can be added in the future as a configurable option. + #include "lazy_load_system.hpp" #include "../../data_components/serialization_adapters/json_deserializer.hpp" @@ -50,24 +91,6 @@ std::shared_ptr LazyLoad::GetSegment( [this, &key]() { return cache_.GetSegment(key); }); } -// In the normal course of SDK operation, flags and segments are loaded -// on-demand, are resident in memory for a TTL, and then are refreshed. -// This results in a working set that is eventually consistent. Load on the -// underlying source is spread out relative to the pattern of flag evaluations -// performed by an application. -// -// However, AllFlags is a special case. Here the SDK is asking for all flags -// in order to perform a mass-evaluation for a single context. This could -// theoretically generate thousands of individual refresh calls for both flags -// and segments, which could overwhelm the source. -// -// To optimize this, two calls are made to grab all flags and all segments. -// As long as the TTL is longer than the actual time it takes to evaluate all of -// the flags, no additional calls will need to be made to the source. -// -// To guard against overloading the source when all flags are being constantly -// evaluated, the "all flags" operation itself is assigned a TTL (the same as a -// flag/segment.) std::unordered_map> LazyLoad::AllFlags() const { auto const state = tracker_.State(Keys::kAllFlags, time_()); diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index e5d087e01..c3cefb7ad 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -8,7 +8,6 @@ #include #include -#include #include #include From 5751834160fd4d120a1c43d02dd449f11ca7f75d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 09:27:25 -0800 Subject: [PATCH 172/244] add [[nodiscard]] in serialized data reader --- .../sources/iserialized_data_reader.hpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp b/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp index eefdea900..48e55361a 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp @@ -56,8 +56,9 @@ class ISerializedDataReader { * 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(integrations::ISerializedItemKind const& kind, - std::string const& itemKey) const = 0; + [[nodiscard]] virtual GetResult Get( + integrations::ISerializedItemKind const& kind, + std::string const& itemKey) const = 0; /** * Retrieves all items from the specified collection. @@ -68,13 +69,13 @@ class ISerializedDataReader { * @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( + [[nodiscard]] virtual AllResult All( integrations::ISerializedItemKind const& kind) const = 0; /** * @return Identity of the reader. Used in logs. */ - virtual std::string const& Identity() const = 0; + [[nodiscard]] virtual std::string const& Identity() const = 0; /** * @return True if the reader has data that can be queried. The reader @@ -83,7 +84,7 @@ class ISerializedDataReader { * implementation would be to store a special data key that is only set * after initial SDK data is stored. */ - virtual bool Initialized() const = 0; + [[nodiscard]] virtual bool Initialized() const = 0; protected: ISerializedDataReader() = default; From b465bd12100f24a0f54af86e05a03a709078c81a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 09:27:48 -0800 Subject: [PATCH 173/244] cleanup logging/error handling in json_deserializer.cpp --- .../json_deserializer.cpp | 12 ++++--- .../json_deserializer.hpp | 34 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp index c8734f58d..11f06305a 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp @@ -10,8 +10,13 @@ namespace launchdarkly::server_side::data_components { JsonDeserializer::JsonDeserializer( + Logger const& logger, std::shared_ptr reader) - : flag_kind_(), segment_kind_(), reader_(std::move(reader)) {} + : logger_(logger), + flag_kind_(), + segment_kind_(), + source_(std::move(reader)), + identity_(source_->Identity() + " (JSON)") {} data_interfaces::IDataReader::Single JsonDeserializer::GetFlag(std::string const& key) const { @@ -34,12 +39,11 @@ JsonDeserializer::AllSegments() const { } std::string const& JsonDeserializer::Identity() const { - static std::string const name = reader_->Identity() + " (JSON)"; - return name; + return identity_; } bool JsonDeserializer::Initialized() const { - return reader_->Initialized(); + return source_->Initialized(); } } // namespace launchdarkly::server_side::data_components diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index b2c7d3d34..67dedcc49 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -3,6 +3,7 @@ #include "../../data_interfaces/source/idata_reader.hpp" #include "../kinds/kinds.hpp" +#include #include #include @@ -12,6 +13,7 @@ namespace launchdarkly::server_side::data_components { class JsonDeserializer final : public data_interfaces::IDataReader { public: explicit JsonDeserializer( + Logger const& logger, std::shared_ptr reader); [[nodiscard]] Single GetFlag( @@ -39,17 +41,13 @@ class JsonDeserializer final : public data_interfaces::IDataReader { tl::expected, JsonError>>(boost_json_val); if (!item) { - /* item couldn't be deserialized from the JSON string */ return tl::make_unexpected(ErrorToString(item.error())); } std::optional maybe_item = item->value(); if (!maybe_item) { - /* JSON was valid, but the value is 'null' - * TODO: log an error? Can this happen? - */ - return tl::make_unexpected("data was null"); + return tl::make_unexpected("JSON value is null"); } return data_model::ItemDescriptor(std::move(*maybe_item)); @@ -59,15 +57,15 @@ class JsonDeserializer final : public data_interfaces::IDataReader { Single> DeserializeSingle( DataKind const& kind, std::string const& key) const { - auto result = reader_->Get(kind, key); + auto result = source_->Get(kind, key); if (!result) { - /* the actual fetch failed */ + /* error in fetching the item */ return tl::make_unexpected(result.error().message); } if (!result->serializedItem) { - /* the fetch succeeded, but the item wasn't found */ + /* no error, but item not found by the source */ return std::nullopt; } @@ -77,10 +75,10 @@ class JsonDeserializer final : public data_interfaces::IDataReader { template Collection> DeserializeCollection( DataKind const& kind) const { - auto result = reader_->All(kind); + auto result = source_->All(kind); if (!result) { - /* the actual fetch failed */ + /* error in fetching the items */ return tl::make_unexpected(result.error().message); } @@ -89,14 +87,22 @@ class JsonDeserializer final : public data_interfaces::IDataReader { for (auto const& [key, descriptor] : *result) { if (!descriptor.serializedItem) { - // Item deleted + /* item is deleted, add a tombstone to the result so that the + * caller can make a decision on what to do. */ + items.emplace(key, data_model::ItemDescriptor( + descriptor.version)); continue; } auto maybe_item = DeserializeItem( *descriptor.serializedItem); + if (!maybe_item) { - // TODO: Log the error? + /* single item failing to deserialize doesn't cause the + * whole operation to fail; other items may be valid. */ + LD_LOG(logger_, LogLevel::kError) + << "failed to deserialize " << key << " while fetching all " + << kind.Namespace() << ": " << maybe_item.error(); continue; } @@ -105,9 +111,11 @@ class JsonDeserializer final : public data_interfaces::IDataReader { return items; } + Logger const& logger_; FlagKind const flag_kind_; FlagKind const segment_kind_; - std::shared_ptr reader_; + std::shared_ptr source_; + std::string const identity_; }; } // namespace launchdarkly::server_side::data_components From 7e0ca2cee53b54918eb04ea082f37a2e57e0e14e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 09:27:59 -0800 Subject: [PATCH 174/244] update lazy load test --- .../lazy_load/lazy_load_system.cpp | 30 +++++++++++-------- .../tests/lazy_load_system_test.cpp | 6 +--- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index a24e1a0b4..0fd0f9274 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -19,25 +19,28 @@ // evaluates all flags for a given context. Because this usage is potentially // extremely costly (if there are thousands of flags or segments to be loaded), // the Lazy Load system optimizes by performing a special "GetAll" operation -// on the underlying source. This operation fetches the entire set of a data kind -// (either flags or segments) in one trip, and then stores them in the cache. +// on the underlying source. This operation fetches the entire set of a data +// kind (either flags or segments) in one trip, and then stores them in the +// cache. // // It's important that the "GetAll" operation also be bound by a TTL, so that -// repeated calls to "AllFlags" don't cause repeated calls to the source. The TTL -// for this operation is identical to the indivudal-item TTL configurable by +// repeated calls to "AllFlags" don't cause repeated calls to the source. The +// TTL for this operation is identical to the indivudal-item TTL configurable by // the user. // // An implication of the current implementation is that if Lazy Load is being // used to handle a "sparse" environment - that is, the environment is too big -// to load into memory, and so loading on demand is desirable - calling "AllFlags" -// will destroy that property because items are not actively evicted from -// the cache. On the other hand, that property could be used to prime the SDK's -// memory cache, preventing the need to individually load flags or segments. +// to load into memory, and so loading on demand is desirable - calling +// "AllFlags" will destroy that property because items are not actively evicted +// from the cache. On the other hand, that property could be used to prime the +// SDK's memory cache, preventing the need to individually load flags or +// segments. // -// The current design does not perform active eviction when an item is stale because -// it is generally better to serve stale data than none at all. If the source -// is unavailable, the SDK will be able to indefinitely serve the last known -// values. Active eviction can be added in the future as a configurable option. +// The current design does not perform active eviction when an item is stale +// because it is generally better to serve stale data than none at all. If the +// source is unavailable, the SDK will be able to indefinitely serve the last +// known values. Active eviction can be added in the future as a configurable +// option. #include "lazy_load_system.hpp" @@ -60,7 +63,8 @@ LazyLoad::LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg, TimeFn time) : logger_(logger), - reader_(std::make_unique(cfg.source)), + reader_(std::make_unique(logger, + cfg.source)), time_(std::move(time)), fresh_duration_(cfg.refresh_ttl) {} diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index e6d14cb8c..80f4fe567 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -221,15 +221,11 @@ TEST_F(LazyLoadTest, AllFlagsRefreshesIndividualFlag) { EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); - EXPECT_CALL(*mock_reader, All(testing::_)) // first call for flags + EXPECT_CALL(*mock_reader, All(testing::_)) .WillOnce(testing::Return( std::unordered_map{ {"foo", {2, false, "{\"key\":\"foo\",\"version\":2}"}}})); - EXPECT_CALL(*mock_reader, All(testing::_)) // second call for segments - .WillOnce(testing::Return( - std::unordered_map{})); } data_systems::LazyLoad const lazy_load(logger, config); From a28633eeaaab7b367309fcc42338f1375ca5f412 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 09:34:03 -0800 Subject: [PATCH 175/244] fix segfault --- .../lazy_load/lazy_load_system.cpp | 5 +++-- .../server-sdk/tests/lazy_load_system_test.cpp | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 0fd0f9274..0e94ce8e7 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -63,8 +63,9 @@ LazyLoad::LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg, TimeFn time) : logger_(logger), - reader_(std::make_unique(logger, - cfg.source)), + reader_(std::make_unique( + logger, + std::move(cfg.source))), time_(std::move(time)), fresh_duration_(cfg.refresh_ttl) {} diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index 80f4fe567..fa09b692b 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -29,23 +29,25 @@ class MockDataReader : public data_interfaces::ISerializedDataReader { (override, const)); MOCK_METHOD(std::string const&, Identity, (), (override, const)); MOCK_METHOD(bool, Initialized, (), (override, const)); + explicit MockDataReader(std::string name) : name_(std::move(name)) { + ON_CALL(*this, Identity).WillByDefault(testing::ReturnRef(name_)); + } + + private: + std::string const name_; }; class LazyLoadTest : public ::testing::Test { public: - std::string mock_reader_name; + std::string mock_reader_name = "fake reader"; std::shared_ptr> mock_reader; std::shared_ptr spy_logger_backend; Logger const logger; LazyLoadTest() - : mock_reader_name("fake reader"), - mock_reader(std::make_shared>()), + : mock_reader( + std::make_shared>(mock_reader_name)), spy_logger_backend(std::make_shared()), - logger(spy_logger_backend) { - ON_CALL(*mock_reader, Identity()).WillByDefault([&]() { - return mock_reader_name; - }); - } + logger(spy_logger_backend) {} }; TEST_F(LazyLoadTest, IdentityWrapsReaderIdentity) { From b203908ef8e516b0be15514844b8edaa8a0bce4b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 09:57:58 -0800 Subject: [PATCH 176/244] more tests --- .../tests/lazy_load_system_test.cpp | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index fa09b692b..d11536d44 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -15,6 +15,7 @@ using namespace launchdarkly::server_side::config; using ::testing::InSequence; using ::testing::NiceMock; +using ::testing::Return; class MockDataReader : public data_interfaces::ISerializedDataReader { public: @@ -244,3 +245,95 @@ TEST_F(LazyLoadTest, AllFlagsRefreshesIndividualFlag) { ASSERT_TRUE(flag2); ASSERT_EQ(flag2->version, 2); } + +TEST_F(LazyLoadTest, AllSegmentsRefreshesIndividualSegment) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + // We want to demonstrate that an individual segment will be + // refreshed not just when we grab that single segment, but also if + // we call AllSegments. + + { + InSequence s; + EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) + .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + 1, false, "{\"key\":\"foo\",\"version\":1}"})); + EXPECT_CALL(*mock_reader, All(testing::_)) + .WillOnce(testing::Return( + std::unordered_map{ + {"foo", {2, false, "{\"key\":\"foo\",\"version\":2}"}}})); + } + + data_systems::LazyLoad const lazy_load(logger, config); + + auto const segment1 = lazy_load.GetSegment("foo"); + ASSERT_TRUE(segment1); + ASSERT_EQ(segment1->version, 1); + + auto const all_segments = lazy_load.AllSegments(); + ASSERT_EQ(all_segments.size(), 1); + ASSERT_EQ(all_segments.at("foo")->version, 2); + + auto const segment2 = lazy_load.GetSegment("foo"); + ASSERT_TRUE(segment2); + ASSERT_EQ(segment2->version, 2); +} + +TEST_F(LazyLoadTest, InitializeNotQueriedRepeatedly) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); + + data_systems::LazyLoad const lazy_load(logger, config); + + for (std::size_t i = 0; i < 10; i++) { + ASSERT_FALSE(lazy_load.Initialized()); + } +} + +TEST_F(LazyLoadTest, InitializeCalledOnceThenNeverAgainAfterReturningTrue) { + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, + std::chrono::seconds(10), mock_reader}; + + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); + + data_systems::LazyLoad const lazy_load(logger, config); + + for (std::size_t i = 0; i < 10; i++) { + ASSERT_TRUE(lazy_load.Initialized()); + } +} + +TEST_F(LazyLoadTest, InitializeCalledAgainAfterTTL) { + using TimePoint = data_systems::LazyLoad::ClockType::time_point; + constexpr auto refresh_ttl = std::chrono::seconds(10); + + built::LazyLoadConfig const config{ + built::LazyLoadConfig::EvictionPolicy::Disabled, refresh_ttl, + mock_reader}; + + { + InSequence s; + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); + EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); + } + + TimePoint now{std::chrono::seconds(0)}; + data_systems::LazyLoad const lazy_load(logger, config, + [&]() { return now; }); + + for (std::size_t i = 0; i < 10; i++) { + ASSERT_FALSE(lazy_load.Initialized()); + now += std::chrono::seconds(1); + } + + for (std::size_t i = 0; i < 10; i++) { + ASSERT_TRUE(lazy_load.Initialized()); + } +} From b80b246e6da51743bc4c02ae1a3232d2dc237738 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 10:47:16 -0800 Subject: [PATCH 177/244] remove commented out initialization code in client --- libs/server-sdk/src/client_impl.cpp | 69 +++++++------------ .../memory_store/memory_store.hpp | 4 +- .../src/data_interfaces/store/istore.hpp | 5 ++ .../data_interfaces/system/idata_system.hpp | 8 +-- .../background_sync_system.cpp | 4 ++ .../background_sync_system.hpp | 2 + .../lazy_load/lazy_load_system.cpp | 20 +++++- .../lazy_load/lazy_load_system.hpp | 28 +++++++- .../tests/lazy_load_system_test.cpp | 51 +++++++------- 9 files changed, 109 insertions(+), 82 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 719e98918..78a89570d 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -116,48 +116,32 @@ ClientImpl::ClientImpl(Config config, std::string const& version) run_thread_ = std::move(std::thread([&]() { ioc_.run(); })); } -static bool IsInitializedSuccessfully(DataSourceStatus::DataSourceState state) { - return state == DataSourceStatus::DataSourceState::kValid; -} - -static bool IsInitialized(DataSourceStatus::DataSourceState state) { - return IsInitializedSuccessfully(state) || - (state != DataSourceStatus::DataSourceState::kInitializing); -} - void ClientImpl::Identify(Context context) { events_default_.Send([&](EventFactory const& factory) { return factory.Identify(std::move(context)); }); } -std::future ClientImpl::StartAsyncInternal( - std::function result_predicate) { +std::future ClientImpl::StartAsync() { auto pr = std::make_shared>(); auto fut = pr->get_future(); - status_manager_.OnDataSourceStatusChangeEx( - [result_predicate, pr](auto 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 */ - }); + status_manager_.OnDataSourceStatusChangeEx([this, pr](auto _) { + if (data_system_->Initialized()) { + pr->set_value(true); + return true; /* delete this change listener since the + desired state was reached */ + } + return false; /* keep the change listener */ + }); data_system_->Initialize(); return fut; } -std::future ClientImpl::StartAsync() { - return StartAsyncInternal(IsInitializedSuccessfully); -} - bool ClientImpl::Initialized() const { - return IsInitializedSuccessfully(status_manager_.Status().State()); + return data_system_->Initialized(); } AllFlagsState ClientImpl::AllFlagsState(Context const& context, @@ -165,23 +149,10 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, std::unordered_map 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 {}; - // } - - // TODO: Fix to use the single data source status, which takes into - // account whether there is any data ore not. + LD_LOG(logger_, LogLevel::kWarn) + << "AllFlagsState() called before client has finished " + "initializing. Data store not available. Returning empty state"; + return {}; } @@ -189,7 +160,15 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, EventScope no_events; - for (auto const& [k, v] : data_system_->AllFlags()) { + auto all_flags = data_system_->AllFlags(); + + // Because evaluating the flags may access many segments, tell the data + // system to fetch them all at once up-front. This may be a no-op + // depending on the data system (e.g. if the segments are all already in + // memory.) + auto _ = data_system_->AllSegments(); + + for (auto const& [k, v] : all_flags) { if (!v || !v->item) { continue; } @@ -440,7 +419,7 @@ IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { // } ClientImpl::~ClientImpl() { - data_system_->Shutdown(); // This is a blocking call. + data_system_->Shutdown(); // This is a blocking call. ioc_.stop(); // TODO(SC-219101) run_thread_.join(); diff --git a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp index 33a663ef6..93dfca485 100644 --- a/libs/server-sdk/src/data_components/memory_store/memory_store.hpp +++ b/libs/server-sdk/src/data_components/memory_store/memory_store.hpp @@ -28,11 +28,11 @@ class MemoryStore final : public data_interfaces::IStore, std::shared_ptr> AllSegments() const override; - [[nodiscard]] bool Initialized() const; + [[nodiscard]] bool Initialized() const override; [[nodiscard]] std::string const& Identity() const override; - void Init(launchdarkly::data_model::SDKDataSet dataSet) override; + void Init(data_model::SDKDataSet dataSet) override; void Upsert(std::string const& key, data_model::FlagDescriptor flag) override; diff --git a/libs/server-sdk/src/data_interfaces/store/istore.hpp b/libs/server-sdk/src/data_interfaces/store/istore.hpp index 1b5fd8bcc..a143a250f 100644 --- a/libs/server-sdk/src/data_interfaces/store/istore.hpp +++ b/libs/server-sdk/src/data_interfaces/store/istore.hpp @@ -47,6 +47,11 @@ class IStore { std::shared_ptr> AllSegments() const = 0; + /** + * @return True if the store has ever contained data. + */ + [[nodiscard]] virtual bool Initialized() const = 0; + virtual ~IStore() = default; IStore(IStore const& item) = delete; IStore(IStore&& item) = delete; diff --git a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp index 2019549f7..1d03078fa 100644 --- a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp +++ b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp @@ -5,11 +5,11 @@ namespace launchdarkly::server_side::data_interfaces { /** - * @brief IDataSystem obtains data used for flag evaluations and makes it available - * to other components. + * @brief IDataSystem obtains data used for flag evaluations and makes it + * available to other components. */ class IDataSystem : public IStore { -public: + public: /** * @return Identity of the system. Used in logs. */ @@ -33,7 +33,7 @@ class IDataSystem : public IStore { IDataSystem& operator=(IDataSystem const&) = delete; IDataSystem& operator=(IDataSystem&&) = delete; -protected: + protected: IDataSystem() = default; }; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 67446a758..c69e0654c 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -47,6 +47,10 @@ void BackgroundSync::Initialize() { nullptr /* no bootstrap data supported yet */); } +bool BackgroundSync::Initialized() const { + return store_.Initialized(); +} + void BackgroundSync::Shutdown() { auto promise = std::make_shared>(); auto const did_shutdown = promise->get_future(); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 280b58e49..0ea3cdcb7 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -59,6 +59,8 @@ class BackgroundSync final : public data_interfaces::IDataSystem { void Initialize() override; + bool Initialized() const override; + void Shutdown() override; private: diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 0e94ce8e7..0285acc69 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -54,18 +54,22 @@ data_components::FlagKind const LazyLoad::Kinds::Flag = data_components::SegmentKind const LazyLoad::Kinds::Segment = data_components::SegmentKind(); -LazyLoad::LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg) - : LazyLoad(logger, std::move(cfg), []() { +LazyLoad::LazyLoad(Logger const& logger, + config::built::LazyLoadConfig cfg, + data_components::DataSourceStatusManager& status_manager) + : LazyLoad(logger, std::move(cfg), status_manager, []() { return std::chrono::steady_clock::now(); }) {} LazyLoad::LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg, + data_components::DataSourceStatusManager& status_manager, TimeFn time) : logger_(logger), reader_(std::make_unique( logger, std::move(cfg.source))), + status_manager_(status_manager), time_(std::move(time)), fresh_duration_(cfg.refresh_ttl) {} @@ -74,7 +78,12 @@ std::string const& LazyLoad::Identity() const { return id; } -void LazyLoad::Initialize() {} +void LazyLoad::Initialize() { + status_manager_.SetState(DataSourceState::kInitializing); + if (Initialized()) { + status_manager_.SetState(DataSourceState::kValid); + } +} void LazyLoad::Shutdown() {} @@ -115,6 +124,11 @@ LazyLoad::AllSegments() const { } bool LazyLoad::Initialized() const { + /* Since the memory store isn't provisioned with an initial SDKDataSet + * like in the Background Sync system, we can't forward this call to + * MemoryStore::Initialized(). Instead, we need to check the state of the + * underlying source. */ + auto const state = tracker_.State(Keys::kInitialized, time_()); if (initialized_.has_value()) { /* Once initialized, we can always return true. */ diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index c3cefb7ad..32e92c9f4 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -3,6 +3,7 @@ #include "../../data_components/expiration_tracker/expiration_tracker.hpp" #include "../../data_components/kinds/kinds.hpp" #include "../../data_components/memory_store/memory_store.hpp" +#include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/source/idata_reader.hpp" #include "../../data_interfaces/system/idata_system.hpp" @@ -29,9 +30,13 @@ class LazyLoad final : public data_interfaces::IDataSystem { using ClockType = std::chrono::steady_clock; using TimeFn = std::function()>; - explicit LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg); + explicit LazyLoad(Logger const& logger, + config::built::LazyLoadConfig cfg, + data_components::DataSourceStatusManager& status_manager); + LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg, + data_components::DataSourceStatusManager& status_manager, TimeFn time); std::string const& Identity() const override; @@ -49,10 +54,11 @@ class LazyLoad final : public data_interfaces::IDataSystem { std::shared_ptr> AllSegments() const override; - bool Initialized() const; - void Initialize() override; + bool Initialized() const override; + + void Shutdown() override; // Public for usage in tests. @@ -94,6 +100,8 @@ class LazyLoad final : public data_interfaces::IDataSystem { tracker_.Add(kind, key, ExpiryTime()); if (auto expected_item = getter(key)) { + status_manager_.SetState(DataSourceState::kValid); + if (auto optional_item = *expected_item) { cache_.Upsert(key, std::move(*optional_item)); } else { @@ -110,6 +118,11 @@ class LazyLoad final : public data_interfaces::IDataSystem { } } } else { + status_manager_.SetState( + DataSourceState::kInterrupted, + common::data_sources::DataSourceStatusErrorKind::kUnknown, + expected_item.error()); + // If there's a persistent error, it will be logged at the refresh // interval. LD_LOG(logger_, LogLevel::kError) @@ -131,11 +144,18 @@ class LazyLoad final : public data_interfaces::IDataSystem { tracker_.Add(all_item_key, updated_expiry); if (auto all_items = getter()) { + status_manager_.SetState(DataSourceState::kValid); + for (auto item : *all_items) { cache_.Upsert(item.first, std::move(item.second)); tracker_.Add(item_kind, item.first, updated_expiry); } } else { + status_manager_.SetState( + DataSourceState::kInterrupted, + common::data_sources::DataSourceStatusErrorKind::kUnknown, + all_items.error()); + // If there's a persistent error, it will be logged at the // refresh interval. LD_LOG(logger_, LogLevel::kError) @@ -151,6 +171,8 @@ class LazyLoad final : public data_interfaces::IDataSystem { mutable data_components::MemoryStore cache_; std::unique_ptr reader_; + data_components::DataSourceStatusManager& status_manager_; + mutable data_components::ExpirationTracker tracker_; TimeFn time_; mutable std::optional initialized_; diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index d11536d44..cb42ed9d3 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -31,7 +31,7 @@ class MockDataReader : public data_interfaces::ISerializedDataReader { MOCK_METHOD(std::string const&, Identity, (), (override, const)); MOCK_METHOD(bool, Initialized, (), (override, const)); explicit MockDataReader(std::string name) : name_(std::move(name)) { - ON_CALL(*this, Identity).WillByDefault(testing::ReturnRef(name_)); + ON_CALL(*this, Identity).WillByDefault(::testing::ReturnRef(name_)); } private: @@ -44,6 +44,7 @@ class LazyLoadTest : public ::testing::Test { std::shared_ptr> mock_reader; std::shared_ptr spy_logger_backend; Logger const logger; + data_components::DataSourceStatusManager status_manager; LazyLoadTest() : mock_reader( std::make_shared>(mock_reader_name)), @@ -56,7 +57,7 @@ TEST_F(LazyLoadTest, IdentityWrapsReaderIdentity) { built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::milliseconds(100), mock_reader}; - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); ASSERT_EQ(lazy_load.Identity(), "lazy load via " + mock_reader_name + " (JSON)"); @@ -67,10 +68,10 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfFlagIsCached) { built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); for (std::size_t i = 0; i < 20; i++) { @@ -83,10 +84,10 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfSegmentIsCached) { built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); for (std::size_t i = 0; i < 20; i++) { @@ -99,10 +100,10 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfFlagCannotBeFetched) { built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(tl::make_unexpected( + .WillOnce(Return(tl::make_unexpected( data_interfaces::ISerializedDataReader::Error{"oops"}))); for (std::size_t i = 0; i < 20; i++) { @@ -118,10 +119,10 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfSegmentCannotBeFetched) { built::LazyLoadConfig::EvictionPolicy::Disabled, std::chrono::seconds(10), mock_reader}; - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(tl::make_unexpected( + .WillOnce(Return(tl::make_unexpected( data_interfaces::ISerializedDataReader::Error{"oops"}))); for (std::size_t i = 0; i < 20; i++) { @@ -143,16 +144,16 @@ TEST_F(LazyLoadTest, RefreshesFlagIfStale) { TimePoint now{std::chrono::seconds(0)}; - data_systems::LazyLoad const lazy_load(logger, config, + data_systems::LazyLoad const lazy_load(logger, config, status_manager, [&]() { return now; }); { InSequence s; EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 2, false, "{\"key\":\"foo\",\"version\":2}"})); } @@ -182,16 +183,16 @@ TEST_F(LazyLoadTest, RefreshesSegmentIfStale) { TimePoint now{std::chrono::seconds(0)}; - data_systems::LazyLoad const lazy_load(logger, config, + data_systems::LazyLoad const lazy_load(logger, config, status_manager, [&]() { return now; }); { InSequence s; EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 2, false, "{\"key\":\"foo\",\"version\":2}"})); } @@ -222,16 +223,16 @@ TEST_F(LazyLoadTest, AllFlagsRefreshesIndividualFlag) { { InSequence s; EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); EXPECT_CALL(*mock_reader, All(testing::_)) - .WillOnce(testing::Return( + .WillOnce(Return( std::unordered_map{ {"foo", {2, false, "{\"key\":\"foo\",\"version\":2}"}}})); } - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); auto const flag1 = lazy_load.GetFlag("foo"); ASSERT_TRUE(flag1); @@ -258,16 +259,16 @@ TEST_F(LazyLoadTest, AllSegmentsRefreshesIndividualSegment) { { InSequence s; EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) - .WillOnce(testing::Return(integrations::SerializedItemDescriptor{ + .WillOnce(Return(integrations::SerializedItemDescriptor{ 1, false, "{\"key\":\"foo\",\"version\":1}"})); EXPECT_CALL(*mock_reader, All(testing::_)) - .WillOnce(testing::Return( + .WillOnce(Return( std::unordered_map{ {"foo", {2, false, "{\"key\":\"foo\",\"version\":2}"}}})); } - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); auto const segment1 = lazy_load.GetSegment("foo"); ASSERT_TRUE(segment1); @@ -289,7 +290,7 @@ TEST_F(LazyLoadTest, InitializeNotQueriedRepeatedly) { EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(false)); - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); for (std::size_t i = 0; i < 10; i++) { ASSERT_FALSE(lazy_load.Initialized()); @@ -303,7 +304,7 @@ TEST_F(LazyLoadTest, InitializeCalledOnceThenNeverAgainAfterReturningTrue) { EXPECT_CALL(*mock_reader, Initialized).WillOnce(Return(true)); - data_systems::LazyLoad const lazy_load(logger, config); + data_systems::LazyLoad const lazy_load(logger, config, status_manager); for (std::size_t i = 0; i < 10; i++) { ASSERT_TRUE(lazy_load.Initialized()); @@ -325,7 +326,7 @@ TEST_F(LazyLoadTest, InitializeCalledAgainAfterTTL) { } TimePoint now{std::chrono::seconds(0)}; - data_systems::LazyLoad const lazy_load(logger, config, + data_systems::LazyLoad const lazy_load(logger, config, status_manager, [&]() { return now; }); for (std::size_t i = 0; i < 10; i++) { From 143f8c16608aecd02a9b710a3c23f0bf7bf9789c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 10:57:47 -0800 Subject: [PATCH 178/244] remove shutdown method --- libs/server-sdk/src/client_impl.cpp | 1 - .../server-sdk/src/data_interfaces/system/idata_system.hpp | 6 ------ .../background_sync/background_sync_system.cpp | 7 ------- .../background_sync/background_sync_system.hpp | 2 -- .../src/data_systems/lazy_load/lazy_load_system.cpp | 2 -- .../src/data_systems/lazy_load/lazy_load_system.hpp | 3 --- 6 files changed, 21 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 78a89570d..3040f8c55 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -419,7 +419,6 @@ IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { // } ClientImpl::~ClientImpl() { - data_system_->Shutdown(); // This is a blocking call. ioc_.stop(); // TODO(SC-219101) run_thread_.join(); diff --git a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp index 1d03078fa..0edf778db 100644 --- a/libs/server-sdk/src/data_interfaces/system/idata_system.hpp +++ b/libs/server-sdk/src/data_interfaces/system/idata_system.hpp @@ -21,12 +21,6 @@ class IDataSystem : public IStore { */ virtual void Initialize() = 0; - /** - * @brief Shuts down the system. This method will be called at some point - * after Initialize, after which no methods from IStore will be called. - */ - virtual void Shutdown() = 0; - virtual ~IDataSystem() override = default; IDataSystem(IDataSystem const& item) = delete; IDataSystem(IDataSystem&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index c69e0654c..6171aac3e 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -51,13 +51,6 @@ bool BackgroundSync::Initialized() const { return store_.Initialized(); } -void BackgroundSync::Shutdown() { - auto promise = std::make_shared>(); - auto const did_shutdown = promise->get_future(); - synchronizer_->ShutdownAsync([promise]() { promise->set_value(); }); - did_shutdown.wait(); -} - std::string const& BackgroundSync::Identity() const { static std::string id = "background sync via " + synchronizer_->Identity(); return id; diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 0ea3cdcb7..12de2092c 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -61,8 +61,6 @@ class BackgroundSync final : public data_interfaces::IDataSystem { bool Initialized() const override; - void Shutdown() override; - private: data_components::MemoryStore store_; data_components::ChangeNotifier change_notifier_; diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 0285acc69..26c246787 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -85,8 +85,6 @@ void LazyLoad::Initialize() { } } -void LazyLoad::Shutdown() {} - std::shared_ptr LazyLoad::GetFlag( std::string const& key) const { auto const state = diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 32e92c9f4..bc3dc3001 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -58,9 +58,6 @@ class LazyLoad final : public data_interfaces::IDataSystem { bool Initialized() const override; - - void Shutdown() override; - // Public for usage in tests. struct Kinds { static data_components::FlagKind const Flag; From 26341f64f6ab63070c8023500a0869c7adc5c12a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 11:04:28 -0800 Subject: [PATCH 179/244] fix contract tests --- libs/server-sdk/src/client_impl.cpp | 13 ++++--------- libs/server-sdk/src/client_impl.hpp | 2 +- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 3040f8c55..64bb4ec0b 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -288,11 +288,10 @@ EvaluationDetail ClientImpl::VariationInternal( } std::optional ClientImpl::PreEvaluationChecks( - Context const& context) { - // if (!memory_store_.Initialized()) { - // return EvaluationReason::ErrorKind::kClientNotReady; - // } - // TODO: Check if initialized + Context const& context) const { + if (!Initialized()) { + return EvaluationReason::ErrorKind::kClientNotReady; + } if (!context.Valid()) { return EvaluationReason::ErrorKind::kUserNotSpecified; } @@ -414,10 +413,6 @@ IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { return status_manager_; } -// flag_manager::IFlagNotifier& ClientImpl::FlagNotifier() { -// return flag_manager_.Notifier(); -// } - ClientImpl::~ClientImpl() { ioc_.stop(); // TODO(SC-219101) diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index bf18f862c..1543e4925 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -145,7 +145,7 @@ class ClientImpl : public IClient { std::optional const& flag); [[nodiscard]] std::optional - PreEvaluationChecks(Context const& context); + PreEvaluationChecks(Context const& context) const; void TrackInternal(Context const& ctx, std::string event_name, From de8f0b871abb71f85f1c53c592f775462dad4d08 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 11:38:06 -0800 Subject: [PATCH 180/244] fix test --- libs/server-sdk/tests/server_c_bindings_test.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index 5a684fdf3..b6b043d1b 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -52,9 +52,11 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { LDListenerConnection connection = LDServerSDK_DataSourceStatus_OnStatusChange(sdk, listener); - bool success = false; + bool success = true; LDServerSDK_Start(sdk, 3000, &success); - EXPECT_TRUE(success); + + // Since we're offline, the SDK won't become initialized. + EXPECT_FALSE(success); LDListenerConnection_Disconnect(connection); From 2d737b671925f0908abfa7d364073a6cdb91b122 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 11:39:20 -0800 Subject: [PATCH 181/244] typo --- libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index bc3dc3001..09b5130d5 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -178,7 +178,7 @@ class LazyLoad final : public data_interfaces::IDataSystem { struct Keys { static inline std::string const kAllFlags = "allFlags"; - static inline std::string const kAllSegments = "allFlags"; + static inline std::string const kAllSegments = "allSegments"; static inline std::string const kInitialized = "initialized"; }; }; From a9187f6bf23097eb39ba19bef25d424ce4ad8fc1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 12:13:37 -0800 Subject: [PATCH 182/244] add redis example --- examples/CMakeLists.txt | 4 + .../hello-cpp-server-redis/CMakeLists.txt | 15 +++ examples/hello-cpp-server-redis/main.cpp | 96 +++++++++++++++++++ .../integrations/redis/redis_source.hpp | 8 +- .../lazy_load/sources/redis/redis_source.cpp | 16 ++-- 5 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 examples/hello-cpp-server-redis/CMakeLists.txt create mode 100644 examples/hello-cpp-server-redis/main.cpp diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 138ec4942..f013bc666 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,3 +3,7 @@ add_subdirectory(hello-cpp-client) add_subdirectory(hello-cpp-server) add_subdirectory(hello-c-server) add_subdirectory(client-and-server-coexistence) + +if (LD_BUILD_REDIS_SUPPORT) + add_subdirectory(hello-cpp-server-redis) +endif () diff --git a/examples/hello-cpp-server-redis/CMakeLists.txt b/examples/hello-cpp-server-redis/CMakeLists.txt new file mode 100644 index 000000000..80287d646 --- /dev/null +++ b/examples/hello-cpp-server-redis/CMakeLists.txt @@ -0,0 +1,15 @@ +# Required for Apple Silicon support. +cmake_minimum_required(VERSION 3.19) + +project( + LaunchDarklyHelloCPPServerRedisSource + VERSION 0.1 + DESCRIPTION "LaunchDarkly Hello CPP Server-side SDK with Redis source" + LANGUAGES CXX +) + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_executable(hello-cpp-server-redis-source main.cpp) +target_link_libraries(hello-cpp-server-redis-source PRIVATE launchdarkly::server Threads::Threads) diff --git a/examples/hello-cpp-server-redis/main.cpp b/examples/hello-cpp-server-redis/main.cpp new file mode 100644 index 000000000..9cb0d1547 --- /dev/null +++ b/examples/hello-cpp-server-redis/main.cpp @@ -0,0 +1,96 @@ +#include +#include +#include + +#include + +#include +#include + +// 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" + +// Set INIT_TIMEOUT_MILLISECONDS to the amount of time you will wait for +// the client to become initialized. +#define INIT_TIMEOUT_MILLISECONDS 3000 + +char const* get_with_env_fallback(char const* source_val, + char const* env_variable, + char const* error_msg); +using namespace launchdarkly; +using namespace launchdarkly::server_side; + +int main() { + char const* sdk_key = get_with_env_fallback( + SDK_KEY, "LD_SDK_KEY", + "Please edit main.c to set SDK_KEY to your LaunchDarkly server-side " + "SDK key " + "first.\n\nAlternatively, set the LD_SDK_KEY environment " + "variable.\n" + "The value of SDK_KEY in main.c takes priority over LD_SDK_KEY."); + + auto config_builder = ConfigBuilder(sdk_key); + + using LazyLoad = server_side::config::builders::LazyLoadBuilder; + + auto const redis = std::make_shared( + "redis://localhost:6379", "launchdarkly"); + + config_builder.DataSystem().Method( + LazyLoad().Source(redis).CacheRefresh(std::chrono::seconds(30))); + + auto config = config_builder.Build(); + if (!config) { + std::cout << "error: config is invalid: " << config.error() << '\n'; + return 1; + } + + auto client = Client(std::move(*config)); + + auto start_result = client.StartAsync(); + + if (auto const status = start_result.wait_for( + std::chrono::milliseconds(INIT_TIMEOUT_MILLISECONDS)); + 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 const context = + ContextBuilder().Kind("user", "example-user-key").Name("Sandy").Build(); + + bool const 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; +} + +char const* get_with_env_fallback(char const* source_val, + char const* env_variable, + char const* error_msg) { + if (strlen(source_val)) { + return source_val; + } + + if (char const* from_env = std::getenv(env_variable); + from_env && strlen(from_env)) { + return from_env; + } + + std::cout << "*** " << error_msg << std::endl; + std::exit(1); +} diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index 0c4502db9..021bfa87b 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -2,7 +2,9 @@ #include -#include +namespace sw::redis { +class Redis; +} namespace launchdarkly::server_side::data_systems { @@ -16,12 +18,14 @@ class RedisDataSource final : public data_interfaces::ISerializedDataReader { [[nodiscard]] std::string const& Identity() const override; [[nodiscard]] bool Initialized() const override; + ~RedisDataSource(); + private: std::string const prefix_; std::string const inited_key_; std::string key_for_kind( integrations::ISerializedItemKind const& kind) const; - mutable sw::redis::Redis redis_; + std::unique_ptr redis_; }; } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp index 231152ef5..3bda6a6a2 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -1,5 +1,7 @@ #include +#include + namespace launchdarkly::server_side::data_systems { std::string RedisDataSource::key_for_kind( @@ -10,12 +12,14 @@ std::string RedisDataSource::key_for_kind( RedisDataSource::RedisDataSource(std::string uri, std::string prefix) : prefix_(std::move(prefix)), inited_key_(prefix_ + ":$inited"), - redis_(std::move(uri)) {} + redis_(std::make_unique(std::move(uri))) {} + +RedisDataSource::~RedisDataSource() = default; data_interfaces::ISerializedDataReader::GetResult RedisDataSource::Get( integrations::ISerializedItemKind const& kind, std::string const& itemKey) const { - if (auto maybe_item = redis_.hget(key_for_kind(kind), itemKey)) { + if (auto maybe_item = redis_->hget(key_for_kind(kind), itemKey)) { return integrations::SerializedItemDescriptor{0, false, maybe_item.value()}; } @@ -26,8 +30,8 @@ data_interfaces::ISerializedDataReader::AllResult RedisDataSource::All( integrations::ISerializedItemKind const& kind) const { std::unordered_map raw_items; AllResult::value_type items; - redis_.hgetall(key_for_kind(kind), - std::inserter(raw_items, raw_items.begin())); + redis_->hgetall(key_for_kind(kind), + std::inserter(raw_items, raw_items.begin())); for (auto const& [key, val] : raw_items) { items.emplace(key, integrations::SerializedItemDescriptor{0, false, val}); @@ -36,12 +40,12 @@ data_interfaces::ISerializedDataReader::AllResult RedisDataSource::All( } std::string const& RedisDataSource::Identity() const { - static std::string const identity = "redis source"; + static std::string const identity = "redis"; return identity; } bool RedisDataSource::Initialized() const { - return redis_.exists(inited_key_) == 1; + return redis_->exists(inited_key_) == 1; } } // namespace launchdarkly::server_side::data_systems From f07c8acdc78fcad82462df5add1824e2c826aa35 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 12:50:23 -0800 Subject: [PATCH 183/244] add offline data system and revert test change --- libs/server-sdk/src/CMakeLists.txt | 2 + libs/server-sdk/src/client_impl.cpp | 24 ++++++++---- .../background_sync_system.cpp | 7 ---- .../background_sync_system.hpp | 5 --- libs/server-sdk/src/data_systems/offline.cpp | 38 +++++++++++++++++++ libs/server-sdk/src/data_systems/offline.hpp | 25 ++++++++++++ .../tests/server_c_bindings_test.cpp | 6 +-- 7 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 libs/server-sdk/src/data_systems/offline.cpp create mode 100644 libs/server-sdk/src/data_systems/offline.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 9ed46c6e2..904b55d1a 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -55,6 +55,8 @@ target_sources(${LIBNAME} data_systems/background_sync/background_sync_system.cpp data_systems/lazy_load/lazy_load_system.hpp data_systems/lazy_load/lazy_load_system.cpp + data_systems/offline.hpp + data_systems/offline.cpp data_interfaces.cpp evaluation/evaluator.cpp evaluation/rules.cpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 64bb4ec0b..2b371634d 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -2,6 +2,8 @@ #include "all_flags_state/all_flags_state_builder.hpp" #include "data_systems/background_sync/background_sync_system.hpp" +#include "data_systems/lazy_load/lazy_load_system.hpp" +#include "data_systems/offline.hpp" #include "data_interfaces/system/idata_system.hpp" @@ -38,8 +40,7 @@ static std::unique_ptr MakeDataSystem( data_components::DataSourceStatusManager& status_manager, Logger& logger) { if (config.DataSystemConfig().disabled) { - return std::make_unique(executor, - status_manager); + return std::make_unique(); } auto const builder = @@ -47,12 +48,21 @@ static std::unique_ptr MakeDataSystem( auto data_source_properties = builder.Build(); - auto const bg_sync_config = std::get( + return std::visit( + [&](auto&& arg) -> std::unique_ptr { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::make_unique( + config.ServiceEndpoints(), arg, data_source_properties, + executor, status_manager, logger); + } else if constexpr (std::is_same_v< + T, config::built::LazyLoadConfig>) { + return std::make_unique(logger, arg, + status_manager); + } + }, config.DataSystemConfig().system_); - - return std::make_unique( - config.ServiceEndpoints(), bg_sync_config, data_source_properties, - executor, status_manager, logger); } static Logger MakeLogger(config::built::Logging const& config) { diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 6171aac3e..1c42d110e 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -35,13 +35,6 @@ BackgroundSync::BackgroundSync( background_sync_config.synchronizer_); } -BackgroundSync::BackgroundSync( - boost::asio::any_io_executor ioc, - data_components::DataSourceStatusManager& status_manager) - : store_(), - change_notifier_(store_, store_), - synchronizer_(std::make_shared(ioc, status_manager)) {} - void BackgroundSync::Initialize() { synchronizer_->StartAsync(&change_notifier_, nullptr /* no bootstrap data supported yet */); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 12de2092c..0a0d08d6c 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -34,11 +34,6 @@ class BackgroundSync final : public data_interfaces::IDataSystem { data_components::DataSourceStatusManager& status_manager, Logger const& logger); - /** - * @brief Constructs a BackgroundSync without a data source. - */ - BackgroundSync(boost::asio::any_io_executor ioc, - data_components::DataSourceStatusManager& status_manager); BackgroundSync(BackgroundSync const& item) = delete; BackgroundSync(BackgroundSync&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/offline.cpp b/libs/server-sdk/src/data_systems/offline.cpp new file mode 100644 index 000000000..f7bd3618c --- /dev/null +++ b/libs/server-sdk/src/data_systems/offline.cpp @@ -0,0 +1,38 @@ +#include "offline.hpp" + +namespace launchdarkly::server_side::data_systems { + +std::shared_ptr OfflineSystem::GetFlag( + std::string const& key) const { + return nullptr; +} + +std::shared_ptr OfflineSystem::GetSegment( + std::string const& key) const { + return nullptr; +} + +std::unordered_map> +OfflineSystem::AllFlags() const { + return std::unordered_map>(); +} + +std::unordered_map> +OfflineSystem::AllSegments() const { + return std::unordered_map>(); +} + +bool OfflineSystem::Initialized() const { + return true; +} + +std::string const& OfflineSystem::Identity() const { + static std::string const ident = "offline"; + return ident; +} + +void Initialize() {} + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/offline.hpp b/libs/server-sdk/src/data_systems/offline.hpp new file mode 100644 index 000000000..3b7ba358f --- /dev/null +++ b/libs/server-sdk/src/data_systems/offline.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "../data_interfaces/system/idata_system.hpp" + +namespace launchdarkly::server_side::data_systems { + +class OfflineSystem final : public data_interfaces::IDataSystem { + public: + [[nodiscard]] std::shared_ptr GetFlag( + std::string const& key) const override; + [[nodiscard]] std::shared_ptr GetSegment( + std::string const& key) const override; + [[nodiscard]] std:: + unordered_map> + AllFlags() const override; + [[nodiscard]] std::unordered_map< + std::string, + std::shared_ptr> + AllSegments() const override; + [[nodiscard]] bool Initialized() const override; + [[nodiscard]] std::string const& Identity() const override; + void Initialize() override; +}; + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index b6b043d1b..5a684fdf3 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -52,11 +52,9 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { LDListenerConnection connection = LDServerSDK_DataSourceStatus_OnStatusChange(sdk, listener); - bool success = true; + bool success = false; LDServerSDK_Start(sdk, 3000, &success); - - // Since we're offline, the SDK won't become initialized. - EXPECT_FALSE(success); + EXPECT_TRUE(success); LDListenerConnection_Disconnect(connection); From 37738f94e176eb6d62b61a5b401c9c5882df17c7 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 12:50:23 -0800 Subject: [PATCH 184/244] add offline data system and revert test change --- libs/server-sdk/src/CMakeLists.txt | 2 + libs/server-sdk/src/client_impl.cpp | 24 ++++++++---- .../background_sync_system.cpp | 7 ---- .../background_sync_system.hpp | 5 --- libs/server-sdk/src/data_systems/offline.cpp | 38 +++++++++++++++++++ libs/server-sdk/src/data_systems/offline.hpp | 25 ++++++++++++ .../tests/server_c_bindings_test.cpp | 6 +-- 7 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 libs/server-sdk/src/data_systems/offline.cpp create mode 100644 libs/server-sdk/src/data_systems/offline.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 9ed46c6e2..904b55d1a 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -55,6 +55,8 @@ target_sources(${LIBNAME} data_systems/background_sync/background_sync_system.cpp data_systems/lazy_load/lazy_load_system.hpp data_systems/lazy_load/lazy_load_system.cpp + data_systems/offline.hpp + data_systems/offline.cpp data_interfaces.cpp evaluation/evaluator.cpp evaluation/rules.cpp diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 64bb4ec0b..2b371634d 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -2,6 +2,8 @@ #include "all_flags_state/all_flags_state_builder.hpp" #include "data_systems/background_sync/background_sync_system.hpp" +#include "data_systems/lazy_load/lazy_load_system.hpp" +#include "data_systems/offline.hpp" #include "data_interfaces/system/idata_system.hpp" @@ -38,8 +40,7 @@ static std::unique_ptr MakeDataSystem( data_components::DataSourceStatusManager& status_manager, Logger& logger) { if (config.DataSystemConfig().disabled) { - return std::make_unique(executor, - status_manager); + return std::make_unique(); } auto const builder = @@ -47,12 +48,21 @@ static std::unique_ptr MakeDataSystem( auto data_source_properties = builder.Build(); - auto const bg_sync_config = std::get( + return std::visit( + [&](auto&& arg) -> std::unique_ptr { + using T = std::decay_t; + if constexpr (std::is_same_v) { + return std::make_unique( + config.ServiceEndpoints(), arg, data_source_properties, + executor, status_manager, logger); + } else if constexpr (std::is_same_v< + T, config::built::LazyLoadConfig>) { + return std::make_unique(logger, arg, + status_manager); + } + }, config.DataSystemConfig().system_); - - return std::make_unique( - config.ServiceEndpoints(), bg_sync_config, data_source_properties, - executor, status_manager, logger); } static Logger MakeLogger(config::built::Logging const& config) { diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 6171aac3e..1c42d110e 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -35,13 +35,6 @@ BackgroundSync::BackgroundSync( background_sync_config.synchronizer_); } -BackgroundSync::BackgroundSync( - boost::asio::any_io_executor ioc, - data_components::DataSourceStatusManager& status_manager) - : store_(), - change_notifier_(store_, store_), - synchronizer_(std::make_shared(ioc, status_manager)) {} - void BackgroundSync::Initialize() { synchronizer_->StartAsync(&change_notifier_, nullptr /* no bootstrap data supported yet */); diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 12de2092c..0a0d08d6c 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -34,11 +34,6 @@ class BackgroundSync final : public data_interfaces::IDataSystem { data_components::DataSourceStatusManager& status_manager, Logger const& logger); - /** - * @brief Constructs a BackgroundSync without a data source. - */ - BackgroundSync(boost::asio::any_io_executor ioc, - data_components::DataSourceStatusManager& status_manager); BackgroundSync(BackgroundSync const& item) = delete; BackgroundSync(BackgroundSync&& item) = delete; diff --git a/libs/server-sdk/src/data_systems/offline.cpp b/libs/server-sdk/src/data_systems/offline.cpp new file mode 100644 index 000000000..69b040690 --- /dev/null +++ b/libs/server-sdk/src/data_systems/offline.cpp @@ -0,0 +1,38 @@ +#include "offline.hpp" + +namespace launchdarkly::server_side::data_systems { + +std::shared_ptr OfflineSystem::GetFlag( + std::string const& key) const { + return nullptr; +} + +std::shared_ptr OfflineSystem::GetSegment( + std::string const& key) const { + return nullptr; +} + +std::unordered_map> +OfflineSystem::AllFlags() const { + return std::unordered_map>(); +} + +std::unordered_map> +OfflineSystem::AllSegments() const { + return std::unordered_map>(); +} + +bool OfflineSystem::Initialized() const { + return true; +} + +std::string const& OfflineSystem::Identity() const { + static std::string const ident = "offline"; + return ident; +} + +void OfflineSystem::Initialize() {} + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/offline.hpp b/libs/server-sdk/src/data_systems/offline.hpp new file mode 100644 index 000000000..3b7ba358f --- /dev/null +++ b/libs/server-sdk/src/data_systems/offline.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "../data_interfaces/system/idata_system.hpp" + +namespace launchdarkly::server_side::data_systems { + +class OfflineSystem final : public data_interfaces::IDataSystem { + public: + [[nodiscard]] std::shared_ptr GetFlag( + std::string const& key) const override; + [[nodiscard]] std::shared_ptr GetSegment( + std::string const& key) const override; + [[nodiscard]] std:: + unordered_map> + AllFlags() const override; + [[nodiscard]] std::unordered_map< + std::string, + std::shared_ptr> + AllSegments() const override; + [[nodiscard]] bool Initialized() const override; + [[nodiscard]] std::string const& Identity() const override; + void Initialize() override; +}; + +} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/tests/server_c_bindings_test.cpp b/libs/server-sdk/tests/server_c_bindings_test.cpp index b6b043d1b..5a684fdf3 100644 --- a/libs/server-sdk/tests/server_c_bindings_test.cpp +++ b/libs/server-sdk/tests/server_c_bindings_test.cpp @@ -52,11 +52,9 @@ TEST(ClientBindings, RegisterDataSourceStatusChangeListener) { LDListenerConnection connection = LDServerSDK_DataSourceStatus_OnStatusChange(sdk, listener); - bool success = true; + bool success = false; LDServerSDK_Start(sdk, 3000, &success); - - // Since we're offline, the SDK won't become initialized. - EXPECT_FALSE(success); + EXPECT_TRUE(success); LDListenerConnection_Disconnect(connection); From 2f699190ae02d9cdead2c3a6ee8481e402cfec23 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 13:36:57 -0800 Subject: [PATCH 185/244] update redis example --- examples/hello-cpp-server-redis/main.cpp | 12 +++-- .../integrations/redis/redis_source.hpp | 10 +++- .../lazy_load/sources/redis/redis_source.cpp | 52 ++++++++++++++----- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/examples/hello-cpp-server-redis/main.cpp b/examples/hello-cpp-server-redis/main.cpp index 9cb0d1547..cf3c0b477 100644 --- a/examples/hello-cpp-server-redis/main.cpp +++ b/examples/hello-cpp-server-redis/main.cpp @@ -36,11 +36,17 @@ int main() { using LazyLoad = server_side::config::builders::LazyLoadBuilder; - auto const redis = std::make_shared( - "redis://localhost:6379", "launchdarkly"); + auto redis = data_systems::RedisDataSource::Create("redis://localhost:6379", + "launchdarkly"); + + if (!redis) { + std::cout << "error: redis config is invalid: " << redis.error() + << '\n'; + return 1; + } config_builder.DataSystem().Method( - LazyLoad().Source(redis).CacheRefresh(std::chrono::seconds(30))); + LazyLoad().Source(*redis).CacheRefresh(std::chrono::seconds(30))); auto config = config_builder.Build(); if (!config) { diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index 021bfa87b..2cb97f8c1 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -2,6 +2,10 @@ #include +#include + +#include + namespace sw::redis { class Redis; } @@ -10,7 +14,9 @@ namespace launchdarkly::server_side::data_systems { class RedisDataSource final : public data_interfaces::ISerializedDataReader { public: - RedisDataSource(std::string uri, std::string prefix); + + static tl::expected, std::string> Create(std::string uri, std::string prefix); + [[nodiscard]] GetResult Get(integrations::ISerializedItemKind const& kind, std::string const& itemKey) const override; [[nodiscard]] AllResult All( @@ -21,6 +27,8 @@ class RedisDataSource final : public data_interfaces::ISerializedDataReader { ~RedisDataSource(); private: + RedisDataSource(std::unique_ptr redis, std::string prefix); + std::string const prefix_; std::string const inited_key_; std::string key_for_kind( diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp index 3bda6a6a2..574146299 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -4,39 +4,61 @@ namespace launchdarkly::server_side::data_systems { +tl::expected, std::string> +RedisDataSource::Create(std::string uri, std::string prefix) { + try { + return std::shared_ptr(new RedisDataSource( + std::make_unique(std::move(uri)), + std::move(prefix))); + } catch (sw::redis::Error const& e) { + return tl::make_unexpected(e.what()); + } +} + std::string RedisDataSource::key_for_kind( integrations::ISerializedItemKind const& kind) const { return prefix_ + ":" + kind.Namespace(); } -RedisDataSource::RedisDataSource(std::string uri, std::string prefix) +RedisDataSource::RedisDataSource(std::unique_ptr redis, + std::string prefix) : prefix_(std::move(prefix)), inited_key_(prefix_ + ":$inited"), - redis_(std::make_unique(std::move(uri))) {} + redis_(std::move(redis)) {} RedisDataSource::~RedisDataSource() = default; data_interfaces::ISerializedDataReader::GetResult RedisDataSource::Get( integrations::ISerializedItemKind const& kind, std::string const& itemKey) const { - if (auto maybe_item = redis_->hget(key_for_kind(kind), itemKey)) { - return integrations::SerializedItemDescriptor{0, false, - maybe_item.value()}; + try { + if (auto maybe_item = redis_->hget(key_for_kind(kind), itemKey)) { + return integrations::SerializedItemDescriptor{0, false, + maybe_item.value()}; + } + return tl::make_unexpected(Error{"not found"}); + } catch (sw::redis::Error const& e) { + return tl::make_unexpected(Error{e.what()}); } - return tl::make_unexpected(Error{"not found"}); } data_interfaces::ISerializedDataReader::AllResult RedisDataSource::All( integrations::ISerializedItemKind const& kind) const { std::unordered_map raw_items; AllResult::value_type items; - redis_->hgetall(key_for_kind(kind), - std::inserter(raw_items, raw_items.begin())); - for (auto const& [key, val] : raw_items) { - items.emplace(key, - integrations::SerializedItemDescriptor{0, false, val}); + + try { + redis_->hgetall(key_for_kind(kind), + std::inserter(raw_items, raw_items.begin())); + for (auto const& [key, val] : raw_items) { + items.emplace( + key, integrations::SerializedItemDescriptor{0, false, val}); + } + return items; + + } catch (sw::redis::Error const& e) { + return tl::make_unexpected(Error{e.what()}); } - return items; } std::string const& RedisDataSource::Identity() const { @@ -45,7 +67,11 @@ std::string const& RedisDataSource::Identity() const { } bool RedisDataSource::Initialized() const { - return redis_->exists(inited_key_) == 1; + try { + return redis_->exists(inited_key_) == 1; + } catch (sw::redis::Error const& e) { + return false; + } } } // namespace launchdarkly::server_side::data_systems From 368dd12a552f66ac9ec599bbf826f875df146e11 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 13:43:00 -0800 Subject: [PATCH 186/244] fix offline tests --- libs/server-sdk/src/client_impl.cpp | 2 +- libs/server-sdk/src/data_systems/offline.cpp | 10 ++++++++-- libs/server-sdk/src/data_systems/offline.hpp | 5 +++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index 2b371634d..c8703ab15 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -40,7 +40,7 @@ static std::unique_ptr MakeDataSystem( data_components::DataSourceStatusManager& status_manager, Logger& logger) { if (config.DataSystemConfig().disabled) { - return std::make_unique(); + return std::make_unique(status_manager); } auto const builder = diff --git a/libs/server-sdk/src/data_systems/offline.cpp b/libs/server-sdk/src/data_systems/offline.cpp index 69b040690..148c10a2d 100644 --- a/libs/server-sdk/src/data_systems/offline.cpp +++ b/libs/server-sdk/src/data_systems/offline.cpp @@ -2,6 +2,10 @@ namespace launchdarkly::server_side::data_systems { +OfflineSystem::OfflineSystem( + data_components::DataSourceStatusManager& status_manager) + : status_manager_(status_manager) {} + std::shared_ptr OfflineSystem::GetFlag( std::string const& key) const { return nullptr; @@ -24,6 +28,10 @@ OfflineSystem::AllSegments() const { std::shared_ptr>(); } +void OfflineSystem::Initialize() { + status_manager_.SetState(DataSourceState::kValid); +} + bool OfflineSystem::Initialized() const { return true; } @@ -33,6 +41,4 @@ std::string const& OfflineSystem::Identity() const { return ident; } -void OfflineSystem::Initialize() {} - } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/offline.hpp b/libs/server-sdk/src/data_systems/offline.hpp index 3b7ba358f..c9355bd24 100644 --- a/libs/server-sdk/src/data_systems/offline.hpp +++ b/libs/server-sdk/src/data_systems/offline.hpp @@ -1,11 +1,13 @@ #pragma once +#include "../data_components/status_notifications/data_source_status_manager.hpp" #include "../data_interfaces/system/idata_system.hpp" namespace launchdarkly::server_side::data_systems { class OfflineSystem final : public data_interfaces::IDataSystem { public: + OfflineSystem(data_components::DataSourceStatusManager& status_manager); [[nodiscard]] std::shared_ptr GetFlag( std::string const& key) const override; [[nodiscard]] std::shared_ptr GetSegment( @@ -20,6 +22,9 @@ class OfflineSystem final : public data_interfaces::IDataSystem { [[nodiscard]] bool Initialized() const override; [[nodiscard]] std::string const& Identity() const override; void Initialize() override; + + private: + data_components::DataSourceStatusManager& status_manager_; }; } // namespace launchdarkly::server_side::data_systems From c9e2e9246f445b3684de96ec6f0ca9e918436105 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 13:45:55 -0800 Subject: [PATCH 187/244] delete old noop data source --- libs/server-sdk/src/CMakeLists.txt | 2 -- .../background_sync_system.cpp | 2 -- .../sources/noop/null_data_source.cpp | 26 ------------------ .../sources/noop/null_data_source.hpp | 27 ------------------- 4 files changed, 57 deletions(-) delete mode 100644 libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp delete mode 100644 libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 904b55d1a..908b87f28 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -43,8 +43,6 @@ target_sources(${LIBNAME} data_components/serialization_adapters/json_destination.cpp data_components/kinds/kinds.hpp data_components/kinds/kinds.cpp - data_systems/background_sync/sources/noop/null_data_source.hpp - data_systems/background_sync/sources/noop/null_data_source.cpp data_systems/background_sync/sources/polling/polling_data_source.hpp data_systems/background_sync/sources/polling/polling_data_source.cpp data_systems/background_sync/sources/streaming/streaming_data_source.hpp diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp index 1c42d110e..1a6de5c0a 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.cpp @@ -1,6 +1,5 @@ #include "background_sync_system.hpp" -#include "sources/noop/null_data_source.hpp" #include "sources/polling/polling_data_source.hpp" #include "sources/streaming/streaming_data_source.hpp" @@ -23,7 +22,6 @@ BackgroundSync::BackgroundSync( synchronizer_ = std::make_shared( ioc, logger, status_manager, endpoints, method_config, http_properties); - } else if constexpr (std::is_same_v< T, config::built::BackgroundSyncConfig:: PollingConfig>) { diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp deleted file mode 100644 index a6b70f2c7..000000000 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.cpp +++ /dev/null @@ -1,26 +0,0 @@ -#include "null_data_source.hpp" - -#include - -namespace launchdarkly::server_side::data_systems { - -void NullDataSource::StartAsync(data_interfaces::IDestination* destination, - data_model::SDKDataSet const* initial_data) { - status_manager_.SetState(DataSourceStatus::DataSourceState::kValid); -} - -void NullDataSource::ShutdownAsync(std::function complete) { - boost::asio::post(exec_, complete); -} - -std::string const& NullDataSource::Identity() const { - static std::string const identity = "no-op data source"; - return identity; -} - -NullDataSource::NullDataSource( - boost::asio::any_io_executor exec, - data_components::DataSourceStatusManager& status_manager) - : status_manager_(status_manager), exec_(exec) {} - -} // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp b/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp deleted file mode 100644 index 657cf49d2..000000000 --- a/libs/server-sdk/src/data_systems/background_sync/sources/noop/null_data_source.hpp +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include "../../../../data_components/status_notifications/data_source_status_manager.hpp" -#include "../../../../data_interfaces/source/idata_synchronizer.hpp" - -#include - -namespace launchdarkly::server_side::data_systems { - -class NullDataSource : public data_interfaces::IDataSynchronizer { - public: - explicit NullDataSource( - boost::asio::any_io_executor exec, - data_components::DataSourceStatusManager& status_manager); - - void StartAsync(data_interfaces::IDestination* destination, - data_model::SDKDataSet const* initial_data) override; - void ShutdownAsync(std::function) override; - - [[nodiscard]] std::string const& Identity() const override; - - private: - data_components::DataSourceStatusManager& status_manager_; - boost::asio::any_io_executor exec_; -}; - -} // namespace launchdarkly::server_side::data_systems From 0f492eda31540d8e1c852b8f7106d9d86dd9857c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 14:06:33 -0800 Subject: [PATCH 188/244] fix formatting of backgroudn_sync_system.hpp --- .../src/data_systems/background_sync/background_sync_system.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp index 0a0d08d6c..a302b7d0d 100644 --- a/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp +++ b/libs/server-sdk/src/data_systems/background_sync/background_sync_system.hpp @@ -34,7 +34,6 @@ class BackgroundSync final : public data_interfaces::IDataSystem { data_components::DataSourceStatusManager& status_manager, Logger const& logger); - BackgroundSync(BackgroundSync const& item) = delete; BackgroundSync(BackgroundSync&& item) = delete; BackgroundSync& operator=(BackgroundSync const&) = delete; From 5fe1a71e08bdf8388457f11478c3eb328f8f3354 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 15:42:19 -0800 Subject: [PATCH 189/244] fix cmake inclusion of Redis --- cmake/redis-plus-plus.cmake | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index bd6f77756..116663e43 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -6,13 +6,19 @@ include(FetchContent) FetchContent_Declare(hiredis GIT_REPOSITORY https://github.com/redis/hiredis.git GIT_TAG 60e5075d4ac77424809f855ba3e398df7aacefe8 + GIT_SHALLOW TRUE + SOURCE_DIR _deps/hiredis + OVERRIDE_FIND_PACKAGE ) FetchContent_MakeAvailable(hiredis) +include_directories(${CMAKE_BINARY_DIR}/_deps) + FetchContent_Declare(redis-plus-plus GIT_REPOSITORY https://github.com/sewenew/redis-plus-plus.git GIT_TAG 8b9ce389099608cf9bae617d79d257d2cc05e12f + GIT_SHALLOW TRUE ) FetchContent_MakeAvailable(redis-plus-plus) From e20a2673472d60596dc34c50b18778a02065488b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 15:55:03 -0800 Subject: [PATCH 190/244] try removing linking to hiredis::hiredis --- libs/server-sdk/src/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index a5ab12439..9b696f0d0 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -76,7 +76,7 @@ if (LD_BUILD_REDIS_SUPPORT) target_sources(${LIBNAME} PRIVATE data_systems/lazy_load/sources/redis/redis_source.cpp) - target_link_libraries(${LIBNAME} PRIVATE hiredis::hiredis redis++::redis++_static) + target_link_libraries(${LIBNAME} PRIVATE redis++::redis++_static) endif () From f66f68099e792e6c0d4b59ece2901b294130d7bc Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 16:39:34 -0800 Subject: [PATCH 191/244] fix wrong data kind in json_deserializer --- .../serialization_adapters/json_deserializer.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index 67dedcc49..efc371bf3 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -113,7 +113,7 @@ class JsonDeserializer final : public data_interfaces::IDataReader { Logger const& logger_; FlagKind const flag_kind_; - FlagKind const segment_kind_; + SegmentKind const segment_kind_; std::shared_ptr source_; std::string const identity_; }; From bff55eac238f8189044475af941adad4aee716cd Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 16:40:01 -0800 Subject: [PATCH 192/244] add debug logs to lazy load system caching functions --- .../lazy_load/lazy_load_system.cpp | 8 ++++---- .../lazy_load/lazy_load_system.hpp | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 26c246787..913b7a8e1 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -90,7 +90,7 @@ std::shared_ptr LazyLoad::GetFlag( auto const state = tracker_.State(data_components::DataKind::kFlag, key, time_()); return Get>( - state, [this, &key]() { RefreshFlag(key); }, + key, state, [this, &key]() { RefreshFlag(key); }, [this, &key]() { return cache_.GetFlag(key); }); } @@ -99,7 +99,7 @@ std::shared_ptr LazyLoad::GetSegment( auto const state = tracker_.State(data_components::DataKind::kSegment, key, time_()); return Get>( - state, [this, &key]() { RefreshSegment(key); }, + key, state, [this, &key]() { RefreshSegment(key); }, [this, &key]() { return cache_.GetSegment(key); }); } @@ -108,7 +108,7 @@ LazyLoad::AllFlags() const { auto const state = tracker_.State(Keys::kAllFlags, time_()); return Get>>( - state, [this]() { RefreshAllFlags(); }, + Keys::kAllFlags, state, [this]() { RefreshAllFlags(); }, [this]() { return cache_.AllFlags(); }); } @@ -117,7 +117,7 @@ LazyLoad::AllSegments() const { auto const state = tracker_.State(Keys::kAllSegments, time_()); return Get>>( - state, [this]() { RefreshAllSegments(); }, + Keys::kAllSegments, state, [this]() { RefreshAllSegments(); }, [this]() { return cache_.AllSegments(); }); } diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 09b5130d5..9df3c959e 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -72,16 +72,25 @@ class LazyLoad final : public data_interfaces::IDataSystem { void RefreshSegment(std::string const& key) const; template - static TResult Get(data_components::ExpirationTracker::TrackState state, - std::function const& refresh, - std::function const& get) { + TResult Get(std::string const& key, + data_components::ExpirationTracker::TrackState const state, + + std::function const& refresh, + std::function const& get) const { switch (state) { case data_components::ExpirationTracker::TrackState::kStale: - [[fallthrough]]; + LD_LOG(logger_, LogLevel::kDebug) + << Identity() << ": " << key << " is stale; refreshing"; + refresh(); + return get(); case data_components::ExpirationTracker::TrackState::kNotTracked: + LD_LOG(logger_, LogLevel::kDebug) + << Identity() << ": " << key << " not cached; refreshing"; refresh(); - [[fallthrough]]; + return get(); case data_components::ExpirationTracker::TrackState::kFresh: + LD_LOG(logger_, LogLevel::kDebug) + << Identity() << ": " << key << " served from cache"; return get(); } detail::unreachable(); From 7bfbd98d3883181dc13b4724fe9cdc8ad4f9b2df Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Wed, 29 Nov 2023 16:48:05 -0800 Subject: [PATCH 193/244] try disabling redis++ tests --- cmake/redis-plus-plus.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 116663e43..17980291e 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -15,6 +15,8 @@ FetchContent_MakeAvailable(hiredis) include_directories(${CMAKE_BINARY_DIR}/_deps) +set(REDIS_PLUS_PLUS_BUILD_TEST OFF) + FetchContent_Declare(redis-plus-plus GIT_REPOSITORY https://github.com/sewenew/redis-plus-plus.git GIT_TAG 8b9ce389099608cf9bae617d79d257d2cc05e12f From 46c95dafa2033cdafcafc62d39fea75fd6950c2f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 08:33:32 -0800 Subject: [PATCH 194/244] use older redis++ release --- cmake/redis-plus-plus.cmake | 6 ++++-- .../data_systems/lazy_load/sources/redis/redis_source.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 17980291e..4d9b52450 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -15,11 +15,13 @@ FetchContent_MakeAvailable(hiredis) include_directories(${CMAKE_BINARY_DIR}/_deps) -set(REDIS_PLUS_PLUS_BUILD_TEST OFF) +set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE) +# 1.3.7 is the last release that works with FetchContent, due to a problem with CheckSymbolExists +# when it tries to do feature detection on hiredis. FetchContent_Declare(redis-plus-plus GIT_REPOSITORY https://github.com/sewenew/redis-plus-plus.git - GIT_TAG 8b9ce389099608cf9bae617d79d257d2cc05e12f + GIT_TAG 1.3.7 GIT_SHALLOW TRUE ) diff --git a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp index 574146299..1ac71732b 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp @@ -1,6 +1,6 @@ #include -#include +#include namespace launchdarkly::server_side::data_systems { From dcf57106e69bc8efac906d124ca9dbecf5c4ca79 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 08:53:26 -0800 Subject: [PATCH 195/244] remove errant boost::movelib --- .../sources/streaming/streaming_data_source.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp index b5a62d0b4..923939774 100644 --- a/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp +++ b/libs/server-sdk/src/data_systems/background_sync/sources/streaming/streaming_data_source.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -50,7 +51,7 @@ StreamingDataSource::StreamingDataSource( void StreamingDataSource::StartAsync( data_interfaces::IDestination* dest, data_model::SDKDataSet const* bootstrap_data) { - boost::movelib::ignore(bootstrap_data); + boost::ignore_unused(bootstrap_data); event_handler_.emplace(*dest, logger_, status_manager_); From aee602959aa2192a1c31cafb45f56473943b6c89 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 08:55:21 -0800 Subject: [PATCH 196/244] fix test --- libs/server-sdk/tests/redis/redis_source_test.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/libs/server-sdk/tests/redis/redis_source_test.cpp b/libs/server-sdk/tests/redis/redis_source_test.cpp index 8ccb17d7a..835bdebb5 100644 --- a/libs/server-sdk/tests/redis/redis_source_test.cpp +++ b/libs/server-sdk/tests/redis/redis_source_test.cpp @@ -7,10 +7,14 @@ using namespace launchdarkly::server_side::data_systems; using namespace launchdarkly::server_side::data_components; TEST(RedisTests, ConnectToRedis) { - RedisDataSource source("tcp://localhost:6379", "test"); - ASSERT_FALSE(source.Initialized()); + auto maybe_source = RedisDataSource::Create("tcp://localhost:6379", "test"); + ASSERT_TRUE(maybe_source); - auto all_flags = source.All(FlagKind{}); + auto const source = *maybe_source; + + ASSERT_FALSE(source->Initialized()); + + auto all_flags = source->All(FlagKind{}); ASSERT_TRUE(all_flags.has_value()); ASSERT_EQ(all_flags->size(), 0); } From 95427db8ca7090cf7b06f90e919fd38039f8fd08 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 09:25:04 -0800 Subject: [PATCH 197/244] add missing header --- .../integrations/redis/redis_source.hpp | 1 + .../tests/redis/redis_source_test.cpp | 41 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index 2cb97f8c1..b868fcc1b 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -5,6 +5,7 @@ #include #include +#include namespace sw::redis { class Redis; diff --git a/libs/server-sdk/tests/redis/redis_source_test.cpp b/libs/server-sdk/tests/redis/redis_source_test.cpp index 835bdebb5..ca760d5c5 100644 --- a/libs/server-sdk/tests/redis/redis_source_test.cpp +++ b/libs/server-sdk/tests/redis/redis_source_test.cpp @@ -3,14 +3,47 @@ #include #include "data_components/kinds/kinds.hpp" +#include + +#include +#include + using namespace launchdarkly::server_side::data_systems; using namespace launchdarkly::server_side::data_components; +using namespace launchdarkly::data_model; + +class RedisTests : public ::testing::Test { + public: + explicit RedisTests() + : uri_("tcp://localhost:6379"), prefix_("testprefix"), client_(uri_) {} + + void SetUp() override { + auto maybe_source = RedisDataSource::Create(uri_, prefix_); + ASSERT_TRUE(maybe_source); + source = std::move(*maybe_source); + } + + testing::AssertionResult PutFlag(Flag const& flag) { + try { + client_.hset(prefix_ + ":features", flag.key, + serialize(boost::json::value_from(flag))); + return testing::AssertionSuccess(); + } catch (sw::redis::Error const& e) { + return testing::AssertionFailure(testing::Message(e.what())); + } + } + + protected: + std::shared_ptr source; -TEST(RedisTests, ConnectToRedis) { - auto maybe_source = RedisDataSource::Create("tcp://localhost:6379", "test"); - ASSERT_TRUE(maybe_source); + private: + std::string const uri_; + std::string const prefix_; + sw::redis::Redis client_; +}; - auto const source = *maybe_source; +TEST_F(RedisTests, Sourc) { + ASSERT_TRUE(PutFlag(Flag{"foo", 2, true})); ASSERT_FALSE(source->Initialized()); From 9b01ee9b0cdb7494fcb6f490262d3d8b9c82287b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 10:08:41 -0800 Subject: [PATCH 198/244] add missing header --- libs/common/tests/data_source_status_test.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/common/tests/data_source_status_test.cpp b/libs/common/tests/data_source_status_test.cpp index c3b165803..6b50ccc74 100644 --- a/libs/common/tests/data_source_status_test.cpp +++ b/libs/common/tests/data_source_status_test.cpp @@ -2,6 +2,8 @@ #include +#include + namespace test_things { enum class TestDataSourceStates { kStateA = 0, kStateB = 1, kStateC = 2 }; From 7def292bae2596421aee913e4d88e8320b48f1bd Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 10:23:16 -0800 Subject: [PATCH 199/244] update googletest --- CMakeLists.txt | 2 +- .../tests/redis/redis_source_test.cpp | 115 ++++++++++++++++-- 2 files changed, 104 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9307384dc..693ee71a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,7 +95,7 @@ if (LD_BUILD_UNIT_TESTS) endif () FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip + URL https://github.com/google/googletest/archive/76bb2afb8b522d24496ad1c757a49784fbfa2e42.zip ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) diff --git a/libs/server-sdk/tests/redis/redis_source_test.cpp b/libs/server-sdk/tests/redis/redis_source_test.cpp index ca760d5c5..b31352a4b 100644 --- a/libs/server-sdk/tests/redis/redis_source_test.cpp +++ b/libs/server-sdk/tests/redis/redis_source_test.cpp @@ -12,25 +12,81 @@ using namespace launchdarkly::server_side::data_systems; using namespace launchdarkly::server_side::data_components; using namespace launchdarkly::data_model; +class PrefixedClient { + public: + PrefixedClient(sw::redis::Redis& client, std::string const& prefix) + : client_(client), prefix_(prefix) {} + + void Init() const { + try { + client_.set(prefix_ + ":$inited", "true"); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + void PutFlag(Flag const& flag) const { + try { + client_.hset(prefix_ + ":features", flag.key, + serialize(boost::json::value_from(flag))); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + void Clear() const { + try { + std::vector> output; + client_.keys(prefix_ + ":*", std::back_inserter(output)); + for (auto const& [key, _] : output) { + client_.del(key); + } + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + private: + sw::redis::Redis& client_; + std::string const& prefix_; +}; + class RedisTests : public ::testing::Test { public: explicit RedisTests() : uri_("tcp://localhost:6379"), prefix_("testprefix"), client_(uri_) {} void SetUp() override { + try { + client_.flushdb(); + } catch (sw::redis::Error const& e) { + FAIL() << "couldn't clear Redis: " << e.what(); + } + auto maybe_source = RedisDataSource::Create(uri_, prefix_); ASSERT_TRUE(maybe_source); source = std::move(*maybe_source); } - testing::AssertionResult PutFlag(Flag const& flag) { - try { - client_.hset(prefix_ + ":features", flag.key, - serialize(boost::json::value_from(flag))); - return testing::AssertionSuccess(); - } catch (sw::redis::Error const& e) { - return testing::AssertionFailure(testing::Message(e.what())); - } + void Init() { + auto const client = PrefixedClient(client_, prefix_); + client.Init(); + } + + void PutFlag(Flag const& flag) { + auto const client = PrefixedClient(client_, prefix_); + client.PutFlag(flag); + } + + void Clear() { + auto const client = PrefixedClient(client_, prefix_); + client.Clear(); + } + + void WithPrefix(std::string const& prefix, + std::function const& f) { + auto const client = PrefixedClient(client_, prefix); + f(client); } protected: @@ -42,12 +98,47 @@ class RedisTests : public ::testing::Test { sw::redis::Redis client_; }; -TEST_F(RedisTests, Sourc) { - ASSERT_TRUE(PutFlag(Flag{"foo", 2, true})); - +TEST_F(RedisTests, RedisEmptyIsNotInitialized) { ASSERT_FALSE(source->Initialized()); auto all_flags = source->All(FlagKind{}); ASSERT_TRUE(all_flags.has_value()); - ASSERT_EQ(all_flags->size(), 0); + ASSERT_TRUE(all_flags->empty()); + + auto all_segments = source->All(SegmentKind{}); + ASSERT_TRUE(all_segments.has_value()); + ASSERT_TRUE(all_flags->empty()); +} + +TEST_F(RedisTests, ChecksInitialized) { + ASSERT_FALSE(source->Initialized()); + Init(); + ASSERT_TRUE(source->Initialized()); +} + +TEST_F(RedisTests, GetFlag) { + PutFlag(Flag{"foo", 1, true}); + + auto result = source->Get(FlagKind{}, "foo"); + ASSERT_TRUE(result); + + ASSERT_EQ(result->version, 1); + ASSERT_FALSE(result->deleted); +} + +TEST_F(RedisTests, ChecksInitializedPrefixIndependence) { + WithPrefix("not_our_prefix", [&](auto const& client) { + client.Init(); + ASSERT_FALSE(source->Initialized()); + }); + + WithPrefix("TestPrefix", [&](auto const& client) { + client.Init(); + ASSERT_FALSE(source->Initialized()); + }); + + WithPrefix("stillnotprefix", [&](auto const& client) { + client.Init(); + ASSERT_FALSE(source->Initialized()); + }); } From 27c4fd9f5bb743a1faac1c549ad1316d630e101f Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 10:39:27 -0800 Subject: [PATCH 200/244] make sure redis++ is linked statically --- cmake/redis-plus-plus.cmake | 2 + .../tests/redis/redis_source_test.cpp | 48 +++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 4d9b52450..3199100f1 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -16,6 +16,8 @@ FetchContent_MakeAvailable(hiredis) include_directories(${CMAKE_BINARY_DIR}/_deps) set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE) +set(REDIS_PLUS_PLUS_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(REDIS_PLUS_PLUS_BUILD_STATIC ON CACHE BOOL "" FORCE) # 1.3.7 is the last release that works with FetchContent, due to a problem with CheckSymbolExists # when it tries to do feature detection on hiredis. diff --git a/libs/server-sdk/tests/redis/redis_source_test.cpp b/libs/server-sdk/tests/redis/redis_source_test.cpp index b31352a4b..5852358a3 100644 --- a/libs/server-sdk/tests/redis/redis_source_test.cpp +++ b/libs/server-sdk/tests/redis/redis_source_test.cpp @@ -3,10 +3,12 @@ #include #include "data_components/kinds/kinds.hpp" -#include +#include +#include #include -#include + +#include using namespace launchdarkly::server_side::data_systems; using namespace launchdarkly::server_side::data_components; @@ -34,6 +36,15 @@ class PrefixedClient { } } + void PutSegment(Segment const& segment) const { + try { + client_.hset(prefix_ + ":segments", segment.key, + serialize(boost::json::value_from(segment))); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + void Clear() const { try { std::vector> output; @@ -78,6 +89,11 @@ class RedisTests : public ::testing::Test { client.PutFlag(flag); } + void PutSegment(Segment const& segment) { + auto const client = PrefixedClient(client_, prefix_); + client.PutSegment(segment); + } + void Clear() { auto const client = PrefixedClient(client_, prefix_); client.Clear(); @@ -119,11 +135,35 @@ TEST_F(RedisTests, ChecksInitialized) { TEST_F(RedisTests, GetFlag) { PutFlag(Flag{"foo", 1, true}); - auto result = source->Get(FlagKind{}, "foo"); + auto const result = source->Get(FlagKind{}, "foo"); ASSERT_TRUE(result); - ASSERT_EQ(result->version, 1); ASSERT_FALSE(result->deleted); + ASSERT_TRUE(result->serializedItem); +} + +TEST_F(RedisTests, GetFlagDoesNotFindSegment) { + PutSegment(Segment{"foo", 1}); + + auto const result = source->Get(FlagKind{}, "foo"); + ASSERT_FALSE(result); +} + +TEST_F(RedisTests, GetSegment) { + PutSegment(Segment{"foo", 1}); + + auto const result = source->Get(SegmentKind{}, "foo"); + ASSERT_TRUE(result); + + ASSERT_FALSE(result->deleted); + ASSERT_TRUE(result->serializedItem); +} + +TEST_F(RedisTests, GetSegmentDoesNotFindFlag) { + PutFlag(Flag{"foo", 1, true}); + + auto const result = source->Get(SegmentKind{}, "foo"); + ASSERT_FALSE(result); } TEST_F(RedisTests, ChecksInitializedPrefixIndependence) { From 0a7eff9c92290b9d3260b97792f70eeab1e1de3d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 11:01:12 -0800 Subject: [PATCH 201/244] try linking gtest's dependencies private --- libs/server-sdk/tests/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 3ead434e5..5ef200eaf 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -21,10 +21,10 @@ add_executable(gtest_${LIBNAME} ${tests} ) -target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock) +target_link_libraries(gtest_${LIBNAME} PRIVATE launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock) if (LD_BUILD_REDIS_SUPPORT) - target_link_libraries(gtest_${LIBNAME} redis++::redis++_static) + target_link_libraries(gtest_${LIBNAME} PRIVATE redis++::redis++_static) endif () gtest_discover_tests(gtest_${LIBNAME}) From ec1dede57fcc42e5df56c2c75761db76dba0998d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 11:16:29 -0800 Subject: [PATCH 202/244] linking against wrong gmock library --- libs/server-sdk/tests/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 5ef200eaf..3aa88e75b 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -21,7 +21,7 @@ add_executable(gtest_${LIBNAME} ${tests} ) -target_link_libraries(gtest_${LIBNAME} PRIVATE launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock) +target_link_libraries(gtest_${LIBNAME} PRIVATE launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock_main) if (LD_BUILD_REDIS_SUPPORT) target_link_libraries(gtest_${LIBNAME} PRIVATE redis++::redis++_static) From 7433b9af07fb936c073fa94183660a0c66ea8a1b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 11:24:09 -0800 Subject: [PATCH 203/244] revert googletest version' --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 693ee71a1..9307384dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,7 +95,7 @@ if (LD_BUILD_UNIT_TESTS) endif () FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/76bb2afb8b522d24496ad1c757a49784fbfa2e42.zip + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) From 73445647a02b425420683a5b60db511fdbbee134 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 11:27:34 -0800 Subject: [PATCH 204/244] try linking only gmock_main --- libs/server-sdk/tests/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 3aa88e75b..06bbbf3f1 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -21,7 +21,7 @@ add_executable(gtest_${LIBNAME} ${tests} ) -target_link_libraries(gtest_${LIBNAME} PRIVATE launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock_main) +target_link_libraries(gtest_${LIBNAME} PRIVATE launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gmock_main) if (LD_BUILD_REDIS_SUPPORT) target_link_libraries(gtest_${LIBNAME} PRIVATE redis++::redis++_static) From fc8a21b744ca663cc98aa8749e8b820d6eb68398 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 11:38:27 -0800 Subject: [PATCH 205/244] try adding redis++ in same target_link_libraries call --- libs/server-sdk/tests/CMakeLists.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 06bbbf3f1..25ba3ab25 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -21,10 +21,20 @@ add_executable(gtest_${LIBNAME} ${tests} ) -target_link_libraries(gtest_${LIBNAME} PRIVATE launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gmock_main) +# Set library list to link: +set(LIBS + launchdarkly::server + launchdarkly::internal + launchdarkly::sse + timestamp + GTest::gtest_main + GTest::gmock +) if (LD_BUILD_REDIS_SUPPORT) - target_link_libraries(gtest_${LIBNAME} PRIVATE redis++::redis++_static) + list(APPEND LIBS redis++::redis++_static) endif () +target_link_libraries(gtest_${LIBNAME} PRIVATE ${LIBS}) + gtest_discover_tests(gtest_${LIBNAME}) From 682197ca6ea9d808ee316605b002c31f37833a63 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 11:46:18 -0800 Subject: [PATCH 206/244] try not compiling redis tests at all --- libs/server-sdk/tests/CMakeLists.txt | 9 --------- 1 file changed, 9 deletions(-) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 25ba3ab25..4934b262f 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -6,11 +6,6 @@ include_directories("${PROJECT_SOURCE_DIR}/src") file(GLOB tests "${PROJECT_SOURCE_DIR}/tests/*.cpp") -if (LD_BUILD_REDIS_SUPPORT) - file(GLOB redis_tests "${PROJECT_SOURCE_DIR}/tests/redis/*.cpp") - list(APPEND tests ${redis_tests}) -endif () - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) # Get things in the same directory on windows. @@ -31,10 +26,6 @@ set(LIBS GTest::gmock ) -if (LD_BUILD_REDIS_SUPPORT) - list(APPEND LIBS redis++::redis++_static) -endif () - target_link_libraries(gtest_${LIBNAME} PRIVATE ${LIBS}) gtest_discover_tests(gtest_${LIBNAME}) From 0e0b2fc106f63d9f9c3c26261b77a93a1c6e48b4 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 12:00:59 -0800 Subject: [PATCH 207/244] disable redis++ pic --- cmake/redis-plus-plus.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 3199100f1..93a28d0e5 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -18,6 +18,7 @@ include_directories(${CMAKE_BINARY_DIR}/_deps) set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE) set(REDIS_PLUS_PLUS_BUILD_SHARED OFF CACHE BOOL "" FORCE) set(REDIS_PLUS_PLUS_BUILD_STATIC ON CACHE BOOL "" FORCE) +set(REDIS_PLUS_PLUS_BUILD_STATIC_WITH_PIC OFF CACHE BOOL "" FORCE) # 1.3.7 is the last release that works with FetchContent, due to a problem with CheckSymbolExists # when it tries to do feature detection on hiredis. From 2b0017f1501ec98ba8ba276770f969d8c782dd61 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 12:10:31 -0800 Subject: [PATCH 208/244] remove all redis stuff --- CMakeLists.txt | 5 ----- examples/CMakeLists.txt | 4 ---- libs/server-sdk/src/CMakeLists.txt | 8 -------- 3 files changed, 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9307384dc..0a401c102 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,11 +144,6 @@ add_subdirectory(libs/common) add_subdirectory(libs/internal) add_subdirectory(libs/server-sent-events) -if (LD_BUILD_REDIS_SUPPORT) - message(STATUS "LaunchDarkly: building redis support") - include(${CMAKE_FILES}/redis-plus-plus.cmake) -endif () - # Built as static or shared depending on LD_BUILD_SHARED_LIBS variable. # This target "links" in common, internal, and sse as object libraries. add_subdirectory(libs/client-sdk) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f013bc666..138ec4942 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,7 +3,3 @@ add_subdirectory(hello-cpp-client) add_subdirectory(hello-cpp-server) add_subdirectory(hello-c-server) add_subdirectory(client-and-server-coexistence) - -if (LD_BUILD_REDIS_SUPPORT) - add_subdirectory(hello-cpp-server-redis) -endif () diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 9b696f0d0..908b87f28 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -72,14 +72,6 @@ target_sources(${LIBNAME} ) -if (LD_BUILD_REDIS_SUPPORT) - target_sources(${LIBNAME} - PRIVATE - data_systems/lazy_load/sources/redis/redis_source.cpp) - target_link_libraries(${LIBNAME} PRIVATE redis++::redis++_static) -endif () - - target_link_libraries(${LIBNAME} PUBLIC launchdarkly::common PRIVATE Boost::disable_autolinking Boost::headers Boost::json Boost::url launchdarkly::sse launchdarkly::internal foxy timestamp) From 0e8b17a58b3a4320dc93070552b786bc371b16bd Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 12:22:28 -0800 Subject: [PATCH 209/244] try fetching redis++ only --- CMakeLists.txt | 5 +++++ cmake/redis-plus-plus.cmake | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a401c102..9307384dc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,11 @@ add_subdirectory(libs/common) add_subdirectory(libs/internal) add_subdirectory(libs/server-sent-events) +if (LD_BUILD_REDIS_SUPPORT) + message(STATUS "LaunchDarkly: building redis support") + include(${CMAKE_FILES}/redis-plus-plus.cmake) +endif () + # Built as static or shared depending on LD_BUILD_SHARED_LIBS variable. # This target "links" in common, internal, and sse as object libraries. add_subdirectory(libs/client-sdk) diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 93a28d0e5..3199100f1 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -18,7 +18,6 @@ include_directories(${CMAKE_BINARY_DIR}/_deps) set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE) set(REDIS_PLUS_PLUS_BUILD_SHARED OFF CACHE BOOL "" FORCE) set(REDIS_PLUS_PLUS_BUILD_STATIC ON CACHE BOOL "" FORCE) -set(REDIS_PLUS_PLUS_BUILD_STATIC_WITH_PIC OFF CACHE BOOL "" FORCE) # 1.3.7 is the last release that works with FetchContent, due to a problem with CheckSymbolExists # when it tries to do feature detection on hiredis. From 2959e10ef4c1ffd6f8a3d9b27e4c4a937f3c6973 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 12:28:37 -0800 Subject: [PATCH 210/244] try using FetchContent git for googletest --- CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9307384dc..c978641c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,7 +95,8 @@ if (LD_BUILD_UNIT_TESTS) endif () FetchContent_Declare( googletest - URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG 76bb2afb8b522d24496ad1c757a49784fbfa2e42 ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) From 0e4dc5bcbd158b25421a0f229420d6c13ee35ea1 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 12:44:51 -0800 Subject: [PATCH 211/244] suppress gtest warnings --- CMakeLists.txt | 3 +++ libs/client-sdk/tests/CMakeLists.txt | 2 ++ libs/common/tests/CMakeLists.txt | 2 ++ libs/internal/tests/CMakeLists.txt | 2 ++ libs/server-sdk/tests/CMakeLists.txt | 4 ++++ 5 files changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c978641c8..3650f2c01 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -103,6 +103,9 @@ if (LD_BUILD_UNIT_TESTS) FetchContent_MakeAvailable(googletest) enable_testing() + set_target_properties(gtest PROPERTIES COMPILE_WARNING_AS_ERROR OFF) + + endif () if (LD_DYNAMIC_LINK_OPENSSL) diff --git a/libs/client-sdk/tests/CMakeLists.txt b/libs/client-sdk/tests/CMakeLists.txt index de8236d5c..1800f8eed 100644 --- a/libs/client-sdk/tests/CMakeLists.txt +++ b/libs/client-sdk/tests/CMakeLists.txt @@ -15,5 +15,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") add_executable(gtest_${LIBNAME} ${tests}) target_link_libraries(gtest_${LIBNAME} launchdarkly::client launchdarkly::internal GTest::gtest_main) +# There's an uninitialized variable warning in gtest that hasn't been resolved. +set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/common/tests/CMakeLists.txt b/libs/common/tests/CMakeLists.txt index ce5b742a3..7f056348e 100644 --- a/libs/common/tests/CMakeLists.txt +++ b/libs/common/tests/CMakeLists.txt @@ -17,5 +17,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") add_executable(gtest_${LIBNAME} ${tests}) target_link_libraries(gtest_${LIBNAME} launchdarkly::common launchdarkly::internal foxy GTest::gtest_main) +# There's an uninitialized variable warning in gtest that hasn't been resolved. +set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/internal/tests/CMakeLists.txt b/libs/internal/tests/CMakeLists.txt index 9a9f3aebd..a199411c6 100644 --- a/libs/internal/tests/CMakeLists.txt +++ b/libs/internal/tests/CMakeLists.txt @@ -15,5 +15,7 @@ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") add_executable(gtest_${LIBNAME} ${tests}) target_link_libraries(gtest_${LIBNAME} launchdarkly::common launchdarkly::internal foxy GTest::gtest_main) +# There's an uninitialized variable warning in gtest that hasn't been resolved. +set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index 4934b262f..ccee74817 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -28,4 +28,8 @@ set(LIBS target_link_libraries(gtest_${LIBNAME} PRIVATE ${LIBS}) +# There's an uninitialized variable warning in gtest that hasn't been resolved. +set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) + + gtest_discover_tests(gtest_${LIBNAME}) From 8a6890cae5994445ce693d5708aab75d23d74f7b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 14:53:16 -0800 Subject: [PATCH 212/244] revert googletest change --- CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3650f2c01..6b82a585b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,8 +95,7 @@ if (LD_BUILD_UNIT_TESTS) endif () FetchContent_Declare( googletest - GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG 76bb2afb8b522d24496ad1c757a49784fbfa2e42 + URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip ) # For Windows: Prevent overriding the parent project's compiler/linker settings set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) From 69e9fa9fb93a4e2b414e62cd96f3a129791b7a8a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 15:11:56 -0800 Subject: [PATCH 213/244] guard windows-only cmake_runtime_output_dir changes to win32 --- libs/client-sdk/tests/CMakeLists.txt | 6 ++++-- libs/common/tests/CMakeLists.txt | 6 ++++-- libs/internal/tests/CMakeLists.txt | 6 ++++-- libs/server-sdk/tests/CMakeLists.txt | 6 ++++-- libs/server-sent-events/tests/CMakeLists.txt | 6 ++++-- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/libs/client-sdk/tests/CMakeLists.txt b/libs/client-sdk/tests/CMakeLists.txt index 1800f8eed..bccdb6a00 100644 --- a/libs/client-sdk/tests/CMakeLists.txt +++ b/libs/client-sdk/tests/CMakeLists.txt @@ -9,8 +9,10 @@ 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}../") +if (WIN32) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") +endif () add_executable(gtest_${LIBNAME} ${tests}) diff --git a/libs/common/tests/CMakeLists.txt b/libs/common/tests/CMakeLists.txt index 7f056348e..53acb0473 100644 --- a/libs/common/tests/CMakeLists.txt +++ b/libs/common/tests/CMakeLists.txt @@ -11,8 +11,10 @@ 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}../") +if (WIN32) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") +endif () add_executable(gtest_${LIBNAME} ${tests}) diff --git a/libs/internal/tests/CMakeLists.txt b/libs/internal/tests/CMakeLists.txt index a199411c6..428ea19db 100644 --- a/libs/internal/tests/CMakeLists.txt +++ b/libs/internal/tests/CMakeLists.txt @@ -9,8 +9,10 @@ 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}../") +if (WIN32) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") +endif () add_executable(gtest_${LIBNAME} ${tests}) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index ccee74817..e2c91097f 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -9,8 +9,10 @@ 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}../") +if (WIN32) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") +endif () add_executable(gtest_${LIBNAME} ${tests} diff --git a/libs/server-sent-events/tests/CMakeLists.txt b/libs/server-sent-events/tests/CMakeLists.txt index 03209aa80..eb20d404f 100644 --- a/libs/server-sent-events/tests/CMakeLists.txt +++ b/libs/server-sent-events/tests/CMakeLists.txt @@ -9,8 +9,10 @@ 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}../") +if (WIN32) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") +endif () add_executable(gtest_${LIBNAME} ${tests}) From 39d08eb8feab444a8fb016f0389b4f22057dac44 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 15:23:23 -0800 Subject: [PATCH 214/244] fix tests --- libs/server-sdk/tests/lazy_load_system_test.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index cb42ed9d3..4b16cad0a 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -110,8 +110,8 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfFlagCannotBeFetched) { ASSERT_FALSE(lazy_load.GetFlag("foo")); }; - ASSERT_TRUE(spy_logger_backend->Count(1)); - ASSERT_TRUE(spy_logger_backend->Contains(0, LogLevel::kError, "oops")); + ASSERT_TRUE(spy_logger_backend->Count(21)); // 20 debug logs + 1 error log + ASSERT_TRUE(spy_logger_backend->Contains(1, LogLevel::kError, "oops")); } TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfSegmentCannotBeFetched) { @@ -129,8 +129,8 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfSegmentCannotBeFetched) { ASSERT_FALSE(lazy_load.GetSegment("foo")); }; - ASSERT_TRUE(spy_logger_backend->Count(1)); - ASSERT_TRUE(spy_logger_backend->Contains(0, LogLevel::kError, "oops")); + ASSERT_TRUE(spy_logger_backend->Count(21)); + ASSERT_TRUE(spy_logger_backend->Contains(1, LogLevel::kError, "oops")); } TEST_F(LazyLoadTest, RefreshesFlagIfStale) { From 4248c26ae71f8558bada25e8889028aa73ee5628 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 16:25:02 -0800 Subject: [PATCH 215/244] add tiny delay to TimeIsUpdatedOnStateChange test --- libs/client-sdk/tests/data_source_status_manager_test.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/client-sdk/tests/data_source_status_manager_test.cpp b/libs/client-sdk/tests/data_source_status_manager_test.cpp index 3ac3c3159..5bfdabbb7 100644 --- a/libs/client-sdk/tests/data_source_status_manager_test.cpp +++ b/libs/client-sdk/tests/data_source_status_manager_test.cpp @@ -2,6 +2,8 @@ #include "data_sources/data_source_status_manager.hpp" +#include + using launchdarkly::client_side::data_sources::DataSourceStatus; using launchdarkly::client_side::data_sources::DataSourceStatusManager; using launchdarkly::client_side::data_sources::IDataSourceStatusProvider; @@ -145,6 +147,8 @@ TEST(DataSourceStatusManagerTests, TimeIsUpdatedOnStateChange) { status_manager.SetState(DataSourceStatus::DataSourceState::kValid); auto initial = status_manager.Status().StateSince(); + + std::this_thread::sleep_for(std::chrono::milliseconds(1)); status_manager.SetState(DataSourceStatus::DataSourceState::kInterrupted); EXPECT_NE(initial.time_since_epoch().count(), From a932419a07cc7dc59a3503fccfb9c8bba6db3952 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Thu, 30 Nov 2023 17:19:02 -0800 Subject: [PATCH 216/244] disable redis by default --- CMakeLists.txt | 2 +- cmake/redis-plus-plus.cmake | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b82a585b..0116168fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,7 +61,7 @@ option(LD_DYNAMIC_LINK_OPENSSL option(LD_BUILD_EXAMPLES "Build hello-world examples." ON) -option(LD_BUILD_REDIS_SUPPORT "Build redis support." ON) +option(LD_BUILD_REDIS_SUPPORT "Build redis support." OFF) # If using 'make' as the build system, CMake causes the 'install' target to have a dependency on 'all', meaning # it will cause a full build. This disables that, allowing us to build piecemeal instead. This is useful diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 3199100f1..020e6bab5 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -11,13 +11,12 @@ FetchContent_Declare(hiredis OVERRIDE_FIND_PACKAGE ) + FetchContent_MakeAvailable(hiredis) include_directories(${CMAKE_BINARY_DIR}/_deps) set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE) -set(REDIS_PLUS_PLUS_BUILD_SHARED OFF CACHE BOOL "" FORCE) -set(REDIS_PLUS_PLUS_BUILD_STATIC ON CACHE BOOL "" FORCE) # 1.3.7 is the last release that works with FetchContent, due to a problem with CheckSymbolExists # when it tries to do feature detection on hiredis. From fee073b0ed194e0e506347e25c4f80abfe90482e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 08:19:07 -0800 Subject: [PATCH 217/244] refactor redis integration into dedicated cmake project --- CMakeLists.txt | 4 +- cmake/redis-plus-plus.cmake | 2 +- libs/server-sdk-redis-source/CMakeLists.txt | 33 +++++++++++++ .../integrations/redis/redis_source.hpp | 23 ++++----- .../src/CMakeLists.txt | 48 +++++++++++++++++++ .../src}/redis_source.cpp | 6 +-- .../tests/CMakeLists.txt | 37 ++++++++++++++ .../tests}/redis_source_test.cpp | 5 +- .../data_system/lazy_load_builder.hpp | 4 +- .../built/data_system/lazy_load_config.hpp | 4 +- .../data_reader}/iserialized_data_reader.hpp | 8 ++-- .../iserialized_item_kind.hpp | 0 .../integrations/data_reader}/kinds.hpp | 10 ++-- .../serialized_item_descriptor.hpp | 0 libs/server-sdk/src/CMakeLists.txt | 3 +- .../json_deserializer.cpp | 4 +- .../json_deserializer.hpp | 12 ++--- .../json_destination.cpp | 7 ++- .../json_destination.hpp | 6 +-- libs/server-sdk/src/data_interfaces.cpp | 4 +- .../destination/iserialized_destination.hpp | 4 +- .../lazy_load/lazy_load_system.cpp | 7 ++- .../lazy_load/lazy_load_system.hpp | 8 ++-- .../data_reader}/kinds.cpp | 6 +-- .../tests/lazy_load_system_test.cpp | 6 +-- 25 files changed, 185 insertions(+), 66 deletions(-) create mode 100644 libs/server-sdk-redis-source/CMakeLists.txt rename libs/{server-sdk => server-sdk-redis-source}/include/launchdarkly/server_side/integrations/redis/redis_source.hpp (52%) create mode 100644 libs/server-sdk-redis-source/src/CMakeLists.txt rename libs/{server-sdk/src/data_systems/lazy_load/sources/redis => server-sdk-redis-source/src}/redis_source.cpp (92%) create mode 100644 libs/server-sdk-redis-source/tests/CMakeLists.txt rename libs/{server-sdk/tests/redis => server-sdk-redis-source/tests}/redis_source_test.cpp (96%) rename libs/server-sdk/include/launchdarkly/server_side/{data_interfaces/sources => integrations/data_reader}/iserialized_data_reader.hpp (91%) rename libs/server-sdk/include/launchdarkly/server_side/integrations/{ => data_reader}/iserialized_item_kind.hpp (100%) rename libs/server-sdk/{src/data_components/kinds => include/launchdarkly/server_side/integrations/data_reader}/kinds.hpp (60%) rename libs/server-sdk/include/launchdarkly/server_side/integrations/{ => data_reader}/serialized_item_descriptor.hpp (100%) rename libs/server-sdk/src/{data_components/kinds => integrations/data_reader}/kinds.cpp (85%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0116168fb..48dfd0f40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,8 +148,8 @@ add_subdirectory(libs/internal) add_subdirectory(libs/server-sent-events) if (LD_BUILD_REDIS_SUPPORT) - message(STATUS "LaunchDarkly: building redis support") - include(${CMAKE_FILES}/redis-plus-plus.cmake) + message("LaunchDarkly: building server-side redis support") + add_subdirectory(libs/server-sdk-redis-source) endif () # Built as static or shared depending on LD_BUILD_SHARED_LIBS variable. diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 020e6bab5..49572b27f 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -14,7 +14,7 @@ FetchContent_Declare(hiredis FetchContent_MakeAvailable(hiredis) -include_directories(${CMAKE_BINARY_DIR}/_deps) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/_deps) set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE) diff --git a/libs/server-sdk-redis-source/CMakeLists.txt b/libs/server-sdk-redis-source/CMakeLists.txt new file mode 100644 index 000000000..edbc69ce8 --- /dev/null +++ b/libs/server-sdk-redis-source/CMakeLists.txt @@ -0,0 +1,33 @@ +# 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( + LaunchDarklyCPPServerRedisSource + VERSION 0.1.0 # {x-release-please-version} + DESCRIPTION "LaunchDarkly C++ Server SDK Redis Source" + LANGUAGES CXX C +) + +set(LIBNAME "launchdarkly-cpp-server-redis-source") + +# 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 () + +# Needed to fetch external dependencies. +include(FetchContent) + +include(${CMAKE_FILES}/redis-plus-plus.cmake) + +add_subdirectory(src) + +if (LD_BUILD_UNIT_TESTS) + add_subdirectory(tests) +endif () diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp b/libs/server-sdk-redis-source/include/launchdarkly/server_side/integrations/redis/redis_source.hpp similarity index 52% rename from libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp rename to libs/server-sdk-redis-source/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index b868fcc1b..12cb1566a 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/redis/redis_source.hpp +++ b/libs/server-sdk-redis-source/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -1,24 +1,25 @@ #pragma once -#include +#include #include -#include #include +#include namespace sw::redis { class Redis; } -namespace launchdarkly::server_side::data_systems { +namespace launchdarkly::server_side::integrations { -class RedisDataSource final : public data_interfaces::ISerializedDataReader { +class RedisDataSource final : public ISerializedDataReader { public: + static tl::expected, std::string> Create( + std::string uri, + std::string prefix); - static tl::expected, std::string> Create(std::string uri, std::string prefix); - - [[nodiscard]] GetResult Get(integrations::ISerializedItemKind const& kind, + [[nodiscard]] GetResult Get(ISerializedItemKind const& kind, std::string const& itemKey) const override; [[nodiscard]] AllResult All( integrations::ISerializedItemKind const& kind) const override; @@ -28,13 +29,13 @@ class RedisDataSource final : public data_interfaces::ISerializedDataReader { ~RedisDataSource(); private: - RedisDataSource(std::unique_ptr redis, std::string prefix); + RedisDataSource(std::unique_ptr redis, + std::string prefix); std::string const prefix_; std::string const inited_key_; - std::string key_for_kind( - integrations::ISerializedItemKind const& kind) const; + std::string key_for_kind(ISerializedItemKind const& kind) const; std::unique_ptr redis_; }; -} // namespace launchdarkly::server_side::data_systems +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk-redis-source/src/CMakeLists.txt b/libs/server-sdk-redis-source/src/CMakeLists.txt new file mode 100644 index 000000000..2fc232602 --- /dev/null +++ b/libs/server-sdk-redis-source/src/CMakeLists.txt @@ -0,0 +1,48 @@ +file(GLOB HEADER_LIST CONFIGURE_DEPENDS + "${LaunchDarklyCPPServerRedisSource_SOURCE_DIR}/include/launchdarkly/server_side/integrations/redis/*.hpp" +) + +if (LD_BUILD_SHARED_LIBS) + message(STATUS "LaunchDarkly: building server-sdk-redis-source as shared library") + add_library(${LIBNAME} SHARED) +else () + message(STATUS "LaunchDarkly: building server-sdk-redis-source as static library") + add_library(${LIBNAME} STATIC) +endif () + + +target_sources(${LIBNAME} + PRIVATE + ${HEADER_LIST} + redis_source.cpp +) + + +target_link_libraries(${LIBNAME} + PUBLIC launchdarkly::server + PRIVATE redis++) + + +add_library(launchdarkly::server_redis_source ALIAS ${LIBNAME}) + +set_property(TARGET ${LIBNAME} PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>DLL") + +# Optional in case only the client SDK is being built. +install(TARGETS ${LIBNAME} OPTIONAL) +if (LD_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 "${LaunchDarklyCPPServerRedisSource_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/data_systems/lazy_load/sources/redis/redis_source.cpp b/libs/server-sdk-redis-source/src/redis_source.cpp similarity index 92% rename from libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp rename to libs/server-sdk-redis-source/src/redis_source.cpp index 1ac71732b..5da82747c 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/sources/redis/redis_source.cpp +++ b/libs/server-sdk-redis-source/src/redis_source.cpp @@ -2,7 +2,7 @@ #include -namespace launchdarkly::server_side::data_systems { +namespace launchdarkly::server_side::integrations { tl::expected, std::string> RedisDataSource::Create(std::string uri, std::string prefix) { @@ -28,7 +28,7 @@ RedisDataSource::RedisDataSource(std::unique_ptr redis, RedisDataSource::~RedisDataSource() = default; -data_interfaces::ISerializedDataReader::GetResult RedisDataSource::Get( +integrations::ISerializedDataReader::GetResult RedisDataSource::Get( integrations::ISerializedItemKind const& kind, std::string const& itemKey) const { try { @@ -42,7 +42,7 @@ data_interfaces::ISerializedDataReader::GetResult RedisDataSource::Get( } } -data_interfaces::ISerializedDataReader::AllResult RedisDataSource::All( +integrations::ISerializedDataReader::AllResult RedisDataSource::All( integrations::ISerializedItemKind const& kind) const { std::unordered_map raw_items; AllResult::value_type items; diff --git a/libs/server-sdk-redis-source/tests/CMakeLists.txt b/libs/server-sdk-redis-source/tests/CMakeLists.txt new file mode 100644 index 000000000..2350db8d4 --- /dev/null +++ b/libs/server-sdk-redis-source/tests/CMakeLists.txt @@ -0,0 +1,37 @@ +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. +if (WIN32) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}../") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}../") +endif () + +add_executable(gtest_${LIBNAME} + ${tests} +) + +set(LIBS + launchdarkly::server_redis_source + # Needed so we can access the flag/segment data models so we can serialize them for putting in redis. + launchdarkly::internal + # Needed because the source doesn't (need to) expose redis++ as a public dependency, but we need to construct + # a redis client in the tests. + redis++ + GTest::gtest_main + GTest::gmock +) + +target_link_libraries(gtest_${LIBNAME} PRIVATE ${LIBS}) + +# There's an uninitialized variable warning in gtest that hasn't been resolved. +set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) + +gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/redis/redis_source_test.cpp b/libs/server-sdk-redis-source/tests/redis_source_test.cpp similarity index 96% rename from libs/server-sdk/tests/redis/redis_source_test.cpp rename to libs/server-sdk-redis-source/tests/redis_source_test.cpp index 5852358a3..cc45ebf8b 100644 --- a/libs/server-sdk/tests/redis/redis_source_test.cpp +++ b/libs/server-sdk-redis-source/tests/redis_source_test.cpp @@ -1,7 +1,7 @@ #include +#include #include -#include "data_components/kinds/kinds.hpp" #include #include @@ -10,8 +10,7 @@ #include -using namespace launchdarkly::server_side::data_systems; -using namespace launchdarkly::server_side::data_components; +using namespace launchdarkly::server_side::integrations; using namespace launchdarkly::data_model; class PrefixedClient { diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp index fbe224dd2..1a34261fa 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/builders/data_system/lazy_load_builder.hpp @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include @@ -25,7 +25,7 @@ namespace launchdarkly::server_side::config::builders { * another SDK) is necessary. */ struct LazyLoadBuilder { - using SourcePtr = std::shared_ptr; + using SourcePtr = std::shared_ptr; using EvictionPolicy = built::LazyLoadConfig::EvictionPolicy; /** * \brief Constructs a new LazyLoadBuilder. diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp index 56dfdadb1..8c82ad8f9 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/built/data_system/lazy_load_config.hpp @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -19,6 +19,6 @@ struct LazyLoadConfig { EvictionPolicy eviction_policy; std::chrono::milliseconds refresh_ttl; - std::shared_ptr source; + std::shared_ptr source; }; } // namespace launchdarkly::server_side::config::built diff --git a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp similarity index 91% rename from libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp rename to libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp index 48e55361a..1f01e0594 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/data_interfaces/sources/iserialized_data_reader.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include @@ -9,7 +9,7 @@ #include #include -namespace launchdarkly::server_side::data_interfaces { +namespace launchdarkly::server_side::integrations { /** * Interface for a data reader that provides feature flags and related data in a @@ -89,4 +89,4 @@ class ISerializedDataReader { protected: ISerializedDataReader() = default; }; -} // namespace launchdarkly::server_side::data_interfaces +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_item_kind.hpp similarity index 100% rename from libs/server-sdk/include/launchdarkly/server_side/integrations/iserialized_item_kind.hpp rename to libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_item_kind.hpp diff --git a/libs/server-sdk/src/data_components/kinds/kinds.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/kinds.hpp similarity index 60% rename from libs/server-sdk/src/data_components/kinds/kinds.hpp rename to libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/kinds.hpp index bb2a0d22e..fdb78bf25 100644 --- a/libs/server-sdk/src/data_components/kinds/kinds.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/kinds.hpp @@ -1,10 +1,10 @@ #pragma once -#include +#include -namespace launchdarkly::server_side::data_components { +namespace launchdarkly::server_side::integrations { -class SegmentKind final : public integrations::ISerializedItemKind { +class SegmentKind final : public ISerializedItemKind { public: std::string const& Namespace() const override; std::uint64_t Version(std::string const& data) const override; @@ -15,7 +15,7 @@ class SegmentKind final : public integrations::ISerializedItemKind { static inline std::string const namespace_ = "segments"; }; -class FlagKind final : public integrations::ISerializedItemKind { +class FlagKind final : public ISerializedItemKind { public: std::string const& Namespace() const override; std::uint64_t Version(std::string const& data) const override; @@ -25,4 +25,4 @@ class FlagKind final : public integrations::ISerializedItemKind { private: static inline std::string const namespace_ = "features"; }; -} // namespace launchdarkly::server_side::data_components +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/serialized_item_descriptor.hpp similarity index 100% rename from libs/server-sdk/include/launchdarkly/server_side/integrations/serialized_item_descriptor.hpp rename to libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/serialized_item_descriptor.hpp diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 908b87f28..825855f2d 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -29,6 +29,7 @@ target_sources(${LIBNAME} all_flags_state/all_flags_state.cpp all_flags_state/json_all_flags_state.cpp all_flags_state/all_flags_state_builder.cpp + integrations/data_reader/kinds.cpp data_components/change_notifier/change_notifier.hpp data_components/change_notifier/change_notifier.cpp data_components/dependency_tracker/dependency_tracker.hpp @@ -41,8 +42,6 @@ target_sources(${LIBNAME} data_components/serialization_adapters/json_deserializer.cpp data_components/serialization_adapters/json_destination.hpp data_components/serialization_adapters/json_destination.cpp - data_components/kinds/kinds.hpp - data_components/kinds/kinds.cpp data_systems/background_sync/sources/polling/polling_data_source.hpp data_systems/background_sync/sources/polling/polling_data_source.cpp data_systems/background_sync/sources/streaming/streaming_data_source.hpp diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp index 11f06305a..e910216dd 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include @@ -11,7 +11,7 @@ namespace launchdarkly::server_side::data_components { JsonDeserializer::JsonDeserializer( Logger const& logger, - std::shared_ptr reader) + std::shared_ptr reader) : logger_(logger), flag_kind_(), segment_kind_(), diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index efc371bf3..ccdf2ad96 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -1,10 +1,10 @@ #pragma once +#include "../../../include/launchdarkly/server_side/integrations/data_reader/kinds.hpp" #include "../../data_interfaces/source/idata_reader.hpp" -#include "../kinds/kinds.hpp" #include -#include +#include #include @@ -14,7 +14,7 @@ class JsonDeserializer final : public data_interfaces::IDataReader { public: explicit JsonDeserializer( Logger const& logger, - std::shared_ptr reader); + std::shared_ptr reader); [[nodiscard]] Single GetFlag( std::string const& key) const override; @@ -112,9 +112,9 @@ class JsonDeserializer final : public data_interfaces::IDataReader { } Logger const& logger_; - FlagKind const flag_kind_; - SegmentKind const segment_kind_; - std::shared_ptr source_; + integrations::FlagKind const flag_kind_; + integrations::SegmentKind const segment_kind_; + std::shared_ptr source_; std::string const identity_; }; diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 52aa626d4..42d4c23fa 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -8,8 +8,11 @@ namespace launchdarkly::server_side::data_components { using data_interfaces::ISerializedDestination; using integrations::SerializedItemDescriptor; -FlagKind const JsonDestination::Kinds::Flag = FlagKind(); -SegmentKind const JsonDestination::Kinds::Segment = SegmentKind(); +integrations::FlagKind const JsonDestination::Kinds::Flag = + integrations::FlagKind(); + +integrations::SegmentKind const JsonDestination::Kinds::Segment = + integrations::SegmentKind(); /** * @brief Creates a boost::json::value representing a tombstone for a given diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp index 37c4d7289..77f4e6a01 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.hpp @@ -1,6 +1,6 @@ #pragma once -#include "../../data_components/kinds/kinds.hpp" +#include "../../../include/launchdarkly/server_side/integrations/data_reader/kinds.hpp" #include "../../data_interfaces/destination/idestination.hpp" #include "../../data_interfaces/destination/iserialized_destination.hpp" @@ -79,8 +79,8 @@ class JsonDestination final : public data_interfaces::IDestination { * @brief These are public so they can be referenced in tests. */ struct Kinds { - static FlagKind const Flag; - static SegmentKind const Segment; + static integrations::FlagKind const Flag; + static integrations::SegmentKind const Segment; }; private: diff --git a/libs/server-sdk/src/data_interfaces.cpp b/libs/server-sdk/src/data_interfaces.cpp index 030e9b2c5..286ba8a80 100644 --- a/libs/server-sdk/src/data_interfaces.cpp +++ b/libs/server-sdk/src/data_interfaces.cpp @@ -5,5 +5,5 @@ #include "data_interfaces/store/istore.hpp" #include "data_interfaces/system/idata_system.hpp" -#include -#include +#include +#include diff --git a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp index af99c100a..7d19c24f4 100644 --- a/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp +++ b/libs/server-sdk/src/data_interfaces/destination/iserialized_destination.hpp @@ -1,7 +1,7 @@ #pragma once -#include -#include +#include +#include #include #include diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 913b7a8e1..97e018631 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -48,11 +48,10 @@ namespace launchdarkly::server_side::data_systems { -data_components::FlagKind const LazyLoad::Kinds::Flag = - data_components::FlagKind(); +integrations::FlagKind const LazyLoad::Kinds::Flag = integrations::FlagKind(); -data_components::SegmentKind const LazyLoad::Kinds::Segment = - data_components::SegmentKind(); +integrations::SegmentKind const LazyLoad::Kinds::Segment = + integrations::SegmentKind(); LazyLoad::LazyLoad(Logger const& logger, config::built::LazyLoadConfig cfg, diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 9df3c959e..a7b27fe63 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -1,14 +1,14 @@ #pragma once +#include "../../../include/launchdarkly/server_side/integrations/data_reader/kinds.hpp" #include "../../data_components/expiration_tracker/expiration_tracker.hpp" -#include "../../data_components/kinds/kinds.hpp" #include "../../data_components/memory_store/memory_store.hpp" #include "../../data_components/status_notifications/data_source_status_manager.hpp" #include "../../data_interfaces/source/idata_reader.hpp" #include "../../data_interfaces/system/idata_system.hpp" #include -#include +#include #include #include @@ -60,8 +60,8 @@ class LazyLoad final : public data_interfaces::IDataSystem { // Public for usage in tests. struct Kinds { - static data_components::FlagKind const Flag; - static data_components::SegmentKind const Segment; + static integrations::FlagKind const Flag; + static integrations::SegmentKind const Segment; }; private: diff --git a/libs/server-sdk/src/data_components/kinds/kinds.cpp b/libs/server-sdk/src/integrations/data_reader/kinds.cpp similarity index 85% rename from libs/server-sdk/src/data_components/kinds/kinds.cpp rename to libs/server-sdk/src/integrations/data_reader/kinds.cpp index eaaea2c35..3c5f7d8b6 100644 --- a/libs/server-sdk/src/data_components/kinds/kinds.cpp +++ b/libs/server-sdk/src/integrations/data_reader/kinds.cpp @@ -1,4 +1,4 @@ -#include "kinds.hpp" +#include #include #include @@ -6,7 +6,7 @@ #include -namespace launchdarkly::server_side::data_components { +namespace launchdarkly::server_side::integrations { template static uint64_t GetVersion(std::string const& data) { @@ -42,4 +42,4 @@ std::uint64_t FlagKind::Version(std::string const& data) const { return GetVersion(data); } -} // namespace launchdarkly::server_side::data_components +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/tests/lazy_load_system_test.cpp b/libs/server-sdk/tests/lazy_load_system_test.cpp index 4b16cad0a..f908a8ac8 100644 --- a/libs/server-sdk/tests/lazy_load_system_test.cpp +++ b/libs/server-sdk/tests/lazy_load_system_test.cpp @@ -17,7 +17,7 @@ using ::testing::InSequence; using ::testing::NiceMock; using ::testing::Return; -class MockDataReader : public data_interfaces::ISerializedDataReader { +class MockDataReader : public integrations::ISerializedDataReader { public: MOCK_METHOD(GetResult, Get, @@ -104,7 +104,7 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfFlagCannotBeFetched) { EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) .WillOnce(Return(tl::make_unexpected( - data_interfaces::ISerializedDataReader::Error{"oops"}))); + integrations::ISerializedDataReader::Error{"oops"}))); for (std::size_t i = 0; i < 20; i++) { ASSERT_FALSE(lazy_load.GetFlag("foo")); @@ -123,7 +123,7 @@ TEST_F(LazyLoadTest, ReaderIsNotQueriedRepeatedlyIfSegmentCannotBeFetched) { EXPECT_CALL(*mock_reader, Get(testing::_, "foo")) .WillOnce(Return(tl::make_unexpected( - data_interfaces::ISerializedDataReader::Error{"oops"}))); + integrations::ISerializedDataReader::Error{"oops"}))); for (std::size_t i = 0; i < 20; i++) { ASSERT_FALSE(lazy_load.GetSegment("foo")); From 47758b30652fbc2c22973ad902c278ad8548e174 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 09:01:51 -0800 Subject: [PATCH 218/244] add github workflows --- .github/workflows/manual-publish-doc.yml | 1 + .../manual-sdk-release-artifacts.yml | 1 + .github/workflows/release-please.yml | 26 ++++++++ .github/workflows/server-redis.yml | 62 +++++++++++++++++++ .github/workflows/server.yml | 2 +- scripts/build-release.sh | 12 +++- scripts/build.sh | 15 ++++- 7 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/server-redis.yml diff --git a/.github/workflows/manual-publish-doc.yml b/.github/workflows/manual-publish-doc.yml index 535888d59..1eb81a308 100644 --- a/.github/workflows/manual-publish-doc.yml +++ b/.github/workflows/manual-publish-doc.yml @@ -9,6 +9,7 @@ on: options: - libs/client-sdk - libs/server-sdk + - libs/server-sdk-redis-source name: Publish Documentation jobs: build-publish: diff --git a/.github/workflows/manual-sdk-release-artifacts.yml b/.github/workflows/manual-sdk-release-artifacts.yml index 99e9f5db7..f5d9c61a3 100644 --- a/.github/workflows/manual-sdk-release-artifacts.yml +++ b/.github/workflows/manual-sdk-release-artifacts.yml @@ -15,6 +15,7 @@ on: options: - libs/client-sdk:launchdarkly-cpp-client - libs/server-sdk:launchdarkly-cpp-server + - libs/server-sdk-redis-source:launchdarkly-cpp-server-redis-source name: Publish SDK Artifacts diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index c1b665973..ce0f2470e 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -12,6 +12,8 @@ jobs: package-client-tag: ${{ steps.release.outputs['libs/client-sdk--tag_name'] }} package-server-released: ${{ steps.release.outputs['libs/server-sdk--release_created'] }} package-server-tag: ${{ steps.release.outputs['libs/server-sdk--tag_name'] }} + package-server-redis-released: ${{ steps.release.outputs['libs/server-sdk-redis-source-release_created'] }} + package-server-redis-tag: ${{ steps.release.outputs['libs/server-sdk-redis-source-tag_name'] }} steps: - uses: google-github-actions/release-please-action@v3 id: release @@ -68,6 +70,30 @@ jobs: sdk_path: 'libs/server-sdk' sdk_cmake_target: 'launchdarkly-cpp-server' + release-server-redis: + strategy: + matrix: + # Each of the platforms for which release-artifacts need generated. + os: [ ubuntu-latest, windows-2022, macos-12 ] + runs-on: ${{ matrix.os }} + needs: [ 'release-please' ] + if: ${{ needs.release-please.outputs.package-server-released }} + outputs: + hashes-linux: ${{ steps.release-server.outputs.hashes-linux }} + hashes-windows: ${{ steps.release-server.outputs.hashes-windows }} + hashes-macos: ${{ steps.release-server.outputs.hashes-macos }} + steps: + - uses: actions/checkout@v3 + - id: release-server-redis + name: Full release of libs/server-sdk-redis-source + uses: ./.github/actions/sdk-release + with: + # The tag of the release to upload artifacts to. + tag_name: ${{ needs.release-please.outputs.package-server-redis-tag }} + github_token: ${{secrets.GITHUB_TOKEN}} + sdk_path: 'libs/server-sdk-redis-source' + sdk_cmake_target: 'launchdarkly-cpp-server-redis-source' + release-client-provenance: needs: [ 'release-please', 'release-client' ] diff --git a/.github/workflows/server-redis.yml b/.github/workflows/server-redis.yml new file mode 100644 index 000000000..60069c87a --- /dev/null +++ b/.github/workflows/server-redis.yml @@ -0,0 +1,62 @@ +name: libs/server-sdk-redis-source + +on: + push: + branches: [ main ] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [ "main", "feat/**" ] + paths-ignore: + - '**.md' + +jobs: + build-test-redis: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + with: + cmake_target: launchdarkly-cpp-server-redis-source + # TODO: Fix installation of redis++/hiredis headers to enable releases. + simulate_release: false + build-test-redis-mac: + runs-on: macos-12 + steps: + - run: | + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" + # For debugging + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + with: + cmake_target: launchdarkly-cpp-server-redis-source + platform_version: 12 + build-test-redis-windows: + runs-on: windows-2022 + steps: + - name: Upgrade OpenSSL + shell: bash + run: | + choco upgrade openssl --no-progress + - name: Determine OpenSSL Installation Directory + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi + - uses: actions/checkout@v3 + - uses: ilammy/msvc-dev-cmd@v1 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + 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-redis-source + platform_version: 2022 + toolset: msvc diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 4f02084c4..e9dc90264 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -75,6 +75,6 @@ jobs: 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-client + cmake_target: launchdarkly-cpp-server platform_version: 2022 toolset: msvc diff --git a/scripts/build-release.sh b/scripts/build-release.sh index c06d26824..7d24f754e 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -5,10 +5,18 @@ set -e + +# Special case: unlike the other targets, enabling redis support will pull in redis++ and hiredis dependencies at +# configuration time. To ensure this only happens when asked, disable the support by default. +build_redis="OFF" +if [ "$1" == "launchdarkly-cpp-server-redis-source" ]; then + build_redis="ON" +fi + # Build a static release. mkdir -p build-static && cd build-static mkdir -p release -cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=OFF -D CMAKE_INSTALL_PREFIX=./release .. +cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D LD_BUILD_REDIS_SUPPORT="$build_redis" -D BUILD_TESTING=OFF -D CMAKE_INSTALL_PREFIX=./release .. cmake --build . --target "$1" cmake --install . @@ -17,7 +25,7 @@ cd .. # Build a dynamic release. mkdir -p build-dynamic && cd build-dynamic mkdir -p release -cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D BUILD_TESTING=OFF -D LD_BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. +cmake -G Ninja -D CMAKE_BUILD_TYPE=Release -D LD_BUILD_REDIS_SUPPORT="$build_redis" -D BUILD_TESTING=OFF -D LD_BUILD_SHARED_LIBS=ON -D CMAKE_INSTALL_PREFIX=./release .. cmake --build . --target "$1" cmake --install . diff --git a/scripts/build.sh b/scripts/build.sh index 7b77203f0..27967a1ad 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -17,6 +17,19 @@ cd build # script ends. trap cleanup EXIT -cmake -G Ninja -D CMAKE_COMPILE_WARNING_AS_ERROR=TRUE -D BUILD_TESTING="$2" -D LD_BUILD_UNIT_TESTS="$2" -D LD_BUILD_CONTRACT_TESTS="$2" .. +# Special case: unlike the other targets, enabling redis support will pull in redis++ and hiredis dependencies at +# configuration time. To ensure this only happens when asked, disable the support by default. +build_redis="OFF" +if [ "$1" == "launchdarkly-cpp-server-redis-source" ] || [ "$1" == "gtest_launchdarkly-cpp-server-redis-source" ]; then + build_redis="ON" +fi + + + +cmake -G Ninja -D CMAKE_COMPILE_WARNING_AS_ERROR=TRUE \ + -D BUILD_TESTING="$2" \ + -D LD_BUILD_UNIT_TESTS="$2" \ + -D LD_BUILD_CONTRACT_TESTS="$2" \ + -D LD_BUILD_REDIS_SUPPORT="$build_redis" .. cmake --build . --target "$1" From f9a37ac126f2be4fc2c3a3d33e9eab31465f9061 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 09:37:17 -0800 Subject: [PATCH 219/244] add redis container --- .github/workflows/server-redis.yml | 85 ++++++++++++++++-------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/.github/workflows/server-redis.yml b/.github/workflows/server-redis.yml index 60069c87a..510250467 100644 --- a/.github/workflows/server-redis.yml +++ b/.github/workflows/server-redis.yml @@ -13,6 +13,11 @@ on: jobs: build-test-redis: runs-on: ubuntu-22.04 + services: + redis: + image: redis + ports: + - 6379:6379 steps: - uses: actions/checkout@v3 - uses: ./.github/actions/ci @@ -20,43 +25,43 @@ jobs: cmake_target: launchdarkly-cpp-server-redis-source # TODO: Fix installation of redis++/hiredis headers to enable releases. simulate_release: false - build-test-redis-mac: - runs-on: macos-12 - steps: - - run: | - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" - # For debugging - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" - - uses: actions/checkout@v3 - - uses: ./.github/actions/ci - env: - OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} - with: - cmake_target: launchdarkly-cpp-server-redis-source - platform_version: 12 - build-test-redis-windows: - runs-on: windows-2022 - steps: - - name: Upgrade OpenSSL - shell: bash - run: | - choco upgrade openssl --no-progress - - name: Determine OpenSSL Installation Directory - shell: bash - run: | - if [ -d "C:\Program Files\OpenSSL-Win64" ]; then - echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" - else - echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" - fi - - uses: actions/checkout@v3 - - uses: ilammy/msvc-dev-cmd@v1 - - uses: ./.github/actions/ci - env: - OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} - 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-redis-source - platform_version: 2022 - toolset: msvc +# build-test-redis-mac: +# runs-on: macos-12 +# steps: +# - run: | +# echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" +# # For debugging +# echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" +# - uses: actions/checkout@v3 +# - uses: ./.github/actions/ci +# env: +# OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} +# with: +# cmake_target: launchdarkly-cpp-server-redis-source +# platform_version: 12 +# build-test-redis-windows: +# runs-on: windows-2022 +# steps: +# - name: Upgrade OpenSSL +# shell: bash +# run: | +# choco upgrade openssl --no-progress +# - name: Determine OpenSSL Installation Directory +# shell: bash +# run: | +# if [ -d "C:\Program Files\OpenSSL-Win64" ]; then +# echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" +# else +# echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" +# fi +# - uses: actions/checkout@v3 +# - uses: ilammy/msvc-dev-cmd@v1 +# - uses: ./.github/actions/ci +# env: +# OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} +# 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-redis-source +# platform_version: 2022 +# toolset: msvc From 4b361af2288e07180738fa3a05d37c6e3c56ca08 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 09:51:13 -0800 Subject: [PATCH 220/244] enable windows + mac builds for redis --- .github/workflows/server-redis.yml | 82 +++++++++++++++--------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/.github/workflows/server-redis.yml b/.github/workflows/server-redis.yml index 510250467..51d2c1c37 100644 --- a/.github/workflows/server-redis.yml +++ b/.github/workflows/server-redis.yml @@ -25,43 +25,45 @@ jobs: cmake_target: launchdarkly-cpp-server-redis-source # TODO: Fix installation of redis++/hiredis headers to enable releases. simulate_release: false -# build-test-redis-mac: -# runs-on: macos-12 -# steps: -# - run: | -# echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" -# # For debugging -# echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" -# - uses: actions/checkout@v3 -# - uses: ./.github/actions/ci -# env: -# OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} -# with: -# cmake_target: launchdarkly-cpp-server-redis-source -# platform_version: 12 -# build-test-redis-windows: -# runs-on: windows-2022 -# steps: -# - name: Upgrade OpenSSL -# shell: bash -# run: | -# choco upgrade openssl --no-progress -# - name: Determine OpenSSL Installation Directory -# shell: bash -# run: | -# if [ -d "C:\Program Files\OpenSSL-Win64" ]; then -# echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" -# else -# echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" -# fi -# - uses: actions/checkout@v3 -# - uses: ilammy/msvc-dev-cmd@v1 -# - uses: ./.github/actions/ci -# env: -# OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} -# 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-redis-source -# platform_version: 2022 -# toolset: msvc + build-redis-mac: + runs-on: macos-12 + steps: + - run: | + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" + # For debugging + echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" + - uses: actions/checkout@v3 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + with: + cmake_target: launchdarkly-cpp-server-redis-source + platform_version: 12 + run_tests: false # TODO: figure out how to run Redis service on Mac + build-test-redis-windows: + runs-on: windows-2022 + steps: + - name: Upgrade OpenSSL + shell: bash + run: | + choco upgrade openssl --no-progress + - name: Determine OpenSSL Installation Directory + shell: bash + run: | + if [ -d "C:\Program Files\OpenSSL-Win64" ]; then + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" + else + echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" + fi + - uses: actions/checkout@v3 + - uses: ilammy/msvc-dev-cmd@v1 + - uses: ./.github/actions/ci + env: + OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} + 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-redis-source + platform_version: 2022 + toolset: msvc + run_tests: false # TODO: figure out how to run Redis service on Windows From c18d743eab168405b35dd04a0755ada6d2a97bef Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 09:54:31 -0800 Subject: [PATCH 221/244] remove some cruft from action --- .github/workflows/server-redis.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/server-redis.yml b/.github/workflows/server-redis.yml index 51d2c1c37..4440de423 100644 --- a/.github/workflows/server-redis.yml +++ b/.github/workflows/server-redis.yml @@ -28,14 +28,8 @@ jobs: build-redis-mac: runs-on: macos-12 steps: - - run: | - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> "$GITHUB_ENV" - # For debugging - echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" - uses: actions/checkout@v3 - uses: ./.github/actions/ci - env: - OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} with: cmake_target: launchdarkly-cpp-server-redis-source platform_version: 12 @@ -43,23 +37,10 @@ jobs: build-test-redis-windows: runs-on: windows-2022 steps: - - name: Upgrade OpenSSL - shell: bash - run: | - choco upgrade openssl --no-progress - - name: Determine OpenSSL Installation Directory - shell: bash - run: | - if [ -d "C:\Program Files\OpenSSL-Win64" ]; then - echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL-Win64" >> "$GITHUB_ENV" - else - echo "OPENSSL_ROOT_DIR=C:\Program Files\OpenSSL" >> "$GITHUB_ENV" - fi - uses: actions/checkout@v3 - uses: ilammy/msvc-dev-cmd@v1 - uses: ./.github/actions/ci env: - OPENSSL_ROOT_DIR: ${{ env.OPENSSL_ROOT_DIR }} 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: From c63c292f2cf3c4eae923d6eb3d45825755436918 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 09:59:34 -0800 Subject: [PATCH 222/244] lints --- examples/CMakeLists.txt | 4 ++++ .../src/redis_source.cpp | 18 ++++++++---------- libs/server-sdk/tests/CMakeLists.txt | 14 +------------- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 138ec4942..f013bc666 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -3,3 +3,7 @@ add_subdirectory(hello-cpp-client) add_subdirectory(hello-cpp-server) add_subdirectory(hello-c-server) add_subdirectory(client-and-server-coexistence) + +if (LD_BUILD_REDIS_SUPPORT) + add_subdirectory(hello-cpp-server-redis) +endif () diff --git a/libs/server-sdk-redis-source/src/redis_source.cpp b/libs/server-sdk-redis-source/src/redis_source.cpp index 5da82747c..bce3f63a1 100644 --- a/libs/server-sdk-redis-source/src/redis_source.cpp +++ b/libs/server-sdk-redis-source/src/redis_source.cpp @@ -16,7 +16,7 @@ RedisDataSource::Create(std::string uri, std::string prefix) { } std::string RedisDataSource::key_for_kind( - integrations::ISerializedItemKind const& kind) const { + ISerializedItemKind const& kind) const { return prefix_ + ":" + kind.Namespace(); } @@ -28,13 +28,12 @@ RedisDataSource::RedisDataSource(std::unique_ptr redis, RedisDataSource::~RedisDataSource() = default; -integrations::ISerializedDataReader::GetResult RedisDataSource::Get( - integrations::ISerializedItemKind const& kind, +ISerializedDataReader::GetResult RedisDataSource::Get( + ISerializedItemKind const& kind, std::string const& itemKey) const { try { if (auto maybe_item = redis_->hget(key_for_kind(kind), itemKey)) { - return integrations::SerializedItemDescriptor{0, false, - maybe_item.value()}; + return SerializedItemDescriptor{0, false, maybe_item.value()}; } return tl::make_unexpected(Error{"not found"}); } catch (sw::redis::Error const& e) { @@ -42,8 +41,8 @@ integrations::ISerializedDataReader::GetResult RedisDataSource::Get( } } -integrations::ISerializedDataReader::AllResult RedisDataSource::All( - integrations::ISerializedItemKind const& kind) const { +ISerializedDataReader::AllResult RedisDataSource::All( + ISerializedItemKind const& kind) const { std::unordered_map raw_items; AllResult::value_type items; @@ -51,8 +50,7 @@ integrations::ISerializedDataReader::AllResult RedisDataSource::All( redis_->hgetall(key_for_kind(kind), std::inserter(raw_items, raw_items.begin())); for (auto const& [key, val] : raw_items) { - items.emplace( - key, integrations::SerializedItemDescriptor{0, false, val}); + items.emplace(key, SerializedItemDescriptor{0, false, val}); } return items; @@ -74,4 +72,4 @@ bool RedisDataSource::Initialized() const { } } -} // namespace launchdarkly::server_side::data_systems +} // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index e2c91097f..d2cf6fc78 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -17,21 +17,9 @@ endif () add_executable(gtest_${LIBNAME} ${tests} ) - -# Set library list to link: -set(LIBS - launchdarkly::server - launchdarkly::internal - launchdarkly::sse - timestamp - GTest::gtest_main - GTest::gmock -) - -target_link_libraries(gtest_${LIBNAME} PRIVATE ${LIBS}) +target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock) # There's an uninitialized variable warning in gtest that hasn't been resolved. set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) - gtest_discover_tests(gtest_${LIBNAME}) From 40a23e780ca35a56d425890f89693dad6aa9331a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 10:48:56 -0800 Subject: [PATCH 223/244] fix hello redis example build to link against proper target --- examples/hello-cpp-server-redis/CMakeLists.txt | 2 +- examples/hello-cpp-server-redis/main.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello-cpp-server-redis/CMakeLists.txt b/examples/hello-cpp-server-redis/CMakeLists.txt index 80287d646..8d75b1ab4 100644 --- a/examples/hello-cpp-server-redis/CMakeLists.txt +++ b/examples/hello-cpp-server-redis/CMakeLists.txt @@ -12,4 +12,4 @@ set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) add_executable(hello-cpp-server-redis-source main.cpp) -target_link_libraries(hello-cpp-server-redis-source PRIVATE launchdarkly::server Threads::Threads) +target_link_libraries(hello-cpp-server-redis-source PRIVATE launchdarkly::server_redis_source Threads::Threads) diff --git a/examples/hello-cpp-server-redis/main.cpp b/examples/hello-cpp-server-redis/main.cpp index cf3c0b477..aae677f55 100644 --- a/examples/hello-cpp-server-redis/main.cpp +++ b/examples/hello-cpp-server-redis/main.cpp @@ -36,7 +36,7 @@ int main() { using LazyLoad = server_side::config::builders::LazyLoadBuilder; - auto redis = data_systems::RedisDataSource::Create("redis://localhost:6379", + auto redis = integrations::RedisDataSource::Create("redis://localhost:6379", "launchdarkly"); if (!redis) { From 0f007aac7c279cfacff5516984aedce9bd620523 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 10:49:07 -0800 Subject: [PATCH 224/244] link redis++ statically always --- libs/server-sdk-redis-source/CMakeLists.txt | 2 ++ libs/server-sdk-redis-source/src/CMakeLists.txt | 5 +++-- libs/server-sdk-redis-source/tests/CMakeLists.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/libs/server-sdk-redis-source/CMakeLists.txt b/libs/server-sdk-redis-source/CMakeLists.txt index edbc69ce8..58894b4b1 100644 --- a/libs/server-sdk-redis-source/CMakeLists.txt +++ b/libs/server-sdk-redis-source/CMakeLists.txt @@ -24,6 +24,8 @@ endif () # Needed to fetch external dependencies. include(FetchContent) +set(REDIS_PLUS_PLUS_BUILD_SHARED OFF CACHE BOOL "" FORCE) + include(${CMAKE_FILES}/redis-plus-plus.cmake) add_subdirectory(src) diff --git a/libs/server-sdk-redis-source/src/CMakeLists.txt b/libs/server-sdk-redis-source/src/CMakeLists.txt index 2fc232602..2c2b39603 100644 --- a/libs/server-sdk-redis-source/src/CMakeLists.txt +++ b/libs/server-sdk-redis-source/src/CMakeLists.txt @@ -10,7 +10,6 @@ else () add_library(${LIBNAME} STATIC) endif () - target_sources(${LIBNAME} PRIVATE ${HEADER_LIST} @@ -20,7 +19,9 @@ target_sources(${LIBNAME} target_link_libraries(${LIBNAME} PUBLIC launchdarkly::server - PRIVATE redis++) + PRIVATE + redis++::redis++_static +) add_library(launchdarkly::server_redis_source ALIAS ${LIBNAME}) diff --git a/libs/server-sdk-redis-source/tests/CMakeLists.txt b/libs/server-sdk-redis-source/tests/CMakeLists.txt index 2350db8d4..8e35437cf 100644 --- a/libs/server-sdk-redis-source/tests/CMakeLists.txt +++ b/libs/server-sdk-redis-source/tests/CMakeLists.txt @@ -24,7 +24,7 @@ set(LIBS launchdarkly::internal # Needed because the source doesn't (need to) expose redis++ as a public dependency, but we need to construct # a redis client in the tests. - redis++ + redis++::redis++_static GTest::gtest_main GTest::gmock ) From 9e922c52880e4709a539066a59cbe6b1739e6cc8 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 10:55:46 -0800 Subject: [PATCH 225/244] revert release please changes --- .github/workflows/release-please.yml | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index ce0f2470e..a35a3e9a2 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -12,8 +12,6 @@ jobs: package-client-tag: ${{ steps.release.outputs['libs/client-sdk--tag_name'] }} package-server-released: ${{ steps.release.outputs['libs/server-sdk--release_created'] }} package-server-tag: ${{ steps.release.outputs['libs/server-sdk--tag_name'] }} - package-server-redis-released: ${{ steps.release.outputs['libs/server-sdk-redis-source-release_created'] }} - package-server-redis-tag: ${{ steps.release.outputs['libs/server-sdk-redis-source-tag_name'] }} steps: - uses: google-github-actions/release-please-action@v3 id: release @@ -70,31 +68,6 @@ jobs: sdk_path: 'libs/server-sdk' sdk_cmake_target: 'launchdarkly-cpp-server' - release-server-redis: - strategy: - matrix: - # Each of the platforms for which release-artifacts need generated. - os: [ ubuntu-latest, windows-2022, macos-12 ] - runs-on: ${{ matrix.os }} - needs: [ 'release-please' ] - if: ${{ needs.release-please.outputs.package-server-released }} - outputs: - hashes-linux: ${{ steps.release-server.outputs.hashes-linux }} - hashes-windows: ${{ steps.release-server.outputs.hashes-windows }} - hashes-macos: ${{ steps.release-server.outputs.hashes-macos }} - steps: - - uses: actions/checkout@v3 - - id: release-server-redis - name: Full release of libs/server-sdk-redis-source - uses: ./.github/actions/sdk-release - with: - # The tag of the release to upload artifacts to. - tag_name: ${{ needs.release-please.outputs.package-server-redis-tag }} - github_token: ${{secrets.GITHUB_TOKEN}} - sdk_path: 'libs/server-sdk-redis-source' - sdk_cmake_target: 'launchdarkly-cpp-server-redis-source' - - release-client-provenance: needs: [ 'release-please', 'release-client' ] strategy: From 14c3cefddf62330d3ae4cd25a65ae5944d92216d Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 11:09:43 -0800 Subject: [PATCH 226/244] try simulating release --- .github/workflows/server-redis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/server-redis.yml b/.github/workflows/server-redis.yml index 4440de423..a3c098fd6 100644 --- a/.github/workflows/server-redis.yml +++ b/.github/workflows/server-redis.yml @@ -23,8 +23,7 @@ jobs: - uses: ./.github/actions/ci with: cmake_target: launchdarkly-cpp-server-redis-source - # TODO: Fix installation of redis++/hiredis headers to enable releases. - simulate_release: false + simulate_release: true build-redis-mac: runs-on: macos-12 steps: From 54ab92254614ccf5be54e67843fbe445a1de3e76 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 11:15:13 -0800 Subject: [PATCH 227/244] make hello apps pass? --- examples/hello-c-server/main.c | 2 +- scripts/run-hello-apps.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello-c-server/main.c b/examples/hello-c-server/main.c index 5c1acc69a..f152ad60a 100644 --- a/examples/hello-c-server/main.c +++ b/examples/hello-c-server/main.c @@ -53,7 +53,7 @@ int main() { return 1; } } else { - printf("SDK initialization didn't complete in %dms\n", + printf("*** SDK initialization didn't complete in %dms\n", INIT_TIMEOUT_MILLISECONDS); return 1; } diff --git a/scripts/run-hello-apps.sh b/scripts/run-hello-apps.sh index 9b87fed5d..e24412a75 100755 --- a/scripts/run-hello-apps.sh +++ b/scripts/run-hello-apps.sh @@ -41,5 +41,5 @@ for target in "$@" do cmake --build . --target "$target" ./examples/"$target"/"$target" | tee "$target"_output.txt - grep "failed to initialize" "$target"_output.txt || (echo "$target: expected connection to LD to fail" && exit 1) + grep -F "*** SDK" "$target"_output.txt || (echo "$target: expected connection to LD to fail" && exit 1) done From 623bdd8292b3d0cb408306c06dbe1a708bfd816e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 15:41:50 -0800 Subject: [PATCH 228/244] significantly simplify ISerializedDataReader interface and usage --- .../src/redis_source.cpp | 7 +- .../tests/redis_source_test.cpp | 110 +++++++++++++--- .../data_reader/iserialized_data_reader.hpp | 15 +-- .../serialized_item_descriptor.hpp | 14 +- .../json_deserializer.cpp | 8 +- .../json_deserializer.hpp | 122 +++++++++++------- .../json_destination.cpp | 2 +- .../data_interfaces/source/idata_reader.hpp | 34 ++++- .../lazy_load/lazy_load_system.cpp | 14 +- .../lazy_load/lazy_load_system.hpp | 44 +++++-- .../tests/json_destination_test.cpp | 4 +- 11 files changed, 258 insertions(+), 116 deletions(-) diff --git a/libs/server-sdk-redis-source/src/redis_source.cpp b/libs/server-sdk-redis-source/src/redis_source.cpp index bce3f63a1..0e8ccdf46 100644 --- a/libs/server-sdk-redis-source/src/redis_source.cpp +++ b/libs/server-sdk-redis-source/src/redis_source.cpp @@ -33,9 +33,10 @@ ISerializedDataReader::GetResult RedisDataSource::Get( std::string const& itemKey) const { try { if (auto maybe_item = redis_->hget(key_for_kind(kind), itemKey)) { - return SerializedItemDescriptor{0, false, maybe_item.value()}; + return SerializedItemDescriptor::Present( + 0, std::move(maybe_item.value())); } - return tl::make_unexpected(Error{"not found"}); + return std::nullopt; } catch (sw::redis::Error const& e) { return tl::make_unexpected(Error{e.what()}); } @@ -50,7 +51,7 @@ ISerializedDataReader::AllResult RedisDataSource::All( redis_->hgetall(key_for_kind(kind), std::inserter(raw_items, raw_items.begin())); for (auto const& [key, val] : raw_items) { - items.emplace(key, SerializedItemDescriptor{0, false, val}); + items.emplace(key, SerializedItemDescriptor::Present(0, val)); } return items; diff --git a/libs/server-sdk-redis-source/tests/redis_source_test.cpp b/libs/server-sdk-redis-source/tests/redis_source_test.cpp index cc45ebf8b..1d2ce850c 100644 --- a/libs/server-sdk-redis-source/tests/redis_source_test.cpp +++ b/libs/server-sdk-redis-source/tests/redis_source_test.cpp @@ -20,7 +20,7 @@ class PrefixedClient { void Init() const { try { - client_.set(prefix_ + ":$inited", "true"); + client_.set(Prefixed("$inited"), "true"); } catch (sw::redis::Error const& e) { FAIL() << e.what(); } @@ -28,16 +28,33 @@ class PrefixedClient { void PutFlag(Flag const& flag) const { try { - client_.hset(prefix_ + ":features", flag.key, + client_.hset(Prefixed("features"), flag.key, serialize(boost::json::value_from(flag))); } catch (sw::redis::Error const& e) { FAIL() << e.what(); } } + void PutDeletedFlag(std::string const& key, std::string const& ts) const { + try { + client_.hset(Prefixed("features"), key, ts); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + + void PutDeletedSegment(std::string const& key, + std::string const& ts) const { + try { + client_.hset(Prefixed("segments"), key, ts); + } catch (sw::redis::Error const& e) { + FAIL() << e.what(); + } + } + void PutSegment(Segment const& segment) const { try { - client_.hset(prefix_ + ":segments", segment.key, + client_.hset(Prefixed("segments"), segment.key, serialize(boost::json::value_from(segment))); } catch (sw::redis::Error const& e) { FAIL() << e.what(); @@ -47,7 +64,7 @@ class PrefixedClient { void Clear() const { try { std::vector> output; - client_.keys(prefix_ + ":*", std::back_inserter(output)); + client_.keys(Prefixed("*"), std::back_inserter(output)); for (auto const& [key, _] : output) { client_.del(key); } @@ -57,6 +74,10 @@ class PrefixedClient { } private: + std::string Prefixed(std::string const& name) const { + return prefix_ + ":" + name; + } + sw::redis::Redis& client_; std::string const& prefix_; }; @@ -88,6 +109,16 @@ class RedisTests : public ::testing::Test { client.PutFlag(flag); } + void PutDeletedFlag(std::string const& key, std::string const& ts) { + auto const client = PrefixedClient(client_, prefix_); + client.PutDeletedFlag(key, ts); + } + + void PutDeletedSegment(std::string const& key, std::string const& ts) { + auto const client = PrefixedClient(client_, prefix_); + client.PutDeletedSegment(key, ts); + } + void PutSegment(Segment const& segment) { auto const client = PrefixedClient(client_, prefix_); client.PutSegment(segment); @@ -132,37 +163,86 @@ TEST_F(RedisTests, ChecksInitialized) { } TEST_F(RedisTests, GetFlag) { - PutFlag(Flag{"foo", 1, true}); + Flag const flag{"foo", 1, true}; + PutFlag(flag); auto const result = source->Get(FlagKind{}, "foo"); ASSERT_TRUE(result); - ASSERT_FALSE(result->deleted); - ASSERT_TRUE(result->serializedItem); + if (auto const f = *result) { + ASSERT_EQ(f->serializedItem, serialize(boost::json::value_from(flag))); + } else { + FAIL() << "expected flag to be found"; + } } -TEST_F(RedisTests, GetFlagDoesNotFindSegment) { - PutSegment(Segment{"foo", 1}); +TEST_F(RedisTests, GetSegment) { + Segment const segment{"foo", 1}; + PutSegment(segment); + + auto const result = source->Get(SegmentKind{}, "foo"); + ASSERT_TRUE(result); + + if (auto const f = *result) { + ASSERT_EQ(f->serializedItem, + serialize(boost::json::value_from(segment))); + } else { + FAIL() << "expected segment to be found"; + } +} +TEST_F(RedisTests, GetMissingFlag) { auto const result = source->Get(FlagKind{}, "foo"); - ASSERT_FALSE(result); + ASSERT_TRUE(result); + ASSERT_FALSE(*result); } -TEST_F(RedisTests, GetSegment) { - PutSegment(Segment{"foo", 1}); +TEST_F(RedisTests, GetMissingSegment) { + auto const result = source->Get(SegmentKind{}, "foo"); + ASSERT_TRUE(result); + ASSERT_FALSE(*result); +} + +TEST_F(RedisTests, GetDeletedFlag) { + PutDeletedFlag("foo", "foo_tombstone"); + + auto const result = source->Get(FlagKind{}, "foo"); + ASSERT_TRUE(result); + + if (auto const f = *result) { + ASSERT_EQ(f->serializedItem, "foo_tombstone"); + } else { + FAIL() << "expected tombstone to be present"; + } +} + +TEST_F(RedisTests, GetDeletedSegment) { + PutDeletedSegment("foo", "foo_tombstone"); auto const result = source->Get(SegmentKind{}, "foo"); ASSERT_TRUE(result); - ASSERT_FALSE(result->deleted); - ASSERT_TRUE(result->serializedItem); + if (auto const f = *result) { + ASSERT_EQ(f->serializedItem, "foo_tombstone"); + } else { + FAIL() << "expected tombstone to be present"; + } +} + +TEST_F(RedisTests, GetFlagDoesNotFindSegment) { + PutSegment(Segment{"foo", 1}); + + auto const result = source->Get(FlagKind{}, "foo"); + ASSERT_TRUE(result); + ASSERT_FALSE(*result); } TEST_F(RedisTests, GetSegmentDoesNotFindFlag) { PutFlag(Flag{"foo", 1, true}); auto const result = source->Get(SegmentKind{}, "foo"); - ASSERT_FALSE(result); + ASSERT_TRUE(result); + ASSERT_FALSE(*result); } TEST_F(RedisTests, ChecksInitializedPrefixIndependence) { diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp index 1f01e0594..155fa4d11 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/iserialized_data_reader.hpp @@ -41,11 +41,11 @@ class ISerializedDataReader { }; using GetResult = - tl::expected; + tl::expected, Error>; - using AllResult = tl::expected< - std::unordered_map, - Error>; + using AllResult = + tl::expected, + Error>; /** * Retrieves an item from the specified collection, if available. @@ -56,9 +56,8 @@ class ISerializedDataReader { * 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. */ - [[nodiscard]] virtual GetResult Get( - integrations::ISerializedItemKind const& kind, - std::string const& itemKey) const = 0; + [[nodiscard]] virtual GetResult Get(ISerializedItemKind const& kind, + std::string const& itemKey) const = 0; /** * Retrieves all items from the specified collection. @@ -70,7 +69,7 @@ class ISerializedDataReader { * no items of the specified type, then return an empty collection. */ [[nodiscard]] virtual AllResult All( - integrations::ISerializedItemKind const& kind) const = 0; + ISerializedItemKind const& kind) const = 0; /** * @return Identity of the reader. Used in logs. diff --git a/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/serialized_item_descriptor.hpp b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/serialized_item_descriptor.hpp index 71f48b986..87ffc3211 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/serialized_item_descriptor.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/integrations/data_reader/serialized_item_descriptor.hpp @@ -20,11 +20,7 @@ struct SerializedItemDescriptor { */ bool deleted; - /** - * When reading from a persistent store the serializedItem may be - * std::nullopt for deleted items. - */ - std::optional serializedItem; + std::string serializedItem; /** * @brief Constructs a SerializedItemDescriptor from a version and a @@ -33,7 +29,7 @@ struct SerializedItemDescriptor { * @param data Serialized item. * @return SerializedItemDescriptor. */ - static SerializedItemDescriptor Present(std::uint64_t version, + static SerializedItemDescriptor Present(std::uint64_t const version, std::string data) { return SerializedItemDescriptor{version, false, std::move(data)}; } @@ -50,8 +46,8 @@ struct SerializedItemDescriptor { * @param tombstone_rep Serialized tombstone representation of the item. * @return SerializedItemDescriptor. */ - static SerializedItemDescriptor Absent(std::uint64_t const version, - std::string tombstone_rep) { + static SerializedItemDescriptor Tombstone(std::uint64_t const version, + std::string tombstone_rep) { return SerializedItemDescriptor{version, true, std::move(tombstone_rep)}; } @@ -65,7 +61,7 @@ inline bool operator==(SerializedItemDescriptor const& lhs, inline void PrintTo(SerializedItemDescriptor const& item, std::ostream* os) { *os << "{version=" << item.version << ", deleted=" << item.deleted - << ", item=" << item.serializedItem.value_or("nullopt") << "}"; + << ", item=" << item.serializedItem << "}"; } } // namespace launchdarkly::server_side::integrations diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp index e910216dd..5f646bae9 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.cpp @@ -18,22 +18,22 @@ JsonDeserializer::JsonDeserializer( source_(std::move(reader)), identity_(source_->Identity() + " (JSON)") {} -data_interfaces::IDataReader::Single +data_interfaces::IDataReader::SingleResult JsonDeserializer::GetFlag(std::string const& key) const { return DeserializeSingle(flag_kind_, key); } -data_interfaces::IDataReader::Single +data_interfaces::IDataReader::SingleResult JsonDeserializer::GetSegment(std::string const& key) const { return DeserializeSingle(segment_kind_, key); } -data_interfaces::IDataReader::Collection +data_interfaces::IDataReader::CollectionResult JsonDeserializer::AllFlags() const { return DeserializeCollection(flag_kind_); } -data_interfaces::IDataReader::Collection +data_interfaces::IDataReader::CollectionResult JsonDeserializer::AllSegments() const { return DeserializeCollection(segment_kind_); } diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index ccdf2ad96..d69192f5a 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -10,22 +10,78 @@ namespace launchdarkly::server_side::data_components { +template +tl::expected, + data_interfaces::IDataReader::Error> +IntoStorageItem(integrations::SerializedItemDescriptor const& descriptor) { + if (descriptor.deleted) { + return data_interfaces::IDataReader::StorageItem( + data_interfaces::IDataReader::Tombstone(descriptor.version)); + } + + auto const json_val = boost::json::parse(descriptor.serializedItem); + + auto result = + boost::json::value_to, JsonError>>( + json_val); + + if (!result) { + /* maybe it's a tombstone */ + + if (json_val.is_object()) { + auto const& obj = json_val.as_object(); + if (auto deleted_it = obj.find("deleted"); + deleted_it != obj.end()) { + auto const& deleted = deleted_it->value(); + + if (deleted.is_bool() && deleted.as_bool()) { + if (auto version_it = obj.find("version"); + version_it != obj.end()) { + auto const& version = version_it->value(); + if (version.is_number()) { + return data_interfaces::IDataReader::Tombstone( + version.as_uint64()); + } + return tl::make_unexpected( + data_interfaces::IDataReader::Error{ + "tombstone field 'version' is invalid"}); + } + return tl::make_unexpected( + "tombstone field 'version' is missing"); + } + return tl::make_unexpected( + "tombstone field 'deleted' is invalid "); + } + } + + return tl::make_unexpected( + "serialized item isn't a valid data item or tombstone"); + } + + auto item = *result; + + if (!item) { + return tl::make_unexpected("serialized item is null JSON value"); + } + + return *item; +} + class JsonDeserializer final : public data_interfaces::IDataReader { public: explicit JsonDeserializer( Logger const& logger, std::shared_ptr reader); - [[nodiscard]] Single GetFlag( + [[nodiscard]] SingleResult GetFlag( std::string const& key) const override; - [[nodiscard]] Single GetSegment( + [[nodiscard]] SingleResult GetSegment( std::string const& key) const override; - [[nodiscard]] Collection AllFlags() - const override; + [[nodiscard]] CollectionResult AllFlags() const override; - [[nodiscard]] Collection AllSegments() + [[nodiscard]] CollectionResult AllSegments() const override; [[nodiscard]] std::string const& Identity() const override; @@ -34,29 +90,8 @@ class JsonDeserializer final : public data_interfaces::IDataReader { private: template - tl::expected, std::string> - DeserializeItem(std::string const& serialized_item) const { - auto const boost_json_val = boost::json::parse(serialized_item); - auto item = boost::json::value_to< - tl::expected, JsonError>>(boost_json_val); - - if (!item) { - return tl::make_unexpected(ErrorToString(item.error())); - } - - std::optional maybe_item = item->value(); - - if (!maybe_item) { - return tl::make_unexpected("JSON value is null"); - } - - return data_model::ItemDescriptor(std::move(*maybe_item)); - } - - template - Single> DeserializeSingle( - DataKind const& kind, - std::string const& key) const { + SingleResult DeserializeSingle(DataKind const& kind, + std::string const& key) const { auto result = source_->Get(kind, key); if (!result) { @@ -64,16 +99,17 @@ class JsonDeserializer final : public data_interfaces::IDataReader { return tl::make_unexpected(result.error().message); } - if (!result->serializedItem) { - /* no error, but item not found by the source */ + auto serialized_item = *result; + + if (!serialized_item) { return std::nullopt; } - return DeserializeItem(*result->serializedItem); + return IntoStorageItem(*serialized_item); } template - Collection> DeserializeCollection( + CollectionResult DeserializeCollection( DataKind const& kind) const { auto result = source_->All(kind); @@ -82,31 +118,19 @@ class JsonDeserializer final : public data_interfaces::IDataReader { return tl::make_unexpected(result.error().message); } - std::unordered_map> - items; + Collection items; for (auto const& [key, descriptor] : *result) { - if (!descriptor.serializedItem) { - /* item is deleted, add a tombstone to the result so that the - * caller can make a decision on what to do. */ - items.emplace(key, data_model::ItemDescriptor( - descriptor.version)); - continue; - } - - auto maybe_item = DeserializeItem( - *descriptor.serializedItem); + auto item = IntoStorageItem(descriptor); - if (!maybe_item) { - /* single item failing to deserialize doesn't cause the - * whole operation to fail; other items may be valid. */ + if (!item) { LD_LOG(logger_, LogLevel::kError) << "failed to deserialize " << key << " while fetching all " - << kind.Namespace() << ": " << maybe_item.error(); + << kind.Namespace() << ": " << item.error(); continue; } - items.emplace(key, *maybe_item); + items.emplace(key, *item); } return items; } diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 42d4c23fa..115dd621b 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -52,7 +52,7 @@ SerializedItemDescriptor Serialize(std::string const& key, ? SerializedItemDescriptor::Present( desc.version, boost::json::serialize( boost::json::value_from(*desc.item))) - : SerializedItemDescriptor::Absent( + : SerializedItemDescriptor::Tombstone( desc.version, boost::json::serialize(Tombstone(key, desc))); } diff --git a/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp b/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp index adcc7e38f..e2c46389e 100644 --- a/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp +++ b/libs/server-sdk/src/data_interfaces/source/idata_reader.hpp @@ -20,13 +20,33 @@ namespace launchdarkly::server_side::data_interfaces { */ class IDataReader { public: + BOOST_STRONG_TYPEDEF(std::uint64_t, Tombstone); + + template + using StorageItem = std::variant; + + template + static data_model::ItemDescriptor StorageItemIntoDescriptor( + StorageItem item) { + if (std::holds_alternative(item)) { + return data_model::ItemDescriptor(std::get(item)); + } + return data_model::ItemDescriptor(std::move(std::get(item))); + } + using Error = std::string; template - using Single = tl::expected, Error>; + using Single = std::optional>; template - using Collection = tl::expected, Error>; + using SingleResult = tl::expected, Error>; + + template + using Collection = std::unordered_map>; + + template + using CollectionResult = tl::expected, Error>; /** * @brief Attempts to get a flag named by key. @@ -34,7 +54,7 @@ class IDataReader { * @return On success, an optional FlagDescriptor (std::nullopt means the * flag doesn't exist.) On failure, an error string. */ - [[nodiscard]] virtual Single GetFlag( + [[nodiscard]] virtual SingleResult GetFlag( std::string const& key) const = 0; /** @@ -43,7 +63,7 @@ class IDataReader { * @return On success, an optional SegmentDescriptor (std::nullopt means the * segment doesn't exist.) On failure, an error string. */ - [[nodiscard]] virtual Single GetSegment( + [[nodiscard]] virtual SingleResult GetSegment( std::string const& key) const = 0; /** @@ -51,7 +71,7 @@ class IDataReader { * @return On success, a collection of FlagDescriptors. On failure, an error * string. */ - [[nodiscard]] virtual Collection AllFlags() + [[nodiscard]] virtual CollectionResult AllFlags() const = 0; /** @@ -59,8 +79,8 @@ class IDataReader { * @return On success, a collection of SegmentDescriptors. On failure, an * error string. */ - [[nodiscard]] virtual Collection - AllSegments() const = 0; + [[nodiscard]] virtual CollectionResult AllSegments() + const = 0; /** * @return Identity of the reader. Used in logs. diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 97e018631..0af904c51 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -143,13 +143,15 @@ bool LazyLoad::Initialized() const { } void LazyLoad::RefreshAllFlags() const { - RefreshAll(Keys::kAllFlags, data_components::DataKind::kFlag, - [this]() { return reader_->AllFlags(); }); + RefreshAll(Keys::kAllFlags, + data_components::DataKind::kFlag, + [this]() { return reader_->AllFlags(); }); } void LazyLoad::RefreshAllSegments() const { - RefreshAll(Keys::kAllSegments, data_components::DataKind::kSegment, - [this]() { return reader_->AllSegments(); }); + RefreshAll( + Keys::kAllSegments, data_components::DataKind::kSegment, + [this]() { return reader_->AllSegments(); }); } void LazyLoad::RefreshInitState() const { @@ -158,14 +160,14 @@ void LazyLoad::RefreshInitState() const { } void LazyLoad::RefreshSegment(std::string const& segment_key) const { - RefreshItem( + RefreshItem( data_components::DataKind::kSegment, segment_key, [this](std::string const& key) { return reader_->GetSegment(key); }, [this](std::string const& key) { return cache_.RemoveSegment(key); }); } void LazyLoad::RefreshFlag(std::string const& flag_key) const { - RefreshItem( + RefreshItem( data_components::DataKind::kFlag, flag_key, [this](std::string const& key) { return reader_->GetFlag(key); }, [this](std::string const& key) { return cache_.RemoveFlag(key); }); diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index a7b27fe63..3efa0b407 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -96,11 +96,13 @@ class LazyLoad final : public data_interfaces::IDataSystem { detail::unreachable(); } - template - void RefreshItem(data_components::DataKind const kind, - std::string const& key, - Getter&& getter, - Evictor&& evictor) const { + template + void RefreshItem( + data_components::DataKind const kind, + std::string const& key, + std::function( + std::string const&)> const& getter, + Evictor&& evictor) const { // Refreshing this item is always rate limited, even // if the refresh has an error. tracker_.Add(kind, key, ExpiryTime()); @@ -109,7 +111,15 @@ class LazyLoad final : public data_interfaces::IDataSystem { status_manager_.SetState(DataSourceState::kValid); if (auto optional_item = *expected_item) { - cache_.Upsert(key, std::move(*optional_item)); + // This transformation is necessary because the memory store + // works with ItemDescriptors, whereas the reader operates using + // IDataReader::StorageItems. This doesn't necessarily need to + // be the case. + cache_.Upsert( + key, + data_interfaces::IDataReader::StorageItemIntoDescriptor( + std::move(*optional_item))); + } else { // If the item is actually *missing* - not just a deleted // tombstone representation - it implies that the source @@ -137,10 +147,13 @@ class LazyLoad final : public data_interfaces::IDataSystem { } } - template - void RefreshAll(std::string const& all_item_key, - data_components::DataKind const item_kind, - Getter&& getter) const { + template + void RefreshAll( + std::string const& all_item_key, + data_components::DataKind const item_kind, + std::function< + data_interfaces::IDataReader::CollectionResult()> const& + getter) const { // Storing an expiry time so that the 'all' key and the individual // item keys will expire at the same time. auto const updated_expiry = ExpiryTime(); @@ -153,8 +166,15 @@ class LazyLoad final : public data_interfaces::IDataSystem { status_manager_.SetState(DataSourceState::kValid); for (auto item : *all_items) { - cache_.Upsert(item.first, std::move(item.second)); - tracker_.Add(item_kind, item.first, updated_expiry); + // This transformation is necessary because the memory store + // works with ItemDescriptors, whereas the reader operates using + // IDataReader::StorageItems. This doesn't necessarily need to + // be the case. + cache_.Upsert( + item.first, + data_interfaces::IDataReader::StorageItemIntoDescriptor( + std::move(item.second))); + tracker_.Add(item.first, updated_expiry); } } else { status_manager_.SetState( diff --git a/libs/server-sdk/tests/json_destination_test.cpp b/libs/server-sdk/tests/json_destination_test.cpp index 1eadae8d8..6cd0ba54f 100644 --- a/libs/server-sdk/tests/json_destination_test.cpp +++ b/libs/server-sdk/tests/json_destination_test.cpp @@ -149,7 +149,7 @@ TEST_F(JsonDestinationTest, UpsertDeletedFlagCreatesTombstone) { EXPECT_CALL( mock_dest, Upsert(Ref(JsonDestination::Kinds::Flag), "flag", - SerializedItemDescriptor::Absent( + SerializedItemDescriptor::Tombstone( 2, "{\"key\":\"flag\",\"version\":2,\"deleted\":true}"))) .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); @@ -160,7 +160,7 @@ TEST_F(JsonDestinationTest, UpsertDeletedSegmentCreatesTombstone) { EXPECT_CALL( mock_dest, Upsert(Ref(JsonDestination::Kinds::Segment), "segment", - SerializedItemDescriptor::Absent( + SerializedItemDescriptor::Tombstone( 2, "{\"key\":\"segment\",\"version\":2,\"deleted\":true}"))) .WillOnce(Return(ISerializedDestination::UpsertResult::kSuccess)); From 2c28a45f6b693db002ff641e76f6ae6fcb5ee500 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 16:03:38 -0800 Subject: [PATCH 229/244] restore elegance of cache algorithm --- .../data_systems/lazy_load/lazy_load_system.cpp | 13 +++++++++++++ .../data_systems/lazy_load/lazy_load_system.hpp | 17 ++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp index 0af904c51..4235b9676 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.cpp @@ -180,4 +180,17 @@ std::chrono::time_point LazyLoad::ExpiryTime() fresh_duration_); } +std::string LazyLoad::CacheTraceMsg( + data_components::ExpirationTracker::TrackState const state) { + switch (state) { + case data_components::ExpirationTracker::TrackState::kStale: + return "cache hit (stale)"; + case data_components::ExpirationTracker::TrackState::kNotTracked: + return "cache miss"; + case data_components::ExpirationTracker::TrackState::kFresh: + return "cache hit"; + } + detail::unreachable(); +} + } // namespace launchdarkly::server_side::data_systems diff --git a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp index 3efa0b407..bc8c5c2d3 100644 --- a/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp +++ b/libs/server-sdk/src/data_systems/lazy_load/lazy_load_system.hpp @@ -71,26 +71,25 @@ class LazyLoad final : public data_interfaces::IDataSystem { void RefreshFlag(std::string const& key) const; void RefreshSegment(std::string const& key) const; + static std::string CacheTraceMsg( + data_components::ExpirationTracker::TrackState state); + template TResult Get(std::string const& key, data_components::ExpirationTracker::TrackState const state, std::function const& refresh, std::function const& get) const { + LD_LOG(logger_, LogLevel::kDebug) + << Identity() << ": get " << key << " - " << CacheTraceMsg(state); + switch (state) { case data_components::ExpirationTracker::TrackState::kStale: - LD_LOG(logger_, LogLevel::kDebug) - << Identity() << ": " << key << " is stale; refreshing"; - refresh(); - return get(); + [[fallthrough]]; case data_components::ExpirationTracker::TrackState::kNotTracked: - LD_LOG(logger_, LogLevel::kDebug) - << Identity() << ": " << key << " not cached; refreshing"; refresh(); - return get(); + [[fallthrough]]; case data_components::ExpirationTracker::TrackState::kFresh: - LD_LOG(logger_, LogLevel::kDebug) - << Identity() << ": " << key << " served from cache"; return get(); } detail::unreachable(); From b1614cad69040af48ef67ce40527579b7219ac0b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 17:21:54 -0800 Subject: [PATCH 230/244] more unit tests --- .../tests/redis_source_test.cpp | 172 +++++++++++++++++- 1 file changed, 166 insertions(+), 6 deletions(-) diff --git a/libs/server-sdk-redis-source/tests/redis_source_test.cpp b/libs/server-sdk-redis-source/tests/redis_source_test.cpp index 1d2ce850c..10b8dcd60 100644 --- a/libs/server-sdk-redis-source/tests/redis_source_test.cpp +++ b/libs/server-sdk-redis-source/tests/redis_source_test.cpp @@ -129,12 +129,21 @@ class RedisTests : public ::testing::Test { client.Clear(); } - void WithPrefix(std::string const& prefix, - std::function const& f) { + void WithPrefixedClient( + std::string const& prefix, + std::function const& f) { auto const client = PrefixedClient(client_, prefix); f(client); } + void WithPrefixedSource( + std::string const& prefix, + std::function const& f) const { + auto maybe_source = RedisDataSource::Create(uri_, prefix); + ASSERT_TRUE(maybe_source); + f((*maybe_source->get())); + } + protected: std::shared_ptr source; @@ -245,19 +254,170 @@ TEST_F(RedisTests, GetSegmentDoesNotFindFlag) { ASSERT_FALSE(*result); } -TEST_F(RedisTests, ChecksInitializedPrefixIndependence) { - WithPrefix("not_our_prefix", [&](auto const& client) { +TEST_F(RedisTests, GetAllSegmentsWhenEmpty) { + auto const result = source->All(SegmentKind{}); + ASSERT_TRUE(result); + ASSERT_TRUE(result->empty()); +} + +TEST_F(RedisTests, GetAllFlagsWhenEmpty) { + auto const result = source->All(FlagKind{}); + ASSERT_TRUE(result); + ASSERT_TRUE(result->empty()); +} + +TEST_F(RedisTests, GetAllFlags) { + Flag const flag1{"foo", 1, true}; + Flag const flag2{"bar", 2, false}; + + PutFlag(flag1); + PutFlag(flag2); + PutDeletedFlag("baz", "baz_tombstone"); + + auto const result = source->All(FlagKind{}); + ASSERT_TRUE(result); + ASSERT_EQ(result->size(), 3); + + auto const& flags = *result; + auto const flag1_it = flags.find("foo"); + ASSERT_NE(flag1_it, flags.end()); + ASSERT_EQ(flag1_it->second.serializedItem, + serialize(boost::json::value_from(flag1))); + + auto const flag2_it = flags.find("bar"); + ASSERT_NE(flag2_it, flags.end()); + ASSERT_EQ(flag2_it->second.serializedItem, + serialize(boost::json::value_from(flag2))); + + auto const flag3_it = flags.find("baz"); + ASSERT_NE(flag3_it, flags.end()); + ASSERT_EQ(flag3_it->second.serializedItem, "baz_tombstone"); +} + +TEST_F(RedisTests, InitializedPrefixIndependence) { + WithPrefixedClient("not_our_prefix", [&](auto const& client) { client.Init(); ASSERT_FALSE(source->Initialized()); }); - WithPrefix("TestPrefix", [&](auto const& client) { + WithPrefixedClient("TestPrefix", [&](auto const& client) { client.Init(); ASSERT_FALSE(source->Initialized()); }); - WithPrefix("stillnotprefix", [&](auto const& client) { + WithPrefixedClient("stillnotprefix", [&](auto const& client) { client.Init(); ASSERT_FALSE(source->Initialized()); }); } + +TEST_F(RedisTests, SegmentPrefixIndependence) { + auto MakeSegment = [](std::uint64_t const version) { + return Segment{"foo", version}; + }; + + auto PrefixName = [](std::uint64_t const version) { + return "prefix" + std::to_string(version); + }; + + auto ValidateSegment = [&](ISerializedDataReader::GetResult const& result, + std::size_t i) { + ASSERT_TRUE(result); + if (auto const f = *result) { + ASSERT_EQ(f->serializedItem, + serialize(boost::json::value_from(MakeSegment(i)))); + } else { + FAIL() << "expected segment to be found under " << PrefixName(i); + } + }; + + constexpr std::size_t kPrefixCount = 10; + + // Setup the same segment key (with different versions) under kPrefixCount + // prefixes. This will allow us to verify that the prefixed clients only + // "see" the single segment and not the ones living under different + // prefixes. + + for (std::size_t i = 0; i < kPrefixCount; i++) { + WithPrefixedClient(PrefixName(i), [&](auto const& client) { + client.PutSegment(MakeSegment(i)); + }); + } + + for (std::size_t i = 0; i < kPrefixCount; i++) { + WithPrefixedSource(PrefixName(i), [&](auto const& source) { + // Checks that the string that was stored for segment #i is the + // same one that was retrieved. + ValidateSegment(source.Get(SegmentKind{}, "foo"), i); + + // Sanity check that the other segments are not visible. + auto all = source.All(SegmentKind{}); + ASSERT_TRUE(all); + ASSERT_EQ(all->size(), 1); + }); + } +} + +TEST_F(RedisTests, FlagPrefixIndependence) { + auto MakeFlag = [](std::uint64_t const version) { + return Flag{"foo", version, true}; + }; + + auto PrefixName = [](std::uint64_t const version) { + return "prefix" + std::to_string(version); + }; + + auto ValidateFlag = [&](ISerializedDataReader::GetResult const& result, + std::size_t i) { + ASSERT_TRUE(result); + if (auto const f = *result) { + ASSERT_EQ(f->serializedItem, + serialize(boost::json::value_from(MakeFlag(i)))); + } else { + FAIL() << "expected flag to be found under " << PrefixName(i); + } + }; + + constexpr std::size_t kPrefixCount = 10; + + // Setup the same flag key (with different versions) under kPrefixCount + // prefixes. This will allow us to verify that the prefixed clients only + // "see" the single flag and not the ones living under different prefixes. + + for (std::size_t i = 0; i < kPrefixCount; i++) { + WithPrefixedClient(PrefixName(i), [&](auto const& client) { + client.PutFlag(MakeFlag(i)); + }); + } + + for (std::size_t i = 0; i < kPrefixCount; i++) { + WithPrefixedSource(PrefixName(i), [&](auto const& source) { + // Checks that the string that was stored for flag #i is the + // same one that was retrieved. + ValidateFlag(source.Get(FlagKind{}, "foo"), i); + + // Sanity check that the other flags are not visible. + auto all = source.All(FlagKind{}); + ASSERT_TRUE(all); + ASSERT_EQ(all->size(), 1); + }); + } +} + +TEST_F(RedisTests, FlagAndSegmentCanCoexistWithSameKey) { + Flag const flag_in{"foo", 1, true}; + Segment const segment_in{"foo", 1}; + + PutFlag(flag_in); + PutSegment(segment_in); + + auto flag = source->Get(FlagKind{}, "foo"); + ASSERT_TRUE(flag); + ASSERT_EQ((*flag)->serializedItem, + serialize(boost::json::value_from(flag_in))); + + auto segment = source->Get(SegmentKind{}, "foo"); + ASSERT_TRUE(segment); + ASSERT_EQ((*segment)->serializedItem, + serialize(boost::json::value_from(segment_in))); +} From aa8ddc012b5c805dd80256b4307dd43793a07456 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 17:23:31 -0800 Subject: [PATCH 231/244] remove useless Clear method in tests --- .../tests/redis_source_test.cpp | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/libs/server-sdk-redis-source/tests/redis_source_test.cpp b/libs/server-sdk-redis-source/tests/redis_source_test.cpp index 10b8dcd60..d87ca31a0 100644 --- a/libs/server-sdk-redis-source/tests/redis_source_test.cpp +++ b/libs/server-sdk-redis-source/tests/redis_source_test.cpp @@ -61,18 +61,6 @@ class PrefixedClient { } } - void Clear() const { - try { - std::vector> output; - client_.keys(Prefixed("*"), std::back_inserter(output)); - for (auto const& [key, _] : output) { - client_.del(key); - } - } catch (sw::redis::Error const& e) { - FAIL() << e.what(); - } - } - private: std::string Prefixed(std::string const& name) const { return prefix_ + ":" + name; @@ -124,11 +112,6 @@ class RedisTests : public ::testing::Test { client.PutSegment(segment); } - void Clear() { - auto const client = PrefixedClient(client_, prefix_); - client.Clear(); - } - void WithPrefixedClient( std::string const& prefix, std::function const& f) { From 6efb432ecb94b95a9d3f48184beab9de53933ca0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 17:43:22 -0800 Subject: [PATCH 232/244] yet even more unit tests --- .../tests/redis_source_test.cpp | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/libs/server-sdk-redis-source/tests/redis_source_test.cpp b/libs/server-sdk-redis-source/tests/redis_source_test.cpp index d87ca31a0..b54c87aa7 100644 --- a/libs/server-sdk-redis-source/tests/redis_source_test.cpp +++ b/libs/server-sdk-redis-source/tests/redis_source_test.cpp @@ -404,3 +404,53 @@ TEST_F(RedisTests, FlagAndSegmentCanCoexistWithSameKey) { ASSERT_EQ((*segment)->serializedItem, serialize(boost::json::value_from(segment_in))); } + +TEST(RedisErrorTests, InvalidURIs) { + std::vector const uris = {"nope, not a redis URI", + "http://foo", + "foo.com" + ""}; + + for (auto const& uri : uris) { + auto const source = RedisDataSource::Create(uri, "prefix"); + ASSERT_FALSE(source); + } +} + +TEST(RedisErrorTests, ValidURIs) { + std::vector const uris = { + "tcp://127.0.0.1:6666", + "tcp://127.0.0.1", + "tcp://pass@127.0.0.1", + "tcp://127.0.0.1:6379/2", + "tcp://127.0.0.1:6379/2?keep_alive=true", + "tcp://127.0.0.1?socket_timeout=50ms&connect_timeout=1s", + "unix://path/to/socket"}; + for (auto const& uri : uris) { + auto const source = RedisDataSource::Create(uri, "prefix"); + ASSERT_TRUE(source); + } +} + +TEST(RedisErrorTests, GetReturnsErrorAndNoExceptionThrown) { + auto const maybe_source = RedisDataSource::Create( + "tcp://foobar:1000" /* no redis service here */, "prefix"); + ASSERT_TRUE(maybe_source); + + auto const source = *maybe_source; + + auto const get_initialized = source->Initialized(); + ASSERT_FALSE(get_initialized); + + auto const get_flag = source->Get(FlagKind{}, "foo"); + ASSERT_FALSE(get_flag); + + auto const get_segment = source->Get(SegmentKind{}, "foo"); + ASSERT_FALSE(get_segment); + + auto const get_all_flag = source->All(FlagKind{}); + ASSERT_FALSE(get_all_flag); + + auto const get_all_segment = source->All(SegmentKind{}); + ASSERT_FALSE(get_all_segment); +} From 0fee65335f3b5f814b1320ae0061a857ae3b7dc2 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 17:47:30 -0800 Subject: [PATCH 233/244] try reverting changes to suppress gtest build errors --- CMakeLists.txt | 3 --- libs/client-sdk/tests/CMakeLists.txt | 2 -- libs/common/tests/CMakeLists.txt | 2 -- libs/internal/tests/CMakeLists.txt | 2 -- libs/server-sdk-redis-source/tests/CMakeLists.txt | 3 --- libs/server-sdk/tests/CMakeLists.txt | 3 --- 6 files changed, 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 48dfd0f40..c40e33cc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -102,9 +102,6 @@ if (LD_BUILD_UNIT_TESTS) FetchContent_MakeAvailable(googletest) enable_testing() - set_target_properties(gtest PROPERTIES COMPILE_WARNING_AS_ERROR OFF) - - endif () if (LD_DYNAMIC_LINK_OPENSSL) diff --git a/libs/client-sdk/tests/CMakeLists.txt b/libs/client-sdk/tests/CMakeLists.txt index bccdb6a00..5f53df407 100644 --- a/libs/client-sdk/tests/CMakeLists.txt +++ b/libs/client-sdk/tests/CMakeLists.txt @@ -17,7 +17,5 @@ endif () add_executable(gtest_${LIBNAME} ${tests}) target_link_libraries(gtest_${LIBNAME} launchdarkly::client launchdarkly::internal GTest::gtest_main) -# There's an uninitialized variable warning in gtest that hasn't been resolved. -set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/common/tests/CMakeLists.txt b/libs/common/tests/CMakeLists.txt index 53acb0473..88e24a280 100644 --- a/libs/common/tests/CMakeLists.txt +++ b/libs/common/tests/CMakeLists.txt @@ -19,7 +19,5 @@ endif () add_executable(gtest_${LIBNAME} ${tests}) target_link_libraries(gtest_${LIBNAME} launchdarkly::common launchdarkly::internal foxy GTest::gtest_main) -# There's an uninitialized variable warning in gtest that hasn't been resolved. -set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/internal/tests/CMakeLists.txt b/libs/internal/tests/CMakeLists.txt index 428ea19db..69c9ffe3c 100644 --- a/libs/internal/tests/CMakeLists.txt +++ b/libs/internal/tests/CMakeLists.txt @@ -17,7 +17,5 @@ endif () add_executable(gtest_${LIBNAME} ${tests}) target_link_libraries(gtest_${LIBNAME} launchdarkly::common launchdarkly::internal foxy GTest::gtest_main) -# There's an uninitialized variable warning in gtest that hasn't been resolved. -set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk-redis-source/tests/CMakeLists.txt b/libs/server-sdk-redis-source/tests/CMakeLists.txt index 8e35437cf..28c3f1fb3 100644 --- a/libs/server-sdk-redis-source/tests/CMakeLists.txt +++ b/libs/server-sdk-redis-source/tests/CMakeLists.txt @@ -31,7 +31,4 @@ set(LIBS target_link_libraries(gtest_${LIBNAME} PRIVATE ${LIBS}) -# There's an uninitialized variable warning in gtest that hasn't been resolved. -set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) - gtest_discover_tests(gtest_${LIBNAME}) diff --git a/libs/server-sdk/tests/CMakeLists.txt b/libs/server-sdk/tests/CMakeLists.txt index d2cf6fc78..837735a10 100644 --- a/libs/server-sdk/tests/CMakeLists.txt +++ b/libs/server-sdk/tests/CMakeLists.txt @@ -19,7 +19,4 @@ add_executable(gtest_${LIBNAME} ) target_link_libraries(gtest_${LIBNAME} launchdarkly::server launchdarkly::internal launchdarkly::sse timestamp GTest::gtest_main GTest::gmock) -# There's an uninitialized variable warning in gtest that hasn't been resolved. -set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) - gtest_discover_tests(gtest_${LIBNAME}) From f40df7bb8481ba4648b123e2c91196184d0ebe6b Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 17:53:05 -0800 Subject: [PATCH 234/244] add Doxyfile --- libs/server-sdk-redis-source/Doxyfile | 94 +++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 libs/server-sdk-redis-source/Doxyfile diff --git a/libs/server-sdk-redis-source/Doxyfile b/libs/server-sdk-redis-source/Doxyfile new file mode 100644 index 000000000..abe5e5df6 --- /dev/null +++ b/libs/server-sdk-redis-source/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 Redis Source" + +# 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 = "Provide SDK data via Redis" + +# 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 From c5e63a1318bcdd3fb4e011cc2ea2549930dc723c Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 18:09:01 -0800 Subject: [PATCH 235/244] try supressing gtest error in only the redis project --- libs/server-sdk-redis-source/tests/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/server-sdk-redis-source/tests/CMakeLists.txt b/libs/server-sdk-redis-source/tests/CMakeLists.txt index 28c3f1fb3..3354e31bf 100644 --- a/libs/server-sdk-redis-source/tests/CMakeLists.txt +++ b/libs/server-sdk-redis-source/tests/CMakeLists.txt @@ -18,6 +18,10 @@ add_executable(gtest_${LIBNAME} ${tests} ) +# Suppress gtest warning about uninitialized variable. +set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) + + set(LIBS launchdarkly::server_redis_source # Needed so we can access the flag/segment data models so we can serialize them for putting in redis. From 256268451d9b9675d3794821b6041e971233df08 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 18:59:12 -0800 Subject: [PATCH 236/244] add github templates --- .../server-sdk-redis-source--bug-report.md | 50 +++++++++++++++++++ ...erver-sdk-redis-source--feature-request.md | 20 ++++++++ 2 files changed, 70 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/server-sdk-redis-source--bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/server-sdk-redis-source--feature-request.md diff --git a/.github/ISSUE_TEMPLATE/server-sdk-redis-source--bug-report.md b/.github/ISSUE_TEMPLATE/server-sdk-redis-source--bug-report.md new file mode 100644 index 000000000..9306f473d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/server-sdk-redis-source--bug-report.md @@ -0,0 +1,50 @@ +--- +name: 'C++ Server SDK / Redis Source Bug Report' +about: Create a report to help us improve +title: '' +labels: 'package: sdk/server-redis, bug' +assignees: '' + +--- + +**Is this a support request?** +This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the code in this +library. If you're not sure whether the problem you are having is specifically related to this library, or to the +LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to +investigate the problem and will consult the SDK team if necessary. You can submit a support request by +going [here](https://support.launchdarkly.com/) and clicking "submit a request", or by emailing +support@launchdarkly.com. + +Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on +your issues. If your problem is specific to your account, you should submit a support request as described above. + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Logs** +If applicable, add any log output related to your problem. +To get more logs from the SDK, change the log level using environment variable `LD_LOG_LEVEL`. For example: + + ``` + LD_LOG_LEVEL=debug ./your-application + ``` + +**SDK version** +The version of this SDK that you are using. + +**Language version, developer tools** +For instance, C++17 or C11. If you are using a language that requires a separate compiler, such as C, please include the +name and version of the compiler too. + +**OS/platform** +For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the +browser type and version. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/server-sdk-redis-source--feature-request.md b/.github/ISSUE_TEMPLATE/server-sdk-redis-source--feature-request.md new file mode 100644 index 000000000..ec64792b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/server-sdk-redis-source--feature-request.md @@ -0,0 +1,20 @@ +--- +name: 'C++ Server SDK / Redis Source Feature Request' +about: Create a report to help us improve +title: '' +labels: 'package: sdk/server-redis, feature' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. From 9181314ce9b273a61f5f93438f6ccc90f703a53e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 18:59:21 -0800 Subject: [PATCH 237/244] add README --- libs/server-sdk-redis-source/README.md | 125 +++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 libs/server-sdk-redis-source/README.md diff --git a/libs/server-sdk-redis-source/README.md b/libs/server-sdk-redis-source/README.md new file mode 100644 index 000000000..1d9d7b4fa --- /dev/null +++ b/libs/server-sdk-redis-source/README.md @@ -0,0 +1,125 @@ +LaunchDarkly Server-Side SDK - Redis Source for C/C++ +=================================== + +### ⚠️ This repository contains alpha software and should not be considered ready for production use while this message is visible. + +### Breaking changes may occur. + +[![Actions Status](https://github.com/launchdarkly/cpp-sdks/actions/workflows/server-redis.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-redis-source/docs/html/) + +The LaunchDarkly Server-Side SDK Redis Source for C/C++ is designed for use with the Server-Side SDK. + +This component allows the Server-Side SDK to retrieve feature flag configurations from Redis, rather than +from LaunchDarkly. + +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 component 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-redis&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 Redis Source + +The component 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. + +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. + +The shared library (so, DLL, dylib), only supports the C interface. + +The examples here are to help with getting started, but generally speaking this component should be incorporated using +your +build system (CMake for instance). + +### CMake Usage + +When configuring the SDK via CMake, you need to explicitly enable this component (example): + +``` +cmake -GNinja -D LD_BUILD_REDIS_SUPPORT=ON .. +``` + +It is disabled by default to avoid pulling in the `redis++` and `hiredis` dependencies that this component is +implemented with. + +This will expose the `launchdarkly::server_redis_source` target. + +Next, link the target to your executable or library: + +```cmake +target_link_libraries(my-target PRIVATE launchdarkly::server_redis_source) +``` + +This will cause `launchdarkly::server` to be linked as well. + +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 4f9e664c0f93495ad7f4cf46f28feb51b397153a Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 18:59:50 -0800 Subject: [PATCH 238/244] add json.hpp to json_destination.cpp --- .../data_components/serialization_adapters/json_destination.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp index 115dd621b..b85f2f978 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_destination.cpp @@ -3,6 +3,8 @@ #include #include +#include + namespace launchdarkly::server_side::data_components { using data_interfaces::ISerializedDestination; From c4c7c326dbcd9959d505285bf8fb06d77e8c0a10 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 1 Dec 2023 19:09:48 -0800 Subject: [PATCH 239/244] take 2 on suppressing gtest errors --- libs/server-sdk-redis-source/tests/CMakeLists.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/server-sdk-redis-source/tests/CMakeLists.txt b/libs/server-sdk-redis-source/tests/CMakeLists.txt index 3354e31bf..eddbc3c16 100644 --- a/libs/server-sdk-redis-source/tests/CMakeLists.txt +++ b/libs/server-sdk-redis-source/tests/CMakeLists.txt @@ -19,8 +19,7 @@ add_executable(gtest_${LIBNAME} ) # Suppress gtest warning about uninitialized variable. -set_target_properties(gtest_${LIBNAME} PROPERTIES COMPILE_WARNING_AS_ERROR OFF) - +set_target_properties(gtest PROPERTIES COMPILE_WARNING_AS_ERROR OFF) set(LIBS launchdarkly::server_redis_source From 0396c8383a80c3547132e9620bb3d3a884583022 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 4 Dec 2023 10:49:23 -0800 Subject: [PATCH 240/244] Update examples/hello-cpp-server-redis/main.cpp Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> --- examples/hello-cpp-server-redis/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/hello-cpp-server-redis/main.cpp b/examples/hello-cpp-server-redis/main.cpp index aae677f55..f8cba476e 100644 --- a/examples/hello-cpp-server-redis/main.cpp +++ b/examples/hello-cpp-server-redis/main.cpp @@ -26,7 +26,7 @@ using namespace launchdarkly::server_side; int main() { char const* sdk_key = get_with_env_fallback( SDK_KEY, "LD_SDK_KEY", - "Please edit main.c to set SDK_KEY to your LaunchDarkly server-side " + "Please edit main.cpp to set SDK_KEY to your LaunchDarkly server-side " "SDK key " "first.\n\nAlternatively, set the LD_SDK_KEY environment " "variable.\n" From f8db865b137afcfc5a603bcd156446b2fbe1dc3e Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 4 Dec 2023 10:48:08 -0800 Subject: [PATCH 241/244] chore: fix broken newline in readme --- libs/server-sdk-redis-source/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libs/server-sdk-redis-source/README.md b/libs/server-sdk-redis-source/README.md index 1d9d7b4fa..9c70c6bfe 100644 --- a/libs/server-sdk-redis-source/README.md +++ b/libs/server-sdk-redis-source/README.md @@ -39,8 +39,7 @@ installing and using the SDK. ### Incorporating the Redis Source The component 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. +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 From b561989aad0a7bc929b0bafded44578fe0c86569 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 4 Dec 2023 10:48:40 -0800 Subject: [PATCH 242/244] polish redis_source.hpp --- .../server_side/integrations/redis/redis_source.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/server-sdk-redis-source/include/launchdarkly/server_side/integrations/redis/redis_source.hpp b/libs/server-sdk-redis-source/include/launchdarkly/server_side/integrations/redis/redis_source.hpp index 12cb1566a..661f4ebfd 100644 --- a/libs/server-sdk-redis-source/include/launchdarkly/server_side/integrations/redis/redis_source.hpp +++ b/libs/server-sdk-redis-source/include/launchdarkly/server_side/integrations/redis/redis_source.hpp @@ -32,9 +32,10 @@ class RedisDataSource final : public ISerializedDataReader { RedisDataSource(std::unique_ptr redis, std::string prefix); + std::string key_for_kind(ISerializedItemKind const& kind) const; + std::string const prefix_; std::string const inited_key_; - std::string key_for_kind(ISerializedItemKind const& kind) const; std::unique_ptr redis_; }; From 2c6f35a16c8c2023cb16d1ab86d5b7addddddde0 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 4 Dec 2023 10:52:37 -0800 Subject: [PATCH 243/244] update example --- examples/hello-cpp-server-redis/main.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/hello-cpp-server-redis/main.cpp b/examples/hello-cpp-server-redis/main.cpp index f8cba476e..f76fc3808 100644 --- a/examples/hello-cpp-server-redis/main.cpp +++ b/examples/hello-cpp-server-redis/main.cpp @@ -17,6 +17,13 @@ // the client to become initialized. #define INIT_TIMEOUT_MILLISECONDS 3000 +// Set REDIS_URI to your own Redis instance's URI. +#define REDIS_URI "redis://localhost:6379" + +// Set REDIS_PREFIX to the prefix containing the launchdarkly +// environment data in Redis. +#define REDIS_PREFIX "launchdarkly" + char const* get_with_env_fallback(char const* source_val, char const* env_variable, char const* error_msg); @@ -36,8 +43,7 @@ int main() { using LazyLoad = server_side::config::builders::LazyLoadBuilder; - auto redis = integrations::RedisDataSource::Create("redis://localhost:6379", - "launchdarkly"); + auto redis = integrations::RedisDataSource::Create(REDIS_URI, REDIS_PREFIX); if (!redis) { std::cout << "error: redis config is invalid: " << redis.error() From 44a72fa050a7d03021eb40b3ee25fafa296d1b61 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Mon, 4 Dec 2023 10:54:58 -0800 Subject: [PATCH 244/244] add todo for tombstone deserializer --- .../serialization_adapters/json_deserializer.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp index d69192f5a..906eac51c 100644 --- a/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp +++ b/libs/server-sdk/src/data_components/serialization_adapters/json_deserializer.hpp @@ -26,8 +26,8 @@ IntoStorageItem(integrations::SerializedItemDescriptor const& descriptor) { json_val); if (!result) { - /* maybe it's a tombstone */ - + /* maybe it's a tombstone - check */ + /* TODO(225976): replace with boost::json deserializer */ if (json_val.is_object()) { auto const& obj = json_val.as_object(); if (auto deleted_it = obj.find("deleted");