fix(fuse): a write closed just as its file's create lands is queued (B176)

This commit is contained in:
nvrl committed 2026-10-04 22:31:49 +02:00
1 parent ce60e006ac
commit 4f141c78ca
5 files changed
+53 -4

No files matched your search

+1 -1
View File
@@ -1806,7 +1806,7 @@ impl ProtonFs {
}
(Some(mut h), false) => {
self.core.follow_landed_create(&mut h);
match self.core.queue_revision(&h) {
match self.core.queue_revision(&mut h) {
Ok(()) => reply.ok(),
Err(e) => reply.error(e),
}
+27 -1
View File
@@ -3242,7 +3242,7 @@ impl Core {
///
/// The scratch file is *moved* into staging, never copied: it is the only
/// copy of what the user wrote.
fn queue_revision(&self, h: &WriteHandle) -> Result<(), Errno> {
fn queue_revision(&self, h: &mut WriteHandle) -> Result<(), Errno> {
// Closing an untouched writable handle is not a mutation. It remains
// valid after a downgrade and only owes cleanup of its empty scratch.
if !h.dirty {
@@ -3266,10 +3266,16 @@ impl Core {
// recovery a second, stale copy of the same write.
self.cache.clear_scratch_durable(&h.path);
if is_local_uid(&h.uid) && !self.db.has_create_op(&h.uid.to_string()).unwrap_or(true) {
// A create that landed since the handle last looked left no op
// either. It moved the row to its uid in the same transaction, so
// the row says which it was (`docs/BUGS.md` B176).
self.follow_landed_create(h);
if is_local_uid(&h.uid) {
debug!(uid = %h.uid, "local node was unlinked before creation; dropping revision");
let _ = std::fs::remove_file(&h.path);
return Ok(());
}
}
// Materialize as much of the full content as is free to materialize. A
// complete blob is uploadable without the network and lets a later write
// to the same file supersede this one without any reconstruction at all.
@@ -8780,6 +8786,26 @@ mod tests {
assert_eq!(replaced.matches("self.withdraw_queued_ops(uid)").count(), 2);
}
#[test]
fn a_write_closed_as_its_create_lands_is_queued() {
// The release followed a landing create, found none, and the create
// landed before it asked for the create op. Finding none, it took the
// file for one unlinked before it was created and dropped the write
// (B176). The landing removes the op and moves the row at once, so a
// missing op is checked against the row.
let revision = function_source(include_str!("lib.rs"), "fn queue_revision(");
assert_before(
revision,
"self.db.has_create_op(",
"self.follow_landed_create(h)",
);
assert_before(
revision,
"self.follow_landed_create(h)",
"dropping revision",
);
}
#[test]
fn a_replaced_node_put_back_shows_in_the_mount_again() {
// The restore after a failed rename left the node hidden and out of
+21
View File
@@ -12,6 +12,27 @@ Conventions:
---
## B176 — A write closed just as its file's create lands is dropped
**Status:** Fixed (unverified).
**Found:** 2026-10-04, by `one_client_on_a_good_link` (seed 12) on 378667f, run three at a time
in a loop. A write at offset 6 to a file whose create was landing never reached Drive; Drive
ended with the file's first bytes and the later truncate only.
**Where:** `Core::queue_revision` in `crates/pdfs-fuse/src/lib.rs`.
**Cause.** A write handle closed on a file not yet on Drive first checks whether the create has
landed and, if so, follows it to the uid Drive gave it. It then checks that the create is still
queued; a file with neither was unlinked before it was created, and its write is dropped. A
create that landed between the two checks looked like that, and the write was dropped.
**Fix.** The landing removes the create op and gives the row its uid in one transaction. So a
missing create op is checked against the row, and a landed file's write is queued for its uid.
**Test:** `a_write_closed_as_its_create_lands_is_queued` in `crates/pdfs-fuse/src/lib.rs`.
---
## B175 — A file replaced by a rename just as its create lands stays on Drive
**Status:** Fixed (unverified).
+1
View File
@@ -78,6 +78,7 @@ by its local id, in the queue, the stored tree and pins alike.
(B174).
- **A file replaced by a rename just as it reached Drive is removed from Drive too.** It no
longer stayed there, and the next rename of its name no longer moved it instead (B175).
- **A write closed just as its file reached Drive is no longer lost** (B176).
- **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,
B172).
+3 -2
View File
@@ -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 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).
- Fixes from the simulation and account runs since then: B127 and B141 to B175.
- Fixes from the simulation and account runs since then: B127 and B141 to B176.
- **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`.
@@ -119,7 +119,8 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
on d850265 failed seed 12 once in nine runs: a file deleted just as its create
landed stayed on Drive (B173, fixed). The suite on 378667f failed seed 6: a rename
in the same gap was refused with `EACCES` (B174, fixed). The loop on 378667f failed
seed 8: a file replaced by a rename in that gap stayed on Drive (B175, fixed).
seed 8: a file replaced by a rename in that gap stayed on Drive (B175, fixed), and
seed 12: a write closed as its file's create landed was dropped (B176, fixed).
5. **Release**: 2.10.0 is tagged; the 2.9.0 tag still waits for the user's go-ahead.
## After 3.0.0