mirror of
https://github.com/awfixers-stuff/src.git
synced 2026-03-31 14:15:59 +00:00
create src
This commit is contained in:
1188
src-lock/CHANGELOG.md
Normal file
1188
src-lock/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
24
src-lock/Cargo.toml
Normal file
24
src-lock/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
lints.workspace = true
|
||||
|
||||
[package]
|
||||
name = "src-lock"
|
||||
version = "21.0.1"
|
||||
repository = "https://github.com/GitoxideLabs/gitoxide"
|
||||
license = "MIT OR Apache-2.0"
|
||||
description = "A git-style lock-file implementation"
|
||||
authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
|
||||
edition = "2021"
|
||||
include = ["src/**/*", "LICENSE-*", "README.md"]
|
||||
rust-version = "1.82"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
|
||||
[dependencies]
|
||||
src-utils = { version = "^0.3.1", default-features = false, path = "../src-utils" }
|
||||
src-tempfile = { version = "^21.0.0", default-features = false, path = "../src-tempfile" }
|
||||
thiserror = "2.0.18"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.26.0"
|
||||
1
src-lock/LICENSE-APACHE
Symbolic link
1
src-lock/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-APACHE
|
||||
1
src-lock/LICENSE-MIT
Symbolic link
1
src-lock/LICENSE-MIT
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-MIT
|
||||
5
src-lock/README.md
Normal file
5
src-lock/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
Use lock-files in the way git does with auto-cleanup being the most notable feature.
|
||||
|
||||
* [x] writable lock files that can be committed to atomically replace the resource they lock
|
||||
* [x] read-only markers that lock a resource without the intend to overwrite it
|
||||
* [x] auto-removal of the lockfiles and intermediate directories on drop or on signal
|
||||
242
src-lock/src/acquire.rs
Normal file
242
src-lock/src/acquire.rs
Normal file
@@ -0,0 +1,242 @@
|
||||
use std::{
|
||||
fmt,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use gix_tempfile::{AutoRemove, ContainingDirectory};
|
||||
|
||||
use crate::{backoff, File, Marker, DOT_LOCK_SUFFIX};
|
||||
|
||||
/// Describe what to do if a lock cannot be obtained as it's already held elsewhere.
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
pub enum Fail {
|
||||
/// Fail after the first unsuccessful attempt of obtaining a lock.
|
||||
#[default]
|
||||
Immediately,
|
||||
/// Retry after failure with quadratically longer sleep times to block the current thread.
|
||||
/// Fail once the given duration is exceeded, similar to [Fail::Immediately]
|
||||
AfterDurationWithBackoff(Duration),
|
||||
}
|
||||
|
||||
impl fmt::Display for Fail {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Fail::Immediately => f.write_str("immediately"),
|
||||
Fail::AfterDurationWithBackoff(duration) => {
|
||||
write!(f, "after {:.02}s", duration.as_secs_f32())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration> for Fail {
|
||||
fn from(value: Duration) -> Self {
|
||||
if value.is_zero() {
|
||||
Fail::Immediately
|
||||
} else {
|
||||
Fail::AfterDurationWithBackoff(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The error returned when acquiring a [`File`] or [`Marker`].
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Error {
|
||||
#[error("Another IO error occurred while obtaining the lock")]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error("The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{}' might need manual deletion.", super::DOT_LOCK_SUFFIX)]
|
||||
PermanentlyLocked {
|
||||
resource_path: PathBuf,
|
||||
mode: Fail,
|
||||
attempts: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// Create a writable lock file with failure `mode` whose content will eventually overwrite the given resource `at_path`.
|
||||
///
|
||||
/// If `boundary_directory` is given, non-existing directories will be created automatically and removed in the case of
|
||||
/// a rollback. Otherwise the containing directory is expected to exist, even though the resource doesn't have to.
|
||||
///
|
||||
/// Note that permissions will be set to `0o666`, which usually results in `0o644` after passing a default umask, on Unix systems.
|
||||
///
|
||||
/// ### Warning of potential resource leak
|
||||
///
|
||||
/// Please note that the underlying file will remain if destructors don't run, as is the case when interrupting the application.
|
||||
/// This results in the resource being locked permanently unless the lock file is removed by other means.
|
||||
/// See [the crate documentation](crate) for more information.
|
||||
pub fn acquire_to_update_resource(
|
||||
at_path: impl AsRef<Path>,
|
||||
mode: Fail,
|
||||
boundary_directory: Option<PathBuf>,
|
||||
) -> Result<File, Error> {
|
||||
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
|
||||
if let Some(permissions) = default_permissions() {
|
||||
gix_tempfile::writable_at_with_permissions(p, d, c, permissions)
|
||||
} else {
|
||||
gix_tempfile::writable_at(p, d, c)
|
||||
}
|
||||
})?;
|
||||
Ok(File {
|
||||
inner: handle,
|
||||
lock_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`acquire_to_update_resource()`](File::acquire_to_update_resource), but allows to set filesystem permissions using `make_permissions`.
|
||||
pub fn acquire_to_update_resource_with_permissions(
|
||||
at_path: impl AsRef<Path>,
|
||||
mode: Fail,
|
||||
boundary_directory: Option<PathBuf>,
|
||||
make_permissions: impl Fn() -> std::fs::Permissions,
|
||||
) -> Result<File, Error> {
|
||||
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
|
||||
gix_tempfile::writable_at_with_permissions(p, d, c, make_permissions())
|
||||
})?;
|
||||
Ok(File {
|
||||
inner: handle,
|
||||
lock_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Marker {
|
||||
/// Like [`acquire_to_update_resource()`](File::acquire_to_update_resource()) but _without_ the possibility to make changes
|
||||
/// and commit them.
|
||||
///
|
||||
/// If `boundary_directory` is given, non-existing directories will be created automatically and removed in the case of
|
||||
/// a rollback.
|
||||
///
|
||||
/// Note that permissions will be set to `0o666`, which usually results in `0o644` after passing a default umask, on Unix systems.
|
||||
///
|
||||
/// ### Warning of potential resource leak
|
||||
///
|
||||
/// Please note that the underlying file will remain if destructors don't run, as is the case when interrupting the application.
|
||||
/// This results in the resource being locked permanently unless the lock file is removed by other means.
|
||||
/// See [the crate documentation](crate) for more information.
|
||||
pub fn acquire_to_hold_resource(
|
||||
at_path: impl AsRef<Path>,
|
||||
mode: Fail,
|
||||
boundary_directory: Option<PathBuf>,
|
||||
) -> Result<Marker, Error> {
|
||||
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
|
||||
if let Some(permissions) = default_permissions() {
|
||||
gix_tempfile::mark_at_with_permissions(p, d, c, permissions)
|
||||
} else {
|
||||
gix_tempfile::mark_at(p, d, c)
|
||||
}
|
||||
})?;
|
||||
Ok(Marker {
|
||||
created_from_file: false,
|
||||
inner: handle,
|
||||
lock_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Like [`acquire_to_hold_resource()`](Marker::acquire_to_hold_resource), but allows to set filesystem permissions using `make_permissions`.
|
||||
pub fn acquire_to_hold_resource_with_permissions(
|
||||
at_path: impl AsRef<Path>,
|
||||
mode: Fail,
|
||||
boundary_directory: Option<PathBuf>,
|
||||
make_permissions: impl Fn() -> std::fs::Permissions,
|
||||
) -> Result<Marker, Error> {
|
||||
let (lock_path, handle) = lock_with_mode(at_path.as_ref(), mode, boundary_directory, &|p, d, c| {
|
||||
gix_tempfile::mark_at_with_permissions(p, d, c, make_permissions())
|
||||
})?;
|
||||
Ok(Marker {
|
||||
created_from_file: false,
|
||||
inner: handle,
|
||||
lock_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_cleanup(boundary: Option<PathBuf>) -> (ContainingDirectory, AutoRemove) {
|
||||
match boundary {
|
||||
None => (ContainingDirectory::Exists, AutoRemove::Tempfile),
|
||||
Some(boundary_directory) => (
|
||||
ContainingDirectory::CreateAllRaceProof(Default::default()),
|
||||
AutoRemove::TempfileAndEmptyParentDirectoriesUntil { boundary_directory },
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn lock_with_mode<T>(
|
||||
resource: &Path,
|
||||
mode: Fail,
|
||||
boundary_directory: Option<PathBuf>,
|
||||
try_lock: &dyn Fn(&Path, ContainingDirectory, AutoRemove) -> std::io::Result<T>,
|
||||
) -> Result<(PathBuf, T), Error> {
|
||||
use std::io::ErrorKind::*;
|
||||
let (directory, cleanup) = dir_cleanup(boundary_directory);
|
||||
let lock_path = add_lock_suffix(resource);
|
||||
let mut attempts = 1;
|
||||
match mode {
|
||||
Fail::Immediately => try_lock(&lock_path, directory, cleanup),
|
||||
Fail::AfterDurationWithBackoff(time) => {
|
||||
for wait in backoff::Quadratic::default_with_random().until_no_remaining(time) {
|
||||
attempts += 1;
|
||||
match try_lock(&lock_path, directory, cleanup.clone()) {
|
||||
Ok(v) => return Ok((lock_path, v)),
|
||||
#[cfg(windows)]
|
||||
Err(err) if err.kind() == AlreadyExists || err.kind() == PermissionDenied => {
|
||||
std::thread::sleep(wait);
|
||||
continue;
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
Err(err) if err.kind() == AlreadyExists => {
|
||||
std::thread::sleep(wait);
|
||||
continue;
|
||||
}
|
||||
Err(err) => return Err(Error::from(err)),
|
||||
}
|
||||
}
|
||||
try_lock(&lock_path, directory, cleanup)
|
||||
}
|
||||
}
|
||||
.map(|v| (lock_path, v))
|
||||
.map_err(|err| match err.kind() {
|
||||
AlreadyExists => Error::PermanentlyLocked {
|
||||
resource_path: resource.into(),
|
||||
mode,
|
||||
attempts,
|
||||
},
|
||||
_ => Error::Io(err),
|
||||
})
|
||||
}
|
||||
|
||||
fn add_lock_suffix(resource_path: &Path) -> PathBuf {
|
||||
resource_path.with_extension(resource_path.extension().map_or_else(
|
||||
|| DOT_LOCK_SUFFIX.chars().skip(1).collect(),
|
||||
|ext| format!("{}{}", ext.to_string_lossy(), DOT_LOCK_SUFFIX),
|
||||
))
|
||||
}
|
||||
|
||||
fn default_permissions() -> Option<std::fs::Permissions> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
Some(std::fs::Permissions::from_mode(0o666))
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn add_lock_suffix_to_file_with_extension() {
|
||||
assert_eq!(add_lock_suffix(Path::new("hello.ext")), Path::new("hello.ext.lock"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_lock_suffix_to_file_without_extension() {
|
||||
assert_eq!(add_lock_suffix(Path::new("hello")), Path::new("hello.lock"));
|
||||
}
|
||||
}
|
||||
76
src-lock/src/commit.rs
Normal file
76
src-lock/src/commit.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{File, Marker};
|
||||
|
||||
mod error {
|
||||
use std::{
|
||||
fmt,
|
||||
fmt::{Debug, Display},
|
||||
};
|
||||
|
||||
/// The error returned by various [`commit(…)`][super::Marker::commit()] methods
|
||||
#[derive(Debug)]
|
||||
pub struct Error<T: Debug> {
|
||||
/// The io error that prevented the attempt to succeed
|
||||
pub error: std::io::Error,
|
||||
/// The marker or file which was used in the attempt to persist it
|
||||
pub instance: T,
|
||||
}
|
||||
|
||||
impl<T: Debug> Display for Error<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.error, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> std::error::Error for Error<T> {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
self.error.source()
|
||||
}
|
||||
}
|
||||
}
|
||||
pub use error::Error;
|
||||
|
||||
impl Marker {
|
||||
/// Commit the changes written to the previously open file and overwrite the original file atomically, returning the resource path
|
||||
/// on success.
|
||||
///
|
||||
/// This fails for markers which weren't created with [`File::close()`]
|
||||
pub fn commit(mut self) -> Result<PathBuf, Error<Self>> {
|
||||
if !self.created_from_file {
|
||||
return Err(Error {
|
||||
error: std::io::Error::other("refusing to commit marker that was never opened"),
|
||||
instance: self,
|
||||
});
|
||||
}
|
||||
let resource_path = self.resource_path();
|
||||
match self.inner.persist(&resource_path) {
|
||||
Ok(_) => Ok(resource_path),
|
||||
Err(err) => Err(Error {
|
||||
error: err.error,
|
||||
instance: {
|
||||
self.inner = err.handle;
|
||||
self
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// Commit the changes written to this lock file and overwrite the original file atomically, returning the resource path
|
||||
/// and an open file handle on success.
|
||||
pub fn commit(mut self) -> Result<(PathBuf, Option<std::fs::File>), Error<Self>> {
|
||||
let resource_path = self.resource_path();
|
||||
match self.inner.persist(&resource_path) {
|
||||
Ok(possibly_file) => Ok((resource_path, possibly_file)),
|
||||
Err(err) => Err(Error {
|
||||
error: err.error,
|
||||
instance: {
|
||||
self.inner = err.handle;
|
||||
self
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src-lock/src/file.rs
Normal file
78
src-lock/src/file.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{File, Marker, DOT_LOCK_SUFFIX};
|
||||
|
||||
fn strip_lock_suffix(lock_path: &Path) -> PathBuf {
|
||||
let ext = lock_path
|
||||
.extension()
|
||||
.expect("at least our own extension")
|
||||
.to_str()
|
||||
.expect("no illegal UTF8 in extension");
|
||||
lock_path.with_extension(ext.split_at(ext.len().saturating_sub(DOT_LOCK_SUFFIX.len())).0)
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// Obtain a mutable reference to the write handle and call `f(out)` with it.
|
||||
pub fn with_mut<T>(&mut self, f: impl FnOnce(&mut std::fs::File) -> std::io::Result<T>) -> std::io::Result<T> {
|
||||
self.inner.with_mut(|tf| f(tf.as_file_mut())).and_then(|res| res)
|
||||
}
|
||||
/// Close the lock file to prevent further writes and to save system resources.
|
||||
/// A call to [`Marker::commit()`] is allowed on the [`Marker`] to write changes back to the resource.
|
||||
pub fn close(self) -> std::io::Result<Marker> {
|
||||
Ok(Marker {
|
||||
inner: self.inner.close()?,
|
||||
created_from_file: true,
|
||||
lock_path: self.lock_path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the path at which the lock file resides
|
||||
pub fn lock_path(&self) -> &Path {
|
||||
&self.lock_path
|
||||
}
|
||||
|
||||
/// Return the path at which the locked resource resides
|
||||
pub fn resource_path(&self) -> PathBuf {
|
||||
strip_lock_suffix(&self.lock_path)
|
||||
}
|
||||
}
|
||||
|
||||
mod io_impls {
|
||||
use std::{io, io::SeekFrom};
|
||||
|
||||
use super::File;
|
||||
|
||||
impl io::Write for File {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.inner.with_mut(|f| f.write(buf))?
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.inner.with_mut(io::Write::flush)?
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Seek for File {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
self.inner.with_mut(|f| f.seek(pos))?
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Read for File {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.inner.with_mut(|f| f.read(buf))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Marker {
|
||||
/// Return the path at which the lock file resides
|
||||
pub fn lock_path(&self) -> &Path {
|
||||
&self.lock_path
|
||||
}
|
||||
|
||||
/// Return the path at which the locked resource resides
|
||||
pub fn resource_path(&self) -> PathBuf {
|
||||
strip_lock_suffix(&self.lock_path)
|
||||
}
|
||||
}
|
||||
56
src-lock/src/lib.rs
Normal file
56
src-lock/src/lib.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
//! git-style registered lock files to make altering resources atomic.
|
||||
//!
|
||||
//! In this model, reads are always atomic and can be performed directly while writes are facilitated by the locking mechanism
|
||||
//! implemented here. Locks are acquired atomically, then written to, to finally atomically overwrite the actual resource.
|
||||
//!
|
||||
//! Lock files are wrapped [`src-tempfile`](gix_tempfile)-handles and add the following:
|
||||
//!
|
||||
//! * consistent naming of lock files
|
||||
//! * block the thread (with timeout) or fail immediately if a lock cannot be obtained right away
|
||||
//! * commit lock files to atomically put them into the location of the originally locked file
|
||||
//!
|
||||
//! # Limitations
|
||||
//!
|
||||
//! * [All limitations of `src-tempfile`](gix_tempfile) apply. **A highlight of such a limitation is resource leakage
|
||||
//! which results in them being permanently locked unless there is user-intervention.**
|
||||
//! * As the lock file is separate from the actual resource, locking is merely a convention rather than being enforced.
|
||||
#![deny(missing_docs, rust_2018_idioms, unsafe_code)]
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub use gix_tempfile as tempfile;
|
||||
use gix_tempfile::handle::{Closed, Writable};
|
||||
|
||||
const DOT_LOCK_SUFFIX: &str = ".lock";
|
||||
|
||||
///
|
||||
pub mod acquire;
|
||||
|
||||
pub use gix_utils::backoff;
|
||||
///
|
||||
pub mod commit;
|
||||
|
||||
/// Locks a resource to eventually be overwritten with the content of this file.
|
||||
///
|
||||
/// Dropping the file without [committing][File::commit] will delete it, leaving the underlying resource unchanged.
|
||||
#[must_use = "A File that is immediately dropped doesn't allow resource updates"]
|
||||
#[derive(Debug)]
|
||||
pub struct File {
|
||||
inner: gix_tempfile::Handle<Writable>,
|
||||
lock_path: PathBuf,
|
||||
}
|
||||
|
||||
/// Locks a resource to allow related resources to be updated using [files][File].
|
||||
///
|
||||
/// As opposed to the [File] type this one won't keep the tempfile open for writing and thus consumes no
|
||||
/// system resources, nor can it be persisted.
|
||||
#[must_use = "A Marker that is immediately dropped doesn't lock a resource meaningfully"]
|
||||
#[derive(Debug)]
|
||||
pub struct Marker {
|
||||
inner: gix_tempfile::Handle<Closed>,
|
||||
created_from_file: bool,
|
||||
lock_path: PathBuf,
|
||||
}
|
||||
|
||||
///
|
||||
pub mod file;
|
||||
3
src-lock/tests/all.rs
Normal file
3
src-lock/tests/all.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
mod lock;
|
||||
155
src-lock/tests/lock/file.rs
Normal file
155
src-lock/tests/lock/file.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
mod close {
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use gix_lock::acquire::Fail;
|
||||
|
||||
#[test]
|
||||
fn acquire_close_commit_to_existing_file() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("resource-existing.ext");
|
||||
std::fs::write(&resource, b"old state")?;
|
||||
let resource_lock = resource.with_extension("ext.lock");
|
||||
let mut file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None)?;
|
||||
assert!(resource_lock.is_file());
|
||||
file.with_mut(|out| out.write_all(b"hello world"))?;
|
||||
let mark = file.close()?;
|
||||
assert_eq!(mark.lock_path(), resource_lock);
|
||||
assert_eq!(mark.resource_path(), resource);
|
||||
assert_eq!(mark.commit()?, resource, "returned and initial resource path match");
|
||||
assert_eq!(
|
||||
std::fs::read(resource)?,
|
||||
&b"hello world"[..],
|
||||
"it created the resource and wrote the data"
|
||||
);
|
||||
assert!(!resource_lock.is_file());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod commit {
|
||||
use gix_lock::acquire::Fail;
|
||||
|
||||
#[test]
|
||||
fn failure_to_commit_does_return_a_registered_marker() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("resource-existing.ext");
|
||||
std::fs::create_dir(&resource)?;
|
||||
let mark = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?;
|
||||
let lock_path = mark.lock_path().to_owned();
|
||||
assert!(lock_path.is_file(), "the lock is placed");
|
||||
|
||||
let err = mark
|
||||
.commit()
|
||||
.expect_err("cannot commit onto existing directory, empty or not");
|
||||
assert!(err.instance.lock_path().is_file(), "the lock is still present");
|
||||
|
||||
drop(err);
|
||||
assert!(
|
||||
!lock_path.is_file(),
|
||||
"the lock file is still owned by the lock instance (and ideally still registered, but hard to test)"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failure_to_commit_does_return_a_registered_file() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("resource-existing.ext");
|
||||
std::fs::create_dir(&resource)?;
|
||||
let file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None)?;
|
||||
let lock_path = file.lock_path().to_owned();
|
||||
assert!(lock_path.is_file(), "the lock is placed");
|
||||
|
||||
let err = file
|
||||
.commit()
|
||||
.expect_err("cannot commit onto existing directory, empty or not");
|
||||
assert!(err.instance.lock_path().is_file(), "the lock is still present");
|
||||
std::fs::remove_dir(resource)?;
|
||||
let (resource, open_file) = err.instance.commit()?;
|
||||
let mut open_file = open_file.expect("file to be present as no interrupt has messed with us");
|
||||
|
||||
assert!(
|
||||
!lock_path.is_file(),
|
||||
"the lock was moved into place, now it's the resource"
|
||||
);
|
||||
|
||||
use std::io::Write;
|
||||
write!(open_file, "hello")?;
|
||||
drop(open_file);
|
||||
assert_eq!(
|
||||
std::fs::read(resource)?,
|
||||
b"hello".to_vec(),
|
||||
"and committing returned a writable file handle"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod acquire {
|
||||
use std::io::{ErrorKind, Write};
|
||||
|
||||
use gix_lock::acquire;
|
||||
|
||||
fn fail_immediately() -> gix_lock::acquire::Fail {
|
||||
acquire::Fail::Immediately
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_create_dir_write_commit() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("a").join("resource-nonexisting");
|
||||
let resource_lock = resource.with_extension("lock");
|
||||
let mut file =
|
||||
gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), Some(dir.path().into()))?;
|
||||
assert_eq!(file.lock_path(), resource_lock);
|
||||
assert_eq!(file.resource_path(), resource);
|
||||
assert!(resource_lock.is_file());
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let perms = resource_lock.metadata()?.permissions();
|
||||
assert_ne!(
|
||||
perms.mode() & !0o170000,
|
||||
0o600,
|
||||
"mode is more permissive now, even after passing the umask"
|
||||
);
|
||||
}
|
||||
file.with_mut(|out| out.write_all(b"hello world"))?;
|
||||
assert_eq!(file.commit()?.0, resource, "returned and computed resource path match");
|
||||
assert_eq!(
|
||||
std::fs::read(resource)?,
|
||||
&b"hello world"[..],
|
||||
"it created the resource and wrote the data"
|
||||
);
|
||||
assert!(!resource_lock.is_file());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_write_drop() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("resource-nonexisting.ext");
|
||||
{
|
||||
let mut file = gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), None)?;
|
||||
file.with_mut(|out| out.write_all(b"probably we will be interrupted"))?;
|
||||
}
|
||||
assert!(!resource.is_file(), "the file wasn't created");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_non_existing_dir_fails() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("a").join("resource.ext");
|
||||
let res = gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), None);
|
||||
assert!(matches!(res, Err(acquire::Error::Io(err)) if err.kind() == ErrorKind::NotFound));
|
||||
assert!(dir.path().is_dir(), "it won't meddle with the containing directory");
|
||||
assert!(!resource.is_file(), "the resource is not created");
|
||||
assert!(
|
||||
!resource.parent().unwrap().is_dir(),
|
||||
"parent dire wasn't created either"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
92
src-lock/tests/lock/marker.rs
Normal file
92
src-lock/tests/lock/marker.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
mod acquire {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gix_lock::acquire::Fail;
|
||||
|
||||
#[test]
|
||||
fn fail_mode_immediately_produces_a_descriptive_error() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("the-resource");
|
||||
let guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?;
|
||||
assert!(guard.lock_path().ends_with("the-resource.lock"));
|
||||
assert!(guard.resource_path().ends_with("the-resource"));
|
||||
let err_str = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None)
|
||||
.expect_err("the lock is taken and there is a failure obtaining it again")
|
||||
.to_string();
|
||||
|
||||
assert!(err_str.contains("the-resource' could not be obtained immediately"));
|
||||
assert!(err_str.contains("the-resource.lock"), "it mentions the lockfile itself");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_mode_after_duration_fails_after_a_given_duration_or_more() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("the-resource");
|
||||
let _guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)?;
|
||||
let start = Instant::now();
|
||||
let time_to_wait = Duration::from_millis(50);
|
||||
let err_str =
|
||||
gix_lock::Marker::acquire_to_hold_resource(resource, Fail::AfterDurationWithBackoff(time_to_wait), None)
|
||||
.expect_err("the lock is taken and there is a failure obtaining it again after some delay")
|
||||
.to_string();
|
||||
assert!(
|
||||
start.elapsed() >= time_to_wait,
|
||||
"it should never wait less than the given wait time"
|
||||
);
|
||||
assert!(
|
||||
err_str.contains("could not be obtained after 0.05s"),
|
||||
"it lets us know that we were waiting for some time"
|
||||
);
|
||||
assert!(err_str.contains("the-resource.lock"), "it mentions the lockfile itself");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
mod commit {
|
||||
use gix_lock::acquire::Fail;
|
||||
|
||||
#[test]
|
||||
fn failure_to_commit_does_return_a_registered_marker() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let resource = dir.path().join("the-resource");
|
||||
let file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None).unwrap();
|
||||
let mark = file.close().unwrap();
|
||||
let resource_lock_path = mark.lock_path().to_owned();
|
||||
|
||||
std::fs::create_dir(&resource).unwrap();
|
||||
let err = mark.commit().expect_err("it fails as the resource path is a directory");
|
||||
assert!(
|
||||
resource_lock_path.is_file(),
|
||||
"the underlying lock wasn't consumed after all"
|
||||
);
|
||||
drop(err);
|
||||
assert!(
|
||||
!resource_lock_path.is_file(),
|
||||
"and is linked to the err which makes the lock recoverable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_for_ordinary_marker_that_was_never_writable() -> crate::Result {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let resource = dir.path().join("the-resource");
|
||||
let mark = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let perms = mark.lock_path().metadata()?.permissions();
|
||||
assert_ne!(
|
||||
perms.mode() & !0o170000,
|
||||
0o600,
|
||||
"mode is more permissive now, even after passing the umask"
|
||||
);
|
||||
}
|
||||
let err = mark.commit().expect_err("should always fail");
|
||||
assert_eq!(err.error.kind(), std::io::ErrorKind::Other);
|
||||
assert_eq!(
|
||||
err.error.get_ref().expect("custom error").to_string(),
|
||||
"refusing to commit marker that was never opened"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
2
src-lock/tests/lock/mod.rs
Normal file
2
src-lock/tests/lock/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod file;
|
||||
mod marker;
|
||||
Reference in New Issue
Block a user