diff --git a/crates/pdfs-fuse/src/filesystem.rs b/crates/pdfs-fuse/src/filesystem.rs index fa22e7d..07013d7 100644 --- a/crates/pdfs-fuse/src/filesystem.rs +++ b/crates/pdfs-fuse/src/filesystem.rs @@ -1464,6 +1464,12 @@ impl ProtonFs { return; } if let Err(e) = self.core.ensure_children(parent) { + // The parent was looked up a moment ago, so ENOENT here means it + // left the tree in between. Log it: the caller only sees "no such + // file" on a create. + if e == Errno::ENOENT { + warn!(parent, name, "create: the parent folder is no longer known"); + } reply.error(e); return; } @@ -1472,6 +1478,10 @@ impl ProtonFs { match st.entries.get(&parent) { Some(e) => e.uid.clone(), None => { + warn!( + parent, + name, "create: the parent folder left the tree during its listing" + ); reply.error(Errno::ENOENT); return; } diff --git a/docs/TESTING.md b/docs/TESTING.md index 05db5ae..1233041 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -97,7 +97,8 @@ Besides the syscall contract, the suite covers: - **Throughput.** 64 MiB written and read sequentially, 1 MiB in 4 KiB writes, and metadata operations on 100 files. The rates are printed. The case fails below `PDFS_ACCEPTANCE_MIN_MIBPS` (default 10) or `PDFS_ACCEPTANCE_MIN_OPS` - (default 20 operations per second). + (default 20 operations per second, or 3 on a FUSE mount, where each create + and unlink waits for Drive before it returns). ### The mount is diffed against an ordinary filesystem diff --git a/scripts/fuse-acceptance.py b/scripts/fuse-acceptance.py index 6910dc9..4e76b51 100755 --- a/scripts/fuse-acceptance.py +++ b/scripts/fuse-acceptance.py @@ -1357,9 +1357,15 @@ def test_throughput(ctx: Context) -> None: reads of just-written data come from the cache, so neither should depend on the network. PDFS_ACCEPTANCE_MIN_MIBPS and PDFS_ACCEPTANCE_MIN_OPS move the floors; 0 turns one off. + + Metadata is the exception. On a Drive mount each create and each unlink + waits for Drive before it returns (about 350 ms for a create), so 20 + operations a second is out of reach there by design. The default floor for + a FUSE target is 3, which still catches a second round trip per operation. """ min_rate = float(os.environ.get("PDFS_ACCEPTANCE_MIN_MIBPS", "10")) - min_ops = float(os.environ.get("PDFS_ACCEPTANCE_MIN_OPS", "20")) + default_ops = "3" if is_fuse(ctx.root) else "20" + min_ops = float(os.environ.get("PDFS_ACCEPTANCE_MIN_OPS", default_ops)) root = ctx.root / "throughput" root.mkdir() chunk = pattern(MIB, "throughput") @@ -3308,6 +3314,20 @@ def is_mountpoint(path: Path) -> bool: return os.path.ismount(path) +def is_fuse(path: Path) -> bool: + """True when `path` lives on a FUSE mount, read from /proc/self/mountinfo.""" + path = path.resolve() + best, fstype = -1, "" + with open("/proc/self/mountinfo", encoding="utf-8") as mounts: + for line in mounts: + fields = line.split() + # Octal escapes (\040 for a space) are how mountinfo writes odd names. + point = Path(fields[4].encode().decode("unicode_escape")) + if (path == point or point in path.parents) and len(point.parts) > best: + best, fstype = len(point.parts), fields[fields.index("-") + 1] + return fstype.startswith("fuse") + + def run_managed_matrix( paths: list[Path], reference: Run, args, janitor: Janitor | None = None ) -> None: