fix(fuse,core): what a create sent, and where, outlives a restart and a rename (B182, B183, B184)

The known-bug confirmation run failed one_client_on_a_flaky_link seeds 6
and 11 on three bugs around a create whose answer was lost:

- B182: after a restart, a write over a file whose create landed but was
  never read back forked a conflict copy. A revision of a file that has
  no revision of Drive's now adopts the landed node first.
- B183: the SHA-1s a create sent were kept in memory, so after a restart
  its trash or retry no longer knew the node Drive made. They are now on
  the op's row (pending_op.sent_sha1, schema 42).
- B184: a rename rewrote the op's target, and the trash and the retry
  looked for the node under the last name only. Each attempt's parent
  and name are recorded (pending_op.sent_to); the trash looks under all
  of them, and a retry adopts the node an earlier attempt made and moves
  it to the current name.
This commit is contained in:
nvrl committed 2026-10-05 02:01:50 +02:00
1 parent a8ed6060af
commit 307305282e
10 files changed
+548 -49

No files matched your search

+66 -2
View File
@@ -12,6 +12,71 @@ Conventions:
---
## B184 — A file renamed after its create lost its answer stays on Drive under its old name
**Status:** Fixed (unverified).
**Found:** 2026-10-05, replaying `one_client_on_a_flaky_link` seed 11 with the B183 fix. `a.txt`'s
create reached Drive and lost its answer. The file was then rewritten, renamed to `b.txt` and
then `d`, and deleted. Drive kept `a.txt` with the bytes the create had sent.
**Where:** `Core::withdrawn_twin` and `Core::create_local_node` in
`crates/pdfs-fuse/src/drain.rs`.
**Cause.** A rename while a create waits rewrites the op's parent and name, but the node a lost
answer left stays where the create was sent. The trash a delete made of the create, and the
create's retry, looked for that node under the op's last name only. The trash found nothing
and left the file on Drive. The retry made a second file under the new name, so Drive had both.
**Fix.** Each attempt records the parent and name it is sent under on the op's row, in a new
`pending_op.sent_to` column (schema 42). The trash looks for its node under each of them. A
retry first adopts a node an earlier attempt made under another name, and the rename to the
current name follows it. Sim tests `a_file_renamed_after_its_create_lost_its_answer_lands_once`
and `a_file_renamed_and_deleted_after_its_create_lost_its_answer_leaves_nothing`, and
`what_a_create_sent_outlives_a_newer_blob_its_trash_and_a_restart` in
`crates/pdfs-core/src/db/tests.rs`.
## B183 — A file deleted after a restart stays on Drive when its create lost its answer
**Status:** Fixed (unverified).
**Found:** 2026-10-05, by the known-bug confirmation run (`PDFS_SIM_KNOWN=fail`) on da013c2:
`one_client_on_a_flaky_link` seed 11 failed every replay. `d` was made, written again while
its create was on the wire, and deleted after several restarts. Drive kept a `d` with the bytes
the create had sent, and the next file renamed to `d` landed as `d (sync-conflict …)`.
**Where:** `Core::withdrawn_twin` and `Core::adoptable_twin` in `crates/pdfs-fuse/src/drain.rs`.
**Cause.** The trash a delete makes of a create that was sent looks for a node Drive made with
the bytes the create sent (B151). Those were kept in memory only (B156). After a restart the
trash knew only the blob the op held last, which a later write had replaced. It found no node of
its own and left the file on Drive. A create retried after a restart had the same gap and forked
a conflict copy.
**Fix.** Each attempt records the SHA-1 of the blob it sends on the op's row, in a new
`pending_op.sent_sha1` column (schema 42). The row keeps it through a newer blob, through the
turn into a trash and across restarts. The in-memory map is gone. Tests
`what_a_create_sent_outlives_a_newer_blob_its_trash_and_a_restart` in
`crates/pdfs-core/src/db/tests.rs`, and sim test
`a_file_deleted_after_a_restart_leaves_drive_with_what_its_create_sent`.
## B182 — A write after a restart over a create never read back lands as a conflict copy
**Status:** Fixed (unverified).
**Found:** 2026-10-05, by the known-bug confirmation run on da013c2:
`one_client_on_a_flaky_link` seed 6 failed two of three replays. `y/x/c.bin`'s create landed and
the link went down before it was read back. The daemon restarted, and a write to the file landed
as `c (sync-conflict …).bin`, with Drive keeping the old bytes.
**Where:** `Core::finish_adoption` in `crates/pdfs-fuse/src/drain.rs`.
**Cause.** A create that landed but was not read back is adopted before the next op for the
node is sent (B143). The node is remembered for that in memory, and a restart forgot it. The
tree then still had the file as it was made here, with no revision of Drive's, and a write was
based on that. The drain found Drive's revision in its way and took it for someone else's.
**Fix.** A queued write over a file the tree holds without a revision id is adopted first, as
B143 does, after a restart too. Sim test
`a_write_after_a_restart_over_a_create_never_read_back_is_not_a_conflict`.
## B181 — The app shows a queued edit kept as a conflict copy in English
**Status:** Fixed.
@@ -673,8 +738,7 @@ the op held at the retry was hashed. The twin held the bytes an earlier attempt
**Fix.** Each attempt records the SHA-1 of the blob it uploads, by op id, in memory. A twin holding
any of them is ours. One holding older bytes than the op's is adopted, and the op's blob goes up
as a revision of it. After a restart such a twin still forks a conflict copy, which loses
nothing. Sim test `a_file_written_again_after_its_create_lost_its_answer_lands_once`; the fake
as a revision of it. Kept on the op's row since B183, so a restart no longer forgets them. Sim test `a_file_written_again_after_its_create_lost_its_answer_lands_once`; the fake
Drive's held and lost create answers now combine.
## B155 — A write whose upload lost its answer lands as a conflict copy of itself
+12 -3
View File
@@ -11,9 +11,10 @@ scratch (user data in `staging/` and `recovery/` is never touched by this).
## [Unreleased]
Schema: **41**. The queue table is rebuilt so an id is never given out twice; queued changes
are kept. Queued changes get a column for the reason Drive refused them, and name their file
and folder by local id as well as by Drive's id. Stored files and folders name their parent
Schema: **42**. The queue table is rebuilt so an id is never given out twice; queued changes
are kept. Queued changes get a column for the reason Drive refused them, and two for the
content a new file was sent with and where, and name their file and folder by local id as well
as by Drive's id. Stored files and folders name their parent
folder by local id too. A file or folder made on this machine that Drive does not have yet goes
by its local id, in the queue, the stored tree and pins alike.
@@ -96,6 +97,14 @@ by its local id, in the queue, the stored tree and pins alike.
- **The daemon no longer hangs when a file being read changes on Drive** (B130).
- **The Activity page says in your language why an edit was kept as a conflicting copy**,
instead of the daemon's English (B181).
- **A file written after a restart no longer lands as a conflict copy of itself** when the
connection dropped just as it first reached Drive (B182).
- **A file deleted after a restart is removed from Drive too** when the connection lost Drive's
answer to its upload and the file was written again meanwhile. It no longer stayed there and
turned the next file under its name into a conflict copy (B183).
- **A file renamed while the connection lost Drive's answer to its upload lands once.** It no
longer stayed on Drive under its old name too, or, when deleted, under its old name alone
(B184).
- **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).
+7 -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: B125, B127, B130 and B141 to B181.
- Fixes from the simulation and account runs since then: B125, B127, B130 and B141 to B184.
- **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`; Discard button and
`pdfs sync discard`; a warning badge on the node in the file browser.
@@ -102,7 +102,12 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
which a68dae9 (not yet pushed then) also takes off the runtime. CI now keeps a hung
run's stacks.
- [ ] Known-bug list in `sim/run.rs` is empty: none open; B125, B127, B129, B130 and B132
fixed but not yet confirmed by the runs.
fixed but not yet confirmed by the runs. The confirmation run (`PDFS_SIM_KNOWN=fail`)
on da013c2 failed `one_client_on_a_flaky_link` seeds 6 and 11 on two new bugs, both
after restarts: a write over a create never read back (B182) and a trash that forgot
what its create sent (B183). Replays with those fixed found a third: a create renamed
after it lost its answer (B184). All three fixed; no known-bug matcher fired. It was
stopped before `three_clients`.
- [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed)
failed B113 and B121 on every on-demand mount, two mirror-to-mirror move cases, and
the journal check. B121 was a bug (B149, fixed); B113's case still expected creates