From 955cd2cf1f7cb77a7e998f2d10c4448ea704d043 Mon Sep 17 00:00:00 2001 From: Mikhail Boldyrev Date: Tue, 16 Apr 2019 08:00:49 +0300 Subject: [PATCH 1/2] shared storage limit Signed-off-by: Mikhail Boldyrev --- .../batch_storage_limit_by_txs.hpp | 59 +++++ .../limitable_storage.hpp | 21 ++ libs/storage_shared_limit/limited_storage.hpp | 118 ++++++++++ libs/storage_shared_limit/moved_item.hpp | 53 +++++ libs/storage_shared_limit/storage_limit.hpp | 22 ++ .../storage_limit_none.hpp | 23 ++ .../multi_sig_transactions/mst_mocks.hpp | 12 + test/module/libs/CMakeLists.txt | 1 + .../libs/storage_shared_limit/CMakeLists.txt | 8 + .../storage_shared_limit_test.cpp | 214 ++++++++++++++++++ 10 files changed, 531 insertions(+) create mode 100644 libs/storage_shared_limit/batch_storage_limit_by_txs.hpp create mode 100644 libs/storage_shared_limit/limitable_storage.hpp create mode 100644 libs/storage_shared_limit/limited_storage.hpp create mode 100644 libs/storage_shared_limit/moved_item.hpp create mode 100644 libs/storage_shared_limit/storage_limit.hpp create mode 100644 libs/storage_shared_limit/storage_limit_none.hpp create mode 100644 test/module/libs/storage_shared_limit/CMakeLists.txt create mode 100644 test/module/libs/storage_shared_limit/storage_shared_limit_test.cpp diff --git a/libs/storage_shared_limit/batch_storage_limit_by_txs.hpp b/libs/storage_shared_limit/batch_storage_limit_by_txs.hpp new file mode 100644 index 0000000000..b17b7a031b --- /dev/null +++ b/libs/storage_shared_limit/batch_storage_limit_by_txs.hpp @@ -0,0 +1,59 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef IROHA_MST_STORAGE_GUARD_HPP +#define IROHA_MST_STORAGE_GUARD_HPP + +#include + +#include "interfaces/iroha_internal/transaction_batch.hpp" +#include "multi_sig_transactions/mst_types.hpp" +#include "storage_shared_limit/storage_limit.hpp" + +namespace iroha { + + class BatchStorageLimitByTxs : public StorageLimit { + public: + explicit BatchStorageLimitByTxs(size_t txs_limit) : txs_limit_(txs_limit) {} + + bool addIfAllowed(const BatchPtr &batch) override { + const auto added_txs_quantity = batch->transactions().size(); + + size_t current_txs_quantity; + size_t new_txs_quantity; + do { + current_txs_quantity = txs_quantity_.load(std::memory_order_relaxed); + new_txs_quantity = current_txs_quantity + added_txs_quantity; + if (new_txs_quantity > txs_limit_) { + return false; + } + } while (not std::atomic_compare_exchange_weak_explicit( + &txs_quantity_, + ¤t_txs_quantity, + new_txs_quantity, + std::memory_order_relaxed, + std::memory_order_relaxed)); + assert(txs_limit_ >= txs_quantity_.load(std::memory_order_relaxed)); + return true; + } + + void remove(const BatchPtr &batch) override { + const size_t extracted_txs = batch->transactions().size(); + assert(txs_quantity_.load(std::memory_order_relaxed) >= extracted_txs); + txs_quantity_.fetch_sub(extracted_txs, std::memory_order_relaxed); + } + + size_t transactionsQuantity() const { + return txs_quantity_; + } + + private: + const size_t txs_limit_; + std::atomic txs_quantity_{0}; + }; + +} // namespace iroha + +#endif // IROHA_MST_STORAGE_GUARD_HPP diff --git a/libs/storage_shared_limit/limitable_storage.hpp b/libs/storage_shared_limit/limitable_storage.hpp new file mode 100644 index 0000000000..074419da75 --- /dev/null +++ b/libs/storage_shared_limit/limitable_storage.hpp @@ -0,0 +1,21 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef IROHA_LIBS_LIMITABLE_STORAGE_HPP +#define IROHA_LIBS_LIMITABLE_STORAGE_HPP + +namespace iroha { + + template + struct LimitableStorage { + using ItemType = Item; + + virtual ~LimitableStorage() = default; + virtual bool insert(Item item) = 0; + }; + +} // namespace iroha + +#endif // IROHA_LIBS_LIMITABLE_STORAGE_HPP diff --git a/libs/storage_shared_limit/limited_storage.hpp b/libs/storage_shared_limit/limited_storage.hpp new file mode 100644 index 0000000000..435e2e69e0 --- /dev/null +++ b/libs/storage_shared_limit/limited_storage.hpp @@ -0,0 +1,118 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef IROHA_LIBS_LIMITED_STORAGE_HPP +#define IROHA_LIBS_LIMITED_STORAGE_HPP + +#include +#include + +#include +#include "storage_shared_limit/limitable_storage.hpp" +#include "storage_shared_limit/moved_item.hpp" +#include "storage_shared_limit/storage_limit.hpp" + +namespace iroha { + + template + class LimitedStorage : boost::noncopyable { + public: + using ItemType = typename StorageImpl::ItemType; + using LimitType = StorageLimit; + using MovedItemType = MovedItem; + + static_assert( + std::is_base_of, StorageImpl>::value, + "The storage implementation must be derived from LimitableStorage!"); + + LimitedStorage(std::shared_ptr limit, + std::unique_ptr storage) + : limit_(std::move(limit)), storage_(std::move(storage)) {} + + LimitedStorage(LimitedStorage &&other) + : limit_(std::move(other.limit_)), + storage_(std::move(other.storage_)), + items_quantity_(other.items_quantity_) {} + + std::shared_ptr sharedLimit() const { + return limit_; + } + + size_t itemsQuantity() const { + return items_quantity_; + } + + bool insert(ItemType item) { + if (not limit_->addIfAllowed(item)) { + return false; + } + updateCountersOnInsertedItem(item); + return storage_->insert(std::move(item)); + } + + template + auto extract(Lambda extractor) -> + typename std::result_of::type { + auto extracted = extractor(*storage_); + for (const auto &item : extracted) { + limit_->remove(item); + updateCountersOnRemovedItem(item); + } + return extracted; + } + + template + auto access(Lambda func) const -> + typename std::result_of::type { + return func(static_cast(*storage_)); + } + + template + std::vector> move(Lambda extractor) { + auto moved_items = extractor(*storage_); + std::vector> wrapped_moved_items; + std::transform(moved_items.begin(), + moved_items.end(), + std::back_inserter(wrapped_moved_items), + [this](auto &&moved_item) { + this->updateCountersOnRemovedItem(moved_item); + return std::shared_ptr( + new MovedItemType(std::move(moved_item), limit_)); + }); + return wrapped_moved_items; + } + + bool insert(std::shared_ptr moved) { + if (moved->limit_ == limit_) { + moved->is_extracted_.test_and_set(); + return insertUnsafe(moved->item_); + } else { + return insert(moved->extract()); + } + } + + private: + bool insertUnsafe(ItemType item) { + updateCountersOnInsertedItem(item); + return storage_->insert(std::move(item)); + } + + void updateCountersOnInsertedItem(const ItemType &item) { + ++items_quantity_; + } + + void updateCountersOnRemovedItem(const ItemType &item) { + assert(items_quantity_ > 0); + --items_quantity_; + } + + std::shared_ptr limit_; + std::unique_ptr storage_; + size_t items_quantity_{0}; + }; + +} // namespace iroha + +#endif // IROHA_LIBS_LIMITED_STORAGE_HPP diff --git a/libs/storage_shared_limit/moved_item.hpp b/libs/storage_shared_limit/moved_item.hpp new file mode 100644 index 0000000000..5ceda83d1c --- /dev/null +++ b/libs/storage_shared_limit/moved_item.hpp @@ -0,0 +1,53 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef IROHA_LIBS_LIMITED_STORAGE_MOVED_ITEM_HPP +#define IROHA_LIBS_LIMITED_STORAGE_MOVED_ITEM_HPP + +#include +#include + +#include +#include "storage_shared_limit/storage_limit.hpp" + +namespace iroha { + + /// RAII item wrapper for transfers between limited storages + template + class MovedItem : public boost::noncopyable { + public: + ~MovedItem() { + if (not is_extracted_.test_and_set()) { + limit_->remove(item_); + } + } + + Item get() const { + return item_; + } + + Item extract() { + if (not is_extracted_.test_and_set()) { + limit_->remove(item_); + } + return item_; + } + + protected: + template + friend class LimitedStorage; + + MovedItem(Item item, std::shared_ptr> limit) + : item_(std::move(item)), limit_(std::move(limit)) {} + + private: + std::atomic_flag is_extracted_ = ATOMIC_FLAG_INIT; + Item item_; + std::shared_ptr> limit_; + }; + +} // namespace iroha + +#endif // IROHA_LIBS_LIMITED_STORAGE_MOVED_ITEM_HPP diff --git a/libs/storage_shared_limit/storage_limit.hpp b/libs/storage_shared_limit/storage_limit.hpp new file mode 100644 index 0000000000..287ee731b7 --- /dev/null +++ b/libs/storage_shared_limit/storage_limit.hpp @@ -0,0 +1,22 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef IROHA_LIBS_STORAGE_LIMIT_HPP +#define IROHA_LIBS_STORAGE_LIMIT_HPP + +namespace iroha { + + template + struct StorageLimit { + virtual ~StorageLimit() = default; + + virtual bool addIfAllowed(const Item &item) = 0; + + virtual void remove(const Item &item) = 0; + }; + +} // namespace iroha + +#endif // IROHA_LIBS_STORAGE_LIMIT_HPP diff --git a/libs/storage_shared_limit/storage_limit_none.hpp b/libs/storage_shared_limit/storage_limit_none.hpp new file mode 100644 index 0000000000..d3b447e2d2 --- /dev/null +++ b/libs/storage_shared_limit/storage_limit_none.hpp @@ -0,0 +1,23 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef IROHA_LIBS_STORAGE_LIMIT_NONE_HPP +#define IROHA_LIBS_STORAGE_LIMIT_NONE_HPP + +#include "storage_shared_limit/storage_limit.hpp" + +namespace iroha { + + template + struct StorageLimitNone : public StorageLimit { + bool addIfAllowed(const Item &item) override { + return true; + } + void remove(const Item &item) override {} + }; + +} // namespace iroha + +#endif // IROHA_LIBS_STORAGE_LIMIT_NONE_HPP diff --git a/test/module/irohad/multi_sig_transactions/mst_mocks.hpp b/test/module/irohad/multi_sig_transactions/mst_mocks.hpp index b5add1d6c0..d1886b82e6 100644 --- a/test/module/irohad/multi_sig_transactions/mst_mocks.hpp +++ b/test/module/irohad/multi_sig_transactions/mst_mocks.hpp @@ -61,5 +61,17 @@ namespace iroha { MOCK_CONST_METHOD0(onExpiredBatchesImpl, rxcpp::observable()); MOCK_CONST_METHOD1(batchInStorageImpl, bool(const DataType &)); }; + + struct MockMovedBatch : public MovedBatch { + explicit MockMovedBatch(BatchPtr batch) + : MovedBatch(batch, std::make_shared>()) { + EXPECT_CALL(*this, get()).WillRepeatedly(::testing::Return(batch)); + EXPECT_CALL(*this, extract()) + .Times(::testing::AtMost(1)) + .WillRepeatedly(::testing::Return(batch)); + } + MOCK_CONST_METHOD0(get, BatchPtr()); + MOCK_METHOD0(extract, BatchPtr()); + }; } // namespace iroha #endif // IROHA_MST_MOCKS_HPP diff --git a/test/module/libs/CMakeLists.txt b/test/module/libs/CMakeLists.txt index d3b3726512..00b1b154e9 100644 --- a/test/module/libs/CMakeLists.txt +++ b/test/module/libs/CMakeLists.txt @@ -13,3 +13,4 @@ add_subdirectory(crypto) add_subdirectory(datetime) add_subdirectory(converter) add_subdirectory(common) +add_subdirectory(storage_shared_limit) diff --git a/test/module/libs/storage_shared_limit/CMakeLists.txt b/test/module/libs/storage_shared_limit/CMakeLists.txt new file mode 100644 index 0000000000..36acb10689 --- /dev/null +++ b/test/module/libs/storage_shared_limit/CMakeLists.txt @@ -0,0 +1,8 @@ +AddTest(storage_shared_limit_test storage_shared_limit_test.cpp) +target_link_libraries(storage_shared_limit_test + schema + shared_model_interfaces_factories + shared_model_proto_backend + test_logger + ) + diff --git a/test/module/libs/storage_shared_limit/storage_shared_limit_test.cpp b/test/module/libs/storage_shared_limit/storage_shared_limit_test.cpp new file mode 100644 index 0000000000..b9d557912c --- /dev/null +++ b/test/module/libs/storage_shared_limit/storage_shared_limit_test.cpp @@ -0,0 +1,214 @@ +/** + * Copyright Soramitsu Co., Ltd. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "storage_shared_limit/batch_storage_limit_by_txs.hpp" +#include "storage_shared_limit/limitable_storage.hpp" +#include "storage_shared_limit/limited_storage.hpp" +#include "storage_shared_limit/storage_limit.hpp" + +#include +#include "module/irohad/multi_sig_transactions/mst_test_helpers.hpp" +#include "module/shared_model/interface_mocks.hpp" + +using namespace iroha; +using namespace testing; + +class StorageLimitTest : public ::testing::Test { + protected: + std::shared_ptr makeTx() { + return clone(txBuilder(tx_counter_++).build()); + } + + std::shared_ptr makeBatch( + size_t num_txs) { + shared_model::interface::types::SharedTxsCollectionType txs; + std::generate_n( + std::back_inserter(txs), num_txs, [this]() { return this->makeTx(); }); + return createMockBatchWithTransactions(txs, {}); + } + + private: + size_t tx_counter_{0}; +}; + +struct StorageImpl : public LimitableStorage { + bool insert(BatchPtr batch) override { + batches.emplace_back(std::move(batch)); + return true; + } + + size_t transactionsQuantity() const { + return std::accumulate(batches.begin(), + batches.end(), + 0, + [](size_t sum, const BatchPtr &batch) { + return sum + batch->transactions().size(); + }); + } + + std::vector batches; +}; + +template +std::vector>> generateStorages( + std::shared_ptr limit, size_t num_storages) { + std::vector>> storages; + storages.reserve(num_storages); + for (size_t i = 0; i < num_storages; ++i) { + storages.emplace_back(std::make_shared>( + limit, std::make_unique())); + } + return storages; +} + +size_t getTransactionsQuantity( + const std::shared_ptr> &storage) { + return storage->access( + [](const auto &storage) { return storage.transactionsQuantity(); }); +} + +/** + * @given 5 storages with shared limit of 10 transactions + * @when first a batch with 2 transactionsa batch with 2 transactions is + * inserted to each storage, than a batch with single transactions is tried to + * be inserted to each storage + * @then first series of batches is inserted successfully and second is not + */ +TEST_F(StorageLimitTest, SharedLimitByTxs) { + auto limit = std::make_shared(10); + auto storages = generateStorages(limit, 5); + + for (auto &storage : storages) { + EXPECT_EQ(storage->itemsQuantity(), 0); + EXPECT_EQ(getTransactionsQuantity(storage), 0); + EXPECT_TRUE(storage->insert(makeBatch(2))); + EXPECT_EQ(storage->itemsQuantity(), 1); + EXPECT_EQ(getTransactionsQuantity(storage), 2); + } + + EXPECT_EQ(limit->transactionsQuantity(), 10); + + for (auto &storage : storages) { + EXPECT_FALSE(storage->insert(makeBatch(1))); + EXPECT_EQ(storage->itemsQuantity(), 1); + EXPECT_EQ(getTransactionsQuantity(storage), 2); + } +} + +/** + * @given a storage with shared limit of 10 transactions + * @when another storage is created using its limit and is filled with 10 + * transactions, then transactions are tried to be inserted to the first storage + * @then batches are not inserted to the first storage because it would violate + * the shared limit + */ +TEST_F(StorageLimitTest, SharedLimitAddStorage) { + LimitedStorage storage1{ + std::make_shared(10), + std::make_unique()}; + LimitedStorage storage2{storage1.sharedLimit(), + std::make_unique()}; + EXPECT_TRUE(storage2.insert(makeBatch(10))); + EXPECT_FALSE(storage1.insert(makeBatch(1))); +} + +/** + * @given 2 storages with shared limit of 10 transactions filled to limit + * @when a batch with 3 transactions is removed from the first one + * @then the second can accept batches with up to 3 transactions in total + */ +TEST_F(StorageLimitTest, SharedLimitRemoveAndAdd) { + auto limit = std::make_shared(10); + auto storages = generateStorages(limit, 2); + EXPECT_TRUE(storages[0]->insert(makeBatch(3))); + EXPECT_TRUE(storages[0]->insert(makeBatch(3))); + EXPECT_TRUE(storages[1]->insert(makeBatch(4))); + EXPECT_EQ(limit->transactionsQuantity(), 10); + + storages[0]->extract([](auto &storage) { + auto extracted_batch = std::move(*storage.batches.begin()); + storage.batches.erase(storage.batches.begin()); + return std::vector{extracted_batch}; + }); + EXPECT_EQ(storages[0]->itemsQuantity(), 1); + EXPECT_EQ(getTransactionsQuantity(storages[0]), 3); + EXPECT_EQ(limit->transactionsQuantity(), 7); + + EXPECT_TRUE(storages[1]->insert(makeBatch(1))); + EXPECT_TRUE(storages[1]->insert(makeBatch(2))); + EXPECT_FALSE(storages[1]->insert(makeBatch(1))); + EXPECT_EQ(limit->transactionsQuantity(), 10); +} + +/** + * @given 2 storages with shared limit of 10 transactions filled to limit + * @when a batch with 3 transactions is moved from the first one + * @then both storages do not accept any new batches + * @and the second storage can accept the moved batch + */ +TEST_F(StorageLimitTest, SharedLimitMoveBatch) { + auto limit = std::make_shared(10); + auto storages = generateStorages(limit, 2); + EXPECT_TRUE(storages[0]->insert(makeBatch(3))); + EXPECT_TRUE(storages[0]->insert(makeBatch(3))); + EXPECT_TRUE(storages[1]->insert(makeBatch(4))); + EXPECT_EQ(limit->transactionsQuantity(), 10); + + auto moved_batches = storages[0]->move([](auto &storage) { + std::vector extracted_batches; + extracted_batches.emplace_back(std::move(*storage.batches.begin())); + storage.batches.erase(storage.batches.begin()); + return extracted_batches; + }); + EXPECT_EQ(storages[0]->itemsQuantity(), 1); + EXPECT_EQ(getTransactionsQuantity(storages[0]), 3); + + EXPECT_EQ(limit->transactionsQuantity(), 10); + EXPECT_FALSE(storages[0]->insert(makeBatch(1))); + EXPECT_FALSE(storages[1]->insert(makeBatch(1))); + + ASSERT_EQ(moved_batches.size(), 1); + EXPECT_TRUE(storages[1]->insert(moved_batches.front())); + EXPECT_EQ(storages[1]->itemsQuantity(), 2); + EXPECT_EQ(getTransactionsQuantity(storages[1]), 7); + + EXPECT_FALSE(storages[0]->insert(makeBatch(1))); + EXPECT_FALSE(storages[1]->insert(makeBatch(1))); +} + +/** + * @given 2 storages with shared limit of 10 transactions filled to limit + * @when a batch with 3 transactions is moved from the first one and then is + * destroyed without being inserted to any storage + * @then both storages can accept new batches with up to 3 transactions in total + */ +TEST_F(StorageLimitTest, SharedLimitMovedBatchDestroyed) { + auto limit = std::make_shared(10); + auto storages = generateStorages(limit, 2); + EXPECT_TRUE(storages[0]->insert(makeBatch(3))); + EXPECT_TRUE(storages[0]->insert(makeBatch(3))); + EXPECT_TRUE(storages[1]->insert(makeBatch(4))); + + { + auto moved_batches = storages[0]->move([](auto &storage) { + std::vector extracted_batches; + extracted_batches.emplace_back(std::move(*storage.batches.begin())); + storage.batches.erase(storage.batches.begin()); + return extracted_batches; + }); + EXPECT_EQ(moved_batches.size(), 1); + + EXPECT_EQ(limit->transactionsQuantity(), 10); + EXPECT_FALSE(storages[0]->insert(makeBatch(1))); + EXPECT_FALSE(storages[1]->insert(makeBatch(1))); + } + + EXPECT_EQ(limit->transactionsQuantity(), 7); + EXPECT_TRUE(storages[0]->insert(makeBatch(1))); + EXPECT_TRUE(storages[1]->insert(makeBatch(2))); + + EXPECT_FALSE(storages[0]->insert(makeBatch(1))); + EXPECT_FALSE(storages[1]->insert(makeBatch(1))); +} From c47e105865af0a91451c7cbe9591dbe415f1575b Mon Sep 17 00:00:00 2001 From: Mikhail Boldyrev Date: Tue, 16 Apr 2019 12:41:26 +0300 Subject: [PATCH 2/2] removed mock moved batch Signed-off-by: Mikhail Boldyrev --- .../irohad/multi_sig_transactions/mst_mocks.hpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/module/irohad/multi_sig_transactions/mst_mocks.hpp b/test/module/irohad/multi_sig_transactions/mst_mocks.hpp index d1886b82e6..b5add1d6c0 100644 --- a/test/module/irohad/multi_sig_transactions/mst_mocks.hpp +++ b/test/module/irohad/multi_sig_transactions/mst_mocks.hpp @@ -61,17 +61,5 @@ namespace iroha { MOCK_CONST_METHOD0(onExpiredBatchesImpl, rxcpp::observable()); MOCK_CONST_METHOD1(batchInStorageImpl, bool(const DataType &)); }; - - struct MockMovedBatch : public MovedBatch { - explicit MockMovedBatch(BatchPtr batch) - : MovedBatch(batch, std::make_shared>()) { - EXPECT_CALL(*this, get()).WillRepeatedly(::testing::Return(batch)); - EXPECT_CALL(*this, extract()) - .Times(::testing::AtMost(1)) - .WillRepeatedly(::testing::Return(batch)); - } - MOCK_CONST_METHOD0(get, BatchPtr()); - MOCK_METHOD0(extract, BatchPtr()); - }; } // namespace iroha #endif // IROHA_MST_MOCKS_HPP