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

Hinted scheduler improvements #4312

Merged
merged 13 commits into from
Oct 17, 2023
5 changes: 5 additions & 0 deletions nano/lib/stats_enums.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,11 @@ enum class detail : uint8_t

// hinting
missing_block,
dependent_unconfirmed,
already_confirmed,
activate,
activate_immediate,
dependent_activated,

// bootstrap server
response,
Expand Down
1 change: 0 additions & 1 deletion nano/node/active_transactions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,6 @@ void nano::active_transactions::cleanup_election (nano::unique_lock<nano::mutex>
auto erased (blocks.erase (hash));
(void)erased;
debug_assert (erased == 1);
node.vote_cache.erase (hash);
}
roots.get<tag_root> ().erase (roots.get<tag_root> ().find (election->qualified_root));

Expand Down
7 changes: 6 additions & 1 deletion nano/node/node.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1273,9 +1273,14 @@ bool nano::node::block_confirmed (nano::block_hash const & hash_a)
return ledger.block_confirmed (transaction, hash_a);
}

bool nano::node::block_confirmed_or_being_confirmed (nano::store::transaction const & transaction, nano::block_hash const & hash_a)
{
return confirmation_height_processor.is_processing_block (hash_a) || ledger.block_confirmed (transaction, hash_a);
}

bool nano::node::block_confirmed_or_being_confirmed (nano::block_hash const & hash_a)
{
return confirmation_height_processor.is_processing_block (hash_a) || ledger.block_confirmed (store.tx_begin_read (), hash_a);
return block_confirmed_or_being_confirmed (store.tx_begin_read (), hash_a);
}

void nano::node::ongoing_online_weight_calculation_queue ()
Expand Down
1 change: 1 addition & 0 deletions nano/node/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class node final : public std::enable_shared_from_this<nano::node>
void add_initial_peers ();
void start_election (std::shared_ptr<nano::block> const & block);
bool block_confirmed (nano::block_hash const &);
bool block_confirmed_or_being_confirmed (nano::store::transaction const &, nano::block_hash const &);
bool block_confirmed_or_being_confirmed (nano::block_hash const &);
void do_rpc_callback (boost::asio::ip::tcp::resolver::iterator i_a, std::string const &, uint16_t, std::shared_ptr<std::string> const &, std::shared_ptr<std::string> const &, std::shared_ptr<boost::asio::ip::tcp::resolver> const &);
void ongoing_online_weight_calculation ();
Expand Down
158 changes: 114 additions & 44 deletions nano/node/scheduler/hinted.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#include <nano/node/scheduler/hinted.hpp>

nano::scheduler::hinted::config::config (nano::node_config const & config) :
vote_cache_check_interval_ms{ config.network_params.network.is_dev_network () ? 100u : 1000u }
vote_cache_check_interval_ms{ config.network_params.network.is_dev_network () ? 100u : 5000u }
{
}

Expand Down Expand Up @@ -48,87 +48,157 @@ void nano::scheduler::hinted::notify ()
condition.notify_all ();
}

bool nano::scheduler::hinted::predicate (nano::uint128_t const & minimum_tally) const
bool nano::scheduler::hinted::predicate () const
{
// Check if there is space inside AEC for a new hinted election
if (active.vacancy (nano::election_behavior::hinted) > 0)
{
// Check if there is any vote cache entry surpassing our minimum vote tally threshold
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree this should be removed.

if (vote_cache.peek (minimum_tally))
{
return true;
}
}
return false;
return active.vacancy (nano::election_behavior::hinted) > 0;
}

bool nano::scheduler::hinted::run_one (nano::uint128_t const & minimum_tally)
void nano::scheduler::hinted::activate (const nano::store::transaction & transaction, const nano::block_hash & hash, bool check_dependents)
{
if (auto top = vote_cache.pop (minimum_tally); top)
std::stack<nano::block_hash> stack;
stack.push (hash);

while (!stack.empty ())
{
const auto hash = top->hash (); // Hash of block we want to hint
const nano::block_hash current_hash = stack.top ();
stack.pop ();

// Check if block exists
auto block = node.block (hash);
if (block != nullptr)
if (auto block = node.store.block.get (transaction, current_hash); block)
{
// Ensure block is not already confirmed
if (!node.block_confirmed_or_being_confirmed (hash))
if (node.block_confirmed_or_being_confirmed (transaction, current_hash))
{
// Try to insert it into AEC as hinted election
// We check for AEC vacancy inside our predicate
auto result = node.active.insert (block, nano::election_behavior::hinted);

stats.inc (nano::stat::type::hinting, result.inserted ? nano::stat::detail::insert : nano::stat::detail::insert_failed);
stats.inc (nano::stat::type::hinting, nano::stat::detail::already_confirmed);
continue; // Move on to the next item in the stack
}

return result.inserted; // Return whether block was inserted
if (check_dependents)
{
// Perform a depth-first search of the dependency graph
if (!node.ledger.dependents_confirmed (transaction, *block))
{
stats.inc (nano::stat::type::hinting, nano::stat::detail::dependent_unconfirmed);
auto dependents = node.ledger.dependent_blocks (transaction, *block);
for (const auto & dependent_hash : dependents)
{
if (!dependent_hash.is_zero ())
{
stack.push (dependent_hash); // Add dependent block to the stack
}
}
continue; // Move on to the next item in the stack
}
}

// Try to insert it into AEC as hinted election
auto result = node.active.insert (block, nano::election_behavior::hinted);
stats.inc (nano::stat::type::hinting, result.inserted ? nano::stat::detail::insert : nano::stat::detail::insert_failed);
}
else
{
// Missing block in ledger to start an election
node.bootstrap_block (hash);

stats.inc (nano::stat::type::hinting, nano::stat::detail::missing_block);
node.bootstrap_block (current_hash);
}
}
}

void nano::scheduler::hinted::run_iterative ()
{
const auto minimum_tally = tally_threshold ();
const auto minimum_final_tally = final_tally_threshold ();

auto transaction = node.store.tx_begin_read ();

for (auto const & entry : vote_cache.top (minimum_tally))
{
if (!predicate ())
{
return;
}

if (cooldown (entry.hash))
{
continue;
}

// Check dependents only if cached tally is lower than quorum
if (entry.final_tally < minimum_final_tally)
{
// Ensure all dependent blocks are already confirmed before activating
stats.inc (nano::stat::type::hinting, nano::stat::detail::activate);
activate (transaction, entry.hash, /* activate dependents */ true);
}
else
{
// Blocks with a vote tally higher than quorum, can be activated and confirmed immediately
stats.inc (nano::stat::type::hinting, nano::stat::detail::activate_immediate);
activate (transaction, entry.hash, false);
}
}
return false;
}

void nano::scheduler::hinted::run ()
{
nano::unique_lock<nano::mutex> lock{ mutex };
while (!stopped)
{
// It is possible that if we are waiting long enough this tally becomes outdated due to changes in trended online weight
// However this is only used here for hinting, election does independent tally calculation, so there is no need to ensure it's always up-to-date
const auto minimum_tally = tally_threshold ();
stats.inc (nano::stat::type::hinting, nano::stat::detail::loop);

// Periodically wakeup for condition checking
// We are not notified every time new vote arrives in inactive vote cache as that happens too often
condition.wait_for (lock, std::chrono::milliseconds (config_m.vote_cache_check_interval_ms), [this, minimum_tally] () {
return stopped || predicate (minimum_tally);
});
condition.wait_for (lock, config.check_interval);

debug_assert ((std::this_thread::yield (), true)); // Introduce some random delay in debug builds

if (!stopped)
{
// We don't need the lock when running main loop
lock.unlock ();

if (predicate (minimum_tally))
if (predicate ())
{
run_one (minimum_tally);
run_iterative ();
}

lock.lock ();
}
}
}

nano::uint128_t nano::scheduler::hinted::tally_threshold () const
{
auto min_tally = (online_reps.trended () / 100) * node.config.election_hint_weight_percent;
return min_tally;
// auto min_tally = (online_reps.trended () / 100) * node.config.election_hint_weight_percent;
// return min_tally;

return 0;
}

nano::uint128_t nano::scheduler::hinted::final_tally_threshold () const
{
auto quorum = online_reps.delta ();
return quorum;
}

bool nano::scheduler::hinted::cooldown (const nano::block_hash & hash)
{
auto const now = std::chrono::steady_clock::now ();
auto const cooldown = std::chrono::seconds{ 15 };

// Check if the hash is still in the cooldown period using the hashed index
auto const & hashed_index = cooldowns_m.get<tag_hash> ();
if (auto it = hashed_index.find (hash); it != hashed_index.end ())
{
if (it->timeout > now)
{
return true; // Needs cooldown
}
cooldowns_m.erase (it); // Entry is outdated, so remove it
}

// Insert the new entry
cooldowns_m.insert ({ hash, now + cooldown });

// Trim old entries
auto & seq_index = cooldowns_m.get<tag_timeout> ();
while (!seq_index.empty () && seq_index.begin ()->timeout <= now)
{
seq_index.erase (seq_index.begin ());
}

return false; // No need to cooldown
}
38 changes: 36 additions & 2 deletions nano/node/scheduler/hinted.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@
#include <nano/lib/numbers.hpp>
#include <nano/secure/common.hpp>

#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/ordered_index.hpp>
#include <boost/multi_index_container.hpp>

#include <chrono>
#include <condition_variable>
#include <thread>
#include <unordered_map>

namespace nano
{
Expand Down Expand Up @@ -43,11 +49,13 @@ class hinted final
void notify ();

private:
bool predicate (nano::uint128_t const & minimum_tally) const;
bool predicate () const;
void run ();
bool run_one (nano::uint128_t const & minimum_tally);
void run_iterative ();
void activate (nano::store::transaction const &, nano::block_hash const & hash, bool check_dependents);

nano::uint128_t tally_threshold () const;
nano::uint128_t final_tally_threshold () const;

private: // Dependencies
nano::node & node;
Expand All @@ -63,5 +71,31 @@ class hinted final
nano::condition_variable condition;
mutable nano::mutex mutex;
std::thread thread;

private:
bool cooldown (nano::block_hash const & hash);

struct cooldown_entry
{
nano::block_hash hash;
std::chrono::steady_clock::time_point timeout;
};

// clang-format off
class tag_hash {};
class tag_timeout {};
// clang-format on

// clang-format off
using ordered_cooldowns = boost::multi_index_container<cooldown_entry,
mi::indexed_by<
mi::hashed_unique<mi::tag<tag_hash>,
mi::member<cooldown_entry, nano::block_hash, &cooldown_entry::hash>>,
mi::ordered_non_unique<mi::tag<tag_timeout>,
mi::member<cooldown_entry, std::chrono::steady_clock::time_point, &cooldown_entry::timeout>>
>>;
// clang-format on

ordered_cooldowns cooldowns_m;
};
}
Loading