diff options
Diffstat (limited to 'src/build_guard.rs')
| -rw-r--r-- | src/build_guard.rs | 128 |
1 files changed, 128 insertions, 0 deletions
diff --git a/src/build_guard.rs b/src/build_guard.rs new file mode 100644 index 0000000..0c7fed3 --- /dev/null +++ b/src/build_guard.rs @@ -0,0 +1,128 @@ +use dashmap::DashSet; +use std::sync::Arc; + +/// Manages per-site build scheduling: immediate execution and a depth-1 queue. +/// +/// When a build is already in progress, a single rebuild can be queued. +/// Subsequent requests while a rebuild is already queued are collapsed (no-op). +pub struct BuildScheduler { + pub in_progress: DashSet<String>, + pub queued: DashSet<String>, +} + +impl BuildScheduler { + #[must_use] + pub fn new() -> Self { + Self { + in_progress: DashSet::new(), + queued: DashSet::new(), + } + } + + /// Queue a rebuild for a site that is currently building. + /// Returns `true` if newly queued, `false` if already queued (collapse). + pub(crate) fn try_queue(&self, site_name: &str) -> bool { + self.queued.insert(site_name.to_owned()) + } + + /// Check and clear queued rebuild. Returns `true` if there was one. + pub(crate) fn take_queued(&self, site_name: &str) -> bool { + self.queued.remove(site_name).is_some() + } +} + +impl Default for BuildScheduler { + fn default() -> Self { + Self::new() + } +} + +/// RAII guard for per-site build exclusion. +/// Inserting into the scheduler's `in_progress` set acquires the lock; +/// dropping removes it. +pub(crate) struct BuildGuard { + site_name: String, + scheduler: Arc<BuildScheduler>, +} + +impl BuildGuard { + pub(crate) fn try_acquire(site_name: String, scheduler: &Arc<BuildScheduler>) -> Option<Self> { + if scheduler.in_progress.insert(site_name.clone()) { + Some(Self { + site_name, + scheduler: Arc::clone(scheduler), + }) + } else { + None + } + } +} + +impl Drop for BuildGuard { + fn drop(&mut self) { + self.scheduler.in_progress.remove(&self.site_name); + } +} + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::indexing_slicing)] +mod tests { + use super::*; + + #[test] + fn build_guard_try_acquire_success() { + let scheduler = Arc::new(BuildScheduler::new()); + let guard = BuildGuard::try_acquire("my-site".to_owned(), &scheduler); + assert!(guard.is_some()); + assert!(scheduler.in_progress.contains("my-site")); + } + + #[test] + fn build_guard_try_acquire_fails_when_held() { + let scheduler = Arc::new(BuildScheduler::new()); + let _guard = BuildGuard::try_acquire("my-site".to_owned(), &scheduler); + let second = BuildGuard::try_acquire("my-site".to_owned(), &scheduler); + assert!(second.is_none()); + } + + #[test] + fn build_guard_drop_releases_lock() { + let scheduler = Arc::new(BuildScheduler::new()); + { + let _guard = BuildGuard::try_acquire("my-site".to_owned(), &scheduler); + assert!(scheduler.in_progress.contains("my-site")); + } + // Guard dropped — lock released + assert!(!scheduler.in_progress.contains("my-site")); + let again = BuildGuard::try_acquire("my-site".to_owned(), &scheduler); + assert!(again.is_some()); + } + + #[test] + fn scheduler_try_queue_succeeds() { + let scheduler = BuildScheduler::new(); + assert!(scheduler.try_queue("my-site")); + assert!(scheduler.queued.contains("my-site")); + } + + #[test] + fn scheduler_try_queue_collapse() { + let scheduler = BuildScheduler::new(); + assert!(scheduler.try_queue("my-site")); + assert!(!scheduler.try_queue("my-site")); + } + + #[test] + fn scheduler_take_queued_clears_flag() { + let scheduler = BuildScheduler::new(); + scheduler.try_queue("my-site"); + assert!(scheduler.take_queued("my-site")); + assert!(!scheduler.queued.contains("my-site")); + } + + #[test] + fn scheduler_take_queued_returns_false_when_empty() { + let scheduler = BuildScheduler::new(); + assert!(!scheduler.take_queued("my-site")); + } +} |
