提交 a04607ac 编写于 作者: B Bart Wyatt

- 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
上级 fb50bb96
......@@ -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.
......
......@@ -16,13 +16,20 @@ namespace eosio { namespace chain { namespace resource_limits {
void initialize_account( const account_name& account );
void add_account_usage( const vector<account_name>& 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;
......
......@@ -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
......
......@@ -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>([](resource_limits_config_object& config){
const auto& config = _db.create<resource_limits_config_object>([](resource_limits_config_object& config){
// see default settings in the declaration
});
_db.create<resource_limits_state_object>([](resource_limits_state_object& state){
_db.create<resource_limits_state_object>([&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<account_name>& acco
const auto& limits = _db.get<resource_limits_object,by_owner>( 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<account_name>& 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<resource_limits_index>();
auto& by_owner_index = multi_index.indices().get<by_owner>();
......@@ -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<resource_limits_state_object>();
_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<resource_limits_state_object>();
return state.virtual_cpu_limit;
}
uint64_t resource_limits_manager::get_virtual_block_net_limit() const {
const auto& state = _db.get<resource_limits_state_object>();
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<resource_limits_state_object>();
const auto& usage = _db.get<resource_usage_object, by_owner>(name);
const auto& limits = _db.get<resource_limits_object, by_owner>(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<resource_limits_state_object>();
const auto& usage = _db.get<resource_usage_object, by_owner>(name);
const auto& limits = _db.get<resource_limits_object, by_owner>(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
......@@ -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)
......
#include <chainbase/chainbase.hpp>
#include <fc/filesystem.hpp>
#include <memory>
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<uint64_t MAX_SIZE>
struct chainbase_fixture {
chainbase_fixture()
: _tempdir()
, _db(std::make_unique<chainbase::database>(_tempdir.path(), chainbase::database::read_write, MAX_SIZE))
{
}
~chainbase_fixture()
{
_db.reset();
_tempdir.remove();
}
fc::temp_directory _tempdir;
std::unique_ptr<chainbase::database> _db;
};
} } } // eosio::chain::test
\ No newline at end of file
#include <boost/test/unit_test.hpp>
#include <eosio/chain/resource_limits.hpp>
#include <eosio/chain/config.hpp>
#include <eosio/chain/test/chainbase_fixture.hpp>
#include <algorithm>
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<int64_t> weights = { 234, 511, 672, 800, 1213 };
const int64_t total = std::accumulate(std::begin(weights), std::end(weights), 0LL);
vector<int64_t> 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<int64_t> weights = { 234, 511, 672, 800, 1213 };
const int64_t total = std::accumulate(std::begin(weights), std::end(weights), 0LL);
vector<int64_t> 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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册