use crate::agent::log_line::LogLine;
use crate::agent::task_logger::TaskLogger;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::{OwnedSemaphorePermit, Semaphore};
#[derive(Debug, Clone)]
pub enum BuildLockState {
Idle,
Building,
Completed,
}
#[derive(Debug)]
struct ImageLock {
state: BuildLockState,
semaphore: Arc<Semaphore>,
waiting_count: usize,
}
impl ImageLock {
fn new() -> Self {
Self {
state: BuildLockState::Idle,
semaphore: Arc::new(Semaphore::new(1)),
waiting_count: 0,
}
}
}
pub struct BuildLockGuard {
image_tag: String,
manager: Arc<DockerBuildLockManager>,
_permit: OwnedSemaphorePermit,
}
impl Drop for BuildLockGuard {
fn drop(&mut self) {
let mut locks = self.manager.locks.lock().unwrap();
if let Some(lock) = locks.get_mut(&self.image_tag) {
lock.state = BuildLockState::Completed;
}
}
}
pub struct DockerBuildLockManager {
locks: Arc<Mutex<HashMap<String, ImageLock>>>,
}
impl DockerBuildLockManager {
pub fn new() -> Self {
Self {
locks: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn acquire_build_lock(
self: &Arc<Self>,
image_tag: &str,
logger: &TaskLogger,
) -> BuildLockGuard {
let semaphore = {
let mut locks = self.locks.lock().unwrap();
let lock = locks
.entry(image_tag.to_string())
.or_insert_with(ImageLock::new);
if matches!(lock.state, BuildLockState::Building) {
lock.waiting_count += 1;
let waiting_position = lock.waiting_count;
logger.log(LogLine::tsk_message(format!(
"Waiting for Docker build lock for image '{}' (position {} in queue)",
image_tag, waiting_position
)));
}
Arc::clone(&lock.semaphore)
};
let permit = semaphore.acquire_owned().await.unwrap();
{
let mut locks = self.locks.lock().unwrap();
if let Some(lock) = locks.get_mut(image_tag) {
if lock.waiting_count > 0 {
lock.waiting_count -= 1;
}
lock.state = BuildLockState::Building;
}
}
BuildLockGuard {
image_tag: image_tag.to_string(),
manager: Arc::clone(self),
_permit: permit,
}
}
}
impl Default for DockerBuildLockManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::time::timeout;
#[tokio::test]
async fn test_acquire_build_lock_exclusive() {
let manager = Arc::new(DockerBuildLockManager::new());
let _guard1 = manager
.acquire_build_lock("test-image", &TaskLogger::no_file())
.await;
let manager_clone = Arc::clone(&manager);
let acquire_task = tokio::spawn(async move {
manager_clone
.acquire_build_lock("test-image", &TaskLogger::no_file())
.await
});
tokio::time::sleep(Duration::from_millis(10)).await;
drop(_guard1);
let result = timeout(Duration::from_secs(1), acquire_task).await;
assert!(result.is_ok(), "Second lock acquisition should succeed");
}
#[tokio::test]
async fn test_parallel_builds_different_images() {
let manager = Arc::new(DockerBuildLockManager::new());
let manager1 = Arc::clone(&manager);
let manager2 = Arc::clone(&manager);
let logger1 = TaskLogger::no_file();
let logger2 = TaskLogger::no_file();
let (guard1, guard2) = tokio::join!(
manager1.acquire_build_lock("image1", &logger1),
manager2.acquire_build_lock("image2", &logger2)
);
drop(guard1);
drop(guard2);
}
#[tokio::test]
async fn test_build_lock_state_transitions() {
let manager = Arc::new(DockerBuildLockManager::new());
let guard = manager
.acquire_build_lock("test-image", &TaskLogger::no_file())
.await;
{
let locks = manager.locks.lock().unwrap();
if let Some(lock) = locks.get("test-image") {
assert!(matches!(lock.state, BuildLockState::Building));
}
}
drop(guard);
{
let locks = manager.locks.lock().unwrap();
if let Some(lock) = locks.get("test-image") {
assert!(matches!(lock.state, BuildLockState::Completed));
}
}
}
}