diff options
Diffstat (limited to 'toolkit/components/extensions/storage/webext_storage_bridge/src/store.rs')
-rw-r--r-- | toolkit/components/extensions/storage/webext_storage_bridge/src/store.rs | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/toolkit/components/extensions/storage/webext_storage_bridge/src/store.rs b/toolkit/components/extensions/storage/webext_storage_bridge/src/store.rs new file mode 100644 index 0000000000..79189f4761 --- /dev/null +++ b/toolkit/components/extensions/storage/webext_storage_bridge/src/store.rs @@ -0,0 +1,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) + } +} |