Bug 1551062 - Abort merging when the synced bookmarks mirror is finalized. r=tcsc
authorLina Cambridge <lina@yakshaving.ninja>
Tue, 14 May 2019 03:40:30 +0000
changeset 532562 3fae03e3595a90150c92e848ad53bf17f6ed493d
parent 532561 db99095ae0b53c30927add34be3dc20a7bdcb52e
child 532563 030c742c25d5e3aab01c2e020345a7cd5506b207
push id11270
push userrgurzau@mozilla.com
push dateWed, 15 May 2019 15:07:19 +0000
treeherdermozilla-beta@571bc76da583 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstcsc
bugs1551062
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1551062 - Abort merging when the synced bookmarks mirror is finalized. r=tcsc This commit adds an `AbortController` to the bookmark merger that aborts fetching and merging when the mirror is finalized on shutdown. Differential Revision: https://phabricator.services.mozilla.com/D31000
Cargo.lock
third_party/rust/dogear/.cargo-checksum.json
third_party/rust/dogear/Cargo.toml
third_party/rust/dogear/book.toml
third_party/rust/dogear/src/driver.rs
third_party/rust/dogear/src/error.rs
third_party/rust/dogear/src/guid.rs
third_party/rust/dogear/src/lib.rs
third_party/rust/dogear/src/merge.rs
third_party/rust/dogear/src/store.rs
third_party/rust/dogear/src/tests.rs
third_party/rust/dogear/src/tree.rs
toolkit/components/places/bookmark_sync/Cargo.toml
toolkit/components/places/bookmark_sync/src/driver.rs
toolkit/components/places/bookmark_sync/src/error.rs
toolkit/components/places/bookmark_sync/src/merger.rs
toolkit/components/places/bookmark_sync/src/store.rs
toolkit/components/places/tests/sync/test_bookmark_abort_merging.js
toolkit/components/places/tests/sync/xpcshell.ini
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -357,17 +357,17 @@ dependencies = [
 ]
 
 [[package]]
 name = "bookmark_sync"
 version = "0.1.0"
 dependencies = [
  "atomic_refcell 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "cstr 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
- "dogear 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "dogear 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)",
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "moz_task 0.1.0",
  "nserror 0.1.0",
  "nsstring 0.1.0",
  "storage 0.1.0",
  "storage_variant 0.1.0",
  "thin-vec 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -915,17 +915,17 @@ dependencies = [
  "regex 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde 1.0.88 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_derive 1.0.88 (git+https://github.com/servo/serde?branch=deserialize_from_enums10)",
  "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "dogear"
-version = "0.2.4"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "smallbitvec 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
 name = "dtoa"
@@ -3686,17 +3686,17 @@ dependencies = [
 "checksum darling_macro 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)" = "244e8987bd4e174385240cde20a3657f607fb0797563c28255c353b5819a07b1"
 "checksum deflate 0.7.19 (registry+https://github.com/rust-lang/crates.io-index)" = "8a6abb26e16e8d419b5c78662aa9f82857c2386a073da266840e474d5055ec86"
 "checksum derive_more 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3f57d78cf3bd45270dad4e70c21ec77a960b36c7a841ff9db76aaa775a8fb871"
 "checksum devd-rs 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e7c9ac481c38baf400d3b732e4a06850dfaa491d1b6379a249d9d40d14c2434c"
 "checksum diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3c2b69f912779fbb121ceb775d74d51e915af17aaebc38d28a592843a2dd0a3a"
 "checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c"
 "checksum dirs 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "88972de891f6118092b643d85a0b28e0678e0f948d7f879aa32f2d5aafe97d2a"
 "checksum docopt 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "db2906c2579b5b7207fc1e328796a9a8835dc44e22dbe8e460b1d636f9a7b225"
-"checksum dogear 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "30ac4a8e8f834f02deb2266b1f279aa5494e990c625d8be8f2988a7c708ba1f8"
+"checksum dogear 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "26b7583e1427e296c852f3217eaab3890e698f742b8d7349beb1f40c4e946fc9"
 "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab"
 "checksum dtoa-short 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "068d4026697c1a18f0b0bb8cfcad1b0c151b90d8edb9bf4c235ad68128920d1d"
 "checksum dwrote 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c31c624339dab99c223a4b26c2e803b7c248adaca91549ce654c76f39a03f5c8"
 "checksum either 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "18785c1ba806c258137c937e44ada9ee7e69a37e3c72077542cd2f069d78562a"
 "checksum ena 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "25b4e5febb25f08c49f1b07dc33a182729a6b21edfb562b5aef95f78e0dbe5bb"
 "checksum encoding_c 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "769ecb8b33323998e482b218c0d13cd64c267609023b4b7ec3ee740714c318ee"
 "checksum encoding_rs 0.8.16 (registry+https://github.com/rust-lang/crates.io-index)" = "0535f350c60aac0b87ccf28319abc749391e912192255b0c00a2c12c6917bd73"
 "checksum env_logger 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0561146661ae44c579e993456bc76d11ce1e0c7d745e57b2fa7146b6e49fa2ad"
--- a/third_party/rust/dogear/.cargo-checksum.json
+++ b/third_party/rust/dogear/.cargo-checksum.json
@@ -1,1 +1,1 @@
-{"files":{"CODE_OF_CONDUCT.md":"e85149c44f478f164f7d5f55f6e66c9b5ae236d4a11107d5e2a93fe71dd874b9","Cargo.toml":"ef36c6d2e8475c91f1a28a4ae1de871f8311f1b0044e6ba20b7c21c0af11f0a1","LICENSE":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4","README.md":"303ea5ec53d4e86f2c321056e8158e31aa061353a99e52de3d76859d40919efc","src/driver.rs":"541d0d5a3f87ebafb4294bebc8a08b259b174b2c0607fa7edef570b0d7b52b7f","src/error.rs":"b78609cf0f0a87b2e6d01bcaf565f1ce8723f33f22f36e1847639557bcd49a2e","src/guid.rs":"6185985ca3e416c1bb9b1691b83789f718fd532fc011efd4a28c82f1edd23650","src/lib.rs":"0bdb83959fc75d9ec99108e0c4c0ced4b9a80c08e950ad2ac59d095e74b39f0f","src/merge.rs":"176353b45ce1079e20d705ca82a154a375eaf927e5a6075d1469d490ff8662d3","src/store.rs":"612d90ea0614aa7cc943c4ac0faaee35c155f57b553195ac28518ae7c0b8ebb1","src/tests.rs":"8a12b2d571ca4c59d645879b555c321c7a6fb6445956d41fcb37747ac06b54df","src/tree.rs":"194ccd6642d64347cf79dea3237e6d124aa4a75cad654360d65945617e749afc"},"package":"30ac4a8e8f834f02deb2266b1f279aa5494e990c625d8be8f2988a7c708ba1f8"}
\ No newline at end of file
+{"files":{"CODE_OF_CONDUCT.md":"e85149c44f478f164f7d5f55f6e66c9b5ae236d4a11107d5e2a93fe71dd874b9","Cargo.toml":"1530236b0a46fdd3afe7225a1e83009fd3e7817ae22566161ec698b724efb562","LICENSE":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4","README.md":"303ea5ec53d4e86f2c321056e8158e31aa061353a99e52de3d76859d40919efc","book.toml":"ff2de447613bd392bb16e4c72b6a39d83f76decbe35ae79df25a9e84e7f8bec4","src/driver.rs":"e07c4c1c0646d12a11fabb288a6fad569c806611e46f36e1f7e952c5829d1308","src/error.rs":"d4ef0cba5c7fc54959ed62da166f10435548d705e0a817eed449fb001fe4e21d","src/guid.rs":"790700aa07b1d1616d76476c48c9bfda6014350b4b028d4b7c05ac1b8f1c8870","src/lib.rs":"ee32682a94a2eb363b2b9708a2dd0c8eb0eb6afc1e5e6a58fba5ab7681bbde7c","src/merge.rs":"63b2f30fea4034d4ca37f835bf18e6344781cd0d5db0f0b56c49395ba3650ed7","src/store.rs":"fdac19148c662aff1ea2ece90dbc384b59acf3adbf469157844d04812e7c048a","src/tests.rs":"f17cd786b501867174b8fa2524c509d4301064c219ee001129c889f0b84ee6e9","src/tree.rs":"7d3a1fbe7f92673f0c1f8297c86dd03ce1abe1b0f64e1554b635911e8edef82b"},"package":"26b7583e1427e296c852f3217eaab3890e698f742b8d7349beb1f40c4e946fc9"}
\ No newline at end of file
--- a/third_party/rust/dogear/Cargo.toml
+++ b/third_party/rust/dogear/Cargo.toml
@@ -8,20 +8,21 @@
 # If you believe there's an error in this file please file an
 # issue against the rust-lang/cargo repository. If you're
 # editing this file be aware that the upstream Cargo.toml
 # will likely look very different (and much more reasonable)
 
 [package]
 edition = "2018"
 name = "dogear"
-version = "0.2.4"
+version = "0.2.5"
 authors = ["Lina Cambridge <lina@mozilla.com>"]
-exclude = ["/.travis/**", ".travis.yml"]
+exclude = ["/.travis/**", ".travis.yml", "/docs/**"]
 description = "A library for merging bookmark trees."
+readme = "README.md"
 license = "Apache-2.0"
 repository = "https://github.com/mozilla/dogear"
 [dependencies.log]
 version = "0.4"
 
 [dependencies.smallbitvec]
 version = "2.3.0"
 [dev-dependencies.env_logger]
new file mode 100644
--- /dev/null
+++ b/third_party/rust/dogear/book.toml
@@ -0,0 +1,9 @@
+[book]
+title = "The Dogeared Book"
+author = "Lina Cambridge"
+description = "A guided introduction to bookmark merging with Dogear."
+src = "docs"
+
+[build]
+build-dir = "book"
+create-missing = false
--- a/third_party/rust/dogear/src/driver.rs
+++ b/third_party/rust/dogear/src/driver.rs
@@ -14,16 +14,50 @@
 
 use std::fmt::Arguments;
 
 use log::{Level, LevelFilter, Log};
 
 use crate::error::{ErrorKind, Result};
 use crate::guid::Guid;
 
+/// An abort signal is used to abort merging. Implementations of `AbortSignal`
+/// can store an aborted flag, usually as an atomic integer or Boolean, set
+/// the flag on abort, and have `AbortSignal::aborted` return the flag's value.
+///
+/// Since merging is synchronous, it's not possible to interrupt a merge from
+/// the same thread that started it. In practice, this means a signal will
+/// implement `Send` and `Sync`, too, so that another thread can set the
+/// aborted flag.
+///
+/// The name comes from the `AbortSignal` DOM API.
+pub trait AbortSignal {
+    /// Indicates if the caller signaled to abort.
+    fn aborted(&self) -> bool;
+
+    /// Returns an error if the caller signaled to abort. This helper makes it
+    /// easier to use the signal with the `?` operator.
+    fn err_if_aborted(&self) -> Result<()> {
+        if self.aborted() {
+            Err(ErrorKind::Abort.into())
+        } else {
+            Ok(())
+        }
+    }
+}
+
+/// A default signal that can't be aborted.
+pub struct DefaultAbortSignal;
+
+impl AbortSignal for DefaultAbortSignal {
+    fn aborted(&self) -> bool {
+        false
+    }
+}
+
 /// A merge driver provides methods to customize merging behavior.
 pub trait Driver {
     /// Generates a new GUID for the given invalid GUID. This is used to fix up
     /// items with GUIDs that Places can't store (bug 1380606, bug 1313026).
     ///
     /// The default implementation returns an error, forbidding invalid GUIDs.
     ///
     /// Implementations of `Driver` can either use the `rand` and `base64`
--- a/third_party/rust/dogear/src/error.rs
+++ b/third_party/rust/dogear/src/error.rs
@@ -83,16 +83,17 @@ impl fmt::Display for Error {
             ErrorKind::UnmergedRemoteItems => {
                 write!(f, "Merged tree doesn't mention all items from remote tree")
             }
             ErrorKind::InvalidGuid(invalid_guid) => {
                 write!(f, "Merged tree contains invalid GUID {}", invalid_guid)
             }
             ErrorKind::InvalidByte(b) => write!(f, "Invalid byte {} in UTF-16 encoding", b),
             ErrorKind::MalformedString(err) => err.fmt(f),
+            ErrorKind::Abort => write!(f, "Operation aborted"),
         }
     }
 }
 
 #[derive(Debug)]
 pub enum ErrorKind {
     MismatchedItemKind(Kind, Kind),
     DuplicateItem(Guid),
@@ -101,9 +102,10 @@ pub enum ErrorKind {
     MissingItem(Guid),
     Cycle(Guid),
     MergeConflict,
     UnmergedLocalItems,
     UnmergedRemoteItems,
     InvalidGuid(Guid),
     InvalidByte(u16),
     MalformedString(Box<dyn error::Error + Send + Sync + 'static>),
+    Abort,
 }
--- a/third_party/rust/dogear/src/guid.rs
+++ b/third_party/rust/dogear/src/guid.rs
@@ -32,17 +32,17 @@ pub trait IsValidGuid {
 
 /// The internal representation of a GUID. Valid GUIDs are 12 bytes, and contain
 /// only Base64url characters; we can store them on the stack without a heap
 /// allocation. However, both local and remote items might have invalid GUIDs,
 /// in which case we fall back to a heap-allocated string.
 #[derive(Clone)]
 enum Repr {
     Valid([u8; 12]),
-    Invalid(String),
+    Invalid(Box<str>),
 }
 
 /// The Places root GUID, used to root all items in a bookmark tree.
 pub const ROOT_GUID: Guid = Guid(Repr::Valid(*b"root________"));
 
 /// The bookmarks toolbar GUID.
 pub const TOOLBAR_GUID: Guid = Guid(Repr::Valid(*b"toolbar_____"));
 
@@ -90,29 +90,29 @@ impl Guid {
                 if byte > u16::from(u8::max_value()) {
                     return Err(ErrorKind::InvalidByte(byte).into());
                 }
                 bytes[index] = byte as u8;
             }
             Repr::Valid(bytes)
         } else {
             match String::from_utf16(b) {
-                Ok(s) => Repr::Invalid(s),
+                Ok(s) => Repr::Invalid(s.into()),
                 Err(err) => return Err(err.into()),
             }
         };
         Ok(Guid(repr))
     }
 
     /// Returns the GUID as a byte slice.
     #[inline]
     pub fn as_bytes(&self) -> &[u8] {
         match self.0 {
             Repr::Valid(ref bytes) => bytes,
-            Repr::Invalid(ref s) => s.as_ref(),
+            Repr::Invalid(ref s) => s.as_bytes(),
         }
     }
 
     /// Returns the GUID as a string slice.
     #[inline]
     pub fn as_str(&self) -> &str {
         // We actually could use from_utf8_unchecked here, and depending on how
         // often we end up doing this, it's arguable that we should. We know
@@ -209,17 +209,17 @@ impl PartialOrd for Guid {
         Some(self.cmp(other))
     }
 }
 
 // Allow direct comparison with str
 impl PartialEq<str> for Guid {
     #[inline]
     fn eq(&self, other: &str) -> bool {
-        self.as_str() == other
+        self.as_bytes() == other.as_bytes()
     }
 }
 
 impl<'a> PartialEq<&'a str> for Guid {
     #[inline]
     fn eq(&self, other: &&'a str) -> bool {
         self == *other
     }
--- a/third_party/rust/dogear/src/lib.rs
+++ b/third_party/rust/dogear/src/lib.rs
@@ -22,17 +22,17 @@ mod guid;
 mod merge;
 #[macro_use]
 mod store;
 mod tree;
 
 #[cfg(test)]
 mod tests;
 
-pub use crate::driver::{DefaultDriver, Driver};
+pub use crate::driver::{AbortSignal, DefaultAbortSignal, DefaultDriver, Driver};
 pub use crate::error::{Error, ErrorKind, Result};
 pub use crate::guid::{Guid, MENU_GUID, MOBILE_GUID, ROOT_GUID, TOOLBAR_GUID, UNFILED_GUID};
 pub use crate::merge::{Deletion, Merger, StructureCounts};
 pub use crate::store::{MergeTimings, Stats, Store};
 pub use crate::tree::{
-    Content, IntoTree, Item, Kind, MergeState, MergedDescendant, MergedNode, MergedRoot, Tree,
-    UploadReason, Validity,
+    Content, Item, Kind, MergeState, MergedDescendant, MergedNode, MergedRoot, Tree, UploadReason,
+    Validity,
 };
--- a/third_party/rust/dogear/src/merge.rs
+++ b/third_party/rust/dogear/src/merge.rs
@@ -8,21 +8,21 @@
 //
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
 use std::{
-    collections::{HashMap, HashSet, VecDeque},
+    collections::{hash_map::Entry, HashMap, HashSet, VecDeque},
     mem,
 };
 
-use crate::driver::{DefaultDriver, Driver};
+use crate::driver::{AbortSignal, DefaultAbortSignal, DefaultDriver, Driver};
 use crate::error::{ErrorKind, Result};
 use crate::guid::{Guid, IsValidGuid};
 use crate::tree::{Content, MergeState, MergedNode, MergedRoot, Node, Tree, Validity};
 
 /// Structure change types, used to indicate if a node on one side is moved
 /// or deleted on the other.
 #[derive(Eq, PartialEq)]
 enum StructureChange {
@@ -91,35 +91,37 @@ enum ConflictResolution {
 /// The `needs_merge` flag notes *that* a bookmark changed, but not *how*. This
 /// means we might detect conflicts, and revert changes on one side, for cases
 /// that iOS can merge cleanly.
 ///
 /// Fortunately, most of our users don't organize their bookmarks into deeply
 /// nested hierarchies, or make conflicting changes on multiple devices
 /// simultaneously. A simpler two-way tree merge strikes a good balance between
 /// correctness and complexity.
-pub struct Merger<'t, D = DefaultDriver> {
+pub struct Merger<'t, D = DefaultDriver, A = DefaultAbortSignal> {
     driver: &'t D,
+    signal: &'t A,
     local_tree: &'t Tree,
     new_local_contents: Option<&'t HashMap<Guid, Content>>,
     remote_tree: &'t Tree,
     new_remote_contents: Option<&'t HashMap<Guid, Content>>,
     matching_dupes_by_local_parent_guid: HashMap<Guid, MatchingDupes<'t>>,
     merged_guids: HashSet<Guid>,
     delete_locally: HashSet<Guid>,
     delete_remotely: HashSet<Guid>,
     structure_counts: StructureCounts,
 }
 
 #[cfg(test)]
-impl<'t> Merger<'t, DefaultDriver> {
+impl<'t> Merger<'t, DefaultDriver, DefaultAbortSignal> {
     /// Creates a merger with the default merge driver.
     pub fn new(local_tree: &'t Tree, remote_tree: &'t Tree) -> Merger<'t> {
         Merger {
             driver: &DefaultDriver,
+            signal: &DefaultAbortSignal,
             local_tree,
             new_local_contents: None,
             remote_tree,
             new_remote_contents: None,
             matching_dupes_by_local_parent_guid: HashMap::new(),
             merged_guids: HashSet::new(),
             delete_locally: HashSet::new(),
             delete_remotely: HashSet::new(),
@@ -131,35 +133,38 @@ impl<'t> Merger<'t, DefaultDriver> {
     pub fn with_contents(
         local_tree: &'t Tree,
         new_local_contents: &'t HashMap<Guid, Content>,
         remote_tree: &'t Tree,
         new_remote_contents: &'t HashMap<Guid, Content>,
     ) -> Merger<'t> {
         Merger::with_driver(
             &DefaultDriver,
+            &DefaultAbortSignal,
             local_tree,
             new_local_contents,
             remote_tree,
             new_remote_contents,
         )
     }
 }
 
-impl<'t, D: Driver> Merger<'t, D> {
+impl<'t, D: Driver, A: AbortSignal> Merger<'t, D, A> {
     /// Creates a merger with the given merge driver and contents.
     pub fn with_driver(
         driver: &'t D,
+        signal: &'t A,
         local_tree: &'t Tree,
         new_local_contents: &'t HashMap<Guid, Content>,
         remote_tree: &'t Tree,
         new_remote_contents: &'t HashMap<Guid, Content>,
-    ) -> Merger<'t, D> {
+    ) -> Merger<'t, D, A> {
         Merger {
             driver,
+            signal,
             local_tree,
             new_local_contents: Some(new_local_contents),
             remote_tree,
             new_remote_contents: Some(new_remote_contents),
             matching_dupes_by_local_parent_guid: HashMap::new(),
             merged_guids: HashSet::new(),
             delete_locally: HashSet::new(),
             delete_remotely: HashSet::new(),
@@ -175,22 +180,24 @@ impl<'t, D: Driver> Merger<'t, D> {
             self.two_way_merge(local_root_node, remote_root_node)?
         };
 
         // Any remaining deletions on one side should be deleted on the other side.
         // This happens when the remote tree has tombstones for items that don't
         // exist locally, or the local tree has tombstones for items that
         // aren't on the server.
         for guid in self.local_tree.deletions() {
+            self.signal.err_if_aborted()?;
             if !self.mentions(guid) {
                 self.delete_remotely.insert(guid.clone());
                 self.structure_counts.merged_deletions += 1;
             }
         }
         for guid in self.remote_tree.deletions() {
+            self.signal.err_if_aborted()?;
             if !self.mentions(guid) {
                 self.delete_locally.insert(guid.clone());
                 self.structure_counts.merged_deletions += 1;
             }
         }
 
         Ok(MergedRoot::with_size(
             merged_root_node,
@@ -265,16 +272,17 @@ impl<'t, D: Driver> Merger<'t, D> {
 
         let merged_guid = if local_node.guid.is_valid_guid() {
             local_node.guid.clone()
         } else {
             warn!(
                 self.driver,
                 "Generating new GUID for local node {}", local_node
             );
+            self.signal.err_if_aborted()?;
             let new_guid = self.driver.generate_new_guid(&local_node.guid)?;
             if new_guid != local_node.guid {
                 if self.merged_guids.contains(&new_guid) {
                     return Err(ErrorKind::DuplicateItem(new_guid).into());
                 }
                 self.merged_guids.insert(new_guid.clone());
             }
             new_guid
@@ -282,16 +290,17 @@ impl<'t, D: Driver> Merger<'t, D> {
 
         let mut merged_node = MergedNode::new(merged_guid, MergeState::LocalOnly(local_node));
         if local_node.is_folder() {
             // The local folder doesn't exist remotely, but its children might, so
             // we still need to recursively walk and merge them. This method will
             // change the merge state from local to new if any children were moved
             // or deleted.
             for local_child_node in local_node.children() {
+                self.signal.err_if_aborted()?;
                 self.merge_local_child_into_merged_node(
                     &mut merged_node,
                     local_node,
                     None,
                     local_child_node,
                 )?;
             }
         }
@@ -306,16 +315,17 @@ impl<'t, D: Driver> Merger<'t, D> {
 
         let merged_guid = if remote_node.guid.is_valid_guid() {
             remote_node.guid.clone()
         } else {
             warn!(
                 self.driver,
                 "Generating new GUID for remote node {}", remote_node
             );
+            self.signal.err_if_aborted()?;
             let new_guid = self.driver.generate_new_guid(&remote_node.guid)?;
             if new_guid != remote_node.guid {
                 if self.merged_guids.contains(&new_guid) {
                     return Err(ErrorKind::DuplicateItem(new_guid).into());
                 }
                 self.merged_guids.insert(new_guid.clone());
                 // Upload tombstones for changed remote GUIDs.
                 self.delete_remotely.insert(remote_node.guid.clone());
@@ -324,16 +334,17 @@ impl<'t, D: Driver> Merger<'t, D> {
             new_guid
         };
         let mut merged_node = MergedNode::new(merged_guid, MergeState::RemoteOnly(remote_node));
         if remote_node.is_folder() {
             // As above, a remote folder's children might still exist locally, so we
             // need to merge them and update the merge state from remote to new if
             // any children were moved or deleted.
             for remote_child_node in remote_node.children() {
+                self.signal.err_if_aborted()?;
                 self.merge_remote_child_into_merged_node(
                     &mut merged_node,
                     None,
                     remote_node,
                     remote_child_node,
                 )?;
             }
         }
@@ -376,16 +387,17 @@ impl<'t, D: Driver> Merger<'t, D> {
 
         let merged_guid = if remote_node.guid.is_valid_guid() {
             remote_node.guid.clone()
         } else {
             warn!(
                 self.driver,
                 "Generating new valid GUID for node {}", remote_node
             );
+            self.signal.err_if_aborted()?;
             let new_guid = self.driver.generate_new_guid(&remote_node.guid)?;
             if new_guid != remote_node.guid {
                 if self.merged_guids.contains(&new_guid) {
                     return Err(ErrorKind::DuplicateItem(new_guid).into());
                 }
                 self.merged_guids.insert(new_guid.clone());
                 // Upload tombstones for changed remote GUIDs.
                 self.delete_remotely.insert(remote_node.guid.clone());
@@ -412,43 +424,47 @@ impl<'t, D: Driver> Merger<'t, D> {
                     remote_node,
                 },
             },
         );
 
         match children {
             ConflictResolution::Local => {
                 for local_child_node in local_node.children() {
+                    self.signal.err_if_aborted()?;
                     self.merge_local_child_into_merged_node(
                         &mut merged_node,
                         local_node,
                         Some(remote_node),
                         local_child_node,
                     )?;
                 }
                 for remote_child_node in remote_node.children() {
+                    self.signal.err_if_aborted()?;
                     self.merge_remote_child_into_merged_node(
                         &mut merged_node,
                         Some(local_node),
                         remote_node,
                         remote_child_node,
                     )?;
                 }
             }
 
             ConflictResolution::Remote | ConflictResolution::Unchanged => {
                 for remote_child_node in remote_node.children() {
+                    self.signal.err_if_aborted()?;
                     self.merge_remote_child_into_merged_node(
                         &mut merged_node,
                         Some(local_node),
                         remote_node,
                         remote_child_node,
                     )?;
                 }
                 for local_child_node in local_node.children() {
+                    self.signal.err_if_aborted()?;
                     self.merge_local_child_into_merged_node(
                         &mut merged_node,
                         local_node,
                         Some(remote_node),
                         local_child_node,
                     )?;
                 }
             }
@@ -633,17 +649,17 @@ impl<'t, D: Driver> Merger<'t, D> {
         );
 
         let mut merged_child_node = if let Some(local_child_node_by_content) = self
             .find_local_node_matching_remote_node(
                 merged_node,
                 local_parent_node,
                 remote_parent_node,
                 remote_child_node,
-            ) {
+            )? {
             self.two_way_merge(local_child_node_by_content, remote_child_node)
         } else {
             self.merge_remote_only_node(remote_child_node)
         }?;
         if merged_node.remote_guid_changed() {
             merged_child_node.merge_state = merged_child_node.merge_state.with_new_structure();
         }
         if merged_child_node.remote_guid_changed() {
@@ -839,17 +855,17 @@ impl<'t, D: Driver> Merger<'t, D> {
         );
 
         let merged_child_node = if let Some(remote_child_node_by_content) = self
             .find_remote_node_matching_local_node(
                 merged_node,
                 local_parent_node,
                 remote_parent_node,
                 local_child_node,
-            ) {
+            )? {
             // The local child has a remote content match, so take the remote GUID
             // and merge.
             let mut merged_child_node =
                 self.two_way_merge(local_child_node, remote_child_node_by_content)?;
             if merged_node.remote_guid_changed() {
                 merged_child_node.merge_state = merged_child_node.merge_state.with_new_structure();
             }
             if merged_child_node.remote_guid_changed() {
@@ -1192,16 +1208,17 @@ impl<'t, D: Driver> Merger<'t, D> {
     /// This is the inverse of `delete_local_node`.
     fn delete_remote_node(
         &mut self,
         merged_node: &mut MergedNode<'t>,
         remote_node: Node<'t>,
     ) -> Result<StructureChange> {
         self.delete_remotely.insert(remote_node.guid.clone());
         for remote_child_node in remote_node.children() {
+            self.signal.err_if_aborted()?;
             if self.merged_guids.contains(&remote_child_node.guid) {
                 trace!(
                     self.driver,
                     "Remote child {} can't be an orphan; already merged",
                     remote_child_node
                 );
                 continue;
             }
@@ -1249,16 +1266,17 @@ impl<'t, D: Driver> Merger<'t, D> {
     /// This is the inverse of `delete_remote_node`.
     fn delete_local_node(
         &mut self,
         merged_node: &mut MergedNode<'t>,
         local_node: Node<'t>,
     ) -> Result<StructureChange> {
         self.delete_locally.insert(local_node.guid.clone());
         for local_child_node in local_node.children() {
+            self.signal.err_if_aborted()?;
             if self.merged_guids.contains(&local_child_node.guid) {
                 trace!(
                     self.driver,
                     "Local child {} can't be an orphan; already merged",
                     local_child_node
                 );
                 continue;
             }
@@ -1319,20 +1337,21 @@ impl<'t, D: Driver> Merger<'t, D> {
     /// children. We cache matches in
     /// `matching_dupes_by_local_parent_guid`, so deduping all
     /// remaining children of the same folder, on both sides, only needs two
     /// O(1) map lookups per child.
     fn find_all_matching_dupes_in_folders(
         &self,
         local_parent_node: Node<'t>,
         remote_parent_node: Node<'t>,
-    ) -> MatchingDupes<'t> {
+    ) -> Result<MatchingDupes<'t>> {
         let mut dupe_key_to_local_nodes: HashMap<&Content, VecDeque<_>> = HashMap::new();
 
         for local_child_node in local_parent_node.children() {
+            self.signal.err_if_aborted()?;
             if local_child_node.is_user_content_root() {
                 continue;
             }
             if let Some(local_child_content) = self
                 .new_local_contents
                 .and_then(|contents| contents.get(&local_child_node.guid))
             {
                 if let Some(remote_child_node) =
@@ -1369,16 +1388,17 @@ impl<'t, D: Driver> Merger<'t, D> {
                 );
             }
         }
 
         let mut local_to_remote = HashMap::new();
         let mut remote_to_local = HashMap::new();
 
         for remote_child_node in remote_parent_node.children() {
+            self.signal.err_if_aborted()?;
             if remote_to_local.contains_key(&remote_child_node.guid) {
                 trace!(
                     self.driver,
                     "Not deduping remote child {}; already deduped",
                     remote_child_node
                 );
                 continue;
             }
@@ -1421,121 +1441,129 @@ impl<'t, D: Driver> Merger<'t, D> {
                 trace!(
                     self.driver,
                     "Not deduping remote child {}; already merged",
                     remote_child_node
                 );
             }
         }
 
-        (local_to_remote, remote_to_local)
+        Ok((local_to_remote, remote_to_local))
     }
 
     /// Finds a remote node with a different GUID that matches the content of a
     /// local node.
     ///
     /// This is the inverse of `find_local_node_matching_remote_node`.
     fn find_remote_node_matching_local_node(
         &mut self,
         merged_node: &MergedNode<'t>,
         local_parent_node: Node<'t>,
         remote_parent_node: Option<Node<'t>>,
         local_child_node: Node<'t>,
-    ) -> Option<Node<'t>> {
+    ) -> Result<Option<Node<'t>>> {
         if let Some(remote_parent_node) = remote_parent_node {
             let mut matching_dupes_by_local_parent_guid = mem::replace(
                 &mut self.matching_dupes_by_local_parent_guid,
                 HashMap::new(),
             );
             let new_remote_node = {
-                let (local_to_remote, _) = matching_dupes_by_local_parent_guid
+                let (local_to_remote, _) = match matching_dupes_by_local_parent_guid
                     .entry(local_parent_node.guid.clone())
-                    .or_insert_with(|| {
+                {
+                    Entry::Occupied(entry) => entry.into_mut(),
+                    Entry::Vacant(entry) => {
                         trace!(
                             self.driver,
                             "First local child {} doesn't exist remotely; \
                              finding all matching dupes in local {} and remote {}",
                             local_child_node,
                             local_parent_node,
                             remote_parent_node
                         );
-                        self.find_all_matching_dupes_in_folders(
+                        let matching_dupes = self.find_all_matching_dupes_in_folders(
                             local_parent_node,
                             remote_parent_node,
-                        )
-                    });
+                        )?;
+                        entry.insert(matching_dupes)
+                    }
+                };
                 let new_remote_node = local_to_remote.get(&local_child_node.guid);
                 new_remote_node.map(|node| {
                     self.structure_counts.dupes += 1;
                     *node
                 })
             };
             mem::replace(
                 &mut self.matching_dupes_by_local_parent_guid,
                 matching_dupes_by_local_parent_guid,
             );
-            new_remote_node
+            Ok(new_remote_node)
         } else {
             trace!(
                 self.driver,
                 "Merged node {} doesn't exist remotely; no potential dupes for local child {}",
                 merged_node,
                 local_child_node
             );
-            None
+            Ok(None)
         }
     }
 
     /// Finds a local node with a different GUID that matches the content of a
     /// remote node.
     ///
     /// This is the inverse of `find_remote_node_matching_local_node`.
     fn find_local_node_matching_remote_node(
         &mut self,
         merged_node: &MergedNode<'t>,
         local_parent_node: Option<Node<'t>>,
         remote_parent_node: Node<'t>,
         remote_child_node: Node<'t>,
-    ) -> Option<Node<'t>> {
+    ) -> Result<Option<Node<'t>>> {
         if let Some(local_parent_node) = local_parent_node {
             let mut matching_dupes_by_local_parent_guid = mem::replace(
                 &mut self.matching_dupes_by_local_parent_guid,
                 HashMap::new(),
             );
             let new_local_node = {
-                let (_, remote_to_local) = matching_dupes_by_local_parent_guid
+                let (_, remote_to_local) = match matching_dupes_by_local_parent_guid
                     .entry(local_parent_node.guid.clone())
-                    .or_insert_with(|| {
+                {
+                    Entry::Occupied(entry) => entry.into_mut(),
+                    Entry::Vacant(entry) => {
                         trace!(
                             self.driver,
                             "First remote child {} doesn't exist locally; \
                              finding all matching dupes in local {} and remote {}",
                             remote_child_node,
                             local_parent_node,
                             remote_parent_node
                         );
-                        self.find_all_matching_dupes_in_folders(
+                        let matching_dupes = self.find_all_matching_dupes_in_folders(
                             local_parent_node,
                             remote_parent_node,
-                        )
-                    });
+                        )?;
+                        entry.insert(matching_dupes)
+                    }
+                };
                 let new_local_node = remote_to_local.get(&remote_child_node.guid);
                 new_local_node.map(|node| {
                     self.structure_counts.dupes += 1;
                     *node
                 })
             };
             mem::replace(
                 &mut self.matching_dupes_by_local_parent_guid,
                 matching_dupes_by_local_parent_guid,
             );
-            new_local_node
+            Ok(new_local_node)
         } else {
             trace!(
                 self.driver,
                 "Merged node {} doesn't exist locally; no potential dupes for remote child {}",
                 merged_node,
                 remote_child_node
             );
-            None
+            Ok(None)
         }
     }
 }
--- a/third_party/rust/dogear/src/store.rs
+++ b/third_party/rust/dogear/src/store.rs
@@ -9,17 +9,17 @@
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
 use std::{collections::HashMap, time::Duration};
 
-use crate::driver::{DefaultDriver, Driver};
+use crate::driver::{AbortSignal, DefaultAbortSignal, DefaultDriver, Driver};
 use crate::error::{Error, ErrorKind};
 use crate::guid::Guid;
 use crate::merge::{Deletion, Merger, StructureCounts};
 use crate::tree::{Content, MergedRoot, Tree};
 
 /// Records timings and counters for telemetry.
 #[derive(Clone, Debug, Default, Eq, PartialEq)]
 pub struct Stats {
@@ -76,42 +76,52 @@ pub trait Store<E: From<Error>> {
     fn apply<'t>(
         &mut self,
         root: MergedRoot<'t>,
         deletions: impl Iterator<Item = Deletion<'t>>,
     ) -> Result<(), E>;
 
     /// Builds and applies a merged tree using the default merge driver.
     fn merge(&mut self) -> Result<Stats, E> {
-        self.merge_with_driver(&DefaultDriver)
+        self.merge_with_driver(&DefaultDriver, &DefaultAbortSignal)
     }
 
     /// Builds a complete merged tree from the local and remote trees, resolves
     /// conflicts, dedupes local items, and applies the merged tree using the
     /// given driver.
-    fn merge_with_driver<D: Driver>(&mut self, driver: &D) -> Result<Stats, E> {
+    fn merge_with_driver<D: Driver, A: AbortSignal>(
+        &mut self,
+        driver: &D,
+        signal: &A,
+    ) -> Result<Stats, E> {
         let mut merge_timings = MergeTimings::default();
+
+        signal.err_if_aborted()?;
         let local_tree = time!(merge_timings, fetch_local_tree, { self.fetch_local_tree() })?;
         debug!(driver, "Built local tree from mirror\n{}", local_tree);
 
+        signal.err_if_aborted()?;
         let new_local_contents = time!(merge_timings, fetch_new_local_contents, {
             self.fetch_new_local_contents()
         })?;
 
+        signal.err_if_aborted()?;
         let remote_tree = time!(merge_timings, fetch_remote_tree, {
             self.fetch_remote_tree()
         })?;
         debug!(driver, "Built remote tree from mirror\n{}", remote_tree);
 
+        signal.err_if_aborted()?;
         let new_remote_contents = time!(merge_timings, fetch_new_remote_contents, {
             self.fetch_new_remote_contents()
         })?;
 
         let mut merger = Merger::with_driver(
             driver,
+            signal,
             &local_tree,
             &new_local_contents,
             &remote_tree,
             &new_remote_contents,
         );
         let merged_root = time!(merge_timings, merge, merger.merge())?;
         debug!(
             driver,
@@ -127,19 +137,23 @@ pub trait Store<E: From<Error>> {
                 .map(|d| d.guid.as_str())
                 .collect::<Vec<_>>()
                 .join(", ")
         );
 
         // The merged tree should know about all items mentioned in the local
         // and remote trees. Otherwise, it's incomplete, and we can't apply it.
         // This indicates a bug in the merger.
+
+        signal.err_if_aborted()?;
         if !merger.subsumes(&local_tree) {
             Err(E::from(ErrorKind::UnmergedLocalItems.into()))?;
         }
+
+        signal.err_if_aborted()?;
         if !merger.subsumes(&remote_tree) {
             Err(E::from(ErrorKind::UnmergedRemoteItems.into()))?;
         }
 
         time!(
             merge_timings,
             apply,
             self.apply(merged_root, merger.deletions())
--- a/third_party/rust/dogear/src/tests.rs
+++ b/third_party/rust/dogear/src/tests.rs
@@ -7,72 +7,86 @@
 //     http://www.apache.org/licenses/LICENSE-2.0
 //
 // Unless required by applicable law or agreed to in writing, software
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use std::{cell::Cell, collections::HashMap, sync::Once};
+use std::{
+    cell::Cell,
+    collections::HashMap,
+    convert::{TryFrom, TryInto},
+    sync::Once,
+};
 
 use env_logger;
 
-use crate::driver::Driver;
-use crate::error::{ErrorKind, Result};
+use crate::driver::{DefaultAbortSignal, Driver};
+use crate::error::{Error, ErrorKind, Result};
 use crate::guid::{Guid, ROOT_GUID, UNFILED_GUID};
 use crate::merge::{Merger, StructureCounts};
 use crate::tree::{
-    Builder, Content, DivergedParent, DivergedParentGuid, IntoTree, Item, Kind, Problem, Problems,
-    Tree, Validity,
+    Builder, Content, DivergedParent, DivergedParentGuid, Item, Kind, Problem, Problems, Tree,
+    Validity,
 };
 
 #[derive(Debug)]
 struct Node {
     item: Item,
     children: Vec<Node>,
 }
 
 impl Node {
     fn new(item: Item) -> Node {
         Node {
             item,
             children: Vec::new(),
         }
     }
+    /// For convenience.
+    fn into_tree(self) -> Result<Tree> {
+        self.try_into()
+    }
+}
 
-    fn into_builder(self) -> Result<Builder> {
+impl TryFrom<Node> for Builder {
+    type Error = Error;
+
+    fn try_from(node: Node) -> Result<Builder> {
         fn inflate(b: &mut Builder, parent_guid: &Guid, node: Node) -> Result<()> {
             let guid = node.item.guid.clone();
             b.item(node.item)
                 .map(|_| ())
                 .or_else(|err| match err.kind() {
                     ErrorKind::DuplicateItem(_) => Ok(()),
                     _ => Err(err),
                 })?;
             b.parent_for(&guid).by_structure(&parent_guid)?;
             for child in node.children {
                 inflate(b, &guid, child)?;
             }
             Ok(())
         }
 
-        let guid = self.item.guid.clone();
-        let mut builder = Tree::with_root(self.item);
+        let guid = node.item.guid.clone();
+        let mut builder = Tree::with_root(node.item);
         builder.reparent_orphans_to(&UNFILED_GUID);
-        for child in self.children {
+        for child in node.children {
             inflate(&mut builder, &guid, child)?;
         }
         Ok(builder)
     }
 }
 
-impl IntoTree for Node {
-    fn into_tree(self) -> Result<Tree> {
-        self.into_builder()?.into_tree()
+impl TryFrom<Node> for Tree {
+    type Error = Error;
+    fn try_from(node: Node) -> Result<Tree> {
+        Builder::try_from(node)?.try_into()
     }
 }
 
 macro_rules! nodes {
     ($children:tt) => { nodes!(ROOT_GUID, Folder[needs_merge = true], $children) };
     ($guid:expr, $kind:ident) => { nodes!(Guid::from($guid), $kind[]) };
     ($guid:expr, $kind:ident [ $( $name:ident = $value:expr ),* ]) => {{
         #[allow(unused_mut)]
@@ -2274,16 +2288,17 @@ fn invalid_guids() {
     })
     .into_tree()
     .unwrap();
     let new_remote_contents: HashMap<Guid, Content> = HashMap::new();
 
     let driver = GenerateNewGuid::default();
     let mut merger = Merger::with_driver(
         &driver,
+        &DefaultAbortSignal,
         &local_tree,
         &new_local_contents,
         &remote_tree,
         &new_remote_contents,
     );
     let merged_root = merger.merge().unwrap();
     assert!(merger.subsumes(&local_tree));
     assert!(merger.subsumes(&remote_tree));
@@ -2396,27 +2411,27 @@ fn reparent_orphans() {
         }),
         ("unfiled_____", Folder, {
             ("bookmarkCCCC", Bookmark)
         })
     })
     .into_tree()
     .unwrap();
 
-    let mut remote_tree_builder = nodes!({
+    let mut remote_tree_builder: Builder = nodes!({
         ("toolbar_____", Folder[needs_merge = true], {
             ("bookmarkBBBB", Bookmark),
             ("bookmarkAAAA", Bookmark)
         }),
         ("unfiled_____", Folder[needs_merge = true], {
             ("bookmarkDDDD", Bookmark[needs_merge = true]),
             ("bookmarkCCCC", Bookmark)
         })
     })
-    .into_builder()
+    .try_into()
     .unwrap();
     remote_tree_builder
         .item(Item {
             guid: "bookmarkEEEE".into(),
             kind: Kind::Bookmark,
             age: 0,
             needs_merge: true,
             validity: Validity::Valid,
@@ -2608,17 +2623,17 @@ fn moved_user_content_roots() {
 }
 
 #[test]
 fn cycle() {
     before_each();
 
     // Try to create a cycle: move A into B, and B into the menu, but keep
     // B's parent by children as A.
-    let mut b = nodes!({ ("menu________", Folder) }).into_builder().unwrap();
+    let mut b: Builder = nodes!({ ("menu________", Folder) }).try_into().unwrap();
 
     b.item(Item::new("folderAAAAAA".into(), Kind::Folder))
         .and_then(|p| p.by_parent_guid("folderBBBBBB".into()))
         .expect("Should insert A");
 
     b.item(Item::new("folderBBBBBB".into(), Kind::Folder))
         .and_then(|p| p.by_parent_guid("menu________".into()))
         .and_then(|b| {
--- a/third_party/rust/dogear/src/tree.rs
+++ b/third_party/rust/dogear/src/tree.rs
@@ -11,35 +11,30 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
 use std::{
     borrow::Cow,
     cmp::Ordering,
     collections::{HashMap, HashSet},
+    convert::{TryFrom, TryInto},
     fmt, mem,
     ops::Deref,
     ptr,
 };
 
 use smallbitvec::SmallBitVec;
 
-use crate::error::{ErrorKind, Result};
+use crate::error::{Error, ErrorKind, Result};
 use crate::guid::Guid;
 
 /// The type for entry indices in the tree.
 type Index = usize;
 
-/// Anything that can be turned into a tree.
-pub trait IntoTree {
-    /// Performs the conversion.
-    fn into_tree(self) -> Result<Tree>;
-}
-
 /// A complete, rooted bookmark tree with tombstones.
 ///
 /// The tree stores bookmark items in a vector, and uses indices in the vector
 /// to identify parents and children. This makes traversal and lookup very
 /// efficient. Retrieving a node's parent takes one indexing operation,
 /// retrieving children takes one indexing operation per child, and retrieving
 /// a node by random GUID takes one hash map lookup and one indexing operation.
 #[derive(Debug)]
@@ -113,23 +108,16 @@ impl Tree {
 
     /// Returns the structure divergences found when building the tree.
     #[inline]
     pub fn problems(&self) -> &Problems {
         &self.problems
     }
 }
 
-impl IntoTree for Tree {
-    #[inline]
-    fn into_tree(self) -> Result<Tree> {
-        Ok(self)
-    }
-}
-
 impl fmt::Display for Tree {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         let root = self.root();
         f.write_str(&root.to_ascii_string())?;
         if !self.deleted_guids.is_empty() {
             f.write_str("\nDeleted: [")?;
             for (i, guid) in self.deleted_guids.iter().enumerate() {
                 if i != 0 {
@@ -219,18 +207,18 @@ impl PartialEq for Tree {
 /// - Items whose `parentid`s don't mention the item in their `children`.
 /// - Items with `parentid`s that point to nonexistent or deleted folders.
 /// - Folders with nonexistent `children`.
 /// - Non-syncable items, like custom roots.
 /// - Any combination of these.
 ///
 /// # Resolving divergences
 ///
-/// Building a tree using `Builder::into_tree` resolves divergences using
-/// these rules:
+/// Building a tree using `std::convert::TryInto<Tree>::try_into` resolves
+/// divergences using these rules:
 ///
 /// 1. User content roots should always be children of the Places root. If
 ///    they appear in other parents, we move them.
 /// 2. Items that appear in multiple `children`, and items with mismatched
 ///    `parentid`s, use the chronologically newer parent, based on the parent's
 ///    last modified time. We always prefer parents by `children` over
 ///    `parentid,` because `children` also gives us the item's position.
 /// 3. Items that aren't mentioned in any parent's `children`, but have a
@@ -282,55 +270,64 @@ impl Builder {
     pub fn parent_for(&mut self, child_guid: &Guid) -> ParentBuilder<'_> {
         assert_eq!(self.entries.len(), self.entry_index_by_guid.len());
         let entry_child = match self.entry_index_by_guid.get(child_guid) {
             Some(&child_index) => BuilderEntryChild::Exists(child_index),
             None => BuilderEntryChild::Missing(child_guid.clone()),
         };
         ParentBuilder(self, entry_child)
     }
+
+    /// Equivalent to using our implementation of`TryInto<Tree>::try_into`, but
+    /// provided both for convenience when updating from previous versions of
+    /// `dogear`, and for cases where a type hint would otherwise be needed to
+    /// clarify the target type of the conversion.
+    pub fn into_tree(self) -> Result<Tree> {
+        self.try_into()
+    }
 }
 
-impl IntoTree for Builder {
+impl TryFrom<Builder> for Tree {
+    type Error = Error;
     /// Builds a tree from all stored items and parent-child associations,
     /// resolving inconsistencies like orphans, multiple parents, and
     /// parent-child disagreements.
-    fn into_tree(self) -> Result<Tree> {
+    fn try_from(builder: Builder) -> Result<Tree> {
         let mut problems = Problems::default();
 
         // First, resolve parents for all entries, and build a lookup table for
         // items without a position.
-        let mut parents = Vec::with_capacity(self.entries.len());
+        let mut parents = Vec::with_capacity(builder.entries.len());
         let mut reparented_child_indices_by_parent: HashMap<Index, Vec<Index>> = HashMap::new();
-        for (entry_index, entry) in self.entries.iter().enumerate() {
-            let r = ResolveParent::new(&self, entry, &mut problems);
+        for (entry_index, entry) in builder.entries.iter().enumerate() {
+            let r = ResolveParent::new(&builder, entry, &mut problems);
             let resolved_parent = r.resolve();
             if let ResolvedParent::ByParentGuid(parent_index) = &resolved_parent {
                 // Reparented items are special: since they aren't mentioned in
                 // that parent's `children`, we don't know their positions. Note
                 // them for when we resolve children. We also clone the GUID,
                 // since we use it for sorting, but can't access it by
-                // reference once we call `self.entries.into_iter()` below.
+                // reference once we call `builder.entries.into_iter()` below.
                 let reparented_child_indices = reparented_child_indices_by_parent
                     .entry(*parent_index)
                     .or_default();
                 reparented_child_indices.push(entry_index);
             }
             parents.push(resolved_parent);
         }
 
         // If any parents form cycles, abort. We haven't seen cyclic trees in
         // the wild, and breaking cycles would add complexity.
         if let Some(index) = detect_cycles(&parents) {
-            return Err(ErrorKind::Cycle(self.entries[index].item.guid.clone()).into());
+            return Err(ErrorKind::Cycle(builder.entries[index].item.guid.clone()).into());
         }
 
         // Then, resolve children, and build a slab of entries for the tree.
-        let mut entries = Vec::with_capacity(self.entries.len());
-        for (entry_index, entry) in self.entries.into_iter().enumerate() {
+        let mut entries = Vec::with_capacity(builder.entries.len());
+        for (entry_index, entry) in builder.entries.into_iter().enumerate() {
             // Each entry is consistent, until proven otherwise!
             let mut divergence = Divergence::Consistent;
 
             let parent_index = match &parents[entry_index] {
                 ResolvedParent::Root => {
                     // The Places root doesn't have a parent, and should always
                     // be the first entry.
                     assert_eq!(entry_index, 0);
@@ -412,17 +409,17 @@ impl IntoTree for Builder {
                 parent_index,
                 child_indices,
                 divergence,
             });
         }
 
         // Now we have a consistent tree.
         Ok(Tree {
-            entry_index_by_guid: self.entry_index_by_guid,
+            entry_index_by_guid: builder.entry_index_by_guid,
             entries,
             deleted_guids: HashSet::new(),
             problems,
         })
     }
 }
 
 /// Describes where an item's parent comes from.
@@ -467,17 +464,17 @@ impl<'b> ParentBuilder<'b> {
             }
         }
         Ok(self.0)
     }
 
     /// Records a `parent_guid` from a valid tree structure. This is for
     /// callers who already know their structure is consistent, like
     /// `Store::fetch_local_tree()` on Desktop, and
-    /// `{MergedNode, Node}::into_tree()` in the tests.
+    /// `std::convert::TryInto<Tree>` in the tests.
     ///
     /// Both the item and `parent_guid` must exist, and the `parent_guid` must
     /// refer to a folder.
     ///
     /// `by_structure(parent_guid)` is logically the same as:
     ///
     /// ```no_run
     /// # use dogear::{Item, Kind, Result, ROOT_GUID, Tree};
@@ -1472,40 +1469,47 @@ impl<'t> MergedRoot<'t> {
         results
     }
 
     /// Returns an ASCII art representation of the root and its descendants,
     /// similar to `Node::to_ascii_string`.
     pub fn to_ascii_string(&self) -> String {
         self.node.to_ascii_fragment("")
     }
+
+    /// Lets us avoid needing to specify the target type in tests.
+    #[cfg(test)]
+    pub(crate) fn into_tree(self) -> Result<Tree> {
+        self.try_into()
+    }
 }
 
 #[cfg(test)]
-impl<'t> IntoTree for MergedRoot<'t> {
-    fn into_tree(self) -> Result<Tree> {
+impl<'t> TryFrom<MergedRoot<'t>> for Tree {
+    type Error = Error;
+    fn try_from(merged_root: MergedRoot<'t>) -> Result<Tree> {
         fn to_item(merged_node: &MergedNode<'_>) -> Item {
             let node = merged_node.merge_state.node();
             let mut item = Item::new(merged_node.guid.clone(), node.kind);
             item.age = node.age;
             item.needs_merge = merged_node.merge_state.upload_reason() != UploadReason::None;
             item
         }
 
-        let mut b = Tree::with_root(to_item(&self.node));
+        let mut b = Tree::with_root(to_item(&merged_root.node));
         for MergedDescendant {
             merged_parent_node,
             merged_node,
             ..
-        } in self.descendants()
+        } in merged_root.descendants()
         {
             b.item(to_item(merged_node))?
                 .by_structure(&merged_parent_node.guid)?;
         }
-        b.into_tree()
+        b.try_into()
     }
 }
 
 /// A merged bookmark node that indicates which side to prefer, and holds merged
 /// child nodes.
 #[derive(Debug)]
 pub struct MergedNode<'t> {
     pub guid: Guid,
@@ -1746,17 +1750,17 @@ impl<'t> MergeState<'t> {
                     remote_node,
                 }
             }
         }
     }
 
     /// Returns the node from the preferred side. Unlike `local_node()` and
     /// `remote_node()`, this doesn't indicate which side, so it's only used
-    /// for logging and `into_tree()`.
+    /// for logging and `try_from()`.
     fn node(&self) -> &Node<'t> {
         match self {
             MergeState::LocalOnly(local_node) | MergeState::Local { local_node, .. } => local_node,
 
             MergeState::RemoteOnly(remote_node)
             | MergeState::Remote { remote_node, .. }
             | MergeState::RemoteOnlyWithNewStructure(remote_node)
             | MergeState::RemoteWithNewStructure { remote_node, .. } => remote_node,
--- a/toolkit/components/places/bookmark_sync/Cargo.toml
+++ b/toolkit/components/places/bookmark_sync/Cargo.toml
@@ -1,17 +1,17 @@
 [package]
 name = "bookmark_sync"
 version = "0.1.0"
 authors = ["Lina Cambridge <lina@yakshaving.ninja>"]
 edition = "2018"
 
 [dependencies]
 atomic_refcell = "0.1"
-dogear = "0.2.4"
+dogear = "0.2.5"
 libc = "0.2"
 log = "0.4"
 cstr = "0.1"
 moz_task = { path = "../../../../xpcom/rust/moz_task" }
 nserror = { path = "../../../../xpcom/rust/nserror" }
 nsstring = { path = "../../../../xpcom/rust/nsstring" }
 storage = { path = "../../../../storage/rust" }
 storage_variant = { path = "../../../../storage/variant" }
--- a/toolkit/components/places/bookmark_sync/src/driver.rs
+++ b/toolkit/components/places/bookmark_sync/src/driver.rs
@@ -1,15 +1,18 @@
 /* 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::fmt::Write;
+use std::{
+    fmt::Write,
+    sync::atomic::{AtomicBool, Ordering},
+};
 
-use dogear::Guid;
+use dogear::{AbortSignal, Guid};
 use log::{Level, LevelFilter, Log, Metadata, Record};
 use moz_task::{Task, TaskRunnable, ThreadPtrHandle};
 use nserror::nsresult;
 use nsstring::{nsACString, nsCString, nsString};
 use xpcom::interfaces::mozISyncedBookmarksMirrorLogger;
 
 extern "C" {
     fn NS_GeneratePlacesGUID(guid: *mut nsACString) -> nsresult;
@@ -20,16 +23,43 @@ fn generate_guid() -> Result<nsCString, 
     let rv = unsafe { NS_GeneratePlacesGUID(&mut *guid) };
     if rv.succeeded() {
         Ok(guid)
     } else {
         Err(rv)
     }
 }
 
+/// An abort controller is used to abort merges running on the storage thread
+/// from the main thread. Its design is based on the DOM API of the same name.
+pub struct AbortController {
+    aborted: AtomicBool,
+}
+
+impl AbortController {
+    /// Signals the store to stop merging as soon as it can.
+    pub fn abort(&self) {
+        self.aborted.store(true, Ordering::Release)
+    }
+}
+
+impl Default for AbortController {
+    fn default() -> AbortController {
+        AbortController {
+            aborted: AtomicBool::new(false),
+        }
+    }
+}
+
+impl AbortSignal for AbortController {
+    fn aborted(&self) -> bool {
+        self.aborted.load(Ordering::Acquire)
+    }
+}
+
 /// The merger driver, created and used on the storage thread.
 pub struct Driver {
     log: Logger,
 }
 
 impl Driver {
     #[inline]
     pub fn new(log: Logger) -> Driver {
--- a/toolkit/components/places/bookmark_sync/src/error.rs
+++ b/toolkit/components/places/bookmark_sync/src/error.rs
@@ -1,15 +1,18 @@
 /* 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::{error, fmt, result, string::FromUtf16Error};
 
-use nserror::{nsresult, NS_ERROR_INVALID_ARG, NS_ERROR_STORAGE_BUSY, NS_ERROR_UNEXPECTED};
+use nserror::{
+    nsresult, NS_ERROR_ABORT, NS_ERROR_FAILURE, NS_ERROR_INVALID_ARG, NS_ERROR_STORAGE_BUSY,
+    NS_ERROR_UNEXPECTED,
+};
 
 pub type Result<T> = result::Result<T, Error>;
 
 #[derive(Debug)]
 pub enum Error {
     Dogear(dogear::Error),
     Storage(storage::Error),
     InvalidLocalRoots,
@@ -53,19 +56,21 @@ impl From<FromUtf16Error> for Error {
     fn from(error: FromUtf16Error) -> Error {
         Error::MalformedString(error.into())
     }
 }
 
 impl From<Error> for nsresult {
     fn from(error: Error) -> nsresult {
         match error {
-            Error::Dogear(_) | Error::InvalidLocalRoots | Error::InvalidRemoteRoots => {
-                NS_ERROR_UNEXPECTED
-            }
+            Error::Dogear(err) => match err.kind() {
+                dogear::ErrorKind::Abort => NS_ERROR_ABORT,
+                _ => NS_ERROR_FAILURE,
+            },
+            Error::InvalidLocalRoots | Error::InvalidRemoteRoots => NS_ERROR_UNEXPECTED,
             Error::Storage(err) => err.into(),
             Error::Nsresult(result) => result.clone(),
             Error::UnknownItemKind(_)
             | Error::MalformedString(_)
             | Error::UnknownItemValidity(_) => NS_ERROR_INVALID_ARG,
             Error::MergeConflict => NS_ERROR_STORAGE_BUSY,
         }
     }
--- a/toolkit/components/places/bookmark_sync/src/merger.rs
+++ b/toolkit/components/places/bookmark_sync/src/merger.rs
@@ -1,13 +1,13 @@
 /* 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::{cell::RefCell, fmt::Write, mem, time::Duration};
+use std::{cell::RefCell, fmt::Write, mem, sync::Arc, time::Duration};
 
 use atomic_refcell::AtomicRefCell;
 use dogear::{MergeTimings, Stats, Store, StructureCounts};
 use log::LevelFilter;
 use moz_task::{Task, TaskRunnable, ThreadPtrHandle, ThreadPtrHolder};
 use nserror::{nsresult, NS_ERROR_FAILURE, NS_ERROR_UNEXPECTED, NS_OK};
 use nsstring::nsString;
 use storage::Conn;
@@ -15,31 +15,33 @@ use storage_variant::HashPropertyBag;
 use thin_vec::ThinVec;
 use xpcom::{
     interfaces::{
         mozIStorageConnection, mozISyncedBookmarksMirrorCallback, mozISyncedBookmarksMirrorLogger,
     },
     RefPtr,
 };
 
-use crate::driver::{Driver, Logger};
+use crate::driver::{AbortController, Driver, Logger};
 use crate::error;
 use crate::store;
 
 #[derive(xpcom)]
 #[xpimplements(mozISyncedBookmarksMerger)]
 #[refcnt = "nonatomic"]
 pub struct InitSyncedBookmarksMerger {
+    controller: Arc<AbortController>,
     db: RefCell<Option<Conn>>,
     logger: RefCell<Option<RefPtr<mozISyncedBookmarksMirrorLogger>>>,
 }
 
 impl SyncedBookmarksMerger {
     pub fn new() -> RefPtr<SyncedBookmarksMerger> {
         SyncedBookmarksMerger::allocate(InitSyncedBookmarksMerger {
+            controller: Arc::new(AbortController::default()),
             db: RefCell::default(),
             logger: RefCell::default(),
         })
     }
 
     xpcom_method!(get_db => GetDb() -> *const mozIStorageConnection);
     fn get_db(&self) -> Result<RefPtr<mozIStorageConnection>, nsresult> {
         self.db
@@ -89,16 +91,17 @@ impl SyncedBookmarksMerger {
         let db = match *self.db.borrow() {
             Some(ref db) => db.clone(),
             None => return Err(NS_ERROR_FAILURE),
         };
         let logger = &*self.logger.borrow();
         let async_thread = db.thread()?;
         let task = MergeTask::new(
             &db,
+            Arc::clone(&self.controller),
             logger.as_ref().cloned(),
             local_time_seconds,
             remote_time_seconds,
             weak_uploads
                 .map(|w| w.as_slice().to_vec())
                 .unwrap_or_default(),
             callback,
         )?;
@@ -106,36 +109,39 @@ impl SyncedBookmarksMerger {
             "bookmark_sync::SyncedBookmarksMerger::merge",
             Box::new(task),
         )?;
         runnable.dispatch(&async_thread)
     }
 
     xpcom_method!(finalize => Finalize());
     fn finalize(&self) -> Result<(), nsresult> {
+        self.controller.abort();
         mem::drop(self.db.borrow_mut().take());
         mem::drop(self.logger.borrow_mut().take());
         Ok(())
     }
 }
 
 struct MergeTask {
     db: Conn,
+    controller: Arc<AbortController>,
     max_log_level: LevelFilter,
     logger: Option<ThreadPtrHandle<mozISyncedBookmarksMirrorLogger>>,
     local_time_millis: i64,
     remote_time_millis: i64,
     weak_uploads: Vec<nsString>,
     callback: ThreadPtrHandle<mozISyncedBookmarksMirrorCallback>,
     result: AtomicRefCell<Option<error::Result<Stats>>>,
 }
 
 impl MergeTask {
     fn new(
         db: &Conn,
+        controller: Arc<AbortController>,
         logger: Option<RefPtr<mozISyncedBookmarksMirrorLogger>>,
         local_time_seconds: i64,
         remote_time_seconds: i64,
         weak_uploads: Vec<nsString>,
         callback: RefPtr<mozISyncedBookmarksMirrorCallback>,
     ) -> Result<MergeTask, nsresult> {
         let max_log_level = logger
             .as_ref()
@@ -156,39 +162,41 @@ impl MergeTask {
             Some(logger) => Some(ThreadPtrHolder::new(
                 cstr!("mozISyncedBookmarksMirrorLogger"),
                 logger,
             )?),
             None => None,
         };
         Ok(MergeTask {
             db: db.clone(),
+            controller,
             max_log_level,
             logger,
             local_time_millis: local_time_seconds * 1000,
             remote_time_millis: remote_time_seconds * 1000,
             weak_uploads,
             callback: ThreadPtrHolder::new(cstr!("mozISyncedBookmarksMirrorCallback"), callback)?,
             result: AtomicRefCell::default(),
         })
     }
 }
 
 impl Task for MergeTask {
     fn run(&self) {
         let mut db = self.db.clone();
         let mut store = store::Store::new(
             &mut db,
+            &self.controller,
             self.local_time_millis,
             self.remote_time_millis,
             &self.weak_uploads,
         );
         let log = Logger::new(self.max_log_level, self.logger.clone());
         let driver = Driver::new(log);
-        *self.result.borrow_mut() = Some(store.merge_with_driver(&driver));
+        *self.result.borrow_mut() = Some(store.merge_with_driver(&driver, &*self.controller));
     }
 
     fn done(&self) -> Result<(), nsresult> {
         let callback = self.callback.get().unwrap();
         match self.result.borrow_mut().take() {
             Some(Ok(stats)) => {
                 let mut telem = HashPropertyBag::new();
                 telem.set("fetchLocalTreeTime", stats.time(|t| t.fetch_local_tree));
--- a/toolkit/components/places/bookmark_sync/src/store.rs
+++ b/toolkit/components/places/bookmark_sync/src/store.rs
@@ -1,22 +1,23 @@
 /* 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::{collections::HashMap, fmt};
+use std::{collections::HashMap, convert::TryFrom, fmt};
 
 use dogear::{
-    Content, Deletion, Guid, IntoTree, Item, Kind, MergedDescendant, MergedRoot, Tree,
+    AbortSignal, Content, Deletion, Guid, Item, Kind, MergedDescendant, MergedRoot, Tree,
     UploadReason, Validity,
 };
 use nsstring::{nsCString, nsString};
 use storage::{Conn, Step};
 use xpcom::interfaces::{mozISyncedBookmarksMerger, nsINavBookmarksService};
 
+use crate::driver::AbortController;
 use crate::error::{Error, Result};
 
 pub const LMANNO_FEEDURI: &'static str = "livemark/feedURI";
 
 pub const LOCAL_ITEMS_SQL_FRAGMENT: &str = "
   localItems(id, guid, parentId, parentGuid, position, type, title, parentTitle,
              placeId, dateAdded, lastModified, syncChangeCounter, level) AS (
   SELECT b.id, b.guid, 0, NULL, b.position, b.type, b.title, NULL,
@@ -35,37 +36,40 @@ extern "C" {
 }
 
 fn total_sync_changes() -> i64 {
     unsafe { NS_NavBookmarksTotalSyncChanges() }
 }
 
 pub struct Store<'s> {
     db: &'s mut Conn,
+    controller: &'s AbortController,
 
     /// The total Sync change count before merging. We store this before
     /// accessing Places, and compare the current and stored counts after
     /// opening our transaction. If they match, we can safely apply the
     /// tree. Otherwise, we bail and try merging again on the next sync.
     total_sync_changes: i64,
 
     local_time_millis: i64,
     remote_time_millis: i64,
     weak_uploads: &'s [nsString],
 }
 
 impl<'s> Store<'s> {
     pub fn new(
         db: &'s mut Conn,
+        controller: &'s AbortController,
         local_time_millis: i64,
         remote_time_millis: i64,
         weak_uploads: &'s [nsString],
     ) -> Store<'s> {
         Store {
             db,
+            controller,
             total_sync_changes: total_sync_changes(),
             local_time_millis,
             remote_time_millis,
             weak_uploads,
         }
     }
 
     /// Creates a local tree item from a row in the `localItems` CTE.
@@ -147,27 +151,29 @@ impl<'s> dogear::Store<Error> for Store<
         items_statement.bind_by_name("separatorKind", mozISyncedBookmarksMerger::KIND_SEPARATOR)?;
         let mut builder = match items_statement.step()? {
             // The first row is always the root.
             Some(step) => Tree::with_root(self.local_row_to_item(&step)?),
             None => return Err(Error::InvalidLocalRoots.into()),
         };
         while let Some(step) = items_statement.step()? {
             // All subsequent rows are descendants.
+            self.controller.err_if_aborted()?;
             let raw_parent_guid: nsString = step.get_by_name("parentGuid")?;
             let parent_guid = Guid::from_utf16(&*raw_parent_guid)?;
             builder
                 .item(self.local_row_to_item(&step)?)?
                 .by_structure(&parent_guid)?;
         }
 
-        let mut tree = builder.into_tree()?;
+        let mut tree = Tree::try_from(builder)?;
 
         let mut deletions_statement = self.db.prepare("SELECT guid FROM moz_bookmarks_deleted")?;
         while let Some(step) = deletions_statement.step()? {
+            self.controller.err_if_aborted()?;
             let raw_guid: nsString = step.get_by_name("guid")?;
             let guid = Guid::from_utf16(&*raw_guid)?;
             tree.note_deleted(guid);
         }
 
         Ok(tree)
     }
 
@@ -186,16 +192,17 @@ impl<'s> dogear::Store<Error> for Store<
                LEFT JOIN items v ON v.guid = b.guid
                WHERE v.guid IS NULL AND
                      p.guid <> :rootGuid AND
                      b.syncStatus <> :syncStatus"#,
         )?;
         statement.bind_by_name("rootGuid", nsCString::from(&*dogear::ROOT_GUID))?;
         statement.bind_by_name("syncStatus", nsINavBookmarksService::SYNC_STATUS_NORMAL)?;
         while let Some(step) = statement.step()? {
+            self.controller.err_if_aborted()?;
             let typ: i64 = step.get_by_name("type")?;
             let content = match typ {
                 nsINavBookmarksService::TYPE_BOOKMARK => {
                     let raw_title: nsString = step.get_by_name("title")?;
                     let title = String::from_utf16(&*raw_title)?;
                     let raw_url_href: nsString = step.get_by_name("url")?;
                     let url_href = String::from_utf16(&*raw_url_href)?;
                     Content::Bookmark { title, url_href }
@@ -241,47 +248,50 @@ impl<'s> dogear::Store<Error> for Store<
             "SELECT guid, parentGuid, serverModified, kind, needsMerge, validity
              FROM items
              WHERE NOT isDeleted AND
                    guid <> :rootGuid
              ORDER BY guid",
         )?;
         items_statement.bind_by_name("rootGuid", nsCString::from(&*dogear::ROOT_GUID))?;
         while let Some(step) = items_statement.step()? {
+            self.controller.err_if_aborted()?;
             let p = builder.item(self.remote_row_to_item(&step)?)?;
             let raw_parent_guid: Option<nsString> = step.get_by_name("parentGuid")?;
             if let Some(raw_parent_guid) = raw_parent_guid {
                 p.by_parent_guid(Guid::from_utf16(&*raw_parent_guid)?)?;
             }
         }
 
         let mut structure_statement = self.db.prepare(
             "SELECT guid, parentGuid FROM structure
              WHERE guid <> :rootGuid
              ORDER BY parentGuid, position",
         )?;
         structure_statement.bind_by_name("rootGuid", nsCString::from(&*dogear::ROOT_GUID))?;
         while let Some(step) = structure_statement.step()? {
+            self.controller.err_if_aborted()?;
             let raw_guid: nsString = step.get_by_name("guid")?;
             let guid = Guid::from_utf16(&*raw_guid)?;
 
             let raw_parent_guid: nsString = step.get_by_name("parentGuid")?;
             let parent_guid = Guid::from_utf16(&*raw_parent_guid)?;
 
             builder.parent_for(&guid).by_children(&parent_guid)?;
         }
 
-        let mut tree = builder.into_tree()?;
+        let mut tree = Tree::try_from(builder)?;
 
         let mut deletions_statement = self.db.prepare(
             "SELECT guid FROM items
              WHERE isDeleted AND
                    needsMerge",
         )?;
         while let Some(step) = deletions_statement.step()? {
+            self.controller.err_if_aborted()?;
             let raw_guid: nsString = step.get_by_name("guid")?;
             let guid = Guid::from_utf16(&*raw_guid)?;
             tree.note_deleted(guid);
         }
 
         Ok(tree)
     }
 
@@ -300,16 +310,17 @@ impl<'s> dogear::Store<Error> for Store<
                WHERE NOT v.isDeleted AND
                      v.needsMerge AND
                      b.guid IS NULL AND
                      IFNULL(s.parentGuid, :unfiledGuid) <> :rootGuid"#,
         )?;
         statement.bind_by_name("unfiledGuid", nsCString::from(&*dogear::UNFILED_GUID))?;
         statement.bind_by_name("rootGuid", nsCString::from(&*dogear::ROOT_GUID))?;
         while let Some(step) = statement.step()? {
+            self.controller.err_if_aborted()?;
             let kind: i64 = step.get_by_name("kind")?;
             let content = match kind {
                 mozISyncedBookmarksMerger::KIND_BOOKMARK
                 | mozISyncedBookmarksMerger::KIND_QUERY => {
                     let raw_title: nsString = step.get_by_name("title")?;
                     let title = String::from_utf16(&*raw_title)?;
 
                     let raw_url_href: nsString = step.get_by_name("url")?;
@@ -339,28 +350,36 @@ impl<'s> dogear::Store<Error> for Store<
         Ok(contents)
     }
 
     fn apply<'t>(
         &mut self,
         root: MergedRoot<'t>,
         deletions: impl Iterator<Item = Deletion<'t>>,
     ) -> Result<()> {
+        self.controller.err_if_aborted()?;
         let descendants = root.descendants();
+
+        self.controller.err_if_aborted()?;
         let deletions = deletions.collect::<Vec<_>>();
 
         // Apply the merged tree and stage outgoing items. This transaction
         // blocks writes from the main connection until it's committed, so we
         // try to do as little work as possible within it.
         let tx = self.db.transaction()?;
         if self.total_sync_changes != total_sync_changes() {
             return Err(Error::MergeConflict);
         }
+
+        self.controller.err_if_aborted()?;
         update_local_items_in_places(&tx, descendants, deletions)?;
+
+        self.controller.err_if_aborted()?;
         stage_items_to_upload(&tx, &self.weak_uploads)?;
+
         cleanup(&tx)?;
         tx.commit()?;
 
         Ok(())
     }
 }
 
 /// Builds a temporary table with the merge states of all nodes in the merged
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/sync/test_bookmark_abort_merging.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_abort_merging() {
+  let buf = await openMirror("abort_merging");
+
+  let promise = new Promise((resolve, reject) => {
+    let callback = {
+      handleResult() {
+        reject(new Error("Shouldn't have merged after aborting"));
+      },
+      handleError(code, message) {
+        equal(code, Cr.NS_ERROR_ABORT, "Should abort merge with result code");
+        resolve();
+      },
+    };
+    // `merge` schedules a runnable to start the merge on the storage thread, on
+    // the next turn of the event loop. In the same turn, before the runnable is
+    // scheduled, we call `finalize`, which sets the abort controller's aborted
+    // flag.
+    buf.merger.merge(0, 0, [], callback);
+    buf.merger.finalize();
+  });
+
+  await promise;
+
+  // Even though the merger is already finalized on the Rust side, the DB
+  // connection is still open on the JS side. Finalizing `buf` closes it.
+  await buf.finalize();
+  await PlacesUtils.bookmarks.eraseEverything();
+  await PlacesSyncUtils.bookmarks.reset();
+});
--- a/toolkit/components/places/tests/sync/xpcshell.ini
+++ b/toolkit/components/places/tests/sync/xpcshell.ini
@@ -1,16 +1,17 @@
 [DEFAULT]
 head = head_sync.js
 support-files =
   sync_utils_bookmarks.html
   sync_utils_bookmarks.json
   mirror_corrupt.sqlite
   mirror_v1.sqlite
 
+[test_bookmark_abort_merging.js]
 [test_bookmark_chunking.js]
 [test_bookmark_corruption.js]
 [test_bookmark_deduping.js]
 [test_bookmark_deletion.js]
 [test_bookmark_explicit_weakupload.js]
 [test_bookmark_haschanges.js]
 [test_bookmark_kinds.js]
 [test_bookmark_merge_conflicts.js]