summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/storage/webext_storage_bridge/src/store.rs
blob: 79189f4761e4ab0296c19d04b41cfa81bf685cc5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::{fs::remove_file, path::PathBuf, sync::Arc};

use interrupt_support::SqlInterruptHandle;
use once_cell::sync::OnceCell;
use webext_storage::store::Store;

use crate::error::{self, Error};

/// Options for an extension storage area.
pub struct LazyStoreConfig {
    /// The path to the database file for this storage area.
    pub path: PathBuf,
    /// The path to the old kinto database. If it exists, we should attempt to
    /// migrate from this database as soon as we open our DB. It's not Option<>
    /// because the caller will not have checked whether it exists or not, so
    /// will assume it might.
    pub kinto_path: PathBuf,
}

/// A lazy store is automatically initialized on a background thread with its
/// configuration the first time it's used.
#[derive(Default)]
pub struct LazyStore {
    store: OnceCell<InterruptStore>,
    config: OnceCell<LazyStoreConfig>,
}

/// An `InterruptStore` wraps an inner extension store, and its interrupt
/// handle.
struct InterruptStore {
    inner: Store,
    handle: Arc<SqlInterruptHandle>,
}

impl LazyStore {
    /// Configures the lazy store. Returns an error if the store has already
    /// been configured. This method should be called from the main thread.
    pub fn configure(&self, config: LazyStoreConfig) -> error::Result<()> {
        self.config
            .set(config)
            .map_err(|_| Error::AlreadyConfigured)
    }

    /// Interrupts all pending operations on the store. If a database statement
    /// is currently running, this will interrupt that statement. If the
    /// statement is a write inside an active transaction, the entire
    /// transaction will be rolled back. This method should be called from the
    /// main thread.
    pub fn interrupt(&self) {
        if let Some(outer) = self.store.get() {
            outer.handle.interrupt();
        }
    }

    /// Returns the underlying store, initializing it if needed. This method
    /// should only be called from a background thread or task queue, since
    /// opening the database does I/O.
    pub fn get(&self) -> error::Result<&Store> {
        Ok(&self
            .store
            .get_or_try_init(|| match self.config.get() {
                Some(config) => {
                    let store = init_store(config)?;
                    let handle = store.interrupt_handle();
                    Ok(InterruptStore {
                        inner: store,
                        handle,
                    })
                }
                None => Err(Error::NotConfigured),
            })?
            .inner)
    }

    /// Tears down the store. If the store wasn't initialized, this is a no-op.
    /// This should only be called from a background thread or task queue,
    /// because closing the database also does I/O.
    pub fn teardown(self) -> error::Result<()> {
        if let Some(store) = self.store.into_inner() {
            store.inner.close()?;
        }
        Ok(())
    }
}

// Initialize the store, performing a migration if necessary.
// The requirements for migration are, roughly:
// * If kinto_path doesn't exist, we don't try to migrate.
// * If our DB path exists, we assume we've already migrated and don't try again
// * If the migration fails, we close our store and delete the DB, then return
//   a special error code which tells our caller about the failure. It's then
//   expected to fallback to the "old" kinto store and we'll try next time.
// Note that the migrate() method on the store is written such that is should
// ignore all "read" errors from the source, but propagate "write" errors on our
// DB - the intention is that things like corrupted source databases never fail,
// but disk-space failures on our database does.
fn init_store(config: &LazyStoreConfig) -> error::Result<Store> {
    let should_migrate = config.kinto_path.exists() && !config.path.exists();
    let store = Store::new(&config.path)?;
    if should_migrate {
        match store.migrate(&config.kinto_path) {
            // It's likely to be too early for us to stick the MigrationInfo
            // into the sync telemetry, a separate call to `take_migration_info`
            // must be made to the store (this is done by telemetry after it's
            // ready to submit the data).
            Ok(()) => {
                // need logging, but for now let's print to stdout.
                println!("extension-storage: migration complete");
                Ok(store)
            }
            Err(e) => {
                println!("extension-storage: migration failure: {e}");
                if let Err(e) = store.close() {
                    // welp, this probably isn't going to end well...
                    println!(
                        "extension-storage: failed to close the store after migration failure: {e}"
                    );
                }
                if let Err(e) = remove_file(&config.path) {
                    // this is bad - if it happens regularly it will defeat
                    // out entire migration strategy - we'll assume it
                    // worked.
                    // So it's desirable to make noise if this happens.
                    println!("Failed to remove file after failed migration: {e}");
                }
                Err(Error::MigrationFailed(e))
            }
        }
    } else {
        Ok(store)
    }
}