From 8f771fdcc40ce06fb394818b35e3270482364488 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Fri, 16 Oct 2026 18:44:51 +0200 Subject: [PATCH] fix(fuse): read the size again for a baseline row without a revision id A mirror upload that Drive did not list yet when it was read back left a baseline row with no revision id. The next walk reused the recorded size whenever the modification time matched, and a row without an id never counts as moved. So a revision another computer saved in the same second looked unchanged and never arrived (B254). The B250 simulation hit this about one run in five on CI. A row without an id now reuses its size only while Drive gives no id either. Otherwise the walk reads the size, and an unchanged file gets Drive's revision id written into its row, so each row costs one extra read. Rows from before schema 44 are read once after upgrading. --- crates/pdfs-fuse/src/sync.rs | 18 +++++++++++++++++- crates/pdfs-fuse/src/sync/planner.rs | 20 +++++++++++++++++++- docs/BUGS.md | 28 ++++++++++++++++++++++++++++ docs/CHANGELOG.md | 3 +++ 4 files changed, 67 insertions(+), 2 deletions(-) diff --git a/crates/pdfs-fuse/src/sync.rs b/crates/pdfs-fuse/src/sync.rs index 9c9ff3d..12cf67d 100644 --- a/crates/pdfs-fuse/src/sync.rs +++ b/crates/pdfs-fuse/src/sync.rs @@ -776,7 +776,23 @@ impl Core { } match plan_file(l, r, baseline.get(rel)) { - FilePlan::Unchanged => {} + // The size was read for real for a row without a revision id + // (B254), and still matches: give the row the id, so later + // walks trust the recorded size again. + FilePlan::Unchanged => { + if let (Some(base), Some(revision)) = + (baseline.get(rel), r.and_then(|r| r.revision.as_ref())) + && base.remote_revision_id.is_none() + { + let row = StoredSyncEntry { + remote_revision_id: Some(revision.clone()), + ..base.clone() + }; + if let Err(e) = self.db.sync_entry_upsert(folder_id, &row) { + debug!(rel, error = ?e, "sync: cannot record the revision id"); + } + } + } FilePlan::AdoptBaseline => { // First sync of a file the same size on both sides: the apply // step hashes the local copy and records the baseline only if diff --git a/crates/pdfs-fuse/src/sync/planner.rs b/crates/pdfs-fuse/src/sync/planner.rs index 8fe5fa6..95b6f04 100644 --- a/crates/pdfs-fuse/src/sync/planner.rs +++ b/crates/pdfs-fuse/src/sync/planner.rs @@ -333,6 +333,13 @@ pub(super) fn remote_subtree_changed( /// The size the baseline recorded for `rel`, if its remote signature's mtime /// still matches `mtime` and its revision is still `revision`. +/// +/// A row without a revision id, while Drive reports one, cannot vouch for the +/// revision, so the size is read for real. Such a row comes from before +/// schema 44, or from an upload Drive did not list yet when it was read back. +/// Another computer's revision in the same second as that upload kept the +/// modification time, and only the size could show it (B254). The pass then +/// records the id on the row, so this costs one read per row. pub(super) fn unchanged_remote_size( baseline: &HashMap, rel: &str, @@ -340,7 +347,9 @@ pub(super) fn unchanged_remote_size( revision: Option<&str>, ) -> Option { let base = baseline.get(rel)?; - if revision_moved(Some(base), revision) { + if revision_moved(Some(base), revision) + || (base.remote_revision_id.is_none() && revision.is_some()) + { return None; } match remote_sig(base) { @@ -756,6 +765,15 @@ mod file_plan_tests { assert_eq!(unchanged_remote_size(&rows, "file", 31, Some("r1")), None); } + /// A row without a revision id reuses its size only while Drive gives no + /// id either (B254). + #[test] + fn a_row_without_a_revision_makes_the_walk_read_the_size_again() { + let rows = HashMap::from([("file".to_string(), baseline(10, 20, 30, 40))]); + assert_eq!(unchanged_remote_size(&rows, "file", 30, Some("r1")), None); + assert_eq!(unchanged_remote_size(&rows, "file", 30, None), Some(40)); + } + #[test] fn a_missing_remote_signature_is_treated_as_changed() { let local = local(10, 20); diff --git a/docs/BUGS.md b/docs/BUGS.md index 92b99e3..aad9d31 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -100,6 +100,7 @@ a live account since. | Entry | Title | |---|---| +| [B254](#b254--an-edit-in-the-same-second-as-a-new-upload-never-arrives) | An edit in the same second as a new upload never arrives | | [B253](#b253--the-app-shows-the-sign-in-page-when-the-keyring-is-locked) | The app shows the sign-in page when the keyring is locked | | [B252](#b252--computers-sharing-a-device-overwrite-each-others-profile) | Computers sharing a device overwrite each other's profile | | [B251](#b251--two-computers-can-make-the-same-conflict-copy-name) | Two computers can make the same conflict copy name | @@ -356,6 +357,33 @@ a live account since. Newest first. +### B254 — An edit in the same second as a new upload never arrives + +**Status:** Fixed, not verified live +**Found:** 2026-10-16, the B250 simulation failing about one run in five on CI. +**Where:** `unchanged_remote_size` in `crates/pdfs-fuse/src/sync/planner.rs`; the +`FilePlan::Unchanged` arm of `do_reconcile` in `crates/pdfs-fuse/src/sync.rs` + +**Repro:** A mirror pass uploads a new file, and Drive does not list it yet when the pass +reads it back. Another computer saves a new revision with the same modification time, to the +second. The mirror keeps the old content, and no pass ever downloads the new one. + +**Cause.** The read-back found no node, so the baseline row got the local modification time +and size but no revision id. The next walk reused the recorded size whenever the modification +time matched and the revision id had not moved. A row without an id never counts as moved, so +the walk never read the new size, and the file looked unchanged. + +**Fix.** A row without a revision id reuses its size only while Drive gives no id either. +Otherwise the walk reads the size for real. If the file is still unchanged, the pass writes +Drive's revision id into the row, so the cost is one extra read per row. Rows from before +schema 44 have no id either, so the first pass after upgrading reads every file's size once. + +**Test:** `a_row_without_a_revision_makes_the_walk_read_the_size_again` in `pdfs-fuse`. The +simulation `a_revision_made_during_a_mirror_download_still_arrives` failed 3 of about 15 runs +before the fix and passed 25 of 25 after it. + +--- + ### B253 — The app shows the sign-in page when the keyring is locked **Status:** Fixed, not verified live diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6fb963c..5ddcb5c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -31,6 +31,9 @@ version on Proton Drive it was synced at. Nothing is rewritten, and the upgrade - **The app showed the sign-in page when the system keyring was locked or not running yet**, as if the sign-in had been lost. It now says that the keyring could not be read, and opens on its own once the keyring answers (B253). +- **A new version saved on another computer in the same second as an upload from here could + be missed for good** when Drive had not listed the uploaded file yet. The first sync pass + after upgrading reads each file's size from Drive once more to close this (B254). - **An edit in a synced folder could replace one another computer had just saved.** The newer version from the other computer was left only in the version history. The edit is now kept as a conflict copy (B249).