use std::collections::BTreeMap;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LeaseBackup {
Bytes {
path: PathBuf,
sha256: String,
size: u64,
},
DriveCopy {
file_id: String,
},
}
impl LeaseBackup {
pub(crate) fn audit_fields(&self) -> (Option<String>, Option<String>, Option<u64>) {
match self {
Self::Bytes { path, sha256, size } => (
Some(path.display().to_string()),
Some(sha256.clone()),
Some(*size),
),
Self::DriveCopy { file_id } => (Some(file_id.clone()), None, None),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct LeaseRecord {
pub(crate) token: String,
pub(crate) file_id: String,
pub(crate) version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) modified_time: Option<String>,
pub(crate) backup: LeaseBackup,
pub(crate) acquired_at: DateTime<Utc>,
pub(crate) expires_at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) released_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) superseded_by: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) restored_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) restored_sheet_id: Option<i64>,
}
impl LeaseRecord {
pub(crate) fn is_live(&self, now: DateTime<Utc>) -> bool {
self.released_at.is_none() && now < self.expires_at
}
}
#[derive(Debug, Default)]
pub(crate) struct LeaseLedger(BTreeMap<String, LeaseRecord>);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ReleaseOutcome {
Released {
file_id: String,
expires_at: DateTime<Utc>,
},
NotLive {
file_id: String,
expires_at: DateTime<Utc>,
released_at: Option<DateTime<Utc>>,
},
NotFound,
}
impl LeaseLedger {
pub(crate) fn load(path: &Path) -> Result<Self> {
let text = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Self::default()),
Err(e) => {
return Err(e)
.with_context(|| format!("Failed to read lease ledger at {}", path.display()))
}
};
let mut map = BTreeMap::new();
for (i, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let record: LeaseRecord = serde_json::from_str(line).with_context(|| {
format!(
"Failed to parse lease ledger line {} at {} — this ledger is the sole \
record of which lease tokens are still valid, against what version, and \
which backups they point at. Fix the line or restore the ledger from a \
backup before running any leased write",
i + 1,
path.display()
)
})?;
map.insert(record.token.clone(), record);
}
Ok(Self(map))
}
pub(crate) fn get(&self, token: &str) -> Option<&LeaseRecord> {
self.0.get(token)
}
pub(crate) fn insert(&mut self, record: LeaseRecord) {
self.0.insert(record.token.clone(), record);
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &LeaseRecord> {
self.0.values()
}
pub(crate) fn remove(&mut self, token: &str) -> Option<LeaseRecord> {
self.0.remove(token)
}
pub(crate) fn live_lease_for_file(
&self,
file_id: &str,
now: DateTime<Utc>,
exclude: Option<&str>,
) -> Option<&LeaseRecord> {
self.0.values().find(|record| {
record.file_id == file_id
&& record.is_live(now)
&& Some(record.token.as_str()) != exclude
})
}
pub(crate) fn release(
&mut self,
token: &str,
at: DateTime<Utc>,
superseded_by: Option<&str>,
) -> ReleaseOutcome {
let Some(rec) = self.0.get_mut(token) else {
return ReleaseOutcome::NotFound;
};
if !rec.is_live(at) {
return ReleaseOutcome::NotLive {
file_id: rec.file_id.clone(),
expires_at: rec.expires_at,
released_at: rec.released_at,
};
}
rec.released_at = Some(at);
rec.superseded_by = superseded_by.map(str::to_string);
ReleaseOutcome::Released {
file_id: rec.file_id.clone(),
expires_at: rec.expires_at,
}
}
pub(crate) fn mark_restored(
&mut self,
token: &str,
at: DateTime<Utc>,
restored_sheet_id: Option<i64>,
) {
if let Some(rec) = self.0.get_mut(token) {
rec.restored_at = Some(at);
rec.restored_sheet_id = restored_sheet_id;
}
}
pub(crate) fn record_write(
&mut self,
token: &str,
version: String,
modified_time: Option<String>,
) {
if let Some(rec) = self.0.get_mut(token) {
rec.version = version;
rec.modified_time = modified_time;
}
}
pub(crate) fn mutate<R>(
_lock: &LedgerLock,
path: &Path,
f: impl FnOnce(&mut Self) -> R,
) -> Result<R> {
let mut ledger = Self::load(path)?;
let result = f(&mut ledger);
ledger.save(path)?;
Ok(result)
}
#[cfg(test)]
pub(crate) fn mutate_locked<R>(path: &Path, f: impl FnOnce(&mut Self) -> R) -> Result<R> {
let lock = LedgerLock::acquire(path)?;
Self::mutate(&lock, path, f)
}
pub(crate) fn save(&self, path: &Path) -> Result<()> {
crate::daemon::paths::ensure_parent_dir_0700(path)?;
let dir = path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let mut tmp = tempfile::NamedTempFile::new_in(dir)
.with_context(|| format!("Failed to create a temp file in {}", dir.display()))?;
for record in self.0.values() {
serde_json::to_writer(&mut tmp, record)
.context("Failed to serialise a lease ledger record")?;
tmp.write_all(b"\n")
.context("Failed to write a lease ledger record")?;
}
tmp.flush().context("Failed to flush the lease ledger")?;
tmp.persist(path)
.map_err(|e| e.error)
.with_context(|| format!("Failed to publish lease ledger to {}", path.display()))?;
Ok(())
}
}
pub(crate) fn ledger_path() -> Result<PathBuf> {
crate::request_log::omni_dev_state_subpath("lease-ledger.jsonl")
.context("could not resolve the state/data directory for the lease ledger")
}
fn lock_path_for(ledger_path: &Path) -> PathBuf {
let mut name = ledger_path.as_os_str().to_owned();
name.push(".lock");
PathBuf::from(name)
}
const LEASE_LOCK_WAIT_ENV_VAR: &str = "OMNI_DEV_LEASE_LOCK_WAIT_SECS";
pub(super) fn default_lock_wait_timeout() -> Duration {
let raw = crate::utils::settings::get_env_var(LEASE_LOCK_WAIT_ENV_VAR).ok();
crate::utils::http::duration_from_secs(raw, crate::utils::http::read_timeout() * 4)
}
const INITIAL_BACKOFF: Duration = Duration::from_millis(50);
const MAX_BACKOFF: Duration = Duration::from_millis(500);
struct LockWait<'a> {
path: &'a Path,
start: Instant,
delay: Duration,
max_wait: Duration,
announced: bool,
}
impl<'a> LockWait<'a> {
fn new(path: &'a Path, max_wait: Duration) -> Self {
Self {
path,
start: Instant::now(),
delay: INITIAL_BACKOFF,
max_wait,
announced: false,
}
}
fn next_delay(&mut self) -> Result<Duration> {
if !self.announced {
tracing::warn!(
"drive lease: waiting for another `drive lease` operation to finish ({} is \
locked)",
self.path.display()
);
self.announced = true;
}
if self.start.elapsed() >= self.max_wait {
anyhow::bail!(
"timed out after {:?} waiting for another `drive lease` operation to finish ({} \
is locked) — concurrent access would clobber the ledger",
self.max_wait,
self.path.display()
);
}
let delay = self.delay;
self.delay = (self.delay * 2).min(MAX_BACKOFF);
Ok(delay)
}
}
#[derive(Debug)]
pub(crate) struct LedgerLock {
#[allow(dead_code)] inner: crate::daemon::paths::FileLock,
}
impl LedgerLock {
pub(crate) fn acquire(ledger_path: &Path) -> Result<Self> {
let path = lock_path_for(ledger_path);
crate::daemon::paths::ensure_parent_dir_0700(&path)?;
match Self::try_acquire_once(&path)? {
Some(lock) => Ok(lock),
None => anyhow::bail!(
"another `drive lease` operation appears to already be in progress ({} is \
locked) — concurrent access would clobber the ledger",
path.display()
),
}
}
fn try_acquire_once(path: &Path) -> Result<Option<Self>> {
Ok(
crate::daemon::paths::try_lock_or_busy(path, "the lease lock file")?
.map(|inner| Self { inner }),
)
}
pub(crate) async fn acquire_waiting(ledger_path: &Path) -> Result<Self> {
Self::acquire_waiting_with_timeout(ledger_path, default_lock_wait_timeout()).await
}
pub(crate) async fn acquire_waiting_with_timeout(
ledger_path: &Path,
max_wait: Duration,
) -> Result<Self> {
let path = lock_path_for(ledger_path);
crate::daemon::paths::ensure_parent_dir_0700(&path)?;
let mut wait = LockWait::new(&path, max_wait);
loop {
let attempt_path = path.clone();
let attempt =
tokio::task::spawn_blocking(move || Self::try_acquire_once(&attempt_path))
.await
.context("the ledger lock attempt did not complete")??;
match attempt {
Some(lock) => return Ok(lock),
None => tokio::time::sleep(wait.next_delay()?).await,
}
}
}
pub(crate) fn acquire_waiting_blocking_with_timeout(
ledger_path: &Path,
max_wait: Duration,
) -> Result<Self> {
let path = lock_path_for(ledger_path);
crate::daemon::paths::ensure_parent_dir_0700(&path)?;
let mut wait = LockWait::new(&path, max_wait);
loop {
match Self::try_acquire_once(&path)? {
Some(lock) => return Ok(lock),
None => std::thread::sleep(wait.next_delay()?),
}
}
}
}
pub(crate) fn offload_short_blocking_io<R>(f: impl FnOnce() -> R) -> R {
use tokio::runtime::{Handle, RuntimeFlavor};
match Handle::try_current().map(|handle| handle.runtime_flavor()) {
Ok(RuntimeFlavor::MultiThread) => tokio::task::block_in_place(f),
_ => f(),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use chrono::Duration as ChronoDuration;
fn sample_record(token: &str) -> LeaseRecord {
LeaseRecord {
token: token.to_string(),
file_id: "file1".to_string(),
version: "1".to_string(),
modified_time: Some("2026-09-11T00:00:00Z".to_string()),
backup: LeaseBackup::Bytes {
path: PathBuf::from("/tmp/backup1"),
sha256: "abc123".to_string(),
size: 42,
},
acquired_at: Utc::now(),
expires_at: Utc::now() + ChronoDuration::minutes(30),
released_at: None,
superseded_by: None,
restored_at: None,
restored_sheet_id: None,
}
}
#[test]
fn lease_backup_drive_copy_round_trips_through_json() {
let backup = LeaseBackup::DriveCopy {
file_id: "copy-1".to_string(),
};
let json = serde_json::to_string(&backup).unwrap();
assert!(json.contains("\"kind\":\"drive_copy\""), "{json}");
let back: LeaseBackup = serde_json::from_str(&json).unwrap();
assert_eq!(back, backup);
}
#[test]
fn is_live_true_before_expiry_and_unreleased() {
let rec = sample_record("t1");
assert!(rec.is_live(Utc::now()));
}
#[test]
fn is_live_false_after_expiry() {
let rec = sample_record("t1");
assert!(!rec.is_live(Utc::now() + ChronoDuration::hours(1)));
}
#[test]
fn is_live_false_once_released() {
let mut rec = sample_record("t1");
rec.released_at = Some(Utc::now());
assert!(!rec.is_live(Utc::now()));
}
#[test]
fn release_with_no_superseder_leaves_superseded_by_unset() {
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
let outcome = ledger.release("t1", Utc::now(), None);
assert!(matches!(outcome, ReleaseOutcome::Released { .. }));
assert_eq!(ledger.get("t1").unwrap().superseded_by, None);
}
#[test]
fn release_with_a_superseder_stamps_superseded_by() {
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
let outcome = ledger.release("t1", Utc::now(), Some("fresh-token"));
assert!(matches!(outcome, ReleaseOutcome::Released { .. }));
assert_eq!(
ledger.get("t1").unwrap().superseded_by.as_deref(),
Some("fresh-token")
);
}
#[test]
fn releasing_an_already_dead_row_never_stamps_superseded_by() {
let mut rec = sample_record("t1");
rec.expires_at = Utc::now() - ChronoDuration::hours(1);
let mut ledger = LeaseLedger::default();
ledger.insert(rec);
let outcome = ledger.release("t1", Utc::now(), Some("fresh-token"));
assert!(matches!(outcome, ReleaseOutcome::NotLive { .. }));
assert_eq!(
ledger.get("t1").unwrap().superseded_by,
None,
"an already-dead row must be left exactly as it was, not stamped"
);
}
#[test]
fn load_absent_file_is_an_empty_ledger() {
let dir = tempfile::tempdir().unwrap();
let ledger = LeaseLedger::load(&dir.path().join("nope.jsonl")).unwrap();
assert!(ledger.get("t1").is_none());
}
#[test]
fn load_surfaces_a_non_not_found_read_error() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
std::fs::create_dir(&path).unwrap();
let err = LeaseLedger::load(&path).unwrap_err();
assert!(err.to_string().contains("Failed to read lease ledger"));
}
#[test]
fn load_rejects_a_corrupt_line() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
std::fs::write(&path, "not json\n").unwrap();
assert!(LeaseLedger::load(&path).is_err());
}
#[test]
fn load_skips_blank_lines() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
std::fs::write(&path, "\n\n").unwrap();
let ledger = LeaseLedger::load(&path).unwrap();
assert!(ledger.get("t1").is_none());
}
#[test]
fn save_then_load_round_trips_a_record() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
ledger.save(&path).unwrap();
let reloaded = LeaseLedger::load(&path).unwrap();
let rec = reloaded.get("t1").unwrap();
assert_eq!(rec.file_id, "file1");
assert_eq!(rec.version, "1");
}
#[test]
fn record_write_updates_version_and_modified_time() {
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
ledger.record_write("t1", "2".to_string(), Some("later".to_string()));
let rec = ledger.get("t1").unwrap();
assert_eq!(rec.version, "2");
assert_eq!(rec.modified_time.as_deref(), Some("later"));
}
#[test]
fn record_write_on_an_absent_token_is_a_noop() {
let mut ledger = LeaseLedger::default();
ledger.record_write("missing", "2".to_string(), None);
assert!(ledger.get("missing").is_none());
}
#[test]
fn iter_yields_every_row() {
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
ledger.insert(sample_record("t2"));
let tokens: Vec<&str> = ledger.iter().map(|r| r.token.as_str()).collect();
assert_eq!(tokens.len(), 2);
assert!(tokens.contains(&"t1"));
assert!(tokens.contains(&"t2"));
}
#[test]
fn remove_drops_a_present_row_and_returns_it() {
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
let removed = ledger.remove("t1").unwrap();
assert_eq!(removed.token, "t1");
assert!(ledger.get("t1").is_none());
}
#[test]
fn remove_on_an_absent_token_is_a_noop() {
let mut ledger = LeaseLedger::default();
assert!(ledger.remove("missing").is_none());
}
#[test]
fn expired_row_is_kept_on_save_not_dropped() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
let mut rec = sample_record("t1");
rec.expires_at = Utc::now() - ChronoDuration::hours(1);
let mut ledger = LeaseLedger::default();
ledger.insert(rec);
ledger.save(&path).unwrap();
let reloaded = LeaseLedger::load(&path).unwrap();
assert!(reloaded.get("t1").is_some());
}
#[test]
fn save_creates_a_missing_parent_directory() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("nested").join("lease-ledger.jsonl");
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
ledger.save(&path).unwrap();
assert!(path.exists());
let reloaded = LeaseLedger::load(&path).unwrap();
assert!(reloaded.get("t1").is_some());
}
#[test]
fn save_is_atomic_and_leaves_no_temp_file_behind() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
ledger.save(&path).unwrap();
let entries: Vec<_> = std::fs::read_dir(dir.path())
.unwrap()
.map(|e| e.unwrap().file_name())
.collect();
assert_eq!(
entries,
vec![std::ffi::OsString::from("lease-ledger.jsonl")]
);
}
#[test]
fn ledger_lock_acquire_creates_a_missing_parent_directory() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("nested").join("lease-ledger.jsonl");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
assert!(dir
.path()
.join("nested")
.join("lease-ledger.jsonl.lock")
.exists());
drop(lock);
}
#[test]
fn ledger_lock_acquire_derives_its_path_from_the_ledger_path_and_refuses_a_second_caller() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
let first = LedgerLock::acquire(&ledger_path).unwrap();
assert!(dir.path().join("lease-ledger.jsonl.lock").exists());
assert!(LedgerLock::acquire(&ledger_path).is_err());
drop(first);
assert!(LedgerLock::acquire(&ledger_path).is_ok());
}
#[cfg(unix)]
#[test]
fn ledger_lock_acquire_reports_a_non_collision_failure_distinctly() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o500)).unwrap();
let err = LedgerLock::acquire(&ledger_path).unwrap_err();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o700)).unwrap();
assert!(err.to_string().contains("failed to lock"), "{err:?}");
assert!(
!err.to_string().contains("already be in progress"),
"{err:?}"
);
}
#[test]
fn mutate_loads_applies_and_saves() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
let mut ledger = LeaseLedger::default();
ledger.insert(sample_record("t1"));
ledger.save(&path).unwrap();
let lock = LedgerLock::acquire(&path).unwrap();
let returned = LeaseLedger::mutate(&lock, &path, |ledger| {
ledger.mark_restored("t1", Utc::now(), Some(999));
"ok"
})
.unwrap();
assert_eq!(returned, "ok");
let reloaded = LeaseLedger::load(&path).unwrap();
let record = reloaded.get("t1").unwrap();
assert!(record.restored_at.is_some());
assert_eq!(
record.restored_sheet_id,
Some(999),
"the restored sheet's live id must survive the save/load round-trip — it is what \
a later restore reads back to refuse a duplicate (#1689)"
);
}
#[test]
fn a_ledger_line_predating_restored_sheet_id_still_parses() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
let mut without = serde_json::to_value(sample_record("t1")).unwrap();
without.as_object_mut().unwrap().remove("restored_sheet_id");
std::fs::write(&path, format!("{without}\n")).unwrap();
let record = LeaseLedger::load(&path).unwrap();
assert_eq!(record.get("t1").unwrap().restored_sheet_id, None);
}
#[test]
fn a_ledger_line_predating_superseded_by_still_parses() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
let mut without = serde_json::to_value(sample_record("t1")).unwrap();
without.as_object_mut().unwrap().remove("superseded_by");
std::fs::write(&path, format!("{without}\n")).unwrap();
let record = LeaseLedger::load(&path).unwrap();
assert_eq!(record.get("t1").unwrap().superseded_by, None);
}
#[test]
fn mutate_locked_acquires_and_releases_its_own_lock() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
LeaseLedger::mutate_locked(&path, |ledger| {
ledger.insert(sample_record("t1"));
})
.unwrap();
assert!(LedgerLock::acquire(&path).is_ok());
let reloaded = LeaseLedger::load(&path).unwrap();
assert!(reloaded.get("t1").is_some());
}
#[test]
fn mutate_locked_refuses_while_another_lock_is_held() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("lease-ledger.jsonl");
let _held = LedgerLock::acquire(&path).unwrap();
let err = LeaseLedger::mutate_locked(&path, |ledger| {
ledger.insert(sample_record("t1"));
})
.unwrap_err();
assert!(err.to_string().contains("already be in progress"));
}
#[cfg(unix)]
#[test]
fn ledger_lock_file_persists_after_drop_and_is_re_lockable() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
let lock_path = dir.path().join("lease-ledger.jsonl.lock");
let lock = LedgerLock::acquire(&ledger_path).unwrap();
assert!(lock_path.exists());
drop(lock);
assert!(
lock_path.exists(),
"the lock file must survive its own drop"
);
LedgerLock::acquire(&ledger_path).unwrap();
}
#[cfg(unix)]
#[test]
fn ledger_lock_is_released_by_plain_fd_close_with_no_explicit_unlock() {
use std::os::fd::AsRawFd;
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
let lock_path = dir.path().join("lease-ledger.jsonl.lock");
let file = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(&lock_path)
.unwrap();
#[allow(deprecated)]
nix::fcntl::flock(file.as_raw_fd(), nix::fcntl::FlockArg::LockExclusive).unwrap();
drop(file);
LedgerLock::acquire(&ledger_path).unwrap();
}
#[tokio::test]
async fn ledger_lock_acquire_waiting_waits_for_a_concurrent_holder_then_succeeds() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
let held = LedgerLock::acquire(&ledger_path).unwrap();
let releaser = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
drop(held);
});
LedgerLock::acquire_waiting_with_timeout(&ledger_path, Duration::from_secs(5))
.await
.unwrap();
releaser.join().unwrap();
}
#[tokio::test]
async fn ledger_lock_acquire_waiting_times_out_when_never_released() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
let _held = LedgerLock::acquire(&ledger_path).unwrap();
let err =
LedgerLock::acquire_waiting_with_timeout(&ledger_path, Duration::from_millis(150))
.await
.unwrap_err();
assert!(err.to_string().contains("timed out"), "{err:?}");
}
#[test]
fn the_lock_wait_loop_never_offloads_to_a_runtime_worker() {
let source = include_str!("ledger.rs");
let start = source
.find("pub(crate) async fn acquire_waiting_with_timeout(")
.expect("acquire_waiting_with_timeout signature not found");
let end = source[start..]
.find("/// [`Self::acquire_waiting_with_timeout`]'s synchronous twin")
.expect("acquire_waiting_blocking_with_timeout doc comment not found")
+ start;
let wait_loop = &source[start..end];
assert!(
wait_loop.contains("spawn_blocking"),
"the sliced span is not the wait loop — re-anchor this guard:\n{wait_loop}"
);
for forbidden in ["block_in_place", "offload_short_blocking_io"] {
assert!(
!wait_loop.contains(forbidden),
"the ledger-lock wait loop must not hand a runtime worker's core away for the \
whole wait — it found `{forbidden}`:\n{wait_loop}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn offload_short_blocking_io_runs_the_closure_on_a_runtime_worker() {
let (outer, inner, ran_on) = tokio::spawn(async {
let outer = std::thread::current().id();
let (inner, ran_on) = offload_short_blocking_io(|| {
let inner = std::thread::current().id();
(
inner,
offload_short_blocking_io(|| std::thread::current().id()),
)
});
(outer, inner, ran_on)
})
.await
.unwrap();
assert_eq!(inner, outer);
assert_eq!(ran_on, outer);
}
#[tokio::test]
async fn offload_short_blocking_io_does_not_panic_on_a_current_thread_runtime() {
assert!(offload_short_blocking_io(|| true));
}
#[test]
fn offload_short_blocking_io_runs_the_closure_outside_any_runtime() {
assert!(offload_short_blocking_io(|| true));
}
#[test]
fn ledger_lock_acquire_waiting_blocking_waits_for_a_concurrent_holder_then_succeeds() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
let held = LedgerLock::acquire(&ledger_path).unwrap();
let releaser = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(100));
drop(held);
});
LedgerLock::acquire_waiting_blocking_with_timeout(&ledger_path, Duration::from_secs(5))
.unwrap();
releaser.join().unwrap();
}
#[test]
fn ledger_lock_acquire_waiting_blocking_times_out_when_never_released() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
let _held = LedgerLock::acquire(&ledger_path).unwrap();
let err = LedgerLock::acquire_waiting_blocking_with_timeout(
&ledger_path,
Duration::from_millis(150),
)
.unwrap_err();
assert!(err.to_string().contains("timed out"), "{err:?}");
}
#[test]
fn ledger_lock_acquire_waiting_blocking_does_not_retry_an_io_failure() {
let dir = tempfile::tempdir().unwrap();
let ledger_path = dir.path().join("lease-ledger.jsonl");
std::fs::create_dir(lock_path_for(&ledger_path)).unwrap();
let start = Instant::now();
let err = LedgerLock::acquire_waiting_blocking_with_timeout(
&ledger_path,
Duration::from_secs(30),
)
.unwrap_err();
assert!(err.to_string().contains("failed to lock"), "{err:?}");
assert!(
start.elapsed() < Duration::from_secs(5),
"an I/O failure was retried"
);
}
}