fix(blob): fsync blob files via a write handle so Windows works

The sync sweep (and the EXDEV copy fallback) opened blob files with
File::open — a read-only handle — before calling sync_all. POSIX fsync
accepts read-only fds, so Linux never noticed, but Windows
FlushFileBuffers requires a GENERIC_WRITE handle and fails with
ACCESS_DENIED (os error 5) on every call. On Windows deployments the
strict sweep therefore failed every deferred sync, and the post-copy
fsync silently never happened.

Files now open via OpenOptions::write(true); the best-effort directory
fsyncs keep the read-only POSIX dirent idiom unchanged.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-14 16:02:02 +08:00
parent 6ee26e46e6
commit 5f9f91ee2f
@@ -102,7 +102,23 @@ async fn fsync_paths_parallel(paths: Vec<PathBuf>, strict: bool) -> Result<(), D
tasks.push(tokio::task::spawn_blocking(
move || -> Result<(), (PathBuf, std::io::Error)> {
for path in &group {
let result = std::fs::File::open(path).and_then(|f| f.sync_all());
// `strict` marks blob *file* fsyncs; best-effort marks
// prefix *directory* fsyncs. That distinction also picks
// the open mode: Windows `FlushFileBuffers` needs a
// GENERIC_WRITE handle and fails with ACCESS_DENIED on
// the read-only handle `File::open` returns (POSIX fsync
// accepts read-only fds, which is why this only surfaced
// on Windows). Directories keep the read-only POSIX
// dirent-sync idiom — they can't be fsync'd on Windows
// at all, and their failures stay best-effort warnings.
let result = if strict {
std::fs::OpenOptions::new()
.write(true)
.open(path)
.and_then(|f| f.sync_all())
} else {
std::fs::File::open(path).and_then(|f| f.sync_all())
};
if let Err(e) = result {
if strict {
return Err((path.clone(), e));
@@ -394,7 +410,11 @@ impl BlobStorageBackend for LocalBlobBackend {
format!("Failed to copy file to blob store: {}", ce),
)
})?;
if let Ok(f) = fs::File::open(&blob_path).await {
// Open for write: Windows `FlushFileBuffers` requires a
// GENERIC_WRITE handle — the read-only handle from
// `File::open` fails with ACCESS_DENIED, silently
// skipping this fsync on every Windows deployment.
if let Ok(f) = fs::OpenOptions::new().write(true).open(&blob_path).await {
let _ = f.sync_all().await;
}
let _ = fs::remove_file(&source_path).await;