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_core::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}