Optimistic elections (#4111)

This commit is contained in:
Piotr Wójcik 2023-02-23 16:36:10 +01:00 committed by GitHub
commit e1c893b7dc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 537 additions and 22 deletions

View file

@ -33,6 +33,7 @@ add_executable(
network.cpp network.cpp
network_filter.cpp network_filter.cpp
node.cpp node.cpp
optimistic_scheduler.cpp
processing_queue.cpp processing_queue.cpp
processor_service.cpp processor_service.cpp
peer_container.cpp peer_container.cpp

View file

@ -428,6 +428,7 @@ TEST (active_transactions, inactive_votes_cache_election_start)
nano::test::system system; nano::test::system system;
nano::node_config node_config (nano::test::get_available_port (), system.logging); nano::node_config node_config (nano::test::get_available_port (), system.logging);
node_config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled; node_config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
node_config.optimistic_scheduler.enabled = false;
auto & node = *system.add_node (node_config); auto & node = *system.add_node (node_config);
nano::block_hash latest (node.latest (nano::dev::genesis_key.pub)); nano::block_hash latest (node.latest (nano::dev::genesis_key.pub));
nano::keypair key1, key2; nano::keypair key1, key2;
@ -1418,6 +1419,7 @@ TEST (active_transactions, limit_vote_hinted_elections)
nano::node_config config = system.default_config (); nano::node_config config = system.default_config ();
const int aec_limit = 10; const int aec_limit = 10;
config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled; config.frontiers_confirmation = nano::frontiers_confirmation_mode::disabled;
config.optimistic_scheduler.enabled = false;
config.active_elections_size = aec_limit; config.active_elections_size = aec_limit;
config.active_elections_hinted_limit_percentage = 10; // Should give us a limit of 1 hinted election config.active_elections_hinted_limit_percentage = 10; // Should give us a limit of 1 hinted election
auto & node = *system.add_node (config); auto & node = *system.add_node (config);

View file

@ -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 ()));
}

View file

@ -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.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.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.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) TEST (toml, optional_child)
@ -519,6 +523,11 @@ TEST (toml, daemon_config_deserialize_no_defaults)
max_databases = 999 max_databases = 999
map_size = 999 map_size = 999
[node.optimistic_scheduler]
enabled = false
gap_threshold = 999
max_size = 999
[node.rocksdb] [node.rocksdb]
enable = true enable = true
memory_multiplier = 3 memory_multiplier = 3
@ -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_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.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.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 **/ /** There should be no required values **/

View file

@ -251,6 +251,7 @@ public:
telemetry_cache_cutoff = 2000ms; telemetry_cache_cutoff = 2000ms;
telemetry_request_interval = 500ms; telemetry_request_interval = 500ms;
telemetry_broadcast_interval = 500ms; telemetry_broadcast_interval = 500ms;
optimistic_activation_delay = 2s;
} }
} }
@ -302,6 +303,9 @@ public:
/** Telemetry data older than this value is considered stale */ /** Telemetry data older than this value is considered stale */
std::chrono::milliseconds telemetry_cache_cutoff{ 1000 * 130 }; // 2 * `telemetry_broadcast_interval` + some margin 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 */ /** Returns the network this object contains values for */
nano::networks network () const nano::networks network () const
{ {

View file

@ -42,6 +42,7 @@ enum class type : uint8_t
backlog, backlog,
unchecked, unchecked,
election_scheduler, election_scheduler,
optimistic_scheduler,
_last // Must be the last enum _last // Must be the last enum
}; };
@ -159,6 +160,7 @@ enum class detail : uint8_t
// election types // election types
normal, normal,
hinted, hinted,
optimistic,
// received messages // received messages
invalid_header, invalid_header,

View file

@ -107,6 +107,8 @@ std::string nano::thread_role::get_string (nano::thread_role::name role)
break; break;
case nano::thread_role::name::telemetry: case nano::thread_role::name::telemetry:
thread_role_name_string = "Telemetry"; thread_role_name_string = "Telemetry";
case nano::thread_role::name::optimistic_scheduler:
thread_role_name_string = "Optimistic";
break; break;
default: default:
debug_assert (false && "nano::thread_role::get_string unhandled thread role"); debug_assert (false && "nano::thread_role::get_string unhandled thread role");

View file

@ -48,6 +48,7 @@ namespace thread_role
vote_generator_queue, vote_generator_queue,
bootstrap_server, bootstrap_server,
telemetry, telemetry,
optimistic_scheduler,
}; };
/* /*

View file

@ -203,4 +203,24 @@ constexpr TARGET_TYPE narrow_cast (SOURCE_TYPE const & val)
// Issue #3748 // Issue #3748
void sort_options_description (const boost::program_options::options_description & source, boost::program_options::options_description & target); 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 ());
}
} }

View file

@ -143,6 +143,8 @@ add_library(
openclconfig.cpp openclconfig.cpp
openclwork.hpp openclwork.hpp
openclwork.cpp openclwork.cpp
optimistic_scheduler.hpp
optimistic_scheduler.cpp
peer_exclusion.hpp peer_exclusion.hpp
peer_exclusion.cpp peer_exclusion.cpp
portmapping.hpp portmapping.hpp

View file

@ -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; const uint64_t limit = node.config.active_elections_hinted_limit_percentage * node.config.active_elections_size / 100;
return static_cast<int64_t> (limit); 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"); debug_assert (false, "unknown election behavior");
@ -210,8 +215,8 @@ int64_t nano::active_transactions::vacancy (nano::election_behavior behavior) co
case nano::election_behavior::normal: case nano::election_behavior::normal:
return limit () - static_cast<int64_t> (roots.size ()); return limit () - static_cast<int64_t> (roots.size ());
case nano::election_behavior::hinted: 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 debug_assert (false); // Unknown enum
return 0; return 0;
@ -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) 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 ()); debug_assert (lock_a.owns_lock ());
node.stats.inc (completion_type (*election), nano::to_stat_detail (election->behavior ())); node.stats.inc (completion_type (*election), nano::to_stat_detail (election->behavior ()));
@ -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) 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 (lock_a.owns_lock ());
debug_assert (block_a->has_sideband ()); debug_assert (block_a->has_sideband ());
nano::election_insertion_result result; nano::election_insertion_result result;
@ -594,7 +601,7 @@ bool nano::active_transactions::publish (std::shared_ptr<nano::block> const & bl
{ {
cache->fill (election); 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; return result;
@ -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 ()) if (node.ledger.weight (vote->account) > node.minimum_principal_weight ())
{ {
node.inactive_vote_cache.vote (hash, vote); node.inactive_vote_cache.vote (hash, vote);
node.stats.inc (nano::stat::type::vote_cache, nano::stat::detail::vote_processed);
} }
} }
@ -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{ "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{ "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{ "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_confirmed.collect_container_info ("recently_confirmed"));
composite->add_component (active_transactions.recently_cemented.collect_container_info ("recently_cemented")); composite->add_component (active_transactions.recently_cemented.collect_container_info ("recently_cemented"));

View file

@ -124,9 +124,23 @@ bool nano::election::state_change (nano::election::state_t expected_a, nano::ele
return result; 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) 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 }; nano::lock_guard<nano::mutex> guard{ mutex };
if (!solicitor_a.add (*this)) if (!solicitor_a.add (*this))
@ -225,8 +239,10 @@ std::chrono::milliseconds nano::election::time_to_live () const
case election_behavior::normal: case election_behavior::normal:
return std::chrono::milliseconds (5 * 60 * 1000); return std::chrono::milliseconds (5 * 60 * 1000);
case election_behavior::hinted: case election_behavior::hinted:
case election_behavior::optimistic:
return std::chrono::milliseconds (30 * 1000); return std::chrono::milliseconds (30 * 1000);
} }
debug_assert (false);
return {}; return {};
} }
@ -646,6 +662,10 @@ nano::stat::detail nano::to_stat_detail (nano::election_behavior behavior)
{ {
return nano::stat::detail::hinted; return nano::stat::detail::hinted;
} }
case nano::election_behavior::optimistic:
{
return nano::stat::detail::optimistic;
}
} }
debug_assert (false, "unknown election behavior"); debug_assert (false, "unknown election behavior");

View file

@ -46,7 +46,19 @@ public:
enum class election_behavior enum class election_behavior
{ {
normal, 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); nano::stat::detail to_stat_detail (nano::election_behavior);
@ -161,10 +173,14 @@ private:
void remove_block (nano::block_hash const &); void remove_block (nano::block_hash const &);
bool replace_by_weight (nano::unique_lock<nano::mutex> & lock_a, 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; std::chrono::milliseconds time_to_live () const;
/* /**
* Calculates minimum time delay between subsequent votes when processing non-final votes * Calculates minimum time delay between subsequent votes when processing non-final votes
*/ */
std::chrono::seconds cooldown_time (nano::uint128_t weight) const; 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: private:
std::unordered_map<nano::block_hash, std::shared_ptr<nano::block>> last_blocks; std::unordered_map<nano::block_hash, std::shared_ptr<nano::block>> last_blocks;

View file

@ -156,7 +156,6 @@ void nano::election_scheduler::run ()
auto block = priority.top (); auto block = priority.top ();
priority.pop (); priority.pop ();
lock.unlock (); lock.unlock ();
std::shared_ptr<nano::election> election;
stats.inc (nano::stat::type::election_scheduler, nano::stat::detail::insert_priority); stats.inc (nano::stat::type::election_scheduler, nano::stat::detail::insert_priority);
auto result = node.active.insert (block); auto result = node.active.insert (block);
if (result.inserted) if (result.inserted)

View file

@ -74,7 +74,7 @@ bool nano::hinted_scheduler::run_one (nano::uint128_t const & minimum_tally)
// We check for AEC vacancy inside our predicate // We check for AEC vacancy inside our predicate
auto result = node.active.insert (block, nano::election_behavior::hinted); auto result = node.active.insert (block, nano::election_behavior::hinted);
stats.inc (nano::stat::type::hinting, result.inserted ? nano::stat::detail::hinted : nano::stat::detail::insert_failed); stats.inc (nano::stat::type::hinting, result.inserted ? nano::stat::detail::insert : nano::stat::detail::insert_failed);
return result.inserted; // Return whether block was inserted return result.inserted; // Return whether block was inserted
} }

View file

@ -195,6 +195,7 @@ nano::node::node (boost::asio::io_context & io_ctx_a, boost::filesystem::path co
generator{ config, ledger, wallets, vote_processor, history, network, stats, /* non-final */ false }, generator{ config, ledger, wallets, vote_processor, history, network, stats, /* non-final */ false },
final_generator{ config, ledger, wallets, vote_processor, history, network, stats, /* final */ true }, final_generator{ config, ledger, wallets, vote_processor, history, network, stats, /* final */ true },
active (*this, confirmation_height_processor), active (*this, confirmation_height_processor),
optimistic{ config.optimistic_scheduler, *this, ledger, active, network_params.network, stats },
scheduler{ *this, stats }, scheduler{ *this, stats },
hinting{ nano::nodeconfig_to_hinted_scheduler_config (config), *this, inactive_vote_cache, active, online_reps, stats }, hinting{ nano::nodeconfig_to_hinted_scheduler_config (config), *this, inactive_vote_cache, active, online_reps, stats },
aggregator (config, stats, generator, final_generator, history, ledger, wallets, active), aggregator (config, stats, generator, final_generator, history, ledger, wallets, active),
@ -216,6 +217,7 @@ nano::node::node (boost::asio::io_context & io_ctx_a, boost::filesystem::path co
backlog.activate_callback.add ([this] (nano::transaction const & transaction, nano::account const & account, nano::account_info const & account_info, nano::confirmation_height_info const & conf_info) { backlog.activate_callback.add ([this] (nano::transaction const & transaction, nano::account const & account, nano::account_info const & account_info, nano::confirmation_height_info const & conf_info) {
scheduler.activate (account, transaction); scheduler.activate (account, transaction);
optimistic.activate (account, account_info, conf_info);
}); });
if (!init_error ()) if (!init_error ())
@ -224,6 +226,7 @@ nano::node::node (boost::asio::io_context & io_ctx_a, boost::filesystem::path co
active.vacancy_update = [this] () { active.vacancy_update = [this] () {
scheduler.notify (); scheduler.notify ();
hinting.notify (); hinting.notify ();
optimistic.notify ();
}; };
wallets.observer = [this] (bool active) { wallets.observer = [this] (bool active) {
@ -684,6 +687,7 @@ void nano::node::start ()
active.start (); active.start ();
generator.start (); generator.start ();
final_generator.start (); final_generator.start ();
optimistic.start ();
scheduler.start (); scheduler.start ();
backlog.start (); backlog.start ();
hinting.start (); hinting.start ();
@ -711,6 +715,7 @@ void nano::node::stop ()
aggregator.stop (); aggregator.stop ();
vote_processor.stop (); vote_processor.stop ();
scheduler.stop (); scheduler.stop ();
optimistic.stop ();
hinting.stop (); hinting.stop ();
active.stop (); active.stop ();
generator.stop (); generator.stop ();

View file

@ -22,6 +22,7 @@
#include <nano/node/node_observers.hpp> #include <nano/node/node_observers.hpp>
#include <nano/node/nodeconfig.hpp> #include <nano/node/nodeconfig.hpp>
#include <nano/node/online_reps.hpp> #include <nano/node/online_reps.hpp>
#include <nano/node/optimistic_scheduler.hpp>
#include <nano/node/portmapping.hpp> #include <nano/node/portmapping.hpp>
#include <nano/node/repcrawler.hpp> #include <nano/node/repcrawler.hpp>
#include <nano/node/request_aggregator.hpp> #include <nano/node/request_aggregator.hpp>
@ -181,6 +182,7 @@ public:
nano::vote_generator generator; nano::vote_generator generator;
nano::vote_generator final_generator; nano::vote_generator final_generator;
nano::active_transactions active; nano::active_transactions active;
nano::optimistic_scheduler optimistic;
nano::election_scheduler scheduler; nano::election_scheduler scheduler;
nano::hinted_scheduler hinting; nano::hinted_scheduler hinting;
nano::request_aggregator aggregator; nano::request_aggregator aggregator;

View file

@ -194,6 +194,10 @@ nano::error nano::node_config::serialize_toml (nano::tomlconfig & toml) const
lmdb_config.serialize_toml (lmdb_l); lmdb_config.serialize_toml (lmdb_l);
toml.put_child ("lmdb", lmdb_l); toml.put_child ("lmdb", lmdb_l);
nano::tomlconfig optimistic_l;
optimistic_scheduler.serialize (optimistic_l);
toml.put_child ("optimistic_scheduler", optimistic_l);
return toml.get_error (); return toml.get_error ();
} }
@ -245,6 +249,12 @@ nano::error nano::node_config::deserialize_toml (nano::tomlconfig & toml)
rocksdb_config.deserialize_toml (rocksdb_config_l); rocksdb_config.deserialize_toml (rocksdb_config_l);
} }
if (toml.has_key ("optimistic_scheduler"))
{
auto config_l = toml.get_required_child ("optimistic_scheduler");
optimistic_scheduler.deserialize (config_l);
}
if (toml.has_key ("work_peers")) if (toml.has_key ("work_peers"))
{ {
work_peers.clear (); work_peers.clear ();

View file

@ -9,6 +9,7 @@
#include <nano/lib/stats.hpp> #include <nano/lib/stats.hpp>
#include <nano/node/ipc/ipc_config.hpp> #include <nano/node/ipc/ipc_config.hpp>
#include <nano/node/logging.hpp> #include <nano/node/logging.hpp>
#include <nano/node/optimistic_scheduler.hpp>
#include <nano/node/websocketconfig.hpp> #include <nano/node/websocketconfig.hpp>
#include <nano/secure/common.hpp> #include <nano/secure/common.hpp>
@ -44,6 +45,7 @@ public:
nano::account random_representative () const; nano::account random_representative () const;
nano::network_params network_params; nano::network_params network_params;
std::optional<uint16_t> peering_port{}; std::optional<uint16_t> peering_port{};
nano::optimistic_scheduler_config optimistic_scheduler;
nano::logging logging; nano::logging logging;
std::vector<std::pair<std::string, uint16_t>> work_peers; std::vector<std::pair<std::string, uint16_t>> work_peers;
std::vector<std::pair<std::string, uint16_t>> secondary_work_peers{ { "127.0.0.1", 8076 } }; /* Default of nano-pow-server */ std::vector<std::pair<std::string, uint16_t>> secondary_work_peers{ { "127.0.0.1", 8076 } }; /* Default of nano-pow-server */
@ -85,8 +87,12 @@ public:
/** Timeout for initiated async operations */ /** Timeout for initiated async operations */
std::chrono::seconds tcp_io_timeout{ (network_params.network.is_dev_network () && !is_sanitizer_build ()) ? std::chrono::seconds (5) : std::chrono::seconds (15) }; std::chrono::seconds tcp_io_timeout{ (network_params.network.is_dev_network () && !is_sanitizer_build ()) ? std::chrono::seconds (5) : std::chrono::seconds (15) };
std::chrono::nanoseconds pow_sleep_interval{ 0 }; std::chrono::nanoseconds pow_sleep_interval{ 0 };
// TODO: Move related settings to `active_transactions_config` class
std::size_t active_elections_size{ 5000 }; std::size_t active_elections_size{ 5000 };
std::size_t active_elections_hinted_limit_percentage{ 20 }; // Limit of hinted elections as percentage of active_elections_size /** Limit of hinted elections as percentage of `active_elections_size` */
std::size_t active_elections_hinted_limit_percentage{ 20 };
/** Limit of optimistic elections as percentage of `active_elections_size` */
std::size_t active_elections_optimistic_limit_percentage{ 10 };
/** Default maximum incoming TCP connections, including realtime network & bootstrap */ /** Default maximum incoming TCP connections, including realtime network & bootstrap */
unsigned tcp_incoming_connections_max{ 2048 }; unsigned tcp_incoming_connections_max{ 2048 };
bool use_memory_pools{ true }; bool use_memory_pools{ true };

View file

@ -0,0 +1,184 @@
#include <nano/lib/stats.hpp>
#include <nano/lib/tomlconfig.hpp>
#include <nano/node/node.hpp>
#include <nano/node/optimistic_scheduler.hpp>
nano::optimistic_scheduler::optimistic_scheduler (optimistic_scheduler_config const & config_a, nano::node & node_a, nano::ledger & ledger_a, nano::active_transactions & active_a, nano::network_constants const & network_constants_a, nano::stats & stats_a) :
config{ config_a },
node{ node_a },
ledger{ ledger_a },
active{ active_a },
network_constants{ network_constants_a },
stats{ stats_a }
{
}
nano::optimistic_scheduler::~optimistic_scheduler ()
{
// Thread must be stopped before destruction
debug_assert (!thread.joinable ());
}
void nano::optimistic_scheduler::start ()
{
if (!config.enabled)
{
return;
}
debug_assert (!thread.joinable ());
thread = std::thread{ [this] () {
nano::thread_role::set (nano::thread_role::name::optimistic_scheduler);
run ();
} };
}
void nano::optimistic_scheduler::stop ()
{
{
nano::lock_guard<nano::mutex> guard{ mutex };
stopped = true;
}
notify ();
nano::join_or_pass (thread);
}
void nano::optimistic_scheduler::notify ()
{
condition.notify_all ();
}
bool nano::optimistic_scheduler::activate_predicate (const nano::account_info & account_info, const nano::confirmation_height_info & conf_info) const
{
// Chain with a big enough gap between account frontier and confirmation frontier
if (account_info.block_count - conf_info.height > config.gap_threshold)
{
return true;
}
// Account with nothing confirmed yet
if (conf_info.height == 0)
{
return true;
}
return false;
}
bool nano::optimistic_scheduler::activate (const nano::account & account, const nano::account_info & account_info, const nano::confirmation_height_info & conf_info)
{
if (!config.enabled)
{
return false;
}
debug_assert (account_info.block_count >= conf_info.height);
if (activate_predicate (account_info, conf_info))
{
{
nano::lock_guard<nano::mutex> lock{ mutex };
// Prevent duplicate candidate accounts
if (candidates.get<tag_account> ().contains (account))
{
return false; // Not activated
}
// Limit candidates container size
if (candidates.size () >= config.max_size)
{
return false; // Not activated
}
stats.inc (nano::stat::type::optimistic_scheduler, nano::stat::detail::activated);
candidates.push_back ({ account, nano::clock::now () });
}
return true; // Activated
}
return false; // Not activated
}
bool nano::optimistic_scheduler::predicate () const
{
debug_assert (!mutex.try_lock ());
if (active.vacancy (nano::election_behavior::optimistic) <= 0)
{
return false;
}
if (candidates.empty ())
{
return false;
}
auto candidate = candidates.front ();
bool result = nano::elapsed (candidate.timestamp, network_constants.optimistic_activation_delay);
return result;
}
void nano::optimistic_scheduler::run ()
{
nano::unique_lock<nano::mutex> lock{ mutex };
while (!stopped)
{
stats.inc (nano::stat::type::optimistic_scheduler, nano::stat::detail::loop);
if (predicate ())
{
auto transaction = ledger.store.tx_begin_read ();
while (predicate ())
{
debug_assert (!candidates.empty ());
auto candidate = candidates.front ();
candidates.pop_front ();
lock.unlock ();
run_one (transaction, candidate);
lock.lock ();
}
}
condition.wait_for (lock, network_constants.optimistic_activation_delay / 2, [this] () {
return stopped || predicate ();
});
}
}
void nano::optimistic_scheduler::run_one (nano::transaction const & transaction, entry const & candidate)
{
auto block = ledger.head_block (transaction, candidate.account);
if (block)
{
// Ensure block is not already confirmed
if (!node.block_confirmed_or_being_confirmed (block->hash ()))
{
// Try to insert it into AEC
// We check for AEC vacancy inside our predicate
auto result = node.active.insert (block, nano::election_behavior::optimistic);
stats.inc (nano::stat::type::optimistic_scheduler, result.inserted ? nano::stat::detail::insert : nano::stat::detail::insert_failed);
}
}
}
/*
* optimistic_scheduler_config
*/
nano::error nano::optimistic_scheduler_config::deserialize (nano::tomlconfig & toml)
{
toml.get ("enabled", enabled);
toml.get ("gap_threshold", gap_threshold);
toml.get ("max_size", max_size);
return toml.get_error ();
}
nano::error nano::optimistic_scheduler_config::serialize (nano::tomlconfig & toml) const
{
toml.put ("enable", enabled, "Enable or disable optimistic elections\ntype:bool");
toml.put ("gap_threshold", gap_threshold, "Minimum difference between confirmation frontier and account frontier to become a candidate for optimistic confirmation\ntype:uint64");
toml.put ("max_size", max_size, "Maximum number of candidates stored in memory\ntype:uint64");
return toml.get_error ();
}

View file

@ -0,0 +1,108 @@
#pragma once
#include <nano/lib/locks.hpp>
#include <nano/lib/numbers.hpp>
#include <nano/lib/timer.hpp>
#include <nano/lib/utility.hpp>
#include <nano/secure/common.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/member.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index/sequenced_index.hpp>
#include <boost/multi_index_container.hpp>
#include <condition_variable>
#include <memory>
#include <queue>
#include <thread>
#include <vector>
namespace mi = boost::multi_index;
namespace nano
{
class node;
class ledger;
class active_transactions;
class optimistic_scheduler_config final
{
public:
nano::error deserialize (nano::tomlconfig & toml);
nano::error serialize (nano::tomlconfig & toml) const;
public:
bool enabled{ true };
/** Minimum difference between confirmation frontier and account frontier to become a candidate for optimistic confirmation */
std::size_t gap_threshold{ 32 };
/** Maximum number of candidates stored in memory */
std::size_t max_size{ 1024 * 64 };
};
class optimistic_scheduler final
{
struct entry;
public:
optimistic_scheduler (optimistic_scheduler_config const &, nano::node &, nano::ledger &, nano::active_transactions &, nano::network_constants const & network_constants, nano::stats &);
~optimistic_scheduler ();
void start ();
void stop ();
/**
* Called from backlog population to process accounts with unconfirmed blocks
*/
bool activate (nano::account const &, nano::account_info const &, nano::confirmation_height_info const &);
/**
* Notify about changes in AEC vacancy
*/
void notify ();
private:
bool activate_predicate (nano::account_info const &, nano::confirmation_height_info const &) const;
bool predicate () const;
void run ();
void run_one (nano::transaction const &, entry const & candidate);
private: // Dependencies
optimistic_scheduler_config const & config;
nano::node & node;
nano::ledger & ledger;
nano::active_transactions & active;
nano::network_constants const & network_constants;
nano::stats & stats;
private:
struct entry
{
nano::account account;
nano::clock::time_point timestamp;
};
// clang-format off
class tag_sequenced {};
class tag_account {};
using ordered_candidates = boost::multi_index_container<entry,
mi::indexed_by<
mi::sequenced<mi::tag<tag_sequenced>>,
mi::hashed_unique<mi::tag<tag_account>,
mi::member<entry, nano::account, &entry::account>>
>>;
// clang-format on
/** Accounts eligible for optimistic scheduling */
ordered_candidates candidates;
bool stopped{ false };
nano::condition_variable condition;
mutable nano::mutex mutex;
std::thread thread;
};
}

View file

@ -2,7 +2,7 @@
using namespace std::chrono_literals; using namespace std::chrono_literals;
nano::block_list_t nano::test::setup_chain (nano::test::system & system, nano::node & node, int count, nano::keypair target) nano::block_list_t nano::test::setup_chain (nano::test::system & system, nano::node & node, int count, nano::keypair target, bool confirm)
{ {
auto latest = node.latest (target.pub); auto latest = node.latest (target.pub);
auto balance = node.balance (target.pub); auto balance = node.balance (target.pub);
@ -30,14 +30,17 @@ nano::block_list_t nano::test::setup_chain (nano::test::system & system, nano::n
EXPECT_TRUE (nano::test::process (node, blocks)); EXPECT_TRUE (nano::test::process (node, blocks));
if (confirm)
{
// Confirm whole chain at once // Confirm whole chain at once
EXPECT_TIMELY (5s, nano::test::confirm (node, { blocks.back () })); EXPECT_TIMELY (5s, nano::test::confirm (node, { blocks.back () }));
EXPECT_TIMELY (5s, nano::test::confirmed (node, blocks)); EXPECT_TIMELY (5s, nano::test::confirmed (node, blocks));
}
return blocks; return blocks;
} }
std::vector<std::pair<nano::account, nano::block_list_t>> nano::test::setup_chains (nano::test::system & system, nano::node & node, int chain_count, int block_count, nano::keypair source) std::vector<std::pair<nano::account, nano::block_list_t>> nano::test::setup_chains (nano::test::system & system, nano::node & node, int chain_count, int block_count, nano::keypair source, bool confirm)
{ {
auto latest = node.latest (source.pub); auto latest = node.latest (source.pub);
auto balance = node.balance (source.pub); auto balance = node.balance (source.pub);
@ -69,12 +72,16 @@ std::vector<std::pair<nano::account, nano::block_list_t>> nano::test::setup_chai
latest = send->hash (); latest = send->hash ();
// Ensure blocks are in the ledger and confirmed
EXPECT_TRUE (nano::test::process (node, { send, open })); EXPECT_TRUE (nano::test::process (node, { send, open }));
if (confirm)
{
// Ensure blocks are in the ledger and confirmed
EXPECT_TIMELY (5s, nano::test::confirm (node, { send, open })); EXPECT_TIMELY (5s, nano::test::confirm (node, { send, open }));
EXPECT_TIMELY (5s, nano::test::confirmed (node, { send, open })); EXPECT_TIMELY (5s, nano::test::confirmed (node, { send, open }));
}
auto added_blocks = nano::test::setup_chain (system, node, block_count, key); auto added_blocks = nano::test::setup_chain (system, node, block_count, key, confirm);
auto blocks = block_list_t{ open }; auto blocks = block_list_t{ open };
blocks.insert (blocks.end (), added_blocks.begin (), added_blocks.end ()); blocks.insert (blocks.end (), added_blocks.begin (), added_blocks.end ());

View file

@ -17,13 +17,13 @@ namespace nano::test
* Creates `count` random 1 raw send blocks in a `target` account chain * Creates `count` random 1 raw send blocks in a `target` account chain
* @returns created blocks * @returns created blocks
*/ */
nano::block_list_t setup_chain (nano::test::system & system, nano::node & node, int count, nano::keypair target = nano::dev::genesis_key); nano::block_list_t setup_chain (nano::test::system & system, nano::node & node, int count, nano::keypair target = nano::dev::genesis_key, bool confirm = true);
/** /**
* Creates `chain_count` account chains, each with `block_count` 1 raw random send blocks, all accounts are seeded from `source` account * Creates `chain_count` account chains, each with `block_count` 1 raw random send blocks, all accounts are seeded from `source` account
* @returns list of created accounts and their blocks * @returns list of created accounts and their blocks
*/ */
std::vector<std::pair<nano::account, nano::block_list_t>> setup_chains (nano::test::system & system, nano::node & node, int chain_count, int block_count, nano::keypair source = nano::dev::genesis_key); std::vector<std::pair<nano::account, nano::block_list_t>> setup_chains (nano::test::system & system, nano::node & node, int chain_count, int block_count, nano::keypair source = nano::dev::genesis_key, bool confirm = true);
/** /**
* Creates `count` 1 raw send blocks from `source` account, each to randomly created account * Creates `count` 1 raw send blocks from `source` account, each to randomly created account