summaryrefslogtreecommitdiff
path: root/src/build_guard.rs
blob: 0c7fed3f4bc38870f6d7f73ba7ab48fce14d7894 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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"));
    }
}