From 00a3da9a2bfadf76a921cc9a859f42e8e1dc89b3 Mon Sep 17 00:00:00 2001 From: Chalith Desaman Date: Fri, 12 Mar 2021 12:13:46 +0530 Subject: [PATCH] Request historical shards when max shard range is increased (#266) * Remove and request historical shards at the startup * Shard history requesting only at the first consensus round * Removed test log * Skip max shard seq no file in removing loop * Updated the code comments * Persisting condition changed * Fixed code comment typos * Fixed code comment typos * Resolved PR comments * Halt consensus until completing only the latest shard sync * Added meaningful comments * Resolved PR comments and updated hpfs binary * Logic enhancement and cleanup * Cleanup the code comment --- src/consensus.cpp | 18 ++++++++- src/hpfs/hpfs_serve.cpp | 2 +- src/ledger/ledger.cpp | 72 +++++++++++++++++++++++++++++++++++- src/ledger/ledger.hpp | 7 ++++ src/ledger/ledger_mount.cpp | 11 ------ src/ledger/ledger_sync.cpp | 2 + src/ledger/ledger_sync.hpp | 4 ++ test/bin/hpfs | Bin 307608 -> 256160 bytes 8 files changed, 102 insertions(+), 14 deletions(-) diff --git a/src/consensus.cpp b/src/consensus.cpp index bdf373be..91b99769 100644 --- a/src/consensus.cpp +++ b/src/consensus.cpp @@ -199,6 +199,7 @@ namespace consensus const std::string majority_shard_seq_no_str = std::to_string(majority_primary_shard_id.seq_no); const std::string sync_name = "primary shard " + majority_shard_seq_no_str; const std::string shard_path = std::string(ledger::PRIMARY_DIR).append("/").append(majority_shard_seq_no_str); + ledger::ledger_sync_worker.is_last_primary_shard_syncing = true; ledger::ledger_sync_worker.set_target_push_front(hpfs::sync_target{sync_name, majority_primary_shard_id.hash, shard_path, hpfs::BACKLOG_ITEM_TYPE::DIR}); } @@ -238,10 +239,24 @@ namespace consensus const std::string majority_shard_seq_no_str = std::to_string(majority_blob_shard_id.seq_no); const std::string sync_name = "blob shard " + majority_shard_seq_no_str; const std::string shard_path = std::string(ledger::BLOB_DIR).append("/").append(majority_shard_seq_no_str); + ledger::ledger_sync_worker.is_last_blob_shard_syncing = true; ledger::ledger_sync_worker.set_target_push_back(hpfs::sync_target{sync_name, majority_blob_shard_id.hash, shard_path, hpfs::BACKLOG_ITEM_TYPE::DIR}); } } + // If shards aren't aligned with max shard count, Do the relevant shard cleanups and requests. + // In the first consensus round sync completion after the startup. + if (!ledger::ledger_sync_worker.is_syncing && (!ledger::ctx.primary_shards_persisted || !ledger::ctx.blob_shards_persisted) && ledger::ledger_fs.acquire_rw_session() != -1) + { + if (!ledger::ctx.primary_shards_persisted) + ledger::persist_shard_history(majority_primary_shard_id.seq_no, ledger::PRIMARY_DIR); + + if (!ledger::ctx.blob_shards_persisted) + ledger::persist_shard_history(majority_blob_shard_id.seq_no, ledger::BLOB_DIR); + + ledger::ledger_fs.release_rw_session(); + } + // Proceed further only if last primary shard, last blob shard, state and patch hashes are in sync with majority. if (!is_last_primary_shard_desync && !is_last_blob_shard_desync && !is_state_desync && !is_patch_desync) { @@ -263,7 +278,8 @@ namespace consensus */ void check_sync_completion() { - if (conf::cfg.node.role == conf::ROLE::OBSERVER && !sc::contract_sync_worker.is_syncing && !ledger::ledger_sync_worker.is_syncing) + // In ledger sync we only concern about last shard sync status to proceed with consensus. + if (conf::cfg.node.role == conf::ROLE::OBSERVER && !sc::contract_sync_worker.is_syncing && !ledger::ledger_sync_worker.is_last_primary_shard_syncing && !ledger::ledger_sync_worker.is_last_blob_shard_syncing) conf::change_role(conf::ROLE::VALIDATOR); } diff --git a/src/hpfs/hpfs_serve.cpp b/src/hpfs/hpfs_serve.cpp index 37448c30..0784ef62 100644 --- a/src/hpfs/hpfs_serve.cpp +++ b/src/hpfs/hpfs_serve.cpp @@ -242,7 +242,7 @@ namespace hpfs const int fd = open(file_path.c_str(), O_RDONLY | O_CLOEXEC); if (fd == -1) { - LOG_ERROR << errno << ": Open failed. " << file_path; + LOG_ERROR << errno << ": Open failed " << file_path; result = -1; } else diff --git a/src/ledger/ledger.cpp b/src/ledger/ledger.cpp index 1a5d1deb..a8fa9e73 100644 --- a/src/ledger/ledger.cpp +++ b/src/ledger/ledger.cpp @@ -274,7 +274,7 @@ namespace ledger /** * Remove old shards that exceeds max shard range from file system. - * @param led_shard_no minimum shard number to be in history. + * @param led_shard_no Minimum shard number to be in history. */ void remove_old_shards(const uint64_t led_shard_no, std::string_view shard_parent_dir) { @@ -297,6 +297,76 @@ namespace ledger } } + /** + * Cleanup and request historical shards according to the max we can keep. + * @param shard_seq_no Latest shard sequence number. + * @param shard_parent_dir Shard parent directory. + */ + void persist_shard_history(const uint64_t shard_seq_no, std::string_view shard_parent_dir) + { + // Skip if shard cleanup and requesting has been already done. + if ((shard_parent_dir == PRIMARY_DIR && ctx.primary_shards_persisted) || (shard_parent_dir == BLOB_DIR && ctx.blob_shards_persisted)) + return; + + // Set persisted flag to true. So this cleanup won't get executed again. + shard_parent_dir == PRIMARY_DIR ? ctx.primary_shards_persisted = true : ctx.blob_shards_persisted = true; + + const std::string shard_dir_path = std::string(ledger_fs.physical_path(hpfs::RW_SESSION_NAME, shard_parent_dir)); + const uint64_t max_shard_count = shard_dir_path == PRIMARY_DIR ? conf::cfg.node.history_config.max_primary_shards : conf::cfg.node.history_config.max_blob_shards; + const std::list shard_list = util::fetch_dir_entries(shard_dir_path); + // Skip the sequence no file from the count. + uint64_t shard_count = shard_list.size() - 1; + + // First, In history custom mode remove all the historical shards which are older than the min we can keep. + if (conf::cfg.node.history == conf::HISTORY::CUSTOM && shard_seq_no >= max_shard_count) + { + for (const std::string &shard : shard_list) + { + // Skip the sequence no file. + if (("/" + shard) == SHARD_SEQ_NO_FILENAME) + continue; + + uint64_t seq_no; + if (util::stoull(shard, seq_no) != -1 && seq_no <= (shard_seq_no - max_shard_count)) + { + const std::string shard_path = std::string(shard_dir_path).append("/").append(shard); + if (util::is_dir_exists(shard_path) && util::remove_directory_recursively(shard_path) == -1) + LOG_ERROR << errno << ": Error deleting shard: " << shard; + else + shard_count--; + } + } + } + + // In full history mode request for all the historical nodes if not exists, Otherwise request if max count haven't reached + if (shard_seq_no >= shard_count && (conf::cfg.node.history == conf::HISTORY::FULL || shard_count < max_shard_count)) + { + const uint64_t seq_no = shard_seq_no - shard_count; + + const std::string prev_shard_hash_file_path = shard_dir_path + "/" + std::to_string(seq_no + 1) + PREV_SHARD_HASH_FILENAME; + const int fd = open(prev_shard_hash_file_path.c_str(), O_RDONLY | O_CLOEXEC); + if (fd == -1) + { + LOG_DEBUG << "Cannot read " << prev_shard_hash_file_path; + return; + } + + util::h32 prev_shard_hash_from_file; + // Start reading hash excluding hp_version header. + const int res = pread(fd, &prev_shard_hash_from_file, sizeof(util::h32), util::HP_VERSION_HEADER_SIZE); + close(fd); + if (res == -1) + { + LOG_ERROR << errno << ": Error reading hash file. " << prev_shard_hash_file_path; + return; + } + + const std::string sync_name = (shard_parent_dir == PRIMARY_DIR ? "primary" : "blob") + std::string(" shard ") + std::to_string(seq_no); + const std::string shard_path = std::string(shard_parent_dir).append("/").append(std::to_string(seq_no)); + ledger_sync_worker.set_target_push_back(hpfs::sync_target{sync_name, prev_shard_hash_from_file, shard_path, hpfs::BACKLOG_ITEM_TYPE::DIR}); + } + } + /** * Save raw data from the consensused proposal. A blob file is only created if there is any user inputs or contract outputs * to save disk space. diff --git a/src/ledger/ledger.hpp b/src/ledger/ledger.hpp index 948a36ec..68271e0c 100644 --- a/src/ledger/ledger.hpp +++ b/src/ledger/ledger.hpp @@ -25,6 +25,11 @@ namespace ledger p2p::sequence_hash last_blob_shard_id; public: + // These flags will be marked as true after doing the shards cleanup and requesting + // at the first consensus round to align with the max shard counts. + std::atomic primary_shards_persisted = false; + std::atomic blob_shards_persisted = false; + const p2p::sequence_hash get_lcl_id() { std::shared_lock lock(lcl_mutex); @@ -87,6 +92,8 @@ namespace ledger void remove_old_shards(const uint64_t led_shard_no, std::string_view shard_parent_dir); + void persist_shard_history(const uint64_t shard_seq_no, std::string_view shard_parent_dir); + int get_last_ledger_and_update_context(std::string_view session_name, const p2p::sequence_hash &last_primary_shard_id); int get_last_shard_info(std::string_view session_name, p2p::sequence_hash &last_shard_id, const std::string &shard_parent_dir); diff --git a/src/ledger/ledger_mount.cpp b/src/ledger/ledger_mount.cpp index 1c8b108e..206c6cc4 100644 --- a/src/ledger/ledger_mount.cpp +++ b/src/ledger/ledger_mount.cpp @@ -27,17 +27,6 @@ namespace ledger return -1; } - if (conf::cfg.node.history == conf::HISTORY::CUSTOM) - { - //Remove old primary shards that exceeds max shard range. - if (last_primary_shard_id.seq_no >= conf::cfg.node.history_config.max_primary_shards) - remove_old_shards(last_primary_shard_id.seq_no - conf::cfg.node.history_config.max_primary_shards + 1, PRIMARY_DIR); - - //Remove old blob shards that exceeds max shard range. - if (last_blob_shard_id.seq_no >= conf::cfg.node.history_config.max_blob_shards) - remove_old_shards(last_blob_shard_id.seq_no - conf::cfg.node.history_config.max_blob_shards + 1, BLOB_DIR); - } - if (release_rw_session() == -1) { LOG_ERROR << "Failed to release rw session at mount " << mount_dir << "."; diff --git a/src/ledger/ledger_sync.cpp b/src/ledger/ledger_sync.cpp index 6d9d0b92..9dc64d4c 100644 --- a/src/ledger/ledger_sync.cpp +++ b/src/ledger/ledger_sync.cpp @@ -68,6 +68,7 @@ namespace ledger } ctx.set_last_primary_shard_id(updated_primary_shard_id); last_primary_shard_seq_no = synced_shard_seq_no; + is_last_primary_shard_syncing = false; } if (conf::cfg.node.history == conf::HISTORY::FULL || // Sync all shards if this is a full history node. @@ -116,6 +117,7 @@ namespace ledger last_blob_shard_seq_no = synced_shard_seq_no; ctx.set_last_blob_shard_id(p2p::sequence_hash{synced_shard_seq_no, synced_target.hash}); + is_last_blob_shard_syncing = false; } if (conf::cfg.node.history == conf::HISTORY::FULL || // Sync all blob shards if this is a full history node. diff --git a/src/ledger/ledger_sync.hpp b/src/ledger/ledger_sync.hpp index 14000376..4500b7a1 100644 --- a/src/ledger/ledger_sync.hpp +++ b/src/ledger/ledger_sync.hpp @@ -13,6 +13,10 @@ namespace ledger private: void swap_collected_responses(); void on_current_sync_state_acheived(const hpfs::sync_target &synced_target); + + public: + std::atomic is_last_primary_shard_syncing = false; + std::atomic is_last_blob_shard_syncing = false; }; } // namespace ledger #endif \ No newline at end of file diff --git a/test/bin/hpfs b/test/bin/hpfs index e67bfc32ad2f24f3aa2db6746e09aea176690b8a..c2a5b7dc5078cc3f17912c86ded61fec11ce84a4 100755 GIT binary patch delta 3773 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 delta 43736 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

*tv2>QO?;dZo-K03%c&aV7~g)~ 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