Sync Overview
This document provides a high-level overview of how syncing works. Note: each component has its own quirks and will handle sync slightly differently than the general process described here.
General flow and architecture
- Crates involved:
- The
sync15
andsupport/sync15-traits
handle the general syncing logic and define theSyncEngine
trait - Individual component crates (
logins
,places
,autofill
, etc). These implementSyncEngine
. sync_manager
manages the overall syncing process.
- The
- High level sync flow:
- Sync is initiated by the application that embeds application-services.
- The application calls
SyncManager.sync()
to start the sync process. SyncManager
createsSyncEngine
instances to sync the individual components. EachSyncEngine
corresponds to acollection
on the sync server.
Sync manager
SyncManager
is responsible for performing the high-level parts of the sync process:
- The consumer code calls it's
sync()
function to start the sync, passing in aSyncParams
object in, which describes what should be synced. SyncManager
performs all network operations on behalf of the individual engines. It's also responsible for tracking the general authentication state (primarily by inspecting the responses from these network requests) and fetching tokens from the token server.SyncManager
checks if we are currently in a backoff period and should wait before contacting the server again.- Before syncing any engines, the sync manager checks the state of the meta/global collection and compares it with the enabled engines specified in the SyncParams. This handles the cases when the user has requested an engine be enabled or disabled on this device, or when it was requested on a different device. (Note that engines enabled and disabled states are state on the account itself and not a per-device setting). Part of this process is comparing the collection's GUID on the server with the GUID known locally - if they are different, it implies some other device has "reset" the collection, so the engine drops all metadata and attempts to reconcile with every record on the server (ie, acts as though this is the very first sync this engine has ever done).
SyncManager
instantiates aSyncEngine
for each enabled component. We currently use 2 different methods for this:- The older method is for the
SyncManager
to hold a weakref to aStore
use that to create theSyncEngine
(tabs and places). TheSyncEngine
uses theStore
for database access, see theTabsStore
for an example. - The newer method is for the components to provide a function to create the
SyncEngine
, hiding the details of how that engine gets created (autofill/logins). These components also define aStore
instance for theSyncEngine
to use, but it's all transparent to theSyncManager
. (Seeautofill::get_registered_sync_engine()
andautofill::db::store::Store
)
- The older method is for the
- For components that use local encryption,
SyncManager
passes the local encryption key to theirSyncEngine
- Finally, calls
sync_multiple()
function from thesync15
crate, sending it theSyncEngine
instances.sync_multiple()
then calls thesync()
function for each individualSyncEngine
Sync engines
SyncEngine
is defined in thesupport/sync15-traits
crate and defines the interface for syncing a component.- A new
SyncEngine
instance is created for each sync SyncEngine.apply_incoming()
does the main work. It is responsible for processing incoming records from the server in order to update the local records and calculating which local records should be synced back.
The apply_incoming
pattern
SyncEngine
instances are free to implement apply_incoming()
any way they want, but the most components follow a general pattern.
Database Tables
- The local table stores records for the local application
- The mirror table stores the last known record from the server
- The staging temporary table stores the incoming records that we're currently processing
- The local/mirror/staging tables contains a
guid
as its primary key. A record will share the sameguid
for the local/mirror/staging table. - The metadata table stores the GUID for the collection as a whole and the the last-known server timestamp of the collection.
apply_incoming
stages
- stage incoming: write out each incoming server record to the staging table
- fetch states: take the rows from all 3 tables and combine them into a single struct containing
Option
s for the local/mirror/staging records. - iterate states: loop through each state, decide how to do change the local records, then execute that plan.
- reconcile/plan: For each state we create an action plan for it. The action plan is a low-level description of what to change (add this record, delete this one, modify this field, etc). Here are some common situations:
- A record only appears in the staging table. It's a new record from the server and should be added to the local DB
- A record only appears in the local table. It's a new record on the local instance and should be synced back to the serve
- Identical records appear in the local/mirror tables and a changed record is in the staging table. The record was updated remotely and the changes should be propagated to the local DB.
- A record appears in the mirror table and changed records appear in both the local and staging tables. The record was updated both locally and remotely and we should perform a 3-way merge.
- apply plan: After we create the action plan, then we execute it.
- reconcile/plan: For each state we create an action plan for it. The action plan is a low-level description of what to change (add this record, delete this one, modify this field, etc). Here are some common situations:
- fetch outgoing:
- Calculate which records need to be sent back to the server
- Update the mirror table
- Return those records back to the
sync15
code so that it can upload them to the server. - The sync15 code returns the timestamp reported by the server in the POST response and hands it back to the engine. The engine persists this timestamp in the metadata table - the next sync will then use this timestamp to only fetch records that have since been changed by other devices
syncChangeCounter
The local table has an integer column syncChangeCounter which is incremented every time the embedding app makes a change to a local record (eg, updating a field). Thus, any local record with a non-zero change counter will need to be updated on the server (with either the local record being used, or after it being merged if the record also changed remotely). At the start of the sync, when we are determining what action to take, we take a copy of the change counter, typically in a temp staging table. After we have uploaded the record to the server, we decrement the counter by whatever it was when the sync started. This means that if a record is changed in between staging the record and uploading it, the change counter will not drop to zero, and so it will correctly be seen as locally modified on the next sync