Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
- Verify signature of imported data when `.get()` is called with `sync_if_empty: true` ([#7518](https://github.com/mozilla/application-services/pull/7518))
- Do not quote `_since` values with the v2 API ([#7523](https://github.com/mozilla/application-services/pull/7523))

### Sync Manager

- `SyncManager::sync()` now fails immediately with a new `SyncManagerError::Busy` when a sync is already in progress, instead of blocking until it finishes.

# v154.0 (_2026-07-20_)

## ✨ What's Changed ✨
Expand Down
2 changes: 2 additions & 0 deletions components/sync_manager/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum SyncManagerError {
UnknownEngine(String),
#[error("Manager was compiled without support for {0:?}")]
UnsupportedFeature(String),
#[error("Another sync is already in progress")]
Busy,
// Used for things like 'failed to decode the provided sync key because it's
// completely the wrong format', etc.
#[error("Sync error: {0}")]
Expand Down
39 changes: 38 additions & 1 deletion components/sync_manager/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,14 @@ impl SyncManager {
}

/// Perform a sync. See [SyncParams] and [SyncResult] for details on how this works
///
/// Fails with [SyncManagerError::Busy] if a sync is already in progress.
pub fn sync(&self, params: SyncParams) -> Result<SyncResult> {
breadcrumb!("SyncManager::sync started");
let mut state = self.mem_cached_state.lock();
let Some(mut state) = self.mem_cached_state.try_lock() else {
breadcrumb!("SyncManager::sync is already in progress, bailing out early");
return Err(SyncManagerError::Busy);
};
let engines = self.calc_engines_to_sync(&params.engines)?;
let next_sync_after = state.as_ref().and_then(|mcs| mcs.get_next_sync_after());
let result = if !backoff_in_effect(next_sync_after, &params) {
Expand Down Expand Up @@ -312,11 +317,43 @@ impl CommandProcessor for SyncClient {
#[cfg(test)]
mod test {
use super::*;
use crate::types::{DeviceSettings, SyncAuthInfo};

#[test]
fn test_engine_id_sanity() {
for engine_id in SyncEngineId::iter() {
assert_eq!(engine_id, SyncEngineId::try_from(engine_id.name()).unwrap());
}
}

fn dummy_sync_params() -> SyncParams {
SyncParams {
reason: SyncReason::Scheduled,
engines: SyncEngineSelection::All,
enabled_changes: HashMap::new(),
local_encryption_keys: HashMap::new(),
auth_info: SyncAuthInfo {
kid: "kid".to_string(),
fxa_access_token: "token".to_string(),
sync_key: "sync-key".to_string(),
tokenserver_url: "https://example.com/token/1.0/sync/1.5".to_string(),
},
persisted_state: None,
device_settings: DeviceSettings {
fxa_device_id: "device-id".to_string(),
name: "Test Device".to_string(),
kind: sync15::DeviceType::Mobile,
},
}
}

#[test]
fn test_sync_is_busy_while_a_sync_is_in_progress() {
let manager = SyncManager::new();
let _in_progress = manager.mem_cached_state.lock();
assert!(matches!(
manager.sync(dummy_sync_params()),
Err(SyncManagerError::Busy)
));
}
}
1 change: 1 addition & 0 deletions components/sync_manager/src/syncmanager.udl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ namespace syncmanager { };
enum SyncManagerError {
"UnknownEngine",
"UnsupportedFeature",
"Busy",
"Sync15Error",
"UrlParseError",
"InterruptedError",
Expand Down