Skip to content

Commit

Permalink
Track the number of active elections by each bucket and cap at the bu…
Browse files Browse the repository at this point in the history
…cket maximum.

The number of elections started for a particular bucket is now limited on a per-bucket basis rather than globally across all buckets.
  • Loading branch information
clemahieu committed May 15, 2024
1 parent 235f369 commit 46f9ad5
Show file tree
Hide file tree
Showing 15 changed files with 127 additions and 388 deletions.
108 changes: 6 additions & 102 deletions nano/core_test/active_elections.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ TEST (active_elections, keep_local)
nano::node_config node_config = system.default_config ();
node_config.enable_voting = false;
// Bound to 2, won't drop wallet created transactions, but good to test dropping remote
node_config.active_elections.size = 2;
node_config.priority_scheduler.bucket_maximum = 2;
// Disable frontier confirmation to allow the test to finish before
node_config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;

Expand Down Expand Up @@ -192,7 +192,10 @@ TEST (active_elections, keep_local)
ASSERT_NE (nullptr, send6);

// force-confirm blocks
nano::test::confirm (node.ledger, send6);
auto election = nano::test::start_election (system, node, send6->hash ());
ASSERT_NE (nullptr, election);
election->force_confirm ();
ASSERT_TIMELY (5s, node.active.empty ());

nano::state_block_builder builder{};
const auto receive1 = builder.make_block ()
Expand Down Expand Up @@ -227,8 +230,7 @@ TEST (active_elections, keep_local)
node.process_active (receive3);

/// bound elections, should drop after one loop
ASSERT_TIMELY_EQ (5s, node.active.size (), node_config.active_elections.size);
// ASSERT_EQ (1, node.scheduler.size ());
ASSERT_TIMELY_EQ (5s, node.active.size (), node_config.priority_scheduler.bucket_maximum);
}

TEST (inactive_votes_cache, basic)
Expand Down Expand Up @@ -1389,101 +1391,3 @@ TEST (active_elections, limit_vote_hinted_elections)
// Ensure there was no overflow of elections
ASSERT_EQ (0, node.stats.count (nano::stat::type::active_dropped, nano::stat::detail::priority));
}

/*
* Tests that when AEC is running at capacity from normal elections, it is still possible to schedule a limited number of hinted elections
*/
TEST (active_elections, allow_limited_overflow)
{
nano::test::system system;
nano::node_config config = system.default_config ();
const int aec_limit = 20;
config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
config.active_elections.size = aec_limit;
config.active_elections.hinted_limit_percentage = 20; // Should give us a limit of 4 hinted elections
auto & node = *system.add_node (config);

auto blocks = nano::test::setup_independent_blocks (system, node, aec_limit * 4);

// Split blocks in two halves
std::vector<std::shared_ptr<nano::block>> blocks1 (blocks.begin (), blocks.begin () + blocks.size () / 2);
std::vector<std::shared_ptr<nano::block>> blocks2 (blocks.begin () + blocks.size () / 2, blocks.end ());

// Even though automatic frontier confirmation is disabled, AEC is doing funny stuff and inserting elections, clear that
WAIT (1s);
node.active.clear ();
ASSERT_TRUE (node.active.empty ());

// Insert the first part of the blocks into normal election scheduler
for (auto const & block : blocks1)
{
node.scheduler.priority.activate (node.ledger.tx_begin_read (), block->account ());
}

// Ensure number of active elections reaches AEC limit and there is no overfill
ASSERT_TIMELY_EQ (5s, node.active.size (), node.active.limit (nano::election_behavior::priority));
// And it stays that way without increasing
ASSERT_ALWAYS (1s, node.active.size () == node.active.limit (nano::election_behavior::priority));

// Insert votes for the second part of the blocks, so that those are scheduled as hinted elections
for (auto const & block : blocks2)
{
// Non-final vote, so it stays in the AEC without getting confirmed
auto vote = nano::test::make_vote (nano::dev::genesis_key, { block });
node.vote_cache.insert (vote);
}

// Ensure active elections overfill AEC only up to normal + hinted limit
ASSERT_TIMELY_EQ (5s, node.active.size (), node.active.limit (nano::election_behavior::priority) + node.active.limit (nano::election_behavior::hinted));
// And it stays that way without increasing
ASSERT_ALWAYS (1s, node.active.size () == node.active.limit (nano::election_behavior::priority) + node.active.limit (nano::election_behavior::hinted));
}

/*
* Tests that when hinted elections are present in the AEC, normal scheduler adapts not to exceed the limit of all elections
*/
TEST (active_elections, allow_limited_overflow_adapt)
{
nano::test::system system;
nano::node_config config = system.default_config ();
const int aec_limit = 20;
config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
config.active_elections.size = aec_limit;
config.active_elections.hinted_limit_percentage = 20; // Should give us a limit of 4 hinted elections
auto & node = *system.add_node (config);

auto blocks = nano::test::setup_independent_blocks (system, node, aec_limit * 4);

// Split blocks in two halves
std::vector<std::shared_ptr<nano::block>> blocks1 (blocks.begin (), blocks.begin () + blocks.size () / 2);
std::vector<std::shared_ptr<nano::block>> blocks2 (blocks.begin () + blocks.size () / 2, blocks.end ());

// Even though automatic frontier confirmation is disabled, AEC is doing funny stuff and inserting elections, clear that
WAIT (1s);
node.active.clear ();
ASSERT_TRUE (node.active.empty ());

// Insert votes for the second part of the blocks, so that those are scheduled as hinted elections
for (auto const & block : blocks2)
{
// Non-final vote, so it stays in the AEC without getting confirmed
auto vote = nano::test::make_vote (nano::dev::genesis_key, { block });
node.vote_cache.insert (vote);
}

// Ensure hinted election amount is bounded by hinted limit
ASSERT_TIMELY_EQ (5s, node.active.size (), node.active.limit (nano::election_behavior::hinted));
// And it stays that way without increasing
ASSERT_ALWAYS (1s, node.active.size () == node.active.limit (nano::election_behavior::hinted));

// Insert the first part of the blocks into normal election scheduler
for (auto const & block : blocks1)
{
node.scheduler.priority.activate (node.ledger.tx_begin_read (), block->account ());
}

// Ensure number of active elections reaches AEC limit and there is no overfill
ASSERT_TIMELY_EQ (5s, node.active.size (), node.active.limit (nano::election_behavior::priority));
// And it stays that way without increasing
ASSERT_ALWAYS (1s, node.active.size () == node.active.limit (nano::election_behavior::priority));
}
93 changes: 0 additions & 93 deletions nano/core_test/election_scheduler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,96 +53,3 @@ TEST (election_scheduler, activate_one_flush)
system.nodes[0]->scheduler.priority.activate (system.nodes[0]->ledger.tx_begin_read (), nano::dev::genesis_key.pub);
ASSERT_TIMELY (5s, system.nodes[0]->active.election (send1->qualified_root ()));
}

/**
* Tests that the election scheduler and the active transactions container (AEC)
* work in sync with regards to the node configuration value "active_elections.size".
*
* The test sets up two forcefully cemented blocks -- a send on the genesis account and a receive on a second account.
* It then creates two other blocks, each a successor to one of the previous two,
* and processes them locally (without the node starting elections for them, but just saving them to disk).
*
* Elections for these latter two (B1 and B2) are started by the test code manually via `election_scheduler::activate`.
* The test expects E1 to start right off and take its seat into the AEC.
* E2 is expected not to start though (because the AEC is full), so B2 should be awaiting in the scheduler's queue.
*
* As soon as the test code manually confirms E1 (and thus evicts it out of the AEC),
* it is expected that E2 begins and the scheduler's queue becomes empty again.
*/
TEST (election_scheduler, no_vacancy)
{
nano::test::system system{};

nano::node_config config = system.default_config ();
config.active_elections.size = 1;
config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;

auto & node = *system.add_node (config);
nano::state_block_builder builder{};
nano::keypair key{};

// Activating accounts depends on confirmed dependencies. First, prepare 2 accounts
auto send = builder.make_block ()
.account (nano::dev::genesis_key.pub)
.previous (nano::dev::genesis->hash ())
.representative (nano::dev::genesis_key.pub)
.link (key.pub)
.balance (nano::dev::constants.genesis_amount - nano::Gxrb_ratio)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (nano::dev::genesis->hash ()))
.build ();
ASSERT_EQ (nano::block_status::progress, node.process (send));
node.process_confirmed (nano::election_status{ send });

auto receive = builder.make_block ()
.account (key.pub)
.previous (0)
.representative (key.pub)
.link (send->hash ())
.balance (nano::Gxrb_ratio)
.sign (key.prv, key.pub)
.work (*system.work.generate (key.pub))
.build ();
ASSERT_EQ (nano::block_status::progress, node.process (receive));
node.process_confirmed (nano::election_status{ receive });

ASSERT_TIMELY (5s, nano::test::confirmed (node, { send, receive }));

// Second, process two eligible transactions
auto block1 = builder.make_block ()
.account (nano::dev::genesis_key.pub)
.previous (send->hash ())
.representative (nano::dev::genesis_key.pub)
.link (nano::dev::genesis_key.pub)
.balance (nano::dev::constants.genesis_amount - 2 * nano::Gxrb_ratio)
.sign (nano::dev::genesis_key.prv, nano::dev::genesis_key.pub)
.work (*system.work.generate (send->hash ()))
.build ();
ASSERT_EQ (nano::block_status::progress, node.process (block1));

// There is vacancy so it should be inserted
node.scheduler.priority.activate (node.ledger.tx_begin_read (), nano::dev::genesis_key.pub);
std::shared_ptr<nano::election> election{};
ASSERT_TIMELY (5s, (election = node.active.election (block1->qualified_root ())) != nullptr);

auto block2 = builder.make_block ()
.account (key.pub)
.previous (receive->hash ())
.representative (key.pub)
.link (key.pub)
.balance (0)
.sign (key.prv, key.pub)
.work (*system.work.generate (receive->hash ()))
.build ();
ASSERT_EQ (nano::block_status::progress, node.process (block2));

// There is no vacancy so it should stay queued
node.scheduler.priority.activate (node.ledger.tx_begin_read (), key.pub);
ASSERT_TIMELY_EQ (5s, node.scheduler.priority.size (), 1);
ASSERT_EQ (node.active.election (block2->qualified_root ()), nullptr);

// Election confirmed, next in queue should begin
election->force_confirm ();
ASSERT_TIMELY (5s, node.active.election (block2->qualified_root ()) != nullptr);
ASSERT_TRUE (node.scheduler.priority.empty ());
}
Loading

0 comments on commit 46f9ad5

Please sign in to comment.