fix(fuse): keep what an open file reads when its delete is queued (B186)

This commit is contained in:
nvrl committed 2026-10-05 02:51:55 +02:00
1 parent d26dc8ada4
commit c271a1897c
6 files changed
+109 -5

No files matched your search

+11
View File
@@ -994,6 +994,17 @@ impl Core {
Err(e) => return Err(e.into()),
}
self.retire_trash_op(op, &uid)?;
// A file trashed while open kept the bytes its handle reads, and a
// final close while the trash was queued left them to this
// (`docs/BUGS.md` B186).
if !self.is_open_anywhere(&uid) {
if let Some(blob) = self.pending_blob(&uid) {
self.release_pending(&uid, &blob);
self.cache.discard_staged(&blob);
}
self.cache.evict(&uid);
self.evict_reader(&uid);
}
if let Err(e) = self.db.clear_own_sealed_rev(&uid.to_string()) {
debug!(%uid, error = %e, "clearing the sealed-revision record failed");
}
+21 -3
View File
@@ -3902,6 +3902,11 @@ impl Core {
/// staged bytes that never landed — which is precisely what deleting an
/// un-uploaded file means, and the alternative (upload it, then trash it) is
/// worse in every way.
///
/// A file still open keeps the bytes its handle reads, the queued ones and
/// the cached ones, as when the trash is sent at once: a partial write
/// fills its gaps from them, offline too (`docs/BUGS.md` B186). The final
/// close or the trash landing, whichever comes last, lets go of them.
fn queue_trash(&self, uid: &NodeUid, name: &str) -> Result<(), Errno> {
self.require_uid_writable(uid)?;
// The bytes below are about to be discarded, and a drain worker may be
@@ -3917,7 +3922,18 @@ impl Core {
error!(%uid, error = %e, "queueing trash failed");
Errno::EIO
})?;
self.release_dropped_ops(uid, blobs);
let open_now = self.is_open_anywhere(uid);
if open_now {
let read_from = self.pending_blob(uid);
self.release_dropped_below(
blobs
.into_iter()
.filter(|blob| read_from.as_deref() != Some(Path::new(blob)))
.collect(),
);
} else {
self.release_dropped_ops(uid, blobs);
}
self.hidden.lock().insert(uid.clone());
// Withdrawn from the tree, which every mount serves from, so no other
// mount goes on serving a file the user just trashed (`docs/BUGS.md`
@@ -3925,8 +3941,10 @@ impl Core {
self.for_each_state(|st| {
st.unlink_mem(uid);
});
self.cache.evict(uid);
self.evict_reader(uid);
if !open_now {
self.cache.evict(uid);
self.evict_reader(uid);
}
self.wake_drain();
debug!(%uid, name, "trashed offline; queued");
Ok(())
+53
View File
@@ -423,6 +423,59 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_file_deleted_offline_while_open_still_reads_and_takes_writes() {
// Queuing the trash dropped the cached bytes the open handle reads,
// and a partial write offline failed with EIO (B186).
use std::io::{Read, Seek, SeekFrom, Write};
let drive = FakeDrive::new();
let dir = scratch("unlink-open-offline");
let daemon = Daemon::start(&dir, drive.client(1, Faults::lan())).unwrap();
let path = daemon.mountpoint.join("f.txt");
std::fs::write(&path, b"0123456789").unwrap();
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.client.set_online(false);
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
std::fs::remove_file(&path).unwrap();
file.seek(SeekFrom::End(0)).unwrap();
file.write_all(b"ab").unwrap();
let mut read = Vec::new();
file.seek(SeekFrom::Start(0)).unwrap();
file.read_to_end(&mut read).unwrap();
assert_eq!(read, b"0123456789ab");
drop(file);
daemon.client.set_online(true);
assert!(
wait_until(Duration::from_secs(30), || {
daemon.pending().is_ok_and(|items| items.is_empty()) && drive.tree().is_empty()
}),
"{:?} {:?}",
daemon.pending(),
drive.tree()
);
// The bytes kept for the handle went once the trash landed with it closed.
let cached: Vec<_> = std::fs::read_dir(dir.join("cache"))
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_ok_and(|t| t.is_file()))
.map(|e| e.file_name())
.collect();
assert!(cached.is_empty(), "{cached:?}");
assert!(matches!(daemon.stop(), Ok(MountOutcome::Shutdown)));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
#[ignore = "mounts FUSE: run with `cargo test -p pdfs-fuse sim:: -- --ignored`"]
fn a_write_opened_as_an_upload_lands_takes_the_landed_revision_as_its_base() {