From e4c60c58ca59a7ee25a6d69923c10e21c5b8cb52 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Fri, 28 Nov 2025 08:20:28 -0800 Subject: [PATCH] Guard against self-watermarks and apply backfill Skip self-watermarks when peer is the local device; log a warning. Apply current_state in dependency order by computing registry order. Fall back to unordered if the computation fails. On automatic backfill failure, reset device state to Uninitialized. This triggers a retry and clears backfill_attempted. --- core/src/infra/sync/watermarks.rs | 11 +++++++++++ core/src/service/sync/backfill.rs | 20 +++++++++++++++++++- core/src/service/sync/mod.rs | 3 +++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/src/infra/sync/watermarks.rs b/core/src/infra/sync/watermarks.rs index 12a3803db..f4a6cfdb7 100644 --- a/core/src/infra/sync/watermarks.rs +++ b/core/src/infra/sync/watermarks.rs @@ -115,6 +115,17 @@ impl ResourceWatermarkStore { resource_type: &str, watermark: DateTime, ) -> Result<(), WatermarkError> { + // Prevent self-watermarks - device should never track watermarks for its own data + if peer_device_uuid == self.device_uuid { + tracing::warn!( + device_uuid = %self.device_uuid, + peer_device_uuid = %peer_device_uuid, + resource_type = %resource_type, + "Attempted to create self-watermark (device tracking itself) - skipping" + ); + return Ok(()); + } + // Check if newer before updating let existing = self.get(conn, peer_device_uuid, resource_type).await?; diff --git a/core/src/service/sync/backfill.rs b/core/src/service/sync/backfill.rs index 85d2b028a..0c61acb53 100644 --- a/core/src/service/sync/backfill.rs +++ b/core/src/service/sync/backfill.rs @@ -781,7 +781,25 @@ impl BackfillManager { // Apply current_state snapshot (contains pre-sync data not in peer_log) if let Some(state) = current_state { if let Some(state_map) = state.as_object() { - for (model_type, records_value) in state_map { + // Get dependency-ordered list of models to prevent FK violations + // CRITICAL: Must apply parent models before children (e.g., user_metadata before user_metadata_tag) + let sync_order = match crate::infra::sync::registry::compute_registry_sync_order().await { + Ok(order) => order, + Err(e) => { + warn!("Failed to compute sync order, using unordered: {}", e); + // Fallback to unordered if dependency graph fails + state_map.keys().map(|k| k.clone()).collect::>() + } + }; + + // Apply snapshot records in dependency order + for model_type in sync_order { + // Skip if model not in snapshot + let records_value = match state_map.get(&model_type) { + Some(val) => val, + None => continue, + }; + if let Some(records_array) = records_value.as_array() { info!( model_type = %model_type, diff --git a/core/src/service/sync/mod.rs b/core/src/service/sync/mod.rs index aa7d80f7a..693ed50bd 100644 --- a/core/src/service/sync/mod.rs +++ b/core/src/service/sync/mod.rs @@ -330,6 +330,9 @@ impl SyncService { } Err(e) => { warn!("Automatic backfill failed: {}", e); + // Reset state to Uninitialized so retry logic runs + let mut state = peer_sync.state.write().await; + *state = DeviceSyncState::Uninitialized; // Reset flag to retry on next loop backfill_attempted = false; }