fix: update local manifest cache immediately after upload, before NAS write

Root cause of the alreadySafe=1 loop: after a backup where all assets are
collision-skipped (Object Name Collision), writeManifest() connects to the NAS
to write the updated manifest. If that connection fails or times out, the cache
is never updated with the 263 new entries, so the next refresh(force:true) reads
the old 1-entry cache and computes alreadySafe=1 → needBackup=263 → loop.

BackupEngine: after the upload loop, immediately merge all manifestEntries
(uploaded + collision-skipped + fileExists-skipped) into baseManifest and call
NASManifestCache.shared.update(). This happens before refreshAfterBackup so
the local truth is correct regardless of whether the NAS write later succeeds.

BackupStatusService.writeManifest: move NASManifestCache.shared.update() to
before transfer.writeData(). The local cache is now updated as soon as the
merged manifest is computed, decoupling local reconcile correctness from NAS
write success. The NAS write remains best-effort and non-blocking.

Co-Authored-By: Kutesir <kutesir@provoc.ug>
Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
Robin Kutesa
2026-05-18 20:52:40 +03:00
parent 0e65a548f2
commit 809929e0ed
2 changed files with 17 additions and 4 deletions

View File

@@ -366,7 +366,18 @@ final class BackupEngine: ObservableObject {
signposter.endInterval("UploadLoop", spUpload, "\(uploaded) uploaded, \(failed) failed")
// Keep directoryCount current without a full rescan.
// Update local manifest cache immediately with every completed entry
// (uploaded + collision-skipped + fileExists-skipped).
// writeManifest() will persist this to the NAS, but even if that network
// write fails the next fast-path reconcile reads the correct counts here.
// Without this, a failed NAS write leaves the cache at 1 entry and the
// next reconcile computes alreadySafe = 1 instead of the real count.
if !manifestEntries.isEmpty {
var localMerged = baseManifest
localMerged.merge(entries: manifestEntries)
await NASManifestCache.shared.update(localMerged)
logger.info("Local cache updated — \(localMerged.entries.count) total entries")
}
if uploaded > 0 {
await NASManifestCache.shared.addToDirectoryCount(uploaded)
}

View File

@@ -432,11 +432,13 @@ final class BackupStatusService: NSObject, ObservableObject {
guard let (encoded, mergedManifest) = result else { return }
try await transfer.writeData(encoded, to: manifestPath)
// Update local cache so fast-path reconcile reads fresh data.
// Update local cache BEFORE the NAS write decouples reconcile correctness
// from NAS write success. If the write fails/times out, the next fast-path
// reconcile still reads the correct merged manifest from local cache.
await NASManifestCache.shared.update(mergedManifest)
lastManifest = mergedManifest
try await transfer.writeData(encoded, to: manifestPath)
log.info("writeManifest: done — \(entries.count) new, total=\(mergedManifest.entries.count)")
} catch {
log.error("writeManifest failed (non-fatal): \(error.localizedDescription)")