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