fix(fuse): do not take a new file Drive lists before its commit as empty (B163)

This commit is contained in:
nvrl committed 2026-10-04 18:51:39 +02:00
1 parent 18587124ac
commit b37d5e7928
6 files changed
+166 -4

No files matched your search

+22 -1
View File
@@ -3130,10 +3130,19 @@ impl Core {
/// [`Core::fetch_node`] for a node Drive has just made, asked again for a
/// moment while Drive does not list it yet.
///
/// A file Drive lists without an active revision is not listed yet either:
/// the create commits one before it answers, so this answer is from before
/// the commit, with no size. Taken, it emptied the new file in the tree,
/// and a read of it right after found nothing (`docs/BUGS.md` B163).
pub(crate) fn read_back(&self, uid: &NodeUid) -> Result<Node, Errno> {
let mut delays = MINTED_READ_BACK_DELAYS.iter();
loop {
match self.fetch_node(uid) {
let fetched = match self.fetch_node(uid) {
Ok(node) if is_unrevised(&node) => Err(Errno::ENOENT),
fetched => fetched,
};
match fetched {
Err(e)
if e == Errno::ENOENT
&& let Some(delay) = delays.next() =>
@@ -6259,6 +6268,18 @@ fn check_replaceable(src_dir: bool, dst_dir: bool, dst_empty: bool) -> Result<()
}
}
/// Whether `node` is a file Drive lists without an active revision, as it does
/// one whose create has not been committed.
fn is_unrevised(node: &Node) -> bool {
matches!(
node.kind,
NodeKind::File {
active_revision_id: None,
..
}
)
}
fn node_size(node: &Node) -> u64 {
match &node.kind {
NodeKind::Folder => 0,
+60
View File
@@ -494,6 +494,66 @@ 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_read_back_before_drive_committed_it_keeps_its_bytes() {
// Drive listed the new file a moment before its create was committed,
// with no revision and no size. The drain took that answer for the
// file, and a read right after the create landed found it empty
// (B163).
let drive = FakeDrive::new();
let dir = scratch("read-back-unrevised");
// The feed reports the file as it is long after the read.
let faults = Faults {
event_delay: Duration::from_secs(60),
..Faults::lan()
};
let daemon = Daemon::start(&dir, drive.client(1, faults)).unwrap();
let mnt = daemon.mountpoint.clone();
// A folder made here is listed from the tree, not from Drive.
std::fs::create_dir(mnt.join("x")).unwrap();
assert!(
wait_until(Duration::from_secs(30), || {
daemon.pending().is_ok_and(|items| items.is_empty()) && drive.lookup("x").is_some()
}),
"{:?} {:?}",
daemon.pending(),
drive.tree()
);
take_logged();
let create = daemon.client.hold_reply_to_next_create();
std::fs::write(mnt.join("x/c.bin"), b"abc").unwrap();
assert!(wait_until(Duration::from_secs(30), || create.reached()));
let uid = drive.lookup("x/c.bin").unwrap();
// The read-back after the create, the one adopting it, and the size
// fetch a listing makes.
daemon.client.answer_reads_unrevised(&uid, 3);
drop(create);
let mut logged = Vec::new();
assert!(
wait_until(Duration::from_secs(30), || {
logged.extend(take_logged());
logged
.iter()
.any(|line| line.starts_with("pending create landed") && line.contains("c.bin"))
}),
"{logged:?}"
);
// A listing refreshes the size the kernel holds, as the account run's
// did before it read the file.
let sizes: Vec<_> = std::fs::read_dir(mnt.join("x"))
.unwrap()
.map(|e| e.unwrap().metadata().unwrap().len())
.collect();
assert_eq!(sizes, [3]);
assert_eq!(std::fs::read(mnt.join("x/c.bin")).unwrap(), b"abc");
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_folder_made_again_while_the_old_one_is_trashed_is_a_new_folder() {
+44 -1
View File
@@ -34,7 +34,9 @@
//! A test can also hold the next read of one node on its way to Drive
//! ([`FakeClient::hold_next_read`]), to make a change while the daemon waits
//! for the answer, or on its way back ([`FakeClient::hold_answer_to_next_read`]),
//! so the daemon gets an answer a change made meanwhile has overtaken.
//! so the daemon gets an answer a change made meanwhile has overtaken. It can
//! also have a new file's next reads answered as Drive lists it before its
//! create is committed ([`FakeClient::answer_reads_unrevised`]; B163).
use std::collections::{BTreeMap, HashMap};
use std::io::{Read, Write};
@@ -339,6 +341,7 @@ impl FakeDrive {
lose_revision_reply: AtomicBool::new(false),
lose_revision_read_back: AtomicBool::new(false),
lose_read: Mutex::new(None),
unrevised_reads: Mutex::new(None),
}))
}
@@ -602,6 +605,9 @@ struct ClientInner {
lose_revision_read_back: AtomicBool,
/// The file whose next read goes unanswered, once its revision is made.
lose_read: Mutex<Option<NodeUid>>,
/// Set by [`FakeClient::answer_reads_unrevised`]: the file and how many
/// more of its reads are answered without an active revision.
unrevised_reads: Mutex<Option<(NodeUid, usize)>>,
}
struct HoldState {
@@ -723,6 +729,13 @@ impl FakeClient {
self.0.lose_revision_read_back.store(true, Ordering::SeqCst);
}
/// Answer the next `reads` reads of file `uid` alone that find it as Drive
/// lists a file whose create it has not committed yet: no active revision,
/// no size.
pub(crate) fn answer_reads_unrevised(&self, uid: &NodeUid, reads: usize) {
*self.0.unrevised_reads.lock() = Some((uid.clone(), reads));
}
/// Wait here if `uids` is the read a [`Held`] in `slot` is for.
async fn wait_if_held(&self, slot: &Mutex<Option<Arc<HoldState>>>, uids: &[NodeUid]) {
let state = {
@@ -982,6 +995,36 @@ impl DriveApi for FakeClient {
})
.await?;
self.wait_if_held(&self.0.hold_answer, uids).await;
let mut nodes = nodes;
{
let mut unrevised = self.0.unrevised_reads.lock();
if let Some((uid, left)) = unrevised.as_mut()
&& uids == std::slice::from_ref(uid)
&& !nodes.is_empty()
{
*left -= 1;
if *left == 0 {
*unrevised = None;
}
for node in &mut nodes {
if let NodeKind::File {
total_size_on_storage,
active_revision_state,
active_revision_id,
claimed_size,
content_sha1,
..
} = &mut node.kind
{
*total_size_on_storage = 0;
*active_revision_state = None;
*active_revision_id = None;
*claimed_size = None;
*content_sha1 = None;
}
}
}
}
self.cache(&nodes);
Ok(nodes)
}