sync15/engine/
sync_engine.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use super::CollectionRequest;
6use crate::bso::{IncomingBso, OutgoingBso};
7use crate::client_types::ClientData;
8use crate::{telemetry, CollectionName, Guid, ServerTimestamp};
9use anyhow::Result;
10use std::fmt;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct CollSyncIds {
14    pub global: Guid,
15    pub coll: Guid,
16}
17
18/// Defines how an engine is associated with a particular set of records
19/// on a sync storage server. It's either disconnected, or believes it is
20/// connected with a specific set of GUIDs. If the server and the engine don't
21/// agree on the exact GUIDs, the engine will assume something radical happened
22/// so it can't believe anything it thinks it knows about the state of the
23/// server (ie, it will "reset" then do a full reconcile)
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum EngineSyncAssociation {
26    /// This store is disconnected (although it may be connected in the future).
27    Disconnected,
28    /// Sync is connected, and has the following sync IDs.
29    Connected(CollSyncIds),
30}
31
32/// The concrete `SyncEngine` implementations
33#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
34pub enum SyncEngineId {
35    // Note that we've derived PartialOrd etc, which uses lexicographic ordering
36    // of the variants. We leverage that such that the higher priority engines
37    // are listed first.
38    // This order matches desktop.
39    Passwords,
40    Tabs,
41    Bookmarks,
42    Addresses,
43    CreditCards,
44    History,
45}
46
47impl SyncEngineId {
48    // Iterate over all possible engines. Note that we've made a policy decision
49    // that this should enumerate in "order" as defined by PartialCmp, and tests
50    // enforce this.
51    pub fn iter() -> impl Iterator<Item = SyncEngineId> {
52        [
53            Self::Passwords,
54            Self::Tabs,
55            Self::Bookmarks,
56            Self::Addresses,
57            Self::CreditCards,
58            Self::History,
59        ]
60        .into_iter()
61    }
62
63    // Get the string identifier for this engine.  This must match the strings in SyncEngineSelection.
64    pub fn name(&self) -> &'static str {
65        match self {
66            Self::Passwords => "passwords",
67            Self::History => "history",
68            Self::Bookmarks => "bookmarks",
69            Self::Tabs => "tabs",
70            Self::Addresses => "addresses",
71            Self::CreditCards => "creditcards",
72        }
73    }
74}
75
76impl fmt::Display for SyncEngineId {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        write!(f, "{}", self.name())
79    }
80}
81
82impl TryFrom<&str> for SyncEngineId {
83    type Error = String;
84
85    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
86        match value {
87            "passwords" => Ok(Self::Passwords),
88            "history" => Ok(Self::History),
89            "bookmarks" => Ok(Self::Bookmarks),
90            "tabs" => Ok(Self::Tabs),
91            "addresses" => Ok(Self::Addresses),
92            "creditcards" => Ok(Self::CreditCards),
93            _ => Err(value.into()),
94        }
95    }
96}
97
98/// A "sync engine" is a thing that knows how to sync. It's often implemented
99/// by a "store" (which is the generic term responsible for all storage
100/// associated with a component, including storage required for sync.)
101///
102/// The model described by this trait is that engines first "stage" sets of incoming records,
103/// then apply them returning outgoing records, then handle the success (or otherwise) of each
104/// batch as it's uploaded.
105///
106/// Staging incoming records is (or should be ;) done in batches - eg, 1000 record chunks.
107/// Some engines will "stage" these into a database temp table, while ones expecting less records
108/// might just store them in memory.
109///
110/// For outgoing records, a single vec is supplied by the engine. The sync client will use the
111/// batch facilities of the server to make multiple POST requests and commit them.
112/// Sadly it's not truly atomic (there's a batch size limit) - so the model reflects that in that
113/// the engine gets told each time a batch is committed, which might happen more than once for the
114/// supplied vec. We should upgrade this model so the engine can avoid reading every outgoing
115/// record into memory at once (ie, we should try and better reflect the upload batch model at
116/// this level)
117///
118/// Sync Engines should not assume they live for exactly one sync, so `prepare_for_sync()` should
119/// clean up any state, including staged records, from previous syncs.
120///
121/// Different engines will produce errors of different types.  To accommodate
122/// this, we force them all to return anyhow::Error.
123pub trait SyncEngine {
124    fn collection_name(&self) -> CollectionName;
125
126    /// Prepares the engine for syncing. The tabs engine currently uses this to
127    /// store the current list of clients, which it uses to look up device names
128    /// and types.
129    ///
130    /// Note that this method is only called by `sync_multiple`, and only if a
131    /// command processor is registered. In particular, `prepare_for_sync` will
132    /// not be called if the store is synced using `sync::synchronize` or
133    /// `sync_multiple::sync_multiple`. It _will_ be called if the store is
134    /// synced via the Sync Manager.
135    ///
136    /// TODO(issue #2590): This is pretty cludgey and will be hard to extend for
137    /// any case other than the tabs case. We should find another way to support
138    /// tabs...
139    fn prepare_for_sync(&self, _get_client_data: &dyn Fn() -> ClientData) -> Result<()> {
140        Ok(())
141    }
142
143    /// Tells the engine what the local encryption key is for the data managed
144    /// by the engine. This is only used by collections that store data
145    /// encrypted locally and is unrelated to the encryption used by Sync.
146    /// The intent is that for such collections, this key can be used to
147    /// decrypt local data before it is re-encrypted by Sync and sent to the
148    /// storage servers, and similarly, data from the storage servers will be
149    /// decrypted by Sync, then encrypted by the local encryption key before
150    /// being added to the local database.
151    ///
152    /// The expectation is that the key value is being maintained by the
153    /// embedding application in some secure way suitable for the environment
154    /// in which the app is running - eg, the OS "keychain". The value of the
155    /// key is implementation dependent - it is expected that the engine and
156    /// embedding application already have some external agreement about how
157    /// to generate keys and in what form they are exchanged. Finally, there's
158    /// an assumption that sync engines are short-lived and only live for a
159    /// single sync - this means that sync doesn't hold on to the key for an
160    /// extended period. In practice, all sync engines which aren't a "bridged
161    /// engine" are short lived - we might need to rethink this later if we need
162    /// engines with local encryption keys to be used on desktop.
163    ///
164    /// This will panic if called by an engine that doesn't have explicit
165    /// support for local encryption keys as that implies a degree of confusion
166    /// which shouldn't be possible to ignore.
167    fn set_local_encryption_key(&mut self, _key: &str) -> Result<()> {
168        unimplemented!("This engine does not support local encryption");
169    }
170
171    /// Stage some incoming records. This might be called multiple times in the same sync
172    /// if we fetch the incoming records in batches.
173    ///
174    /// Note there is no timestamp provided here, because the procedure for fetching in batches
175    /// means that the timestamp advancing during a batch means we must abort and start again.
176    /// The final collection timestamp after staging all records is supplied to `apply()`
177    fn stage_incoming(
178        &self,
179        inbound: Vec<IncomingBso>,
180        telem: &mut telemetry::Engine,
181    ) -> Result<()>;
182
183    /// Apply the staged records, returning outgoing records.
184    /// Ideally we would adjust this model to better support batching of outgoing records
185    /// without needing to keep them all in memory (ie, an iterator or similar?)
186    fn apply(
187        &self,
188        timestamp: ServerTimestamp,
189        telem: &mut telemetry::Engine,
190    ) -> Result<Vec<OutgoingBso>>;
191
192    /// Indicates that the given record IDs were uploaded successfully to the server.
193    /// This may be called multiple times per sync, once for each batch. Batching is determined
194    /// dynamically based on payload sizes and counts via the server's advertised limits.
195    fn set_uploaded(&self, new_timestamp: ServerTimestamp, ids: Vec<Guid>) -> Result<()>;
196
197    /// Called once the sync is finished. Not currently called if uploads fail (which
198    /// seems sad, but the other batching confusion there needs sorting out first).
199    /// Many engines will have nothing to do here, as most "post upload" work should be
200    /// done in `set_uploaded()`
201    fn sync_finished(&self) -> Result<()> {
202        Ok(())
203    }
204
205    /// The engine is responsible for building a single collection request. Engines
206    /// typically will store a lastModified timestamp and use that to build a
207    /// request saying "give me full records since that date" - however, other
208    /// engines might do something fancier. It can return None if the server timestamp
209    /// has not advanced since the last sync.
210    /// This could even later be extended to handle "backfills", and we might end up
211    /// wanting one engine to use multiple collections (eg, as a "foreign key" via guid), etc.
212    fn get_collection_request(
213        &self,
214        server_timestamp: ServerTimestamp,
215    ) -> Result<Option<CollectionRequest>>;
216
217    /// Get persisted sync IDs. If they don't match the global state we'll be
218    /// `reset()` with the new IDs.
219    fn get_sync_assoc(&self) -> Result<EngineSyncAssociation>;
220
221    /// Reset the engine (and associated store) without wiping local data,
222    /// ready for a "first sync".
223    /// `assoc` defines how this store is to be associated with sync.
224    fn reset(&self, assoc: &EngineSyncAssociation) -> Result<()>;
225
226    /// Wipes the engine's data
227    /// This is typically triggered by a client command, which at the time of writing, only
228    /// supported wiping bookmarks.
229    ///
230    /// This panics if triggered on a sync engine that does not explicitly implement wipe, because
231    /// that implies a confustion that shouldn't occur.
232    fn wipe(&self) -> Result<()> {
233        unimplemented!("The engine does not implement wipe, no wipe should be requested")
234    }
235}
236
237#[cfg(test)]
238mod test {
239    use super::*;
240    use std::iter::zip;
241
242    #[test]
243    fn test_engine_priority() {
244        fn sorted(mut engines: Vec<SyncEngineId>) -> Vec<SyncEngineId> {
245            engines.sort();
246            engines
247        }
248        assert_eq!(
249            vec![SyncEngineId::Passwords, SyncEngineId::Tabs],
250            sorted(vec![SyncEngineId::Passwords, SyncEngineId::Tabs])
251        );
252        assert_eq!(
253            vec![SyncEngineId::Passwords, SyncEngineId::Tabs],
254            sorted(vec![SyncEngineId::Tabs, SyncEngineId::Passwords])
255        );
256    }
257
258    #[test]
259    fn test_engine_enum_order() {
260        let unsorted = SyncEngineId::iter().collect::<Vec<SyncEngineId>>();
261        let mut sorted = SyncEngineId::iter().collect::<Vec<SyncEngineId>>();
262        sorted.sort();
263
264        // iterating should supply identical elements in each.
265        assert!(zip(unsorted, sorted).fold(true, |acc, (a, b)| acc && (a == b)))
266    }
267}