use std::fs;
use std::path::Path;
use std::time::{Duration, SystemTime};
use crate::util::parse_rfc3339;
pub(crate) const FAILURE_GRACE_SECS: u64 = 60;
pub(crate) struct PendingIndexMarker {
pub prior_ts: String,
pub created_at: SystemTime,
pub child_pid: Option<u32>,
}
pub(crate) fn read(marker_path: &Path) -> Option<PendingIndexMarker> {
let content = fs::read_to_string(marker_path).ok()?;
let mut lines = content.lines();
let prior_ts = lines.next()?.to_owned();
let created_at = parse_rfc3339(lines.next()?).ok()?;
let child_pid = lines
.next()
.and_then(|line| line.trim().parse::<u32>().ok())
.filter(|pid| *pid != 0);
Some(PendingIndexMarker {
prior_ts,
created_at,
child_pid,
})
}
pub(crate) fn try_claim(marker_path: &Path, payload: &str) -> bool {
use std::fs::OpenOptions;
use std::io::Write;
for _ in 0..2 {
match OpenOptions::new()
.write(true)
.create_new(true)
.open(marker_path)
{
Ok(mut file) => return file.write_all(payload.as_bytes()).is_ok(),
Err(_) => {
if is_fresh(marker_path) {
return false;
}
if fs::remove_file(marker_path).is_err() {
return false;
}
}
}
}
false
}
fn is_fresh(marker_path: &Path) -> bool {
let Some(marker) = read(marker_path) else {
return false;
};
SystemTime::now()
.duration_since(marker.created_at)
.map(|age| age < Duration::from_secs(FAILURE_GRACE_SECS))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::util::now_rfc3339;
use tempfile::tempdir;
#[test]
fn round_trips() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let marker_path = dir.path().join("indexing-pending");
let payload = format!("2026-04-20T00:00:00Z\n{}\n42\n", now_rfc3339());
assert!(try_claim(&marker_path, &payload));
let marker = read(&marker_path);
assert!(marker.is_some());
let marker = marker.unwrap_or_else(|| unreachable!());
assert_eq!(marker.prior_ts, "2026-04-20T00:00:00Z");
assert_eq!(marker.child_pid, Some(42));
}
#[test]
fn treats_zero_pid_as_placeholder() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let marker_path = dir.path().join("indexing-pending");
let payload = format!("none\n{}\n0\n", now_rfc3339());
assert!(try_claim(&marker_path, &payload));
let marker = read(&marker_path).unwrap_or_else(|| unreachable!());
assert_eq!(marker.child_pid, None);
}
#[test]
fn claim_is_atomic() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let marker_path = dir.path().join("indexing-pending");
let first = format!("none\n{}\n", now_rfc3339());
assert!(try_claim(&marker_path, &first));
let second = format!("ts-2\n{}\n", now_rfc3339());
assert!(!try_claim(&marker_path, &second));
let marker = read(&marker_path).unwrap_or_else(|| unreachable!());
assert_eq!(marker.prior_ts, "none", "first claim must remain in place");
}
#[test]
fn future_timestamp_is_not_fresh() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let marker_path = dir.path().join("indexing-pending");
let future = SystemTime::now() + Duration::from_secs(3_600);
let future_ts = crate::util::format_rfc3339(future);
fs::write(&marker_path, format!("none\n{future_ts}\n0\n"))
.unwrap_or_else(|_| unreachable!());
assert!(
!is_fresh(&marker_path),
"future-timestamped marker must be reclaimable"
);
}
#[test]
fn claim_replaces_legacy_or_unparseable() {
let dir = tempdir().ok().unwrap_or_else(|| unreachable!());
let marker_path = dir.path().join("indexing-pending");
assert!(fs::write(&marker_path, "legacy-single-line").is_ok());
let payload = format!("none\n{}\n", now_rfc3339());
assert!(try_claim(&marker_path, &payload));
let marker = read(&marker_path);
assert!(marker.is_some());
}
}