rustev_remote_http/
budget.rs1use std::fmt;
7use std::future::Future;
8use std::time::Duration;
9
10use rustev_contract::time::DurationMs;
11use tokio::time::Instant;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Budget {
17 deadline: Instant,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct Expired;
23
24impl Budget {
25 pub fn start(remaining: DurationMs) -> Self {
27 Self::from_ms(remaining.0)
28 }
29
30 pub fn from_ms(ms: u64) -> Self {
31 Self::from_start(Instant::now(), DurationMs(ms))
32 }
33
34 pub(crate) fn from_start(start: Instant, remaining: DurationMs) -> Self {
36 Budget {
37 deadline: start + Duration::from_millis(remaining.0),
38 }
39 }
40
41 pub fn deadline(&self) -> Instant {
42 self.deadline
43 }
44
45 pub fn min(self, other: Budget) -> Budget {
47 Budget {
48 deadline: self.deadline.min(other.deadline),
49 }
50 }
51
52 pub fn remaining(&self) -> Duration {
53 self.deadline.saturating_duration_since(Instant::now())
54 }
55
56 pub fn remaining_ms(&self) -> u64 {
58 u64::try_from(self.remaining().as_millis()).unwrap_or(u64::MAX)
59 }
60
61 pub fn expired(&self) -> bool {
62 Instant::now() >= self.deadline
63 }
64
65 pub fn told_ms(&self, transit_margin_ms: u64) -> u64 {
68 self.remaining_ms().saturating_sub(transit_margin_ms)
69 }
70
71 pub fn phase_deadline(&self, limit: Option<Duration>) -> Instant {
74 match limit {
75 Some(l) => self.deadline.min(Instant::now() + l),
76 None => self.deadline,
77 }
78 }
79
80 pub async fn run<F: Future>(
83 &self,
84 limit: Option<Duration>,
85 fut: F,
86 ) -> Result<F::Output, Expired> {
87 tokio::time::timeout_at(self.phase_deadline(limit), fut)
88 .await
89 .map_err(|_| Expired)
90 }
91
92 pub async fn expiry(&self) {
94 tokio::time::sleep_until(self.deadline).await
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct TimeoutBeyondBudget {
102 pub transport_timeout_ms: u64,
103 pub budget_ms: u64,
104}
105
106impl fmt::Display for TimeoutBeyondBudget {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 write!(
109 f,
110 "a transport timeout of {} ms is longer than the {} ms budget it serves",
111 self.transport_timeout_ms, self.budget_ms
112 )
113 }
114}
115
116impl std::error::Error for TimeoutBeyondBudget {}
117
118pub fn check_transport_timeout(
121 transport_timeout_ms: Option<u64>,
122 budget_ms: u64,
123) -> Result<(), TimeoutBeyondBudget> {
124 match transport_timeout_ms {
125 Some(t) if t > budget_ms => Err(TimeoutBeyondBudget {
126 transport_timeout_ms: t,
127 budget_ms,
128 }),
129 _ => Ok(()),
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 #[tokio::test(flavor = "current_thread", start_paused = true)]
138 async fn batch_preparation_does_not_extend_later_member_deadlines() {
139 let start = Instant::now();
140 let first = Budget::from_start(start, DurationMs(100));
141 tokio::time::advance(Duration::from_millis(40)).await;
142 let second = Budget::from_start(start, DurationMs(100));
143 let longer = Budget::from_start(start, DurationMs(150));
144 assert_eq!(second.remaining_ms(), 60);
145 first.expiry().await;
146 assert!(first.expired());
147 assert!(second.expired());
148 assert!(!longer.expired());
149 assert_eq!(longer.remaining_ms(), 50);
150 }
151
152 #[tokio::test(flavor = "current_thread", start_paused = true)]
153 async fn a_phase_limit_never_outlives_the_budget() {
154 let b = Budget::from_ms(100);
155 let t0 = Instant::now();
156 assert_eq!(
157 b.phase_deadline(Some(Duration::from_secs(60))),
158 b.deadline()
159 );
160 assert_eq!(
161 b.phase_deadline(Some(Duration::from_millis(10))),
162 t0 + Duration::from_millis(10)
163 );
164 let r = b
165 .run(Some(Duration::from_secs(60)), std::future::pending::<()>())
166 .await;
167 assert_eq!(r, Err(Expired));
168 assert_eq!(Instant::now(), t0 + Duration::from_millis(100));
169 assert!(b.expired());
170 assert_eq!(b.told_ms(5), 0);
171 }
172
173 #[tokio::test(flavor = "current_thread", start_paused = true)]
174 async fn the_remote_side_is_told_the_budget_minus_the_margin() {
175 let b = Budget::from_ms(1_000);
176 assert_eq!(b.told_ms(50), 950);
177 assert_eq!(b.told_ms(5_000), 0);
178 }
179
180 #[test]
181 fn a_transport_timeout_longer_than_the_budget_is_refused() {
182 assert!(check_transport_timeout(None, 10).is_ok());
183 assert!(check_transport_timeout(Some(10), 10).is_ok());
184 assert_eq!(
185 check_transport_timeout(Some(11), 10),
186 Err(TimeoutBeyondBudget {
187 transport_timeout_ms: 11,
188 budget_ms: 10
189 })
190 );
191 }
192}