From 17bf41c1ae0d73ceef4794b98caafa283131afe2 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Tue, 6 May 2025 11:26:04 +0300 Subject: [PATCH 01/15] feature: in-memory storage Signed-off-by: Dmitriy Khaustov aka xDimon --- src/CMakeLists.txt | 3 + src/injector/CMakeLists.txt | 1 + src/injector/node_injector.cpp | 9 +- src/log/formatters/variant.hpp | 17 +--- src/log/profiling_logger.cpp | 4 +- src/log/profiling_logger.hpp | 24 +++--- src/log/trace_macros.hpp | 14 +-- src/metrics/handler.hpp | 2 +- src/metrics/registry.hpp | 2 +- src/storage/CMakeLists.txt | 15 ++++ src/storage/buffer_map_types.hpp | 71 +++++++++++++++ src/storage/face/batch_writeable.hpp | 47 ++++++++++ src/storage/face/generic_maps.hpp | 48 +++++++++++ src/storage/face/iterable.hpp | 33 +++++++ src/storage/face/map_cursor.hpp | 96 +++++++++++++++++++++ src/storage/face/owned_or_view.hpp | 39 +++++++++ src/storage/face/readable.hpp | 48 +++++++++++ src/storage/face/view.hpp | 15 ++++ src/storage/face/write_batch.hpp | 52 +++++++++++ src/storage/face/writeable.hpp | 57 ++++++++++++ src/storage/in_memory/cursor.hpp | 74 ++++++++++++++++ src/storage/in_memory/in_memory_batch.hpp | 47 ++++++++++ src/storage/in_memory/in_memory_storage.cpp | 72 ++++++++++++++++ src/storage/in_memory/in_memory_storage.hpp | 54 ++++++++++++ src/storage/storage_error.cpp | 33 +++++++ src/storage/storage_error.hpp | 48 +++++++++++ 26 files changed, 884 insertions(+), 41 deletions(-) create mode 100644 src/storage/CMakeLists.txt create mode 100644 src/storage/buffer_map_types.hpp create mode 100644 src/storage/face/batch_writeable.hpp create mode 100644 src/storage/face/generic_maps.hpp create mode 100644 src/storage/face/iterable.hpp create mode 100644 src/storage/face/map_cursor.hpp create mode 100644 src/storage/face/owned_or_view.hpp create mode 100644 src/storage/face/readable.hpp create mode 100644 src/storage/face/view.hpp create mode 100644 src/storage/face/write_batch.hpp create mode 100644 src/storage/face/writeable.hpp create mode 100644 src/storage/in_memory/cursor.hpp create mode 100644 src/storage/in_memory/in_memory_batch.hpp create mode 100644 src/storage/in_memory/in_memory_storage.cpp create mode 100644 src/storage/in_memory/in_memory_storage.hpp create mode 100644 src/storage/storage_error.cpp create mode 100644 src/storage/storage_error.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 45ee63fe..5e31f158 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -31,3 +31,6 @@ add_subdirectory(se) # Modules subsystem add_subdirectory(modules) + +# Storage +add_subdirectory(storage) diff --git a/src/injector/CMakeLists.txt b/src/injector/CMakeLists.txt index 3b6979b1..480c2449 100644 --- a/src/injector/CMakeLists.txt +++ b/src/injector/CMakeLists.txt @@ -17,4 +17,5 @@ target_link_libraries(node_injector clock se_async modules + storage ) diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index 213b2470..b84b8fd6 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -33,6 +33,7 @@ #include "modules/module.hpp" #include "se/impl/async_dispatcher_impl.hpp" #include "se/subscription.hpp" +#include "storage/in_memory/in_memory_storage.hpp" namespace { namespace di = boost::di; @@ -65,12 +66,12 @@ namespace { di::bind.to>(), di::bind.to([](const auto &injector) { return metrics::Exposer::Configuration{ - {boost::asio::ip::address_v4::from_string("127.0.0.1"), 7777} - // injector - // .template create() - // .openmetricsHttpEndpoint() + injector + .template create() + .openmetricsHttpEndpoint() }; }), + di::bind.to(), // user-defined overrides... std::forward(args)...); diff --git a/src/log/formatters/variant.hpp b/src/log/formatters/variant.hpp index afeefaf3..747b952f 100644 --- a/src/log/formatters/variant.hpp +++ b/src/log/formatters/variant.hpp @@ -7,33 +7,22 @@ #pragma once #include + #include #include "common/visitor.hpp" template struct fmt::formatter> { - // Parses format specifications of the form ['s' | 'l']. constexpr auto parse(format_parse_context &ctx) -> decltype(ctx.begin()) { - // Parse the presentation format and store it in the formatter: - auto it = ctx.begin(), end = ctx.end(); - - // Check if reached the end of the range: - if (it != end && *it != '}') { - throw format_error("invalid format"); - } - - // Return an iterator past the end of the parsed range: - return it; + return ctx.begin(); } - // Formats the BlockInfo using the parsed format specification (presentation) - // stored in this formatter. template auto format(const boost::variant &variant, FormatContext &ctx) const -> decltype(ctx.out()) { // ctx.out() is an output iterator to write to. - return kagome::visit_in_place(variant, [&](const auto &value) { + return jam::visit_in_place(variant, [&](const auto &value) { return fmt::format_to(ctx.out(), "{}", value); }); } diff --git a/src/log/profiling_logger.cpp b/src/log/profiling_logger.cpp index 5ebacd00..1dfb8930 100644 --- a/src/log/profiling_logger.cpp +++ b/src/log/profiling_logger.cpp @@ -6,7 +6,7 @@ #include "log/profiling_logger.hpp" -namespace kagome::log { +namespace jam::log { // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) Logger profiling_logger = nullptr; -} // namespace kagome::log +} // namespace jam::log diff --git a/src/log/profiling_logger.hpp b/src/log/profiling_logger.hpp index c16d6273..59da0df6 100644 --- a/src/log/profiling_logger.hpp +++ b/src/log/profiling_logger.hpp @@ -10,12 +10,12 @@ #include "clock/impl/clock_impl.hpp" -namespace kagome::log { +namespace jam::log { extern Logger profiling_logger; // NOLINT struct ProfileScope { - using Clock = ::kagome::clock::SteadyClockImpl; + using Clock = ::jam::clock::SteadyClockImpl; explicit ProfileScope(std::string_view scope, log::Logger logger = profiling_logger) @@ -52,22 +52,22 @@ namespace kagome::log { Clock::TimePoint start; log::Logger logger; }; -} // namespace kagome::log +} // namespace jam::log -#ifdef KAGOME_PROFILING +#ifdef JAM_PROFILING -#define KAGOME_PROFILE_START_L(logger, scope) \ - auto _profiling_scope_##scope = ::kagome::log::ProfileScope{#scope, logger}; +#define JAM_PROFILE_START_L(logger, scope) \ + auto _profiling_scope_##scope = ::jam::log::ProfileScope{#scope, logger}; -#define KAGOME_PROFILE_START(scope) \ - KAGOME_PROFILE_START_L(::kagome::log::profiling_logger, scope) -#define KAGOME_PROFILE_END(scope) _profiling_scope_##scope.end(); +#define JAM_PROFILE_START(scope) \ + JAM_PROFILE_START_L(::jam::log::profiling_logger, scope) +#define JAM_PROFILE_END(scope) _profiling_scope_##scope.end(); #else -#define KAGOME_PROFILE_START(scope) -#define KAGOME_PROFILE_END(scope) +#define JAM_PROFILE_START(scope) +#define JAM_PROFILE_END(scope) -#define KAGOME_PROFILE_START_L(logger, scope) +#define JAM_PROFILE_START_L(logger, scope) #endif diff --git a/src/log/trace_macros.hpp b/src/log/trace_macros.hpp index 6ffc9474..74842b27 100644 --- a/src/log/trace_macros.hpp +++ b/src/log/trace_macros.hpp @@ -8,7 +8,7 @@ #include "log/logger.hpp" -namespace kagome::log { +namespace jam::log { struct TraceReturnVoid {}; template @@ -32,19 +32,19 @@ namespace kagome::log { #define _SL_TRACE_FUNC_CALL(logger, ret, ...) \ SL_TRACE(logger, \ "{}", \ - (::kagome::log::TraceFuncCall{ \ + (::jam::log::TraceFuncCall{ \ this, __FUNCTION__, ret, std::forward_as_tuple(__VA_ARGS__)})) #endif -} // namespace kagome::log +} // namespace jam::log template -struct fmt::formatter> { +struct fmt::formatter> { static constexpr auto parse(format_parse_context &ctx) { return ctx.begin(); } - static auto format(const kagome::log::TraceFuncCall &v, + static auto format(const jam::log::TraceFuncCall &v, format_context &ctx) { auto out = ctx.out(); out = fmt::format_to(out, "call '{}' from {}", v.func_name, v.caller); @@ -61,7 +61,7 @@ struct fmt::formatter> { }; std::apply([&](auto &...arg) { (f(arg), ...); }, v.args); } - if constexpr (not std::is_same_v) { + if constexpr (not std::is_same_v) { out = fmt::format_to(out, " -> ret: {}", v.ret); } return out; @@ -72,4 +72,4 @@ struct fmt::formatter> { _SL_TRACE_FUNC_CALL(logger, ret, ##__VA_ARGS__) #define SL_TRACE_VOID_FUNC_CALL(logger, ...) \ - _SL_TRACE_FUNC_CALL(logger, ::kagome::log::TraceReturnVoid{}, ##__VA_ARGS__) + _SL_TRACE_FUNC_CALL(logger, ::jam::log::TraceReturnVoid{}, ##__VA_ARGS__) diff --git a/src/metrics/handler.hpp b/src/metrics/handler.hpp index a3844dc9..b008826e 100644 --- a/src/metrics/handler.hpp +++ b/src/metrics/handler.hpp @@ -34,4 +34,4 @@ namespace jam::metrics { std::shared_ptr session) = 0; }; -} // namespace kagome::metrics +} // namespace jam::metrics diff --git a/src/metrics/registry.hpp b/src/metrics/registry.hpp index 6660968f..f1735733 100644 --- a/src/metrics/registry.hpp +++ b/src/metrics/registry.hpp @@ -108,4 +108,4 @@ namespace jam::metrics { const std::map &labels = {}) = 0; }; -} // namespace kagome::metrics +} // namespace jam::metrics diff --git a/src/storage/CMakeLists.txt b/src/storage/CMakeLists.txt new file mode 100644 index 00000000..66003805 --- /dev/null +++ b/src/storage/CMakeLists.txt @@ -0,0 +1,15 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(storage + in_memory/in_memory_storage.cpp + storage_error.cpp +) + +target_link_libraries(storage + qtils::qtils +) + diff --git a/src/storage/buffer_map_types.hpp b/src/storage/buffer_map_types.hpp new file mode 100644 index 00000000..b8b15e97 --- /dev/null +++ b/src/storage/buffer_map_types.hpp @@ -0,0 +1,71 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Convenience typedefs for ByteVec-based storage interfaces. + * + * Defines specializations and using aliases for storage interfaces + * when keys and values are qtils::ByteVec. + */ + +#pragma once + +#include + +#include "storage/face/generic_maps.hpp" +#include "storage/face/write_batch.hpp" + +namespace jam::storage::face { + + /** + * @brief OwnedOrView trait for ByteVec values. + * + * Resolves to ByteVecOrView, allowing either an owned container + * or a view over ByteVec data. + */ + template <> + struct OwnedOrViewTrait { + using type = qtils::ByteVecOrView; + }; + + /** + * @brief ViewTrait for ByteVec keys. + * + * Resolves to ByteView, providing a view over ByteVec key data. + */ + template <> + struct ViewTrait { + using type = qtils::ByteView; + }; + +} // namespace jam::storage::face + +namespace jam::storage { + + using qtils::ByteVec; + using qtils::ByteVecOrView; + using qtils::ByteView; + + /** + * @brief Alias for a byte-vector write batch. + * + * Provides a WriteBatch specialized for ByteVec keys and values. + */ + using BufferBatch = face::WriteBatch; + + /** + * @brief Alias for generic byte-vector storage. + * + * Combines read, write, iteration, and batch support for ByteVec. + */ + using BufferStorage = face::GenericStorage; + + /** + * @brief Cursor type for iterating over byte-vector storage. + */ + using BufferStorageCursor = face::MapCursor; + +} // namespace jam::storage diff --git a/src/storage/face/batch_writeable.hpp b/src/storage/face/batch_writeable.hpp new file mode 100644 index 00000000..23982a3e --- /dev/null +++ b/src/storage/face/batch_writeable.hpp @@ -0,0 +1,47 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Interface mixin for batched write support. + * + * BatchWriteable exposes a method to create a WriteBatch + * for efficient bulk updates to a map-like storage. + */ + +#pragma once + +#include + +#include "storage/face/write_batch.hpp" + +namespace jam::storage::face { + + /** + * @brief Mixin interface for batched map modifications. + * @tparam K Key type. + * @tparam V Value type. + * + * BatchWriteable implementations provide a method to create + * write batches that group multiple write operations for + * atomic and efficient application. + */ + template + struct BatchWriteable { + virtual ~BatchWriteable() = default; + + /** + * @brief Create a new write batch. + * + * @return std::unique_ptr> A batch object + * for efficient bulk writes. The default implementation throws + * logic_error if not overridden. + */ + virtual std::unique_ptr> batch() { + throw std::logic_error{"BatchWriteable::batch not implemented"}; + } + }; + +} // namespace jam::storage::face diff --git a/src/storage/face/generic_maps.hpp b/src/storage/face/generic_maps.hpp new file mode 100644 index 00000000..333ec788 --- /dev/null +++ b/src/storage/face/generic_maps.hpp @@ -0,0 +1,48 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Composite interface for generic key-value storage. + * + * Combines readable, writable, iterable, and batched write support + * into a single storage abstraction. + */ + +#pragma once + +#include "storage/face/batch_writeable.hpp" +#include "storage/face/iterable.hpp" +#include "storage/face/readable.hpp" +#include "storage/face/writeable.hpp" + +namespace jam::storage::face { + + /** + * @brief Abstraction over a key-value storage supporting read, write, + * iteration, and batch writes. + * @tparam K Key type. + * @tparam V Value type. + * + * GenericStorage merges multiple storage interfaces to provide a unified + * API for key-value operations. + */ + template + struct GenericStorage : Readable, + Iterable, + Writeable, + BatchWriteable { + /** + * @brief Hint for approximate RAM usage. + * + * @return std::optional Optional in-memory size in bytes, + * or std::nullopt if no size hint is available. + */ + [[nodiscard]] virtual std::optional byteSizeHint() const { + return std::nullopt; + } + }; + +} // namespace jam::storage::face diff --git a/src/storage/face/iterable.hpp b/src/storage/face/iterable.hpp new file mode 100644 index 00000000..a9869f3b --- /dev/null +++ b/src/storage/face/iterable.hpp @@ -0,0 +1,33 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "storage/face/map_cursor.hpp" + +namespace jam::storage::face { + + /** + * @brief A mixin for an iterable map. + * @tparam K map key type + * @tparam V map value type + */ + template + struct Iterable { + using Cursor = MapCursor; + + virtual ~Iterable() = default; + + /** + * @brief Returns new key-value iterator. + * @return kv iterator + */ + virtual std::unique_ptr cursor() = 0; + }; + +} // namespace jam::storage::face diff --git a/src/storage/face/map_cursor.hpp b/src/storage/face/map_cursor.hpp new file mode 100644 index 00000000..a6c6007a --- /dev/null +++ b/src/storage/face/map_cursor.hpp @@ -0,0 +1,96 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include "storage/face/owned_or_view.hpp" +#include "storage/face/view.hpp" + +namespace jam::storage::face { + + /** + * @brief An abstraction over generic map cursor. + * @tparam K key type + * @tparam V value type + */ + template + struct MapCursor { + virtual ~MapCursor() = default; + + /** + * @brief Same as std::begin(...); + * @return error if any, true if trie is not empty, false otherwise + */ + virtual outcome::result seekFirst() = 0; + + /** + * @brief Find given key and seek iterator to this key. + * @return error if any, true if \arg key found, false otherwise + */ + virtual outcome::result seek(const View &key) = 0; + + /** + * Lower bound in reverse order. + * rocks_db.put(2) + * rocks_db.seek(1) -> 2 + * rocks_db.seek(2) -> 2 + * rocks_db.seek(3) -> none + * seekReverse(rocks_db, 1) -> none + * seekReverse(rocks_db, 2) -> 2 + * seekReverse(rocks_db, 3) -> 2 + */ + outcome::result seekReverse(const View &prefix) { + OUTCOME_TRY(ok, seek(prefix)); + if (not ok) { + return seekLast(); + } + if (View{*key()} > prefix) { + OUTCOME_TRY(prev()); + return isValid(); + } + return true; + } + + /** + * @brief Same as std::rbegin(...);, e.g. points to the last valid element + * @return error if any, true if trie is not empty, false otherwise + */ + virtual outcome::result seekLast() = 0; + + /** + * @brief Is the cursor in a valid state? + * @return true if the cursor points to an element of the map, false + * otherwise + */ + virtual bool isValid() const = 0; + + /** + * @brief Make step forward. + */ + virtual outcome::result next() = 0; + + /** + * @brief Make step backward. + */ + virtual outcome::result prev() = 0; + + /** + * @brief Getter for the key of the element currently pointed at. + * @return key if isValid() + */ + virtual std::optional key() const = 0; + + /** + * @brief Getter for value of the element currently pointed at. + * @return value if isValid() + */ + virtual std::optional> value() const = 0; + }; + +} // namespace jam::storage::face diff --git a/src/storage/face/owned_or_view.hpp b/src/storage/face/owned_or_view.hpp new file mode 100644 index 00000000..728bff32 --- /dev/null +++ b/src/storage/face/owned_or_view.hpp @@ -0,0 +1,39 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Trait to select between owned or view type for storage values. + * + * OwnedOrViewTrait defines a member type `type` that resolves to either + * an owning container or a view type for T. The alias OwnedOrView + * simplifies accessing the resolved type. + */ + +#pragma once + +namespace jam::storage::face { + + /** + * @brief Trait to determine the storage value type. + * + * Specialize this trait to define `type` as either an owned + * container or a view for the template parameter T. + * + * @tparam T Underlying value type. + */ + template + struct OwnedOrViewTrait; + + /** + * @brief Alias to the resolved owned or view type. + * + * @tparam T Underlying value type. + * @see OwnedOrViewTrait + */ + template + using OwnedOrView = typename OwnedOrViewTrait::type; + +} // namespace jam::storage::face diff --git a/src/storage/face/readable.hpp b/src/storage/face/readable.hpp new file mode 100644 index 00000000..f47b429a --- /dev/null +++ b/src/storage/face/readable.hpp @@ -0,0 +1,48 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "storage/face/owned_or_view.hpp" +#include "storage/face/view.hpp" + +namespace jam::storage::face { + /** + * @brief A mixin for read-only map. + * @tparam K key type + * @tparam V value type + */ + template + struct Readable { + virtual ~Readable() = default; + + /** + * @brief Checks if given key-value binding exists in the storage. + * @param key K + * @return true if key has value, false if does not, or error at . + */ + [[nodiscard]] virtual outcome::result contains( + const View &key) const = 0; + + /** + * @brief Get value by key + * @param key K + * @return V + */ + [[nodiscard]] virtual outcome::result> get( + const View &key) const = 0; + + /** + * @brief Get value by key + * @param key K + * @return V if contains(K) or std::nullopt + */ + [[nodiscard]] virtual outcome::result>> tryGet( + const View &key) const = 0; + }; +} // namespace jam::storage::face diff --git a/src/storage/face/view.hpp b/src/storage/face/view.hpp new file mode 100644 index 00000000..0a078794 --- /dev/null +++ b/src/storage/face/view.hpp @@ -0,0 +1,15 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +namespace jam::storage::face { + template + struct ViewTrait; + + template + using View = typename ViewTrait::type; +} // namespace jam::storage::face diff --git a/src/storage/face/write_batch.hpp b/src/storage/face/write_batch.hpp new file mode 100644 index 00000000..d93c800f --- /dev/null +++ b/src/storage/face/write_batch.hpp @@ -0,0 +1,52 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Interface for batch write operations on storage. + * + * A WriteBatch accumulates multiple write operations and applies them + * in a single atomic commit to improve performance and ensure consistency. + */ + +#pragma once + +#include "storage/face/writeable.hpp" + +namespace jam::storage::face { + + /** + * @brief An abstraction over a storage, which can be used for batch writes. + * + * @tparam K Key type. + * @tparam V Value type. + * + * A WriteBatch implementation collects multiple write operations and + * applies them together when commit() is invoked. After committing, + * the batch can be cleared and reused. + */ + template + struct WriteBatch : public Writeable { + /** + * @brief Writes batch. + * + * @details Executes all accumulated write operations in this batch + * and applies them atomically to the underlying storage. + * + * @return outcome::result Result indicating success or containing + * an error code in case of failure. + */ + virtual outcome::result commit() = 0; + + /** + * @brief Clear batch. + * + * @details Removes all pending write operations from this batch, resetting + * it to an empty state and allowing reuse without creating a new instance. + */ + virtual void clear() = 0; + }; + +} // namespace jam::storage::face diff --git a/src/storage/face/writeable.hpp b/src/storage/face/writeable.hpp new file mode 100644 index 00000000..84a85405 --- /dev/null +++ b/src/storage/face/writeable.hpp @@ -0,0 +1,57 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Interface for batch-modifiable storage map. + * + * Writeable provides methods to atomically add or remove entries + * in the underlying storage by key. + */ + +#pragma once + +#include + +#include "storage/face/owned_or_view.hpp" +#include "storage/face/view.hpp" + +namespace jam::storage::face { + + /** + * @brief Interface for batch-modifiable map storage. + * @tparam K Key type. + * @tparam V Value type. + * + * Writeable is a mixin that exposes methods to put or remove + * entries in a map-like storage. Implementations should apply + * each operation immediately or as part of a larger batch. + */ + template + struct Writeable { + virtual ~Writeable() = default; + + /** + * @brief Store or update a value by key. + * + * @param key Key to associate with the value. + * @param value The value to store, either owned or a view. + * @return outcome::result Returns void on success or + * an error code on failure. + */ + virtual outcome::result put(const View &key, + OwnedOrView &&value) = 0; + + /** + * @brief Remove a value by key. + * + * @param key Key whose mapping should be removed. + * @return outcome::result Returns void on success or + * an error code if the removal fails. + */ + virtual outcome::result remove(const View &key) = 0; + }; + +} // namespace jam::storage::face diff --git a/src/storage/in_memory/cursor.hpp b/src/storage/in_memory/cursor.hpp new file mode 100644 index 00000000..9f85ecbe --- /dev/null +++ b/src/storage/in_memory/cursor.hpp @@ -0,0 +1,74 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "storage/in_memory/in_memory_storage.hpp" + +namespace jam::storage { + class InMemoryCursor : public BufferStorageCursor { + public: + explicit InMemoryCursor(InMemoryStorage &db) : db{db} {} + + outcome::result seekFirst() override { + return seek(db.storage_.begin()); + } + + outcome::result seek(const ByteView &key) override { + return seek(db.storage_.lower_bound(key.toHex())); + } + + outcome::result seekLast() override { + return seek(db.storage_.empty() ? db.storage_.end() + : std::prev(db.storage_.end())); + } + + bool isValid() const override { + return kv.has_value(); + } + + outcome::result next() override { + seek(db.storage_.upper_bound(kv->first.toHex())); + return outcome::success(); + } + + outcome::result prev() override { + auto it = db.storage_.lower_bound(kv->first.toHex()); + seek(it == db.storage_.begin() ? db.storage_.end() : std::prev(it)); + return outcome::success(); + } + + std::optional key() const override { + if (kv) { + return kv->first; + } + return std::nullopt; + } + + std::optional value() const override { + if (kv) { + return ByteView{kv->second}; + } + return std::nullopt; + } + + private: + bool seek(decltype(InMemoryStorage::storage_)::iterator it) { + if (it == db.storage_.end()) { + kv.reset(); + } else { + kv.emplace(ByteVec::fromHex(it->first).value(), it->second); + } + return isValid(); + } + + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + InMemoryStorage &db; + std::optional> kv; + }; +} // namespace jam::storage diff --git a/src/storage/in_memory/in_memory_batch.hpp b/src/storage/in_memory/in_memory_batch.hpp new file mode 100644 index 00000000..9d36bdd4 --- /dev/null +++ b/src/storage/in_memory/in_memory_batch.hpp @@ -0,0 +1,47 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "storage/in_memory/in_memory_storage.hpp" + +namespace jam::storage { + using qtils::ByteVec; + + class InMemoryBatch : public BufferBatch { + public: + explicit InMemoryBatch(InMemoryStorage &db) : db{db} {} + + outcome::result put(const ByteView &key, + ByteVecOrView &&value) override { + entries[key.toHex()] = std::move(value).intoByteVec(); + return outcome::success(); + } + + outcome::result remove(const ByteView &key) override { + entries.erase(key.toHex()); + return outcome::success(); + } + + outcome::result commit() override { + for (auto &entry : entries) { + OUTCOME_TRY(db.put(ByteVec::fromHex(entry.first).value(), + ByteView{entry.second})); + } + return outcome::success(); + } + + void clear() override { + entries.clear(); + } + + private: + std::map entries; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + InMemoryStorage &db; + }; +} // namespace jam::storage diff --git a/src/storage/in_memory/in_memory_storage.cpp b/src/storage/in_memory/in_memory_storage.cpp new file mode 100644 index 00000000..5a14058c --- /dev/null +++ b/src/storage/in_memory/in_memory_storage.cpp @@ -0,0 +1,72 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "storage/in_memory/in_memory_storage.hpp" + +#include "storage/in_memory/cursor.hpp" +#include "storage/in_memory/in_memory_batch.hpp" +#include "storage/storage_error.hpp" + +using qtils::ByteVec; + +namespace jam::storage { + + outcome::result InMemoryStorage::get( + const ByteView &key) const { + if (storage_.find(key.toHex()) != storage_.end()) { + return ByteView{storage_.at(key.toHex())}; + } + + return StorageError::NOT_FOUND; + } + + outcome::result> InMemoryStorage::tryGet( + const qtils::ByteView &key) const { + if (storage_.find(key.toHex()) != storage_.end()) { + return ByteView{storage_.at(key.toHex())}; + } + + return std::nullopt; + } + + outcome::result InMemoryStorage::put(const ByteView &key, + ByteVecOrView &&value) { + auto it = storage_.find(key.toHex()); + if (it != storage_.end()) { + size_t old_value_size = it->second.size(); + BOOST_ASSERT(size_ >= old_value_size); + size_ -= old_value_size; + } + size_ += value.size(); + storage_[key.toHex()] = std::move(value).intoByteVec(); + return outcome::success(); + } + + outcome::result InMemoryStorage::contains(const ByteView &key) const { + return storage_.find(key.toHex()) != storage_.end(); + } + + outcome::result InMemoryStorage::remove(const ByteView &key) { + auto it = storage_.find(key.toHex()); + if (it != storage_.end()) { + size_ -= it->second.size(); + storage_.erase(it); + } + return outcome::success(); + } + + std::unique_ptr InMemoryStorage::batch() { + return std::make_unique(*this); + } + + std::unique_ptr InMemoryStorage::cursor() { + return std::make_unique(*this); + } + + std::optional InMemoryStorage::byteSizeHint() const { + return size_; + } +} // namespace jam::storage diff --git a/src/storage/in_memory/in_memory_storage.hpp b/src/storage/in_memory/in_memory_storage.hpp new file mode 100644 index 00000000..8ace0e33 --- /dev/null +++ b/src/storage/in_memory/in_memory_storage.hpp @@ -0,0 +1,54 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + +#include "storage/buffer_map_types.hpp" + +namespace jam::storage { + + /** + * Simple storage that conforms PersistentMap interface + * Mostly needed to have an in-memory trie in tests to avoid integration with + * an actual persistent database + */ + class InMemoryStorage : public BufferStorage { + public: + ~InMemoryStorage() override = default; + + [[nodiscard]] outcome::result get( + const ByteView &key) const override; + + [[nodiscard]] outcome::result> tryGet( + const ByteView &key) const override; + + outcome::result put(const ByteView &key, + ByteVecOrView &&value) override; + + [[nodiscard]] outcome::result contains( + const ByteView &key) const override; + + outcome::result remove(const ByteView &key) override; + + std::unique_ptr batch() override; + + std::unique_ptr cursor() override; + + [[nodiscard]] std::optional byteSizeHint() const override; + + private: + std::map storage_; + size_t size_ = 0; + + friend class InMemoryCursor; + }; + +} // namespace jam::storage diff --git a/src/storage/storage_error.cpp b/src/storage/storage_error.cpp new file mode 100644 index 00000000..28111e21 --- /dev/null +++ b/src/storage/storage_error.cpp @@ -0,0 +1,33 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "storage/storage_error.hpp" + +OUTCOME_CPP_DEFINE_CATEGORY(jam::storage, StorageError, e) { + using E = StorageError; + switch (e) { + case E::OK: + return "success"; + case E::NOT_SUPPORTED: + return "operation is not supported in storage"; + case E::CORRUPTION: + return "data corruption in storage"; + case E::INVALID_ARGUMENT: + return "invalid argument to storage"; + case E::IO_ERROR: + return "IO error in storage"; + case E::NOT_FOUND: + return "entry not found in storage"; + case E::DB_PATH_NOT_CREATED: + return "storage path was not created"; + case E::STORAGE_GONE: + return "storage instance has been uninitialized"; + case E::UNKNOWN: + break; + } + + return "unknown error"; +} diff --git a/src/storage/storage_error.hpp b/src/storage/storage_error.hpp new file mode 100644 index 00000000..4c6b82ed --- /dev/null +++ b/src/storage/storage_error.hpp @@ -0,0 +1,48 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Definitions of storage interface error codes. + * + * Provides an enumeration of error codes that can be returned by + * storage operations, along with integration into outcome error handling. + */ + +#pragma once + +#include + +namespace jam::storage { + + /** + * @brief Universal error codes for storage interface. + * + * Defines common error conditions returned by storage operations, + * such as missing entries, corruption, or IO failures. + */ + enum class StorageError : int { // NOLINT(performance-enum-size) + + OK = 0, ///< success (no error) + + NOT_SUPPORTED = 1, ///< operation is not supported in storage + CORRUPTION = 2, ///< data corruption in storage + INVALID_ARGUMENT = 3, ///< invalid argument to storage + IO_ERROR = 4, ///< IO error in storage + NOT_FOUND = 5, ///< entry not found in storage + DB_PATH_NOT_CREATED = 6, ///< storage path was not created + STORAGE_GONE = 7, ///< storage instance has been uninitialized + + UNKNOWN = 1000, ///< unknown error + }; +} // namespace jam::storage + +/** + * @brief Declare StorageError integration with Outcome library. + * + * Enables automatic conversion between StorageError and + * outcome::result for error handling. + */ +OUTCOME_HPP_DECLARE_ERROR(jam::storage, StorageError); From 57ebefea82489b1cb7ec09ec1626e3ea62d4966f Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Thu, 8 May 2025 11:55:41 +0300 Subject: [PATCH 02/15] feature: spaced storage Signed-off-by: Dmitriy Khaustov aka xDimon --- src/injector/node_injector.cpp | 3 +- .../in_memory/in_memory_spaced_storage.hpp | 59 +++++++++++++++++++ src/storage/spaced_storage.hpp | 46 +++++++++++++++ src/storage/spaces.hpp | 39 ++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/storage/in_memory/in_memory_spaced_storage.hpp create mode 100644 src/storage/spaced_storage.hpp create mode 100644 src/storage/spaces.hpp diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index b84b8fd6..41529499 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -34,7 +34,7 @@ #include "se/impl/async_dispatcher_impl.hpp" #include "se/subscription.hpp" #include "storage/in_memory/in_memory_storage.hpp" - +#include "storage/in_memory/in_memory_spaced_storage.hpp" namespace { namespace di = boost::di; namespace fs = std::filesystem; @@ -72,6 +72,7 @@ namespace { }; }), di::bind.to(), + di::bind.to(), // user-defined overrides... std::forward(args)...); diff --git a/src/storage/in_memory/in_memory_spaced_storage.hpp b/src/storage/in_memory/in_memory_spaced_storage.hpp new file mode 100644 index 00000000..faae188a --- /dev/null +++ b/src/storage/in_memory/in_memory_spaced_storage.hpp @@ -0,0 +1,59 @@ +/** +* Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file in_memory_spaced_storage.hpp + * @brief Implements an in-memory version of SpacedStorage for testing purposes. + * + * This class provides an in-memory implementation of the SpacedStorage interface. + * It is useful for unit tests and other scenarios where a persistent backend is + * not required or desired. + */ + +#pragma once + +#include + +#include "in_memory_storage.hpp" +#include "storage/buffer_map_types.hpp" +#include "storage/spaced_storage.hpp" + +namespace jam::storage { + + /** + * @class InMemorySpacedStorage + * @brief In-memory implementation of the SpacedStorage interface. + * + * This class maps Space identifiers to corresponding instances of + * InMemoryStorage. It is typically used in tests to simulate isolated + * persistent storage spaces without involving a real database. + */ + class InMemorySpacedStorage : public storage::SpacedStorage { + public: + /** + * @brief Retrieve or create an in-memory storage for a given space. + * + * If the storage for the given space already exists, returns it. + * Otherwise, creates a new InMemoryStorage and stores it. + * + * @param space The logical storage space to retrieve. + * @return A shared pointer to the corresponding BufferStorage. + */ + std::shared_ptr getSpace(Space space) override { + auto it = spaces_.find(space); + if (it != spaces_.end()) { + return it->second; + } + return spaces_.emplace(space, std::make_shared()) + .first->second; + } + + private: + /// Map of storage spaces to their corresponding in-memory storages + std::map> spaces_; + }; + +} // namespace jam::storage \ No newline at end of file diff --git a/src/storage/spaced_storage.hpp b/src/storage/spaced_storage.hpp new file mode 100644 index 00000000..496b27f1 --- /dev/null +++ b/src/storage/spaced_storage.hpp @@ -0,0 +1,46 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Declares the SpacedStorage interface, which provides access to + * separate logical storage spaces represented by BufferStorage. + * + * This interface is used as an abstraction for managing multiple namespaces + * or isolated segments of storage within a broader system. Each space is + * uniquely identified by a Space enum value. + */ + +#pragma once + +#include + +#include "storage/buffer_map_types.hpp" +#include "storage/spaces.hpp" + +namespace jam::storage { + + /** + * @class SpacedStorage + * @brief Abstract interface for accessing different logical storage spaces. + * + * The SpacedStorage class provides a mechanism to retrieve storage units + * (BufferStorage) that correspond to different logical spaces within a + * system. Implementations of this interface can be used to isolate and + * organize data based on the space identifier. + */ + class SpacedStorage { + public: + virtual ~SpacedStorage() = default; + + /** + * Retrieve a pointer to the map representing particular storage space + * @param space - identifier of required space + * @return a pointer buffer storage for a space + */ + virtual std::shared_ptr getSpace(Space space) = 0; + }; + +} // namespace jam::storage diff --git a/src/storage/spaces.hpp b/src/storage/spaces.hpp new file mode 100644 index 00000000..f9929709 --- /dev/null +++ b/src/storage/spaces.hpp @@ -0,0 +1,39 @@ +/** +* Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @brief Defines the Space enumeration used to identify logical storage spaces. + * + * The Space enum provides identifiers for different logical areas of storage + * within the system. These values are used to access corresponding buffer + * storages via the SpacedStorage interface. + */ + +#pragma once + +#include + +namespace jam::storage { + + /** + * @enum Space + * @brief Enumerates the logical storage spaces used by the system. + * + * Each value in this enum represents a distinct namespace or segment + * of the system's storage. The values are used as keys to retrieve + * specific BufferStorage instances. + */ + enum Space : uint8_t { + Default = 0, ///< Default space used for general-purpose storage + LookupKey, ///< Space used for mapping lookup keys + + // application-defined spaces + // ... append here + + kTotal ///< Total number of defined spaces (must be last) + }; + +} \ No newline at end of file From bebe39e5af9203060fa7c2e6d744f17989f71e2f Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Mon, 16 Jun 2025 15:20:06 +0300 Subject: [PATCH 03/15] feature: database config Signed-off-by: Dmitriy Khaustov aka xDimon --- example/config.yaml | 7 ++- src/app/configuration.cpp | 8 ++++ src/app/configuration.hpp | 13 +++++- src/app/configurator.cpp | 95 +++++++++++++++++++++++++++++++++++++++ src/app/configurator.hpp | 1 + 5 files changed, 121 insertions(+), 3 deletions(-) diff --git a/example/config.yaml b/example/config.yaml index bb82a33d..2795ca41 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -3,6 +3,10 @@ general: base_path: /tmp/jam_node modules_dir: modules +database: + directory: db + cache_size: 1G + metrics: enabled: true host: 127.0.0.1 @@ -33,4 +37,5 @@ logging: - name: application - name: rpc - name: metrics - - name: threads \ No newline at end of file + - name: threads + - name: storage \ No newline at end of file diff --git a/src/app/configuration.cpp b/src/app/configuration.cpp index 9c9ab1ac..ac2c397f 100644 --- a/src/app/configuration.cpp +++ b/src/app/configuration.cpp @@ -10,6 +10,10 @@ namespace jam::app { Configuration::Configuration() : version_("undefined"), name_("unnamed"), + database_{ + .directory = "db", + .cache_size = 1 << 30, + }, metrics_{ .endpoint{}, .enabled{}, @@ -31,6 +35,10 @@ namespace jam::app { return modules_dir_; } + const Configuration::DatabaseConfig &Configuration::database() const { + return database_; + } + const Configuration::MetricsConfig &Configuration::metrics() const { return metrics_; } diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index 9e335bca..c28b647d 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -8,21 +8,27 @@ #include #include -#include #include #include namespace jam::app { - class Configuration final : Singleton { + class Configuration : Singleton { public: using Endpoint = boost::asio::ip::tcp::endpoint; + struct DatabaseConfig { + std::filesystem::path directory = "db"; + size_t cache_size = 1 << 30; // 1GiB + bool migration_enabled = false; + }; + struct MetricsConfig { Endpoint endpoint; std::optional enabled; }; Configuration(); + virtual ~Configuration() = default; // /// Generate yaml-file with actual config // virtual void generateConfigFile() const = 0; @@ -32,6 +38,8 @@ namespace jam::app { [[nodiscard]] virtual const std::filesystem::path &basePath() const; [[nodiscard]] virtual const std::filesystem::path &modulesDir() const; + [[nodiscard]] virtual const DatabaseConfig &database() const; + [[nodiscard]] virtual const MetricsConfig &metrics() const; private: @@ -42,6 +50,7 @@ namespace jam::app { std::filesystem::path base_path_; std::filesystem::path modules_dir_; + DatabaseConfig database_; MetricsConfig metrics_; }; diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index b2b9ce6b..84ee7805 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -77,6 +77,11 @@ namespace jam::app { config_->version_ = buildVersion(); config_->name_ = "noname"; + + config_->database_.directory = "db"; + config_->database_.cache_size = 512; + config_->database_.migration_enabled = false; + config_->metrics_.endpoint = {boost::asio::ip::address_v4::any(), 9615}; config_->metrics_.enabled = std::nullopt; @@ -100,6 +105,14 @@ namespace jam::app { "Global log level can be set with: -l.") ; + po::options_description storage_options("Storage options"); + storage_options.add_options() + ("db_path", po::value()->default_value(config_->database_.directory), "Path to DB directory. Can be relative on base path.") + // ("db-tmp", "Use temporary storage path.") + ("db_cache_size", po::value()->default_value(config_->database_.cache_size), "Limit the memory the database cache can use .") + ("db_enable_migration", po::bool_switch(), "Enable automatic db migration.") + ; + po::options_description metrics_options("Metric options"); metrics_options.add_options() ("prometheus_disable", "Set to disable OpenMetrics.") @@ -111,6 +124,7 @@ namespace jam::app { cli_options_ .add(general_options) // + .add(storage_options) .add(metrics_options); } @@ -199,6 +213,7 @@ namespace jam::app { qtils::SharedRef logger) { logger_ = std::move(logger); OUTCOME_TRY(initGeneralConfig()); + OUTCOME_TRY(initDatabaseConfig()); OUTCOME_TRY(initOpenMetricsConfig()); return config_; @@ -314,6 +329,86 @@ namespace jam::app { return outcome::success(); } + outcome::result Configurator::initDatabaseConfig() { + // Init by config-file + if (config_file_.has_value()) { + auto section = (*config_file_)["database"]; + if (section.IsDefined()) { + if (section.IsMap()) { + auto path = section["path"]; + if (path.IsDefined()) { + if (path.IsScalar()) { + auto value = path.as(); + config_->database_.directory = value; + } else { + file_errors_ << "E: Value 'database.path' must be scalar\n"; + file_has_error_ = true; + } + } + auto spec_file = section["cache_size"]; + if (spec_file.IsDefined()) { + if (spec_file.IsScalar()) { + auto value = spec_file.as(); + config_->database_.cache_size = value; + } else { + file_errors_ + << "E: Value 'database.cache_size_mb' must be scalar\n"; + file_has_error_ = true; + } + } + } else { + file_errors_ << "E: Section 'database' defined, but is not map\n"; + file_has_error_ = true; + } + } + } + + if (file_has_error_) { + std::string path; + find_argument( + cli_values_map_, "config", [&](const std::string &value) { + path = value; + }); + SL_ERROR(logger_, "Config file `{}` has some problems:", path); + std::istringstream iss(file_errors_.str()); + std::string line; + while (std::getline(iss, line)) { + SL_ERROR(logger_, " {}", std::string_view(line).substr(3)); + } + return Error::ConfigFileParseFailed; + } + + // Adjust by CLI arguments + bool fail; + + fail = false; + find_argument( + cli_values_map_, "db_path", [&](const std::string &value) { + config_->database_.directory = value; + }); + find_argument( + cli_values_map_, "db_cache_size", [&](const uint32_t &value) { + config_->database_.cache_size = value; + }); + if (find_argument(cli_values_map_, "db_migration_enabled")) { + config_->database_.migration_enabled = true; + } + if (fail) { + return Error::CliArgsParseFailed; + } + + // Check values + auto make_absolute = [&](const std::filesystem::path &path) { + return weakly_canonical(config_->base_path_.is_absolute() + ? path + : (config_->base_path_ / path)); + }; + + config_->database_.directory = make_absolute(config_->database_.directory); + + return outcome::success(); + } + outcome::result Configurator::initOpenMetricsConfig() { if (config_file_.has_value()) { auto section = (*config_file_)["metrics"]; diff --git a/src/app/configurator.hpp b/src/app/configurator.hpp index e403bb58..9ad6e0e0 100644 --- a/src/app/configurator.hpp +++ b/src/app/configurator.hpp @@ -56,6 +56,7 @@ namespace jam::app { private: outcome::result initGeneralConfig(); + outcome::result initDatabaseConfig(); outcome::result initOpenMetricsConfig(); int argc_; From 052f9acfe156f9e881f3c37d8484b6737e3787ce Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Mon, 16 Jun 2025 15:21:47 +0300 Subject: [PATCH 04/15] feature: fd limit --- src/CMakeLists.txt | 3 ++ src/utils/CMakeLists.txt | 13 +++++++ src/utils/fd_limit.cpp | 74 ++++++++++++++++++++++++++++++++++++++++ src/utils/fd_limit.hpp | 21 ++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 src/utils/CMakeLists.txt create mode 100644 src/utils/fd_limit.cpp create mode 100644 src/utils/fd_limit.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5e31f158..584dd6f2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,3 +34,6 @@ add_subdirectory(modules) # Storage add_subdirectory(storage) + +# Utilities +add_subdirectory(utils) diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt new file mode 100644 index 00000000..d8107af9 --- /dev/null +++ b/src/utils/CMakeLists.txt @@ -0,0 +1,13 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(fd_limit SHARED + fd_limit.cpp +) +target_link_libraries(fd_limit + Boost::boost + logger +) diff --git a/src/utils/fd_limit.cpp b/src/utils/fd_limit.cpp new file mode 100644 index 00000000..553a2b6f --- /dev/null +++ b/src/utils/fd_limit.cpp @@ -0,0 +1,74 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "utils/fd_limit.hpp" + +#include + +#include +#include + +namespace jam { + namespace { + bool getFdLimit(rlimit &r, const log::Logger &logger) { + if (getrlimit(RLIMIT_NOFILE, &r) != 0) { + SL_WARN(logger, + "Error: getrlimit(RLIMIT_NOFILE) errno={} {}", + errno, + strerror(errno)); + return false; + } + return true; + } + + bool setFdLimit(const rlimit &r) { + return setrlimit(RLIMIT_NOFILE, &r) == 0; + } + } // namespace + + std::optional getFdLimit(const log::Logger &logger) { + rlimit r{}; + if (!getFdLimit(r, logger)) { + return std::nullopt; + } + return r.rlim_cur; + } + + void setFdLimit(size_t limit, const log::Logger &logger) { + rlimit r{}; + if (!getFdLimit(r, logger)) { + return; + } + if (r.rlim_max == RLIM_INFINITY) { + SL_VERBOSE(logger, "current={} max=unlimited", r.rlim_cur); + } else { + SL_VERBOSE(logger, "current={} max={}", r.rlim_cur, r.rlim_max); + } + const rlim_t current = r.rlim_cur; + if (limit == current) { + return; + } + r.rlim_cur = limit; + if (limit < current) { + SL_WARN(logger, "requested limit is lower than system allowed limit"); + setFdLimit(r); + } else if (!setFdLimit(r)) { + std::ignore = std::upper_bound(boost::counting_iterator{current}, + boost::counting_iterator{rlim_t{limit}}, + nullptr, + [&](std::nullptr_t, auto new_current) { + r.rlim_cur = new_current; + return !setFdLimit(r); + }); + } + if (!getFdLimit(r, logger)) { + return; + } + if (r.rlim_cur != current) { + SL_VERBOSE(logger, "changed current={}", r.rlim_cur); + } + } +} // namespace jam diff --git a/src/utils/fd_limit.hpp b/src/utils/fd_limit.hpp new file mode 100644 index 00000000..b8f294f3 --- /dev/null +++ b/src/utils/fd_limit.hpp @@ -0,0 +1,21 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include "log/logger.hpp" + +struct rlimit; + +namespace jam { + + std::optional getFdLimit(const log::Logger &logger); + void setFdLimit(size_t limit, const log::Logger &logger); + +} // namespace jam From 03abe523d413da9bf64acd13ee16ef1716e37b4a Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Tue, 13 May 2025 10:39:38 +0300 Subject: [PATCH 05/15] feature: rocksdb as spaced storage impl Signed-off-by: Dmitriy Khaustov aka xDimon --- src/injector/node_injector.cpp | 5 +- src/storage/CMakeLists.txt | 6 + src/storage/rocksdb/rocksdb.cpp | 652 +++++++++++++++++++++++++ src/storage/rocksdb/rocksdb.hpp | 175 +++++++ src/storage/rocksdb/rocksdb_batch.cpp | 44 ++ src/storage/rocksdb/rocksdb_batch.hpp | 36 ++ src/storage/rocksdb/rocksdb_cursor.cpp | 54 ++ src/storage/rocksdb/rocksdb_cursor.hpp | 40 ++ src/storage/rocksdb/rocksdb_spaces.cpp | 44 ++ src/storage/rocksdb/rocksdb_spaces.hpp | 25 + src/storage/rocksdb/rocksdb_util.hpp | 57 +++ src/storage/spaces.hpp | 13 +- vcpkg.json | 3 +- 13 files changed, 1146 insertions(+), 8 deletions(-) create mode 100644 src/storage/rocksdb/rocksdb.cpp create mode 100644 src/storage/rocksdb/rocksdb.hpp create mode 100644 src/storage/rocksdb/rocksdb_batch.cpp create mode 100644 src/storage/rocksdb/rocksdb_batch.hpp create mode 100644 src/storage/rocksdb/rocksdb_cursor.cpp create mode 100644 src/storage/rocksdb/rocksdb_cursor.hpp create mode 100644 src/storage/rocksdb/rocksdb_spaces.cpp create mode 100644 src/storage/rocksdb/rocksdb_spaces.hpp create mode 100644 src/storage/rocksdb/rocksdb_util.hpp diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index 41529499..53b0b7f6 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -35,6 +35,8 @@ #include "se/subscription.hpp" #include "storage/in_memory/in_memory_storage.hpp" #include "storage/in_memory/in_memory_spaced_storage.hpp" +#include "storage/rocksdb/rocksdb.hpp" + namespace { namespace di = boost::di; namespace fs = std::filesystem; @@ -72,7 +74,8 @@ namespace { }; }), di::bind.to(), - di::bind.to(), + //di::bind.to(), + di::bind.to(), // user-defined overrides... std::forward(args)...); diff --git a/src/storage/CMakeLists.txt b/src/storage/CMakeLists.txt index 66003805..486f52b5 100644 --- a/src/storage/CMakeLists.txt +++ b/src/storage/CMakeLists.txt @@ -7,9 +7,15 @@ add_library(storage in_memory/in_memory_storage.cpp storage_error.cpp + rocksdb/rocksdb.cpp + rocksdb/rocksdb_batch.cpp + rocksdb/rocksdb_cursor.cpp + rocksdb/rocksdb_spaces.cpp ) target_link_libraries(storage qtils::qtils + RocksDB::rocksdb + fd_limit ) diff --git a/src/storage/rocksdb/rocksdb.cpp b/src/storage/rocksdb/rocksdb.cpp new file mode 100644 index 00000000..4fbf5c96 --- /dev/null +++ b/src/storage/rocksdb/rocksdb.cpp @@ -0,0 +1,652 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "storage/rocksdb/rocksdb.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "storage/rocksdb/rocksdb_batch.hpp" +#include "storage/rocksdb/rocksdb_cursor.hpp" +#include "storage/rocksdb/rocksdb_spaces.hpp" +#include "storage/rocksdb/rocksdb_util.hpp" +#include "storage/storage_error.hpp" +#include "utils/fd_limit.hpp" + +namespace jam::storage { + namespace fs = std::filesystem; + + rocksdb::ColumnFamilyOptions configureColumn(uint64_t memory_budget) { + rocksdb::ColumnFamilyOptions options; + options.OptimizeLevelStyleCompaction(memory_budget); + auto table_options = RocksDb::tableOptionsConfiguration(); + options.table_factory.reset(NewBlockBasedTableFactory(table_options)); + return options; + } + + template + void configureColumnFamilies( + std::vector &column_family_descriptors, + std::vector &ttls, + ColumnFamilyNames &&cf_names, + const std::unordered_map &column_ttl, + const std::unordered_map &column_cache_sizes, + uint64_t memory_budget, + log::Logger &log) { + double distributed_cache_part = 0; + size_t count = 0; + for (const auto &[column, value] : column_cache_sizes) { + if (qtils::cxx23::ranges::contains(cf_names, column)) { + distributed_cache_part += value; + ++count; + } + } + BOOST_ASSERT_MSG(distributed_cache_part <= 1.0, + "Special cache distribution must not be greater 100%"); + + const uint64_t other_spaces_cache_size = + (cf_names.size() > count) + ? static_cast(memory_budget) + * (1.0 - distributed_cache_part) / (cf_names.size() - count) + : 0; + + for (auto &space_name : std::forward(cf_names)) { + auto ttl = 0; + auto cache_size = 0ull; + if (const auto it = column_ttl.find(space_name); it != column_ttl.end()) { + ttl = it->second; + } + if (const auto it = column_cache_sizes.find(space_name); + it != column_cache_sizes.end()) { + cache_size = static_cast(memory_budget) * it->second; + } else { + cache_size = other_spaces_cache_size; + } + auto column_options = configureColumn(cache_size); + column_family_descriptors.emplace_back(space_name, column_options); + ttls.push_back(ttl); + SL_DEBUG( + log, + "Column family '{}' configured with ttl={}sec, cache_size={:.0f}Mb", + space_name, + ttl, + static_cast(cache_size) / 1024.0 / 1024.0); + } + } + + RocksDb::RocksDb(qtils::SharedRef logsys, + qtils::SharedRef app_config) + : logger_(logsys->getLogger("RocksDB", "storage")) { + ro_.fill_cache = false; + + const auto &path = app_config->database().directory; + bool enable_migration = app_config->database().migration_enabled; + + auto options = rocksdb::Options{}; + options.create_if_missing = true; + options.optimize_filters_for_hits = true; + options.table_factory.reset(rocksdb::NewBlockBasedTableFactory( + storage::RocksDb::tableOptionsConfiguration())); + + // Setting limit for open rocksdb files to a half of system soft limit + auto soft_limit = getFdLimit(logger_); + if (!soft_limit) { + SL_CRITICAL(logger_, "Call getrlimit(RLIMIT_NOFILE) was failed"); + qtils::raise(StorageError::UNKNOWN); + } + // NOLINTNEXTLINE(cppcoreguidelines-narrowing-conversions) + options.max_open_files = soft_limit.value() / 2; + + const auto no_db_presented = not exists(path); + + std::error_code ec; + create_directories(path, ec); + if (ec) { + SL_CRITICAL(logger_, "Can't create DB directory: {}", ec); + qtils::raise(ec); + } + + if (auto res = createDirectory(path, logger_); res.has_error()) { + SL_CRITICAL(logger_, + "Can't create DB directory ({}): {}", + path.native(), + res.error()); + qtils::raise(ec); + } + + std::vector existing_families; + auto res = rocksdb::DB::ListColumnFamilies( + options, path.native(), &existing_families); + if (not res.ok() and not res.IsPathNotFound()) { + SL_ERROR(logger_, + "Can't list column families in {}: {}", + path.native(), + res.ToString()); + qtils::raise(status_as_error(res, logger_)); + } + + std::unordered_set all_families; + auto required_families = + std::views::iota(0, static_cast(SpacesCount)) + | std::views::transform([](int i) -> std::string { + return std::string(spaceName(static_cast(i))); + }); + std::ranges::copy(required_families, + std::inserter(all_families, all_families.end())); + + for (auto &existing_family : existing_families) { + auto [_, was_inserted] = all_families.insert(existing_family); + if (was_inserted) { + SL_WARN(logger_, + "Column family '{}' present in database but not used by JAM; " + "Probably obsolete.", + existing_family); + } + } + + const std::unordered_map column_ttl = { + // Example: + // {"avaliability_storage", 25 * 60 * 60} // 25 hours + }; + + const std::unordered_map column_cache_size = { + // Example: + // {"trie_node", 0.9} // 90% + }; + + const auto memory_budget = app_config->database().cache_size; + + std::vector column_family_descriptors; + std::vector ttls; + configureColumnFamilies(column_family_descriptors, + ttls, + all_families, + column_ttl, + column_cache_size, + memory_budget, + logger_); + + options.create_missing_column_families = true; + + const auto ttl_migrated_path = path.parent_path() / "ttl_migrated"; + const auto ttl_migrated_exists = exists(ttl_migrated_path); + + if (no_db_presented or ttl_migrated_exists) { + qtils::raise_on_err(openDatabaseWithTTL(options, + path, + column_family_descriptors, + ttls, + *this, + ttl_migrated_path, + logger_)); + } else { + if (not enable_migration) { + SL_ERROR(logger_, + "Database migration is disabled, use older node version or " + "run with --db_migration_enabled flag"); + qtils::raise(StorageError::IO_ERROR); + } + + qtils::raise_on_err(migrateDatabase(options, + path, + column_family_descriptors, + ttls, + *this, + ttl_migrated_path, + logger_)); + } + + // Print size of each column family + SL_VERBOSE(logger_, "Current column family sizes:"); + for (const auto &handle : column_family_handles_) { + std::string size_str; + if (db_->GetProperty( + handle, "rocksdb.estimate-live-data-size", &size_str)) { + uint64_t size_bytes = std::stoull(size_str); + double size_mb = static_cast(size_bytes) / 1024.0 / 1024.0; + SL_VERBOSE(logger_, " - {}: {:.2f} Mb", handle->GetName(), size_mb); + } else { + SL_WARN(logger_, + "Failed to get size of column family '{}'", + handle->GetName()); + } + } + } + + RocksDb::~RocksDb() { + for (auto *handle : column_family_handles_) { + db_->DestroyColumnFamilyHandle(handle); + } + delete db_; + } + + outcome::result RocksDb::createDirectory( + const std::filesystem::path &absolute_path, log::Logger &log) { + std::error_code ec; + if (not fs::create_directory(absolute_path.native(), ec) and ec.value()) { + SL_ERROR(log, + "Can't create directory {} for database: {}", + absolute_path.native(), + ec); + return StorageError::IO_ERROR; + } + if (not fs::is_directory(absolute_path.native())) { + SL_ERROR(log, + "Can't open {} for database: is not a directory", + absolute_path.native()); + return StorageError::IO_ERROR; + } + return outcome::success(); + } + + outcome::result RocksDb::openDatabaseWithTTL( + const rocksdb::Options &options, + const std::filesystem::path &path, + const std::vector + &column_family_descriptors, + const std::vector &ttls, + RocksDb &rocks_db, + const std::filesystem::path &ttl_migrated_path, + log::Logger &log) { + const auto status = + rocksdb::DBWithTTL::Open(options, + path.native(), + column_family_descriptors, + &rocks_db.column_family_handles_, + &rocks_db.db_, + ttls); + if (not status.ok()) { + SL_ERROR(log, + "Can't open database in {}: {}", + path.native(), + status.ToString()); + return status_as_error(status, log); + } + if (not fs::exists(ttl_migrated_path)) { + std::ofstream file(ttl_migrated_path.native()); + if (not file) { + SL_ERROR(log, + "Can't create file {} for database", + ttl_migrated_path.native()); + return StorageError::IO_ERROR; + } + file.close(); + } + return outcome::success(); + } + + outcome::result RocksDb::migrateDatabase( + const rocksdb::Options &options, + const std::filesystem::path &path, + const std::vector + &column_family_descriptors, + const std::vector &ttls, + RocksDb &rocks_db, + const std::filesystem::path &ttl_migrated_path, + log::Logger &log) { + rocksdb::DB *db_raw = nullptr; + std::vector column_family_handles; + auto status = rocksdb::DB::Open(options, + path.native(), + column_family_descriptors, + &column_family_handles, + &db_raw); + std::shared_ptr db(db_raw); + if (not status.ok()) { + SL_ERROR(log, + "Can't open old database in {}: {}", + path.native(), + status.ToString()); + return status_as_error(status, log); + } + auto defer_db = + std::make_unique(db, column_family_handles, log); + + std::vector column_family_handles_with_ttl; + const auto ttl_path = path.parent_path() / "db_ttl"; + std::error_code ec; + fs::create_directories(ttl_path, ec); + if (ec) { + SL_ERROR(log, + "Can't create directory {} for database: {}", + ttl_path.native(), + ec); + return StorageError::IO_ERROR; + } + rocksdb::DBWithTTL *db_with_ttl_raw = nullptr; + status = rocksdb::DBWithTTL::Open(options, + ttl_path.native(), + column_family_descriptors, + &column_family_handles_with_ttl, + &db_with_ttl_raw, + ttls); + if (not status.ok()) { + SL_ERROR(log, + "Can't open database in {}: {}", + ttl_path.native(), + status.ToString()); + return status_as_error(status, log); + } + std::shared_ptr db_with_ttl(db_with_ttl_raw); + auto defer_db_ttl = std::make_unique( + db_with_ttl, column_family_handles_with_ttl, log); + + for (std::size_t i = 0; i < column_family_handles.size(); ++i) { + const auto from_handle = column_family_handles[i]; + auto to_handle = column_family_handles_with_ttl[i]; + std::unique_ptr it( + db->NewIterator(rocksdb::ReadOptions(), from_handle)); + for (it->SeekToFirst(); it->Valid(); it->Next()) { + const auto &key = it->key(); + const auto &value = it->value(); + status = + db_with_ttl->Put(rocksdb::WriteOptions(), to_handle, key, value); + if (not status.ok()) { + SL_ERROR(log, "Can't write to ttl database: {}", status.ToString()); + return status_as_error(status, log); + } + } + if (not it->status().ok()) { + SL_ERROR(log, "DB operation failed: {}", status.ToString()); + return status_as_error(it->status(), log); + } + } + defer_db_ttl.reset(); + defer_db.reset(); + fs::remove_all(path, ec); + if (ec) { + SL_ERROR(log, "Can't remove old database in {}: {}", path.native(), ec); + return StorageError::IO_ERROR; + } + fs::create_directories(path, ec); + if (ec) { + SL_ERROR(log, + "Can't create directory {} for final database: {}", + path.native(), + ec); + return StorageError::IO_ERROR; + } + fs::rename(ttl_path, path, ec); + if (ec) { + SL_ERROR(log, + "Can't rename database from {} to {}: {}", + ttl_path.native(), + path.native(), + ec); + return StorageError::IO_ERROR; + } + status = rocksdb::DBWithTTL::Open(options, + path.native(), + column_family_descriptors, + &rocks_db.column_family_handles_, + &rocks_db.db_, + ttls); + if (not status.ok()) { + SL_ERROR(log, + "Can't open database in {}: {}", + path.native(), + status.ToString()); + return status_as_error(status, log); + } + std::ofstream file(ttl_migrated_path.native()); + if (not file) { + SL_ERROR( + log, "Can't create file {} for database", ttl_migrated_path.native()); + return StorageError::IO_ERROR; + } + file.close(); + return outcome::success(); + } + + std::shared_ptr RocksDb::getSpace(Space space) { + if (spaces_.contains(space)) { + return spaces_[space]; + } + auto space_name = spaceName(space); + auto column = std::ranges::find_if( + column_family_handles_, + [&space_name](const ColumnFamilyHandlePtr &handle) { + return handle->GetName() == space_name; + }); + if (column_family_handles_.end() == column) { + throw StorageError::INVALID_ARGUMENT; + } + auto space_ptr = + std::make_shared(weak_from_this(), *column, logger_); + spaces_[space] = space_ptr; + return space_ptr; + } + + void RocksDb::dropColumn(jam::storage::Space space) { + auto space_name = spaceName(space); + auto column_it = std::ranges::find_if( + column_family_handles_, + [&space_name](const ColumnFamilyHandlePtr &handle) { + return handle->GetName() == space_name; + }); + if (column_family_handles_.end() == column_it) { + throw StorageError::INVALID_ARGUMENT; + } + auto &handle = *column_it; + auto e = [this](const rocksdb::Status &status) { + if (!status.ok()) { + logger_->error("DB operation failed: {}", status.ToString()); + throw status_as_error(status, logger_); + } + }; + e(db_->DropColumnFamily(handle)); + e(db_->DestroyColumnFamilyHandle(handle)); + e(db_->CreateColumnFamily({}, std::string(space_name), &handle)); + } + + rocksdb::BlockBasedTableOptions RocksDb::tableOptionsConfiguration( + uint32_t lru_cache_size_mib, uint32_t block_size_kib) { + rocksdb::BlockBasedTableOptions table_options; + table_options.format_version = 5; + table_options.block_cache = rocksdb::NewLRUCache( + static_cast(lru_cache_size_mib * 1024 * 1024)); + table_options.block_size = static_cast(block_size_kib * 1024); + table_options.cache_index_and_filter_blocks = true; + table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(10, false)); + return table_options; + } + + RocksDb::DatabaseGuard::DatabaseGuard( + std::shared_ptr db, + std::vector column_family_handles, + log::Logger log) + : db_(std::move(db)), + column_family_handles_(std::move(column_family_handles)), + log_(std::move(log)) {} + + RocksDb::DatabaseGuard::DatabaseGuard( + std::shared_ptr db_ttl, + std::vector column_family_handles, + log::Logger log) + : db_ttl_(std::move(db_ttl)), + column_family_handles_(std::move(column_family_handles)), + log_(std::move(log)) {} + + RocksDb::DatabaseGuard::~DatabaseGuard() { + const auto clean = [this](auto db) { + auto status = db->Flush(rocksdb::FlushOptions()); + if (not status.ok()) { + SL_ERROR(log_, "Can't flush database: {}", status.ToString()); + } + + status = db->WaitForCompact(rocksdb::WaitForCompactOptions()); + if (not status.ok()) { + SL_ERROR(log_, + "Can't wait for background compaction: {}", + status.ToString()); + } + + for (auto *handle : column_family_handles_) { + db->DestroyColumnFamilyHandle(handle); + } + + status = db->Close(); + if (not status.ok()) { + SL_ERROR(log_, "Can't close database: {}", status.ToString()); + } + db.reset(); + }; + if (db_) { + clean(db_); + } else if (db_ttl_) { + clean(db_ttl_); + } + } + + RocksDbSpace::RocksDbSpace(std::weak_ptr storage, + const RocksDb::ColumnFamilyHandlePtr &column, + log::Logger logger) + : storage_{std::move(storage)}, + column_{column}, + logger_{std::move(logger)} {} + + std::unique_ptr RocksDbSpace::batch() { + return std::make_unique(*this, logger_); + } + + std::optional RocksDbSpace::byteSizeHint() const { + auto rocks = storage_.lock(); + if (!rocks) { + return 0; + } + size_t usage_bytes = 0; + if (rocks->db_) { + std::string usage; + bool result = + rocks->db_->GetProperty("rocksdb.cur-size-all-mem-tables", &usage); + if (result) { + try { + usage_bytes = std::stoul(usage); + } catch (...) { + logger_->error("Unable to parse memory usage value"); + } + } else { + logger_->error("Unable to retrieve memory usage value"); + } + } + return usage_bytes; + } + + std::unique_ptr RocksDbSpace::cursor() { + auto rocks = storage_.lock(); + if (!rocks) { + throw StorageError::STORAGE_GONE; + } + auto it = std::unique_ptr( + rocks->db_->NewIterator(rocks->ro_, column_)); + return std::make_unique(std::move(it)); + } + + outcome::result RocksDbSpace::contains(const ByteView &key) const { + OUTCOME_TRY(rocks, use()); + std::string value; + auto status = rocks->db_->Get(rocks->ro_, column_, make_slice(key), &value); + if (status.ok()) { + return true; + } + + if (status.IsNotFound()) { + return false; + } + + return status_as_error(status, logger_); + } + + outcome::result RocksDbSpace::get(const ByteView &key) const { + OUTCOME_TRY(rocks, use()); + std::string value; + auto status = rocks->db_->Get(rocks->ro_, column_, make_slice(key), &value); + if (status.ok()) { + // cannot move string content to a buffer + return ByteVec( + reinterpret_cast(value.data()), // NOLINT + reinterpret_cast(value.data()) + value.size()); // NOLINT + } + return status_as_error(status, logger_); + } + + outcome::result> RocksDbSpace::tryGet( + const ByteView &key) const { + OUTCOME_TRY(rocks, use()); + std::string value; + auto status = rocks->db_->Get(rocks->ro_, column_, make_slice(key), &value); + if (status.ok()) { + auto buf = ByteVec( + reinterpret_cast(value.data()), // NOLINT + reinterpret_cast(value.data()) + value.size()); // NOLINT + return std::make_optional(ByteVecOrView(std::move(buf))); + } + + if (status.IsNotFound()) { + return std::nullopt; + } + + return status_as_error(status, logger_); + } + + outcome::result RocksDbSpace::put(const ByteView &key, + ByteVecOrView &&value) { + OUTCOME_TRY(rocks, use()); + auto status = rocks->db_->Put( + rocks->wo_, column_, make_slice(key), make_slice(std::move(value))); + if (status.ok()) { + return outcome::success(); + } + + return status_as_error(status,logger_); + } + + outcome::result RocksDbSpace::remove(const ByteView &key) { + OUTCOME_TRY(rocks, use()); + auto status = rocks->db_->Delete(rocks->wo_, column_, make_slice(key)); + if (status.ok()) { + return outcome::success(); + } + + return status_as_error(status, logger_); + } + + void RocksDbSpace::compact(const ByteVec &first, const ByteVec &last) { + auto rocks = storage_.lock(); + if (!rocks) { + return; + } + if (rocks->db_) { + std::unique_ptr begin( + rocks->db_->NewIterator(rocks->ro_, column_)); + first.empty() ? begin->SeekToFirst() : begin->Seek(make_slice(first)); + auto bk = begin->key(); + std::unique_ptr end( + rocks->db_->NewIterator(rocks->ro_, column_)); + last.empty() ? end->SeekToLast() : end->Seek(make_slice(last)); + auto ek = end->key(); + rocksdb::CompactRangeOptions options; + rocks->db_->CompactRange(options, column_, &bk, &ek); + } + } + + outcome::result> RocksDbSpace::use() const { + auto rocks = storage_.lock(); + if (!rocks) { + return StorageError::STORAGE_GONE; + } + return rocks; + } + +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb.hpp b/src/storage/rocksdb/rocksdb.hpp new file mode 100644 index 00000000..e406ce8b --- /dev/null +++ b/src/storage/rocksdb/rocksdb.hpp @@ -0,0 +1,175 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include "log/logger.hpp" +#include "storage/buffer_map_types.hpp" +#include "storage/spaced_storage.hpp" +#include "utils/ctor_limiters.hpp" + +namespace jam::app { + class Configuration; +} + +namespace jam::storage { + + class RocksDb : public SpacedStorage, + public std::enable_shared_from_this, + NonCopyable, + NonMovable { + using ColumnFamilyHandlePtr = rocksdb::ColumnFamilyHandle *; + + public: + RocksDb(qtils::SharedRef logsys, + qtils::SharedRef app_config); + + ~RocksDb() override; + + static constexpr uint32_t kDefaultStateCacheSizeMiB = 512; + static constexpr uint32_t kDefaultLruCacheSizeMiB = 512; + static constexpr uint32_t kDefaultBlockSizeKiB = 32; + + /** + * @brief Factory method to create an instance of RocksDb class. + * @param path filesystem path where database is going to be + * @param options rocksdb options, such as caching, logging, etc. + * @param prevent_destruction - avoid destruction of underlying db if true + * @param memory_budget_mib - state cache size in MiB, 90% would be set for + * trie nodes, and the rest - distributed evenly among left spaces + * @return instance of RocksDB + */ + static outcome::result> create( + log::Logger logger, + const std::filesystem::path &path, + rocksdb::Options options = rocksdb::Options(), + uint32_t memory_budget_mib = kDefaultStateCacheSizeMiB, + bool prevent_destruction = false, + const std::unordered_map &column_ttl = {}, + bool enable_migration = true); + + std::shared_ptr getSpace(Space space) override; + + /** + * Implementation-specific way to erase the whole space data. + * Not exposed at SpacedStorage level as only used in pruner. + * @param space - storage space identifier to clear + */ + void dropColumn(Space space); + + /** + * Prepare configuration structure + * @param lru_cache_size_mib - LRU rocksdb cache in MiB + * @param block_size_kib - internal rocksdb block size in KiB + * @return options structure + */ + static rocksdb::BlockBasedTableOptions tableOptionsConfiguration( + uint32_t lru_cache_size_mib = kDefaultLruCacheSizeMiB, + uint32_t block_size_kib = kDefaultBlockSizeKiB); + + friend class RocksDbSpace; + friend class RocksDbBatch; + + private: + struct DatabaseGuard { + DatabaseGuard( + std::shared_ptr db, + std::vector column_family_handles, + log::Logger log); + + DatabaseGuard( + std::shared_ptr db_ttl, + std::vector column_family_handles, + log::Logger log); + + ~DatabaseGuard(); + + private: + std::shared_ptr db_; + std::shared_ptr db_ttl_; + std::vector column_family_handles_; + log::Logger log_; + }; + + static outcome::result createDirectory( + const std::filesystem::path &absolute_path, log::Logger &log); + + static outcome::result openDatabaseWithTTL( + const rocksdb::Options &options, + const std::filesystem::path &path, + const std::vector + &column_family_descriptors, + const std::vector &ttls, + RocksDb &rocks_db, + const std::filesystem::path &ttl_migrated_path, + log::Logger &log); + + static outcome::result migrateDatabase( + const rocksdb::Options &options, + const std::filesystem::path &path, + const std::vector + &column_family_descriptors, + const std::vector &ttls, + RocksDb &rocks_db, + const std::filesystem::path &ttl_migrated_path, + log::Logger &log); + + rocksdb::DBWithTTL *db_{}; + std::vector column_family_handles_; + boost::container::flat_map> spaces_; + rocksdb::ReadOptions ro_; + rocksdb::WriteOptions wo_; + log::Logger logger_; + }; + + class RocksDbSpace : public BufferStorage { + public: + ~RocksDbSpace() override = default; + + RocksDbSpace(std::weak_ptr storage, + const RocksDb::ColumnFamilyHandlePtr &column, + log::Logger logger); + + std::unique_ptr batch() override; + + std::optional byteSizeHint() const override; + + std::unique_ptr cursor() override; + + outcome::result contains(const ByteView &key) const override; + + outcome::result get(const ByteView &key) const override; + + outcome::result> tryGet( + const ByteView &key) const override; + + outcome::result put(const ByteView &key, + ByteVecOrView &&value) override; + + outcome::result remove(const ByteView &key) override; + + void compact(const ByteVec &first, const ByteVec &last); + + friend class RocksDbBatch; + + private: + // gather storage instance from weak ptr + outcome::result> use() const; + + log::Logger logger_; + std::weak_ptr storage_; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + const RocksDb::ColumnFamilyHandlePtr &column_; + }; +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb_batch.cpp b/src/storage/rocksdb/rocksdb_batch.cpp new file mode 100644 index 00000000..b3b90330 --- /dev/null +++ b/src/storage/rocksdb/rocksdb_batch.cpp @@ -0,0 +1,44 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "storage/rocksdb/rocksdb_batch.hpp" + +#include "storage/rocksdb/rocksdb_util.hpp" +#include "storage/storage_error.hpp" + +namespace jam::storage { + + RocksDbBatch::RocksDbBatch(RocksDbSpace &db, log::Logger &logger) + : db_(db), logger_(logger) {} + + outcome::result RocksDbBatch::put(const ByteView &key, + ByteVecOrView &&value) { + batch_.Put(db_.column_, make_slice(key), make_slice(std::move(value))); + return outcome::success(); + } + + outcome::result RocksDbBatch::remove(const ByteView &key) { + batch_.Delete(db_.column_, make_slice(key)); + return outcome::success(); + } + + outcome::result RocksDbBatch::commit() { + auto rocks = db_.storage_.lock(); + if (!rocks) { + return StorageError::STORAGE_GONE; + } + auto status = rocks->db_->Write(rocks->wo_, &batch_); + if (status.ok()) { + return outcome::success(); + } + + return status_as_error(status, logger_); + } + + void RocksDbBatch::clear() { + batch_.Clear(); + } +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb_batch.hpp b/src/storage/rocksdb/rocksdb_batch.hpp new file mode 100644 index 00000000..25b6814b --- /dev/null +++ b/src/storage/rocksdb/rocksdb_batch.hpp @@ -0,0 +1,36 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "storage/rocksdb/rocksdb.hpp" + +namespace jam::storage { + + class RocksDbBatch : public BufferBatch { + public: + ~RocksDbBatch() override = default; + + RocksDbBatch(RocksDbSpace &db, log::Logger &logger); + + outcome::result commit() override; + + void clear() override; + + outcome::result put(const ByteView &key, + ByteVecOrView &&value) override; + + outcome::result remove(const ByteView &key) override; + + private: + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + RocksDbSpace &db_; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + log::Logger &logger_; + rocksdb::WriteBatch batch_; + }; +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb_cursor.cpp b/src/storage/rocksdb/rocksdb_cursor.cpp new file mode 100644 index 00000000..59dccdf8 --- /dev/null +++ b/src/storage/rocksdb/rocksdb_cursor.cpp @@ -0,0 +1,54 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "storage/rocksdb/rocksdb_cursor.hpp" + +#include "rocksdb_util.hpp" + +namespace jam::storage { + + RocksDBCursor::RocksDBCursor(std::shared_ptr it) + : i_{std::move(it)} {} + + outcome::result RocksDBCursor::seekFirst() { + i_->SeekToFirst(); + return isValid(); + } + + outcome::result RocksDBCursor::seek(const ByteView &key) { + i_->Seek(make_slice(key)); + return isValid(); + } + + outcome::result RocksDBCursor::seekLast() { + i_->SeekToLast(); + return isValid(); + } + + bool RocksDBCursor::isValid() const { + return i_->Valid(); + } + + outcome::result RocksDBCursor::next() { + i_->Next(); + return outcome::success(); + } + + outcome::result RocksDBCursor::prev() { + i_->Prev(); + return outcome::success(); + } + + std::optional RocksDBCursor::key() const { + return isValid() ? std::make_optional(make_buffer(i_->key())) + : std::nullopt; + } + + std::optional RocksDBCursor::value() const { + return isValid() ? std::make_optional(make_buffer(i_->value())) + : std::nullopt; + } +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb_cursor.hpp b/src/storage/rocksdb/rocksdb_cursor.hpp new file mode 100644 index 00000000..95344a83 --- /dev/null +++ b/src/storage/rocksdb/rocksdb_cursor.hpp @@ -0,0 +1,40 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "storage/rocksdb/rocksdb.hpp" + +namespace jam::storage { + + class RocksDBCursor : public BufferStorageCursor { + public: + ~RocksDBCursor() override = default; + + explicit RocksDBCursor(std::shared_ptr it); + + outcome::result seekFirst() override; + + outcome::result seek(const ByteView &key) override; + + outcome::result seekLast() override; + + bool isValid() const override; + + outcome::result next() override; + + outcome::result prev() override; + + std::optional key() const override; + + std::optional value() const override; + + private: + std::shared_ptr i_; + }; + +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb_spaces.cpp b/src/storage/rocksdb/rocksdb_spaces.cpp new file mode 100644 index 00000000..337e13cd --- /dev/null +++ b/src/storage/rocksdb/rocksdb_spaces.cpp @@ -0,0 +1,44 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "storage/rocksdb/rocksdb_spaces.hpp" + +#include +#include +#include + +#include +#include +#include + +namespace jam::storage { + + static constexpr std::string_view kNamesArr[] = { + "" + // Add here names of non-default space + }; + constexpr std::span kNames = kNamesArr; + + // static_assert(kNames.size() == (SpacesCount - 1)); + + std::string_view spaceName(Space space) { + if (space != Space::Default) { + BOOST_ASSERT(space < Space::Total); + return kNames[static_cast(space) - 1]; + } + return rocksdb::kDefaultColumnFamilyName; + } + + std::optional spaceFromString(std::string_view string) { + std::optional space; + const auto it = std::find(std::begin(kNames), std::end(kNames), string); + if (it != std::end(kNames)) { + space.emplace(static_cast(std::distance(std::begin(kNames), it))); + } + return space; + } + +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb_spaces.hpp b/src/storage/rocksdb/rocksdb_spaces.hpp new file mode 100644 index 00000000..3684243b --- /dev/null +++ b/src/storage/rocksdb/rocksdb_spaces.hpp @@ -0,0 +1,25 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "storage/spaces.hpp" + +#include +#include + +namespace jam::storage { + + /** + * Map space item to its string name for Rocks DB needs + * @param space - space identifier + * @return string representation of space name + */ + std::string_view spaceName(Space space); + + std::optional spaceFromString(std::string_view string); + +} // namespace jam::storage diff --git a/src/storage/rocksdb/rocksdb_util.hpp b/src/storage/rocksdb/rocksdb_util.hpp new file mode 100644 index 00000000..a0e36f7a --- /dev/null +++ b/src/storage/rocksdb/rocksdb_util.hpp @@ -0,0 +1,57 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "storage/storage_error.hpp" + +namespace jam::storage { + inline StorageError status_as_error(const rocksdb::Status &s, const log::Logger &log) { + if (s.IsNotFound()) { + return StorageError::NOT_FOUND; + } + + if (s.IsIOError()) { + SL_ERROR(log, ":{}", s.ToString()); + return StorageError::IO_ERROR; + } + + if (s.IsInvalidArgument()) { + return StorageError::INVALID_ARGUMENT; + } + + if (s.IsCorruption()) { + return StorageError::CORRUPTION; + } + + if (s.IsNotSupported()) { + return StorageError::NOT_SUPPORTED; + } + + return StorageError::UNKNOWN; + } + + inline rocksdb::Slice make_slice(const qtils::ByteView &buf) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + const auto *ptr = reinterpret_cast(buf.data()); + size_t n = buf.size(); + return rocksdb::Slice{ptr, n}; + } + + inline ByteView make_span(const rocksdb::Slice &s) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + return {reinterpret_cast(s.data()), s.size()}; + } + + inline qtils::ByteVec make_buffer(const rocksdb::Slice &s) { + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + const auto *ptr = reinterpret_cast(s.data()); + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic) + return {ptr, ptr + s.size()}; + } +} // namespace jam::storage diff --git a/src/storage/spaces.hpp b/src/storage/spaces.hpp index f9929709..dca4f5ec 100644 --- a/src/storage/spaces.hpp +++ b/src/storage/spaces.hpp @@ -1,5 +1,5 @@ /** -* Copyright Quadrivium LLC + * Copyright Quadrivium LLC * All Rights Reserved * SPDX-License-Identifier: Apache-2.0 */ @@ -14,6 +14,7 @@ #pragma once +#include #include namespace jam::storage { @@ -26,14 +27,14 @@ namespace jam::storage { * of the system's storage. The values are used as keys to retrieve * specific BufferStorage instances. */ - enum Space : uint8_t { - Default = 0, ///< Default space used for general-purpose storage - LookupKey, ///< Space used for mapping lookup keys + enum class Space : uint8_t { + Default = 0, ///< Default space used for general-purpose storage // application-defined spaces // ... append here - kTotal ///< Total number of defined spaces (must be last) + Total ///< Total number of defined spaces (must be last) }; -} \ No newline at end of file + constexpr size_t SpacesCount = static_cast(Space::Total); +} diff --git a/vcpkg.json b/vcpkg.json index 43afc63e..7c7f7f11 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,7 +13,8 @@ "boost-asio", "boost-beast", "prometheus-cpp", - "ftxui" + "ftxui", + "rocksdb" ], "features": { "test": { "description": "Test", "dependencies": ["gtest"]} From 0ffecfb2ecfb35b6c70cc1de92c9c24f90c7ff14 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Mon, 26 May 2025 18:07:06 +0300 Subject: [PATCH 06/15] feature: literals for tests --- tests/testutil/literals.hpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tests/testutil/literals.hpp diff --git a/tests/testutil/literals.hpp b/tests/testutil/literals.hpp new file mode 100644 index 00000000..4e92369c --- /dev/null +++ b/tests/testutil/literals.hpp @@ -0,0 +1,25 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#define CREATE_LITERAL(N) \ + consteval qtils::ByteArr operator""_arr##N(const char *c, size_t s) { \ + if (s > N) throw std::invalid_argument("Literal too long for ByteArr"); \ + qtils::ByteArr arr{}; \ + std::copy_n(c, s, arr.begin()); \ + return arr; \ + } + +CREATE_LITERAL(8) +CREATE_LITERAL(16) +CREATE_LITERAL(32) + +#undef CREATE_LITERAL From 968d3b903f1f29a4041fe32008742970d78028a9 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Wed, 21 May 2025 16:43:02 +0300 Subject: [PATCH 07/15] feature: storage tests --- CMakeLists.txt | 13 +- src/app/configuration.hpp | 1 + src/storage/rocksdb/rocksdb.cpp | 2 +- tests/CMakeLists.txt | 38 +++++- tests/mock/app/configuration_mock.hpp | 29 ++++ tests/testutil/CMakeLists.txt | 12 ++ tests/testutil/prepare_loggers.hpp | 72 ++++++++++ tests/testutil/storage/CMakeLists.txt | 28 ++++ tests/testutil/storage/base_fs_test.cpp | 53 ++++++++ tests/testutil/storage/base_fs_test.hpp | 48 +++++++ tests/testutil/storage/base_rocksdb_test.cpp | 46 +++++++ tests/testutil/storage/base_rocksdb_test.hpp | 35 +++++ tests/unit/CMakeLists.txt | 7 + tests/unit/storage/CMakeLists.txt | 7 + tests/unit/storage/rocksdb/CMakeLists.txt | 23 ++++ .../unit/storage/rocksdb/rocksdb_fs_test.cpp | 78 +++++++++++ .../rocksdb/rocksdb_integration_test.cpp | 128 ++++++++++++++++++ 17 files changed, 613 insertions(+), 7 deletions(-) create mode 100644 tests/mock/app/configuration_mock.hpp create mode 100644 tests/testutil/CMakeLists.txt create mode 100644 tests/testutil/prepare_loggers.hpp create mode 100644 tests/testutil/storage/CMakeLists.txt create mode 100644 tests/testutil/storage/base_fs_test.cpp create mode 100644 tests/testutil/storage/base_fs_test.hpp create mode 100644 tests/testutil/storage/base_rocksdb_test.cpp create mode 100644 tests/testutil/storage/base_rocksdb_test.hpp create mode 100644 tests/unit/CMakeLists.txt create mode 100644 tests/unit/storage/CMakeLists.txt create mode 100644 tests/unit/storage/rocksdb/CMakeLists.txt create mode 100644 tests/unit/storage/rocksdb/rocksdb_fs_test.cpp create mode 100644 tests/unit/storage/rocksdb/rocksdb_integration_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 31a6fd32..27135bc3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API ON) project(cpp-jam VERSION 0.0.1 - LANGUAGES CXX + LANGUAGES CXX C ) if(DEFINED CMAKE_TOOLCHAIN_FILE AND CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg") @@ -75,6 +75,7 @@ find_package(soralog CONFIG REQUIRED) find_package(Boost.DI CONFIG REQUIRED) find_package(qtils CONFIG REQUIRED) find_package(prometheus-cpp CONFIG REQUIRED) +find_package(RocksDB CONFIG REQUIRED) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") add_compile_options(-fmodules-ts) @@ -84,10 +85,18 @@ endif() add_library(headers INTERFACE) target_include_directories(headers INTERFACE - $ + $ + $ + $ $ ) +include_directories(${CMAKE_SOURCE_DIR}) +include_directories(${CMAKE_SOURCE_DIR}/src) +include_directories(${CMAKE_SOURCE_DIR}/src/third_party) +include_directories(${CMAKE_SOURCE_DIR}/src/_TODO) +include_directories(${CMAKE_BINARY_DIR}/generated) + add_subdirectory(src) if (TESTING) diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index c28b647d..8f10b8e7 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include diff --git a/src/storage/rocksdb/rocksdb.cpp b/src/storage/rocksdb/rocksdb.cpp index 4fbf5c96..49ab15c8 100644 --- a/src/storage/rocksdb/rocksdb.cpp +++ b/src/storage/rocksdb/rocksdb.cpp @@ -609,7 +609,7 @@ namespace jam::storage { return outcome::success(); } - return status_as_error(status,logger_); + return status_as_error(status, logger_); } outcome::result RocksDbSpace::remove(const ByteView &key) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 36d2cad2..ec5f20dd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,9 +4,39 @@ # SPDX-License-Identifier: Apache-2.0 # +function(addtest test_name) + add_executable(${test_name} ${ARGN}) + addtest_part(${test_name} ${ARGN}) + + target_link_libraries(${test_name} + GTest::gmock_main + ) + file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/xunit) + set(xml_output "--gtest_output=xml:${CMAKE_BINARY_DIR}/xunit/xunit-${test_name}.xml") + add_test( + NAME ${test_name} + COMMAND $ ${xml_output} "--output-on-failure" + ) + set_target_properties(${test_name} PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/test_bin + ARCHIVE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/test_lib + LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/test_lib + ) +endfunction() + +function(addtest_part test_name) + if (POLICY CMP0076) + cmake_policy(SET CMP0076 NEW) + endif () + target_sources(${test_name} PUBLIC + ${ARGN} + ) +endfunction() + include_directories( - ${CMAKE_CURRENT_SOURCE_DIR} - ${PROJECT_SOURCE_DIR}/src - ) + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/tests +) -# add_subdirectory(utils) +add_subdirectory(testutil) +add_subdirectory(unit) \ No newline at end of file diff --git a/tests/mock/app/configuration_mock.hpp b/tests/mock/app/configuration_mock.hpp new file mode 100644 index 00000000..757661dd --- /dev/null +++ b/tests/mock/app/configuration_mock.hpp @@ -0,0 +1,29 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + + +#pragma once + +#include + +#include "app/configuration.hpp" + +namespace jam::app { + + class ConfigurationMock : public Configuration { + public: + MOCK_METHOD(const std::string&, nodeVersion, (), (const, override)); + MOCK_METHOD(const std::string&, nodeName, (), (const, override)); + MOCK_METHOD(const std::filesystem::path&, basePath, (), (const, override)); + MOCK_METHOD(const std::filesystem::path&, specFile, (), (const, override)); + MOCK_METHOD(const std::filesystem::path&, modulesDir, (), (const, override)); + + MOCK_METHOD(const DatabaseConfig &, database, (), (const, override)); + + MOCK_METHOD(const MetricsConfig &, metrics, (), (const, override)); + }; + +} // namespace jam::app diff --git a/tests/testutil/CMakeLists.txt b/tests/testutil/CMakeLists.txt new file mode 100644 index 00000000..d9c44e1a --- /dev/null +++ b/tests/testutil/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(logger_for_tests INTERFACE) +target_link_libraries(logger_for_tests INTERFACE + logger + ) + +add_subdirectory(storage) diff --git a/tests/testutil/prepare_loggers.hpp b/tests/testutil/prepare_loggers.hpp new file mode 100644 index 00000000..6aba19c3 --- /dev/null +++ b/tests/testutil/prepare_loggers.hpp @@ -0,0 +1,72 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +// #include + +// #include +#include + +namespace testutil { + + // supposed to be called in SetUpTestCase + inline qtils::SharedRef prepareLoggers( + soralog::Level level = soralog::Level::INFO) { + static qtils::SharedRef logging_system = ({ + auto testing_log_config = std::string(R"( +sinks: + - name: console + type: console + capacity: 4 + latency: 0 +groups: + - name: main + sink: console + level: info + is_fallback: true + children: + - name: testing + level: trace + - name: libp2p + level: off +)"); + + // Setup logging system + auto log_config = YAML::Load(testing_log_config); + if (not log_config.IsDefined()) { + throw std::runtime_error("Log config is not defined"); + } + + auto log_configurator = std::make_shared( + std::shared_ptr(nullptr), log_config); + + auto logging_system_ = + std::make_shared(std::move(log_configurator)); + + auto config_result = logging_system_->configure(); + if (not config_result.message.empty()) { + (config_result.has_error ? std::cerr : std::cout) + << config_result.message << '\n'; + } + if (config_result.has_error) { + throw std::runtime_error("Cannot configure logging"); + } + + std::make_shared(std::move(logging_system_)); + }); + + std::ignore = + logging_system->setLevelOfGroup(jam::log::defaultGroupName, level); + + return logging_system; + } + +} // namespace testutil diff --git a/tests/testutil/storage/CMakeLists.txt b/tests/testutil/storage/CMakeLists.txt new file mode 100644 index 00000000..1e8e0d77 --- /dev/null +++ b/tests/testutil/storage/CMakeLists.txt @@ -0,0 +1,28 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_library(base_fs_test + base_fs_test.hpp + base_fs_test.cpp + ) +target_link_libraries(base_fs_test + GTest::gtest + app_configuration +) + +add_library(base_rocksdb_test + base_rocksdb_test.hpp + base_rocksdb_test.cpp + ) +target_link_libraries(base_rocksdb_test + base_fs_test + ) + +add_library(std_list_adapter INTERFACE) + +target_link_libraries(std_list_adapter INTERFACE + outcome + ) diff --git a/tests/testutil/storage/base_fs_test.cpp b/tests/testutil/storage/base_fs_test.cpp new file mode 100644 index 00000000..5d1f151c --- /dev/null +++ b/tests/testutil/storage/base_fs_test.cpp @@ -0,0 +1,53 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "testutil/storage/base_fs_test.hpp" + +#include "log/logger.hpp" +#include "testutil/prepare_loggers.hpp" + +namespace test { + + void BaseFS_Test::clear() { + if (fs::exists(base_path)) { + fs::remove_all(base_path); + } + } + + void BaseFS_Test::mkdir() { + fs::create_directory(base_path); + } + + std::string BaseFS_Test::getPathString() const { + return fs::canonical(base_path).string(); + } + + BaseFS_Test::~BaseFS_Test() { + clear(); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + BaseFS_Test::BaseFS_Test(fs::path path) + : logger(testutil::prepareLoggers()->createLogger( + fs::weakly_canonical(path).string(), + "testing", + jam::log::Level::DEBUG)), + base_path(std::move(path)) { + clear(); + mkdir(); + } +#pragma GCC diagnostic pop + + void BaseFS_Test::SetUp() { + clear(); + mkdir(); + } + + void BaseFS_Test::TearDown() { + clear(); + } +} // namespace test diff --git a/tests/testutil/storage/base_fs_test.hpp b/tests/testutil/storage/base_fs_test.hpp new file mode 100644 index 00000000..8ac4f547 --- /dev/null +++ b/tests/testutil/storage/base_fs_test.hpp @@ -0,0 +1,48 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "log/logger.hpp" +#include "testutil/prepare_loggers.hpp" + +// intentionally here, so users can use fs shortcut +namespace fs = std::filesystem; + +namespace test { + + /** + * @brief Base test, which involves filesystem. Can be created with given + * path. Clears path before test and after test. + */ + struct BaseFS_Test : public ::testing::Test { + // not explicit, intentionally + BaseFS_Test(fs::path path); + + void clear(); + + void mkdir(); + + std::string getPathString() const; + + ~BaseFS_Test() override; + + void TearDown() override; + + void SetUp() override; + + static void SetUpTestCase() { + testutil::prepareLoggers(); + } + + protected: + jam::log::Logger logger; + fs::path base_path; + }; + +} // namespace test diff --git a/tests/testutil/storage/base_rocksdb_test.cpp b/tests/testutil/storage/base_rocksdb_test.cpp new file mode 100644 index 00000000..0ae3e7e0 --- /dev/null +++ b/tests/testutil/storage/base_rocksdb_test.cpp @@ -0,0 +1,46 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "testutil/storage/base_rocksdb_test.hpp" + +namespace test { + + void BaseRocksDB_Test::open() { + rocksdb::Options options; + options.create_if_missing = true; + + rocks_.reset(); + ASSERT_NO_THROW( + rocks_ = std::make_shared(logsys, app_config)); + + db_ = rocks_->getSpace(jam::storage::Space::Default); + ASSERT_TRUE(db_) << "BaseRocksDB_Test: db is nullptr"; + } + + BaseRocksDB_Test::BaseRocksDB_Test(fs::path path) + : BaseFS_Test(std::move(path)) {} + + void BaseRocksDB_Test::SetUp() { + logsys = testutil::prepareLoggers(); + app_config = std::make_shared(); + + jam::app::Configuration::DatabaseConfig db_config{ + .directory = getPathString() + "/db", + .cache_size = 8 << 20, // 8Mb + .migration_enabled = false}; + + EXPECT_CALL(*app_config, database()) + .WillRepeatedly(testing::ReturnRef(db_config)); + + open(); + } + + void BaseRocksDB_Test::TearDown() { + app_config.reset(); + clear(); + } + +} // namespace test diff --git a/tests/testutil/storage/base_rocksdb_test.hpp b/tests/testutil/storage/base_rocksdb_test.hpp new file mode 100644 index 00000000..e973e943 --- /dev/null +++ b/tests/testutil/storage/base_rocksdb_test.hpp @@ -0,0 +1,35 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "mock/app/configuration_mock.hpp" +#include "storage/rocksdb/rocksdb.hpp" +#include "testutil/storage/base_fs_test.hpp" + +namespace test { + + struct BaseRocksDB_Test : public BaseFS_Test { + using RocksDB = jam::storage::RocksDb; + using Buffer = qtils::ByteVec; + using BufferView = qtils::ByteView; + + BaseRocksDB_Test(fs::path path); + + void open(); + + void SetUp() override; + + void TearDown() override; + + std::shared_ptr logsys; + std::shared_ptr app_config; + + std::shared_ptr rocks_; + std::shared_ptr db_; + }; + +} // namespace test diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..93406f4b --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,7 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_subdirectory(storage) diff --git a/tests/unit/storage/CMakeLists.txt b/tests/unit/storage/CMakeLists.txt new file mode 100644 index 00000000..035899a8 --- /dev/null +++ b/tests/unit/storage/CMakeLists.txt @@ -0,0 +1,7 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +add_subdirectory(rocksdb) diff --git a/tests/unit/storage/rocksdb/CMakeLists.txt b/tests/unit/storage/rocksdb/CMakeLists.txt new file mode 100644 index 00000000..ab800bd1 --- /dev/null +++ b/tests/unit/storage/rocksdb/CMakeLists.txt @@ -0,0 +1,23 @@ +# +# Copyright Quadrivium LLC +# All Rights Reserved +# SPDX-License-Identifier: Apache-2.0 +# + +addtest(rocksdb_fs_test + rocksdb_fs_test.cpp +) +target_link_libraries(rocksdb_fs_test + storage + base_fs_test + logger_for_tests +) + +addtest(rocksdb_integration_test + rocksdb_integration_test.cpp +) +target_link_libraries(rocksdb_integration_test + storage + base_rocksdb_test + logger_for_tests +) diff --git a/tests/unit/storage/rocksdb/rocksdb_fs_test.cpp b/tests/unit/storage/rocksdb/rocksdb_fs_test.cpp new file mode 100644 index 00000000..6331f3b6 --- /dev/null +++ b/tests/unit/storage/rocksdb/rocksdb_fs_test.cpp @@ -0,0 +1,78 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include + +#include "app/configuration.hpp" +#include "storage/rocksdb/rocksdb.hpp" +#include "storage/storage_error.hpp" +#include "testutil/prepare_loggers.hpp" +#include "testutil/storage/base_fs_test.hpp" + +using jam::app::ConfigurationMock; +using jam::log::LoggingSystem; +using jam::storage::RocksDb; +using DatabaseConfig = jam::app::Configuration::DatabaseConfig; +using namespace testing; + +struct RocksDb_Open : public test::BaseFS_Test { + RocksDb_Open() : test::BaseFS_Test("/tmp/jam-test-rocksdb-open") {} + + void SetUp() override { + BaseFS_Test::SetUp(); + + logsys = testutil::prepareLoggers(); + app_config = std::make_shared(); + + DatabaseConfig db_config{.directory = getPathString() + "/db", + .cache_size = 8 << 20, // 8Mb + .migration_enabled = false}; + + EXPECT_CALL(*app_config, database()).WillRepeatedly(ReturnRef(db_config)); + }; + + void TearDown() override { + app_config.reset(); + BaseFS_Test::TearDown(); + } + + std::shared_ptr logsys; + std::shared_ptr app_config; +}; + +/** + * @given options with the disabled option `create_if_missing` + * @when open database + * @then database can not be opened (since there is no db already) + */ +TEST_F(RocksDb_Open, OpenNonExistingDB) { + DatabaseConfig db_config{.directory = "/dev/zero/impossible/path", + .cache_size = 8 << 20, // 8Mb + .migration_enabled = false}; + + EXPECT_CALL(*app_config, database()).WillRepeatedly(ReturnRef(db_config)); + + ASSERT_THROW_OUTCOME(RocksDb(logsys, app_config), std::errc::not_a_directory); +} + +/** + * @given options with enable option `create_if_missing` + * @when open database + * @then database is opened + */ +TEST_F(RocksDb_Open, OpenExistingDB) { + DatabaseConfig db_config{.directory = getPathString() + "/db", + .cache_size = 8 << 20, // 8Mb + .migration_enabled = false}; + + EXPECT_CALL(*app_config, database()).WillRepeatedly(ReturnRef(db_config)); + + ASSERT_NO_THROW(RocksDb(logsys, app_config)); +} diff --git a/tests/unit/storage/rocksdb/rocksdb_integration_test.cpp b/tests/unit/storage/rocksdb/rocksdb_integration_test.cpp new file mode 100644 index 00000000..e9953b2e --- /dev/null +++ b/tests/unit/storage/rocksdb/rocksdb_integration_test.cpp @@ -0,0 +1,128 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "testutil/storage/base_rocksdb_test.hpp" + +#include + +#include +#include + +#include + +// #include "filesystem/common.hpp" +#include "storage/storage_error.hpp" +#include "storage/rocksdb/rocksdb.hpp" +// #include "testutil/prepare_loggers.hpp" + +using namespace jam::storage; +namespace fs = std::filesystem; + +struct RocksDb_Integration_Test : public test::BaseRocksDB_Test { + RocksDb_Integration_Test() + : test::BaseRocksDB_Test("/tmp/kagome_rocksdb_integration_test") {} + + Buffer key_{1, 3, 3, 7}; + Buffer value_{1, 2, 3}; +}; + +/** + * @given opened database, with {key} + * @when read {key} + * @then {value} is correct + */ +TEST_F(RocksDb_Integration_Test, Put_Get) { + ASSERT_OUTCOME_SUCCESS(db_->put(key_, BufferView{value_})); + ASSERT_OUTCOME_SUCCESS(contains, db_->contains(key_)); + EXPECT_TRUE(contains); + ASSERT_OUTCOME_SUCCESS(val, db_->get(key_)); + EXPECT_EQ(val, value_); +} + +/** + * @given empty db + * @when read {key} + * @then get "not found" + */ +TEST_F(RocksDb_Integration_Test, Get_NonExistent) { + ASSERT_OUTCOME_SUCCESS(contains, db_->contains(key_)); + EXPECT_FALSE(contains); + ASSERT_OUTCOME_SUCCESS(db_->remove(key_)); + ASSERT_OUTCOME_ERROR(db_->get(key_), StorageError::NOT_FOUND); +} + +/** + * @given database with [(i,i) for i in range(6)] + * @when create batch and write KVs + * @then data is written only after commit + */ +TEST_F(RocksDb_Integration_Test, WriteBatch) { + std::list keys{{0}, {1}, {2}, {3}, {4}, {5}}; + Buffer toBeRemoved = {3}; + std::list expected{{0}, {1}, {2}, {4}, {5}}; + + auto batch = db_->batch(); + ASSERT_TRUE(batch); + + for (const auto &item : keys) { + ASSERT_OUTCOME_SUCCESS(batch->put(item, BufferView{item})); + ASSERT_OUTCOME_SUCCESS(contains, db_->contains(item)); + EXPECT_FALSE(contains); + } + ASSERT_OUTCOME_SUCCESS(batch->remove(toBeRemoved)); + ASSERT_OUTCOME_SUCCESS(batch->commit()); + + for (const auto &item : expected) { + ASSERT_OUTCOME_SUCCESS(contains, db_->contains(item)); + EXPECT_TRUE(contains); + ASSERT_OUTCOME_SUCCESS(val, db_->get(item)); + EXPECT_EQ(val, item); + } + + ASSERT_OUTCOME_SUCCESS(contains, db_->contains(toBeRemoved)); + EXPECT_FALSE(contains); +} + +/** + * @given database with [(i,i) for i in range(100)] + * @when iterate over kv pairs forward and backward + * @then we iterate over all items + */ +TEST_F(RocksDb_Integration_Test, Iterator) { + const size_t size = 100; + // 100 buffers of size 1 each; 0..99 + std::list keys; + for (size_t i = 0; i < size; i++) { + keys.emplace_back(1, i); + } + + for (const auto &item : keys) { + ASSERT_OUTCOME_SUCCESS(db_->put(item, BufferView{item})); + } + + std::array counter{}; + + logger->warn("forward iteration"); + auto it = db_->cursor(); + ASSERT_OUTCOME_SUCCESS(it->seekFirst()); + for (; it->isValid(); it->next().assume_value()) { + auto k = it->key().value(); + auto v = it->value().value(); + EXPECT_EQ(k, v); + + logger->info("key: {}, value: {}", k.toHex(), v.view().toHex()); + + EXPECT_GE(k[0], 0); + EXPECT_LT(k[0], size); + EXPECT_GT(k.size(), 0); + + counter[k[0]]++; + } + + for (size_t i = 0; i < size; i++) { + EXPECT_EQ(counter[i], 1); + } +} From e152d0598a97e0c568d2a5e1a23db07416c342a3 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Fri, 6 Jun 2025 12:55:34 +0300 Subject: [PATCH 08/15] feature: target all_tests --- tests/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ec5f20dd..04252860 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -4,6 +4,10 @@ # SPDX-License-Identifier: Apache-2.0 # +if (NOT TARGET all_tests) + add_custom_target(all_tests) +endif () + function(addtest test_name) add_executable(${test_name} ${ARGN}) addtest_part(${test_name} ${ARGN}) @@ -22,6 +26,8 @@ function(addtest test_name) ARCHIVE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/test_lib LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR}/test_lib ) + + add_dependencies(all_tests ${test_name}) endfunction() function(addtest_part test_name) From f8a2997516e4efda340e6d6f606d97bad35a0ed1 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Fri, 6 Jun 2025 01:02:19 +0300 Subject: [PATCH 09/15] feature: chain spec --- CMakeLists.txt | 2 +- example/config.yaml | 1 + example/jamduna-spec.json | 34 +++++++++++++ src/app/CMakeLists.txt | 7 +++ src/app/chain_spec.hpp | 38 ++++++++++++++ src/app/configuration.cpp | 4 ++ src/app/configuration.hpp | 3 +- src/app/configurator.cpp | 23 +++++++++ src/app/impl/chain_spec_impl.cpp | 87 ++++++++++++++++++++++++++++++++ src/app/impl/chain_spec_impl.hpp | 80 +++++++++++++++++++++++++++++ src/injector/CMakeLists.txt | 1 + src/injector/node_injector.cpp | 4 +- vcpkg.json | 3 +- 13 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 example/jamduna-spec.json create mode 100644 src/app/chain_spec.hpp create mode 100644 src/app/impl/chain_spec_impl.cpp create mode 100644 src/app/impl/chain_spec_impl.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 27135bc3..ddac40dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,7 +66,7 @@ find_package(Python3 REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(libb2 REQUIRED IMPORTED_TARGET GLOBAL libb2) -find_package(Boost CONFIG REQUIRED COMPONENTS algorithm outcome program_options) +find_package(Boost CONFIG REQUIRED COMPONENTS algorithm outcome program_options property_tree) find_package(fmt CONFIG REQUIRED) find_package(yaml-cpp CONFIG REQUIRED) find_package(qdrvm-crates CONFIG REQUIRED) diff --git a/example/config.yaml b/example/config.yaml index 2795ca41..327ac307 100644 --- a/example/config.yaml +++ b/example/config.yaml @@ -2,6 +2,7 @@ general: name: NameFromConfig base_path: /tmp/jam_node modules_dir: modules + spec_file: jamduna-spec.json database: directory: db diff --git a/example/jamduna-spec.json b/example/jamduna-spec.json new file mode 100644 index 00000000..cf7c2ab6 --- /dev/null +++ b/example/jamduna-spec.json @@ -0,0 +1,34 @@ +{ + "bootnodes": [ + "e6srwvvha36ldoqzzcufiq7fevkxlqub6y2ll5wyzgnaeanvrwiha@127.0.0.1:40000", + "e5rxuvijvsfmre4dnolttxyjfa4yncnfcm2r7dkuxsnjfzzm7gdub@127.0.0.1:40001", + "ek5kkz37kij5alih6gmvustxi2vafc6pb3cvnvknj6pum4echyhpb@127.0.0.1:40002", + "e7lyaziunridjg5athudtcg5zclntartcjjgddvc377fjgmido2ma@127.0.0.1:40003", + "eikzzpoh4oycigw3jlhut5qqd3dl7styzucpn237opt55pulnt5yb@127.0.0.1:40004", + "erx3gfjrqky6per3avfs7jl5bgjegimvkpwl4g5g2mnsfffiozkha@127.0.0.1:40005" + ], + "id": "dev", + "genesis_header": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da293d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e948e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260fafff16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea020000ffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "genesis_state": { + "00b000040000000040641f50717f10e73c8fd443128f04e3abe2f4e0a94f2b": "0100000000", + "00b60011000000006dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf0": "0100000000", + "00fe00ff00ff00ff571677cde4d330fd3cd1d6d32ecbab502c92924433297a": "09626f6f74737472617042010008000011000000016e756c6c5f617574686f72697a6572206761732063616c6c20206761735f726573756c743a2000000000010000000000190000000000000019000100000000000d000000000000001e0000000000000018000000000000000800000000000000200000000000000026000000000000002e00000000000000307830303031303230333034303530363037303830393130313131323133313431353136313731383139323032313232323332343235323632373238323933303331333233333334333533363337333833393430343134323433343434353436343734383439353035313532353335343535353635373538353936303631363236333634363536363637363836393730373137323733373437353736373737383739383038313832383338343835383638373838383939303931393239333934393539363937393839394b598638d6c56d345310000027028e390300bc00c7007301e201fa0169028102f3020b039a033c046005320656068c06b606db06e2060c07c207e807ef07a408c7098a0ade0aff0a420b6e0b830bac0bc40b1e0cd30c6c0dcb0dda0d1e0e287e01951178ff7b108000648280883484891052092884882052085281778979859801977a2098aa209a77da9a0764298210800095118800287008817a3308810095177f33035701648b8bac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce22830817a3308810095177f33033701648b8bac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce295b7fe562780002a9a8c8100641bc87b0b3309780001330801330a0264275010041e098210800095118800320000951178ff7b108000648280883484891052091b84882052084582773308016429821080009511880028b907827a3308810095177f33035701648b98ac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce22830827a3308810095177f33033701648b98ac0484aa0f88a80a330930da8309bea90978799577ff95b8ff64ca520ce295b7fe562780002a9a8c8100641bc87b0b3309780001330801330a02642750100867088210800095118800320000951150ff7b10a8007b15a0007b169800491118fffffe4811240a7b17289517247b17783307027b17107b1780009517287b1788003307067b17087b17900033072800017b177b1748491150024911689518787b18584911600295173095184850100ab103821538821b4082163033070233083309645a33000c0a6401821718c8670753370000ff2533071000039577000001018278c98709ab590fc9680901827aab8af27b7901481124010a7b17289517247b17788217107b1780009517287b1788008217087b17900082177b1748491150024911689518787b18584911600295173095184850100e2a03821538821b4082163033070233083309645a3300100a6401821718c8670753370000ff2533071000039577000001018278c98709ab590fc9680901827aab8af27b79013305027a15240a7b17289517247b17783307027b1780009517287b1788003307067b17900033072800017b17487b15504911689517787b17587b1560951730951848501012a002821538821b4082163033070233083309645a3300140a6401821718c8670753370000ff2733071000039577000001018278c98709ab5911c9680901827aab8af27b79330b01330700000133088210a8008215a000821698009511b00032009511b87b10407b15387b1630c88909ac895264758277977801330a08e4a909e498068568ff98883f5107128259087b1918491120017b17282806491120016417951a18646950101632821882170851081414080100000000000080aa870d520718007b57087b56018210408215388216309511483200009511c87b10307b15287b16206495647251087982a70851077c82a9105109aa0082a833061000033303000001c8360601826ac85a07aca7cf00957b0188bc01d8b30bd4cb0b520bbf00826bababe57b67330c7b1218330a100003330b0000017b1ac8ba0ac97a077b17107b18086496501018bb035436000001980082121882181033077b280828b50049120833070128ac0033091000033307000001c8790901829ac85a08aca85f958b0188bc01d8b70bd4cb0b520b50829bababe77b98330c01ababdf283133091000033307000001c8790901829ac85a08aca82b958b0188bc01d8b70bd4cb0b520b1c829bababe77b98330c013307100004c9870833077b280828393307017b27082831646c82179577000001821218821608018278c98709ab6911c9c80901827aab8af27b79330b0182181033077b2808017b25107b2782103082152882162095113832009511b87b10407b15387b163095880151085364758277977901330a08e4a808e489068568ff98883f5107128259087b1918491120017b17282806491120016417951a18646950101a6dfe821882170851081414080100000000000080aa870d520718007b57087b56018210408215388216309511483200009511c87b10307b15287b1620828a086475510a3582893307959b080182bcc8c70795aaff95bb10520af5828a18510a248299088e99887a1085aa01d4a909897ae0a90952090a3307330901284a977701015107405707736452330b1000033309000001c89b0b0182bcc87c0aacca5995a501885601d85905d4650552054a82b6abc6e77bba3309100004c9a90964252806330901017b177b1908491110641750101c290652072282171082180882197b57107b58087b598210308215288216209511383200000000827233090000ff952afffffeac9a288277083309100003959900000101829ac9a90bab7b12c92a0b01829cabacf27b9b3308320032009511e07b10187b15107b16086475827a108277c9a7076496ac972a7b1a825708c8a707646950102241018217c876067b56103307821018821510821608951120320064577b1864a864695010247bfc8218825a1028c79511d87b10207b15187b16106486838833098000647aae982b82a81082a7ab781264a764a55010280ffe645a8258100182a708c8870778769588017ba81028ab008b670b48110c52071c9867068677c00078170c84673f8677800078170d33060228638b671052072998670c8677e00078170c97673498773a9577800078170d84673f8677800078170e330603283797672b98773d9577f00078170c97672e98773a9577800078170d97673498773a9577800078170e84673f8677800078170f3306040182a51082a7c95707ac672d7b1a82a708c8570795180c646950102a34c8560682177b7610013307821020821518821610951128320064a76458646964a550102c6efb645a82551028c42867049511e07b10187b15107b1608531910648d7a84a207c8270b510215648c6475017cca785a95550195cc01acb5f6c82803c929028424f8843807c84b0a51083b58044b973803848038843cf882c58d8884883895cc080182c6d00505cf8609d459097bb995bb0895cc086465acabeb281c647a520921282f58041364380182897bb995bb08958808acabf6c84308842907510914c8a909017c8b78ab95aa01958801ac9af68210188215108216089511203200827701289511a07b10587b15507b16487b19087b18987a04330927532a71028100330927951b463a080000037b1818491110f0d80033037b1433057a00013300ffe0f505016474821718d6740798770b821810c08702c8420a97a230982832ca3808988c119888108488fe0796cc9cc8ca0a97aa3198aa30c858087c8c7c8801c85a0a7ca6017caa78b8ff78bcfe78b60178ba9599fc95bbfcac40ac5417633b97783098883296887b14988811968a9cc8a7079777319877309599fe330a7a0001c8a7077c7a017c77951b21c89b0b78ba0178b764870155170a149599ff951821c89808837730788728219777019599fe33087a0001c887077c78017c77951a21c89a0a78a80178a701951b21c89b0b9a9c2733097800018217088218330a5010321282105882155082164895116032009511987b10607b15587b16507b1b1851084f807634846801330b2b3305000011db8b057b1528c8c8028468045108427b19303308510a1a64a5017d9b89bbc085bb01c8b8089555ff9599015205efc882027b1a3882787b1c20520823284181763495c2014911282d8468045208c44911307b1a3882787b1c20510824827508ae521e8466085206507c7938c9250a5a1901a1005209ae007b1a1028ad0082752082762864576468821928821a30821b385010346d013308015207bc00826c186457821818821920821060821558821650951168320c7b12408178307b18087c79387b19827920827628481730307b1710461738017b194864976468821928821a30821b385010361901520767821740c97505955501019555ff5105c600826920330830821748b49001385107ec28435219021298a90195a8019888017b1810280864a9491110018278207b18488276288177307b1740959501019555ff510527826920821748821840b490013a5107ed330801016487821060821558821650951168320082154864576468821928821a30821b3850103c89003308015207d8826a186457821818821920b4a0013e3308015207c38217109a777b17383305ff01821738c857075117ff42826920821748821840b49001409555015107e62837826a18821748821818821920b4a0014233080152078233088217108219087a79308219787938286fff821510821710d875082863ff821710d87508285aff9511d87b10207b15187b1610839c330200001164a66485647aaa2c20825c2064a764987b1b087b1ab4c00144821a821b08647833070152082051061a825c1864a7646864b9821020821518821610951128320c3307018210208215188216109511283200951178ff7b1080007b15787b1670491160203309037819684911304911408286207b175033074800017b17585106060182872851077301330982857b18088288107b18189578ff9788039888039588017b189555089678389566183307307b1710017b18207b19288259510917821a588217508258f882aa18b4a0014652074e018167107a17607c67187817688167147a17648268f8826751082652180116977704821818c88707827808821910aa98073308280f827782773308012806330801017b18307b17388268e88267f051082652180116977704821818c88707827808821910aa98073308280f827782773308012806330801017b18407b1748826708977704821818c878088287828908951830b49001485207b5009555108218209588c8956638821928959901520836ff2869828718510771828510330a977904c859097b19207b180882869577ff9777049877049577017b17956608017b1a288269510916821a588217508268f882aa18b4a0014a5207568257825908951830b490014c520747956610955510821a2895aa01821720ab75c7821808821a828708ac7a0d282c330a828708540725828797a804c88709821a58821750829882990882aa18b4a0014e51070833070128053307018210800082157882167095118800320089284952494a112992254952522a922549525212a284a410d1889224a58814c99224292915c99224292909514252886844449049924824114a2489242449952641084bb234264924122592441292a44a93208425599a4c9248244292244948922a4d82109664a9a188889224a924499244ca529404904c93a44952491285902592244a258492a410484a9284902549925253085992242935a49454215992a5a64c495292249524499248590a250124d3246992a4549624494a5252caa410b22449520aa92c29242929497a2124212cc9529524a5949242494952552825494549a950262549924822294924892492924812492289644a4a52a42c49aa0aa5244994a45692244992a49225292995d24a92242b4992d449929220090922044b9224491249929294a424914492242129498d24a92424256992908a2429499224214992d29224494a4a22494a922409912495242452922425494924524942924c24891449529924c92449244b92524942929248523289249194249194a42449922449a14a2a452949aa24a94952441249928444a2922449499249499288244952224992aa9449924892a42a6592242511499244494a9292a44c4992484924499294545292944452238900", + "00fe00ff00ff00ffca4b52ff29ea7c74d6bd301d1cae77ef7f24c8d294c847": "09626f6f747374726170000000000000010000000197040000060284067900300196010902a402e702280d00000028ae0000002803039511e07b10187b15107b1608648664783309043307000001ac967c9566fc5106769587047d7833050159083a8489ff003305025329c0002d3305035329e000253305045329f0001d3305055329f800153305065329fc000d8898fe009a850801ac564564587b17501002f4026478e45607c95707d88709e48707c98707887720d479098217c8750594983307000001da95072805330801821018821510821608951120320000951150ff7b10a8007b15a0007b169800330908ac985c013309fcaa973c0251073802958af8957508510a457d583306015908408489ff003306025329c0002d3306035329e000253306045329f0001d3306055329f800153306065329fc000d8898fe009a860801ae6a092805013306017b166457646864a65010043d02821a5107f000e4a607c9a707531760e600c85a089576a09587607b1751064c7d783305015908378489ff003305025329c0002d3305035329e000253305045329f0001d3305055329f800153305065329fc000d8898fe009a850801ac567f016458501006d70128073305330701e45608c95808e47808c97808330920ac98768219c89505c857079585e09577207b1751055e7d783306015908378489ff003306025329c0002d3306035329e000253306045329f0001d3306055329f800153306065329fc000d8898fe009a860801ac650d0164685010086401ae56198217c867077c7851082933083307289c0033065605ed33083307000001018210a8008215a000821698009511b0003200e46508c96808957501330701e47807330902957affae98093306330828517d573306015907378477ff003306025327c0002d3306035327e000253306045327f0001d3306055327f800153306065327fc000d8877fe009a7608017b1aac6a746457646850100ac9006478821a01c86507e46a09c96909e6890801c878088088fc330964330a640a0964757b1708481114951714330804951908330a040a0395171833098000330850100c33330820a107330964951a1864570a0b8117083d0700000233070000023308202824ff000000003307000001330832008d7a84aa07c8a70b510a0e647c0178c895cc01acbcfbc9a903843cf8c8cb0a580c1d8482ff0014090101010101010101ca920c017bbc95bb08acabfb843907520905280ec8a9090178a895aa01ac9afb320051089b0064797c77510791005127ff0090006c7a570a09330a330828735527c0000d330a01330b80284a5527e0000e330a02330b40ff283c5527f0000e330a03330b20ff282e5527f8000e330a04330b10ff28205527fc000e330a05330b08ff2812887afe00330b04ff93ab02ff85aa0701ae8a2b3308c8b70764ab01c8b90c7ccc97880895bbffd4c808520bf28aa903cf9707c88707320032000000002124492a21494a22212121212132154a9224a5909a248d88482422494924242424244426ad0a45129294924848484848888c4235499292a4944442424242426414524aa5144644942449529544424242424264a950936492549224258a905442483e54495a92241140962465495111942a24854421514814124544a6342549923a", + "01000000000000000000000000000000000000000000000000000000000000": "08000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "02000000000000000000000000000000000000000000000000000000000000": "3cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e83cd36dd9ef53b9ba511a9b80d155c78627fccf80f294e02cf068991a457a06e8", + "03000000000000000000000000000000000000000000000000000000000000": "00", + "04000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf426000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000085f9095f4abd040839d793d89ab5ff25c61e50c844ab6765e2c0b22373b5a8f6fbe5fc0cd61fdde580b3d44fe1be127197e33b91960b10d2c6fc75aec03f36e16c2a8204961097dbc2c5ba7655543385399cc9ef08bf2e520ccf3b0a7569d88492e630ae2b14e758ab0960e372172203f4c9a41777dadd529971d7ab9d23ab29fe0e9c85ec450505dde7f5ac038274cf01aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bc5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d48e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e33d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d03d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d05e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161df16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d3d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d0aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bcaa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bc3d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e300", + "05000000000000000000000000000000000000000000000000000000000000": "00000000", + "06000000000000000000000000000000000000000000000000000000000000": "6f6ad2224d7d58aec6573c623ab110700eaca20a48dc2965d535e466d524af2a835ac82bfa2ce8390bb50680d4b7a73dfa2a4cff6d8c30694b24a605f9574eafd2d34655ebcad804c56d2fd5f932c575b6a5dbb3f5652c5202bcc75ab9c2cc958a715731759b7fceaae288bcb70a605c31cbdfae83d0f4a45dfc2b2458dc9fae", + "07000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf4260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "08000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf4260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "09000000000000000000000000000000000000000000000000000000000000": "5e465beb01dbafe160ce8216047f2155dd0569f058afd52dcea601025a8d161d3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29b27150a1f1cd24bccc792ba7ba4220a1e8c36636e35a969d1d14b4c89bce7d1d463474fb186114a89dd70e88506fefc9830756c27a7845bec1cb6ee31e07211afd0dde34f0dc5d89231993cd323973faa23d84d521fd574e840b8617c75d1a1d0102aa3c71999137001a77464ced6bb2885c460be760c709009e26395716a52c8c52e6e23906a455b4264e7d0c75466e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003d5e5a51aab2b048f8686ecd79712a80e3265a114cc73f14bdb2a59233fb66d022351e22105a19aabb42589162ad7f1ea0df1c25cebf0e4a9fcd261301274862a2534be5b2f761dc898160a9b4762eb46bd171222f6cdf87f5127a9e8970a54c44fe7b2e12dda098854a9aaab03c3a47953085668673a84b0cedb4b0391ed6ae2deb1c3e04f0bc618a2bc1287d8599e8a1c47ff715cd4cbd3fe80e2607744d4514b491ed2ef76ae114ecb1af99ba6af32189bf0471c06aa3e6acdaf82e7a959cb24a5c1444cac3a6678f5182459fd8ce0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000aa2b95f7572875b0d0f186552ae745ba8222fc0b5bd456554bfe51c68938f8bce68e0cf7f26c59f963b5846202d2327cc8bc0c4eff8cb9abd4012f9a71decf008faee314528448651e50bea6d2e7e5d3176698fea0b932405a4ec0a19775e72325e44a6d28f99fba887e04eb818f13d1b73f75f0161644283df61e7fbaad7713fae0ef79fe92499202834c97f512d744515a57971badf2df62e23697e9fe347f168fed0adb9ace131f49bbd500a324e2469569423f37c5d3b430990204ae17b383fcd582cb864168c8b46be8d779e7ca00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007f6190116d118d643a98878e294ccf62b509e214299931aad8ff9764181a4e33b3e0e096b02e2ec98a3441410aeddd78c95e27a0da6f411a09c631c0f2bea6e98dfdac3e2f604ecda637e4969a139ceb70c534bd5edc4210eb5ac71178c1d62f0c977197a2c6a9e8ada6a14395bc9aa3a384d35f40c3493e20cb7efaa799f66d1cedd5b2928f8e34438b07072bbae404d7dfcee3f457f9103173805ee163ba550854e4660ccec49e25fafdb00e6adfbc8e875de1a9541e1721e956b972ef2b135cc7f71682615e12bb7d6acd353d7681000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000048e5fcdce10e0b64ec4eebd0d9211c7bac2f27ce54bca6f7776ff6fee86ab3e35c7f34a4bd4f2d04076a8c6f9060a0c8d2c6bdd082ceb3eda7df381cb260faffb78a95d81f6c7cdc517a36d81191b6f7718dcf44e76c0ce9fb724d3aea39fdb3c5f4ee31eb1f45e55b783b687b1e9087b092a18341c7cda102b4100685b0a014d55f1ccdb7600ec0db14bb90f7fc3126dc2625945bb44f302fc80df0c225546c06fa1952ef05bdc83ceb7a23373de0637cd9914272e3e3d1a455db6c48cc6b2b2c17e1dcf7cd1586a235821308aee0010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f16e5352840afb47e206b5c89f560f2611835855cf2e6ebad1acc9520a72591d837ce344bc9defceb0d7de7e9e9925096768b7adb4dad932e532eb6551e0ea02b0b9121622bf8a9a9e811ee926740a876dd0d9036f2f3060ebfab0c7c489a338a7728ee2da4a265696edcc389fe02b2caf20b5b83aeb64aaf4184bedf127f4eea1d737875854411d58ca4a2b69b066b0a0c09d2a0b7121ade517687c51954df913fe930c227723dd8f58aa2415946044dc3fb15c367a2185d0fc1f7d2bb102ff14a230d5f81cfc8ad445e51efddbf4260000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0a000000000000000000000000000000000000000000000000000000000000": "0000", + "0b000000000000000000000000000000000000000000000000000000000000": "00000000", + "0c000000000000000000000000000000000000000000000000000000000000": "00000000000000000000000000", + "0d000000000000000000000000000000000000000000000000000000000000": "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0e000000000000000000000000000000000000000000000000000000000000": "000000000000000000000000", + "0f000000000000000000000000000000000000000000000000000000000000": "000000000000000000000000", + "ff000000000000000000000000000000000000000000000000000000000000": "fcca4b52ff29ea7c74d6bd301d1cae77ef7f24c8d294c847bc82f93c5a70c26e00e40b540200000064000000000000006400000000000000081700000000000004000000" + } +} \ No newline at end of file diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index c1b37f01..1510bff0 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -46,6 +46,13 @@ target_link_libraries(app_state_manager logger ) +add_library(chain_spec SHARED impl/chain_spec_impl.cpp) +target_link_libraries(chain_spec + logger + Boost::property_tree + app_configuration +) + add_library(application SHARED impl/application_impl.cpp) target_link_libraries(application qtils::qtils diff --git a/src/app/chain_spec.hpp b/src/app/chain_spec.hpp new file mode 100644 index 00000000..51e73a28 --- /dev/null +++ b/src/app/chain_spec.hpp @@ -0,0 +1,38 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include + +#include "jam_types/types.tmp.hpp" + +namespace jam { + + using NodeAddress = Stub; + +} + +namespace jam::app { + + class ChainSpec { + public: + virtual ~ChainSpec() = default; + + virtual const std::string &id() const = 0; + + virtual const std::vector &bootNodes() const = 0; + + virtual const qtils::ByteVec &genesisHeader() const = 0; + + virtual const std::map &genesisState() + const = 0; + }; + +} // namespace jam::app diff --git a/src/app/configuration.cpp b/src/app/configuration.cpp index ac2c397f..9191430c 100644 --- a/src/app/configuration.cpp +++ b/src/app/configuration.cpp @@ -31,6 +31,10 @@ namespace jam::app { return base_path_; } + const std::filesystem::path &Configuration::specFile() const { + return spec_file_; + } + const std::filesystem::path &Configuration::modulesDir() const { return modules_dir_; } diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index 8f10b8e7..ec64310f 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -8,7 +8,6 @@ #include #include -#include #include #include @@ -37,6 +36,7 @@ namespace jam::app { [[nodiscard]] virtual const std::string &nodeVersion() const; [[nodiscard]] virtual const std::string &nodeName() const; [[nodiscard]] virtual const std::filesystem::path &basePath() const; + [[nodiscard]] virtual const std::filesystem::path &specFile() const; [[nodiscard]] virtual const std::filesystem::path &modulesDir() const; [[nodiscard]] virtual const DatabaseConfig &database() const; @@ -49,6 +49,7 @@ namespace jam::app { std::string version_; std::string name_; std::filesystem::path base_path_; + std::filesystem::path spec_file_; std::filesystem::path modules_dir_; DatabaseConfig database_; diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index 84ee7805..89bf0a0b 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -95,6 +95,7 @@ namespace jam::app { ("version,v", "Show version information.") ("base_path", po::value(), "Set base path. All relative paths will be resolved based on this path.") ("config,c", po::value(), "Optional. Filepath to load configuration from. Overrides default configuration values.") + ("spec_file", po::value(), "Set path to spec file.") ("modules_dir", po::value(), "Set path to directory containing modules.") ("name,n", po::value(), "Set name of node.") ("log,l", po::value>(), @@ -245,6 +246,16 @@ namespace jam::app { file_has_error_ = true; } } + auto spec_file = section["spec_file"]; + if (spec_file.IsDefined()) { + if (spec_file.IsScalar()) { + auto value = spec_file.as(); + config_->spec_file_ = value; + } else { + file_errors_ << "E: Value 'general.spec_file' must be scalar\n"; + file_has_error_ = true; + } + } auto modules_dir = section["modules_dir"]; if (modules_dir.IsDefined()) { if (modules_dir.IsScalar()) { @@ -293,6 +304,10 @@ namespace jam::app { cli_values_map_, "modules_dir", [&](const std::string &value) { config_->modules_dir_ = value; }); + find_argument( + cli_values_map_, "spec_file", [&](const std::string &value) { + config_->spec_file_ = value; + }); if (fail) { return Error::CliArgsParseFailed; } @@ -326,6 +341,14 @@ namespace jam::app { return Error::InvalidValue; } + config_->spec_file_ = make_absolute(config_->spec_file_); + if (not is_regular_file(config_->spec_file_)) { + SL_ERROR(logger_, + "The 'spec_file' does not exist or is not a file: {}", + config_->spec_file_.c_str()); + return Error::InvalidValue; + } + return outcome::success(); } diff --git a/src/app/impl/chain_spec_impl.cpp b/src/app/impl/chain_spec_impl.cpp new file mode 100644 index 00000000..858724fe --- /dev/null +++ b/src/app/impl/chain_spec_impl.cpp @@ -0,0 +1,87 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "app/impl/chain_spec_impl.hpp" + +#include +#include +#include + +#include + +OUTCOME_CPP_DEFINE_CATEGORY(jam::app, ChainSpecImpl::Error, e) { + using E = jam::app::ChainSpecImpl::Error; + switch (e) { + case E::MISSING_ENTRY: + return "A required entry is missing in the config file"; + case E::MISSING_PEER_ID: + return "Peer id is missing in a multiaddress provided in the config file"; + case E::PARSER_ERROR: + return "Internal parser error"; + case E::NOT_IMPLEMENTED: + return "Known entry name, but parsing not implemented"; + } + return "Unknown error in ChainSpecImpl"; +} + +namespace jam::app { + + namespace pt = boost::property_tree; + + ChainSpecImpl::ChainSpecImpl(qtils::SharedRef logsys, + qtils::SharedRef app_config) + : log_(logsys->getLogger("ChainSpec", "application")) { + qtils::raise_on_err(loadFromJson(app_config->specFile())); + } + + outcome::result ChainSpecImpl::loadFromJson( + const std::filesystem::path &file_path) { + pt::ptree tree; + try { + pt::read_json(file_path, tree); + } catch (pt::json_parser_error &e) { + log_->error( + "Parser error: {}, line {}: {}", e.filename(), e.line(), e.message()); + return Error::PARSER_ERROR; + } + + OUTCOME_TRY(loadFields(tree)); + OUTCOME_TRY(loadGenesis(tree)); + OUTCOME_TRY(loadBootNodes(tree)); + + return outcome::success(); + } + + outcome::result ChainSpecImpl::loadFields( + const boost::property_tree::ptree &tree) { + OUTCOME_TRY(id, ensure("id", tree.get_child_optional("id"))); + id_ = id.get(""); + + return outcome::success(); + } + + outcome::result ChainSpecImpl::loadGenesis( + const boost::property_tree::ptree &tree) { + try { + auto genesis_header_hex = tree.get("genesis_header"); + OUTCOME_TRY(genesis_header_encoded, + qtils::ByteVec::fromHex(genesis_header_hex)); + genesis_header_ = std::move(genesis_header_encoded); + } catch (const boost::property_tree::ptree_error &e) { + SL_CRITICAL(log_, + "Failed to read genesis block header from chain spec: {}", + e.what()); + } + return outcome::success(); + } + + outcome::result ChainSpecImpl::loadBootNodes( + const boost::property_tree::ptree &tree) { + // TODO Not implemented + return outcome::success(); + } + +} // namespace jam::app diff --git a/src/app/impl/chain_spec_impl.hpp b/src/app/impl/chain_spec_impl.hpp new file mode 100644 index 00000000..a6b2ed5b --- /dev/null +++ b/src/app/impl/chain_spec_impl.hpp @@ -0,0 +1,80 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include "app/chain_spec.hpp" +#include "log/logger.hpp" + +namespace jam::app { + class Configuration; +} +namespace jam::log { + class LoggingSystem; +} + +namespace jam::app { + + class ChainSpecImpl : public ChainSpec { + public: + enum class Error { + MISSING_ENTRY = 1, + MISSING_PEER_ID, + PARSER_ERROR, + NOT_IMPLEMENTED + }; + + ChainSpecImpl(qtils::SharedRef logsys, + qtils::SharedRef app_config); + + const std::string &id() const override { + return id_; + } + + const std::vector &bootNodes() const override { + return boot_nodes_; + } + + const qtils::ByteVec &genesisHeader() const override { + return genesis_header_; + } + + const std::map &genesisState() + const override { + return genesis_state_; + } + + private: + outcome::result loadFromJson(const std::filesystem::path &file_path); + outcome::result loadFields(const boost::property_tree::ptree &tree); + outcome::result loadGenesis(const boost::property_tree::ptree &tree); + outcome::result loadBootNodes( + const boost::property_tree::ptree &tree); + + template + outcome::result> ensure(std::string_view entry_name, + boost::optional opt_entry) { + if (not opt_entry) { + log_->error("Required '{}' entry not found in the chain spec", + entry_name); + return Error::MISSING_ENTRY; + } + return opt_entry.value(); + } + + log::Logger log_; + std::string id_; + std::vector boot_nodes_; + qtils::ByteVec genesis_header_; + std::map genesis_state_; + }; + +} // namespace jam::app + +OUTCOME_HPP_DECLARE_ERROR(jam::app, ChainSpecImpl::Error) diff --git a/src/injector/CMakeLists.txt b/src/injector/CMakeLists.txt index 480c2449..7953afaa 100644 --- a/src/injector/CMakeLists.txt +++ b/src/injector/CMakeLists.txt @@ -11,6 +11,7 @@ target_link_libraries(node_injector Boost::Boost.DI logger app_configurator + chain_spec app_state_manager application metrics diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index 53b0b7f6..c0945cb6 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -22,6 +22,7 @@ #include "app/configuration.hpp" #include "app/impl/application_impl.hpp" +#include "app/impl/chain_spec_impl.hpp" #include "app/impl/state_manager_impl.hpp" #include "app/impl/watchdog.hpp" #include "clock/impl/clock_impl.hpp" @@ -33,8 +34,8 @@ #include "modules/module.hpp" #include "se/impl/async_dispatcher_impl.hpp" #include "se/subscription.hpp" -#include "storage/in_memory/in_memory_storage.hpp" #include "storage/in_memory/in_memory_spaced_storage.hpp" +#include "storage/in_memory/in_memory_storage.hpp" #include "storage/rocksdb/rocksdb.hpp" namespace { @@ -76,6 +77,7 @@ namespace { di::bind.to(), //di::bind.to(), di::bind.to(), + di::bind.to(), // user-defined overrides... std::forward(args)...); diff --git a/vcpkg.json b/vcpkg.json index 7c7f7f11..ee598f75 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -14,7 +14,8 @@ "boost-beast", "prometheus-cpp", "ftxui", - "rocksdb" + "rocksdb", + "boost-property-tree" ], "features": { "test": { "description": "Test", "dependencies": ["gtest"]} From 71eb77ae1ac3630ae7a8bcd1b35976b2a0adbd7f Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Tue, 17 Jun 2025 15:29:09 +0300 Subject: [PATCH 10/15] refactor: code format Signed-off-by: Dmitriy Khaustov aka xDimon --- src/app/impl/watchdog.hpp | 4 ++-- src/executable/dlopen.cpp | 1 + src/executable/jam_node.cpp | 2 +- src/jam_types/types.tmp.hpp | 1 - src/log/logger.cpp | 6 +++--- src/metrics/impl/exposer_impl.cpp | 16 +++++++--------- src/metrics/impl/metrics_watcher.cpp | 2 ++ src/metrics/impl/prometheus/handler_impl.cpp | 1 + src/modules/example/module.cpp | 2 +- src/modules/module_loader.cpp | 3 ++- src/modules/synchronizer/module.cpp | 2 +- tests/mock/app/configuration_mock.hpp | 2 ++ .../storage/rocksdb/rocksdb_integration_test.cpp | 8 +++----- 13 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/app/impl/watchdog.hpp b/src/app/impl/watchdog.hpp index b1f6e07b..2b807c84 100644 --- a/src/app/impl/watchdog.hpp +++ b/src/app/impl/watchdog.hpp @@ -19,9 +19,8 @@ #include #include "app/configuration.hpp" -#include "log/logger.hpp" - #include "injector/dont_inject.hpp" +#include "log/logger.hpp" namespace soralog { class Logger; @@ -55,6 +54,7 @@ namespace { #else #include + #include inline uint64_t getPlatformThreadId() { diff --git a/src/executable/dlopen.cpp b/src/executable/dlopen.cpp index c3a27020..3de2e903 100644 --- a/src/executable/dlopen.cpp +++ b/src/executable/dlopen.cpp @@ -9,6 +9,7 @@ extern "C" { #include #include #include + #include } diff --git a/src/executable/jam_node.cpp b/src/executable/jam_node.cpp index 60023ee4..00a4d975 100644 --- a/src/executable/jam_node.cpp +++ b/src/executable/jam_node.cpp @@ -16,8 +16,8 @@ #include "app/configuration.hpp" #include "app/configurator.hpp" #include "injector/node_injector.hpp" -#include "log/logger.hpp" #include "loaders/loader.hpp" +#include "log/logger.hpp" #include "modules/module_loader.hpp" #include "se/subscription.hpp" diff --git a/src/jam_types/types.tmp.hpp b/src/jam_types/types.tmp.hpp index 4c62c553..ad2a98db 100644 --- a/src/jam_types/types.tmp.hpp +++ b/src/jam_types/types.tmp.hpp @@ -83,4 +83,3 @@ struct fmt::formatter { template struct fmt::formatter> : formatter {}; - diff --git a/src/log/logger.cpp b/src/log/logger.cpp index 2bb1540b..767393a5 100644 --- a/src/log/logger.cpp +++ b/src/log/logger.cpp @@ -4,15 +4,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include +#include "log/logger.hpp" + #include +#include #include #include #include -#include "log/logger.hpp" - OUTCOME_CPP_DEFINE_CATEGORY(jam::log, Error, e) { using E = jam::log::Error; switch (e) { diff --git a/src/metrics/impl/exposer_impl.cpp b/src/metrics/impl/exposer_impl.cpp index 26d07d93..bb993d45 100644 --- a/src/metrics/impl/exposer_impl.cpp +++ b/src/metrics/impl/exposer_impl.cpp @@ -39,11 +39,10 @@ namespace jam::metrics { bool ExposerImpl::prepare() { BOOST_ASSERT(config_->metrics().enabled == true); try { - acceptor_ = jam::api::acceptOnFreePort( - context_, - config_->metrics().endpoint, - jam::api::kDefaultPortTolerance, - logger_); + acceptor_ = jam::api::acceptOnFreePort(context_, + config_->metrics().endpoint, + jam::api::kDefaultPortTolerance, + logger_); } catch (const boost::wrapexcept &exception) { SL_CRITICAL( logger_, "Failed to prepare a listener: {}", exception.what()); @@ -72,10 +71,9 @@ namespace jam::metrics { return false; } - logger_->info( - "Listening for new connections on {}:{}", - config_->metrics().endpoint.address().to_string(), - acceptor_->local_endpoint().port()); + logger_->info("Listening for new connections on {}:{}", + config_->metrics().endpoint.address().to_string(), + acceptor_->local_endpoint().port()); acceptOnce(); thread_ = std::make_unique([context = context_] { diff --git a/src/metrics/impl/metrics_watcher.cpp b/src/metrics/impl/metrics_watcher.cpp index 61e7377b..fee0be28 100644 --- a/src/metrics/impl/metrics_watcher.cpp +++ b/src/metrics/impl/metrics_watcher.cpp @@ -5,7 +5,9 @@ */ #include "metrics/impl/metrics_watcher.hpp" + #include + #include "app/configuration.hpp" #include "app/state_manager.hpp" #include "log/logger.hpp" diff --git a/src/metrics/impl/prometheus/handler_impl.cpp b/src/metrics/impl/prometheus/handler_impl.cpp index 655664fd..71907e1e 100644 --- a/src/metrics/impl/prometheus/handler_impl.cpp +++ b/src/metrics/impl/prometheus/handler_impl.cpp @@ -7,6 +7,7 @@ #include "metrics/impl/prometheus/handler_impl.hpp" #include + #include "log/logger.hpp" #include "registry_impl.hpp" #include "utils/retain_if.hpp" diff --git a/src/modules/example/module.cpp b/src/modules/example/module.cpp index df396fc0..920adf59 100644 --- a/src/modules/example/module.cpp +++ b/src/modules/example/module.cpp @@ -23,7 +23,7 @@ static std::shared_ptr module_instance; #pragma GCC diagnostic ignored "-Wreturn-type-c-linkage" MODULE_C_API std::weak_ptr query_module_instance( - jam::modules::ExampleModuleLoader& loader, + jam::modules::ExampleModuleLoader &loader, std::shared_ptr logger) { if (!module_instance) { module_instance = std::make_shared( diff --git a/src/modules/module_loader.cpp b/src/modules/module_loader.cpp index 35c6de70..9ae1d5d5 100644 --- a/src/modules/module_loader.cpp +++ b/src/modules/module_loader.cpp @@ -88,7 +88,8 @@ namespace jam::modules { return Error::UnexpectedModuleInfo; } - auto module = Module::create(module_path, module_info, std::move(handle), loader_id); + auto module = + Module::create(module_path, module_info, std::move(handle), loader_id); modules.push_back(module); return outcome::success(); } diff --git a/src/modules/synchronizer/module.cpp b/src/modules/synchronizer/module.cpp index 1859301d..fdcb3aec 100644 --- a/src/modules/synchronizer/module.cpp +++ b/src/modules/synchronizer/module.cpp @@ -23,7 +23,7 @@ static std::shared_ptr module_instance; #pragma GCC diagnostic ignored "-Wreturn-type-c-linkage" MODULE_C_API std::weak_ptr query_module_instance( - jam::modules::SynchronizerLoader& loader, + jam::modules::SynchronizerLoader &loader, std::shared_ptr logsys) { if (!module_instance) { module_instance = std::make_shared( diff --git a/tests/mock/app/configuration_mock.hpp b/tests/mock/app/configuration_mock.hpp index 757661dd..6922258e 100644 --- a/tests/mock/app/configuration_mock.hpp +++ b/tests/mock/app/configuration_mock.hpp @@ -15,6 +15,7 @@ namespace jam::app { class ConfigurationMock : public Configuration { public: + // clang-format off MOCK_METHOD(const std::string&, nodeVersion, (), (const, override)); MOCK_METHOD(const std::string&, nodeName, (), (const, override)); MOCK_METHOD(const std::filesystem::path&, basePath, (), (const, override)); @@ -24,6 +25,7 @@ namespace jam::app { MOCK_METHOD(const DatabaseConfig &, database, (), (const, override)); MOCK_METHOD(const MetricsConfig &, metrics, (), (const, override)); + // clang-format on }; } // namespace jam::app diff --git a/tests/unit/storage/rocksdb/rocksdb_integration_test.cpp b/tests/unit/storage/rocksdb/rocksdb_integration_test.cpp index e9953b2e..18714dfb 100644 --- a/tests/unit/storage/rocksdb/rocksdb_integration_test.cpp +++ b/tests/unit/storage/rocksdb/rocksdb_integration_test.cpp @@ -4,8 +4,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "testutil/storage/base_rocksdb_test.hpp" - #include #include @@ -13,10 +11,10 @@ #include -// #include "filesystem/common.hpp" -#include "storage/storage_error.hpp" +#include "testutil/storage/base_rocksdb_test.hpp" + #include "storage/rocksdb/rocksdb.hpp" -// #include "testutil/prepare_loggers.hpp" +#include "storage/storage_error.hpp" using namespace jam::storage; namespace fs = std::filesystem; From 177e7a0a672630d2a4b342d738b4c820528beb18 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Tue, 17 Jun 2025 23:31:15 +0300 Subject: [PATCH 11/15] feature: parse values with suffix Signed-off-by: Dmitriy Khaustov aka xDimon --- src/app/configurator.cpp | 175 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 170 insertions(+), 5 deletions(-) diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index 89bf0a0b..851fed8b 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -5,8 +5,14 @@ #include "app/configurator.hpp" +#include +#include +#include #include #include +#include +#include +#include #include #include @@ -67,6 +73,161 @@ namespace { } return false; } + + std::optional parse_byte_size(std::string_view input) { + // Trim whitespace + auto first = input.find_first_not_of(" \t\n\r"); + if (first == std::string_view::npos) { + return std::nullopt; + } + auto last = input.find_last_not_of(" \t\n\r"); + input = input.substr(first, last - first + 1); + + // Parse number + size_t i = 0; + while (i < input.size() && std::isdigit(input[i])) { + ++i; + } + if (i == 0) { + return std::nullopt; + } + + std::string_view number_part = input.substr(0, i); + while (i < input.size() + && std::isspace(static_cast(input[i]))) { + ++i; + } + std::string_view suffix = input.substr(i); + + uint64_t number = 0; + auto result = + std::from_chars(number_part.begin(), number_part.end(), number); + if (result.ec != std::errc()) { + return std::nullopt; + } + + if (suffix.empty()) { + return number; + } + + // Case-insensitive comparison + auto iequals = [](std::string_view a, std::string_view b) -> bool { + if (a.size() != b.size()) { + return false; + } + for (size_t i = 0; i < a.size(); ++i) { + if (std::tolower(static_cast(a[i])) + != std::tolower(static_cast(b[i]))) { + return false; + } + } + return true; + }; + + struct SuffixDef { + std::string_view suffix; + uint64_t multiplier; + }; + + // Suffixes: IEC (base 1024) and SI (base 1000) + static constexpr SuffixDef units[] = { + // clang-format off + {"b", 1}, + {"k", 1ull << 10}, {"kib", 1ull << 10}, {"kb", 1000ull}, + {"m", 1ull << 20}, {"mib", 1ull << 20}, {"mb", 1000ull * 1000ull}, + {"g", 1ull << 30}, {"gib", 1ull << 30}, {"gb", 1000ull * 1000ull * 1000ull}, + {"t", 1ull << 40}, {"tib", 1ull << 40}, {"tb", 1000ull * 1000ull * 1000ull * 1000ull}, + // clang-format on + }; + + for (const auto &unit : units) { + if (iequals(suffix, unit.suffix)) { + if (number > UINT64_MAX / unit.multiplier) { + return std::nullopt; + } + return number * unit.multiplier; + } + } + + return std::nullopt; + } + + std::optional parse_duration(std::string_view input) { + // Trim whitespace + auto first = input.find_first_not_of(" \t\n\r"); + if (first == std::string_view::npos) { + return std::nullopt; + } + auto last = input.find_last_not_of(" \t\n\r"); + input = input.substr(first, last - first + 1); + + // Parse number + size_t i = 0; + while (i < input.size() && std::isdigit(input[i])) { + ++i; + } + if (i == 0) { + return std::nullopt; + } + + std::string_view number_part = input.substr(0, i); + while (i < input.size() && std::isspace(static_cast(input[i]))) { + ++i; + } + std::string_view suffix = input.substr(i); + + uint64_t number = 0; + auto result = + std::from_chars(number_part.begin(), number_part.end(), number); + if (result.ec != std::errc()) { + return std::nullopt; + } + + if (suffix.empty()) { + return number; + } + + // Lambda: case-insensitive string_view equal + auto iequals = [](std::string_view a, std::string_view b) -> bool { + if (a.size() != b.size()) { + return false; + } + for (size_t i = 0; i < a.size(); ++i) { + if (std::tolower(static_cast(a[i])) + != std::tolower(static_cast(b[i]))) { + return false; + } + } + return true; + }; + + // Suffix table: each entry maps one or more suffix variants to multiplier + struct Entry { + std::string_view suffix; + uint64_t multiplier; + }; + static constexpr Entry suffixes[] = { + // clang-format off + {"s", 1}, {"sec", 1}, {"secs", 1}, {"second", 1}, {"seconds", 1}, + {"m", 60}, {"min", 60}, {"mins", 60}, {"minute", 60}, {"minutes", 60}, + {"h", 3600}, {"hr", 3600}, {"hrs", 3600}, {"hour", 3600}, {"hours", 3600}, + {"d", 86400}, {"day", 86400}, {"days", 86400}, + {"w", 604800}, {"week", 604800}, {"weeks", 604800}, + // clang-format on + }; + + for (const auto &e : suffixes) { + if (iequals(suffix, e.suffix)) { + if (number > UINT64_MAX / e.multiplier) { + return std::nullopt; + } + return number * e.multiplier; + } + } + + return std::nullopt; + } + } // namespace namespace jam::app { @@ -79,7 +240,7 @@ namespace jam::app { config_->name_ = "noname"; config_->database_.directory = "db"; - config_->database_.cache_size = 512; + config_->database_.cache_size = 512 << 20; // 512Mb config_->database_.migration_enabled = false; config_->metrics_.endpoint = {boost::asio::ip::address_v4::any(), 9615}; @@ -371,11 +532,15 @@ namespace jam::app { auto spec_file = section["cache_size"]; if (spec_file.IsDefined()) { if (spec_file.IsScalar()) { - auto value = spec_file.as(); - config_->database_.cache_size = value; + auto value = parse_byte_size(spec_file.as()); + if (value.has_value()) { + config_->database_.cache_size = value.value(); + } else { + file_errors_ << "E: Bad 'cache_size' value; " + "Expected: 4096, 512Mb, 1G, etc.\n"; + } } else { - file_errors_ - << "E: Value 'database.cache_size_mb' must be scalar\n"; + file_errors_ << "E: Value 'database.cache_size' must be scalar\n"; file_has_error_ = true; } } From 8ce307480e6db39758101a47401c4e32cb9e8a31 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Wed, 18 Jun 2025 00:04:30 +0300 Subject: [PATCH 12/15] fix: CI issue Signed-off-by: Dmitriy Khaustov aka xDimon --- CMakeLists.txt | 13 +++++++------ src/app/CMakeLists.txt | 23 ++++++++++++++++++----- src/app/impl/chain_spec_impl.hpp | 2 ++ tests/testutil/storage/CMakeLists.txt | 2 ++ tests/unit/storage/rocksdb/CMakeLists.txt | 5 +---- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ddac40dd..0f5198bd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ endif () set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_EXTENSIONS ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_EXPERIMENTAL_CXX_MODULE_CMAKE_API ON) @@ -77,11 +77,12 @@ find_package(qtils CONFIG REQUIRED) find_package(prometheus-cpp CONFIG REQUIRED) find_package(RocksDB CONFIG REQUIRED) -if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") - add_compile_options(-fmodules-ts) -elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") - add_compile_options(-fmodules) -endif() +# TODO Temporarily commented out until gcc is updated (gcc-13 crashes because of this). +# if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") +# add_compile_options(-fmodules-ts) +# elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang") +# add_compile_options(-fmodules) +# endif() add_library(headers INTERFACE) target_include_directories(headers INTERFACE diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 1510bff0..f6d6d487 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -26,13 +26,17 @@ add_library(build_version ${CMAKE_BINARY_DIR}/generated/app/build_version.cpp ) -add_library(app_configuration SHARED configuration.cpp) +add_library(app_configuration SHARED + configuration.cpp +) target_link_libraries(app_configuration Boost::boost fmt::fmt ) -add_library(app_configurator SHARED configurator.cpp) +add_library(app_configurator SHARED + configurator.cpp +) target_link_libraries(app_configurator app_configuration yaml-cpp::yaml-cpp @@ -40,20 +44,29 @@ target_link_libraries(app_configurator build_version ) -add_library(app_state_manager SHARED impl/state_manager_impl.cpp) +add_library(app_state_manager SHARED + impl/state_manager_impl.cpp +) target_link_libraries(app_state_manager qtils::qtils logger ) -add_library(chain_spec SHARED impl/chain_spec_impl.cpp) +add_library(chain_spec SHARED + impl/chain_spec_impl.cpp +) target_link_libraries(chain_spec logger Boost::property_tree app_configuration ) +add_dependencies(chain_spec + generate_common_types +) -add_library(application SHARED impl/application_impl.cpp) +add_library(application SHARED + impl/application_impl.cpp +) target_link_libraries(application qtils::qtils app_configuration diff --git a/src/app/impl/chain_spec_impl.hpp b/src/app/impl/chain_spec_impl.hpp index a6b2ed5b..1818b9b9 100644 --- a/src/app/impl/chain_spec_impl.hpp +++ b/src/app/impl/chain_spec_impl.hpp @@ -6,6 +6,8 @@ #pragma once +#include + #include #include diff --git a/tests/testutil/storage/CMakeLists.txt b/tests/testutil/storage/CMakeLists.txt index 1e8e0d77..bf6a44ee 100644 --- a/tests/testutil/storage/CMakeLists.txt +++ b/tests/testutil/storage/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(base_fs_test target_link_libraries(base_fs_test GTest::gtest app_configuration + logger_for_tests ) add_library(base_rocksdb_test @@ -19,6 +20,7 @@ add_library(base_rocksdb_test ) target_link_libraries(base_rocksdb_test base_fs_test + storage ) add_library(std_list_adapter INTERFACE) diff --git a/tests/unit/storage/rocksdb/CMakeLists.txt b/tests/unit/storage/rocksdb/CMakeLists.txt index ab800bd1..2415fe15 100644 --- a/tests/unit/storage/rocksdb/CMakeLists.txt +++ b/tests/unit/storage/rocksdb/CMakeLists.txt @@ -8,16 +8,13 @@ addtest(rocksdb_fs_test rocksdb_fs_test.cpp ) target_link_libraries(rocksdb_fs_test - storage base_fs_test - logger_for_tests + storage ) addtest(rocksdb_integration_test rocksdb_integration_test.cpp ) target_link_libraries(rocksdb_integration_test - storage base_rocksdb_test - logger_for_tests ) From 4705832314722a037c94ec09047d51468ccdf0af Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Fri, 20 Jun 2025 10:56:21 +0300 Subject: [PATCH 13/15] refactor: move parsing functions to separated util-header Signed-off-by: Dmitriy Khaustov aka xDimon --- src/app/configurator.cpp | 157 +------------------------------- src/utils/parsers.hpp | 190 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 155 deletions(-) create mode 100644 src/utils/parsers.hpp diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index 851fed8b..a04fa1d3 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -23,6 +23,7 @@ #include "app/build_version.hpp" #include "app/configuration.hpp" +#include "utils/parsers.hpp" using Endpoint = boost::asio::ip::tcp::endpoint; @@ -74,160 +75,6 @@ namespace { return false; } - std::optional parse_byte_size(std::string_view input) { - // Trim whitespace - auto first = input.find_first_not_of(" \t\n\r"); - if (first == std::string_view::npos) { - return std::nullopt; - } - auto last = input.find_last_not_of(" \t\n\r"); - input = input.substr(first, last - first + 1); - - // Parse number - size_t i = 0; - while (i < input.size() && std::isdigit(input[i])) { - ++i; - } - if (i == 0) { - return std::nullopt; - } - - std::string_view number_part = input.substr(0, i); - while (i < input.size() - && std::isspace(static_cast(input[i]))) { - ++i; - } - std::string_view suffix = input.substr(i); - - uint64_t number = 0; - auto result = - std::from_chars(number_part.begin(), number_part.end(), number); - if (result.ec != std::errc()) { - return std::nullopt; - } - - if (suffix.empty()) { - return number; - } - - // Case-insensitive comparison - auto iequals = [](std::string_view a, std::string_view b) -> bool { - if (a.size() != b.size()) { - return false; - } - for (size_t i = 0; i < a.size(); ++i) { - if (std::tolower(static_cast(a[i])) - != std::tolower(static_cast(b[i]))) { - return false; - } - } - return true; - }; - - struct SuffixDef { - std::string_view suffix; - uint64_t multiplier; - }; - - // Suffixes: IEC (base 1024) and SI (base 1000) - static constexpr SuffixDef units[] = { - // clang-format off - {"b", 1}, - {"k", 1ull << 10}, {"kib", 1ull << 10}, {"kb", 1000ull}, - {"m", 1ull << 20}, {"mib", 1ull << 20}, {"mb", 1000ull * 1000ull}, - {"g", 1ull << 30}, {"gib", 1ull << 30}, {"gb", 1000ull * 1000ull * 1000ull}, - {"t", 1ull << 40}, {"tib", 1ull << 40}, {"tb", 1000ull * 1000ull * 1000ull * 1000ull}, - // clang-format on - }; - - for (const auto &unit : units) { - if (iequals(suffix, unit.suffix)) { - if (number > UINT64_MAX / unit.multiplier) { - return std::nullopt; - } - return number * unit.multiplier; - } - } - - return std::nullopt; - } - - std::optional parse_duration(std::string_view input) { - // Trim whitespace - auto first = input.find_first_not_of(" \t\n\r"); - if (first == std::string_view::npos) { - return std::nullopt; - } - auto last = input.find_last_not_of(" \t\n\r"); - input = input.substr(first, last - first + 1); - - // Parse number - size_t i = 0; - while (i < input.size() && std::isdigit(input[i])) { - ++i; - } - if (i == 0) { - return std::nullopt; - } - - std::string_view number_part = input.substr(0, i); - while (i < input.size() && std::isspace(static_cast(input[i]))) { - ++i; - } - std::string_view suffix = input.substr(i); - - uint64_t number = 0; - auto result = - std::from_chars(number_part.begin(), number_part.end(), number); - if (result.ec != std::errc()) { - return std::nullopt; - } - - if (suffix.empty()) { - return number; - } - - // Lambda: case-insensitive string_view equal - auto iequals = [](std::string_view a, std::string_view b) -> bool { - if (a.size() != b.size()) { - return false; - } - for (size_t i = 0; i < a.size(); ++i) { - if (std::tolower(static_cast(a[i])) - != std::tolower(static_cast(b[i]))) { - return false; - } - } - return true; - }; - - // Suffix table: each entry maps one or more suffix variants to multiplier - struct Entry { - std::string_view suffix; - uint64_t multiplier; - }; - static constexpr Entry suffixes[] = { - // clang-format off - {"s", 1}, {"sec", 1}, {"secs", 1}, {"second", 1}, {"seconds", 1}, - {"m", 60}, {"min", 60}, {"mins", 60}, {"minute", 60}, {"minutes", 60}, - {"h", 3600}, {"hr", 3600}, {"hrs", 3600}, {"hour", 3600}, {"hours", 3600}, - {"d", 86400}, {"day", 86400}, {"days", 86400}, - {"w", 604800}, {"week", 604800}, {"weeks", 604800}, - // clang-format on - }; - - for (const auto &e : suffixes) { - if (iequals(suffix, e.suffix)) { - if (number > UINT64_MAX / e.multiplier) { - return std::nullopt; - } - return number * e.multiplier; - } - } - - return std::nullopt; - } - } // namespace namespace jam::app { @@ -532,7 +379,7 @@ namespace jam::app { auto spec_file = section["cache_size"]; if (spec_file.IsDefined()) { if (spec_file.IsScalar()) { - auto value = parse_byte_size(spec_file.as()); + auto value = util::parseByteQuantity(spec_file.as()); if (value.has_value()) { config_->database_.cache_size = value.value(); } else { diff --git a/src/utils/parsers.hpp b/src/utils/parsers.hpp new file mode 100644 index 00000000..b8056186 --- /dev/null +++ b/src/utils/parsers.hpp @@ -0,0 +1,190 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include +#include + +namespace jam::util { + + /** + * Case-insensitive comparison of two string views. + * + * @param lhs First string view + * @param rhs Second string view + * @return true if strings are equal ignoring case, false otherwise + */ + inline bool iequals(const std::string_view lhs, const std::string_view rhs) { + if (lhs.size() != rhs.size()) { + return false; + } + for (size_t i = 0; i < lhs.size(); ++i) { + if (std::tolower(static_cast(lhs[i])) + != std::tolower(static_cast(rhs[i]))) { + return false; + } + } + return true; + } + + /** + * Parses a string representing a byte size (e.g., "10MB", "4 KiB") and + * converts it to its value in bytes. + * + * Recognized suffixes (case-insensitive): + * - SI (base 1000): B, KB, MB, GB, TB + * - IEC (base 1024): KiB, MiB, GiB, TiB + * - Single-letter: K, M, G, T are interpreted as IEC (1024-based) + * + * @param input string representation of byte size + * @return size in bytes if parsing succeeded, std::nullopt otherwise + */ + inline std::optional parseByteQuantity(std::string_view input) { + // Trim whitespace + auto first = input.find_first_not_of(" \t\n\r"); + if (first == std::string_view::npos) { + return std::nullopt; + } + auto last = input.find_last_not_of(" \t\n\r"); + input = input.substr(first, last - first + 1); + + // Parse number + size_t i = 0; + while (i < input.size() && std::isdigit(input[i])) { + ++i; + } + if (i == 0) { + return std::nullopt; + } + + std::string_view number_part = input.substr(0, i); + while (i < input.size() + && std::isspace(static_cast(input[i]))) { + ++i; + } + std::string_view suffix = input.substr(i); + + uint64_t number = 0; + auto [ptr, ec] = + std::from_chars(number_part.begin(), number_part.end(), number); + if (ec != std::errc()) { + return std::nullopt; + } + + if (suffix.empty()) { + return number; + } + + struct SuffixDef { + std::string_view suffix; + uint64_t multiplier; + }; + + // Suffixes: IEC (base 1024) and SI (base 1000) + static constexpr SuffixDef units[] = { + {"b", 1}, + {"k", 1ull << 10}, + {"kib", 1ull << 10}, + {"kb", 1000ull}, + {"m", 1ull << 20}, + {"mib", 1ull << 20}, + {"mb", 1000ull * 1000ull}, + {"g", 1ull << 30}, + {"gib", 1ull << 30}, + {"gb", 1000ull * 1000ull * 1000ull}, + {"t", 1ull << 40}, + {"tib", 1ull << 40}, + {"tb", 1000ull * 1000ull * 1000ull * 1000ull}, + }; + + for (const auto &[suffix, multiplier] : units) { + if (iequals(suffix, suffix)) { + if (number > UINT64_MAX / multiplier) { + return std::nullopt; + } + return number * multiplier; + } + } + + return std::nullopt; + } + + /** + * Parses a string representing a time duration (e.g., "5m", "2 hours") and + * converts it to its value in seconds. + * + * Recognized suffixes (case-insensitive): s, sec, minute, hour, day, week, + * etc. + * + * @param input string representation of duration + * @return duration in seconds if parsing succeeded, std::nullopt otherwise + */ + inline std::optional parseTimeDuration(std::string_view input) { + // Trim whitespace + auto first = input.find_first_not_of(" \t\n\r"); + if (first == std::string_view::npos) { + return std::nullopt; + } + auto last = input.find_last_not_of(" \t\n\r"); + input = input.substr(first, last - first + 1); + + // Parse number + size_t i = 0; + while (i < input.size() && std::isdigit(input[i])) { + ++i; + } + if (i == 0) { + return std::nullopt; + } + + std::string_view number_part = input.substr(0, i); + while (i < input.size() + && std::isspace(static_cast(input[i]))) { + ++i; + } + std::string_view suffix = input.substr(i); + + uint64_t number = 0; + auto [ptr, ec] = + std::from_chars(number_part.begin(), number_part.end(), number); + if (ec != std::errc()) { + return std::nullopt; + } + + if (suffix.empty()) { + return number; + } + + // Suffix table: each entry maps one or more suffix variants to multiplier + struct Entry { + std::string_view suffix; + uint64_t multiplier; + }; + static constexpr Entry suffixes[] = { + {"s", 1}, {"sec", 1}, {"secs", 1}, {"second", 1}, + {"seconds", 1}, {"m", 60}, {"min", 60}, {"mins", 60}, + {"minute", 60}, {"minutes", 60}, {"h", 3600}, {"hr", 3600}, + {"hrs", 3600}, {"hour", 3600}, {"hours", 3600}, {"d", 86400}, + {"day", 86400}, {"days", 86400}, {"w", 604800}, {"week", 604800}, + {"weeks", 604800}, + }; + + for (const auto &[suffix, multiplier] : suffixes) { + if (iequals(suffix, suffix)) { + if (number > UINT64_MAX / multiplier) { + return std::nullopt; + } + return number * multiplier; + } + } + + return std::nullopt; + } + +} // namespace jam::util From 19a1dd10d39169cb00feff43653d8ee8f2589f6e Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Fri, 20 Jun 2025 12:42:37 +0300 Subject: [PATCH 14/15] fix: review issue Signed-off-by: Dmitriy Khaustov aka xDimon --- src/app/impl/chain_spec_impl.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/app/impl/chain_spec_impl.cpp b/src/app/impl/chain_spec_impl.cpp index 858724fe..64692404 100644 --- a/src/app/impl/chain_spec_impl.cpp +++ b/src/app/impl/chain_spec_impl.cpp @@ -34,7 +34,10 @@ namespace jam::app { ChainSpecImpl::ChainSpecImpl(qtils::SharedRef logsys, qtils::SharedRef app_config) : log_(logsys->getLogger("ChainSpec", "application")) { - qtils::raise_on_err(loadFromJson(app_config->specFile())); + if (auto res = loadFromJson(app_config->specFile()); res.has_error()) { + SL_CRITICAL(log_, "Can't init chain spec by json-file: {}", res.error()); + qtils::raise(res.error()); + } } outcome::result ChainSpecImpl::loadFromJson( @@ -65,16 +68,12 @@ namespace jam::app { outcome::result ChainSpecImpl::loadGenesis( const boost::property_tree::ptree &tree) { - try { - auto genesis_header_hex = tree.get("genesis_header"); - OUTCOME_TRY(genesis_header_encoded, - qtils::ByteVec::fromHex(genesis_header_hex)); - genesis_header_ = std::move(genesis_header_encoded); - } catch (const boost::property_tree::ptree_error &e) { - SL_CRITICAL(log_, - "Failed to read genesis block header from chain spec: {}", - e.what()); - } + OUTCOME_TRY( + genesis_header_hex, + ensure("genesis_header", tree.get_child_optional("genesis_header"))); + OUTCOME_TRY(genesis_header_encoded, + qtils::ByteVec::fromHex(genesis_header_hex.data())); + genesis_header_ = std::move(genesis_header_encoded); return outcome::success(); } From b0a27a272bc070892a442d67617bf02503acb5d9 Mon Sep 17 00:00:00 2001 From: Dmitriy Khaustov aka xDimon Date: Fri, 20 Jun 2025 16:00:47 +0300 Subject: [PATCH 15/15] clean: remove migration feature Signed-off-by: Dmitriy Khaustov aka xDimon --- src/app/configuration.hpp | 1 - src/app/configurator.cpp | 7 +- src/storage/rocksdb/rocksdb.cpp | 156 +----------------- src/storage/rocksdb/rocksdb.hpp | 29 ---- tests/testutil/storage/base_rocksdb_test.cpp | 3 +- .../unit/storage/rocksdb/rocksdb_fs_test.cpp | 22 +-- 6 files changed, 15 insertions(+), 203 deletions(-) diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index ec64310f..f3d5c3e0 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -19,7 +19,6 @@ namespace jam::app { struct DatabaseConfig { std::filesystem::path directory = "db"; size_t cache_size = 1 << 30; // 1GiB - bool migration_enabled = false; }; struct MetricsConfig { diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index a04fa1d3..15e95ac8 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -87,8 +87,7 @@ namespace jam::app { config_->name_ = "noname"; config_->database_.directory = "db"; - config_->database_.cache_size = 512 << 20; // 512Mb - config_->database_.migration_enabled = false; + config_->database_.cache_size = 512 << 20; // 512MiB config_->metrics_.endpoint = {boost::asio::ip::address_v4::any(), 9615}; config_->metrics_.enabled = std::nullopt; @@ -119,7 +118,6 @@ namespace jam::app { ("db_path", po::value()->default_value(config_->database_.directory), "Path to DB directory. Can be relative on base path.") // ("db-tmp", "Use temporary storage path.") ("db_cache_size", po::value()->default_value(config_->database_.cache_size), "Limit the memory the database cache can use .") - ("db_enable_migration", po::bool_switch(), "Enable automatic db migration.") ; po::options_description metrics_options("Metric options"); @@ -425,9 +423,6 @@ namespace jam::app { cli_values_map_, "db_cache_size", [&](const uint32_t &value) { config_->database_.cache_size = value; }); - if (find_argument(cli_values_map_, "db_migration_enabled")) { - config_->database_.migration_enabled = true; - } if (fail) { return Error::CliArgsParseFailed; } diff --git a/src/storage/rocksdb/rocksdb.cpp b/src/storage/rocksdb/rocksdb.cpp index 49ab15c8..e58b8648 100644 --- a/src/storage/rocksdb/rocksdb.cpp +++ b/src/storage/rocksdb/rocksdb.cpp @@ -91,7 +91,6 @@ namespace jam::storage { ro_.fill_cache = false; const auto &path = app_config->database().directory; - bool enable_migration = app_config->database().migration_enabled; auto options = rocksdb::Options{}; options.create_if_missing = true; @@ -179,32 +178,13 @@ namespace jam::storage { options.create_missing_column_families = true; - const auto ttl_migrated_path = path.parent_path() / "ttl_migrated"; - const auto ttl_migrated_exists = exists(ttl_migrated_path); - - if (no_db_presented or ttl_migrated_exists) { + if (no_db_presented) { qtils::raise_on_err(openDatabaseWithTTL(options, path, column_family_descriptors, ttls, *this, - ttl_migrated_path, logger_)); - } else { - if (not enable_migration) { - SL_ERROR(logger_, - "Database migration is disabled, use older node version or " - "run with --db_migration_enabled flag"); - qtils::raise(StorageError::IO_ERROR); - } - - qtils::raise_on_err(migrateDatabase(options, - path, - column_family_descriptors, - ttls, - *this, - ttl_migrated_path, - logger_)); } // Print size of each column family @@ -257,7 +237,6 @@ namespace jam::storage { &column_family_descriptors, const std::vector &ttls, RocksDb &rocks_db, - const std::filesystem::path &ttl_migrated_path, log::Logger &log) { const auto status = rocksdb::DBWithTTL::Open(options, @@ -273,139 +252,6 @@ namespace jam::storage { status.ToString()); return status_as_error(status, log); } - if (not fs::exists(ttl_migrated_path)) { - std::ofstream file(ttl_migrated_path.native()); - if (not file) { - SL_ERROR(log, - "Can't create file {} for database", - ttl_migrated_path.native()); - return StorageError::IO_ERROR; - } - file.close(); - } - return outcome::success(); - } - - outcome::result RocksDb::migrateDatabase( - const rocksdb::Options &options, - const std::filesystem::path &path, - const std::vector - &column_family_descriptors, - const std::vector &ttls, - RocksDb &rocks_db, - const std::filesystem::path &ttl_migrated_path, - log::Logger &log) { - rocksdb::DB *db_raw = nullptr; - std::vector column_family_handles; - auto status = rocksdb::DB::Open(options, - path.native(), - column_family_descriptors, - &column_family_handles, - &db_raw); - std::shared_ptr db(db_raw); - if (not status.ok()) { - SL_ERROR(log, - "Can't open old database in {}: {}", - path.native(), - status.ToString()); - return status_as_error(status, log); - } - auto defer_db = - std::make_unique(db, column_family_handles, log); - - std::vector column_family_handles_with_ttl; - const auto ttl_path = path.parent_path() / "db_ttl"; - std::error_code ec; - fs::create_directories(ttl_path, ec); - if (ec) { - SL_ERROR(log, - "Can't create directory {} for database: {}", - ttl_path.native(), - ec); - return StorageError::IO_ERROR; - } - rocksdb::DBWithTTL *db_with_ttl_raw = nullptr; - status = rocksdb::DBWithTTL::Open(options, - ttl_path.native(), - column_family_descriptors, - &column_family_handles_with_ttl, - &db_with_ttl_raw, - ttls); - if (not status.ok()) { - SL_ERROR(log, - "Can't open database in {}: {}", - ttl_path.native(), - status.ToString()); - return status_as_error(status, log); - } - std::shared_ptr db_with_ttl(db_with_ttl_raw); - auto defer_db_ttl = std::make_unique( - db_with_ttl, column_family_handles_with_ttl, log); - - for (std::size_t i = 0; i < column_family_handles.size(); ++i) { - const auto from_handle = column_family_handles[i]; - auto to_handle = column_family_handles_with_ttl[i]; - std::unique_ptr it( - db->NewIterator(rocksdb::ReadOptions(), from_handle)); - for (it->SeekToFirst(); it->Valid(); it->Next()) { - const auto &key = it->key(); - const auto &value = it->value(); - status = - db_with_ttl->Put(rocksdb::WriteOptions(), to_handle, key, value); - if (not status.ok()) { - SL_ERROR(log, "Can't write to ttl database: {}", status.ToString()); - return status_as_error(status, log); - } - } - if (not it->status().ok()) { - SL_ERROR(log, "DB operation failed: {}", status.ToString()); - return status_as_error(it->status(), log); - } - } - defer_db_ttl.reset(); - defer_db.reset(); - fs::remove_all(path, ec); - if (ec) { - SL_ERROR(log, "Can't remove old database in {}: {}", path.native(), ec); - return StorageError::IO_ERROR; - } - fs::create_directories(path, ec); - if (ec) { - SL_ERROR(log, - "Can't create directory {} for final database: {}", - path.native(), - ec); - return StorageError::IO_ERROR; - } - fs::rename(ttl_path, path, ec); - if (ec) { - SL_ERROR(log, - "Can't rename database from {} to {}: {}", - ttl_path.native(), - path.native(), - ec); - return StorageError::IO_ERROR; - } - status = rocksdb::DBWithTTL::Open(options, - path.native(), - column_family_descriptors, - &rocks_db.column_family_handles_, - &rocks_db.db_, - ttls); - if (not status.ok()) { - SL_ERROR(log, - "Can't open database in {}: {}", - path.native(), - status.ToString()); - return status_as_error(status, log); - } - std::ofstream file(ttl_migrated_path.native()); - if (not file) { - SL_ERROR( - log, "Can't create file {} for database", ttl_migrated_path.native()); - return StorageError::IO_ERROR; - } - file.close(); return outcome::success(); } diff --git a/src/storage/rocksdb/rocksdb.hpp b/src/storage/rocksdb/rocksdb.hpp index e406ce8b..fd53ef0f 100644 --- a/src/storage/rocksdb/rocksdb.hpp +++ b/src/storage/rocksdb/rocksdb.hpp @@ -41,24 +41,6 @@ namespace jam::storage { static constexpr uint32_t kDefaultLruCacheSizeMiB = 512; static constexpr uint32_t kDefaultBlockSizeKiB = 32; - /** - * @brief Factory method to create an instance of RocksDb class. - * @param path filesystem path where database is going to be - * @param options rocksdb options, such as caching, logging, etc. - * @param prevent_destruction - avoid destruction of underlying db if true - * @param memory_budget_mib - state cache size in MiB, 90% would be set for - * trie nodes, and the rest - distributed evenly among left spaces - * @return instance of RocksDB - */ - static outcome::result> create( - log::Logger logger, - const std::filesystem::path &path, - rocksdb::Options options = rocksdb::Options(), - uint32_t memory_budget_mib = kDefaultStateCacheSizeMiB, - bool prevent_destruction = false, - const std::unordered_map &column_ttl = {}, - bool enable_migration = true); - std::shared_ptr getSpace(Space space) override; /** @@ -112,17 +94,6 @@ namespace jam::storage { &column_family_descriptors, const std::vector &ttls, RocksDb &rocks_db, - const std::filesystem::path &ttl_migrated_path, - log::Logger &log); - - static outcome::result migrateDatabase( - const rocksdb::Options &options, - const std::filesystem::path &path, - const std::vector - &column_family_descriptors, - const std::vector &ttls, - RocksDb &rocks_db, - const std::filesystem::path &ttl_migrated_path, log::Logger &log); rocksdb::DBWithTTL *db_{}; diff --git a/tests/testutil/storage/base_rocksdb_test.cpp b/tests/testutil/storage/base_rocksdb_test.cpp index 0ae3e7e0..38591b80 100644 --- a/tests/testutil/storage/base_rocksdb_test.cpp +++ b/tests/testutil/storage/base_rocksdb_test.cpp @@ -30,8 +30,7 @@ namespace test { jam::app::Configuration::DatabaseConfig db_config{ .directory = getPathString() + "/db", .cache_size = 8 << 20, // 8Mb - .migration_enabled = false}; - + }; EXPECT_CALL(*app_config, database()) .WillRepeatedly(testing::ReturnRef(db_config)); diff --git a/tests/unit/storage/rocksdb/rocksdb_fs_test.cpp b/tests/unit/storage/rocksdb/rocksdb_fs_test.cpp index 6331f3b6..d3572a97 100644 --- a/tests/unit/storage/rocksdb/rocksdb_fs_test.cpp +++ b/tests/unit/storage/rocksdb/rocksdb_fs_test.cpp @@ -31,10 +31,10 @@ struct RocksDb_Open : public test::BaseFS_Test { logsys = testutil::prepareLoggers(); app_config = std::make_shared(); - DatabaseConfig db_config{.directory = getPathString() + "/db", - .cache_size = 8 << 20, // 8Mb - .migration_enabled = false}; - + DatabaseConfig db_config{ + .directory = getPathString() + "/db", + .cache_size = 8 << 20, // 8Mb + }; EXPECT_CALL(*app_config, database()).WillRepeatedly(ReturnRef(db_config)); }; @@ -53,9 +53,10 @@ struct RocksDb_Open : public test::BaseFS_Test { * @then database can not be opened (since there is no db already) */ TEST_F(RocksDb_Open, OpenNonExistingDB) { - DatabaseConfig db_config{.directory = "/dev/zero/impossible/path", - .cache_size = 8 << 20, // 8Mb - .migration_enabled = false}; + DatabaseConfig db_config{ + .directory = "/dev/zero/impossible/path", + .cache_size = 8 << 20, // 8Mb + }; EXPECT_CALL(*app_config, database()).WillRepeatedly(ReturnRef(db_config)); @@ -68,9 +69,10 @@ TEST_F(RocksDb_Open, OpenNonExistingDB) { * @then database is opened */ TEST_F(RocksDb_Open, OpenExistingDB) { - DatabaseConfig db_config{.directory = getPathString() + "/db", - .cache_size = 8 << 20, // 8Mb - .migration_enabled = false}; + DatabaseConfig db_config{ + .directory = getPathString() + "/db", + .cache_size = 8 << 20, // 8Mb + }; EXPECT_CALL(*app_config, database()).WillRepeatedly(ReturnRef(db_config));