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()); +}