-
Notifications
You must be signed in to change notification settings - Fork 120
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
change(state): Refactor format upgrades into trait #9263
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -6,7 +6,7 @@ use std::{ | |||||||||
thread::{self, JoinHandle}, | ||||||||||
}; | ||||||||||
|
||||||||||
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender, TryRecvError}; | ||||||||||
use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender}; | ||||||||||
use semver::Version; | ||||||||||
use tracing::Span; | ||||||||||
|
||||||||||
|
@@ -20,21 +20,78 @@ use zebra_chain::{ | |||||||||
|
||||||||||
use DbFormatChange::*; | ||||||||||
|
||||||||||
use crate::{ | ||||||||||
constants::latest_version_for_adding_subtrees, | ||||||||||
service::finalized_state::{DiskWriteBatch, ZebraDb}, | ||||||||||
}; | ||||||||||
use crate::service::finalized_state::ZebraDb; | ||||||||||
|
||||||||||
pub(crate) mod add_subtrees; | ||||||||||
pub(crate) mod cache_genesis_roots; | ||||||||||
pub(crate) mod fix_tree_key_type; | ||||||||||
pub(crate) mod no_migration; | ||||||||||
pub(crate) mod prune_trees; | ||||||||||
pub(crate) mod tree_keys_and_caches_upgrade; | ||||||||||
|
||||||||||
#[cfg(not(feature = "indexer"))] | ||||||||||
pub(crate) mod drop_tx_locs_by_spends; | ||||||||||
|
||||||||||
#[cfg(feature = "indexer")] | ||||||||||
pub(crate) mod track_tx_locs_by_spends; | ||||||||||
|
||||||||||
/// Defines method signature for running disk format upgrades. | ||||||||||
pub trait DiskFormatUpgrade { | ||||||||||
/// Returns the version at which this upgrade is applied. | ||||||||||
fn version(&self) -> Version; | ||||||||||
|
||||||||||
/// Returns the description of this upgrade. | ||||||||||
fn description(&self) -> &'static str; | ||||||||||
|
||||||||||
/// Runs disk format upgrade. | ||||||||||
fn run( | ||||||||||
&self, | ||||||||||
initial_tip_height: Height, | ||||||||||
db: &ZebraDb, | ||||||||||
cancel_receiver: &Receiver<CancelFormatChange>, | ||||||||||
) -> Result<(), CancelFormatChange>; | ||||||||||
|
||||||||||
/// Check that state has been upgraded to this format correctly. | ||||||||||
/// | ||||||||||
/// # Panics | ||||||||||
/// | ||||||||||
/// If the state has not been upgraded to this format correctly. | ||||||||||
Comment on lines
+55
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
fn validate( | ||||||||||
&self, | ||||||||||
_db: &ZebraDb, | ||||||||||
_cancel_receiver: &Receiver<CancelFormatChange>, | ||||||||||
) -> Result<Result<(), String>, CancelFormatChange> { | ||||||||||
Ok(Ok(())) | ||||||||||
} | ||||||||||
|
||||||||||
/// Prepare for disk format upgrade. | ||||||||||
fn prepare( | ||||||||||
&self, | ||||||||||
_initial_tip_height: Height, | ||||||||||
_upgrade_db: &ZebraDb, | ||||||||||
_cancel_receiver: &Receiver<CancelFormatChange>, | ||||||||||
_older_disk_version: &Version, | ||||||||||
) -> Result<(), CancelFormatChange> { | ||||||||||
Ok(()) | ||||||||||
} | ||||||||||
|
||||||||||
/// Returns true if the [`DiskFormatUpgrade`] needs to run a migration on existing data in the db. | ||||||||||
fn needs_migration(&self) -> bool { | ||||||||||
true | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
fn format_upgrades() -> Vec<Box<dyn DiskFormatUpgrade>> { | ||||||||||
// Note: Disk format upgrades must be run in order. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be good to check if they upgrades are really in order and panic if they're not
Suggested change
|
||||||||||
vec![ | ||||||||||
Box::new(prune_trees::PruneTrees), | ||||||||||
Box::new(add_subtrees::AddSubtrees), | ||||||||||
Box::new(tree_keys_and_caches_upgrade::FixTreeKeyTypeAndCacheGenesisRoots), | ||||||||||
// Value balance upgrade | ||||||||||
Box::new(no_migration::NoMigration::new(26, 0, 0)), | ||||||||||
] | ||||||||||
} | ||||||||||
|
||||||||||
/// The kind of database format change or validity check we're performing. | ||||||||||
#[derive(Clone, Debug, Eq, PartialEq)] | ||||||||||
pub enum DbFormatChange { | ||||||||||
|
@@ -474,132 +531,39 @@ impl DbFormatChange { | |||||||||
return Ok(()); | ||||||||||
}; | ||||||||||
|
||||||||||
// Note commitment tree de-duplication database upgrade task. | ||||||||||
|
||||||||||
let version_for_pruning_trees = | ||||||||||
Version::parse("25.1.1").expect("Hardcoded version string should be valid."); | ||||||||||
|
||||||||||
// Check if we need to prune the note commitment trees in the database. | ||||||||||
if older_disk_version < &version_for_pruning_trees { | ||||||||||
let timer = CodeTimer::start(); | ||||||||||
|
||||||||||
// Prune duplicate Sapling note commitment trees. | ||||||||||
|
||||||||||
// The last tree we checked. | ||||||||||
let mut last_tree = db | ||||||||||
.sapling_tree_by_height(&Height(0)) | ||||||||||
.expect("Checked above that the genesis block is in the database."); | ||||||||||
|
||||||||||
// Run through all the possible duplicate trees in the finalized chain. | ||||||||||
// The block after genesis is the first possible duplicate. | ||||||||||
for (height, tree) in db.sapling_tree_by_height_range(Height(1)..=initial_tip_height) { | ||||||||||
// Return early if there is a cancel signal. | ||||||||||
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) { | ||||||||||
return Err(CancelFormatChange); | ||||||||||
} | ||||||||||
|
||||||||||
// Delete any duplicate trees. | ||||||||||
if tree == last_tree { | ||||||||||
let mut batch = DiskWriteBatch::new(); | ||||||||||
batch.delete_sapling_tree(db, &height); | ||||||||||
db.write_batch(batch) | ||||||||||
.expect("Deleting Sapling note commitment trees should always succeed."); | ||||||||||
} | ||||||||||
|
||||||||||
// Compare against the last tree to find unique trees. | ||||||||||
last_tree = tree; | ||||||||||
// Apply or validate format upgrades | ||||||||||
for upgrade in format_upgrades() { | ||||||||||
if older_disk_version >= &upgrade.version() { | ||||||||||
upgrade | ||||||||||
.validate(db, cancel_receiver)? | ||||||||||
.expect("failed to validate db format"); | ||||||||||
continue; | ||||||||||
Comment on lines
+536
to
+540
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a bit confused about this. The original code does not seem to do validate if the disk version >= upgrade version. Is this required or just good to have? What is the performance impact? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change was wrong, I thought some of the upgrades were being validated here even when they weren't being applied, but none are.
It shouldn't have a significant performance impact, it won't block anything. These will still always run at startup anyway but should run after any required format upgrades have been applied. |
||||||||||
} | ||||||||||
|
||||||||||
// Prune duplicate Orchard note commitment trees. | ||||||||||
|
||||||||||
// The last tree we checked. | ||||||||||
let mut last_tree = db | ||||||||||
.orchard_tree_by_height(&Height(0)) | ||||||||||
.expect("Checked above that the genesis block is in the database."); | ||||||||||
|
||||||||||
// Run through all the possible duplicate trees in the finalized chain. | ||||||||||
// The block after genesis is the first possible duplicate. | ||||||||||
for (height, tree) in db.orchard_tree_by_height_range(Height(1)..=initial_tip_height) { | ||||||||||
// Return early if there is a cancel signal. | ||||||||||
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) { | ||||||||||
return Err(CancelFormatChange); | ||||||||||
} | ||||||||||
|
||||||||||
// Delete any duplicate trees. | ||||||||||
if tree == last_tree { | ||||||||||
let mut batch = DiskWriteBatch::new(); | ||||||||||
batch.delete_orchard_tree(db, &height); | ||||||||||
db.write_batch(batch) | ||||||||||
.expect("Deleting Orchard note commitment trees should always succeed."); | ||||||||||
} | ||||||||||
|
||||||||||
// Compare against the last tree to find unique trees. | ||||||||||
last_tree = tree; | ||||||||||
} | ||||||||||
|
||||||||||
// Before marking the state as upgraded, check that the upgrade completed successfully. | ||||||||||
Self::check_for_duplicate_trees(db, cancel_receiver)? | ||||||||||
.expect("database format is valid after upgrade"); | ||||||||||
|
||||||||||
// Mark the database as upgraded. Zebra won't repeat the upgrade anymore once the | ||||||||||
// database is marked, so the upgrade MUST be complete at this point. | ||||||||||
Self::mark_as_upgraded_to(db, &version_for_pruning_trees); | ||||||||||
|
||||||||||
timer.finish(module_path!(), line!(), "deduplicate trees upgrade"); | ||||||||||
} | ||||||||||
|
||||||||||
// Note commitment subtree creation database upgrade task. | ||||||||||
|
||||||||||
let latest_version_for_adding_subtrees = latest_version_for_adding_subtrees(); | ||||||||||
let first_version_for_adding_subtrees = | ||||||||||
Version::parse("25.2.0").expect("Hardcoded version string should be valid."); | ||||||||||
|
||||||||||
// Check if we need to add or fix note commitment subtrees in the database. | ||||||||||
if older_disk_version < &latest_version_for_adding_subtrees { | ||||||||||
let timer = CodeTimer::start(); | ||||||||||
|
||||||||||
if older_disk_version >= &first_version_for_adding_subtrees { | ||||||||||
// Clear previous upgrade data, because it was incorrect. | ||||||||||
add_subtrees::reset(initial_tip_height, db, cancel_receiver)?; | ||||||||||
if !upgrade.needs_migration() { | ||||||||||
Self::mark_as_upgraded_to(db, &upgrade.version()); | ||||||||||
continue; | ||||||||||
} | ||||||||||
|
||||||||||
add_subtrees::run(initial_tip_height, db, cancel_receiver)?; | ||||||||||
|
||||||||||
// Before marking the state as upgraded, check that the upgrade completed successfully. | ||||||||||
add_subtrees::subtree_format_validity_checks_detailed(db, cancel_receiver)? | ||||||||||
.expect("database format is valid after upgrade"); | ||||||||||
|
||||||||||
// Mark the database as upgraded. Zebra won't repeat the upgrade anymore once the | ||||||||||
// database is marked, so the upgrade MUST be complete at this point. | ||||||||||
Self::mark_as_upgraded_to(db, &latest_version_for_adding_subtrees); | ||||||||||
|
||||||||||
timer.finish(module_path!(), line!(), "add subtrees upgrade"); | ||||||||||
} | ||||||||||
|
||||||||||
// Sprout & history tree key formats, and cached genesis tree roots database upgrades. | ||||||||||
|
||||||||||
let version_for_tree_keys_and_caches = | ||||||||||
Version::parse("25.3.0").expect("Hardcoded version string should be valid."); | ||||||||||
|
||||||||||
// Check if we need to do the upgrade. | ||||||||||
if older_disk_version < &version_for_tree_keys_and_caches { | ||||||||||
let timer = CodeTimer::start(); | ||||||||||
|
||||||||||
// It shouldn't matter what order these are run in. | ||||||||||
cache_genesis_roots::run(initial_tip_height, db, cancel_receiver)?; | ||||||||||
fix_tree_key_type::run(initial_tip_height, db, cancel_receiver)?; | ||||||||||
upgrade.prepare(initial_tip_height, db, cancel_receiver, older_disk_version)?; | ||||||||||
upgrade.run(initial_tip_height, db, cancel_receiver)?; | ||||||||||
|
||||||||||
// Before marking the state as upgraded, check that the upgrade completed successfully. | ||||||||||
cache_genesis_roots::detailed_check(db, cancel_receiver)? | ||||||||||
.expect("database format is valid after upgrade"); | ||||||||||
fix_tree_key_type::detailed_check(db, cancel_receiver)? | ||||||||||
.expect("database format is valid after upgrade"); | ||||||||||
upgrade | ||||||||||
.validate(db, cancel_receiver)? | ||||||||||
.expect("db should be valid after upgrade"); | ||||||||||
|
||||||||||
// Mark the database as upgraded. Zebra won't repeat the upgrade anymore once the | ||||||||||
// database is marked, so the upgrade MUST be complete at this point. | ||||||||||
Self::mark_as_upgraded_to(db, &version_for_tree_keys_and_caches); | ||||||||||
info!( | ||||||||||
newer_running_version = ?upgrade.version(), | ||||||||||
"Zebra automatically upgraded the database format" | ||||||||||
); | ||||||||||
Self::mark_as_upgraded_to(db, &upgrade.version()); | ||||||||||
|
||||||||||
timer.finish(module_path!(), line!(), "tree keys and caches upgrade"); | ||||||||||
timer.finish(module_path!(), line!(), upgrade.description()); | ||||||||||
} | ||||||||||
|
||||||||||
let version_for_upgrading_value_balance_format = | ||||||||||
|
@@ -610,14 +574,6 @@ impl DbFormatChange { | |||||||||
Self::mark_as_upgraded_to(db, &version_for_upgrading_value_balance_format) | ||||||||||
} | ||||||||||
|
||||||||||
// # New Upgrades Usually Go Here | ||||||||||
// | ||||||||||
// New code goes above this comment! | ||||||||||
// | ||||||||||
// Run the latest format upgrade code after the other upgrades are complete, | ||||||||||
// then mark the format as upgraded. The code should check `cancel_receiver` | ||||||||||
// every time it runs its inner update loop. | ||||||||||
|
||||||||||
info!( | ||||||||||
%newer_running_version, | ||||||||||
"Zebra automatically upgraded the database format to:" | ||||||||||
|
@@ -669,13 +625,9 @@ impl DbFormatChange { | |||||||||
// Do the quick checks first, so we don't have to do this in every detailed check. | ||||||||||
results.push(Self::format_validity_checks_quick(db)); | ||||||||||
|
||||||||||
results.push(Self::check_for_duplicate_trees(db, cancel_receiver)?); | ||||||||||
results.push(add_subtrees::subtree_format_validity_checks_detailed( | ||||||||||
db, | ||||||||||
cancel_receiver, | ||||||||||
)?); | ||||||||||
results.push(cache_genesis_roots::detailed_check(db, cancel_receiver)?); | ||||||||||
results.push(fix_tree_key_type::detailed_check(db, cancel_receiver)?); | ||||||||||
for upgrade in format_upgrades() { | ||||||||||
results.push(upgrade.validate(db, cancel_receiver)?); | ||||||||||
} | ||||||||||
|
||||||||||
// The work is done in the functions we just called. | ||||||||||
timer.finish(module_path!(), line!(), "format_validity_checks_detailed()"); | ||||||||||
|
@@ -689,66 +641,6 @@ impl DbFormatChange { | |||||||||
Ok(Ok(())) | ||||||||||
} | ||||||||||
|
||||||||||
/// Check that note commitment trees were correctly de-duplicated. | ||||||||||
// | ||||||||||
// TODO: move this method into an deduplication upgrade module file, | ||||||||||
// along with the upgrade code above. | ||||||||||
#[allow(clippy::unwrap_in_result)] | ||||||||||
fn check_for_duplicate_trees( | ||||||||||
db: &ZebraDb, | ||||||||||
cancel_receiver: &Receiver<CancelFormatChange>, | ||||||||||
) -> Result<Result<(), String>, CancelFormatChange> { | ||||||||||
// Runtime test: make sure we removed all duplicates. | ||||||||||
// We always run this test, even if the state has supposedly been upgraded. | ||||||||||
let mut result = Ok(()); | ||||||||||
|
||||||||||
let mut prev_height = None; | ||||||||||
let mut prev_tree = None; | ||||||||||
for (height, tree) in db.sapling_tree_by_height_range(..) { | ||||||||||
// Return early if the format check is cancelled. | ||||||||||
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) { | ||||||||||
return Err(CancelFormatChange); | ||||||||||
} | ||||||||||
|
||||||||||
if prev_tree == Some(tree.clone()) { | ||||||||||
result = Err(format!( | ||||||||||
"found duplicate sapling trees after running de-duplicate tree upgrade:\ | ||||||||||
height: {height:?}, previous height: {:?}, tree root: {:?}", | ||||||||||
prev_height.unwrap(), | ||||||||||
tree.root() | ||||||||||
)); | ||||||||||
error!(?result); | ||||||||||
} | ||||||||||
|
||||||||||
prev_height = Some(height); | ||||||||||
prev_tree = Some(tree); | ||||||||||
} | ||||||||||
|
||||||||||
let mut prev_height = None; | ||||||||||
let mut prev_tree = None; | ||||||||||
for (height, tree) in db.orchard_tree_by_height_range(..) { | ||||||||||
// Return early if the format check is cancelled. | ||||||||||
if !matches!(cancel_receiver.try_recv(), Err(TryRecvError::Empty)) { | ||||||||||
return Err(CancelFormatChange); | ||||||||||
} | ||||||||||
|
||||||||||
if prev_tree == Some(tree.clone()) { | ||||||||||
result = Err(format!( | ||||||||||
"found duplicate orchard trees after running de-duplicate tree upgrade:\ | ||||||||||
height: {height:?}, previous height: {:?}, tree root: {:?}", | ||||||||||
prev_height.unwrap(), | ||||||||||
tree.root() | ||||||||||
)); | ||||||||||
error!(?result); | ||||||||||
} | ||||||||||
|
||||||||||
prev_height = Some(height); | ||||||||||
prev_tree = Some(tree); | ||||||||||
} | ||||||||||
|
||||||||||
Ok(result) | ||||||||||
} | ||||||||||
|
||||||||||
/// Mark a newly created database with the current format version. | ||||||||||
/// | ||||||||||
/// This should be called when a newly created database is opened. | ||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
run_migration()
may be a better name for this method.