From bf73a395e58e95e158e3b8dd90815e171069e8a2 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 3 Oct 2026 09:07:56 +0200 Subject: [PATCH] docs: record B132-B134 from the simulation runs - B132: a folder whose mkdir is still queued is gone after a restart. The root's listing, served offline, was invalidated when the link came back, and the restart listed it on Drive without the queued folder. - B133: a file deleted while its upload is in flight can still come back as a recovered copy. B105's check goes by op id, and SQLite gives the next op queued the id the delete freed. - B134: a file deleted online while the drain checks its write is kept as a conflict copy. The unlink trashes the file on Drive before it drops the queued write, so B105's check still finds the op. --- docs/BUGS.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/docs/BUGS.md b/docs/BUGS.md index 8531046..3fe93d3 100644 --- a/docs/BUGS.md +++ b/docs/BUGS.md @@ -12,6 +12,79 @@ Conventions: --- +## B134 — A file deleted online while the drain checks its write is kept as a conflict copy + +**Status:** Open. +**Found:** 2026-10-03, by the simulation runs (`sim::run`, profile three-clients, seed 3). +`c.bin` was written, renamed to `f.txt` and deleted, all online. The drain logged "queued write +conflicts; keeping a conflict copy" for `f.txt`, with "the file was trashed remotely". The copy +was named after the file, not `recovered-…`, so the tree still placed the file when the drain +looked. It did not come up again in five replays with debug logs. + +**Where:** `trash_child` in `crates/pdfs-fuse/src/filesystem.rs`, and `Core::drain_revision` in +`crates/pdfs-fuse/src/drain.rs`. + +**Cause.** An online `unlink` trashes the node on Drive first, and only then forgets it in the +tree and drops its queued write (`discard_queued_ops`, or `withdraw_queued_ops` for an open +file). A drain worker that has claimed the write and reads the node in between finds it trashed. +B105's check asks whether the op is still queued, and it still is, so the drain keeps the write +as a conflict copy of a file the user deleted. When the copy runs before the unlink discards the +staged blob, it lands next to where the file was. + +**Test:** the simulation runs count such a conflict when the copy is named after the file, and +count a copy that lands. With `PDFS_SIM_KNOWN=fail`, seed 3 of three-clients fails on it when it +comes up, which depends on timing. + +## B133 — A file deleted while its upload is in flight can still come back as a recovered copy + +**Status:** Open. +**Found:** 2026-10-03, by the simulation runs (`sim::run`, profile one-client-deletes, seed 3). +`y/e.md` was written, and the drain picked the write up. The step held the drain's read of the +node, deleted the file, and released the read. The next step wrote `a.txt`. The drain then logged +"queued write conflicts; keeping a conflict copy" for `y/e.md` as `recovered-L4`, with "the file +was trashed remotely". It is B105 again, though B105's fix was in place. Here the copy failed +because the blob was already gone. + +**Where:** `Core::drain_revision` in `crates/pdfs-fuse/src/drain.rs`, `Db::op_exists` in +`crates/pdfs-core/src/db/ops.rs`, and the `pending_op` table in +`crates/pdfs-core/src/db/migrations.rs`. + +**Cause.** B105's fix asks whether the op the worker holds is still queued, by id. `pending_op.id` +is an `INTEGER PRIMARY KEY` without `AUTOINCREMENT`, so SQLite gives a new row the largest id +in the table plus one, or 1 when the table is empty. The delete dropped the newest row, so the +next op queued took the same id. `op_exists` found that other op and said the write was still +wanted. A SQLite workload queues a write to the database right after deleting its journal, so +this is the usual case there, not a rare one. + +**Test:** the simulation runs make the file again, or not, while the drain's read is held. When +they did, they count the drain's conflict copy of it. With `PDFS_SIM_KNOWN=fail` one-client-deletes +fails on it in every seed. + +## B132 — A folder whose mkdir is still queued is gone after a restart + +**Status:** Open. +**Found:** 2026-10-03, by the simulation runs (`sim::run`, profile one-client-flaky, seed 4). +The first listing of the Drive root failed with the link down, and the client's folder `c0` was +made offline. The link came back, and the client restarted with 8 ops still queued, `c0`'s +mkdir among them. Right after the restart, a write to `c0/a.txt` failed with `ENOENT`. The kernel's +lookup of `c0` in the root had answered `ENOENT`. A moment later the mkdir landed. + +**Where:** `Core::ensure_children` in `crates/pdfs-fuse/src/lib.rs`, with +`Core::relist_offline_listings` and `State::invalidate_listing`. + +**Cause.** A listing served offline is invalidated when the link comes back +(`relist_offline_listings`), which also clears its `listed` row. The running daemon does not +notice: the kernel still holds the dentries. After a restart, the first lookup in the folder +lists it on Drive. Drive does not hold a node whose create is still queued, or does not list a +fresh one yet (the lag of B118). `ensure_children` takes Drive's listing as the whole folder and +records it as complete, without the queued node's placeholder. Every path through that node fails +with `ENOENT` until something lists the folder again. The same may happen without a restart +whenever a folder holding a queued create is listed on Drive. + +**Test:** the simulation runs count an `ENOENT` after a restart that restored queued ops, in a +seed that lost the link, and end the seed there. With `PDFS_SIM_KNOWN=fail`, seed 4 of +one-client-flaky fails on it when the mkdir is still queued, which depends on timing. + ## B131 — A write to a file whose create could not be read back lands as a conflict copy **Status:** Open. @@ -713,7 +786,7 @@ the blob still on disk the copy would land. **Fix.** Before keeping a conflict copy, the drain checks that its op is still queued. When the op is gone, the write was withdrawn by the delete, or superseded, while the worker held it, and -it is dropped. +it is dropped. The check goes by op id, which the next op queued can take over (B133). ---