From 14c1f72ec817ca06eed011ed51b3046ba047babb Mon Sep 17 00:00:00 2001 From: Colin LeMahieu Date: Thu, 7 Sep 2023 11:03:31 +0100 Subject: [PATCH] Adding limiter --- nano/core_test/CMakeLists.txt | 1 + nano/core_test/scheduler_limiter.cpp | 37 ++++++++++++++++ nano/node/CMakeLists.txt | 2 + nano/node/scheduler/limiter.cpp | 63 ++++++++++++++++++++++++++++ nano/node/scheduler/limiter.hpp | 49 ++++++++++++++++++++++ nano/test_common/testutil.cpp | 2 + nano/test_common/testutil.hpp | 3 ++ 7 files changed, 157 insertions(+) create mode 100644 nano/core_test/scheduler_limiter.cpp create mode 100644 nano/node/scheduler/limiter.cpp create mode 100644 nano/node/scheduler/limiter.hpp diff --git a/nano/core_test/CMakeLists.txt b/nano/core_test/CMakeLists.txt index dec4062fb1..701987826f 100644 --- a/nano/core_test/CMakeLists.txt +++ b/nano/core_test/CMakeLists.txt @@ -40,6 +40,7 @@ add_executable( peer_container.cpp scheduler_buckets.cpp request_aggregator.cpp + scheduler_limiter.cpp signal_manager.cpp signing.cpp socket.cpp diff --git a/nano/core_test/scheduler_limiter.cpp b/nano/core_test/scheduler_limiter.cpp new file mode 100644 index 0000000000..4b81f0dbcd --- /dev/null +++ b/nano/core_test/scheduler_limiter.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include + +#include + +TEST (scheduler_limiter, construction) +{ + auto occupancy = std::make_shared (nano::test::active_transactions_insert_null, 1, nano::election_behavior::normal); + ASSERT_EQ (1, occupancy->limit ()); + ASSERT_TRUE (occupancy->available ()); +} + +TEST (scheduler_limiter, limit) +{ + auto occupancy = std::make_shared (nano::test::active_transactions_insert_null, 1, nano::election_behavior::normal); + ASSERT_EQ (1, occupancy->limit ()); + ASSERT_TRUE (occupancy->available ()); +} + +TEST (scheduler_limiter, election_activate_observer) +{ + nano::test::system system{ 1 }; + auto occupancy = std::make_shared ([&] (auto const & block, auto const & behavior) { + return system.node (0).active.insert (block, behavior); + }, + 1, nano::election_behavior::normal); + auto result = occupancy->activate (nano::dev::genesis); + ASSERT_TRUE (result.inserted); + auto elections = occupancy->elections (); + ASSERT_EQ (1, elections.size ()); + ASSERT_EQ (1, elections.count (nano::dev::genesis->qualified_root ())); + ASSERT_FALSE (occupancy->available ()); + result.election = nullptr; // Implicitly run election destructor notification by clearing the last reference + ASSERT_TIMELY (5s, occupancy->available ()); +} diff --git a/nano/node/CMakeLists.txt b/nano/node/CMakeLists.txt index 4e1f29cc02..831d6419dd 100644 --- a/nano/node/CMakeLists.txt +++ b/nano/node/CMakeLists.txt @@ -202,6 +202,8 @@ add_library( scheduler/component.cpp scheduler/hinted.hpp scheduler/hinted.cpp + scheduler/limiter.hpp + scheduler/limiter.cpp scheduler/manual.hpp scheduler/manual.cpp scheduler/optimistic.hpp diff --git a/nano/node/scheduler/limiter.cpp b/nano/node/scheduler/limiter.cpp new file mode 100644 index 0000000000..4cde8e61fe --- /dev/null +++ b/nano/node/scheduler/limiter.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include + +#include + +nano::scheduler::limiter::limiter (insert_t const & insert, size_t limit, nano::election_behavior behavior) : + insert{ insert }, + limit_m{ limit }, + behavior{ behavior } +{ + debug_assert (limit > 0); +} + +size_t nano::scheduler::limiter::limit () const +{ + return limit_m; +} + +std::unordered_set nano::scheduler::limiter::elections () const +{ + nano::lock_guard lock{ mutex }; + return elections_m; +} + +bool nano::scheduler::limiter::available () const +{ + nano::lock_guard lock{ mutex }; + auto result = elections_m.size () < limit (); + return result; +} + +nano::election_insertion_result nano::scheduler::limiter::activate (std::shared_ptr const & block) +{ + if (!available ()) + { + return { nullptr, false }; + } + + // This code section is not synchronous with respect to available () + // It is assumed 'sink' is thread safe and only one call to + auto result = insert (block, behavior); + if (result.inserted) + { + nano::lock_guard lock{ mutex }; + elections_m.insert (result.election->qualified_root); + // Capture via weak_ptr so we don't have to consider destruction order of nano::scheduler::limiter compared to nano::election. + result.election->destructor_observers.add ([this_w = std::weak_ptr{ shared_from_this () }] (nano::qualified_root const & root) { + if (auto this_l = this_w.lock ()) + { + this_l->election_destruction_notification (root); + } + }); + } + return result; +} + +size_t nano::scheduler::limiter::election_destruction_notification (nano::qualified_root const & root) +{ + nano::lock_guard lock{ mutex }; + return elections_m.erase (root); +} diff --git a/nano/node/scheduler/limiter.hpp b/nano/node/scheduler/limiter.hpp new file mode 100644 index 0000000000..fa16259e01 --- /dev/null +++ b/nano/node/scheduler/limiter.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace nano +{ +class block; +class election; +enum class election_behavior; +class stats; +} +namespace nano::scheduler +{ +/** + This class is a facade around active_transactions that limits the number of elections that can be inserted. +*/ +class limiter : public std::enable_shared_from_this +{ +public: + using insert_t = std::function const & block, nano::election_behavior behavior)>; + + limiter (insert_t const & insert, size_t limit, nano::election_behavior behavior); + // Checks whether there is availability to insert an election for 'block' and if so, spawns a new election + nano::election_insertion_result activate (std::shared_ptr const & block); + // Returns whether there is availability to insert a new election + bool available () const; + // Returns the upper limit on the number of elections allowed to be started + size_t limit () const; + std::unordered_set elections () const; + +private: + size_t election_destruction_notification (nano::qualified_root const & root); + + insert_t insert; + size_t const limit_m; + nano::election_behavior behavior; + // Tracks the elections that have been started through this facade + std::unordered_set elections_m; + std::function block)> start_election; + + mutable nano::mutex mutex; +}; +} // namespace nano::scheduler diff --git a/nano/test_common/testutil.cpp b/nano/test_common/testutil.cpp index 05a6db689b..64ef5d956e 100644 --- a/nano/test_common/testutil.cpp +++ b/nano/test_common/testutil.cpp @@ -244,3 +244,5 @@ bool nano::test::start_elections (nano::test::system & system_a, nano::node & no { return nano::test::start_elections (system_a, node_a, blocks_to_hashes (blocks_a), forced_a); } + +std::function const & block, nano::election_behavior behavior)> nano::test::active_transactions_insert_null = [] (std::shared_ptr const & block, nano::election_behavior behavior) { return nano::election_insertion_result{}; }; diff --git a/nano/test_common/testutil.hpp b/nano/test_common/testutil.hpp index 6a5ab1e379..d7a8ab25c8 100644 --- a/nano/test_common/testutil.hpp +++ b/nano/test_common/testutil.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -131,6 +132,7 @@ class network_params; class vote; class block; class election; +enum class election_behavior; extern nano::uint128_t const & genesis_amount; @@ -418,5 +420,6 @@ namespace test * NOTE: Each election is given 5 seconds to complete, if it does not complete in 5 seconds, it will return an error. */ [[nodiscard]] bool start_elections (nano::test::system &, nano::node &, std::vector> const &, bool const forced_a = false); + extern std::function const & block, nano::election_behavior behavior)> active_transactions_insert_null; } }