zebra_state/service/finalized_state/disk_format/upgrade.rs
1//! In-place format upgrades and format validity checks for the Zebra state database.
2
3use std::{
4 cmp::Ordering,
5 sync::Arc,
6 thread::{self, JoinHandle},
7};
8
9use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
10use semver::Version;
11use tracing::Span;
12
13use zebra_chain::{
14 block::Height,
15 diagnostic::{
16 task::{CheckForPanics, WaitForPanics},
17 CodeTimer,
18 },
19};
20
21use DbFormatChange::*;
22
23use crate::service::finalized_state::ZebraDb;
24
25pub(crate) mod add_ironwood_tree;
26pub(crate) mod add_subtrees;
27pub(crate) mod block_info_and_address_received;
28pub(crate) mod cache_genesis_roots;
29pub(crate) mod fix_tree_key_type;
30pub(crate) mod no_migration;
31pub(crate) mod prune_trees;
32pub(crate) mod tree_keys_and_caches_upgrade;
33
34#[cfg(not(feature = "indexer"))]
35pub(crate) mod drop_tx_locs_by_spends;
36
37#[cfg(feature = "indexer")]
38pub(crate) mod track_tx_locs_by_spends;
39
40/// Defines method signature for running disk format upgrades.
41pub trait DiskFormatUpgrade {
42 /// Returns the version at which this upgrade is applied.
43 fn version(&self) -> Version;
44
45 /// Returns the description of this upgrade.
46 fn description(&self) -> &'static str;
47
48 /// Runs disk format upgrade.
49 fn run(
50 &self,
51 initial_tip_height: Height,
52 db: &ZebraDb,
53 cancel_receiver: &Receiver<CancelFormatChange>,
54 ) -> Result<(), CancelFormatChange>;
55
56 /// Check that state has been upgraded to this format correctly.
57 ///
58 /// The outer `Result` indicates whether the validation was cancelled (due to e.g. node shutdown).
59 /// The inner `Result` indicates whether the validation itself failed or not.
60 fn validate(
61 &self,
62 _db: &ZebraDb,
63 _cancel_receiver: &Receiver<CancelFormatChange>,
64 ) -> Result<Result<(), String>, CancelFormatChange> {
65 Ok(Ok(()))
66 }
67
68 /// Prepare for disk format upgrade.
69 fn prepare(
70 &self,
71 _initial_tip_height: Height,
72 _upgrade_db: &ZebraDb,
73 _cancel_receiver: &Receiver<CancelFormatChange>,
74 _older_disk_version: &Version,
75 ) -> Result<(), CancelFormatChange> {
76 Ok(())
77 }
78
79 /// Returns true if the [`DiskFormatUpgrade`] needs to run a migration on existing data in the db.
80 fn needs_migration(&self) -> bool {
81 true
82 }
83
84 /// Returns true if the upgrade is a major upgrade that can reuse the cache in the previous major db format version.
85 fn is_reusable_major_upgrade(&self) -> bool {
86 let version = self.version();
87 version.minor == 0 && version.patch == 0
88 }
89}
90
91fn format_upgrades(
92 min_version: Option<Version>,
93) -> impl DoubleEndedIterator<Item = Box<dyn DiskFormatUpgrade>> {
94 let min_version = move || min_version.clone().unwrap_or(Version::new(0, 0, 0));
95
96 // Note: Disk format upgrades must be run in order of database version.
97 ([
98 Box::new(prune_trees::PruneTrees),
99 Box::new(add_subtrees::AddSubtrees),
100 Box::new(tree_keys_and_caches_upgrade::FixTreeKeyTypeAndCacheGenesisRoots),
101 Box::new(no_migration::NoMigration::new(
102 "add value balance upgrade",
103 Version::new(26, 0, 0),
104 )),
105 Box::new(block_info_and_address_received::Upgrade),
106 // The NU6.3 Ironwood shielded pool adds new column families and widens the chain value pool
107 // serialization. New column families and the wider records are created/read in place when
108 // the database is opened, but the genesis Ironwood tree and anchor must be backfilled so an
109 // upgraded database matches a genesis-synced one (otherwise ironwood_tree_for_tip() panics
110 // and the genesis Ironwood anchor is missing for NU6.3 anchor validation). This is a
111 // major-version upgrade that is restorable from the previous major database format version.
112 Box::new(add_ironwood_tree::Upgrade),
113 ] as [Box<dyn DiskFormatUpgrade>; 6])
114 .into_iter()
115 .filter(move |upgrade| upgrade.version() > min_version())
116}
117
118/// Returns a list of all the major db format versions that can restored from the
119/// previous major database format.
120pub fn restorable_db_versions() -> Vec<u64> {
121 format_upgrades(None)
122 .filter_map(|upgrade| {
123 upgrade
124 .is_reusable_major_upgrade()
125 .then_some(upgrade.version().major)
126 })
127 .collect()
128}
129
130/// The kind of database format change or validity check we're performing.
131#[derive(Clone, Debug, Eq, PartialEq)]
132pub enum DbFormatChange {
133 // Data Format Changes
134 //
135 /// Upgrade the format from `older_disk_version` to `newer_running_version`.
136 ///
137 /// Until this upgrade is complete, the format is a mixture of both versions.
138 Upgrade {
139 older_disk_version: Version,
140 newer_running_version: Version,
141 },
142
143 // Format Version File Changes
144 //
145 /// Mark the format as newly created by `running_version`.
146 ///
147 /// Newly created databases are opened with no disk version.
148 /// It is set to the running version by the format change code.
149 NewlyCreated { running_version: Version },
150
151 /// Mark the format as downgraded from `newer_disk_version` to `older_running_version`.
152 ///
153 /// Until the state is upgraded to `newer_disk_version` by a Zebra version with that state
154 /// version (or greater), the format will be a mixture of both versions.
155 Downgrade {
156 newer_disk_version: Version,
157 older_running_version: Version,
158 },
159
160 // Data Format Checks
161 //
162 /// Check that the database from a previous instance has the current `running_version` format.
163 ///
164 /// Current version databases have a disk version that matches the running version.
165 /// No upgrades are needed, so we just run a format check on the database.
166 /// The data in that database was created or updated by a previous Zebra instance.
167 CheckOpenCurrent { running_version: Version },
168
169 /// Check that the database from this instance has the current `running_version` format.
170 ///
171 /// The data in that database was created or updated by the currently running Zebra instance.
172 /// So we periodically check for data bugs, which can happen if the upgrade and new block
173 /// code produce different data. (They can also be caused by disk corruption.)
174 CheckNewBlocksCurrent { running_version: Version },
175}
176
177/// A handle to a spawned format change thread.
178///
179/// Cloning this struct creates an additional handle to the same thread.
180///
181/// # Concurrency
182///
183/// Cancelling the thread on drop has a race condition, because two handles can be dropped at
184/// the same time.
185///
186/// If cancelling the thread is required for correct operation or usability, the owner of the
187/// handle must call force_cancel().
188#[derive(Clone, Debug)]
189pub struct DbFormatChangeThreadHandle {
190 /// A handle to the format change/check thread.
191 /// If configured, this thread continues running so it can perform periodic format checks.
192 ///
193 /// Panics from this thread are propagated into Zebra's state service.
194 /// The task returns an error if the upgrade was cancelled by a shutdown.
195 update_task: Option<Arc<JoinHandle<Result<(), CancelFormatChange>>>>,
196
197 /// A channel that tells the running format thread to finish early.
198 cancel_handle: Sender<CancelFormatChange>,
199}
200
201/// Marker type that is sent to cancel a format upgrade, and returned as an error on cancellation.
202#[derive(Copy, Clone, Debug, Eq, PartialEq)]
203pub struct CancelFormatChange;
204
205impl DbFormatChange {
206 /// Returns the format change for `running_version` code loading a `disk_version` database.
207 ///
208 /// Also logs that change at info level.
209 ///
210 /// If `disk_version` is `None`, Zebra is creating a new database.
211 pub fn open_database(running_version: &Version, disk_version: Option<Version>) -> Self {
212 let running_version = running_version.clone();
213
214 let Some(disk_version) = disk_version else {
215 info!(
216 %running_version,
217 "creating new database with the current format"
218 );
219
220 return NewlyCreated { running_version };
221 };
222
223 match disk_version.cmp_precedence(&running_version) {
224 Ordering::Less => {
225 info!(
226 %running_version,
227 %disk_version,
228 "trying to open older database format: launching upgrade task"
229 );
230
231 Upgrade {
232 older_disk_version: disk_version,
233 newer_running_version: running_version,
234 }
235 }
236 Ordering::Greater => {
237 info!(
238 %running_version,
239 %disk_version,
240 "trying to open newer database format: data should be compatible"
241 );
242
243 Downgrade {
244 newer_disk_version: disk_version,
245 older_running_version: running_version,
246 }
247 }
248 Ordering::Equal => {
249 info!(%running_version, "trying to open current database format");
250
251 CheckOpenCurrent { running_version }
252 }
253 }
254 }
255
256 /// Returns a format check for newly added blocks in the currently running Zebra version.
257 /// This check makes sure the upgrade and new block code produce the same data.
258 ///
259 /// Also logs the check at info level.
260 pub fn check_new_blocks(db: &ZebraDb) -> Self {
261 let running_version = db.format_version_in_code();
262
263 info!(%running_version, "checking new blocks were written in current database format");
264 CheckNewBlocksCurrent { running_version }
265 }
266
267 /// Returns true if this format change/check is an upgrade.
268 #[allow(dead_code)]
269 pub fn is_upgrade(&self) -> bool {
270 matches!(self, Upgrade { .. })
271 }
272
273 /// Returns true if this format change indicates a newly created database
274 /// (no database was found on disk).
275 pub fn is_newly_created(&self) -> bool {
276 matches!(self, NewlyCreated { .. })
277 }
278
279 /// Returns true if this format change/check happens at startup.
280 #[allow(dead_code)]
281 pub fn is_run_at_startup(&self) -> bool {
282 !matches!(self, CheckNewBlocksCurrent { .. })
283 }
284
285 /// Returns the running version in this format change.
286 pub fn running_version(&self) -> Version {
287 match self {
288 Upgrade {
289 newer_running_version,
290 ..
291 } => newer_running_version,
292 Downgrade {
293 older_running_version,
294 ..
295 } => older_running_version,
296 NewlyCreated { running_version }
297 | CheckOpenCurrent { running_version }
298 | CheckNewBlocksCurrent { running_version } => running_version,
299 }
300 .clone()
301 }
302
303 /// Returns the initial database version before this format change.
304 ///
305 /// Returns `None` if the database was newly created.
306 pub fn initial_disk_version(&self) -> Option<Version> {
307 match self {
308 Upgrade {
309 older_disk_version, ..
310 } => Some(older_disk_version),
311 Downgrade {
312 newer_disk_version, ..
313 } => Some(newer_disk_version),
314 CheckOpenCurrent { running_version } | CheckNewBlocksCurrent { running_version } => {
315 Some(running_version)
316 }
317 NewlyCreated { .. } => None,
318 }
319 .cloned()
320 }
321
322 /// Launch a `std::thread` that applies this format change to the database,
323 /// then continues running to perform periodic format checks.
324 ///
325 /// `initial_tip_height` is the database height when it was opened, and `db` is the
326 /// database instance to upgrade or check.
327 pub fn spawn_format_change(
328 self,
329 db: ZebraDb,
330 initial_tip_height: Option<Height>,
331 ) -> DbFormatChangeThreadHandle {
332 // # Correctness
333 //
334 // Cancel handles must use try_send() to avoid blocking waiting for the format change
335 // thread to shut down.
336 let (cancel_handle, cancel_receiver) = bounded(1);
337
338 let span = Span::current();
339 let update_task = thread::spawn(move || {
340 span.in_scope(move || {
341 self.format_change_run_loop(db, initial_tip_height, cancel_receiver)
342 })
343 });
344
345 let mut handle = DbFormatChangeThreadHandle {
346 update_task: Some(Arc::new(update_task)),
347 cancel_handle,
348 };
349
350 handle.check_for_panics();
351
352 handle
353 }
354
355 /// Run the initial format change or check to the database. Under the default runtime config,
356 /// this method returns after the format change or check.
357 ///
358 /// But if runtime validity checks are enabled, this method periodically checks the format of
359 /// newly added blocks matches the current format. It will run until it is cancelled or panics.
360 fn format_change_run_loop(
361 self,
362 db: ZebraDb,
363 initial_tip_height: Option<Height>,
364 cancel_receiver: Receiver<CancelFormatChange>,
365 ) -> Result<(), CancelFormatChange> {
366 self.run_format_change_or_check(&db, initial_tip_height, &cancel_receiver)?;
367
368 let Some(debug_validity_check_interval) = db.config().debug_validity_check_interval else {
369 return Ok(());
370 };
371
372 loop {
373 // We've just run a format check, so sleep first, then run another one.
374 // But return early if there is a cancel signal.
375 if !matches!(
376 cancel_receiver.recv_timeout(debug_validity_check_interval),
377 Err(RecvTimeoutError::Timeout)
378 ) {
379 return Err(CancelFormatChange);
380 }
381
382 Self::check_new_blocks(&db).run_format_change_or_check(
383 &db,
384 initial_tip_height,
385 &cancel_receiver,
386 )?;
387 }
388 }
389
390 /// Run a format change in the database, or check the format of the database once.
391 #[allow(clippy::unwrap_in_result)]
392 pub(crate) fn run_format_change_or_check(
393 &self,
394 db: &ZebraDb,
395 initial_tip_height: Option<Height>,
396 cancel_receiver: &Receiver<CancelFormatChange>,
397 ) -> Result<(), CancelFormatChange> {
398 // Mark the database as having finished applying any format upgrades if there are no
399 // format upgrades that need to be applied.
400 if !self.is_upgrade() {
401 db.mark_finished_format_upgrades();
402 }
403
404 match self {
405 // Perform any required upgrades, then mark the state as upgraded.
406 Upgrade { .. } => {
407 self.apply_format_upgrade(db, initial_tip_height, cancel_receiver)?;
408 db.mark_finished_format_upgrades();
409 }
410
411 NewlyCreated { .. } => {
412 Self::mark_as_newly_created(db);
413 }
414
415 Downgrade { .. } => {
416 // # Correctness
417 //
418 // At the start of a format downgrade, the database must be marked as partially or
419 // fully downgraded. This lets newer Zebra versions know that some blocks with older
420 // formats have been added to the database.
421 Self::mark_as_downgraded(db);
422
423 // Older supported versions just assume they can read newer formats,
424 // because they can't predict all changes a newer Zebra version could make.
425 //
426 // The responsibility of staying backwards-compatible is on the newer version.
427 // We do this on a best-effort basis for versions that are still supported.
428 }
429
430 CheckOpenCurrent { running_version } => {
431 // If we're re-opening a previously upgraded or newly created database,
432 // the database format should be valid. This check is done below.
433 info!(
434 %running_version,
435 "checking database format produced by a previous zebra instance \
436 is current and valid"
437 );
438 }
439
440 CheckNewBlocksCurrent { running_version } => {
441 // If we've added new blocks using the non-upgrade code,
442 // the database format should be valid. This check is done below.
443 //
444 // TODO: should this check panic or just log an error?
445 // Currently, we panic to avoid consensus bugs, but this could cause a denial
446 // of service. We can make errors fail in CI using ZEBRA_FAILURE_MESSAGES.
447 info!(
448 %running_version,
449 "checking database format produced by new blocks in this instance is valid"
450 );
451 }
452 }
453
454 #[cfg(feature = "indexer")]
455 if let (
456 Upgrade { .. } | CheckOpenCurrent { .. } | Downgrade { .. },
457 Some(initial_tip_height),
458 ) = (self, initial_tip_height)
459 {
460 // Indexing transaction locations by their spent outpoints and revealed nullifiers.
461 let timer = CodeTimer::start();
462
463 // Add build metadata to on-disk version file just before starting to add indexes
464 let mut version = db
465 .format_version_on_disk()
466 .expect("unable to read database format version file")
467 .expect("should write database format version file above");
468 version.build = db.format_version_in_code().build;
469
470 db.update_format_version_on_disk(&version)
471 .expect("unable to write database format version file to disk");
472
473 info!("started checking/adding indexes for spending tx ids");
474 track_tx_locs_by_spends::run(initial_tip_height, db, cancel_receiver)?;
475 info!("finished checking/adding indexes for spending tx ids");
476
477 timer.finish_desc("indexing spending transaction ids");
478 };
479
480 #[cfg(not(feature = "indexer"))]
481 if let (
482 Upgrade { .. } | CheckOpenCurrent { .. } | Downgrade { .. },
483 Some(initial_tip_height),
484 ) = (self, initial_tip_height)
485 {
486 let mut version = db
487 .format_version_on_disk()
488 .expect("unable to read database format version file")
489 .expect("should write database format version file above");
490
491 if version.build.contains("indexer") {
492 // Indexing transaction locations by their spent outpoints and revealed nullifiers.
493 let timer = CodeTimer::start();
494
495 info!("started removing indexes for spending tx ids");
496 drop_tx_locs_by_spends::run(initial_tip_height, db, cancel_receiver)?;
497 info!("finished removing indexes for spending tx ids");
498
499 // Remove build metadata to on-disk version file after indexes have been dropped.
500 version.build = db.format_version_in_code().build;
501 db.update_format_version_on_disk(&version)
502 .expect("unable to write database format version file to disk");
503
504 timer.finish_desc("removing spending transaction ids");
505 }
506 };
507
508 // These checks should pass for all format changes:
509 // - upgrades should produce a valid format (and they already do that check)
510 // - an empty state should pass all the format checks
511 // - since the running Zebra code knows how to upgrade the database to this format,
512 // downgrades using this running code still know how to create a valid database
513 // (unless a future upgrade breaks these format checks)
514 // - re-opening the current version should be valid, regardless of whether the upgrade
515 // or new block code created the format (or any combination).
516 Self::format_validity_checks_detailed(db, cancel_receiver)?.unwrap_or_else(|_| {
517 panic!(
518 "unexpected invalid database format: delete and re-sync the database at '{:?}'",
519 db.path()
520 )
521 });
522
523 let initial_disk_version = self
524 .initial_disk_version()
525 .map_or_else(|| "None".to_string(), |version| version.to_string());
526 info!(
527 running_version = %self.running_version(),
528 %initial_disk_version,
529 "database format is valid"
530 );
531
532 Ok(())
533 }
534
535 // TODO: Move state-specific upgrade code to a finalized_state/* module.
536
537 /// Apply any required format updates to the database.
538 /// Format changes should be launched in an independent `std::thread`.
539 ///
540 /// If `cancel_receiver` gets a message, or its sender is dropped,
541 /// the format change stops running early, and returns an error.
542 ///
543 /// See the format upgrade design docs for more details:
544 /// <https://github.com/ZcashFoundation/zebra/blob/main/book/src/dev/state-db-upgrades.md#design>
545 //
546 // New format upgrades must be added to the *end* of this method.
547 #[allow(clippy::unwrap_in_result)]
548 fn apply_format_upgrade(
549 &self,
550 db: &ZebraDb,
551 initial_tip_height: Option<Height>,
552 cancel_receiver: &Receiver<CancelFormatChange>,
553 ) -> Result<(), CancelFormatChange> {
554 let Upgrade {
555 newer_running_version,
556 older_disk_version,
557 } = self
558 else {
559 unreachable!("already checked for Upgrade")
560 };
561
562 // # New Upgrades Sometimes Go Here
563 //
564 // If the format change is outside RocksDb, put new code above this comment!
565 let Some(initial_tip_height) = initial_tip_height else {
566 // If the database is empty, then the RocksDb format doesn't need any changes.
567 info!(
568 %newer_running_version,
569 %older_disk_version,
570 "marking empty database as upgraded"
571 );
572
573 Self::mark_as_upgraded_to(db, newer_running_version);
574
575 info!(
576 %newer_running_version,
577 %older_disk_version,
578 "empty database is fully upgraded"
579 );
580
581 return Ok(());
582 };
583
584 // Apply or validate format upgrades
585 for upgrade in format_upgrades(Some(older_disk_version.clone())) {
586 if upgrade.needs_migration() {
587 let timer = CodeTimer::start();
588
589 upgrade.prepare(initial_tip_height, db, cancel_receiver, older_disk_version)?;
590 upgrade.run(initial_tip_height, db, cancel_receiver)?;
591
592 // Before marking the state as upgraded, check that the upgrade completed successfully.
593 upgrade
594 .validate(db, cancel_receiver)?
595 .expect("db should be valid after upgrade");
596
597 timer.finish_desc(upgrade.description());
598 }
599
600 // Mark the database as upgraded. Zebra won't repeat the upgrade anymore once the
601 // database is marked, so the upgrade MUST be complete at this point.
602 info!(
603 newer_running_version = ?upgrade.version(),
604 "Zebra automatically upgraded the database format"
605 );
606 Self::mark_as_upgraded_to(db, &upgrade.version());
607 }
608
609 Ok(())
610 }
611
612 /// Run quick checks that the current database format is valid.
613 #[allow(clippy::vec_init_then_push)]
614 pub fn format_validity_checks_quick(db: &ZebraDb) -> Result<(), String> {
615 let timer = CodeTimer::start();
616 let mut results = Vec::new();
617
618 // Check the entire format before returning any errors.
619 results.push(db.check_max_on_disk_tip_height());
620
621 // This check can be run before the upgrade, but the upgrade code is finished, so we don't
622 // run it early any more. (If future code changes accidentally make it depend on the
623 // upgrade, they would accidentally break compatibility with older Zebra cached states.)
624 results.push(add_subtrees::subtree_format_calculation_pre_checks(db));
625
626 results.push(cache_genesis_roots::quick_check(db));
627 results.push(fix_tree_key_type::quick_check(db));
628
629 // The work is done in the functions we just called.
630 timer.finish_desc("format_validity_checks_quick()");
631
632 if results.iter().any(Result::is_err) {
633 let err = Err(format!("invalid quick check: {results:?}"));
634 error!(?err);
635 return err;
636 }
637
638 Ok(())
639 }
640
641 /// Run detailed checks that the current database format is valid.
642 #[allow(clippy::vec_init_then_push)]
643 pub fn format_validity_checks_detailed(
644 db: &ZebraDb,
645 cancel_receiver: &Receiver<CancelFormatChange>,
646 ) -> Result<Result<(), String>, CancelFormatChange> {
647 let timer = CodeTimer::start();
648 let mut results = Vec::new();
649
650 // Check the entire format before returning any errors.
651 //
652 // Do the quick checks first, so we don't have to do this in every detailed check.
653 results.push(Self::format_validity_checks_quick(db));
654
655 for upgrade in format_upgrades(None) {
656 results.push(upgrade.validate(db, cancel_receiver)?);
657 }
658
659 // The work is done in the functions we just called.
660 timer.finish_desc("format_validity_checks_detailed()");
661
662 if results.iter().any(Result::is_err) {
663 let err = Err(format!("invalid detailed check: {results:?}"));
664 error!(?err);
665 return Ok(err);
666 }
667
668 Ok(Ok(()))
669 }
670
671 /// Mark a newly created database with the current format version.
672 ///
673 /// This should be called when a newly created database is opened.
674 ///
675 /// # Concurrency
676 ///
677 /// The version must only be updated while RocksDB is holding the database
678 /// directory lock. This prevents multiple Zebra instances corrupting the version
679 /// file.
680 ///
681 /// # Panics
682 ///
683 /// If the format should not have been upgraded, because the database is not newly created.
684 fn mark_as_newly_created(db: &ZebraDb) {
685 let running_version = db.format_version_in_code();
686 let disk_version = db
687 .format_version_on_disk()
688 .expect("unable to read database format version file path");
689
690 let default_new_version = Some(Version::new(running_version.major, 0, 0));
691
692 // The database version isn't empty any more, because we've created the RocksDB database
693 // and acquired its lock. (If it is empty, we have a database locking bug.)
694 assert_eq!(
695 disk_version, default_new_version,
696 "can't overwrite the format version in an existing database:\n\
697 disk: {disk_version:?}\n\
698 running: {running_version}"
699 );
700
701 db.update_format_version_on_disk(&running_version)
702 .expect("unable to write database format version file to disk");
703
704 info!(
705 %running_version,
706 disk_version = %disk_version.map_or("None".to_string(), |version| version.to_string()),
707 "marked database format as newly created"
708 );
709 }
710
711 /// Mark the database as upgraded to `format_upgrade_version`.
712 ///
713 /// This should be called when an older database is opened by an older Zebra version,
714 /// after each version upgrade is complete.
715 ///
716 /// # Concurrency
717 ///
718 /// The version must only be updated while RocksDB is holding the database
719 /// directory lock. This prevents multiple Zebra instances corrupting the version
720 /// file.
721 ///
722 /// # Panics
723 ///
724 /// If the format should not have been upgraded, because the running version is:
725 /// - older than the disk version (that's a downgrade)
726 /// - the same as to the disk version (no upgrade needed)
727 ///
728 /// If the format should not have been upgraded, because the format upgrade version is:
729 /// - older or the same as the disk version
730 /// (multiple upgrades to the same version are not allowed)
731 /// - greater than the running version (that's a logic bug)
732 fn mark_as_upgraded_to(db: &ZebraDb, format_upgrade_version: &Version) {
733 let running_version = db.format_version_in_code();
734 let disk_version = db
735 .format_version_on_disk()
736 .expect("unable to read database format version file")
737 .expect("tried to upgrade a newly created database");
738
739 assert!(
740 running_version > disk_version,
741 "can't upgrade a database that is being opened by an older or the same Zebra version:\n\
742 disk: {disk_version}\n\
743 upgrade: {format_upgrade_version}\n\
744 running: {running_version}"
745 );
746
747 assert!(
748 format_upgrade_version > &disk_version,
749 "can't upgrade a database that has already been upgraded, or is newer:\n\
750 disk: {disk_version}\n\
751 upgrade: {format_upgrade_version}\n\
752 running: {running_version}"
753 );
754
755 assert!(
756 format_upgrade_version <= &running_version,
757 "can't upgrade to a newer version than the running Zebra version:\n\
758 disk: {disk_version}\n\
759 upgrade: {format_upgrade_version}\n\
760 running: {running_version}"
761 );
762
763 db.update_format_version_on_disk(format_upgrade_version)
764 .expect("unable to write database format version file to disk");
765
766 info!(
767 %running_version,
768 %disk_version,
769 // wait_for_state_version_upgrade() needs this to be the last field,
770 // so the regex matches correctly
771 %format_upgrade_version,
772 "marked database format as upgraded"
773 );
774 }
775
776 /// Mark the database as downgraded to the running database version.
777 /// This should be called after a newer database is opened by an older Zebra version.
778 ///
779 /// # Concurrency
780 ///
781 /// The version must only be updated while RocksDB is holding the database
782 /// directory lock. This prevents multiple Zebra instances corrupting the version
783 /// file.
784 ///
785 /// # Panics
786 ///
787 /// If the format should have been upgraded, because the running version is newer.
788 /// If the state is newly created, because the running version should be the same.
789 ///
790 /// Multiple downgrades are allowed, because they all downgrade to the same running version.
791 fn mark_as_downgraded(db: &ZebraDb) {
792 let running_version = db.format_version_in_code();
793 let disk_version = db
794 .format_version_on_disk()
795 .expect("unable to read database format version file")
796 .expect("can't downgrade a newly created database");
797
798 assert!(
799 disk_version >= running_version,
800 "can't downgrade a database that is being opened by a newer Zebra version:\n\
801 disk: {disk_version}\n\
802 running: {running_version}"
803 );
804
805 db.update_format_version_on_disk(&running_version)
806 .expect("unable to write database format version file to disk");
807
808 info!(
809 %running_version,
810 %disk_version,
811 "marked database format as downgraded"
812 );
813 }
814}
815
816impl DbFormatChangeThreadHandle {
817 /// Cancel the running format change thread, if this is the last handle.
818 /// Returns true if it was actually cancelled.
819 pub fn cancel_if_needed(&self) -> bool {
820 // # Correctness
821 //
822 // Checking the strong count has a race condition, because two handles can be dropped at
823 // the same time.
824 //
825 // If cancelling the thread is important, the owner of the handle must call force_cancel().
826 if let Some(update_task) = self.update_task.as_ref() {
827 if Arc::strong_count(update_task) <= 1 {
828 self.force_cancel();
829 return true;
830 }
831 }
832
833 false
834 }
835
836 /// Force the running format change thread to cancel, even if there are other handles.
837 pub fn force_cancel(&self) {
838 // There's nothing we can do about errors here.
839 // If the channel is disconnected, the task has exited.
840 // If it's full, it's already been cancelled.
841 let _ = self.cancel_handle.try_send(CancelFormatChange);
842 }
843
844 /// Check for panics in the code running in the spawned thread.
845 /// If the thread exited with a panic, resume that panic.
846 ///
847 /// This method should be called regularly, so that panics are detected as soon as possible.
848 pub fn check_for_panics(&mut self) {
849 self.update_task.panic_if_task_has_panicked();
850 }
851
852 /// Wait for the spawned thread to finish. If it exited with a panic, resume that panic.
853 ///
854 /// Exits early if the thread has other outstanding handles.
855 ///
856 /// This method should be called during shutdown.
857 pub fn wait_for_panics(&mut self) {
858 self.update_task.wait_for_panics();
859 }
860}
861
862impl Drop for DbFormatChangeThreadHandle {
863 fn drop(&mut self) {
864 // Only cancel the format change if the state service is shutting down.
865 if self.cancel_if_needed() {
866 self.wait_for_panics();
867 } else {
868 self.check_for_panics();
869 }
870 }
871}
872
873#[test]
874fn format_upgrades_are_in_version_order() {
875 let mut last_version = Version::new(0, 0, 0);
876 for upgrade in format_upgrades(None) {
877 assert!(upgrade.version() > last_version);
878 last_version = upgrade.version();
879 }
880}