daimon_core/
stream_util.rs1use std::time::{Duration, SystemTime, UNIX_EPOCH};
14
15pub const BASE_DELAY_MS: u64 = 100;
17
18pub const MAX_DELAY_MS: u64 = 30_000;
23
24pub fn backoff_delay(attempt: u32, retry_after: Option<Duration>) -> Duration {
36 if let Some(ra) = retry_after {
37 let capped = ra.min(Duration::from_millis(MAX_DELAY_MS));
38 return capped + Duration::from_millis(full_jitter(BASE_DELAY_MS));
39 }
40
41 let exp = BASE_DELAY_MS
42 .saturating_mul(2u64.saturating_pow(attempt))
43 .min(MAX_DELAY_MS);
44 Duration::from_millis(full_jitter(exp))
45}
46
47pub fn full_jitter(cap_ms: u64) -> u64 {
53 if cap_ms == 0 {
54 return 0;
55 }
56 let entropy = SystemTime::now()
57 .duration_since(UNIX_EPOCH)
58 .map(|d| d.subsec_nanos() as u64)
59 .unwrap_or(0);
60 entropy % (cap_ms + 1)
61}
62
63pub fn parse_retry_after_secs(value: &str) -> Option<u64> {
69 value.trim().parse::<u64>().ok()
70}
71
72#[derive(Default)]
86pub struct LineBuffer {
87 buf: Vec<u8>,
88}
89
90impl LineBuffer {
91 pub fn new() -> Self {
93 Self { buf: Vec::new() }
94 }
95
96 pub fn push(&mut self, chunk: &[u8]) {
98 self.buf.extend_from_slice(chunk);
99 }
100
101 pub fn next_line(&mut self) -> Option<String> {
104 let pos = self.buf.iter().position(|&b| b == b'\n')?;
105 let line: Vec<u8> = self.buf.drain(..=pos).collect();
106 Some(String::from_utf8_lossy(&line[..line.len() - 1]).into_owned())
108 }
109
110 pub fn take_remaining(&mut self) -> Option<String> {
117 if self.buf.is_empty() {
118 return None;
119 }
120 let bytes = std::mem::take(&mut self.buf);
121 let trimmed = String::from_utf8_lossy(&bytes).trim().to_string();
122 if trimmed.is_empty() {
123 None
124 } else {
125 Some(trimmed)
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_backoff_within_bounds() {
136 for attempt in 0..8 {
137 let base = (BASE_DELAY_MS * 2u64.pow(attempt)).min(MAX_DELAY_MS);
138 for _ in 0..50 {
139 let d = backoff_delay(attempt, None).as_millis() as u64;
140 assert!(d <= base, "attempt {attempt}: {d} exceeded cap {base}");
141 }
142 }
143 }
144
145 #[test]
146 fn test_backoff_caps_large_attempts() {
147 let d = backoff_delay(100, None);
149 assert!(d.as_millis() as u64 <= MAX_DELAY_MS);
150 }
151
152 #[test]
153 fn test_retry_after_is_honored_as_floor() {
154 let ra = Duration::from_secs(5);
155 let d = backoff_delay(0, Some(ra));
156 assert!(
157 d >= ra,
158 "delay {d:?} should be at least the Retry-After {ra:?}"
159 );
160 assert!(d < ra + Duration::from_millis(BASE_DELAY_MS + 1));
162 }
163
164 #[test]
165 fn test_retry_after_clamped_to_cap() {
166 let d = backoff_delay(0, Some(Duration::from_secs(999_999)));
170 assert!(
171 d <= Duration::from_millis(MAX_DELAY_MS + BASE_DELAY_MS + 1),
172 "delay {d:?} exceeded the clamped ceiling"
173 );
174 }
175
176 #[test]
177 fn test_full_jitter_zero_cap() {
178 assert_eq!(full_jitter(0), 0);
179 }
180
181 #[test]
182 fn test_parse_retry_after_secs_numeric() {
183 assert_eq!(parse_retry_after_secs("12"), Some(12));
184 assert_eq!(parse_retry_after_secs(" 12 "), Some(12));
185 assert_eq!(parse_retry_after_secs("0"), Some(0));
186 }
187
188 #[test]
189 fn test_parse_retry_after_secs_http_date_ignored() {
190 assert_eq!(
191 parse_retry_after_secs("Wed, 21 Oct 2015 07:28:00 GMT"),
192 None
193 );
194 }
195
196 #[test]
197 fn test_parse_retry_after_secs_garbage() {
198 assert_eq!(parse_retry_after_secs(""), None);
199 assert_eq!(parse_retry_after_secs("abc"), None);
200 assert_eq!(parse_retry_after_secs("1.5"), None);
201 assert_eq!(parse_retry_after_secs("-3"), None);
202 }
203
204 #[test]
205 fn test_ascii_lines() {
206 let mut lb = LineBuffer::new();
207 lb.push(b"hello\nworld\n");
208 assert_eq!(lb.next_line().as_deref(), Some("hello"));
209 assert_eq!(lb.next_line().as_deref(), Some("world"));
210 assert_eq!(lb.next_line(), None);
211 }
212
213 #[test]
214 fn test_incomplete_line_retained() {
215 let mut lb = LineBuffer::new();
216 lb.push(b"partial");
217 assert_eq!(lb.next_line(), None);
218 lb.push(b" line\n");
219 assert_eq!(lb.next_line().as_deref(), Some("partial line"));
220 }
221
222 #[test]
223 fn test_multibyte_char_split_across_chunks() {
224 let full = "café\n".as_bytes();
227 let split = 4; let mut lb = LineBuffer::new();
229 lb.push(&full[..split]);
230 assert_eq!(lb.next_line(), None, "no complete line yet");
231 lb.push(&full[split..]);
232 let line = lb.next_line().expect("line completes after second chunk");
233 assert_eq!(line, "café");
234 assert!(!line.contains('\u{FFFD}'), "must not contain U+FFFD");
235 }
236
237 #[test]
238 fn test_emoji_split_across_chunks() {
239 let full = "🎉done\n".as_bytes();
241 let mut lb = LineBuffer::new();
242 for b in full {
243 lb.push(&[*b]);
244 }
245 let line = lb.next_line().expect("line completes");
246 assert_eq!(line, "🎉done");
247 assert!(!line.contains('\u{FFFD}'));
248 }
249
250 #[test]
251 fn test_multiple_lines_one_chunk() {
252 let mut lb = LineBuffer::new();
253 lb.push(b"a\nb\nc");
254 assert_eq!(lb.next_line().as_deref(), Some("a"));
255 assert_eq!(lb.next_line().as_deref(), Some("b"));
256 assert_eq!(lb.next_line(), None);
257 }
258
259 #[test]
260 fn test_take_remaining_returns_unterminated_final_line() {
261 let mut lb = LineBuffer::new();
264 lb.push(b"data: {\"ok\":true}");
265 assert_eq!(lb.next_line(), None, "no complete line without a newline");
266 assert_eq!(lb.take_remaining().as_deref(), Some("data: {\"ok\":true}"));
267 assert_eq!(lb.take_remaining(), None, "buffer drained after take");
268 }
269
270 #[test]
271 fn test_take_remaining_trims_and_none_on_empty() {
272 let mut lb = LineBuffer::new();
273 assert_eq!(lb.take_remaining(), None, "empty buffer yields None");
274 lb.push(b" \r\n");
275 assert_eq!(lb.next_line().as_deref(), Some(" \r"));
277 assert_eq!(lb.take_remaining(), None);
278 lb.push(b" ");
279 assert_eq!(
280 lb.take_remaining(),
281 None,
282 "whitespace-only remainder is None"
283 );
284 }
285}