fix(core): a file deleted after its failed create was written again leaves Drive (B191)

This commit is contained in:
nvrl committed 2026-10-05 04:50:02 +02:00
1 parent 1436c5f37e
commit 57a825887c
4 files changed
+71 -3

No files matched your search

+6 -1
View File
@@ -107,6 +107,10 @@ pub fn op_supersedes(kind: &str) -> bool {
/// name and bytes (`docs/BUGS.md` B151). Such a trash keeps the create's /// name and bytes (`docs/BUGS.md` B151). Such a trash keeps the create's
/// parent and blob, which the drain looks for the node by, and which it /// parent and blob, which the drain looks for the node by, and which it
/// discards once the trash is done ([`Db::complete_trash_op`]). /// discards once the trash is done ([`Db::complete_trash_op`]).
///
/// Sent is where it was sent to ([`Db::note_create_target`]), not only its
/// attempts: a write after the failure clears those for a prompt retry, and
/// the delete then dropped the create whole (`docs/BUGS.md` B191).
fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<String>> { fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<String>> {
const DOOMED: &str = " const DOOMED: &str = "
WITH RECURSIVE WITH RECURSIVE
@@ -134,7 +138,8 @@ fn drop_doomed_ops(tx: &rusqlite::Transaction<'_>, uid: &str) -> Result<Vec<Stri
OR (kind = 'revision' OR (kind = 'revision'
AND (uid IN (SELECT uid FROM below) OR lid IN (SELECT lid FROM below))) AND (uid IN (SELECT uid FROM below) OR lid IN (SELECT lid FROM below)))
)"; )";
const SENT: &str = "kind IN ('create', 'mkdir') AND (claimed_at <> 0 OR attempts > 0)"; const SENT: &str = "kind IN ('create', 'mkdir')
AND (claimed_at <> 0 OR attempts > 0 OR sent_to IS NOT NULL)";
let lid = node_lid(tx, uid)?; let lid = node_lid(tx, uid)?;
let blobs: Vec<String> = { let blobs: Vec<String> = {
let mut stmt = tx.prepare(&format!( let mut stmt = tx.prepare(&format!(
+42
View File
@@ -3951,6 +3951,48 @@ fn what_a_create_sent_outlives_a_newer_blob_its_trash_and_a_restart() {
let _ = std::fs::remove_file(path); let _ = std::fs::remove_file(path);
} }
/// A write after a create failed clears its attempts, so the next try goes
/// out at once. The create was still sent: deleting the file then left no
/// trash, and the file a lost answer made stayed on Drive under the name it
/// was sent with (`docs/BUGS.md` B191).
#[test]
fn a_create_rewritten_after_it_failed_still_leaves_a_trash_when_deleted() {
let db = Db::open_in_memory().unwrap();
let parent = uid("parent").to_string();
let (id, _) = db
.enqueue_op(&PendingOp {
id: 0,
kind: OP_CREATE.to_string(),
uid: "local~sent".to_string(),
parent_uid: Some(parent.clone()),
name: Some("a.txt".to_string()),
blob_path: Some("/staging/first".to_string()),
meta_json: Some("{}".to_string()),
created_at: 1,
attempts: 0,
last_error: None,
next_attempt_at: 0,
})
.unwrap();
db.note_create_target(id, &parent, "a.txt").unwrap();
db.note_create_sent(id, "aaaa").unwrap();
db.record_op_failure(id, "no answer", 0).unwrap();
db.attach_blob_to_create("local~sent", "/staging/second", "{}")
.unwrap()
.expect("the create is still queued");
assert!(db.delete_ops_for_uid("local~sent").unwrap().is_empty());
let ops = db.pending_ops().unwrap();
assert_eq!(ops.len(), 1);
assert_eq!((ops[0].id, ops[0].kind.as_str()), (id, OP_TRASH));
assert_eq!(ops[0].blob_path.as_deref(), Some("/staging/second"));
assert_eq!(
db.create_targets(id).unwrap(),
vec![(parent.clone(), "a.txt".to_string())]
);
}
/// A create deleted after it was sent becomes a trash in its place. The /// A create deleted after it was sent becomes a trash in its place. The
/// create's backoff, and the failure of an attempt still on the wire, are the /// create's backoff, and the failure of an attempt still on the wire, are the
/// create's: the trash is due at once (`docs/BUGS.md` B171). /// create's: the trash is due at once (`docs/BUGS.md` B171).
+19
View File
@@ -12,6 +12,25 @@ Conventions:
--- ---
## B191 — A file deleted after its create failed and was written again can stay on Drive
**Status:** Fixed (unverified).
**Found:** 2026-10-05, in the confirmation run on 1436c5f (`one_client_on_a_flaky_link` seed 11,
one replay in six). A create of `a.txt` got no answer, though Drive had made the file. The file
was written again, renamed twice and deleted. Drive kept `a.txt`, and nothing was queued.
**Where:** `drop_doomed_ops` in `crates/pdfs-core/src/db/ops.rs`.
**Cause.** A delete turns a create that was sent into a trash of what it may have made (B151).
Whether it was sent was read from its attempts and its claim. A write after the failure attaches
its bytes to the create and clears the attempts, so that the retry goes out at once
(`Db::attach_blob_to_create`). The delete then took the create for one never sent, and dropped
it whole.
**Fix.** A create with a target recorded (`sent_to`, B184) counts as sent as well. Test
`a_create_rewritten_after_it_failed_still_leaves_a_trash_when_deleted` in
`crates/pdfs-core/src/db/tests.rs`.
## B190 — A file saved just before the daemon stops can keep its old content on Drive ## B190 — A file saved just before the daemon stops can keep its old content on Drive
**Status:** Fixed (unverified). **Status:** Fixed (unverified).
+4 -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 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 - **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). 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 B190. - Fixes from the simulation and account runs since then: B125, B127, B130 and B141 to B191.
- **Sync issues, first cut** (§6.2): refusals sorted into issues, shown at once in the app, - **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 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. `pdfs sync discard`; a warning badge on the node in the file browser.
@@ -116,7 +116,9 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase
The run on 2baad67 found that a restart before such a file's trash went out queued The run on 2baad67 found that a restart before such a file's trash went out queued
its kept bytes again, for good (B188, fixed), and changes that sat out their retry its kept bytes again, for good (B188, fixed), and changes that sat out their retry
after an outage only the drain had met (B189, fixed; older than 3.0.0), and a file after an outage only the drain had met (B189, fixed; older than 3.0.0), and a file
saved just before a stop whose release the stop dropped (B190, fixed; older too). saved just before a stop whose release the stop dropped (B190, fixed; older too). The run
on 1436c5f found a file deleted after its failed create was written again left on Drive
(B191, fixed).
- [ ] A clean account run. The one on a6f36d5 (2026-10-04, LAN: 427 passed, 12 failed) - [ ] 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 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 the journal check. B121 was a bug (B149, fixed); B113's case still expected creates