Skip to content
Closed
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gix-lock/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ test = true
[dependencies]
gix-utils = { version = "^0.3.3", default-features = false, path = "../gix-utils" }
gix-tempfile = { version = "^23.0.0", default-features = false, path = "../gix-tempfile" }
thiserror = "2.0.18"
gix-error = { version = "^0.2.4", path = "../gix-error" }

[dev-dependencies]
tempfile = "3.26.0"
56 changes: 43 additions & 13 deletions gix-lock/src/acquire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::{
time::Duration,
};

use gix_error::ErrorExt;
use gix_tempfile::{AutoRemove, ContainingDirectory};

use crate::{DOT_LOCK_SUFFIX, File, Marker, backoff};
Expand Down Expand Up @@ -40,23 +41,51 @@ impl From<Duration> for Fail {
}
}

/// The error returned when acquiring a [`File`] or [`Marker`].
#[derive(Debug, thiserror::Error)]
/// The failure that occurred when acquiring a [`File`] or [`Marker`].
///
/// It's a concrete type to let callers tell actual lock contention apart from
/// other IO errors, like path collisions between a lock file and a directory.
#[derive(Debug)]
#[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
)]
pub enum Failure {
Io(std::io::Error),
PermanentlyLocked {
resource_path: PathBuf,
mode: Fail,
attempts: usize,
},
}

impl fmt::Display for Failure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Failure::Io(_) => f.write_str("Another IO error occurred while obtaining the lock"),
Failure::PermanentlyLocked {
resource_path,
mode,
attempts,
} => write!(
f,
"The lock for resource '{resource_path}' could not be obtained {mode} after {attempts} attempt(s). The lockfile at '{resource_path}{suffix}' might need manual deletion.",
resource_path = resource_path.display(),
suffix = DOT_LOCK_SUFFIX
),
}
}
}

impl std::error::Error for Failure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Failure::Io(err) => Some(err),
Failure::PermanentlyLocked { .. } => None,
}
}
}

/// The error returned when acquiring a [`File`] or [`Marker`].
pub type Error = gix_error::Exn<Failure>;

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.

P1 Badge Adapt all callers before exposing Exn errors

With gix_lock::acquire::Error now aliased to gix_error::Exn<Failure>, the rest of this commit still has workspace callers that treat it as a normal std::error::Error/From source; I checked the f26 tree with git grep and found examples such as gix-index/src/file/write.rs:10 and gix-protocol/src/fetch/error.rs:21 using #[from] gix_lock::acquire::Error, plus gitoxide-core/src/pack/multi_index.rs:27 using ? into anyhow::Result. Since Exn deliberately does not implement std::error::Error, these crates can no longer build until those call sites are converted like the updated gix-ref paths or this public boundary keeps an error type that implements Error.

Useful? React with 👍 / 👎.


impl File {
/// Create a writable lock file with failure `mode` whose content will eventually overwrite the given resource `at_path`.
///
Expand Down Expand Up @@ -193,20 +222,21 @@ fn lock_with_mode<T>(
std::thread::sleep(wait);
continue;
}
Err(err) => return Err(Error::from(err)),
Err(err) => return Err(Failure::Io(err).raise()),
}
}
try_lock(&lock_path, directory, cleanup)
}
}
.map(|v| (lock_path, v))
.map_err(|err| match err.kind() {
AlreadyExists => Error::PermanentlyLocked {
AlreadyExists => Failure::PermanentlyLocked {
resource_path: resource.into(),
mode,
attempts,
},
_ => Error::Io(err),
}
.raise(),
_ => Failure::Io(err).raise(),
})
}

Expand Down
3 changes: 2 additions & 1 deletion gix-lock/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
//! &resource,
//! gix_lock::acquire::Fail::Immediately,
//! None,
//! )?;
//! )
//! .map_err(|err| err.into_error())?;
//! lock.write_all(b"new = value\n")?;
//! let (resource_path, _) = lock.commit()?;
//!
Expand Down
20 changes: 14 additions & 6 deletions gix-lock/tests/lock/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ mod close {
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)?;
let mut file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None)
.map_err(gix_lock::acquire::Error::into_error)?;
assert!(resource_lock.is_file());
file.with_mut(|out| out.write_all(b"hello world"))?;
let mark = file.close()?;
Expand All @@ -35,7 +36,8 @@ mod commit {
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 mark = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)
.map_err(gix_lock::acquire::Error::into_error)?;
let lock_path = mark.lock_path().to_owned();
assert!(lock_path.is_file(), "the lock is placed");

Expand All @@ -57,7 +59,8 @@ mod commit {
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 file = gix_lock::File::acquire_to_update_resource(&resource, Fail::Immediately, None)
.map_err(gix_lock::acquire::Error::into_error)?;
let lock_path = file.lock_path().to_owned();
assert!(lock_path.is_file(), "the lock is placed");

Expand Down Expand Up @@ -101,7 +104,8 @@ mod acquire {
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()))?;
gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), Some(dir.path().into()))
.map_err(gix_lock::acquire::Error::into_error)?;
assert_eq!(file.lock_path(), resource_lock);
assert_eq!(file.resource_path(), resource);
assert!(resource_lock.is_file());
Expand Down Expand Up @@ -131,7 +135,8 @@ mod acquire {
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)?;
let mut file = gix_lock::File::acquire_to_update_resource(&resource, fail_immediately(), None)
.map_err(gix_lock::acquire::Error::into_error)?;
file.with_mut(|out| out.write_all(b"probably we will be interrupted"))?;
}
assert!(!resource.is_file(), "the file wasn't created");
Expand All @@ -143,7 +148,10 @@ mod acquire {
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!(
matches!(res.map_err(acquire::Error::into_inner), Err(acquire::Failure::Io(err)) if err.kind() == ErrorKind::NotFound),
"the underlying failure is still identifiable after type-erasure"
);
assert!(dir.path().is_dir(), "it won't meddle with the containing directory");
assert!(!resource.is_file(), "the resource is not created");
assert!(
Expand Down
9 changes: 6 additions & 3 deletions gix-lock/tests/lock/marker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ mod acquire {
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)?;
let guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)
.map_err(gix_lock::acquire::Error::into_error)?;
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)
Expand All @@ -23,7 +24,8 @@ mod acquire {
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 _guard = gix_lock::Marker::acquire_to_hold_resource(&resource, Fail::Immediately, None)
.map_err(gix_lock::acquire::Error::into_error)?;
let start = Instant::now();
let time_to_wait = Duration::from_millis(50);
let err_str =
Expand Down Expand Up @@ -70,7 +72,8 @@ mod commit {
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)?;
let mark = gix_lock::Marker::acquire_to_hold_resource(resource, Fail::Immediately, None)
.map_err(gix_lock::acquire::Error::into_error)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
Expand Down
5 changes: 3 additions & 2 deletions gix-ref/src/store/file/packed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ impl file::Store {
&self,
lock_mode: gix_lock::acquire::Fail,
) -> Result<packed::Transaction, transaction::Error> {
let lock = gix_lock::File::acquire_to_update_resource(self.packed_refs_path(), lock_mode, None)?;
let lock = gix_lock::File::acquire_to_update_resource(self.packed_refs_path(), lock_mode, None)
.map_err(|err| transaction::Error::TransactionLock(err.into_inner()))?;
// We 'steal' the possibly existing packed buffer which may safe time if it's already there and fresh.
// If nothing else is happening, nobody will get to see the soon stale buffer either, but if so, they will pay
// for reloading it. That seems preferred over always loading up a new one.
Expand Down Expand Up @@ -72,7 +73,7 @@ pub mod transaction {
#[error("An existing pack couldn't be opened or read when preparing a transaction")]
BufferOpen(#[from] packed::buffer::open::Error),
#[error("The lock for a packed transaction could not be obtained")]
TransactionLock(#[from] gix_lock::acquire::Error),
TransactionLock(#[source] gix_lock::acquire::Failure),
}
}

Expand Down
10 changes: 5 additions & 5 deletions gix-ref/src/store/file/transaction/prepare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ impl Transaction<'_, '_> {
/// burying them in [`Error::LockAcquire`], which is reserved for actual contention.
// This happens for path collisions where `a` is a ref file, and `a/b` is the lock to be created.
fn lock_acquire_error(err: gix_lock::acquire::Error, full_name: &str) -> Error {
match err {
gix_lock::acquire::Error::Io(err) => Error::Io(err),
match err.into_inner() {
gix_lock::acquire::Failure::Io(err) => Error::Io(err),
source => Error::LockAcquire {
source,
full_name: full_name.into(),
Expand Down Expand Up @@ -360,7 +360,7 @@ impl Transaction<'_, '_> {
self.store.precompose_unicode,
self.store.namespace.clone(),
)
.map_err(Error::PackedTransactionAcquire)
.map_err(|err| Error::PackedTransactionAcquire(err.into_inner()))
})
.transpose()?
};
Expand Down Expand Up @@ -480,7 +480,7 @@ mod error {
#[error("The packed ref buffer could not be loaded")]
Packed(#[from] packed::buffer::open::Error),
#[error("The lock for the packed-ref file could not be obtained")]
PackedTransactionAcquire(#[source] gix_lock::acquire::Error),
PackedTransactionAcquire(#[source] gix_lock::acquire::Failure),
#[error("The packed transaction could not be prepared")]
PackedTransactionPrepare(#[from] packed::transaction::prepare::Error),
#[error("The packed ref file could not be parsed")]
Expand All @@ -489,7 +489,7 @@ mod error {
PreprocessingFailed(#[source] std::io::Error),
#[error("A lock could not be obtained for reference {full_name:?}")]
LockAcquire {
source: gix_lock::acquire::Error,
source: gix_lock::acquire::Failure,
full_name: BString,
},
#[error("An IO error occurred while applying an edit")]
Expand Down
3 changes: 2 additions & 1 deletion gix-shallow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
//! &shallow_file,
//! gix_lock::acquire::Fail::Immediately,
//! None,
//! )?;
//! )
//! .map_err(|err| err.into_error())?;
//! gix_shallow::write(lock, Some(shallow), &[gix_shallow::Update::Shallow(second)])?;
//!
//! let ids = gix_shallow::read(&shallow_file)?.unwrap().into_iter().collect::<Vec<_>>();
Expand Down
3 changes: 2 additions & 1 deletion tests/tools/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,8 @@ fn marker_if_needed(
None,
)
})
.transpose()?)
.transpose()
.map_err(gix_lock::acquire::Error::into_error)?)
}

fn force_and_dir(
Expand Down
Loading