diff --git a/crates/pdfs-core/src/db/nodes.rs b/crates/pdfs-core/src/db/nodes.rs index e1c1d46..21a2dda 100644 --- a/crates/pdfs-core/src/db/nodes.rs +++ b/crates/pdfs-core/src/db/nodes.rs @@ -900,8 +900,25 @@ fn adopt_children_tx(tx: &Transaction<'_>, uid: &str, lid: i64) -> Result<()> { } fn upsert_node_tx(tx: &Transaction<'_>, node: &Node) -> Result<()> { - let json = serde_json::to_string(node)?; let uid = node.uid.to_string(); + // A node written under its stand-in after it landed is its row, which has + // the uid Drive gave it by now: written under the stand-in, it would bring + // the placeholder row back beside it. + if let Some(lid) = local_lid(&uid) { + let landed: Option = tx + .query_row( + "SELECT uid FROM nodes WHERE lid = ?1 AND uid IS NOT ?2", + params![lid, uid], + |row| row.get(0), + ) + .optional()?; + if let Some(real) = landed.as_deref().and_then(parse_node_uid) { + let mut node = node.clone(); + node.uid = real; + return upsert_node_tx(tx, &node); + } + } + let json = serde_json::to_string(node)?; let parent_uid = node.parent_uid.as_ref().map(|u| u.to_string()); let prior: Option = tx diff --git a/crates/pdfs-core/src/db/tests.rs b/crates/pdfs-core/src/db/tests.rs index 5567709..88a1435 100644 --- a/crates/pdfs-core/src/db/tests.rs +++ b/crates/pdfs-core/src/db/tests.rs @@ -3964,6 +3964,43 @@ fn a_landed_folder_rewrites_no_row_or_op_below_it() { ); } +/// A node the mount still holds under its stand-in, written after it landed, +/// updates the landed row: written under the stand-in, it brought the +/// placeholder row back beside it. +#[test] +fn a_stand_in_written_after_landing_updates_the_landed_row() { + let db = Db::open_in_memory().unwrap(); + db.upsert_node(&folder("root", None, "My Files")).unwrap(); + let lid = db + .add_local_row(&uid("root").to_string(), "a.txt", false, 1) + .unwrap(); + let local = local_uid(lid); + let mut node = file("x", "root", "a.txt", 3); + node.uid = NodeUid::new(VolumeId::from("local"), LinkId::from(lid.to_string())); + db.upsert_node(&node).unwrap(); + db.land_placeholder_row(&local, &uid("a").to_string()) + .unwrap(); + + node.name = "b.txt".into(); + db.upsert_node(&node).unwrap(); + + let rows: Vec<(i64, String, String)> = { + let conn = db.conn.lock(); + let mut stmt = conn + .prepare("SELECT lid, uid, name FROM nodes WHERE uid != ?1") + .unwrap(); + stmt.query_map([uid("root").to_string()], |r| { + Ok((r.get(0)?, r.get(1)?, r.get(2)?)) + }) + .unwrap() + .collect::>() + .unwrap() + }; + assert_eq!(rows, [(lid, uid("a").to_string(), "b.txt".to_string())]); + let landed = db.node_by_uid(&uid("a").to_string()).unwrap().unwrap(); + assert_eq!((landed.uid, landed.name), (uid("a"), "b.txt".to_string())); +} + /// A create queued inside a folder made here is sent under the folder's real /// uid once it lands. Landing where it was sent is no move. #[test] diff --git a/crates/pdfs-fuse/src/lib.rs b/crates/pdfs-fuse/src/lib.rs index 1c704de..95574a6 100644 --- a/crates/pdfs-fuse/src/lib.rs +++ b/crates/pdfs-fuse/src/lib.rs @@ -169,10 +169,6 @@ const DRAIN_BACKOFF_MAX: Duration = Duration::from_secs(300); /// directly on a new write or a reconnect, so this only bounds how late a /// backoff can fire. const DRAIN_IDLE_POLL: Duration = Duration::from_secs(30); -/// How long a write whose create just landed waits for the drain to repoint -/// its inode (`Core::landed_uid`): 200 polls of 10 ms, two seconds in all. -const LANDED_UID_POLLS: u32 = 200; -const LANDED_UID_POLL: Duration = Duration::from_millis(10); /// How long the mount waits before asking again for a node it has just created /// when Drive does not list it yet (`Core::fetch_minted_node`). const MINTED_READ_BACK_DELAYS: [Duration; 2] = [Duration::from_millis(250), Duration::from_secs(1)]; @@ -3430,9 +3426,9 @@ impl Core { break 'queued attached.superseded; } drop(pending); - // The create landed while this handle was open. Its uid is the - // one the drain has just given the inode. - let Some(real) = self.landed_uid(ino) else { + // The create landed while this handle was open, and its row + // already has the uid Drive gave it. + let Some(real) = self.landed_row_uid(ino) else { error!(%uid, staged = %path.display(), "queued create vanished under a write; bytes kept in staging"); return Err(Errno::EIO); @@ -3511,23 +3507,6 @@ impl Core { Ok(()) } - /// The real uid the drain gave `ino` when its queued create landed. - /// - /// Called by a write that found the create already retired. The drain - /// repoints the inode right after it drops `pending`, with no network in - /// between, so a short wait covers the gap; `None` means the inode is gone - /// or never stopped being a placeholder. - fn landed_uid(&self, ino: u64) -> Option { - for _ in 0..LANDED_UID_POLLS { - let uid = self.state().entries.get(&ino).map(|e| e.uid.clone())?; - if !is_local_uid(&uid) { - return Some(uid); - } - std::thread::sleep(LANDED_UID_POLL); - } - None - } - /// Queue the new content of a path-based truncate — `> file`, or any /// `setattr(size=…)` arriving without a write handle. /// diff --git a/docs/MILESTONE-3.0.0-STATUS.md b/docs/MILESTONE-3.0.0-STATUS.md index e2b44c3..44763f0 100644 --- a/docs/MILESTONE-3.0.0-STATUS.md +++ b/docs/MILESTONE-3.0.0-STATUS.md @@ -36,7 +36,7 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase places each node under its parent's row as it is now. - [x] Queries over children, subtrees and ancestors by `parent_lid`, and no rewrite of the children's `parent_uid` when a folder lands (R3), nor of the ops made inside it (R2). - - [ ] No more placeholders: a node made here goes by its local id until it lands. In four + - [x] No more placeholders: a node made here goes by its local id until it lands. In four parts: - [x] 6a: a node made here goes by `local~`, a function of its row, instead of a minted `local~-`; schema 41 rewrites the old placeholders (nodes, ops, pins). @@ -48,10 +48,13 @@ Tracks `docs/MILESTONE-3.0.0.md`. Update it with every commit that moves a phase now. The row keeps its stand-in in `nodes.uid` rather than `NULL`: it is as stable, and `NULL` would have changed some 40 node queries for no change in behaviour. Pins stay keyed by uid and move with the row in the landing transaction (B148). - - [ ] 6d: in memory, `pending`, `hidden` and `creating` by lid; `is_local_uid` checks - become `remote_uid().is_none()`; a write or rename after landing queues under the - stand-in, so `landed_uid`, `follow_landed_create`, `landed_row_uid` and the - `rewrite_op_target` fallback go. + - [x] 6d: a node written under its stand-in after it landed updates its landed row + instead of bringing the placeholder row back, so a write that finds its create + landed takes the uid from the row and no longer polls the tree for up to 2 s + (`landed_uid` is gone). In memory, `pending`, the uploads and the tree stay keyed by + the uid Drive knows: ops are matched by lid in the database (6b), `hidden` and + `creating` only ever hold remote uids, and `landed_row_uid` is the one place at the + mount's edge where a stand-in a handle still holds becomes the real uid. - [ ] An executor that runs ops in dependency order, many at once. 3. **Speed (§7)**: run independent ops in parallel, batch trashes and moves; measure 1,000 files and 100 folders on Wi-Fi against LAN (target: within 10 %).