Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement file_lock feature #130999

Merged
merged 4 commits into from
Nov 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 217 additions & 0 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,223 @@ impl File {
self.inner.datasync()
}

/// Acquire an exclusive advisory lock on the file. Blocks until the lock can be acquired.
///
/// This acquires an exclusive advisory lock; no other file handle to this file may acquire
/// another lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then an exclusive lock is held.
///
/// If the file not open for writing, it is unspecified whether this function returns an error.
///
/// Note, this is an advisory lock meant to interact with [`lock_shared`], [`try_lock`],
/// [`try_lock_shared`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
cberner marked this conversation as resolved.
Show resolved Hide resolved
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_EX` flag,
/// and the `LockFileEx` function on Windows with the `LOCKFILE_EXCLUSIVE_LOCK` flag. Note that,
/// this [may change in the future][changes].
Copy link
Member

Choose a reason for hiding this comment

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

It must not change for programs compiled with different rustc versions to interoperate wrt locking, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I included may change in the future because that language is used in all the other places that Platform-specific behavior is documented, that I could find. Is there guidance on when to include or not include it?

It seems appropriate to include it to me, because the implementation can be changed and still interoperate. For example, LockFile can be called instead of LockFileEx in the case of lock() and lock_shared(). Similarly, you can imagine that a new syscall might be added to Unix and/or Windows which can be used as an alternative.

///
/// [changes]: io#platform-specific-behavior
///
/// [`lock_shared`]: File::lock_shared
/// [`try_lock`]: File::try_lock
/// [`try_lock_shared`]: File::try_lock_shared
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.lock()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn lock(&self) -> io::Result<()> {
self.inner.lock()
}

/// Acquire a shared advisory lock on the file. Blocks until the lock can be acquired.
///
/// This acquires a shared advisory lock; more than one file handle may hold a shared lock, but
/// none may hold an exclusive lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock, the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then a shared lock is held.
///
/// Note, this is an advisory lock meant to interact with [`lock`], [`try_lock`],
/// [`try_lock_shared`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_SH` flag,
/// and the `LockFileEx` function on Windows. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// [`lock`]: File::lock
/// [`try_lock`]: File::try_lock
/// [`try_lock_shared`]: File::try_lock_shared
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.lock_shared()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn lock_shared(&self) -> io::Result<()> {
self.inner.lock_shared()
}

/// Acquire an exclusive advisory lock on the file. Returns `Ok(false)` if the file is locked.
///
/// This acquires an exclusive advisory lock; no other file handle to this file may acquire
/// another lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock, the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then an exclusive lock is held.
///
/// If the file not open for writing, it is unspecified whether this function returns an error.
///
/// Note, this is an advisory lock meant to interact with [`lock`], [`lock_shared`],
/// [`try_lock_shared`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_EX` and
/// `LOCK_NB` flags, and the `LockFileEx` function on Windows with the `LOCKFILE_EXCLUSIVE_LOCK`
/// and `LOCKFILE_FAIL_IMMEDIATELY` flags. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// [`lock`]: File::lock
/// [`lock_shared`]: File::lock_shared
/// [`try_lock_shared`]: File::try_lock_shared
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.try_lock()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn try_lock(&self) -> io::Result<bool> {
self.inner.try_lock()
}

/// Acquire a shared advisory lock on the file.
/// Returns `Ok(false)` if the file is exclusively locked.
///
/// This acquires a shared advisory lock; more than one file handle may hold a shared lock, but
/// none may hold an exclusive lock.
///
/// If this file handle, or a clone of it, already holds an advisory lock, the exact behavior is
/// unspecified and platform dependent, including the possibility that it will deadlock.
/// However, if this method returns, then a shared lock is held.
///
/// Note, this is an advisory lock meant to interact with [`lock`], [`try_lock`],
/// [`try_lock`], and [`unlock`]. Its interactions with other methods, such as [`read`]
/// and [`write`] are platform specific, and it may or may not cause non-lockholders to block.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_SH` and
/// `LOCK_NB` flags, and the `LockFileEx` function on Windows with the
/// `LOCKFILE_FAIL_IMMEDIATELY` flag. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// [`lock`]: File::lock
/// [`lock_shared`]: File::lock_shared
/// [`try_lock`]: File::try_lock
/// [`unlock`]: File::unlock
/// [`read`]: Read::read
/// [`write`]: Write::write
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.try_lock_shared()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn try_lock_shared(&self) -> io::Result<bool> {
self.inner.try_lock_shared()
}

/// Release all locks on the file.
///
/// All remaining locks are released when the file handle, and all clones of it, are dropped.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `flock` function on Unix with the `LOCK_UN` flag,
/// and the `UnlockFile` function on Windows. Note that, this
/// [may change in the future][changes].
///
/// [changes]: io#platform-specific-behavior
///
/// # Examples
///
/// ```no_run
/// #![feature(file_lock)]
/// use std::fs::File;
///
/// fn main() -> std::io::Result<()> {
/// let f = File::open("foo.txt")?;
/// f.lock()?;
/// f.unlock()?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "file_lock", issue = "130994")]
pub fn unlock(&self) -> io::Result<()> {
self.inner.unlock()
}

/// Truncates or extends the underlying file, updating the size of
/// this file to become `size`.
///
Expand Down
118 changes: 118 additions & 0 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,124 @@ fn file_test_io_seek_and_write() {
assert!(read_str == final_msg);
}

#[test]
fn file_lock_multiple_shared() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_multiple_shared_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that we can acquire concurrent shared locks
check!(f1.lock_shared());
check!(f2.lock_shared());
check!(f1.unlock());
check!(f2.unlock());
assert!(check!(f1.try_lock_shared()));
assert!(check!(f2.try_lock_shared()));
}

#[test]
fn file_lock_blocking() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_blocking_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that shared locks block exclusive locks
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
check!(f1.unlock());

// Check that exclusive locks block shared locks
check!(f1.lock());
assert!(!check!(f2.try_lock_shared()));
}

#[test]
fn file_lock_drop() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_dup_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that locks are released when the File is dropped
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
drop(f1);
assert!(check!(f2.try_lock()));
}

#[test]
fn file_lock_dup() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_dup_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// Check that locks are not dropped if the File has been cloned
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
let cloned = check!(f1.try_clone());
drop(f1);
assert!(!check!(f2.try_lock()));
drop(cloned)
}

#[test]
#[cfg(windows)]
fn file_lock_double_unlock() {
let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_double_unlock_test.txt");
let f1 = check!(File::create(filename));
let f2 = check!(OpenOptions::new().write(true).open(filename));

// On Windows a file handle may acquire both a shared and exclusive lock.
// Check that both are released by unlock()
check!(f1.lock());
check!(f1.lock_shared());
assert!(!check!(f2.try_lock()));
check!(f1.unlock());
assert!(check!(f2.try_lock()));
}

#[test]
#[cfg(windows)]
fn file_lock_blocking_async() {
use crate::thread::{sleep, spawn};
const FILE_FLAG_OVERLAPPED: u32 = 0x40000000;

let tmpdir = tmpdir();
let filename = &tmpdir.join("file_lock_blocking_async.txt");
let f1 = check!(File::create(filename));
let f2 =
check!(OpenOptions::new().custom_flags(FILE_FLAG_OVERLAPPED).write(true).open(filename));

check!(f1.lock());

// Ensure that lock() is synchronous when the file is opened for asynchronous IO
let t = spawn(move || {
check!(f2.lock());
});
sleep(Duration::from_secs(1));
assert!(!t.is_finished());
check!(f1.unlock());
t.join().unwrap();

// Ensure that lock_shared() is synchronous when the file is opened for asynchronous IO
let f2 =
check!(OpenOptions::new().custom_flags(FILE_FLAG_OVERLAPPED).write(true).open(filename));
check!(f1.lock());

// Ensure that lock() is synchronous when the file is opened for asynchronous IO
let t = spawn(move || {
check!(f2.lock_shared());
});
sleep(Duration::from_secs(1));
assert!(!t.is_finished());
check!(f1.unlock());
t.join().unwrap();
}

#[test]
fn file_test_io_seek_shakedown() {
// 01234567890123
Expand Down
20 changes: 20 additions & 0 deletions library/std/src/sys/pal/hermit/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,26 @@ impl File {
self.fsync()
}

pub fn lock(&self) -> io::Result<()> {
unsupported()
}

pub fn lock_shared(&self) -> io::Result<()> {
unsupported()
}

pub fn try_lock(&self) -> io::Result<bool> {
unsupported()
}

pub fn try_lock_shared(&self) -> io::Result<bool> {
unsupported()
}

pub fn unlock(&self) -> io::Result<()> {
unsupported()
}

pub fn truncate(&self, _size: u64) -> io::Result<()> {
Err(Error::from_raw_os_error(22))
}
Expand Down
Loading
Loading