Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimistic elections #4111

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5eb0feb
Optimistic scheduler
pwojcikdev Jan 25, 2023
35011b9
Add toml configuration
pwojcikdev Jan 25, 2023
1edc99e
Add unit tests
pwojcikdev Jan 25, 2023
7d3b2ed
Refactor `active_transactions` election behavior handling
pwojcikdev Jan 26, 2023
103bcdf
Make use of the refactored functionality in `election_scheduler`
pwojcikdev Jan 26, 2023
8d924ee
Renaming
pwojcikdev Jan 26, 2023
c0e4302
Improve active elections stat tracking
pwojcikdev Feb 2, 2023
a004a33
Formatting
pwojcikdev Feb 8, 2023
2176199
Move `head_block` to ledger class
pwojcikdev Feb 8, 2023
0d74cab
Merge branch 'develop' into optimistic-elections-2-rebased
clemahieu Feb 14, 2023
ec5fecc
Add elapsed helper function
pwojcikdev Feb 22, 2023
5cf169f
Delay optimistic scheduling
pwojcikdev Feb 22, 2023
c244949
Move to multi index container
pwojcikdev Feb 21, 2023
395408c
Use `network_constants` for configuration
pwojcikdev Feb 21, 2023
67c69ec
Periodically check for predicate
pwojcikdev Feb 21, 2023
3a87e18
Comments
pwojcikdev Feb 21, 2023
e8e7d02
Increase activation delay + queue size
pwojcikdev Feb 22, 2023
bb4f14f
Stats renaming
pwojcikdev Feb 22, 2023
f8587f6
Merge remote-tracking branch 'nano/develop' into optimistic-elections…
pwojcikdev Feb 22, 2023
c47b86b
Do not notify when candidate is inserter, since cooldown won't allow …
pwojcikdev Feb 22, 2023
becfb76
Increase dev network delay
pwojcikdev Feb 22, 2023
35cca28
Merge branch 'develop' into optimistic-elections-2-rebased
clemahieu Feb 23, 2023
2213183
Fix merge issue.
clemahieu Feb 23, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nano/core_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ add_executable(
network.cpp
network_filter.cpp
node.cpp
optimistic_scheduler.cpp
processing_queue.cpp
processor_service.cpp
peer_container.cpp
Expand Down
2 changes: 2 additions & 0 deletions nano/core_test/active_transactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ TEST (active_transactions, inactive_votes_cache_election_start)
nano::test::system system;
nano::node_config node_config (nano::test::get_available_port (), system.logging);
node_config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
node_config.optimistic_scheduler.enabled = false;
auto & node = *system.add_node (node_config);
nano::block_hash latest (node.latest (nano::dev::genesis_key.pub));
nano::keypair key1, key2;
Expand Down Expand Up @@ -1418,6 +1419,7 @@ TEST (active_transactions, limit_vote_hinted_elections)
nano::node_config config = system.default_config ();
const int aec_limit = 10;
config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
config.optimistic_scheduler.enabled = false;
config.active_elections_size = aec_limit;
config.active_elections_hinted_limit_percentage = 10; // Should give us a limit of 1 hinted election
auto & node = *system.add_node (config);
Expand Down
105 changes: 105 additions & 0 deletions nano/core_test/optimistic_scheduler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#include <nano/node/election.hpp>
#include <nano/test_common/chains.hpp>
#include <nano/test_common/system.hpp>
#include <nano/test_common/testutil.hpp>

#include <gtest/gtest.h>

#include <chrono>

using namespace std::chrono_literals;

/*
* Ensure account gets activated for a single unconfirmed account chain
*/
TEST (optimistic_scheduler, activate_one)
{
nano::test::system system{};
auto & node = *system.add_node ();

// Needs to be greater than optimistic scheduler `gap_threshold`
const int howmany_blocks = 64;

auto chains = nano::test::setup_chains (system, node, /* single chain */ 1, howmany_blocks, nano::dev::genesis_key, /* do not confirm */ false);
auto & [account, blocks] = chains.front ();

// Confirm block towards at the beginning the chain, so gap between confirmation and account frontier is larger than `gap_threshold`
ASSERT_TRUE (nano::test::confirm (node, { blocks.at (11) }));
ASSERT_TIMELY (5s, nano::test::confirmed (node, { blocks.at (11) }));

// Ensure unconfirmed account head block gets activated
auto const & block = blocks.back ();
ASSERT_TIMELY (5s, node.active.active (block->hash ()));
ASSERT_TRUE (node.active.election (block->qualified_root ())->behavior () == nano::election_behavior::optimistic);
}

/*
* Ensure account gets activated for a single unconfirmed account chain with nothing yet confirmed
*/
TEST (optimistic_scheduler, activate_one_zero_conf)
{
nano::test::system system{};
auto & node = *system.add_node ();

// Can be smaller than optimistic scheduler `gap_threshold`
// This is meant to activate short account chains (eg. binary tree spam leaf accounts)
const int howmany_blocks = 6;

auto chains = nano::test::setup_chains (system, node, /* single chain */ 1, howmany_blocks, nano::dev::genesis_key, /* do not confirm */ false);
auto & [account, blocks] = chains.front ();

// Ensure unconfirmed account head block gets activated
auto const & block = blocks.back ();
ASSERT_TIMELY (5s, node.active.active (block->hash ()));
ASSERT_TRUE (node.active.election (block->qualified_root ())->behavior () == nano::election_behavior::optimistic);
}

/*
* Ensure account gets activated for a multiple unconfirmed account chains
*/
TEST (optimistic_scheduler, activate_many)
{
nano::test::system system{};
auto & node = *system.add_node ();

// Needs to be greater than optimistic scheduler `gap_threshold`
const int howmany_blocks = 64;
const int howmany_chains = 16;

auto chains = nano::test::setup_chains (system, node, howmany_chains, howmany_blocks, nano::dev::genesis_key, /* do not confirm */ false);

// Ensure all unconfirmed accounts head block gets activated
ASSERT_TIMELY (5s, std::all_of (chains.begin (), chains.end (), [&] (auto const & entry) {
auto const & [account, blocks] = entry;
auto const & block = blocks.back ();
return node.active.active (block->hash ()) && node.active.election (block->qualified_root ())->behavior () == nano::election_behavior::optimistic;
}));
}

/*
* Ensure accounts with some blocks already confirmed and with less than `gap_threshold` blocks do not get activated
*/
TEST (optimistic_scheduler, under_gap_threshold)
{
nano::test::system system{};
nano::node_config config = system.default_config ();
config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
auto & node = *system.add_node (config);

// Must be smaller than optimistic scheduler `gap_threshold`
const int howmany_blocks = 64;

auto chains = nano::test::setup_chains (system, node, /* single chain */ 1, howmany_blocks, nano::dev::genesis_key, /* do not confirm */ false);
auto & [account, blocks] = chains.front ();

// Confirm block towards the end of the chain, so gap between confirmation and account frontier is less than `gap_threshold`
ASSERT_TRUE (nano::test::confirm (node, { blocks.at (55) }));
ASSERT_TIMELY (5s, nano::test::confirmed (node, { blocks.at (55) }));

// Manually trigger backlog scan
node.backlog.trigger ();

// Ensure unconfirmed account head block gets activated
auto const & block = blocks.back ();
ASSERT_NEVER (3s, node.active.active (block->hash ()));
}
13 changes: 13 additions & 0 deletions nano/core_test/toml.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,10 @@ TEST (toml, daemon_config_deserialize_defaults)
ASSERT_EQ (conf.node.rocksdb_config.enable, defaults.node.rocksdb_config.enable);
ASSERT_EQ (conf.node.rocksdb_config.memory_multiplier, defaults.node.rocksdb_config.memory_multiplier);
ASSERT_EQ (conf.node.rocksdb_config.io_threads, defaults.node.rocksdb_config.io_threads);

ASSERT_EQ (conf.node.optimistic_scheduler.enabled, defaults.node.optimistic_scheduler.enabled);
ASSERT_EQ (conf.node.optimistic_scheduler.gap_threshold, defaults.node.optimistic_scheduler.gap_threshold);
ASSERT_EQ (conf.node.optimistic_scheduler.max_size, defaults.node.optimistic_scheduler.max_size);
}

TEST (toml, optional_child)
Expand Down Expand Up @@ -519,6 +523,11 @@ TEST (toml, daemon_config_deserialize_no_defaults)
max_databases = 999
map_size = 999

[node.optimistic_scheduler]
enabled = false
gap_threshold = 999
max_size = 999

[node.rocksdb]
enable = true
memory_multiplier = 3
Expand Down Expand Up @@ -682,6 +691,10 @@ TEST (toml, daemon_config_deserialize_no_defaults)
ASSERT_EQ (nano::rocksdb_config::using_rocksdb_in_tests (), defaults.node.rocksdb_config.enable);
ASSERT_NE (conf.node.rocksdb_config.memory_multiplier, defaults.node.rocksdb_config.memory_multiplier);
ASSERT_NE (conf.node.rocksdb_config.io_threads, defaults.node.rocksdb_config.io_threads);

ASSERT_NE (conf.node.optimistic_scheduler.enabled, defaults.node.optimistic_scheduler.enabled);
ASSERT_NE (conf.node.optimistic_scheduler.gap_threshold, defaults.node.optimistic_scheduler.gap_threshold);
ASSERT_NE (conf.node.optimistic_scheduler.max_size, defaults.node.optimistic_scheduler.max_size);
}

/** There should be no required values **/
Expand Down
4 changes: 4 additions & 0 deletions nano/lib/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ class network_constants
telemetry_cache_cutoff = 2000ms;
telemetry_request_interval = 500ms;
telemetry_broadcast_interval = 500ms;
optimistic_activation_delay = 2s;
}
}

Expand Down Expand Up @@ -302,6 +303,9 @@ class network_constants
/** Telemetry data older than this value is considered stale */
std::chrono::milliseconds telemetry_cache_cutoff{ 1000 * 130 }; // 2 * `telemetry_broadcast_interval` + some margin

/** How much to delay activation of optimistic elections to avoid interfering with election scheduler */
std::chrono::seconds optimistic_activation_delay{ 30 };

/** Returns the network this object contains values for */
nano::networks network () const
{
Expand Down
2 changes: 2 additions & 0 deletions nano/lib/stats_enums.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ enum class type : uint8_t
backlog,
unchecked,
election_scheduler,
optimistic_scheduler,

_last // Must be the last enum
};
Expand Down Expand Up @@ -159,6 +160,7 @@ enum class detail : uint8_t
// election types
normal,
hinted,
optimistic,

// received messages
invalid_header,
Expand Down
2 changes: 2 additions & 0 deletions nano/lib/threading.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ std::string nano::thread_role::get_string (nano::thread_role::name role)
break;
case nano::thread_role::name::telemetry:
thread_role_name_string = "Telemetry";
case nano::thread_role::name::optimistic_scheduler:
thread_role_name_string = "Optimistic";
break;
default:
debug_assert (false && "nano::thread_role::get_string unhandled thread role");
Expand Down
1 change: 1 addition & 0 deletions nano/lib/threading.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ namespace thread_role
vote_generator_queue,
bootstrap_server,
telemetry,
optimistic_scheduler,
};

/*
Expand Down
20 changes: 20 additions & 0 deletions nano/lib/utility.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,24 @@ constexpr TARGET_TYPE narrow_cast (SOURCE_TYPE const & val)

// Issue #3748
void sort_options_description (const boost::program_options::options_description & source, boost::program_options::options_description & target);

using clock = std::chrono::steady_clock;

/**
* Check whether time elapsed between `last` and `now` is greater than `duration`
*/
template <typename Duration>
bool elapsed (nano::clock::time_point const & last, Duration duration, nano::clock::time_point const & now)
{
return last + duration < now;
}

/**
* Check whether time elapsed since `last` is greater than `duration`
*/
template <typename Duration>
bool elapsed (nano::clock::time_point const & last, Duration duration)
{
return elapsed (last, duration, nano::clock::now ());
}
}
2 changes: 2 additions & 0 deletions nano/node/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ add_library(
openclconfig.cpp
openclwork.hpp
openclwork.cpp
optimistic_scheduler.hpp
optimistic_scheduler.cpp
peer_exclusion.hpp
peer_exclusion.cpp
portmapping.hpp
Expand Down
16 changes: 11 additions & 5 deletions nano/node/active_transactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ int64_t nano::active_transactions::limit (nano::election_behavior behavior) cons
const uint64_t limit = node.config.active_elections_hinted_limit_percentage * node.config.active_elections_size / 100;
return static_cast<int64_t> (limit);
}
case nano::election_behavior::optimistic:
{
const uint64_t limit = node.config.active_elections_optimistic_limit_percentage * node.config.active_elections_size / 100;
return static_cast<int64_t> (limit);
}
}

debug_assert (false, "unknown election behavior");
Expand All @@ -210,8 +215,8 @@ int64_t nano::active_transactions::vacancy (nano::election_behavior behavior) co
case nano::election_behavior::normal:
return limit () - static_cast<int64_t> (roots.size ());
case nano::election_behavior::hinted:
return limit (nano::election_behavior::hinted) - count_by_behavior[nano::election_behavior::hinted];
;
case nano::election_behavior::optimistic:
return limit (behavior) - count_by_behavior[behavior];
}
debug_assert (false); // Unknown enum
return 0;
Expand Down Expand Up @@ -261,6 +266,7 @@ void nano::active_transactions::request_confirm (nano::unique_lock<nano::mutex>

void nano::active_transactions::cleanup_election (nano::unique_lock<nano::mutex> & lock_a, std::shared_ptr<nano::election> election)
{
debug_assert (!mutex.try_lock ());
debug_assert (lock_a.owns_lock ());

node.stats.inc (completion_type (*election), nano::to_stat_detail (election->behavior ()));
Expand Down Expand Up @@ -368,6 +374,7 @@ nano::election_insertion_result nano::active_transactions::insert (const std::sh

nano::election_insertion_result nano::active_transactions::insert_impl (nano::unique_lock<nano::mutex> & lock_a, std::shared_ptr<nano::block> const & block_a, nano::election_behavior election_behavior_a, std::function<void (std::shared_ptr<nano::block> const &)> const & confirmation_action_a)
{
debug_assert (!mutex.try_lock ());
debug_assert (lock_a.owns_lock ());
debug_assert (block_a->has_sideband ());
nano::election_insertion_result result;
Expand Down Expand Up @@ -594,7 +601,7 @@ bool nano::active_transactions::publish (std::shared_ptr<nano::block> const & bl
{
cache->fill (election);
}
node.stats.inc (nano::stat::type::election, nano::stat::detail::election_block_conflict);
node.stats.inc (nano::stat::type::active, nano::stat::detail::election_block_conflict);
}
}
return result;
Expand Down Expand Up @@ -645,8 +652,6 @@ void nano::active_transactions::add_inactive_vote_cache (nano::block_hash const
if (node.ledger.weight (vote->account) > node.minimum_principal_weight ())
{
node.inactive_vote_cache.vote (hash, vote);

node.stats.inc (nano::stat::type::vote_cache, nano::stat::detail::vote_processed);
}
}

Expand Down Expand Up @@ -676,6 +681,7 @@ std::unique_ptr<nano::container_info_component> nano::collect_container_info (ac
composite->add_component (std::make_unique<container_info_leaf> (container_info{ "election_winner_details", active_transactions.election_winner_details_size (), sizeof (decltype (active_transactions.election_winner_details)::value_type) }));
composite->add_component (std::make_unique<container_info_leaf> (container_info{ "normal", static_cast<std::size_t> (active_transactions.count_by_behavior[nano::election_behavior::normal]), 0 }));
composite->add_component (std::make_unique<container_info_leaf> (container_info{ "hinted", static_cast<std::size_t> (active_transactions.count_by_behavior[nano::election_behavior::hinted]), 0 }));
composite->add_component (std::make_unique<container_info_leaf> (container_info{ "optimistic", static_cast<std::size_t> (active_transactions.count_by_behavior[nano::election_behavior::optimistic]), 0 }));

composite->add_component (active_transactions.recently_confirmed.collect_container_info ("recently_confirmed"));
composite->add_component (active_transactions.recently_cemented.collect_container_info ("recently_cemented"));
Expand Down
22 changes: 21 additions & 1 deletion nano/node/election.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,23 @@ bool nano::election::state_change (nano::election::state_t expected_a, nano::ele
return result;
}

std::chrono::milliseconds nano::election::confirm_req_time () const
{
switch (behavior ())
{
case election_behavior::normal:
case election_behavior::hinted:
return base_latency () * 5;
case election_behavior::optimistic:
return base_latency () * 2;
}
debug_assert (false);
return {};
}

void nano::election::send_confirm_req (nano::confirmation_solicitor & solicitor_a)
{
if ((base_latency () * 5) < (std::chrono::steady_clock::now () - last_req))
if (confirm_req_time () < (std::chrono::steady_clock::now () - last_req))
{
nano::lock_guard<nano::mutex> guard{ mutex };
if (!solicitor_a.add (*this))
Expand Down Expand Up @@ -225,8 +239,10 @@ std::chrono::milliseconds nano::election::time_to_live () const
case election_behavior::normal:
return std::chrono::milliseconds (5 * 60 * 1000);
case election_behavior::hinted:
case election_behavior::optimistic:
return std::chrono::milliseconds (30 * 1000);
}
debug_assert (false);
return {};
}

Expand Down Expand Up @@ -646,6 +662,10 @@ nano::stat::detail nano::to_stat_detail (nano::election_behavior behavior)
{
return nano::stat::detail::hinted;
}
case nano::election_behavior::optimistic:
{
return nano::stat::detail::optimistic;
}
}

debug_assert (false, "unknown election behavior");
Expand Down
20 changes: 18 additions & 2 deletions nano/node/election.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,19 @@ class election_vote_result final
enum class election_behavior
{
normal,
hinted
/**
* Hinted elections:
* - shorter timespan
* - limited space inside AEC
*/
hinted,
/**
* Optimistic elections:
* - shorter timespan
* - limited space inside AEC
* - more frequent confirmation requests
*/
optimistic,
};

nano::stat::detail to_stat_detail (nano::election_behavior);
Expand Down Expand Up @@ -161,10 +173,14 @@ class election final : public std::enable_shared_from_this<nano::election>
void remove_block (nano::block_hash const &);
bool replace_by_weight (nano::unique_lock<nano::mutex> & lock_a, nano::block_hash const &);
std::chrono::milliseconds time_to_live () const;
/*
/**
* Calculates minimum time delay between subsequent votes when processing non-final votes
*/
std::chrono::seconds cooldown_time (nano::uint128_t weight) const;
/**
* Calculates time delay between broadcasting confirmation requests
*/
std::chrono::milliseconds confirm_req_time () const;

private:
std::unordered_map<nano::block_hash, std::shared_ptr<nano::block>> last_blocks;
Expand Down
Loading