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.
This commit is contained in:
nvrl committed 2026-10-16 18:44:51 +02:00
1 parent c1911ea1de
commit 8f771fdcc4
4 files changed
+67 -2

No files matched your search

+17 -1
View File
@@ -776,7 +776,23 @@ impl Core {
} }
match plan_file(l, r, baseline.get(rel)) { 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 => { FilePlan::AdoptBaseline => {
// First sync of a file the same size on both sides: the apply // First sync of a file the same size on both sides: the apply
// step hashes the local copy and records the baseline only if // step hashes the local copy and records the baseline only if
+19 -1
View File
@@ -333,6 +333,13 @@ pub(super) fn remote_subtree_changed(
/// The size the baseline recorded for `rel`, if its remote signature's mtime /// The size the baseline recorded for `rel`, if its remote signature's mtime
/// still matches `mtime` and its revision is still `revision`. /// 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( pub(super) fn unchanged_remote_size(
baseline: &HashMap<String, StoredSyncEntry>, baseline: &HashMap<String, StoredSyncEntry>,
rel: &str, rel: &str,
@@ -340,7 +347,9 @@ pub(super) fn unchanged_remote_size(
revision: Option<&str>, revision: Option<&str>,
) -> Option<i64> { ) -> Option<i64> {
let base = baseline.get(rel)?; 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; return None;
} }
match remote_sig(base) { match remote_sig(base) {
@@ -756,6 +765,15 @@ mod file_plan_tests {
assert_eq!(unchanged_remote_size(&rows, "file", 31, Some("r1")), None); 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] #[test]
fn a_missing_remote_signature_is_treated_as_changed() { fn a_missing_remote_signature_is_treated_as_changed() {
let local = local(10, 20); let local = local(10, 20);
+28
View File
@@ -100,6 +100,7 @@ a live account since.
| Entry | Title | | 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 | | [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 | | [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 | | [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. 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 ### B253 — The app shows the sign-in page when the keyring is locked
**Status:** Fixed, not verified live **Status:** Fixed, not verified live
+3
View File
@@ -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 - **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 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). 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 - **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 version from the other computer was left only in the version history. The edit is now kept as
a conflict copy (B249). a conflict copy (B249).