mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
fix(fuse): land a conflict copy in the root when its folder is gone (B150)
This commit is contained in:
1 parent
50ae25bb88
commit
23dc5c3723
5 files changed
+78
-3
No files matched your search
@@ -1585,8 +1585,14 @@ impl Core {
|
|||||||
.map(|(_, n)| n.clone())
|
.map(|(_, n)| n.clone())
|
||||||
.unwrap_or_else(|| self.node_name(uid));
|
.unwrap_or_else(|| self.node_name(uid));
|
||||||
// A node the tree has forgotten still has bytes worth keeping, so the
|
// A node the tree has forgotten still has bytes worth keeping, so the
|
||||||
// copy falls back to the root rather than being abandoned.
|
// copy falls back to the root rather than being abandoned. So does one
|
||||||
let Some(parent) = place.map(|(p, _)| p).or_else(|| self.root_uid()) else {
|
// whose folder went with it: an upload into a folder Drive no longer
|
||||||
|
// has fails on every retry (B150).
|
||||||
|
let folder = match place.map(|(p, _)| p) {
|
||||||
|
Some(p) if self.fetch_node_remote(&p)?.is_some_and(|n| !n.trashed) => Some(p),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let Some(parent) = folder.or_else(|| self.root_uid()) else {
|
||||||
return self.abandon_to_staging(op, blob, uid, &name, reason);
|
return self.abandon_to_staging(op, blob, uid, &name, reason);
|
||||||
};
|
};
|
||||||
warn!(%uid, name, reason, "queued write conflicts; keeping a conflict copy");
|
warn!(%uid, name, reason, "queued write conflicts; keeping a conflict copy");
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ pub(crate) fn wait_until(limit: Duration, mut done: impl FnMut() -> bool) -> boo
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::sim::fake_drive::{Entry, FakeDrive, Faults};
|
use crate::sim::fake_drive::{Entry, FakeDrive, Faults};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
|
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
|
||||||
@@ -534,6 +535,52 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
|
||||||
|
fn a_queued_write_whose_folder_was_trashed_lands_in_the_root() {
|
||||||
|
// The conflict copy of a write to a file trashed elsewhere went into
|
||||||
|
// the file's folder. With that folder gone too, the upload failed on
|
||||||
|
// every retry, every two minutes (B150).
|
||||||
|
let drive = FakeDrive::new();
|
||||||
|
let dir = scratch("conflict-copy-folder-gone");
|
||||||
|
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
|
||||||
|
|
||||||
|
let mount = &daemon.mountpoint;
|
||||||
|
std::fs::create_dir(mount.join("d")).unwrap();
|
||||||
|
std::fs::write(mount.join("d/f.txt"), b"first").unwrap();
|
||||||
|
assert!(wait_until(Duration::from_secs(30), || {
|
||||||
|
drive.tree().contains_key("d/f.txt") && daemon.pending().is_ok_and(|ops| ops.is_empty())
|
||||||
|
}));
|
||||||
|
|
||||||
|
let paused = |paused| Request::SetSyncPaused {
|
||||||
|
paused,
|
||||||
|
until: None,
|
||||||
|
};
|
||||||
|
daemon.request(&paused(true)).unwrap();
|
||||||
|
std::fs::write(mount.join("d/f.txt"), b"second").unwrap();
|
||||||
|
drive.device().trash("d/f.txt");
|
||||||
|
drive.device().trash("d");
|
||||||
|
daemon.request(&paused(false)).unwrap();
|
||||||
|
|
||||||
|
let copy = |tree: &BTreeMap<String, Entry>| {
|
||||||
|
tree.iter()
|
||||||
|
.find(|(path, _)| path.starts_with("f (sync-conflict "))
|
||||||
|
.map(|(_, entry)| entry.clone())
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
wait_until(Duration::from_secs(30), || {
|
||||||
|
copy(&drive.tree()) == Some(Entry::File(Arc::new(b"second".to_vec())))
|
||||||
|
&& daemon.pending().is_ok_and(|ops| ops.is_empty())
|
||||||
|
}),
|
||||||
|
"{:?} {:?}",
|
||||||
|
drive.tree().keys().collect::<Vec<_>>(),
|
||||||
|
daemon.pending()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
/// Make 100 folders of 10 empty files each through a mount on `faults`, and
|
/// Make 100 folders of 10 empty files each through a mount on `faults`, and
|
||||||
/// time the syscalls and then the drain until Drive holds all of it.
|
/// time the syscalls and then the drain until Drive holds all of it.
|
||||||
fn time_a_thousand_files(faults: Faults) -> (Duration, Duration) {
|
fn time_a_thousand_files(faults: Faults) -> (Duration, Duration) {
|
||||||
|
|||||||
@@ -12,6 +12,26 @@ Conventions:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## B150 — A conflict copy whose folder is gone retries forever
|
||||||
|
|
||||||
|
**Status:** Fixed (unverified).
|
||||||
|
**Found:** 2026-10-04, by the journal check of the account run on a6f36d5: 33 warnings, one
|
||||||
|
every two minutes for the whole run, all "queued write conflicts; keeping a conflict copy" for a
|
||||||
|
`write.bin` from an earlier run's B79 case, each followed by `DoesNotExist` from Drive.
|
||||||
|
|
||||||
|
**Where:** `Core::keep_as_conflict_copy` in `crates/pdfs-fuse/src/drain.rs`.
|
||||||
|
|
||||||
|
**Cause.** A queued write to a file that was trashed or deleted on Drive is kept as a conflict
|
||||||
|
copy beside the file. When the file's folder was deleted too, the upload into it failed, the op
|
||||||
|
backed off and tried again, with no end. The copy only fell back to the root when the tree had
|
||||||
|
forgotten the file, not when Drive had lost its folder.
|
||||||
|
|
||||||
|
**Fix.** The copy checks that the folder is still on Drive and not trashed, and lands in the
|
||||||
|
location's root otherwise. Sim test `a_queued_write_whose_folder_was_trashed_lands_in_the_root`.
|
||||||
|
|
||||||
|
**Left open.** How the B79 case's bytes stayed in `staging/` after the file was deleted, so that
|
||||||
|
the next start queued them again ("reconciled orphaned staged writes queued=1"), is not known yet.
|
||||||
|
|
||||||
## B149 — `pdfs rename` or `pdfs move` right after a rename through the mount is undone
|
## B149 — `pdfs rename` or `pdfs move` right after a rename through the mount is undone
|
||||||
|
|
||||||
**Status:** Fixed (unverified).
|
**Status:** Fixed (unverified).
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ folder by local id too.
|
|||||||
for this release.
|
for this release.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
- **A change to a file deleted elsewhere, together with its folder, no longer retries forever.**
|
||||||
|
The conflict copy that keeps it now lands in the location's root (B150).
|
||||||
- **`pdfs rename` and `pdfs move` right after a change through the mount are no longer undone.**
|
- **`pdfs rename` and `pdfs move` right after a change through the mount are no longer undone.**
|
||||||
They are queued behind it, and they work on a file that has not reached Drive yet (B149).
|
They are queued behind it, and they work on a file that has not reached Drive yet (B149).
|
||||||
- **A folder kept offline before it reached Drive stays kept offline.** Its pin was lost when the
|
- **A folder kept offline before it reached Drive stays kept offline.** Its pin was lost when the
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
|
|||||||
- **Phase 2**: local ids, inode = local id, one tree for every mount (2.10.0).
|
- **Phase 2**: local ids, inode = local id, one tree for every mount (2.10.0).
|
||||||
- **Phase 3, first part**: mkdir, create, rename, unlink and rmdir on the mount are recorded
|
- **Phase 3, first part**: mkdir, create, rename, unlink and rmdir on the mount are recorded
|
||||||
locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c).
|
locally and sent from the queue; `"local_first": false` brings back the old path (5e0f30c).
|
||||||
- Fixes from the simulation and account runs since then: B141 to B149.
|
- Fixes from the simulation and account runs since then: B141 to B150.
|
||||||
- **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app,
|
- **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app,
|
||||||
tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`.
|
tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`.
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user