use std::path::{Path, PathBuf};
use std::sync::Arc;
use dashmap::DashMap;
use objectiveai_sdk::lockfile::LockClaim;
use tokio::sync::{Mutex, OwnedMutexGuard};
pub fn agent_instance_lock(state_dir: &Path, agent_instance_hierarchy: &str) -> (PathBuf, String) {
let mut dir = state_dir.join("locks").join("agents").join("instances");
let mut segments = agent_instance_hierarchy.split('/').peekable();
let mut key = String::new();
while let Some(segment) = segments.next() {
if segments.peek().is_some() {
dir.push(segment);
} else {
key = segment.to_string();
}
}
(dir, key)
}
pub fn agent_tag_lock(state_dir: &Path, agent_tag: &str) -> (PathBuf, String) {
(
state_dir.join("locks").join("agents").join("tags"),
agent_tag.to_string(),
)
}
pub type AgentLockMap = DashMap<(PathBuf, String), Arc<Mutex<()>>>;
pub struct AgentLock {
claim: Option<LockClaim>,
_guard: OwnedMutexGuard<()>,
}
impl AgentLock {
pub fn release(mut self) -> std::io::Result<()> {
match self.claim.take() {
Some(claim) => claim.release(),
None => Ok(()),
}
}
pub fn take_claim(&mut self) -> Option<LockClaim> {
self.claim.take()
}
}
impl Drop for AgentLock {
fn drop(&mut self) {
if let Some(claim) = self.claim.take() {
let _ = claim.release();
}
}
}
fn mutex_for(map: &AgentLockMap, dir: &Path, key: &str) -> Arc<Mutex<()>> {
map.entry((dir.to_path_buf(), key.to_string()))
.or_insert_with(|| Arc::new(Mutex::new(())))
.clone()
}
pub async fn try_acquire(map: &AgentLockMap, dir: &Path, key: &str) -> Option<AgentLock> {
let guard = mutex_for(map, dir, key).try_lock_owned().ok()?;
match objectiveai_sdk::lockfile::try_acquire(dir, key, "").await {
Some(claim) => Some(AgentLock {
claim: Some(claim),
_guard: guard,
}),
None => None,
}
}
pub async fn wait_acquire(
map: &AgentLockMap,
dir: &Path,
key: &str,
) -> std::io::Result<AgentLock> {
let guard = mutex_for(map, dir, key).lock_owned().await;
let claim = objectiveai_sdk::lockfile::wait_acquire(dir, key, "").await?;
Ok(AgentLock {
claim: Some(claim),
_guard: guard,
})
}
#[derive(Clone)]
pub enum Family {
Group(i64),
Hierarchy(String),
}
pub struct AcquiredFamily {
pub aih: Option<(String, AgentLock)>,
pub tags: Vec<AgentLock>,
}
impl AcquiredFamily {
pub fn into_locks(self) -> Vec<AgentLock> {
let mut locks = Vec::with_capacity(self.tags.len() + 1);
if let Some((_, aih)) = self.aih {
locks.push(aih);
}
locks.extend(self.tags);
locks
}
}
pub async fn try_acquire_family(
map: &AgentLockMap,
pool: &crate::db::Pool,
state_dir: &Path,
family: Family,
) -> Result<Option<AcquiredFamily>, crate::error::Error> {
let aih = match &family {
Family::Group(_) => None,
Family::Hierarchy(h) => Some(h.clone()),
};
let coords = family_coords(pool, state_dir, family).await?;
let Some(mut locks) = try_acquire_all(map, &coords).await else {
return Ok(None);
};
Ok(Some(match aih {
Some(hierarchy) => AcquiredFamily {
aih: Some((hierarchy, locks.remove(0))),
tags: locks,
},
None => AcquiredFamily { aih: None, tags: locks },
}))
}
pub async fn family_coords(
pool: &crate::db::Pool,
state_dir: &Path,
family: Family,
) -> Result<Vec<(PathBuf, String)>, crate::error::Error> {
let mut coords = Vec::new();
match family {
Family::Group(tag_group) => {
for tag in crate::db::tags::tags_for_group(pool, tag_group).await? {
coords.push(agent_tag_lock(state_dir, &tag));
}
}
Family::Hierarchy(agent_instance_hierarchy) => {
coords.push(agent_instance_lock(state_dir, &agent_instance_hierarchy));
for tag in crate::db::tags::tags_for_hierarchy(pool, &agent_instance_hierarchy).await? {
coords.push(agent_tag_lock(state_dir, &tag));
}
}
}
Ok(coords)
}
pub async fn try_acquire_all(
map: &AgentLockMap,
coords: &[(PathBuf, String)],
) -> Option<Vec<AgentLock>> {
let got = futures::future::join_all(
coords.iter().map(|(dir, key)| try_acquire(map, dir, key)),
)
.await;
if got.iter().all(Option::is_some) {
Some(got.into_iter().flatten().collect())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_instance_lock_splits_hierarchy() {
let state = Path::new("state");
let (dir, key) = agent_instance_lock(state, "root/child-1/leaf-2");
assert_eq!(
dir,
Path::new("state")
.join("locks")
.join("agents")
.join("instances")
.join("root")
.join("child-1"),
);
assert_eq!(key, "leaf-2");
let (dir, key) = agent_instance_lock(state, "UNKNOWN");
assert_eq!(
dir,
Path::new("state").join("locks").join("agents").join("instances"),
);
assert_eq!(key, "UNKNOWN");
}
}