1use std::sync::{Arc, Mutex, MutexGuard};
4use std::time::Duration;
5
6use tokio::time::Instant;
7
8const FAR_FUTURE: Duration = Duration::from_secs(86_400 * 365 * 30);
10
11#[derive(Clone)]
29pub struct Hold {
30 inner: Arc<HoldInner>,
31}
32
33struct HoldInner {
34 until: Mutex<Option<Instant>>,
35 ceiling: Option<Duration>,
36}
37
38impl Hold {
39 pub fn unbounded() -> Self {
42 Self::new(None)
43 }
44
45 pub fn with_ceiling(ceiling: Duration) -> Self {
48 Self::new(Some(ceiling))
49 }
50
51 fn new(ceiling: Option<Duration>) -> Self {
52 Self {
53 inner: Arc::new(HoldInner {
54 until: Mutex::new(None),
55 ceiling,
56 }),
57 }
58 }
59
60 pub fn extend(&self, delay: Duration) {
65 let delay = self
66 .inner
67 .ceiling
68 .map_or(delay, |ceiling| delay.min(ceiling));
69 let now = Instant::now();
70 let until = now.checked_add(delay).unwrap_or(now + FAR_FUTURE);
71 let mut slot = self.lock();
72 if slot.is_none_or(|current| until > current) {
73 *slot = Some(until);
74 }
75 }
76
77 pub async fn wait(&self) {
79 loop {
80 let deadline = *self.lock();
84 let Some(deadline) = deadline else { return };
85 if deadline <= Instant::now() {
86 return;
87 }
88 tokio::time::sleep_until(deadline).await;
92 }
93 }
94
95 pub fn is_held(&self) -> bool {
97 self.lock().is_some_and(|until| until > Instant::now())
98 }
99
100 fn lock(&self) -> MutexGuard<'_, Option<Instant>> {
105 self.inner
106 .until
107 .lock()
108 .unwrap_or_else(|poisoned| poisoned.into_inner())
109 }
110}
111
112impl Default for Hold {
113 fn default() -> Self {
115 Self::unbounded()
116 }
117}
118
119impl std::fmt::Debug for Hold {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 let remaining =
122 (*self.lock()).and_then(|until| until.checked_duration_since(Instant::now()));
123 f.debug_struct("Hold")
124 .field("remaining", &remaining)
125 .field("ceiling", &self.inner.ceiling)
126 .finish()
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[tokio::test(start_paused = true)]
135 async fn a_hold_never_shortens() {
136 let hold = Hold::unbounded();
137 hold.extend(Duration::from_secs(10));
138 hold.extend(Duration::from_secs(1));
139
140 let t = Instant::now();
141 hold.wait().await;
142 assert!(
143 t.elapsed() >= Duration::from_secs(10),
144 "the longer hold was cut to {:?}",
145 t.elapsed()
146 );
147 }
148
149 #[tokio::test(start_paused = true)]
150 async fn a_hold_extended_mid_wait_is_honoured_in_full() {
151 let hold = Hold::unbounded();
155 hold.extend(Duration::from_secs(2));
156 let extender = {
157 let hold = hold.clone();
158 tokio::spawn(async move {
159 tokio::time::sleep(Duration::from_secs(1)).await;
160 hold.extend(Duration::from_secs(5));
161 })
162 };
163
164 let t = Instant::now();
165 hold.wait().await;
166 extender.await.unwrap();
167 assert!(
168 t.elapsed() >= Duration::from_secs(6),
169 "woke at {:?}, at the deadline it first read",
170 t.elapsed()
171 );
172 }
173
174 #[tokio::test(start_paused = true)]
175 async fn the_ceiling_clamps_a_longer_hold() {
176 const DAY: Duration = Duration::from_secs(86_400);
177 let hold = Hold::with_ceiling(3 * DAY);
178 hold.extend(10 * DAY);
179
180 let t = Instant::now();
181 hold.wait().await;
182 assert!(
183 t.elapsed() >= 3 * DAY && t.elapsed() < 10 * DAY,
184 "held {:?}, not the 3-day ceiling",
185 t.elapsed()
186 );
187 }
188
189 #[tokio::test(start_paused = true)]
190 async fn a_huge_delay_saturates_rather_than_panics() {
191 let hold = Hold::unbounded();
192 hold.extend(Duration::MAX);
193 assert!(
194 tokio::time::timeout(Duration::from_secs(86_400 * 365), hold.wait())
195 .await
196 .is_err(),
197 "a hold of Duration::MAX released within a year"
198 );
199 }
200}