asupersync/time/
budget_ext.rs1use crate::cx::Cx;
4use crate::time::{Elapsed, Sleep, sleep_until};
5use crate::types::{Budget, Time};
6use std::future::Future;
7use std::marker::Unpin;
8use std::time::Duration;
9
10pub trait BudgetTimeExt {
12 fn remaining_duration(&self, now: Time) -> Option<Duration>;
14
15 fn deadline_sleep(&self) -> Option<Sleep>;
17
18 fn deadline_elapsed(&self, now: Time) -> bool;
20}
21
22impl BudgetTimeExt for Budget {
23 #[inline]
24 fn remaining_duration(&self, now: Time) -> Option<Duration> {
25 self.deadline.map(|d| {
26 if now >= d {
27 Duration::ZERO
28 } else {
29 let diff_nanos = d.as_nanos().saturating_sub(now.as_nanos());
30 Duration::from_nanos(diff_nanos)
31 }
32 })
33 }
34
35 #[inline]
36 fn deadline_sleep(&self) -> Option<Sleep> {
37 self.deadline.map(sleep_until)
38 }
39
40 #[inline]
41 fn deadline_elapsed(&self, now: Time) -> bool {
42 self.deadline.is_some_and(|d| d <= now)
43 }
44}
45
46pub async fn budget_sleep(cx: &Cx, duration: Duration, now: Time) -> Result<(), Elapsed> {
51 let budget = cx.budget();
52
53 let remaining = BudgetTimeExt::remaining_duration(&budget, now);
56
57 let effective_duration = match remaining {
58 Some(rem) if rem < duration => rem,
59 _ => duration,
60 };
61
62 if effective_duration.is_zero() && BudgetTimeExt::deadline_elapsed(&budget, now) {
63 let deadline = budget.deadline.unwrap_or(now);
64 return Err(Elapsed::new(deadline));
65 }
66
67 crate::time::sleep(now, effective_duration).await;
68
69 if effective_duration < duration {
71 let deadline = budget.deadline.unwrap_or(now);
73 return Err(Elapsed::new(deadline));
74 }
75
76 Ok(())
77}
78
79pub async fn budget_timeout<F: Future + Unpin>(
81 cx: &Cx,
82 duration: Duration,
83 future: F,
84 now: Time,
85) -> Result<F::Output, Elapsed> {
86 let budget = cx.budget();
87
88 let remaining = BudgetTimeExt::remaining_duration(&budget, now);
90 let effective_timeout = match remaining {
91 Some(rem) if rem < duration => rem,
92 _ => duration,
93 };
94
95 crate::time::timeout(now, effective_timeout, future).await
96}
97
98#[cfg(test)]
99mod tests {
100 #![allow(
101 clippy::pedantic,
102 clippy::nursery,
103 clippy::expect_fun_call,
104 clippy::map_unwrap_or,
105 clippy::cast_possible_wrap,
106 clippy::future_not_send
107 )]
108 use super::*;
109 use crate::cx::Cx;
110 use crate::test_utils::init_test_logging;
111 use crate::types::{Budget, RegionId, TaskId};
112 use crate::util::ArenaIndex;
113 use proptest::prelude::*;
114 use std::future::{pending, ready};
115 use std::time::Duration;
116
117 fn init_test(name: &str) {
118 init_test_logging();
119 crate::test_phase!(name);
120 }
121
122 fn test_cx(budget: Budget) -> Cx {
123 Cx::new(
124 RegionId::from_arena(ArenaIndex::new(0, 1)),
125 TaskId::from_arena(ArenaIndex::new(0, 0)),
126 budget,
127 )
128 }
129
130 #[test]
131 fn budget_time_ext_deadline_boundaries() {
132 init_test("budget_time_ext_deadline_boundaries");
133
134 let unconstrained = Budget::new();
135 assert_eq!(
136 BudgetTimeExt::remaining_duration(&unconstrained, Time::from_secs(5)),
137 None
138 );
139 assert!(!BudgetTimeExt::deadline_elapsed(
140 &unconstrained,
141 Time::from_secs(5)
142 ));
143 assert!(BudgetTimeExt::deadline_sleep(&unconstrained).is_none());
144
145 let deadline = Time::from_secs(10);
146 let budget = Budget::new().with_deadline(deadline);
147
148 assert_eq!(
149 BudgetTimeExt::remaining_duration(&budget, Time::from_secs(4)),
150 Some(Duration::from_secs(6))
151 );
152 assert!(!BudgetTimeExt::deadline_elapsed(
153 &budget,
154 Time::from_secs(4)
155 ));
156 assert!(BudgetTimeExt::deadline_sleep(&budget).is_some());
157
158 assert_eq!(
159 BudgetTimeExt::remaining_duration(&budget, deadline),
160 Some(Duration::ZERO)
161 );
162 assert!(BudgetTimeExt::deadline_elapsed(&budget, deadline));
163
164 assert_eq!(
165 BudgetTimeExt::remaining_duration(&budget, Time::from_secs(12)),
166 Some(Duration::ZERO)
167 );
168 assert!(BudgetTimeExt::deadline_elapsed(
169 &budget,
170 Time::from_secs(12)
171 ));
172 crate::test_complete!("budget_time_ext_deadline_boundaries");
173 }
174
175 proptest! {
176 #[test]
177 fn budget_remaining_duration_metamorphic_monotonic_as_now_advances(
178 deadline_nanos in 0u64..1_000_000_000_000,
179 first_now_nanos in 0u64..1_000_000_000_000,
180 second_now_nanos in 0u64..1_000_000_000_000,
181 ) {
182 let budget = Budget::new().with_deadline(Time::from_nanos(deadline_nanos));
183 let earlier_now_nanos = first_now_nanos.min(second_now_nanos);
184 let later_now_nanos = first_now_nanos.max(second_now_nanos);
185
186 let earlier_remaining = BudgetTimeExt::remaining_duration(
187 &budget,
188 Time::from_nanos(earlier_now_nanos),
189 )
190 .expect("budget has a deadline");
191 let later_remaining = BudgetTimeExt::remaining_duration(
192 &budget,
193 Time::from_nanos(later_now_nanos),
194 )
195 .expect("budget has a deadline");
196 let elapsed_between_reads =
197 Duration::from_nanos(later_now_nanos - earlier_now_nanos);
198
199 prop_assert!(
200 later_remaining <= earlier_remaining,
201 "remaining duration must not increase as now advances",
202 );
203 prop_assert_eq!(
204 later_remaining,
205 earlier_remaining.saturating_sub(elapsed_between_reads),
206 "advancing now must reduce remaining duration by the elapsed interval, floored at zero",
207 );
208 prop_assert_eq!(
209 BudgetTimeExt::deadline_elapsed(&budget, Time::from_nanos(later_now_nanos)),
210 later_now_nanos >= deadline_nanos,
211 "deadline_elapsed must agree with remaining_duration's zero boundary",
212 );
213 }
214 }
215
216 #[test]
217 fn budget_timeout_respects_exhausted_deadline_boundary() {
218 init_test("budget_timeout_respects_exhausted_deadline_boundary");
219 let cx = test_cx(Budget::new().with_deadline(Time::ZERO));
220
221 futures_lite::future::block_on(async {
222 let elapsed = budget_timeout(&cx, Duration::from_secs(10), pending::<()>(), Time::ZERO)
223 .await
224 .expect_err("pending work must time out at an exhausted budget deadline");
225 assert_eq!(elapsed.deadline(), Time::ZERO);
226
227 let completed = budget_timeout(
228 &cx,
229 Duration::from_secs(10),
230 ready("already-complete"),
231 Time::ZERO,
232 )
233 .await
234 .expect("ready work wins the timeout boundary");
235 assert_eq!(completed, "already-complete");
236 });
237 crate::test_complete!("budget_timeout_respects_exhausted_deadline_boundary");
238 }
239
240 #[test]
241 fn test_budget_sleep() {
242 init_test("test_budget_sleep");
243 let now = Time::ZERO;
246 let deadline = now.saturating_add_nanos(5_000_000); let budget = Budget::new().with_deadline(deadline);
248 let cx = test_cx(budget);
249
250 futures_lite::future::block_on(async {
252 let result = budget_sleep(&cx, Duration::from_secs(10), now).await;
253 let is_err = result.is_err();
254 crate::assert_with_log!(is_err, "budget sleep errors", true, is_err);
255 });
256 crate::test_complete!("test_budget_sleep");
257 }
258}