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    ] 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}