Skip to content

feat(local): explicit close with error checking for LocalFileSystem#676

Merged
crepererum merged 1 commit intoapache:mainfrom
jgiannuzzi:explicit-close-with-error-reporting
Apr 17, 2026
Merged

feat(local): explicit close with error checking for LocalFileSystem#676
crepererum merged 1 commit intoapache:mainfrom
jgiannuzzi:explicit-close-with-error-reporting

Conversation

@jgiannuzzi
Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Implements one part of #661.

Rationale for this change

See issue. This PR is limited to explicitly calling close on written files with error checking.

What changes are included in this PR?

  • Add close_file() helper that calls libc::close() (Unix) / CloseHandle (Windows) directly and propagates errors, instead of relying on File::drop(). Note that this uses unsafe, like in the platforms' respective drop() implementations.
  • Apply both to the put and put_multipart (via LocalUpload) code paths.
  • Add libc (Unix) and windows-sys (Windows) as optional dependencies behind the fs feature for their respective platforms.
  • Add unit test verifying close_file detects EBADF when the file descriptor is closed behind Rust's back. This is just to validate that close_file properly returns the OS error.

Are there any user-facing changes?

Close errors are now detected and propagated as Error::UnableToCopyDataToFile. Previously these were silently ignored. This is a correctness improvement but could surface errors that were previously hidden.

@jgiannuzzi jgiannuzzi marked this pull request as ready for review March 23, 2026 13:07
@jgiannuzzi jgiannuzzi changed the title feat: explicit close with error checking for LocalFileSystem feat(local): explicit close with error checking for LocalFileSystem Mar 23, 2026
Comment thread src/local.rs Outdated
Comment on lines +146 to +152
use std::os::unix::io::IntoRawFd;
let fd = file.into_raw_fd();
// SAFETY: `fd` is a valid, owned file descriptor obtained from `into_raw_fd()`.
match unsafe { libc::close(fd) } {
0 => Ok(()),
_ => Err(io::Error::last_os_error()),
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already depend on nix, could we use that library on Unix instead?

https://docs.rs/nix/latest/nix/unistd/fn.close.html

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nix was only used as a dev dependency which is why I went with libc instead, cutting down the middleman (nix uses libc under the hood). I am happy to use nix instead though, as it simplifies this piece of code significantly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think nix is a fine choice here and keeps the dev and prod world kinda in-sync 👍

Comment thread src/local.rs Outdated
Comment on lines +1960 to +1970
#[test]
fn test_close_file_detects_error() {
use std::os::unix::io::AsRawFd;

let file = tempfile::tempfile().unwrap();
let fd = file.as_raw_fd();
// Close the fd behind Rust's back so close_file will get EBADF
assert_eq!(unsafe { libc::close(fd) }, 0);
let err = super::close_file(file).unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::EBADF));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test tests the new function that you've just added. However I somewhat fail to see what actual error case you're trying to prevent here. The file descriptor isn't exposed to the API user of object_store. In #661 you reference pola-rs/polars#21002 but that issue is about polar's own file handling, not about object_store. Also you say this is an alternative to fsync, but that's not true. Closing the file doesn't give you ANY guarantees with regards to durability. So what concrete order of events leads to a case where the additional error check (via the manual FD close) would help?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is just validating our logic by emulating a failure case.

The actual failure that can happen is when using a remote file system (e.g. NFS) and suffering from an underlying issue with the mount. In this case, the close could fail because some previous queued writes failed and checking the close return code allows the calling application to know that something failed, even if fsync wasn't called.

See this comment on the Polars issue for more details: pola-rs/polars#21002 (comment)

I don't think I claimed this was an alternative to fsync. It's rather a way to make sure these errors are reported to the caller. For durability guarantees, fsync remains required (and I think this should be part of another PR).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, NFS our old friend 😁 . This makes sense, thank you!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW: you could also call close_file twice and expect that the 2nd call fails. That would make this test here also work on Windows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! I've updated the Unix test and added a Windows version of it. Sadly I still need to use a raw fd/handle in order to not consume the file and be able to close it twice. Thank you Rust for generally preventing misusing APIs! (That's not sarcasm 😅)

@jgiannuzzi jgiannuzzi force-pushed the explicit-close-with-error-reporting branch from dd6f9ff to eee62ba Compare April 8, 2026 13:57
Add explicit close with error checking on Unix (libc::close) and Windows
(CloseHandle), replacing silent drop.
@jgiannuzzi jgiannuzzi force-pushed the explicit-close-with-error-reporting branch from eee62ba to 4a0ba0b Compare April 8, 2026 15:25
@jgiannuzzi
Copy link
Copy Markdown
Contributor Author

hey @crepererum, could you please take another look at this?

Copy link
Copy Markdown
Contributor

@crepererum crepererum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thank you!

@crepererum crepererum merged commit 9b8cf52 into apache:main Apr 17, 2026
10 checks passed
@jgiannuzzi jgiannuzzi deleted the explicit-close-with-error-reporting branch April 17, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants