Skip to main content

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}