kaish_kernel/scheduler/
stream.rs1use std::collections::VecDeque;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13pub const DEFAULT_STREAM_MAX_SIZE: usize = 10 * 1024 * 1024;
15
16#[derive(Clone)]
36pub struct BoundedStream {
37 inner: Arc<RwLock<BoundedStreamInner>>,
38}
39
40struct BoundedStreamInner {
41 buffer: VecDeque<u8>,
43 max_size: usize,
45 total_written: u64,
47 bytes_evicted: u64,
49 closed: bool,
51}
52
53impl BoundedStream {
54 pub fn new(max_size: usize) -> Self {
56 Self {
57 inner: Arc::new(RwLock::new(BoundedStreamInner {
58 buffer: VecDeque::with_capacity(max_size.min(8192)), max_size,
60 total_written: 0,
61 bytes_evicted: 0,
62 closed: false,
63 })),
64 }
65 }
66
67 pub fn default_size() -> Self {
69 Self::new(DEFAULT_STREAM_MAX_SIZE)
70 }
71
72 pub async fn write(&self, data: &[u8]) {
77 let mut inner = self.inner.write().await;
78
79 if inner.closed {
80 return;
81 }
82
83 inner.total_written += data.len() as u64;
84
85 if data.len() >= inner.max_size {
87 let start = data.len() - inner.max_size;
88 inner.bytes_evicted += inner.buffer.len() as u64 + start as u64;
89 inner.buffer.clear();
90 inner.buffer.extend(&data[start..]);
91 return;
92 }
93
94 let needed = data.len();
96 let available = inner.max_size.saturating_sub(inner.buffer.len());
97
98 if needed > available {
99 let to_evict = needed - available;
100 let actual_evict = to_evict.min(inner.buffer.len());
101 inner.buffer.drain(..actual_evict);
102 inner.bytes_evicted += actual_evict as u64;
103 }
104
105 inner.buffer.extend(data);
107 }
108
109 pub async fn read(&self) -> Vec<u8> {
114 let inner = self.inner.read().await;
115 inner.buffer.iter().copied().collect()
116 }
117
118 pub async fn read_string(&self) -> String {
120 let data = self.read().await;
121 String::from_utf8_lossy(&data).into_owned()
122 }
123
124 pub async fn close(&self) {
128 let mut inner = self.inner.write().await;
129 inner.closed = true;
130 }
131
132 pub async fn is_closed(&self) -> bool {
134 let inner = self.inner.read().await;
135 inner.closed
136 }
137
138 pub async fn len(&self) -> usize {
140 let inner = self.inner.read().await;
141 inner.buffer.len()
142 }
143
144 pub async fn is_empty(&self) -> bool {
146 self.len().await == 0
147 }
148
149 pub async fn has_overflowed(&self) -> bool {
157 let inner = self.inner.read().await;
158 inner.bytes_evicted > 0
159 }
160
161 pub async fn stats(&self) -> StreamStats {
163 let inner = self.inner.read().await;
164 StreamStats {
165 current_size: inner.buffer.len(),
166 max_size: inner.max_size,
167 total_written: inner.total_written,
168 bytes_evicted: inner.bytes_evicted,
169 closed: inner.closed,
170 }
171 }
172}
173
174impl std::fmt::Debug for BoundedStream {
175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176 f.debug_struct("BoundedStream")
177 .field("inner", &"<locked>")
178 .finish()
179 }
180}
181
182#[derive(Debug, Clone)]
184pub struct StreamStats {
185 pub current_size: usize,
187 pub max_size: usize,
189 pub total_written: u64,
191 pub bytes_evicted: u64,
193 pub closed: bool,
195}
196
197impl StreamStats {
198 pub fn overflow_marker(&self, label: &str) -> String {
209 let max_mb = self.max_size as f64 / (1024.0 * 1024.0);
210 format!(
211 "[{label} truncated: output exceeded the {max_mb:.0}MB capture buffer \
212 — first {} bytes lost ({} bytes total written); enable output-limit \
213 to spill to disk]\n",
214 self.bytes_evicted, self.total_written,
215 )
216 }
217}
218
219pub async fn drain_to_stream<R>(mut reader: R, stream: Arc<BoundedStream>)
224where
225 R: tokio::io::AsyncRead + Unpin,
226{
227 use tokio::io::AsyncReadExt;
228
229 let mut buf = [0u8; 8192];
230 loop {
231 match reader.read(&mut buf).await {
232 Ok(0) => break, Ok(n) => stream.write(&buf[..n]).await,
234 Err(e) => {
235 tracing::warn!("drain_to_stream read error: {}", e);
236 break;
237 }
238 }
239 }
240 stream.close().await;
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[tokio::test]
248 async fn test_basic_write_read() {
249 let stream = BoundedStream::new(100);
250 stream.write(b"hello").await;
251 assert_eq!(stream.read().await, b"hello");
252 }
253
254 #[tokio::test]
255 async fn test_multiple_writes() {
256 let stream = BoundedStream::new(100);
257 stream.write(b"hello ").await;
258 stream.write(b"world").await;
259 assert_eq!(stream.read().await, b"hello world");
260 }
261
262 #[tokio::test]
263 async fn test_eviction_on_overflow() {
264 let stream = BoundedStream::new(10);
265 stream.write(b"12345").await;
266 stream.write(b"67890").await;
267 assert_eq!(stream.len().await, 10);
268
269 stream.write(b"ABCDE").await;
271 assert_eq!(stream.read().await, b"67890ABCDE");
272
273 let stats = stream.stats().await;
274 assert_eq!(stats.bytes_evicted, 5);
275 assert_eq!(stats.total_written, 15);
276 }
277
278 #[tokio::test]
279 async fn test_large_write_exceeds_buffer() {
280 let stream = BoundedStream::new(10);
281 stream.write(b"0123456789ABCDEFGHIJ").await;
283 assert_eq!(stream.read().await, b"ABCDEFGHIJ");
284 }
285
286 #[tokio::test]
287 async fn test_close_prevents_writes() {
288 let stream = BoundedStream::new(100);
289 stream.write(b"before").await;
290 stream.close().await;
291 stream.write(b"after").await;
292 assert_eq!(stream.read().await, b"before");
293 }
294
295 #[tokio::test]
296 async fn test_read_string() {
297 let stream = BoundedStream::new(100);
298 stream.write(b"hello world").await;
299 assert_eq!(stream.read_string().await, "hello world");
300 }
301
302 #[tokio::test]
303 async fn test_concurrent_writes() {
304 use std::sync::Arc;
305
306 let stream = Arc::new(BoundedStream::new(1000));
307
308 let handles: Vec<_> = (0..10)
309 .map(|i| {
310 let s = stream.clone();
311 tokio::spawn(async move {
312 for j in 0..10 {
313 s.write(format!("[{}-{}]", i, j).as_bytes()).await;
314 }
315 })
316 })
317 .collect();
318
319 for h in handles {
320 h.await.expect("task should not panic");
321 }
322
323 let data = stream.read().await;
326 assert!(!data.is_empty());
327 }
328
329 #[tokio::test]
330 async fn test_stats() {
331 let stream = BoundedStream::new(10);
332 stream.write(b"1234567890").await;
333
334 let stats = stream.stats().await;
335 assert_eq!(stats.current_size, 10);
336 assert_eq!(stats.max_size, 10);
337 assert_eq!(stats.total_written, 10);
338 assert_eq!(stats.bytes_evicted, 0);
339 assert!(!stats.closed);
340 }
341
342 #[tokio::test]
343 async fn test_empty_stream() {
344 let stream = BoundedStream::new(100);
345 assert!(stream.is_empty().await);
346 assert_eq!(stream.len().await, 0);
347 assert_eq!(stream.read().await, Vec::<u8>::new());
348 }
349
350 #[tokio::test]
351 async fn test_drain_to_stream() {
352 use std::io::Cursor;
353
354 let data = b"test data from reader";
355 let cursor = Cursor::new(data.to_vec());
356 let stream = Arc::new(BoundedStream::new(100));
357
358 drain_to_stream(cursor, stream.clone()).await;
359
360 assert_eq!(stream.read().await, data);
361 assert!(stream.is_closed().await);
362 }
363
364 #[tokio::test]
365 async fn test_default_size() {
366 let stream = BoundedStream::default_size();
367 let stats = stream.stats().await;
368 assert_eq!(stats.max_size, DEFAULT_STREAM_MAX_SIZE);
369 }
370
371 #[tokio::test]
372 async fn test_has_overflowed() {
373 let stream = BoundedStream::new(10);
374 assert!(!stream.has_overflowed().await, "empty stream has not overflowed");
375
376 stream.write(b"1234567890").await;
377 assert!(
378 !stream.has_overflowed().await,
379 "exactly filling the buffer is not an overflow"
380 );
381
382 stream.write(b"more").await; assert!(
384 stream.has_overflowed().await,
385 "writing past capacity must flip has_overflowed"
386 );
387 }
388
389 #[test]
390 fn test_overflow_marker_wording() {
391 let stats = StreamStats {
392 current_size: 10 * 1024 * 1024,
393 max_size: 10 * 1024 * 1024,
394 total_written: 15 * 1024 * 1024,
395 bytes_evicted: 5 * 1024 * 1024,
396 closed: true,
397 };
398 let marker = stats.overflow_marker("stdout");
399 assert!(marker.starts_with("[stdout truncated:"), "got: {marker}");
400 assert!(marker.contains("10MB"), "got: {marker}");
401 assert!(marker.contains(&(5 * 1024 * 1024).to_string()), "got: {marker}");
402 assert!(marker.contains(&(15 * 1024 * 1024).to_string()), "got: {marker}");
403 assert!(marker.contains("output-limit"), "got: {marker}");
404 }
405}