clockworklabs/SpacetimeDB · error · io::Error
failed to open directory
Error message
failed to open directory {} for fsync: {} What it means
crates/snapshot's sync helper fsyncs both files and (on Unix) their enclosing directories for durability. For Self::Dir(path) it must File::open the directory first, since directories are opened for fsync rather than created. If that open fails, the original io::Error kind is preserved and wrapped with 'failed to open directory {path} for fsync: {source}'. Typical underlying causes: the directory was deleted or renamed between snapshot and sync, permissions changed, or the path is not a directory.
Solutions
- Check that the directory still exists and is a directory right before syncing; re-create it if the workflow allows
- Fix permissions so the process can open (read) the directory it needs to sync
- Serialize directory cleanup with snapshot finalization so they cannot race
- Match on io::ErrorKind in the caller (NotFound/PermissionDenied) to decide between recreate, retry, and abort
Example fix
// before
SyncTarget::Dir(&dir).sync_all()?; // NotFound: dir removed by cleaner mid-snapshot
// after
if !dir.is_dir() {
std::fs::create_dir_all(&dir)?; // or: skip sync if dir intentionally gone
}
SyncTarget::Dir(&dir).sync_all()?; Defensive patterns
Strategy: try-catch
Validate before calling
if !dir.as_path().is_dir() {
std::fs::create_dir_all(&dir)?; // or skip if legitimately gone
} Type guard
fn is_syncable_dir(p: &Path) -> bool { p.is_dir() } Try / catch
if let Err(e) = SyncTarget::Dir(&dir).sync_all() {
if e.kind() == std::io::ErrorKind::NotFound { /* recreated/removed concurrently: decide policy */ }
else if e.kind() == std::io::ErrorKind::PermissionDenied { /* fix perms */ }
else { return Err(e); }
} Prevention
- Serialize snapshot finalization with directory GC/cleanup
- Ensure the process can open (read) directories it must sync
- Keep snapshot trees on local POSIX filesystems
When it happens
Trigger: Snapshot/restore code syncing a directory that a concurrent process removed or moved; running under a user without read (open) permission on the directory; a path that points at a file rather than a directory; container/overlay filesystems that reject opening directories.
Common situations: Concurrent cleanup (tmp cleaners, GC of snapshot dirs) racing with durability sync; restrictive permissions in hardened deployments; NFS/FUSE mounts with odd open semantics for directories.
Related errors
- failed to fsync directory
- failed to fsync file
- failed to open file for fsync
- `close` was called on this `NoDurability` instance
- error syncing data to disk
AI-assisted analysis of clockworklabs/SpacetimeDB@9e0d92412f (2026-08-20).
Data as JSON: /api/errors/801af999b94def3a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/snapshot/src/lib.rs:1630
Dir(&'a Path),
}
impl FileOrDirPath<'_> {
/// `fsync` the file or directory at path `self`.
///
/// On *nix systems, both a file and its enclosing directory should be
/// `fsync`ed to make the file durable.
///
/// On Windows, only the file needs to be synced, and it's even an error to
/// sync a directory. Passing in [Self::Dir] is thus a no-op on Windows.
fn sync_all(&self) -> io::Result<()> {
match self {
#[cfg(target_os = "windows")]
Self::Dir(path) => Ok(()),
#[cfg(not(target_os = "windows"))]
Self::Dir(path) => File::open(path)
.map_err(|e| {
io::Error::new(
e.kind(),
format!("failed to open directory {} for fsync: {}", path.display(), e),
)
})?
.sync_all()
.map_err(|e| io::Error::new(e.kind(), format!("failed to fsync directory {}: {}", path.display(), e))),
Self::File(path) => {
File::options()
.read(true)
// Windows needs the file to be writable for `sync_all` to work.
// Set all the open options explicitly, just for visibility.
.write(true)
.truncate(false)
.create(false)
.append(false)
.open(path)
.map_err(|e| {
io::Error::new(View on GitHub (pinned to 9e0d92412f)