fix(core,fuse): a node written under its stand-in after landing updates its landed row

This commit is contained in:
nvrl committed 2026-10-04 13:38:54 +02:00
1 parent 9c93860cd8
commit dbc4d9a8c5
4 files changed
+66 -30

No files matched your search

+18 -1
View File
@@ -900,8 +900,25 @@ fn adopt_children_tx(tx: &Transaction<'_>, uid: &str, lid: i64) -> Result<()> {
} }
fn upsert_node_tx(tx: &Transaction<'_>, node: &Node) -> Result<()> { fn upsert_node_tx(tx: &Transaction<'_>, node: &Node) -> Result<()> {
let json = serde_json::to_string(node)?;
let uid = node.uid.to_string(); 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<String> = 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 parent_uid = node.parent_uid.as_ref().map(|u| u.to_string());
let prior: Option<PriorRow> = tx let prior: Option<PriorRow> = tx
+37
View File
@@ -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::<rusqlite::Result<_>>()
.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 /// 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. /// uid once it lands. Landing where it was sent is no move.
#[test] #[test]
+3 -24
View File
@@ -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 /// directly on a new write or a reconnect, so this only bounds how late a
/// backoff can fire. /// backoff can fire.
const DRAIN_IDLE_POLL: Duration = Duration::from_secs(30); 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 /// 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`). /// 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)]; 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; break 'queued attached.superseded;
} }
drop(pending); drop(pending);
// The create landed while this handle was open. Its uid is the // The create landed while this handle was open, and its row
// one the drain has just given the inode. // already has the uid Drive gave it.
let Some(real) = self.landed_uid(ino) else { let Some(real) = self.landed_row_uid(ino) else {
error!(%uid, staged = %path.display(), error!(%uid, staged = %path.display(),
"queued create vanished under a write; bytes kept in staging"); "queued create vanished under a write; bytes kept in staging");
return Err(Errno::EIO); return Err(Errno::EIO);
@@ -3511,23 +3507,6 @@ impl Core {
Ok(()) 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<NodeUid> {
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 /// Queue the new content of a path-based truncate — `> file`, or any
/// `setattr(size=…)` arriving without a write handle. /// `setattr(size=…)` arriving without a write handle.
/// ///
+8 -5
View File
@@ -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. 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 - [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). 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: parts:
- [x] 6a: a node made here goes by `local~<lid>`, a function of its row, instead of a - [x] 6a: a node made here goes by `local~<lid>`, a function of its row, instead of a
minted `local~<ms>-<n>`; schema 41 rewrites the old placeholders (nodes, ops, pins). minted `local~<ms>-<n>`; 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, 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 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). 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 - [x] 6d: a node written under its stand-in after it landed updates its landed row
become `remote_uid().is_none()`; a write or rename after landing queues under the instead of bringing the placeholder row back, so a write that finds its create
stand-in, so `landed_uid`, `follow_landed_create`, `landed_row_uid` and the landed takes the uid from the row and no longer polls the tree for up to 2 s
`rewrite_op_target` fallback go. (`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. - [ ] 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 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 %). and 100 folders on Wi-Fi against LAN (target: within 10 %).