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)]
91pub struct LineBuffer {
92 buf: Vec<u8>,
93 cursor: usize,
95}
96
97const COMPACT_THRESHOLD: usize = 8 * 1024;
99
100impl LineBuffer {
101 pub fn new() -> Self {
103 Self::default()
104 }
105
106 pub fn push(&mut self, chunk: &[u8]) {
108 self.buf.extend_from_slice(chunk);
109 }
110
111 pub fn next_line(&mut self) -> Option<String> {
114 let rel = self.buf[self.cursor..].iter().position(|&b| b == b'\n')?;
115 let end = self.cursor + rel;
116 let line = String::from_utf8_lossy(&self.buf[self.cursor..end]).into_owned();
119 self.cursor = end + 1;
120
121 if self.cursor == self.buf.len() {
122 self.buf.clear();
123 self.cursor = 0;
124 } else if self.cursor >= COMPACT_THRESHOLD {
125 self.buf.drain(..self.cursor);
126 self.cursor = 0;
127 }
128
129 Some(line)
130 }
131
132 pub fn take_remaining(&mut self) -> Option<String> {
139 let bytes = std::mem::take(&mut self.buf);
140 let start = std::mem::take(&mut self.cursor);
141 if start >= bytes.len() {
142 return None;
143 }
144 let trimmed = String::from_utf8_lossy(&bytes[start..]).trim().to_string();
145 if trimmed.is_empty() {
146 None
147 } else {
148 Some(trimmed)
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn test_backoff_within_bounds() {
159 for attempt in 0..8 {
160 let base = (BASE_DELAY_MS * 2u64.pow(attempt)).min(MAX_DELAY_MS);
161 for _ in 0..50 {
162 let d = backoff_delay(attempt, None).as_millis() as u64;
163 assert!(d <= base, "attempt {attempt}: {d} exceeded cap {base}");
164 }
165 }
166 }
167
168 #[test]
169 fn test_backoff_caps_large_attempts() {
170 let d = backoff_delay(100, None);
172 assert!(d.as_millis() as u64 <= MAX_DELAY_MS);
173 }
174
175 #[test]
176 fn test_retry_after_is_honored_as_floor() {
177 let ra = Duration::from_secs(5);
178 let d = backoff_delay(0, Some(ra));
179 assert!(
180 d >= ra,
181 "delay {d:?} should be at least the Retry-After {ra:?}"
182 );
183 assert!(d < ra + Duration::from_millis(BASE_DELAY_MS + 1));
185 }
186
187 #[test]
188 fn test_retry_after_clamped_to_cap() {
189 let d = backoff_delay(0, Some(Duration::from_secs(999_999)));
193 assert!(
194 d <= Duration::from_millis(MAX_DELAY_MS + BASE_DELAY_MS + 1),
195 "delay {d:?} exceeded the clamped ceiling"
196 );
197 }
198
199 #[test]
200 fn test_full_jitter_zero_cap() {
201 assert_eq!(full_jitter(0), 0);
202 }
203
204 #[test]
205 fn test_parse_retry_after_secs_numeric() {
206 assert_eq!(parse_retry_after_secs("12"), Some(12));
207 assert_eq!(parse_retry_after_secs(" 12 "), Some(12));
208 assert_eq!(parse_retry_after_secs("0"), Some(0));
209 }
210
211 #[test]
212 fn test_parse_retry_after_secs_http_date_ignored() {
213 assert_eq!(
214 parse_retry_after_secs("Wed, 21 Oct 2015 07:28:00 GMT"),
215 None
216 );
217 }
218
219 #[test]
220 fn test_parse_retry_after_secs_garbage() {
221 assert_eq!(parse_retry_after_secs(""), None);
222 assert_eq!(parse_retry_after_secs("abc"), None);
223 assert_eq!(parse_retry_after_secs("1.5"), None);
224 assert_eq!(parse_retry_after_secs("-3"), None);
225 }
226
227 #[test]
228 fn test_ascii_lines() {
229 let mut lb = LineBuffer::new();
230 lb.push(b"hello\nworld\n");
231 assert_eq!(lb.next_line().as_deref(), Some("hello"));
232 assert_eq!(lb.next_line().as_deref(), Some("world"));
233 assert_eq!(lb.next_line(), None);
234 }
235
236 #[test]
237 fn test_incomplete_line_retained() {
238 let mut lb = LineBuffer::new();
239 lb.push(b"partial");
240 assert_eq!(lb.next_line(), None);
241 lb.push(b" line\n");
242 assert_eq!(lb.next_line().as_deref(), Some("partial line"));
243 }
244
245 #[test]
246 fn test_multibyte_char_split_across_chunks() {
247 let full = "café\n".as_bytes();
250 let split = 4; let mut lb = LineBuffer::new();
252 lb.push(&full[..split]);
253 assert_eq!(lb.next_line(), None, "no complete line yet");
254 lb.push(&full[split..]);
255 let line = lb.next_line().expect("line completes after second chunk");
256 assert_eq!(line, "café");
257 assert!(!line.contains('\u{FFFD}'), "must not contain U+FFFD");
258 }
259
260 #[test]
261 fn test_emoji_split_across_chunks() {
262 let full = "🎉done\n".as_bytes();
264 let mut lb = LineBuffer::new();
265 for b in full {
266 lb.push(&[*b]);
267 }
268 let line = lb.next_line().expect("line completes");
269 assert_eq!(line, "🎉done");
270 assert!(!line.contains('\u{FFFD}'));
271 }
272
273 #[test]
274 fn test_multiple_lines_one_chunk() {
275 let mut lb = LineBuffer::new();
276 lb.push(b"a\nb\nc");
277 assert_eq!(lb.next_line().as_deref(), Some("a"));
278 assert_eq!(lb.next_line().as_deref(), Some("b"));
279 assert_eq!(lb.next_line(), None);
280 }
281
282 #[test]
283 fn test_take_remaining_returns_unterminated_final_line() {
284 let mut lb = LineBuffer::new();
287 lb.push(b"data: {\"ok\":true}");
288 assert_eq!(lb.next_line(), None, "no complete line without a newline");
289 assert_eq!(lb.take_remaining().as_deref(), Some("data: {\"ok\":true}"));
290 assert_eq!(lb.take_remaining(), None, "buffer drained after take");
291 }
292
293 #[test]
294 fn test_many_lines_one_chunk_and_compaction() {
295 let mut data = Vec::new();
299 let line_count = COMPACT_THRESHOLD / 8 + 16;
300 for i in 0..line_count {
301 data.extend_from_slice(format!("line-{i:04}\n").as_bytes());
302 }
303 data.extend_from_slice(b"partial");
304
305 let mut lb = LineBuffer::new();
306 lb.push(&data);
307 for i in 0..line_count {
308 assert_eq!(
309 lb.next_line().as_deref(),
310 Some(format!("line-{i:04}").as_str())
311 );
312 }
313 assert_eq!(lb.next_line(), None);
314 lb.push(b" tail\n");
315 assert_eq!(lb.next_line().as_deref(), Some("partial tail"));
316 }
317
318 #[test]
319 fn test_take_remaining_after_consumed_lines() {
320 let mut lb = LineBuffer::new();
321 lb.push(b"first\nleftover");
322 assert_eq!(lb.next_line().as_deref(), Some("first"));
323 assert_eq!(lb.take_remaining().as_deref(), Some("leftover"));
324 assert_eq!(lb.take_remaining(), None);
325 assert_eq!(lb.next_line(), None);
326 }
327
328 #[test]
329 fn test_take_remaining_trims_and_none_on_empty() {
330 let mut lb = LineBuffer::new();
331 assert_eq!(lb.take_remaining(), None, "empty buffer yields None");
332 lb.push(b" \r\n");
333 assert_eq!(lb.next_line().as_deref(), Some(" \r"));
335 assert_eq!(lb.take_remaining(), None);
336 lb.push(b" ");
337 assert_eq!(
338 lb.take_remaining(),
339 None,
340 "whitespace-only remainder is None"
341 );
342 }
343}