Skip to main content

gwseq_io/source/
sink.rs

1//! The write side.
2//!
3//! Mirrors [`ByteSource`](crate::source::ByteSource) for the bbi writer, which
4//! needs positioned writes: its header carries counts and offsets whose values
5//! are only known once the sections, the R-tree and the zoom levels have been
6//! built, so they are reserved at open and patched at close.
7//!
8//! One handle serves the writer's read-back pass too. Building zoom levels
9//! means reading the data section back while the file is still open for
10//! writing; a second read-only handle would see only what had been flushed. A
11//! `std::fs::File` opened `.read(true).write(true)` does both, and `read_at`
12//! on it is the same positioned read as everywhere else — one handle, one
13//! flush, no second view of the file that can lag.
14
15use std::fs::File;
16use std::path::Path;
17
18use bytes::Bytes;
19
20use crate::error::{Error, Result};
21use crate::source::ByteSource;
22
23/// Bytes staged before a write reaches the file. A block is a few kilobytes, so
24/// this batches a few hundred of them into one call.
25pub const WRITE_BUFFER_SIZE: usize = 1 << 20;
26
27pub trait ByteSink: Send {
28    fn path(&self) -> &str;
29
30    /// Append, returning the offset the data landed at.
31    fn append(&mut self, data: &[u8]) -> Result<u64>;
32
33    /// Overwrite bytes already written — a reserved header field, an item count.
34    fn write_all_at(&mut self, offset: u64, data: &[u8]) -> Result<()>;
35
36    /// Bytes written so far, which is also the offset the next append lands at.
37    fn position(&self) -> u64;
38
39    fn flush(&mut self) -> Result<()>;
40
41    /// Finish. Idempotent, and also called from `Drop` — but a `Drop` that
42    /// fails cannot report, so the writer's own `close()` is what callers use
43    /// and what returns the error.
44    fn close(&mut self) -> Result<()>;
45}
46
47/// A local file opened for both directions, so the zoom pass reads back through
48/// the handle that wrote.
49#[derive(Debug)]
50pub struct LocalSink {
51    /// `None` once closed.
52    file: Option<File>,
53    path: String,
54    /// Staged bytes and the offset the first of them belongs at. `position` is
55    /// the end of the staged bytes, i.e. what a caller sees as the file's
56    /// length — the writer hands out offsets from it before the bytes have
57    /// reached the disk.
58    stage: Vec<u8>,
59    stage_offset: u64,
60    position: u64,
61}
62
63impl LocalSink {
64    pub fn create(path: impl AsRef<Path>) -> Result<Self> {
65        let path = path.as_ref();
66        let display = path.to_string_lossy().into_owned();
67        let file = File::options()
68            // Truncating rather than appending: a writer starts a file, it does
69            // not continue one, and a stale tail past the trailing magic would
70            // be the one thing a bbi reader cannot notice.
71            .read(true)
72            .write(true)
73            .create(true)
74            .truncate(true)
75            .open(path)
76            .map_err(|e| Error::io(&display, e))?;
77        Ok(Self {
78            file: Some(file),
79            path: display,
80            stage: Vec::with_capacity(WRITE_BUFFER_SIZE),
81            stage_offset: 0,
82            position: 0,
83        })
84    }
85
86    /// Drop the handle and remove the file, without flushing what is staged.
87    ///
88    /// What a writer that has given up wants: `create` truncated the path at
89    /// open, so leaving quietly still leaves a zero-magic stub behind that the
90    /// caller has to clean up. Nothing staged is worth writing to a file about
91    /// to be unlinked.
92    ///
93    /// The handle goes first because Windows refuses to remove an open file.
94    /// A removal that fails — the path already gone, a directory that is not
95    /// writable — is not reported: this is the cleanup path of an error that is
96    /// already on its way to the caller, and a second error would only hide the
97    /// first.
98    pub fn discard(&mut self) {
99        self.file = None;
100        self.stage.clear();
101        let _ = std::fs::remove_file(&self.path);
102    }
103
104    /// A read view over what has been written, for the zoom pass. Flushes
105    /// first, so the view never lags the writer.
106    pub fn as_source(&mut self) -> Result<SinkSource<'_>> {
107        self.flush()?;
108        let path = self.path.clone();
109        let file = self.file.as_ref().ok_or(Error::Closed { path })?;
110        Ok(SinkSource {
111            file,
112            path: &self.path,
113            len: self.position,
114        })
115    }
116
117    fn handle(&self) -> Result<&File> {
118        self.file.as_ref().ok_or_else(|| Error::Closed {
119            path: self.path.clone(),
120        })
121    }
122
123    /// Push the staged bytes out.
124    ///
125    /// `what` describes the moment for the error message: a full device is
126    /// usually only reported here, the appends before it having gone into the
127    /// buffer, so the wording says which of the three moments it was.
128    fn spill(&mut self, what: &str) -> Result<()> {
129        if self.stage.is_empty() {
130            return Ok(());
131        }
132        let offset = self.stage_offset;
133        {
134            let file = self.handle()?;
135            // The `io::Error` says whether the device is full, read-only or
136            // gone, which is the difference between "make room" and "this was
137            // never going to work". Dropping it left every failed write saying
138            // the same sentence.
139            pwrite(file, offset, &self.stage)
140                .map_err(|e| Error::write(&self.path, format!("{what}: {e}")))?;
141        }
142        self.stage_offset += self.stage.len() as u64;
143        self.stage.clear();
144        Ok(())
145    }
146}
147
148impl ByteSink for LocalSink {
149    fn path(&self) -> &str {
150        &self.path
151    }
152
153    fn append(&mut self, data: &[u8]) -> Result<u64> {
154        let offset = self.position;
155        self.stage.extend_from_slice(data);
156        self.position += data.len() as u64;
157        if self.stage.len() >= WRITE_BUFFER_SIZE {
158            self.spill("the device reported an error while writing to it")?;
159        }
160        Ok(offset)
161    }
162
163    fn write_all_at(&mut self, offset: u64, data: &[u8]) -> Result<()> {
164        // Staged bytes go out first: a patch may land inside them — the header
165        // is patched at close, and by then nothing of it is staged — and
166        // writing around them would let the spill overwrite the patch.
167        self.spill("the device reported an error while writing to it")?;
168        let file = self.handle()?;
169        pwrite(file, offset, data).map_err(|e| {
170            Error::write(
171                &self.path,
172                format!("the device reported an error while writing to it: {e}"),
173            )
174        })?;
175        // A patch may reach past what has been appended: the writer reserves
176        // its prefix by appending zeroes, so in practice it never does, but a
177        // sink that let `position` lag the file would hand out an offset that
178        // overwrites.
179        self.position = self.position.max(offset + data.len() as u64);
180        self.stage_offset = self.stage_offset.max(self.position);
181        Ok(())
182    }
183
184    fn position(&self) -> u64 {
185        self.position
186    }
187
188    fn flush(&mut self) -> Result<()> {
189        self.spill("the device reported an error while flushing it")?;
190        // No `sync_all`: the read view is the same handle, so it sees the
191        // written bytes without the file reaching the disk, and forcing it to
192        // would cost a fsync per zoom level for nothing.
193        Ok(())
194    }
195
196    fn close(&mut self) -> Result<()> {
197        if self.file.is_none() {
198            return Ok(());
199        }
200        self.spill("the device reported an error while closing it, so the file is incomplete")?;
201        // Dropped rather than `sync_all`ed, for the same reason `flush` does
202        // not: durability across a power cut is not what this file promises,
203        // and the caller can fsync the path if it is.
204        self.file = None;
205        Ok(())
206    }
207}
208
209impl Drop for LocalSink {
210    fn drop(&mut self) {
211        let _ = self.close();
212    }
213}
214
215/// The read half of a [`LocalSink`], borrowed for as long as the zoom pass
216/// needs it.
217///
218/// Not `LocalSource`: that snapshots the length at open, which is exactly wrong
219/// for a file still being written. This one carries the sink's own position,
220/// which is the only length that is true.
221#[derive(Debug)]
222pub struct SinkSource<'a> {
223    file: &'a File,
224    path: &'a str,
225    len: u64,
226}
227
228impl ByteSource for SinkSource<'_> {
229    fn path(&self) -> &str {
230        self.path
231    }
232
233    fn len(&self) -> Result<u64> {
234        Ok(self.len)
235    }
236
237    fn read_at(&self, offset: u64, len: usize) -> Result<Bytes> {
238        if len == 0 || offset >= self.len {
239            return Ok(Bytes::new());
240        }
241        let len = len.min((self.len - offset) as usize);
242        let mut buf = vec![0u8; len];
243        let read = pread(self.file, offset, &mut buf).map_err(|e| Error::io(self.path, e))?;
244        buf.truncate(read);
245        Ok(Bytes::from(buf))
246    }
247
248    fn close(&self) {
249        // The sink owns the handle; a borrowed view does not get to close it.
250    }
251}
252
253/// Positioned write, looping until the whole buffer is out.
254///
255/// One `write_at` may take less than it was given, so the loop is not optional.
256#[cfg(unix)]
257fn pwrite(file: &File, offset: u64, buf: &[u8]) -> std::io::Result<()> {
258    use std::os::unix::fs::FileExt;
259    let mut written = 0;
260    while written < buf.len() {
261        match file.write_at(&buf[written..], offset + written as u64) {
262            Ok(0) => {
263                return Err(std::io::Error::new(
264                    std::io::ErrorKind::WriteZero,
265                    "write returned zero bytes",
266                ))
267            }
268            Ok(n) => written += n,
269            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
270            Err(e) => return Err(e),
271        }
272    }
273    Ok(())
274}
275
276#[cfg(windows)]
277fn pwrite(file: &File, offset: u64, buf: &[u8]) -> std::io::Result<()> {
278    use std::os::windows::fs::FileExt;
279    let mut written = 0;
280    while written < buf.len() {
281        match file.seek_write(&buf[written..], offset + written as u64) {
282            Ok(0) => {
283                return Err(std::io::Error::new(
284                    std::io::ErrorKind::WriteZero,
285                    "write returned zero bytes",
286                ))
287            }
288            Ok(n) => written += n,
289            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
290            Err(e) => return Err(e),
291        }
292    }
293    Ok(())
294}
295
296#[cfg(unix)]
297fn pread(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
298    use std::os::unix::fs::FileExt;
299    let mut filled = 0;
300    while filled < buf.len() {
301        match file.read_at(&mut buf[filled..], offset + filled as u64) {
302            Ok(0) => break,
303            Ok(n) => filled += n,
304            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
305            Err(e) => return Err(e),
306        }
307    }
308    Ok(filled)
309}
310
311#[cfg(windows)]
312fn pread(file: &File, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> {
313    use std::os::windows::fs::FileExt;
314    let mut filled = 0;
315    while filled < buf.len() {
316        match file.seek_read(&mut buf[filled..], offset + filled as u64) {
317            Ok(0) => break,
318            Ok(n) => filled += n,
319            Err(e) if e.kind() == std::io::ErrorKind::Interrupted => {}
320            Err(e) => return Err(e),
321        }
322    }
323    Ok(filled)
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    fn temp(name: &str) -> std::path::PathBuf {
331        let dir = std::env::temp_dir().join("gwseq_sink_tests");
332        std::fs::create_dir_all(&dir).unwrap();
333        dir.join(name)
334    }
335
336    #[test]
337    fn appends_land_end_to_end_and_report_their_offsets() {
338        let path = temp("append.bin");
339        let mut sink = LocalSink::create(&path).unwrap();
340        assert_eq!(sink.append(b"hello").unwrap(), 0);
341        assert_eq!(sink.append(b" world").unwrap(), 5);
342        assert_eq!(sink.position(), 11);
343        sink.close().unwrap();
344        assert_eq!(std::fs::read(&path).unwrap(), b"hello world");
345        std::fs::remove_file(&path).ok();
346    }
347
348    #[test]
349    fn a_patch_overwrites_staged_bytes_rather_than_racing_them() {
350        // The staged bytes have not reached the file when the patch is made,
351        // so a sink that wrote the patch and then spilled the stage would lose
352        // it. This is the header case, where every reserved field is patched.
353        let path = temp("patch.bin");
354        let mut sink = LocalSink::create(&path).unwrap();
355        sink.append(&[0u8; 16]).unwrap();
356        sink.append(b"tail").unwrap();
357        sink.write_all_at(4, b"ABCD").unwrap();
358        sink.close().unwrap();
359        let bytes = std::fs::read(&path).unwrap();
360        assert_eq!(&bytes[0..4], &[0, 0, 0, 0]);
361        assert_eq!(&bytes[4..8], b"ABCD");
362        assert_eq!(&bytes[16..20], b"tail");
363        std::fs::remove_file(&path).ok();
364    }
365
366    #[test]
367    fn the_read_view_sees_what_has_only_been_staged() {
368        // What the zoom pass depends on: it reads back sections it wrote a
369        // moment ago, most of which are still in the buffer.
370        let path = temp("readback.bin");
371        let mut sink = LocalSink::create(&path).unwrap();
372        sink.append(b"0123456789").unwrap();
373        {
374            let source = sink.as_source().unwrap();
375            assert_eq!(source.len().unwrap(), 10);
376            assert_eq!(&source.read_at(3, 4).unwrap()[..], b"3456");
377        }
378        // And writing goes on afterwards, at the right offset.
379        assert_eq!(sink.append(b"abc").unwrap(), 10);
380        sink.close().unwrap();
381        assert_eq!(std::fs::read(&path).unwrap(), b"0123456789abc");
382        std::fs::remove_file(&path).ok();
383    }
384
385    #[test]
386    fn a_write_larger_than_the_buffer_still_lands_whole() {
387        let path = temp("large.bin");
388        let mut sink = LocalSink::create(&path).unwrap();
389        let big = vec![7u8; WRITE_BUFFER_SIZE + 1234];
390        sink.append(&big).unwrap();
391        sink.append(b"end").unwrap();
392        sink.close().unwrap();
393        let bytes = std::fs::read(&path).unwrap();
394        assert_eq!(bytes.len(), big.len() + 3);
395        assert!(bytes[..big.len()].iter().all(|b| *b == 7));
396        assert_eq!(&bytes[big.len()..], b"end");
397        std::fs::remove_file(&path).ok();
398    }
399
400    #[test]
401    fn closing_twice_is_harmless_and_writing_after_it_is_not() {
402        let path = temp("closed.bin");
403        let mut sink = LocalSink::create(&path).unwrap();
404        sink.append(b"x").unwrap();
405        sink.close().unwrap();
406        sink.close().unwrap();
407        // The stage is empty, so the failure comes from the handle being gone.
408        let err = sink.write_all_at(0, b"y").unwrap_err();
409        assert!(matches!(err, Error::Closed { .. }), "{err}");
410        std::fs::remove_file(&path).ok();
411    }
412
413    #[test]
414    fn a_truncating_create_leaves_no_tail_of_an_older_file() {
415        let path = temp("truncate.bin");
416        std::fs::write(&path, vec![9u8; 4096]).unwrap();
417        let mut sink = LocalSink::create(&path).unwrap();
418        sink.append(b"new").unwrap();
419        sink.close().unwrap();
420        assert_eq!(std::fs::read(&path).unwrap(), b"new");
421        std::fs::remove_file(&path).ok();
422    }
423}