Skip to main content

oxideav_source/
buffered.rs

1//! Prefetch-ring-buffer wrapper around any `ReadSeek`.
2//!
3//! A worker thread owns the inner source and continuously fills a ring
4//! buffer ahead of the read cursor. Reads serve from the ring; seeks
5//! either move the cursor inside the ring (no IO) or restart the worker
6//! at the new offset.
7//!
8//! Designed for streaming playback over a slow source (HTTP).
9
10use std::collections::VecDeque;
11use std::io::{self, Read, Seek, SeekFrom};
12use std::sync::{Arc, Condvar, Mutex};
13use std::thread::{self, JoinHandle};
14use std::time::Duration;
15
16use oxideav_container::ReadSeek;
17
18/// Worker reads at most this many bytes per `inner.read` call.
19const BLOCK: usize = 256 * 1024;
20
21/// Shared state between reader and worker.
22struct RingState {
23    /// Bytes prefetched, oldest first. `buf[0]` corresponds to `ring_start`.
24    buf: VecDeque<u8>,
25    /// Absolute offset of `buf[0]` in the inner source.
26    ring_start: u64,
27    /// Maximum number of bytes the ring may hold.
28    capacity: usize,
29    /// Total length of inner source, if known.
30    total_len: Option<u64>,
31    /// Worker has reached EOF at the current ring tail.
32    eof: bool,
33    /// Sticky error from the worker; surfaced on the next reader call.
34    err: Option<io::Error>,
35    /// Reader has set this to ask the worker to discard the ring and
36    /// reposition the inner source. The worker clears it when it has acted.
37    target_pos: Option<u64>,
38    /// Reader is gone; worker should exit promptly.
39    stop: bool,
40}
41
42struct Shared {
43    state: Mutex<RingState>,
44    not_full: Condvar,
45    not_empty: Condvar,
46}
47
48/// Buffered, prefetching wrapper around any `ReadSeek`.
49pub struct BufferedSource {
50    shared: Arc<Shared>,
51    /// Reader's logical position in the inner source.
52    pos: u64,
53    /// Worker handle. `None` only between drop signal and join.
54    worker: Option<JoinHandle<()>>,
55}
56
57impl BufferedSource {
58    /// Wrap `inner`, allocating up to `capacity` bytes for the prefetch
59    /// ring. Spawns one worker thread that takes ownership of `inner`.
60    /// `capacity` is rounded up to at least 4 × `BLOCK` so the worker
61    /// always has room to make forward progress.
62    pub fn new(mut inner: Box<dyn ReadSeek>, capacity: usize) -> io::Result<Self> {
63        let capacity = capacity.max(4 * BLOCK);
64
65        // Determine total length up front (cheap for File / HttpSource).
66        let pos = inner.stream_position()?;
67        let end = inner.seek(SeekFrom::End(0))?;
68        let total_len = Some(end);
69        // Restore position.
70        inner.seek(SeekFrom::Start(pos))?;
71
72        let state = RingState {
73            buf: VecDeque::with_capacity(capacity),
74            ring_start: pos,
75            capacity,
76            total_len,
77            eof: total_len == Some(pos),
78            err: None,
79            target_pos: None,
80            stop: false,
81        };
82        let shared = Arc::new(Shared {
83            state: Mutex::new(state),
84            not_full: Condvar::new(),
85            not_empty: Condvar::new(),
86        });
87
88        let worker_shared = Arc::clone(&shared);
89        let worker = thread::spawn(move || worker_loop(worker_shared, inner));
90
91        Ok(Self {
92            shared,
93            pos,
94            worker: Some(worker),
95        })
96    }
97
98    /// Total length of the inner source, if known.
99    pub fn len(&self) -> Option<u64> {
100        self.shared.state.lock().unwrap().total_len
101    }
102
103    /// Whether the inner source is known to be empty. Returns `false` if
104    /// the length couldn't be determined (treat as non-empty).
105    pub fn is_empty(&self) -> bool {
106        matches!(self.len(), Some(0))
107    }
108}
109
110fn worker_loop(shared: Arc<Shared>, mut inner: Box<dyn ReadSeek>) {
111    let mut scratch = vec![0u8; BLOCK];
112    loop {
113        // Phase 1: handle stop / seek requests, wait if ring is full.
114        let to_read: usize;
115        {
116            let mut st = shared.state.lock().unwrap();
117            loop {
118                if st.stop {
119                    return;
120                }
121                if let Some(target) = st.target_pos.take() {
122                    st.buf.clear();
123                    st.ring_start = target;
124                    st.eof = matches!(st.total_len, Some(end) if target >= end);
125                    st.err = None;
126                    // Reader may already be sleeping on not_empty waiting
127                    // for data at the new position. Wake it so it sees the
128                    // updated ring_start / eof state.
129                    shared.not_empty.notify_all();
130                    drop(st);
131                    if let Err(e) = inner.seek(SeekFrom::Start(target)) {
132                        let mut st = shared.state.lock().unwrap();
133                        st.err = Some(e);
134                        shared.not_empty.notify_all();
135                        return;
136                    }
137                    st = shared.state.lock().unwrap();
138                    continue;
139                }
140                if st.eof {
141                    // No more data to fetch; sleep until reader seeks or drops.
142                    st = shared.not_full.wait(st).unwrap();
143                    continue;
144                }
145                let free = st.capacity - st.buf.len();
146                if free == 0 {
147                    // Wait for reader to drain.
148                    st = shared.not_full.wait(st).unwrap();
149                    continue;
150                }
151                to_read = free.min(BLOCK);
152                break;
153            }
154        }
155
156        // Phase 2: read into scratch outside the lock.
157        let read_result = inner.read(&mut scratch[..to_read]);
158
159        // Phase 3: deposit in ring or surface error / EOF.
160        let mut st = shared.state.lock().unwrap();
161        // Reader may have requested a seek while we were reading; if so,
162        // discard what we just read and let phase 1 handle it next loop.
163        if st.target_pos.is_some() || st.stop {
164            continue;
165        }
166        match read_result {
167            Ok(0) => {
168                st.eof = true;
169                shared.not_empty.notify_all();
170            }
171            Ok(n) => {
172                st.buf.extend(scratch[..n].iter().copied());
173                shared.not_empty.notify_all();
174            }
175            Err(e) => {
176                st.err = Some(e);
177                shared.not_empty.notify_all();
178                return;
179            }
180        }
181    }
182}
183
184impl Read for BufferedSource {
185    fn read(&mut self, out: &mut [u8]) -> io::Result<usize> {
186        if out.is_empty() {
187            return Ok(0);
188        }
189        let mut st = self.shared.state.lock().unwrap();
190        loop {
191            if let Some(e) = st.err.take() {
192                return Err(e);
193            }
194            // Position relative to ring_start.
195            let rel = self.pos.saturating_sub(st.ring_start) as usize;
196            // If reader is somehow before ring_start (shouldn't happen — Seek
197            // bumps target_pos), surface as InvalidInput.
198            if self.pos < st.ring_start {
199                return Err(io::Error::new(
200                    io::ErrorKind::InvalidInput,
201                    "BufferedSource: reader behind ring start",
202                ));
203            }
204            if rel < st.buf.len() {
205                // Hit. Copy out.
206                let avail = st.buf.len() - rel;
207                let n = avail.min(out.len());
208                // VecDeque slice view via into-iterator — copy element-wise.
209                for (i, byte) in st.buf.iter().skip(rel).take(n).enumerate() {
210                    out[i] = *byte;
211                }
212                self.pos += n as u64;
213                // If we've consumed past the front of the ring, drop those
214                // bytes so the worker can refill.
215                let drop_n = rel + n;
216                // But keep some slack so backward seeks within recent past
217                // still hit. Use 1/8 of capacity as the "rear" the reader
218                // can lookback into without re-fetching.
219                let rear = st.capacity / 8;
220                if drop_n > rear {
221                    let to_drop = drop_n - rear;
222                    st.buf.drain(..to_drop);
223                    st.ring_start += to_drop as u64;
224                    self.shared.not_full.notify_one();
225                }
226                return Ok(n);
227            }
228            // Miss: at or past the end of the ring.
229            if st.eof {
230                return Ok(0);
231            }
232            // Wait for worker to push more bytes — bounded so a stuck
233            // worker becomes visible rather than deadlocking forever.
234            let (new_st, wait_result) = self
235                .shared
236                .not_empty
237                .wait_timeout(st, Duration::from_secs(30))
238                .unwrap();
239            st = new_st;
240            if wait_result.timed_out() && st.err.is_none() && !st.eof {
241                return Err(io::Error::new(
242                    io::ErrorKind::TimedOut,
243                    "BufferedSource: prefetch timeout (30s)",
244                ));
245            }
246        }
247    }
248}
249
250impl Seek for BufferedSource {
251    fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
252        let mut st = self.shared.state.lock().unwrap();
253        let total = st.total_len;
254        let new_pos: u64 = match from {
255            SeekFrom::Start(n) => n,
256            SeekFrom::Current(d) => add_signed(self.pos, d)?,
257            SeekFrom::End(d) => {
258                let end = total.ok_or_else(|| {
259                    io::Error::new(io::ErrorKind::Unsupported, "stream length unknown")
260                })?;
261                add_signed(end, d)?
262            }
263        };
264        // If the new position is inside the current ring window, just
265        // update the cursor — no IO needed.
266        let ring_end = st.ring_start + st.buf.len() as u64;
267        if new_pos >= st.ring_start && new_pos <= ring_end {
268            self.pos = new_pos;
269            return Ok(new_pos);
270        }
271        // Otherwise tell the worker to reposition the inner source and
272        // restart prefetch from `new_pos`. Reset ring state here under the
273        // lock so that `self.pos == ring_start` is invariant by the time
274        // Seek returns — otherwise a Read call landing before the worker
275        // acts on `target_pos` would see `self.pos < ring_start` (for
276        // backward seeks) and wrongly return "reader behind ring start".
277        st.target_pos = Some(new_pos);
278        st.buf.clear();
279        st.ring_start = new_pos;
280        st.eof = matches!(total, Some(end) if new_pos >= end);
281        st.err = None;
282        self.pos = new_pos;
283        self.shared.not_full.notify_all();
284        self.shared.not_empty.notify_all();
285        Ok(new_pos)
286    }
287}
288
289fn add_signed(base: u64, delta: i64) -> io::Result<u64> {
290    if delta >= 0 {
291        base.checked_add(delta as u64)
292            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek overflow"))
293    } else {
294        let mag = delta.unsigned_abs();
295        base.checked_sub(mag)
296            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "seek before start"))
297    }
298}
299
300impl Drop for BufferedSource {
301    fn drop(&mut self) {
302        {
303            let mut st = self.shared.state.lock().unwrap();
304            st.stop = true;
305        }
306        self.shared.not_full.notify_all();
307        self.shared.not_empty.notify_all();
308        if let Some(h) = self.worker.take() {
309            let _ = h.join();
310        }
311    }
312}