summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/storage/webext_storage_bridge/src/store.rs
diff options
context:
space:
mode:
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.rs136
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)
+ }
+}