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"));
}
}
|