mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
fix(fuse): a stop queues the writes whose release the kernel never sent (B190)
This commit is contained in:
1 parent
f0a43fb923
commit
1436c5f37e
6 files changed
+94
-2
No files matched your search
@@ -3281,6 +3281,42 @@ impl Core {
|
|||||||
.filter(|uid| !is_local_uid(uid))
|
.filter(|uid| !is_local_uid(uid))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Queue what the write handles the kernel never released hold.
|
||||||
|
///
|
||||||
|
/// `close(2)` returns before the kernel sends `release`, and the stop
|
||||||
|
/// aborts the FUSE connection with the releases still queued: a file a
|
||||||
|
/// program had just saved was never queued, and Drive kept it as it was
|
||||||
|
/// (`docs/BUGS.md` B190). Runs once every session has ended, so no release
|
||||||
|
/// can race it. A handle still open in a program goes the same way, with
|
||||||
|
/// what it wrote so far; one on an unlinked file only leaves its scratch.
|
||||||
|
pub(crate) fn queue_unreleased_writes(&self) {
|
||||||
|
let handles: Vec<(WriteHandle, bool)> = {
|
||||||
|
let mut st = self.state();
|
||||||
|
st.handles.clear();
|
||||||
|
let inos: Vec<u64> = st.active_writes.keys().copied().collect();
|
||||||
|
inos.into_iter()
|
||||||
|
.filter_map(|ino| {
|
||||||
|
let unlinked = st.entries.get(&ino).is_some_and(|e| e.unlinked);
|
||||||
|
st.active_writes.remove(&ino).map(|h| (h, unlinked))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
for (mut h, unlinked) in handles {
|
||||||
|
if unlinked {
|
||||||
|
self.cache.clear_scratch_durable(&h.path);
|
||||||
|
let _ = std::fs::remove_file(&h.path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
self.follow_landed_create(&mut h);
|
||||||
|
match self.queue_revision(&mut h) {
|
||||||
|
Ok(()) => info!(uid = %h.uid, "queued a write the kernel never released"),
|
||||||
|
Err(error) => {
|
||||||
|
warn!(uid = %h.uid, ?error, "cannot queue a write the kernel never released")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Accept a released write handle's bytes and queue their upload
|
/// Accept a released write handle's bytes and queue their upload
|
||||||
/// (offline.md Phase 3).
|
/// (offline.md Phase 3).
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -750,6 +750,7 @@ pub(crate) fn mount_with(
|
|||||||
warn!(id, error = %e, "unmount on-demand folder failed");
|
warn!(id, error = %e, "unmount on-demand folder failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
core.queue_unreleased_writes();
|
||||||
|
|
||||||
// Last, because the workers may legitimately be finishing an upload, and
|
// Last, because the workers may legitimately be finishing an upload, and
|
||||||
// nothing they can still do is harmful once the mounts are down.
|
// nothing they can still do is harmful once the mounts are down.
|
||||||
|
|||||||
@@ -476,6 +476,37 @@ 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_stop_keeps_what_a_handle_wrote_before_its_release() {
|
||||||
|
// close(2) returns before the kernel sends the release, and the stop
|
||||||
|
// aborted the connection with the release still queued: the bytes of a
|
||||||
|
// file just saved were never queued, and Drive kept it empty (B190).
|
||||||
|
// A handle still open at the stop is the same case, held open.
|
||||||
|
use std::io::Write;
|
||||||
|
let drive = FakeDrive::new();
|
||||||
|
let dir = scratch("stop-before-release");
|
||||||
|
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
|
||||||
|
let mut file = std::fs::File::create(daemon.mountpoint.join("f.txt")).unwrap();
|
||||||
|
file.write_all(b"0123456789").unwrap();
|
||||||
|
|
||||||
|
let daemon = daemon.restart().unwrap();
|
||||||
|
drop(file);
|
||||||
|
assert!(
|
||||||
|
wait_until(Duration::from_secs(30), || {
|
||||||
|
daemon.pending().is_ok_and(|items| items.is_empty())
|
||||||
|
&& drive.tree().get("f.txt")
|
||||||
|
== Some(&Entry::File(Arc::new(b"0123456789".to_vec())))
|
||||||
|
}),
|
||||||
|
"{:?} {:?}",
|
||||||
|
daemon.pending(),
|
||||||
|
drive.tree()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
#[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`"]
|
||||||
fn a_write_that_failed_on_a_short_outage_goes_out_soon_after_the_link_returns() {
|
fn a_write_that_failed_on_a_short_outage_goes_out_soon_after_the_link_returns() {
|
||||||
|
|||||||
@@ -12,6 +12,27 @@ Conventions:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## B190 — A file saved just before the daemon stops can keep its old content on Drive
|
||||||
|
|
||||||
|
**Status:** Fixed (unverified).
|
||||||
|
**Found:** 2026-10-05, in the confirmation run on 2baad67 (`one_client_on_a_flaky_link` seed 9,
|
||||||
|
about one run in three). A client wrote a 32-byte file and closed it, and the daemon was
|
||||||
|
restarted at once. Drive held the file empty, and nothing was queued.
|
||||||
|
|
||||||
|
**Where:** the stop in `crates/pdfs-fuse/src/mount.rs`, and `Core::queue_unreleased_writes` in
|
||||||
|
`crates/pdfs-fuse/src/lib.rs`.
|
||||||
|
|
||||||
|
**Cause.** `close(2)` returns before the kernel sends `release`, and the release is what queues
|
||||||
|
a handle's bytes. The stop aborts the FUSE connection so that the unmount cannot hang, and the
|
||||||
|
abort drops the requests the kernel still has queued. A release among them never reached the
|
||||||
|
daemon. The file's create went out with what it held before the write, and the bytes went with
|
||||||
|
the handle's scratch file. A handle a program still held open at the stop lost what it had
|
||||||
|
written in the same way.
|
||||||
|
|
||||||
|
**Fix.** Once every session has ended, the stop queues each write handle the kernel never
|
||||||
|
released, as its release would have. Test `a_stop_keeps_what_a_handle_wrote_before_its_release`
|
||||||
|
in `crates/pdfs-fuse/src/sim/daemon.rs`.
|
||||||
|
|
||||||
## B189 — A change that failed on a short outage waits out its retry long after the link is back
|
## B189 — A change that failed on a short outage waits out its retry long after the link is back
|
||||||
|
|
||||||
**Status:** Fixed (unverified).
|
**Status:** Fixed (unverified).
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ by its local id, in the queue, the stored tree and pins alike.
|
|||||||
(B187).
|
(B187).
|
||||||
- **Changes that failed during a short outage go out soon after the connection is back**, not
|
- **Changes that failed during a short outage go out soon after the connection is back**, not
|
||||||
after a retry delay of up to five minutes (B189).
|
after a retry delay of up to five minutes (B189).
|
||||||
|
- **A file saved just before the daemon stops reaches Drive** with what was written. It could
|
||||||
|
keep its old content, or stay empty (B190).
|
||||||
- **A file renamed just as its upload finished keeps its new name.** The mount no longer showed
|
- **A file renamed just as its upload finished keeps its new name.** The mount no longer showed
|
||||||
the old name, and a second rename no longer failed with "No such file or directory" (B152,
|
the old name, and a second rename no longer failed with "No such file or directory" (B152,
|
||||||
B172).
|
B172).
|
||||||
|
|||||||
@@ -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: B125, B127, B130 and B141 to B189.
|
- Fixes from the simulation and account runs since then: B125, B127, B130 and B141 to B190.
|
||||||
- **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`; Discard button and
|
tray and CLI; Export button and `pdfs sync export`; `pdfs sync issues`; Discard button and
|
||||||
`pdfs sync discard`; a warning badge on the node in the file browser.
|
`pdfs sync discard`; a warning badge on the node in the file browser.
|
||||||
@@ -115,7 +115,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
|
|||||||
while open that lost the bytes its handle read (B186, fixed; older than 3.0.0).
|
while open that lost the bytes its handle read (B186, fixed; older than 3.0.0).
|
||||||
The run on 2baad67 found that a restart before such a file's trash went out queued
|
The run on 2baad67 found that a restart before such a file's trash went out queued
|
||||||
its kept bytes again, for good (B188, fixed), and changes that sat out their retry
|
its kept bytes again, for good (B188, fixed), and changes that sat out their retry
|
||||||
after an outage only the drain had met (B189, fixed; older than 3.0.0).
|
after an outage only the drain had met (B189, fixed; older than 3.0.0), and a file
|
||||||
|
saved just before a stop whose release the stop dropped (B190, fixed; older too).
|
||||||
- [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed)
|
- [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed)
|
||||||
failed B113 and B121 on every on-demand mount, two mirror-to-mirror move cases, and
|
failed B113 and B121 on every on-demand mount, two mirror-to-mirror move cases, and
|
||||||
the journal check. B121 was a bug (B149, fixed); B113's case still expected creates
|
the journal check. B121 was a bug (B149, fixed); B113's case still expected creates
|
||||||
|
|||||||
Reference in new issue
Block a user