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..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,9 +84,71 @@ class PersistenceBuilder { 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 built::Persistence(); + return persistence_; } + + private: + built::Persistence persistence_; }; } // namespace launchdarkly::config::shared::builders diff --git a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp index 326d7b53f..35a90e918 100644 --- a/libs/common/include/launchdarkly/config/shared/built/persistence.hpp +++ b/libs/common/include/launchdarkly/config/shared/built/persistence.hpp @@ -1,9 +1,12 @@ #pragma once +#include #include +#include #include #include +#include namespace launchdarkly::config::shared::built { @@ -18,6 +21,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/common/include/launchdarkly/config/shared/defaults.hpp b/libs/common/include/launchdarkly/config/shared/defaults.hpp index bd62ae2c4..b7d958236 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,11 @@ struct Defaults { return {std::chrono::seconds{30}, "/sdk/latest-all", std::chrono::seconds{30}}; } + + 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/common/include/launchdarkly/persistence/persistent_store_core.hpp b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp new file mode 100644 index 000000000..515db0b69 --- /dev/null +++ b/libs/common/include/launchdarkly/persistence/persistent_store_core.hpp @@ -0,0 +1,216 @@ +#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/CMakeLists.txt b/libs/server-sdk/CMakeLists.txt index d1e7f9376..312f24998 100644 --- a/libs/server-sdk/CMakeLists.txt +++ b/libs/server-sdk/CMakeLists.txt @@ -24,7 +24,7 @@ endif () #set(CMAKE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/cmake") #set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_FILES}) -# Needed to fetch external dependencies. +# Needed to fetch external dependencies. include(FetchContent) # Needed to parse RFC3339 dates in flag rules. 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..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,3 +1,211 @@ #include "persistent_data_store.hpp" +#include +#include -namespace launchdarkly::server_side::data_store::persistent {} +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 index 523e09889..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,7 +5,8 @@ #include "../memory_store.hpp" #include "expiration_tracker.hpp" -#include +#include +#include #include #include @@ -14,9 +15,43 @@ 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 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 +78,77 @@ 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; + + 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 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); +}