Skip to main content

structfs_handles/
byte_stream.rs

1//! `ByteStream`: an append-only byte buffer with parked ranged reads.
2//!
3//! The byte-level analogue of [`crate::TailLog`], for stores that serve
4//! `read(2)`-shaped traffic — stdio, sockets, file tails, response
5//! bodies. The store convention it backs (see
6//! `docs/patterns/bytestream.md`) serves ranges at `at/{offset}/len/{n}`
7//! — the same path shape `structfs-sys` file handles use — with
8//! `read(2)` semantics: a blocking read parks until at least one byte
9//! past the offset exists, and returns empty exactly at end-of-stream.
10
11use std::sync::Mutex;
12
13use structfs_core_store::Value;
14
15use crate::gate::{CancelToken, Cancelled, Gate};
16
17/// One ranged read's result.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ByteChunk {
20    /// The bytes read (possibly fewer than requested).
21    pub bytes: Vec<u8>,
22    /// The offset the bytes start at.
23    pub offset: u64,
24    /// True when this chunk ends at the end of a closed stream. For a
25    /// parked read this is equivalent to `bytes.is_empty()` — the
26    /// `read(2)` contract.
27    pub eof: bool,
28}
29
30impl ByteChunk {
31    /// The store-convention encoding: the bytes themselves. Empty bytes
32    /// from a blocking read mean end-of-stream.
33    pub fn into_value(self) -> Value {
34        Value::Bytes(self.bytes)
35    }
36
37    /// The cursor for the next read.
38    pub fn next_offset(&self) -> u64 {
39        self.offset + self.bytes.len() as u64
40    }
41}
42
43struct StreamState {
44    data: Vec<u8>,
45    closed: bool,
46}
47
48/// An append-only byte buffer with terminal state and parked ranged
49/// reads.
50pub struct ByteStream {
51    state: Mutex<StreamState>,
52    gate: Gate,
53}
54
55impl Default for ByteStream {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl ByteStream {
62    /// Create an empty, open stream.
63    pub fn new() -> Self {
64        Self {
65            state: Mutex::new(StreamState {
66                data: Vec::new(),
67                closed: false,
68            }),
69            gate: Gate::new(),
70        }
71    }
72
73    fn lock(&self) -> std::sync::MutexGuard<'_, StreamState> {
74        self.state.lock().unwrap_or_else(|e| e.into_inner())
75    }
76
77    /// Append bytes and wake parked readers.
78    ///
79    /// Returns false (dropping the bytes) if the stream is closed.
80    pub fn push(&self, bytes: &[u8]) -> bool {
81        {
82            let mut state = self.lock();
83            if state.closed {
84                return false;
85            }
86            state.data.extend_from_slice(bytes);
87        }
88        self.gate.notify();
89        true
90    }
91
92    /// Close the stream and wake parked readers. Idempotent.
93    pub fn close(&self) {
94        self.lock().closed = true;
95        self.gate.notify();
96    }
97
98    /// Whether the stream is closed.
99    pub fn is_closed(&self) -> bool {
100        self.lock().closed
101    }
102
103    /// Total bytes appended so far.
104    pub fn len(&self) -> u64 {
105        self.lock().data.len() as u64
106    }
107
108    /// Whether no bytes have been appended.
109    pub fn is_empty(&self) -> bool {
110        self.len() == 0
111    }
112
113    fn chunk_at(state: &StreamState, offset: u64, max: usize) -> ByteChunk {
114        // Clamp a past-the-end offset instead of erroring: it reads as
115        // an empty chunk at the current end.
116        let start = (offset as usize).min(state.data.len());
117        let end = start.saturating_add(max).min(state.data.len());
118        ByteChunk {
119            bytes: state.data[start..end].to_vec(),
120            offset: start as u64,
121            eof: state.closed && end == state.data.len(),
122        }
123    }
124
125    /// Non-blocking ranged read: whatever is available right now.
126    pub fn snapshot_at(&self, offset: u64, max: usize) -> ByteChunk {
127        Self::chunk_at(&self.lock(), offset, max)
128    }
129
130    /// `read(2)`: park until at least one byte past `offset` exists or
131    /// the stream is closed. Returns an empty chunk exactly at
132    /// end-of-stream.
133    pub async fn read_at(&self, offset: u64, max: usize) -> ByteChunk {
134        self.gate
135            .wait_until(|| {
136                let state = self.lock();
137                if (state.data.len() as u64) > offset || state.closed {
138                    Some(Self::chunk_at(&state, offset, max))
139                } else {
140                    None
141                }
142            })
143            .await
144    }
145
146    /// [`ByteStream::read_at`], cancellable.
147    pub async fn read_at_cancellable(
148        &self,
149        offset: u64,
150        max: usize,
151        token: &CancelToken,
152    ) -> Result<ByteChunk, Cancelled> {
153        self.gate
154            .wait_until_cancellable(token, || {
155                let state = self.lock();
156                if (state.data.len() as u64) > offset || state.closed {
157                    Some(Self::chunk_at(&state, offset, max))
158                } else {
159                    None
160                }
161            })
162            .await
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use std::sync::Arc;
170
171    #[tokio::test]
172    async fn ranged_reads_return_available_bytes() {
173        let stream = ByteStream::new();
174        stream.push(b"hello world");
175
176        let chunk = stream.read_at(0, 5).await;
177        assert_eq!(chunk.bytes, b"hello");
178        assert_eq!(chunk.next_offset(), 5);
179        assert!(!chunk.eof);
180
181        let chunk = stream.read_at(chunk.next_offset(), 100).await;
182        assert_eq!(chunk.bytes, b" world");
183        assert!(!chunk.eof);
184    }
185
186    #[tokio::test]
187    async fn read_parks_until_push() {
188        let stream = Arc::new(ByteStream::new());
189        let reader = {
190            let stream = stream.clone();
191            tokio::spawn(async move { stream.read_at(0, 16).await })
192        };
193        tokio::task::yield_now().await;
194        stream.push(b"data");
195
196        let chunk = reader.await.unwrap();
197        assert_eq!(chunk.bytes, b"data");
198    }
199
200    #[tokio::test]
201    async fn empty_read_means_eof() {
202        let stream = Arc::new(ByteStream::new());
203        stream.push(b"tail");
204
205        // Reader drains, then parks; close resolves it with empty+eof.
206        let reader = {
207            let stream = stream.clone();
208            tokio::spawn(async move {
209                let first = stream.read_at(0, 100).await;
210                let second = stream.read_at(first.next_offset(), 100).await;
211                (first, second)
212            })
213        };
214        tokio::task::yield_now().await;
215        stream.close();
216
217        let (first, second) = reader.await.unwrap();
218        assert_eq!(first.bytes, b"tail");
219        assert!(second.bytes.is_empty());
220        assert!(second.eof);
221    }
222
223    #[tokio::test]
224    async fn data_racing_close_arrives_before_eof() {
225        // The close-out race, byte edition: bytes pushed just before
226        // close must be readable before an empty EOF chunk is seen.
227        let stream = Arc::new(ByteStream::new());
228        let reader = {
229            let stream = stream.clone();
230            tokio::spawn(async move { stream.read_at(0, 100).await })
231        };
232        tokio::task::yield_now().await;
233        stream.push(b"last words");
234        stream.close();
235
236        let chunk = reader.await.unwrap();
237        assert_eq!(chunk.bytes, b"last words");
238        assert!(chunk.eof);
239    }
240
241    #[tokio::test]
242    async fn stale_offset_clamps() {
243        let stream = ByteStream::new();
244        stream.push(b"abc");
245        stream.close();
246
247        let chunk = stream.read_at(999, 10).await;
248        assert!(chunk.bytes.is_empty());
249        assert_eq!(chunk.offset, 3);
250        assert!(chunk.eof);
251    }
252
253    #[test]
254    fn push_after_close_is_dropped() {
255        let stream = ByteStream::new();
256        assert!(stream.push(b"a"));
257        stream.close();
258        assert!(!stream.push(b"b"));
259        assert_eq!(stream.len(), 1);
260    }
261
262    #[test]
263    fn snapshot_is_nonblocking() {
264        let stream = ByteStream::new();
265        let chunk = stream.snapshot_at(0, 10);
266        assert!(chunk.bytes.is_empty());
267        assert!(!chunk.eof); // open and empty: not EOF, just nothing yet
268    }
269
270    #[tokio::test]
271    async fn cancellation_fails_parked_read() {
272        let stream = Arc::new(ByteStream::new());
273        let token = CancelToken::new();
274        let reader = {
275            let stream = stream.clone();
276            let token = token.clone();
277            tokio::spawn(async move { stream.read_at_cancellable(0, 10, &token).await })
278        };
279        tokio::task::yield_now().await;
280        token.cancel();
281        assert!(reader.await.unwrap().is_err());
282    }
283
284    #[test]
285    fn value_encoding_is_bytes() {
286        let chunk = ByteChunk {
287            bytes: vec![1, 2, 3],
288            offset: 0,
289            eof: false,
290        };
291        assert_eq!(chunk.into_value(), Value::Bytes(vec![1, 2, 3]));
292    }
293}