From a04607ac10687d26c3b615e14c5348c42e2bde90 Mon Sep 17 00:00:00 2001 From: Bart Wyatt Date: Wed, 28 Mar 2018 19:12:02 -0400 Subject: [PATCH] - added testing rig for resource_limits_manager - added accessors for budgeting and tests - fixed math to behave like the old algorithm while maintaining the new continuity across window size changes --- libraries/chain/chain_controller.cpp | 2 +- .../include/eosio/chain/resource_limits.hpp | 11 +- .../eosio/chain/resource_limits_private.hpp | 29 +-- libraries/chain/resource_limits.cpp | 77 +++++-- libraries/chain/test/CMakeLists.txt | 6 +- .../eosio/chain/test/chainbase_fixture.hpp | 31 +++ libraries/chain/test/resource_limits_test.cpp | 190 ++++++++++++++++++ 7 files changed, 315 insertions(+), 31 deletions(-) create mode 100644 libraries/chain/test/include/eosio/chain/test/chainbase_fixture.hpp create mode 100644 libraries/chain/test/resource_limits_test.cpp diff --git a/libraries/chain/chain_controller.cpp b/libraries/chain/chain_controller.cpp index 199fd521c..31c8e7449 100644 --- a/libraries/chain/chain_controller.cpp +++ b/libraries/chain/chain_controller.cpp @@ -436,7 +436,7 @@ void chain_controller::_finalize_block( const block_trace& trace ) { try { clear_expired_transactions(); update_last_irreversible_block(); - _resource_limits.process_pending_updates(); + _resource_limits.process_account_limit_updates(); // for block usage tracking the ordinal is based on actual blocks, this means that gaps from skipped blocks are // do not affect the calculation of elastic target/maximums. diff --git a/libraries/chain/include/eosio/chain/resource_limits.hpp b/libraries/chain/include/eosio/chain/resource_limits.hpp index 114cb8df7..3ac7d3333 100644 --- a/libraries/chain/include/eosio/chain/resource_limits.hpp +++ b/libraries/chain/include/eosio/chain/resource_limits.hpp @@ -16,13 +16,20 @@ namespace eosio { namespace chain { namespace resource_limits { void initialize_account( const account_name& account ); void add_account_usage( const vector& accounts, uint64_t cpu_usage, uint64_t net_usage, uint32_t ordinal ); + void add_account_ram_usage( const account_name account, int64_t ram_delta, const char* use_format = "Unspecified", const fc::variant_object& args = fc::variant_object() ); - void add_block_usage( uint64_t cpu_usage, uint64_t net_usage, uint32_t ordinal ); + void add_block_usage( uint64_t cpu_usage, uint64_t net_usage, uint32_t ordinal); void set_account_limits( const account_name& account, int64_t ram_bytes, int64_t net_weight, int64_t cpu_weight); void get_account_limits( const account_name& account, int64_t& ram_bytes, int64_t& net_weight, int64_t& cpu_weight) const; + void process_account_limit_updates(); + + // accessors + uint64_t get_virtual_block_cpu_limit() const; + uint64_t get_virtual_block_net_limit() const; - void process_pending_updates(); + int64_t get_account_block_cpu_limit( const account_name& name ) const; + int64_t get_account_block_net_limit( const account_name& name ) const; private: chainbase::database& _db; diff --git a/libraries/chain/include/eosio/chain/resource_limits_private.hpp b/libraries/chain/include/eosio/chain/resource_limits_private.hpp index 27f6d4687..0c38e9f8d 100644 --- a/libraries/chain/include/eosio/chain/resource_limits_private.hpp +++ b/libraries/chain/include/eosio/chain/resource_limits_private.hpp @@ -35,17 +35,21 @@ namespace eosio { namespace chain { namespace resource_limits { { exponential_moving_average_accumulator() : last_ordinal(0) - , value(0) + , value_ex(0) + , consumed(0) { } - uint32_t last_ordinal; - uint64_t value; + uint32_t last_ordinal; ///< The ordinal of the last period which has contributed to the average + uint64_t value_ex; ///< The current average pre-multiplied by Precision + uint64_t consumed; ///< The the last periods average + the current periods contribution so far /** - * return the average value in rate_limiting_precision + * return the average value */ - uint64_t average() const { return value; } + uint64_t average() const { + return value_ex / Precision; + } void add( uint64_t units, uint32_t ordinal, uint32_t window_size ) { @@ -57,15 +61,17 @@ namespace eosio { namespace chain { namespace resource_limits { (uint64_t)window_size ); - value = value * decay; + value_ex = value_ex * decay; } else { - value = 0; + value_ex = 0; } last_ordinal = ordinal; + consumed = value_ex / Precision; } - value += units * Precision; + consumed += units; + value_ex += units * Precision / (uint64_t)window_size; } }; } @@ -114,6 +120,7 @@ namespace eosio { namespace chain { namespace resource_limits { usage_accumulator net_usage; usage_accumulator cpu_usage; + uint64_t ram_usage = 0; }; @@ -171,12 +178,6 @@ namespace eosio { namespace chain { namespace resource_limits { uint64_t total_cpu_weight = 0; uint64_t total_ram_bytes = 0; - /* TODO: we have to guarantee that we don't over commit our ram. This can be tricky if multiple threads are - * updating limits in parallel. For now, we are not running in parallel so this is a temporary measure that - * allows us to quarantine this guarantee from the rest of the otherwise parallel-compatible code - */ - uint64_t speculative_ram_bytes = 0; - /** * The virtual number of bytes that would be consumed over blocksize_average_window_ms * if all blocks were at their maximum virtual size. This is virtual because the diff --git a/libraries/chain/resource_limits.cpp b/libraries/chain/resource_limits.cpp index c46a45d01..4654bce42 100644 --- a/libraries/chain/resource_limits.cpp +++ b/libraries/chain/resource_limits.cpp @@ -7,16 +7,15 @@ namespace eosio { namespace chain { namespace resource_limits { -static uint64_t update_elastic_limit(uint64_t current_limit, uint64_t average_usage_ex, const elastic_limit_parameters& params) { +static uint64_t update_elastic_limit(uint64_t current_limit, uint64_t average_usage, const elastic_limit_parameters& params) { uint64_t result = current_limit; - if (average_usage_ex > (params.target * config::rate_limiting_precision) ) { + if (average_usage > params.target ) { result = result * params.contract_rate; } else { result = result * params.expand_rate; } - uint64_t min = params.max * params.periods; - return std::min(std::max(result, min), min * params.max_multiplier); + return std::min(std::max(result, params.max), params.max * params.max_multiplier); } void resource_limits_state_object::update_virtual_cpu_limit( const resource_limits_config_object& cfg ) { @@ -35,12 +34,16 @@ void resource_limits_manager::initialize_database() { } void resource_limits_manager::initialize_chain() { - _db.create([](resource_limits_config_object& config){ + const auto& config = _db.create([](resource_limits_config_object& config){ // see default settings in the declaration }); - _db.create([](resource_limits_state_object& state){ + _db.create([&config](resource_limits_state_object& state){ // see default settings in the declaration + + // start the chain off in a way that it is "congested" aka slow-start + state.virtual_cpu_limit = config.cpu_limit_parameters.max; + state.virtual_net_limit = config.net_limit_parameters.max; }); } @@ -65,10 +68,10 @@ void resource_limits_manager::add_account_usage(const vector& acco const auto& limits = _db.get( boost::make_tuple(false, a)); _db.modify( usage, [&]( auto& bu ){ bu.net_usage.add( net_usage, block_num, config.net_limit_parameters.periods ); - bu.cpu_usage.add( cpu_usage, block_num, config.net_limit_parameters.periods ); + bu.cpu_usage.add( cpu_usage, block_num, config.cpu_limit_parameters.periods ); }); - uint128_t consumed_cpu_ex = usage.cpu_usage.value; // this is pre-multiplied by config::rate_limiting_precision + uint128_t consumed_cpu_ex = usage.cpu_usage.consumed * config::rate_limiting_precision; uint128_t capacity_cpu_ex = state.virtual_cpu_limit * config::rate_limiting_precision; EOS_ASSERT( limits.cpu_weight < 0 || (consumed_cpu_ex * state.total_cpu_weight) <= (limits.cpu_weight * capacity_cpu_ex), tx_resource_exhausted, @@ -80,7 +83,7 @@ void resource_limits_manager::add_account_usage(const vector& acco ("total_cpu_weight", state.total_cpu_weight) ); - uint128_t consumed_net_ex = usage.net_usage.value; // this is pre-multiplied by config::rate_limiting_precision + uint128_t consumed_net_ex = usage.net_usage.consumed * config::rate_limiting_precision; uint128_t capacity_net_ex = state.virtual_net_limit * config::rate_limiting_precision; EOS_ASSERT( limits.net_weight < 0 || (consumed_net_ex * state.total_net_weight) <= (limits.net_weight * capacity_net_ex), @@ -175,7 +178,7 @@ void resource_limits_manager::get_account_limits( const account_name& account, i } -void resource_limits_manager::process_pending_updates() { +void resource_limits_manager::process_account_limit_updates() { auto& multi_index = _db.get_mutable_index(); auto& by_owner_index = multi_index.indices().get(); @@ -187,7 +190,7 @@ void resource_limits_manager::process_pending_updates() { } if (pending_value > 0) { - EOS_ASSERT(UINT64_MAX - total < value, rate_limiting_state_inconsistent, "overflow when applying new value to ${which}", ("which", debug_which)); + EOS_ASSERT(UINT64_MAX - total >= pending_value, rate_limiting_state_inconsistent, "overflow when applying new value to ${which}", ("which", debug_which)); total += pending_value; } @@ -195,7 +198,7 @@ void resource_limits_manager::process_pending_updates() { }; const auto& state = _db.get(); - _db.modify(state, [&](resource_limits_state_object rso){ + _db.modify(state, [&](resource_limits_state_object& rso){ while(!by_owner_index.empty()) { const auto& itr = by_owner_index.lower_bound(boost::make_tuple(true)); if (itr == by_owner_index.end() || itr->pending!= true) { @@ -214,4 +217,54 @@ void resource_limits_manager::process_pending_updates() { }); } +uint64_t resource_limits_manager::get_virtual_block_cpu_limit() const { + const auto& state = _db.get(); + return state.virtual_cpu_limit; +} + +uint64_t resource_limits_manager::get_virtual_block_net_limit() const { + const auto& state = _db.get(); + return state.virtual_net_limit; +} + +int64_t resource_limits_manager::get_account_block_cpu_limit( const account_name& name ) const { + const auto& state = _db.get(); + const auto& usage = _db.get(name); + const auto& limits = _db.get(boost::make_tuple(false, name)); + if (limits.cpu_weight < 0) { + return -1; + } + + uint128_t consumed_ex = (uint128_t)usage.cpu_usage.consumed * (uint128_t)config::rate_limiting_precision; + uint128_t virtual_capacity_ex = (uint128_t)state.virtual_cpu_limit * (uint128_t)config::rate_limiting_precision; + uint128_t usable_capacity_ex = (uint128_t)(virtual_capacity_ex * limits.cpu_weight) / (uint128_t)state.total_cpu_weight; + + if (usable_capacity_ex < consumed_ex) { + return 0; + } + + return (int64_t)((usable_capacity_ex - consumed_ex) / (uint128_t)config::rate_limiting_precision); +} + +int64_t resource_limits_manager::get_account_block_net_limit( const account_name& name ) const { + const auto& state = _db.get(); + const auto& usage = _db.get(name); + const auto& limits = _db.get(boost::make_tuple(false, name)); + if (limits.net_weight < 0) { + return -1; + } + + uint128_t consumed_ex = (uint128_t)usage.net_usage.consumed * (uint128_t)config::rate_limiting_precision; + uint128_t virtual_capacity_ex = (uint128_t)state.virtual_net_limit * (uint128_t)config::rate_limiting_precision; + uint128_t usable_capacity_ex = (uint128_t)(virtual_capacity_ex * limits.net_weight) / (uint128_t)state.total_net_weight; + + if (usable_capacity_ex < consumed_ex) { + return 0; + } + + return (int64_t)((usable_capacity_ex - consumed_ex) / (uint128_t)config::rate_limiting_precision); + +} + + } } } /// eosio::chain::resource_limits diff --git a/libraries/chain/test/CMakeLists.txt b/libraries/chain/test/CMakeLists.txt index e89fb6844..48256d764 100644 --- a/libraries/chain/test/CMakeLists.txt +++ b/libraries/chain/test/CMakeLists.txt @@ -6,8 +6,10 @@ add_executable(chain_unit_test test.cpp block_test.cpp name_test.cpp - transaction_test.cpp) -target_include_directories(chain_unit_test PRIVATE ${Boost_INCLUDE_DIRS}) + transaction_test.cpp + resource_limits_test.cpp ) + +target_include_directories(chain_unit_test PRIVATE ${Boost_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/include ) target_compile_definitions(chain_unit_test PRIVATE "BOOST_TEST_DYN_LINK=1") target_link_libraries(chain_unit_test eosio_chain) diff --git a/libraries/chain/test/include/eosio/chain/test/chainbase_fixture.hpp b/libraries/chain/test/include/eosio/chain/test/chainbase_fixture.hpp new file mode 100644 index 000000000..4cdf47b20 --- /dev/null +++ b/libraries/chain/test/include/eosio/chain/test/chainbase_fixture.hpp @@ -0,0 +1,31 @@ +#include +#include + +#include + +namespace eosio { namespace chain { namespace test { + +/** + * Utility class to create and tear down a temporary chainbase::database using RAII + * + * @tparam MAX_SIZE - the maximum size of the chainbase::database + */ +template +struct chainbase_fixture { + chainbase_fixture() + : _tempdir() + , _db(std::make_unique(_tempdir.path(), chainbase::database::read_write, MAX_SIZE)) + { + } + + ~chainbase_fixture() + { + _db.reset(); + _tempdir.remove(); + } + + fc::temp_directory _tempdir; + std::unique_ptr _db; +}; + +} } } // eosio::chain::test \ No newline at end of file diff --git a/libraries/chain/test/resource_limits_test.cpp b/libraries/chain/test/resource_limits_test.cpp new file mode 100644 index 000000000..5adcc3a97 --- /dev/null +++ b/libraries/chain/test/resource_limits_test.cpp @@ -0,0 +1,190 @@ +#include +#include +#include +#include + +#include + +using namespace eosio::chain::resource_limits; +using namespace eosio::chain::test; +using namespace eosio::chain; + + + +class resource_limits_fixture: private chainbase_fixture<512*1024>, public resource_limits_manager +{ + public: + resource_limits_fixture() + :chainbase_fixture() + ,resource_limits_manager(*chainbase_fixture::_db) + { + initialize_database(); + initialize_chain(); + } + + ~resource_limits_fixture() {} + + chainbase::database::session start_session() { + return chainbase_fixture::_db->start_undo_session(true); + } +}; + +constexpr int expected_elastic_iterations(uint64_t from, uint64_t to, uint64_t rate_num, uint64_t rate_den ) { + int result = 0; + uint64_t cur = from; + + while((from < to && cur < to) || (from > to && cur > to)) { + cur = cur * rate_num / rate_den; + result += 1; + } + + return result; +} + + +constexpr int expected_exponential_average_iterations( uint64_t from, uint64_t to, uint64_t value, uint64_t window_size ) { + int result = 0; + uint64_t cur = from; + + while((from < to && cur < to) || (from > to && cur > to)) { + cur = cur * (uint128_t)(window_size - 1) / (uint64_t)(window_size); + cur += value / (uint64_t)(window_size); + result += 1; + } + + return result; +} + +BOOST_AUTO_TEST_SUITE(resource_limits_test) + + /** + * Test to make sure that the elastic limits for blocks relax and contract as expected + */ + BOOST_FIXTURE_TEST_CASE(elastic_cpu_relax_contract, resource_limits_fixture) try { + const uint64_t desired_virtual_limit = config::default_max_block_cpu_usage * 1000ULL; + const int expected_relax_iterations = expected_elastic_iterations( config::default_max_block_cpu_usage, desired_virtual_limit, 1000, 999 ); + + // this is enough iterations for the average to reach/exceed the target (triggering congestion handling) and then the iterations to contract down to the min + // subtracting 1 for the iteration that pulls double duty as reaching/exceeding the target and starting congestion handling + const int expected_contract_iterations = + expected_exponential_average_iterations(0, config::default_target_block_cpu_usage, config::default_max_block_cpu_usage, config::block_cpu_usage_average_window_ms / config::block_interval_ms ) + + expected_elastic_iterations( desired_virtual_limit, config::default_max_block_cpu_usage, 99, 100 ) - 1; + + // relax from the starting state (congested) to the idle state as fast as possible + int iterations = 0; + while (get_virtual_block_cpu_limit() < desired_virtual_limit && iterations <= expected_relax_iterations) { + add_block_usage(0,0,iterations++); + } + + BOOST_REQUIRE_EQUAL(iterations, expected_relax_iterations); + BOOST_REQUIRE_EQUAL(get_virtual_block_cpu_limit(), desired_virtual_limit); + + // push maximum resources to go from idle back to congested as fast as possible + iterations = 0; + while (get_virtual_block_cpu_limit() > config::default_max_block_cpu_usage && iterations <= expected_contract_iterations) { + add_block_usage(config::default_max_block_cpu_usage, 0, iterations++); + } + + BOOST_REQUIRE_EQUAL(iterations, expected_contract_iterations); + BOOST_REQUIRE_EQUAL(get_virtual_block_cpu_limit(), config::default_max_block_cpu_usage); + } FC_LOG_AND_RETHROW(); + + /** + * Test to make sure that the elastic limits for blocks relax and contract as expected + */ + BOOST_FIXTURE_TEST_CASE(elastic_net_relax_contract, resource_limits_fixture) try { + const uint64_t desired_virtual_limit = config::default_max_block_size * 1000ULL; + const int expected_relax_iterations = expected_elastic_iterations( config::default_max_block_size, desired_virtual_limit, 1000, 999 ); + + // this is enough iterations for the average to reach/exceed the target (triggering congestion handling) and then the iterations to contract down to the min + // subtracting 1 for the iteration that pulls double duty as reaching/exceeding the target and starting congestion handling + const int expected_contract_iterations = + expected_exponential_average_iterations(0, config::default_target_block_size, config::default_max_block_size, config::block_size_average_window_ms / config::block_interval_ms ) + + expected_elastic_iterations( desired_virtual_limit, config::default_max_block_size, 99, 100 ) - 1; + + // relax from the starting state (congested) to the idle state as fast as possible + int iterations = 0; + while (get_virtual_block_net_limit() < desired_virtual_limit && iterations <= expected_relax_iterations) { + add_block_usage(0,0,iterations++); + } + + BOOST_REQUIRE_EQUAL(iterations, expected_relax_iterations); + BOOST_REQUIRE_EQUAL(get_virtual_block_net_limit(), desired_virtual_limit); + + // push maximum resources to go from idle back to congested as fast as possible + iterations = 0; + while (get_virtual_block_net_limit() > config::default_max_block_size && iterations <= expected_contract_iterations) { + add_block_usage(0, config::default_max_block_size, iterations++); + } + + BOOST_REQUIRE_EQUAL(iterations, expected_contract_iterations); + BOOST_REQUIRE_EQUAL(get_virtual_block_net_limit(), config::default_max_block_size); + } FC_LOG_AND_RETHROW(); + + /** + * create 5 accounts with different weights, verify that the capacities are as expected and that usage properly enforces them + */ + BOOST_FIXTURE_TEST_CASE(weighted_capacity_cpu, resource_limits_fixture) try { + const vector weights = { 234, 511, 672, 800, 1213 }; + const int64_t total = std::accumulate(std::begin(weights), std::end(weights), 0LL); + vector expected_limits; + std::transform(std::begin(weights), std::end(weights), std::back_inserter(expected_limits), [total](const auto& v){ return v * config::default_max_block_cpu_usage / total; }); + + for (int64_t idx = 0; idx < weights.size(); idx++) { + const account_name account(idx + 100); + initialize_account(account); + set_account_limits(account, -1, -1, weights.at(idx)); + } + + process_account_limit_updates(); + + for (int64_t idx = 0; idx < weights.size(); idx++) { + const account_name account(idx + 100); + BOOST_CHECK_EQUAL(get_account_block_cpu_limit(account), expected_limits.at(idx)); + + { // use the expected limit, should succeed ... roll it back + auto s = start_session(); + add_account_usage({account}, expected_limits.at(idx), 0, 0); + s.undo(); + } + + // use too much, and expect failure; + BOOST_REQUIRE_THROW(add_account_usage({account}, expected_limits.at(idx) + 1, 0, 0), tx_resource_exhausted); + } + } FC_LOG_AND_RETHROW(); + + /** + * create 5 accounts with different weights, verify that the capacities are as expected and that usage properly enforces them + */ + BOOST_FIXTURE_TEST_CASE(weighted_capacity_net, resource_limits_fixture) try { + const vector weights = { 234, 511, 672, 800, 1213 }; + const int64_t total = std::accumulate(std::begin(weights), std::end(weights), 0LL); + vector expected_limits; + std::transform(std::begin(weights), std::end(weights), std::back_inserter(expected_limits), [total](const auto& v){ return v * config::default_max_block_size / total; }); + + for (int64_t idx = 0; idx < weights.size(); idx++) { + const account_name account(idx + 100); + initialize_account(account); + set_account_limits(account, -1, weights.at(idx), -1 ); + } + + process_account_limit_updates(); + + for (int64_t idx = 0; idx < weights.size(); idx++) { + const account_name account(idx + 100); + BOOST_CHECK_EQUAL(get_account_block_net_limit(account), expected_limits.at(idx)); + + { // use the expected limit, should succeed ... roll it back + auto s = start_session(); + add_account_usage({account}, 0, expected_limits.at(idx), 0); + s.undo(); + } + + // use too much, and expect failure; + BOOST_REQUIRE_THROW(add_account_usage({account}, 0, expected_limits.at(idx) + 1, 0), tx_resource_exhausted); + } + } FC_LOG_AND_RETHROW(); + + + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file -- GitLab