Copilot commented on code in PR #890:
URL: 
https://github.com/apache/arrow-rs-object-store/pull/890#discussion_r4459293773


##########
src/local.rs:
##########
@@ -900,79 +906,103 @@ impl LocalFileSystem {
 
 /// Creates the parent directories of `path` or returns an error based on 
`source` if no parent
 ///
-/// When `fsync` is true, every directory created here is fsynced, up to and 
including the first
-/// pre-existing ancestor (whose entry list also changed), so the new 
directory entries are durable.
+/// When `fsync` is true, every directory whose entries this changes is 
fsynced, so the new
+/// directory entries are durable.
 fn create_parent_dirs(path: &std::path::Path, source: io::Error, fsync: bool) 
-> Result<()> {
     let parent = path.parent().ok_or_else(|| {
         let path = path.to_path_buf();
         Error::UnableToCreateFile { path, source }
     })?;
+    if fsync {
+        create_dir_all_durable(parent, |dir| std::fs::create_dir(dir), 
fsync_dir)
+    } else {
+        std::fs::create_dir_all(parent).map_err(|source| {
+            let path = parent.into();
+            Error::UnableToCreateDir { source, path }.into()
+        })
+    }
+}
 
-    // Record the deepest already-existing ancestor *before* creating any 
directories, so that
-    // afterwards we know exactly which directories are new and need to be 
fsynced.
-    let first_existing = fsync.then(|| {
-        let mut dir = parent;
-        while !dir.exists() {
-            match dir.parent() {
-                Some(p) => dir = p,
-                None => break,
-            }
+/// Creates `dir` and its missing ancestors with `create`, recording in 
[`UNSYNCED`] every
+/// directory whose entries that changes, and fsyncs them with `sync`. Tests 
pass closures to
+/// interleave other changes and to observe the sync order.
+fn create_dir_all_durable(
+    dir: &std::path::Path,
+    mut create: impl FnMut(&std::path::Path) -> io::Result<()>,
+    sync: impl FnMut(&std::path::Path) -> io::Result<()>,
+) -> Result<()> {
+    // A directory found missing here may exist by the time `create` reaches 
it, made by a
+    // creator this process does not see (another process, a store without 
`fsync`) that may not
+    // sync its entry. Recording the directories above the missing ones covers 
that entry too.
+    let missing = dir.ancestors().take_while(|dir| !dir.exists());
+    let above_missing: Vec<_> = missing.filter_map(|dir| 
dir.parent()).collect();
+    changing(above_missing, || create_dir_all_recording(dir, &mut 
create)).map_err(|source| {
+        Error::UnableToCreateDir {
+            source,
+            path: dir.into(),
         }
-        dir.to_path_buf()
-    });
-
-    std::fs::create_dir_all(parent).map_err(|source| {
-        let path = parent.into();
-        Error::UnableToCreateDir { source, path }
     })?;
-
-    if let Some(first_existing) = first_existing {
-        // Walk from `parent` up to `first_existing`, fsyncing each directory 
whose entries changed.
-        let mut dir = parent;
-        loop {
-            fsync_dir(dir).map_err(|source| Error::UnableToSyncFile {
-                source,
-                path: dir.into(),
-            })?;
-            if dir == first_existing {
-                break;
-            }
-            dir = match dir.parent() {
-                Some(p) => p,
-                None => break,
-            };
+    sync_unsynced_ancestors_with(dir, sync).map_err(|source| {
+        Error::UnableToSyncFile {
+            source,
+            path: dir.into(),
         }
+        .into()
+    })

Review Comment:
   When syncing fails, the error is attributed to `path: dir` (the requested 
directory), but the actual failing sync may be for a specific ancestor 
directory. This makes operational debugging harder (e.g., permission issues on 
a particular parent). Consider plumbing the failing directory path out of 
`sync_unsynced_ancestors_with` (e.g., return `(PathBuf, io::Error)` or a small 
custom error type) so `UnableToSyncFile.path` can point at the directory that 
couldn't be synced.



##########
src/local.rs:
##########
@@ -900,79 +906,103 @@ impl LocalFileSystem {
 
 /// Creates the parent directories of `path` or returns an error based on 
`source` if no parent
 ///
-/// When `fsync` is true, every directory created here is fsynced, up to and 
including the first
-/// pre-existing ancestor (whose entry list also changed), so the new 
directory entries are durable.
+/// When `fsync` is true, every directory whose entries this changes is 
fsynced, so the new
+/// directory entries are durable.
 fn create_parent_dirs(path: &std::path::Path, source: io::Error, fsync: bool) 
-> Result<()> {
     let parent = path.parent().ok_or_else(|| {
         let path = path.to_path_buf();
         Error::UnableToCreateFile { path, source }
     })?;
+    if fsync {
+        create_dir_all_durable(parent, |dir| std::fs::create_dir(dir), 
fsync_dir)
+    } else {
+        std::fs::create_dir_all(parent).map_err(|source| {
+            let path = parent.into();
+            Error::UnableToCreateDir { source, path }.into()
+        })
+    }
+}
 
-    // Record the deepest already-existing ancestor *before* creating any 
directories, so that
-    // afterwards we know exactly which directories are new and need to be 
fsynced.
-    let first_existing = fsync.then(|| {
-        let mut dir = parent;
-        while !dir.exists() {
-            match dir.parent() {
-                Some(p) => dir = p,
-                None => break,
-            }
+/// Creates `dir` and its missing ancestors with `create`, recording in 
[`UNSYNCED`] every
+/// directory whose entries that changes, and fsyncs them with `sync`. Tests 
pass closures to
+/// interleave other changes and to observe the sync order.
+fn create_dir_all_durable(
+    dir: &std::path::Path,
+    mut create: impl FnMut(&std::path::Path) -> io::Result<()>,
+    sync: impl FnMut(&std::path::Path) -> io::Result<()>,
+) -> Result<()> {
+    // A directory found missing here may exist by the time `create` reaches 
it, made by a
+    // creator this process does not see (another process, a store without 
`fsync`) that may not
+    // sync its entry. Recording the directories above the missing ones covers 
that entry too.
+    let missing = dir.ancestors().take_while(|dir| !dir.exists());
+    let above_missing: Vec<_> = missing.filter_map(|dir| 
dir.parent()).collect();
+    changing(above_missing, || create_dir_all_recording(dir, &mut 
create)).map_err(|source| {
+        Error::UnableToCreateDir {
+            source,
+            path: dir.into(),
         }
-        dir.to_path_buf()
-    });
-
-    std::fs::create_dir_all(parent).map_err(|source| {
-        let path = parent.into();
-        Error::UnableToCreateDir { source, path }
     })?;
-
-    if let Some(first_existing) = first_existing {
-        // Walk from `parent` up to `first_existing`, fsyncing each directory 
whose entries changed.
-        let mut dir = parent;
-        loop {
-            fsync_dir(dir).map_err(|source| Error::UnableToSyncFile {
-                source,
-                path: dir.into(),
-            })?;
-            if dir == first_existing {
-                break;
-            }
-            dir = match dir.parent() {
-                Some(p) => p,
-                None => break,
-            };
+    sync_unsynced_ancestors_with(dir, sync).map_err(|source| {
+        Error::UnableToSyncFile {
+            source,
+            path: dir.into(),
         }
+        .into()
+    })
+}
+
+/// Creates `dir` and its missing ancestors one at a time, as 
[`std::fs::create_dir_all`] does,
+/// recording the parent of each as changing while `create` runs on it.
+fn create_dir_all_recording(
+    dir: &std::path::Path,
+    create: &mut impl FnMut(&std::path::Path) -> io::Result<()>,
+) -> io::Result<()> {
+    match changing(dir.parent(), || create(dir)) {
+        Err(e) if e.kind() == ErrorKind::NotFound => {}
+        Err(e) if !dir.is_dir() => return Err(e),
+        _ => return Ok(()),
+    }
+    match dir.parent() {
+        Some(p) => create_dir_all_recording(p, create)?,
+        None => return Err(io::Error::other("failed to create whole tree")),
+    }
+    match changing(dir.parent(), || create(dir)) {
+        Err(e) if !dir.is_dir() => Err(e),
+        _ => Ok(()),
     }

Review Comment:
   `create_dir_all_recording` is recursive on `dir.parent()`. For very deep 
paths this can overflow the stack, whereas `std::fs::create_dir_all` is 
iterative. Consider rewriting this to an iterative approach (collect missing 
components, then create from shallowest to deepest), while keeping the 
parent-recording behavior, to avoid a path-depth-dependent stack risk.



##########
src/local.rs:
##########
@@ -1032,6 +1062,131 @@ fn sync_and_close(file: File, path: &std::path::Path, 
fsync: bool) -> Result<()>
     Ok(())
 }
 
+/// Directories whose entries changed, or are changing, without an fsync 
since. Shared by every
+/// `LocalFileSystem` in the process: a durable write into a directory another 
call is still
+/// making durable fsyncs it too, and a failed fsync leaves the directory to 
the next write.
+///
+/// A map rather than a queue: changes to one directory collapse into one 
entry, and the change
+/// counter tells a sync whether the directory changed again after the sync 
started, so the entry
+/// is then kept.
+static UNSYNCED: Mutex<Unsynced> = Mutex::new(Unsynced {
+    dirs: BTreeMap::new(),
+    changes: 0,
+});
+
+struct Unsynced {
+    dirs: BTreeMap<PathBuf, Pending>,
+    /// The stamp of the last finished change.
+    changes: u64,
+}
+
+#[derive(Default)]
+struct Pending {
+    /// Changes begun but not finished; the entry stays while there is one.
+    in_flight: usize,
+    /// The stamp of the last finished change, until a sync covers it.
+    changed: Option<u64>,
+}
+
+/// Runs `change`, which changes the entries of `dirs`, with them recorded in 
[`UNSYNCED`] from
+/// before it runs (so a write that sees the change also sees the record) 
until a later sync
+/// succeeds. A failed `mkdir`, `rename` or `link` changes no directory, so an 
error records
+/// nothing; a panic counts as a change.
+fn changing<'a, T>(
+    dirs: impl IntoIterator<Item = &'a std::path::Path>,
+    change: impl FnOnce() -> io::Result<T>,
+) -> io::Result<T> {
+    let dirs: Vec<_> = dirs.into_iter().collect();
+    {
+        let mut unsynced = UNSYNCED.lock();
+        for dir in &dirs {
+            unsynced
+                .dirs
+                .entry(dir.to_path_buf())
+                .or_default()
+                .in_flight += 1;
+        }
+    }
+    let mut guard = Changing {
+        dirs,
+        changed: true,
+    };
+    let result = change();
+    guard.changed = result.is_ok();
+    result
+}
+
+/// Finishes the changes [`changing`] began, also when `change` panics.
+struct Changing<'a> {
+    dirs: Vec<&'a std::path::Path>,
+    changed: bool,
+}
+
+impl Drop for Changing<'_> {
+    fn drop(&mut self) {
+        let mut unsynced = UNSYNCED.lock();
+        let Unsynced { dirs, changes } = &mut *unsynced;
+        for dir in &self.dirs {
+            // Always present: an entry is not removed while a change is in 
flight.
+            let Some(pending) = dirs.get_mut(*dir) else {
+                continue;
+            };
+            pending.in_flight -= 1;
+            if self.changed {
+                *changes += 1;
+                pending.changed = Some(*changes);
+            } else if pending.in_flight == 0 && pending.changed.is_none() {
+                dirs.remove(*dir);
+            }
+        }
+    }
+}
+
+/// Fsyncs the directories recorded in [`UNSYNCED`] among `path` and its 
ancestors, deepest first,
+/// so an operation returns only once every directory entry leading to what it 
wrote is durable,
+/// whoever changed it. An entry leaves the set only when a sync that started 
after its last
+/// change succeeds or finds the directory gone; a failed sync leaves it to 
the next write.
+fn sync_unsynced_ancestors(path: &std::path::Path) -> io::Result<()> {
+    sync_unsynced_ancestors_with(path, fsync_dir)
+}
+
+/// [`sync_unsynced_ancestors`] with `sync` in place of `fsync_dir`, so tests 
can observe the order.
+fn sync_unsynced_ancestors_with(
+    path: &std::path::Path,
+    mut sync: impl FnMut(&std::path::Path) -> io::Result<()>,
+) -> io::Result<()> {
+    let pending: Vec<_> = {
+        let unsynced = UNSYNCED.lock();
+        path.ancestors()
+            .filter_map(|dir| Some((dir, unsynced.dirs.get(dir)?.changed)))
+            .collect()
+    };
+    let mut synced = 0;
+    let mut result = Ok(());
+    for (dir, _) in &pending {
+        match sync(dir) {
+            Ok(()) => synced += 1,
+            Err(e) => {
+                // A directory that is gone has no entries left to make 
durable.
+                if e.kind() == ErrorKind::NotFound {
+                    synced += 1;
+                }
+                result = Err(e);
+                break;
+            }
+        }

Review Comment:
   `ErrorKind::NotFound` is treated as 'covered' (increments `synced`) but 
still sets `result = Err(e)` and breaks, which makes the durable operation fail 
even though the comment/doc imply a missing directory should be treated as a 
non-error case ('directory gone drops its entry'). Consider treating `NotFound` 
as success (do not set `result` to `Err`, and do not break), so ancestor 
syncing can continue and callers don't see a spurious failure.



##########
src/local.rs:
##########
@@ -1032,6 +1062,131 @@ fn sync_and_close(file: File, path: &std::path::Path, 
fsync: bool) -> Result<()>
     Ok(())
 }
 
+/// Directories whose entries changed, or are changing, without an fsync 
since. Shared by every
+/// `LocalFileSystem` in the process: a durable write into a directory another 
call is still
+/// making durable fsyncs it too, and a failed fsync leaves the directory to 
the next write.
+///
+/// A map rather than a queue: changes to one directory collapse into one 
entry, and the change
+/// counter tells a sync whether the directory changed again after the sync 
started, so the entry
+/// is then kept.
+static UNSYNCED: Mutex<Unsynced> = Mutex::new(Unsynced {
+    dirs: BTreeMap::new(),
+    changes: 0,
+});
+
+struct Unsynced {
+    dirs: BTreeMap<PathBuf, Pending>,
+    /// The stamp of the last finished change.
+    changes: u64,
+}
+
+#[derive(Default)]
+struct Pending {
+    /// Changes begun but not finished; the entry stays while there is one.
+    in_flight: usize,
+    /// The stamp of the last finished change, until a sync covers it.
+    changed: Option<u64>,
+}
+
+/// Runs `change`, which changes the entries of `dirs`, with them recorded in 
[`UNSYNCED`] from
+/// before it runs (so a write that sees the change also sees the record) 
until a later sync
+/// succeeds. A failed `mkdir`, `rename` or `link` changes no directory, so an 
error records
+/// nothing; a panic counts as a change.
+fn changing<'a, T>(
+    dirs: impl IntoIterator<Item = &'a std::path::Path>,
+    change: impl FnOnce() -> io::Result<T>,
+) -> io::Result<T> {
+    let dirs: Vec<_> = dirs.into_iter().collect();
+    {
+        let mut unsynced = UNSYNCED.lock();
+        for dir in &dirs {
+            unsynced
+                .dirs
+                .entry(dir.to_path_buf())
+                .or_default()
+                .in_flight += 1;

Review Comment:
   This allocates a new `PathBuf` for every `dir` on every call 
(`to_path_buf()` happens even when the key is already present). Since this runs 
on every durable operation, consider avoiding allocation on the common path by 
first attempting `get_mut(dir)` (borrowed lookup) and only allocating/inserting 
a `PathBuf` when missing.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to