sync15/client/
coll_state.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::request::InfoConfiguration;
6use super::{CollectionKeys, GlobalState};
7use crate::engine::{CollSyncIds, EngineSyncAssociation, SyncEngine};
8use crate::error;
9use crate::error::{info, trace, warn};
10use crate::KeyBundle;
11use crate::ServerTimestamp;
12
13/// Holds state for a collection necessary to perform a sync of it. Lives for the lifetime
14/// of a single sync.
15#[derive(Debug, Clone)]
16pub struct CollState {
17    // Info about the server configuration/capabilities
18    pub config: InfoConfiguration,
19    // from meta/global, used for XIUS when we POST outgoing record based on this state.
20    pub last_modified: ServerTimestamp,
21    pub key: KeyBundle,
22}
23
24/// This mini state-machine helps build a CollState
25#[derive(Debug)]
26pub enum LocalCollState {
27    /// The state is unknown, with the EngineSyncAssociation the collection
28    /// reports.
29    Unknown { assoc: EngineSyncAssociation },
30
31    /// The engine has been declined. This is a "terminal" state.
32    Declined,
33
34    /// There's no such collection in meta/global. We could possibly update
35    /// meta/global, but currently all known collections are there by default,
36    /// so this is, basically, an error condition.
37    NoSuchCollection,
38
39    /// Either the global or collection sync ID has changed - we will reset the engine.
40    SyncIdChanged { ids: CollSyncIds },
41
42    /// The collection is ready to sync.
43    Ready { coll_state: CollState },
44}
45
46pub struct LocalCollStateMachine<'state> {
47    global_state: &'state GlobalState,
48    root_key: &'state KeyBundle,
49}
50
51impl<'state> LocalCollStateMachine<'state> {
52    fn advance(
53        &self,
54        from: LocalCollState,
55        engine: &dyn SyncEngine,
56    ) -> error::Result<LocalCollState> {
57        let name = &engine.collection_name().to_string();
58        let meta_global = &self.global_state.global;
59        match from {
60            LocalCollState::Unknown { assoc } => {
61                if meta_global.declined.contains(name) {
62                    return Ok(LocalCollState::Declined);
63                }
64                match meta_global.engines.get(name) {
65                    Some(engine_meta) => match assoc {
66                        EngineSyncAssociation::Disconnected => Ok(LocalCollState::SyncIdChanged {
67                            ids: CollSyncIds {
68                                global: meta_global.sync_id.clone(),
69                                coll: engine_meta.sync_id.clone(),
70                            },
71                        }),
72                        EngineSyncAssociation::Connected(ref ids)
73                            if ids.global == meta_global.sync_id
74                                && ids.coll == engine_meta.sync_id =>
75                        {
76                            // We are done - build the CollState
77                            let coll_keys = CollectionKeys::from_encrypted_payload(
78                                self.global_state.keys.clone(),
79                                self.global_state.keys_timestamp,
80                                self.root_key,
81                            )?;
82                            let key = coll_keys.key_for_collection(name).clone();
83                            let name = engine.collection_name();
84                            let config = self.global_state.config.clone();
85                            let last_modified = self
86                                .global_state
87                                .collections
88                                .get(name.as_ref())
89                                .cloned()
90                                .unwrap_or_default();
91                            let coll_state = CollState {
92                                config,
93                                last_modified,
94                                key,
95                            };
96                            Ok(LocalCollState::Ready { coll_state })
97                        }
98                        _ => Ok(LocalCollState::SyncIdChanged {
99                            ids: CollSyncIds {
100                                global: meta_global.sync_id.clone(),
101                                coll: engine_meta.sync_id.clone(),
102                            },
103                        }),
104                    },
105                    None => Ok(LocalCollState::NoSuchCollection),
106                }
107            }
108
109            LocalCollState::Declined => unreachable!("can't advance from declined"),
110
111            LocalCollState::NoSuchCollection => unreachable!("the collection is unknown"),
112
113            LocalCollState::SyncIdChanged { ids } => {
114                let assoc = EngineSyncAssociation::Connected(ids);
115                info!("Resetting {} engine", engine.collection_name());
116                engine.reset(&assoc)?;
117                Ok(LocalCollState::Unknown { assoc })
118            }
119
120            LocalCollState::Ready { .. } => unreachable!("can't advance from ready"),
121        }
122    }
123
124    // A little whimsy - a portmanteau of far and fast
125    fn run_and_run_as_farst_as_you_can(
126        &mut self,
127        engine: &dyn SyncEngine,
128    ) -> error::Result<Option<CollState>> {
129        let mut s = LocalCollState::Unknown {
130            assoc: engine.get_sync_assoc()?,
131        };
132        // This is a simple state machine and should never take more than
133        // 10 goes around.
134        let mut count = 0;
135        loop {
136            trace!("LocalCollState in {:?}", s);
137            match s {
138                LocalCollState::Ready { coll_state } => return Ok(Some(coll_state)),
139                LocalCollState::Declined | LocalCollState::NoSuchCollection => return Ok(None),
140                _ => {
141                    count += 1;
142                    if count > 10 {
143                        warn!("LocalCollStateMachine appears to be looping");
144                        return Ok(None);
145                    }
146                    // should we have better loop detection? Our limit of 10
147                    // goes is probably OK for now, but not really ideal.
148                    s = self.advance(s, engine)?;
149                }
150            };
151        }
152    }
153
154    pub fn get_state(
155        engine: &dyn SyncEngine,
156        global_state: &'state GlobalState,
157        root_key: &'state KeyBundle,
158    ) -> error::Result<Option<CollState>> {
159        let mut gingerbread_man = Self {
160            global_state,
161            root_key,
162        };
163        gingerbread_man.run_and_run_as_farst_as_you_can(engine)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::super::request::{InfoCollections, InfoConfiguration};
170    use super::super::CollectionKeys;
171    use super::*;
172    use crate::bso::{IncomingBso, OutgoingBso};
173    use crate::engine::CollectionRequest;
174    use crate::record_types::{MetaGlobalEngine, MetaGlobalRecord};
175    use crate::{telemetry, CollectionName};
176    use anyhow::Result;
177    use nss::ensure_initialized;
178    use std::cell::{Cell, RefCell};
179    use std::collections::HashMap;
180    use sync_guid::Guid;
181
182    fn get_global_state(root_key: &KeyBundle) -> GlobalState {
183        let keys = CollectionKeys::new_random()
184            .unwrap()
185            .to_encrypted_payload(root_key)
186            .unwrap();
187        GlobalState {
188            config: InfoConfiguration::default(),
189            collections: InfoCollections::new(HashMap::new()),
190            global: MetaGlobalRecord {
191                sync_id: "syncIDAAAAAA".into(),
192                storage_version: 5usize,
193                engines: vec![(
194                    "bookmarks",
195                    MetaGlobalEngine {
196                        version: 1usize,
197                        sync_id: "syncIDBBBBBB".into(),
198                    },
199                )]
200                .into_iter()
201                .map(|(key, value)| (key.to_owned(), value))
202                .collect(),
203                declined: vec![],
204            },
205            global_timestamp: ServerTimestamp::default(),
206            keys,
207            keys_timestamp: ServerTimestamp::default(),
208        }
209    }
210
211    struct TestSyncEngine {
212        collection_name: &'static str,
213        assoc: Cell<EngineSyncAssociation>,
214        num_resets: RefCell<usize>,
215    }
216
217    impl TestSyncEngine {
218        fn new(collection_name: &'static str, assoc: EngineSyncAssociation) -> Self {
219            Self {
220                collection_name,
221                assoc: Cell::new(assoc),
222                num_resets: RefCell::new(0),
223            }
224        }
225        fn get_num_resets(&self) -> usize {
226            *self.num_resets.borrow()
227        }
228    }
229
230    impl SyncEngine for TestSyncEngine {
231        fn collection_name(&self) -> CollectionName {
232            self.collection_name.into()
233        }
234
235        fn stage_incoming(
236            &self,
237            _inbound: Vec<IncomingBso>,
238            _telem: &mut telemetry::Engine,
239        ) -> Result<()> {
240            unreachable!("these tests shouldn't call these");
241        }
242
243        fn apply(
244            &self,
245            _timestamp: ServerTimestamp,
246            _telem: &mut telemetry::Engine,
247        ) -> Result<Vec<OutgoingBso>> {
248            unreachable!("these tests shouldn't call these");
249        }
250
251        fn set_uploaded(&self, _new_timestamp: ServerTimestamp, _ids: Vec<Guid>) -> Result<()> {
252            unreachable!("these tests shouldn't call these");
253        }
254
255        fn sync_finished(&self) -> Result<()> {
256            unreachable!("these tests shouldn't call these");
257        }
258
259        fn get_collection_request(
260            &self,
261            _server_timestamp: ServerTimestamp,
262        ) -> Result<Option<CollectionRequest>> {
263            unreachable!("these tests shouldn't call these");
264        }
265
266        fn get_sync_assoc(&self) -> Result<EngineSyncAssociation> {
267            Ok(self.assoc.replace(EngineSyncAssociation::Disconnected))
268        }
269
270        fn reset(&self, new_assoc: &EngineSyncAssociation) -> Result<()> {
271            self.assoc.replace(new_assoc.clone());
272            *self.num_resets.borrow_mut() += 1;
273            Ok(())
274        }
275
276        fn wipe(&self) -> Result<()> {
277            unreachable!("these tests shouldn't call these");
278        }
279    }
280
281    #[test]
282    fn test_unknown() {
283        ensure_initialized();
284        let root_key = KeyBundle::new_random().expect("should work");
285        let gs = get_global_state(&root_key);
286        let engine = TestSyncEngine::new("unknown", EngineSyncAssociation::Disconnected);
287        let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
288        assert!(cs.is_none(), "unknown collection name can't sync");
289        assert_eq!(engine.get_num_resets(), 0);
290    }
291
292    #[test]
293    fn test_known_no_state() {
294        ensure_initialized();
295        let root_key = KeyBundle::new_random().expect("should work");
296        let gs = get_global_state(&root_key);
297        let engine = TestSyncEngine::new("bookmarks", EngineSyncAssociation::Disconnected);
298        let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
299        assert!(cs.is_some(), "collection can sync");
300        assert_eq!(
301            engine.assoc.replace(EngineSyncAssociation::Disconnected),
302            EngineSyncAssociation::Connected(CollSyncIds {
303                global: "syncIDAAAAAA".into(),
304                coll: "syncIDBBBBBB".into(),
305            })
306        );
307        assert_eq!(engine.get_num_resets(), 1);
308    }
309
310    #[test]
311    fn test_known_wrong_state() {
312        ensure_initialized();
313        let root_key = KeyBundle::new_random().expect("should work");
314        let gs = get_global_state(&root_key);
315        let engine = TestSyncEngine::new(
316            "bookmarks",
317            EngineSyncAssociation::Connected(CollSyncIds {
318                global: "syncIDXXXXXX".into(),
319                coll: "syncIDYYYYYY".into(),
320            }),
321        );
322        let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
323        assert!(cs.is_some(), "collection can sync");
324        assert_eq!(
325            engine.assoc.replace(EngineSyncAssociation::Disconnected),
326            EngineSyncAssociation::Connected(CollSyncIds {
327                global: "syncIDAAAAAA".into(),
328                coll: "syncIDBBBBBB".into(),
329            })
330        );
331        assert_eq!(engine.get_num_resets(), 1);
332    }
333
334    #[test]
335    fn test_known_good_state() {
336        ensure_initialized();
337        let root_key = KeyBundle::new_random().expect("should work");
338        let gs = get_global_state(&root_key);
339        let engine = TestSyncEngine::new(
340            "bookmarks",
341            EngineSyncAssociation::Connected(CollSyncIds {
342                global: "syncIDAAAAAA".into(),
343                coll: "syncIDBBBBBB".into(),
344            }),
345        );
346        let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
347        assert!(cs.is_some(), "collection can sync");
348        assert_eq!(engine.get_num_resets(), 0);
349    }
350
351    #[test]
352    fn test_declined() {
353        ensure_initialized();
354        let root_key = KeyBundle::new_random().expect("should work");
355        let mut gs = get_global_state(&root_key);
356        gs.global.declined.push("bookmarks".to_string());
357        let engine = TestSyncEngine::new(
358            "bookmarks",
359            EngineSyncAssociation::Connected(CollSyncIds {
360                global: "syncIDAAAAAA".into(),
361                coll: "syncIDBBBBBB".into(),
362            }),
363        );
364        let cs = LocalCollStateMachine::get_state(&engine, &gs, &root_key).expect("should work");
365        assert!(cs.is_none(), "declined collection can sync");
366        assert_eq!(engine.get_num_resets(), 0);
367    }
368}