summaryrefslogtreecommitdiff
path: root/src/build_guard.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/build_guard.rs')
-rw-r--r--src/build_guard.rs128
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"));
+ }
+}