Skip to main content

lance_io/
utils.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright The Lance Authors
3
4use std::{cmp::min, collections::VecDeque, num::NonZero, ops::Range, sync::atomic::AtomicU64};
5
6use byteorder::{ByteOrder, LittleEndian};
7use bytes::{Buf, Bytes};
8use futures::{Stream, StreamExt, TryStreamExt};
9use lance_core::deepsize::DeepSizeOf;
10use prost::Message;
11use serde::{Deserialize, Serialize};
12
13use crate::traits::{ProtoStruct, Reader};
14use lance_core::{Error, Result};
15
16pub mod tracking_store;
17
18/// Chunk size for splitting a large metadata read into concurrent range requests.
19///
20/// A single object-store GET streams its body over one connection, so its
21/// throughput is capped by the TCP window over the round-trip time; on
22/// high-latency links that tops out in the tens of MB/s. Fetching the range as
23/// a window of concurrent chunk requests multiplies that per-connection limit.
24/// 16 MiB keeps per-request overhead negligible (a ~1 GiB manifest costs ~64
25/// GET requests) while a `Reader::io_parallelism` window of such chunks is
26/// enough to saturate the link.
27pub const METADATA_READ_CHUNK_SIZE: usize = 16 * 1024 * 1024;
28
29/// Read `range` from `reader` as `chunk_size`-sized concurrent range requests,
30/// yielding the chunks in file order. Concurrency is bounded by
31/// [`Reader::io_parallelism`], clamped to at least 1: a `buffered(0)` window
32/// never polls its input, so an unvalidated reader value (e.g.
33/// `LANCE_URING_IO_PARALLELISM=0`) would hang the read.
34pub fn read_range_in_chunks(
35    reader: &dyn Reader,
36    range: Range<usize>,
37    chunk_size: usize,
38) -> impl Stream<Item = object_store::Result<Bytes>> + '_ {
39    let end = range.end;
40    let chunk_ranges = range
41        .step_by(chunk_size)
42        .map(move |start| start..min(start + chunk_size, end));
43    futures::stream::iter(chunk_ranges.map(|chunk| reader.get_range(chunk)))
44        .buffered(reader.io_parallelism().max(1))
45}
46
47/// A [`Buf`] over a sequence of [`Bytes`] chunks in file order.
48///
49/// Lets a protobuf message fetched as concurrent range requests be decoded
50/// without first copying the chunks into one contiguous buffer. A copy of a
51/// large message is not free: the destination pages are touched for the first
52/// time, so the copy costs a page fault per 4 KiB on top of the `memmove`
53/// (measured at ~0.5 s and ~1 GiB of peak memory for a 945 MiB manifest).
54/// `copy_to_bytes` hands out a slice of the underlying chunk when the request
55/// lies within one, so `bytes`-typed fields decode zero-copy as well.
56#[derive(Debug, Default)]
57pub struct ChunkedBuf {
58    chunks: VecDeque<Bytes>,
59    remaining: usize,
60}
61
62impl ChunkedBuf {
63    /// Append `chunk` after the chunks pushed so far. Empty chunks are dropped
64    /// so `chunk()` never reports an empty slice while data remains.
65    pub fn push(&mut self, chunk: Bytes) {
66        if !chunk.is_empty() {
67            self.remaining += chunk.len();
68            self.chunks.push_back(chunk);
69        }
70    }
71
72    /// Keep only the first `len` bytes.
73    ///
74    /// Prost decodes until the buffer is exhausted, so a message followed by
75    /// trailing bytes (a footer, the next section) must be cut to its exact
76    /// length before decoding.
77    pub fn truncate(&mut self, len: usize) {
78        if len >= self.remaining {
79            return;
80        }
81        let mut to_drop = self.remaining - len;
82        while to_drop > 0 {
83            let Some(back) = self.chunks.back_mut() else {
84                break;
85            };
86            if to_drop >= back.len() {
87                to_drop -= back.len();
88                self.chunks.pop_back();
89            } else {
90                back.truncate(back.len() - to_drop);
91                to_drop = 0;
92            }
93        }
94        self.remaining = len;
95    }
96}
97
98impl Buf for ChunkedBuf {
99    fn remaining(&self) -> usize {
100        self.remaining
101    }
102
103    fn chunk(&self) -> &[u8] {
104        self.chunks.front().map(Bytes::as_ref).unwrap_or(&[])
105    }
106
107    fn advance(&mut self, mut cnt: usize) {
108        assert!(
109            cnt <= self.remaining,
110            "cannot advance ChunkedBuf by {cnt} bytes: only {} remain",
111            self.remaining
112        );
113        self.remaining -= cnt;
114        while cnt > 0 {
115            let front = self
116                .chunks
117                .front_mut()
118                .expect("remaining bytes imply a chunk");
119            if cnt < front.len() {
120                front.advance(cnt);
121                return;
122            }
123            cnt -= front.len();
124            self.chunks.pop_front();
125        }
126    }
127
128    fn copy_to_bytes(&mut self, len: usize) -> Bytes {
129        assert!(
130            len <= self.remaining,
131            "cannot copy {len} bytes from ChunkedBuf: only {} remain",
132            self.remaining
133        );
134        let within_front = self.chunks.front().is_some_and(|front| len <= front.len());
135        if within_front {
136            let front = self
137                .chunks
138                .front_mut()
139                .expect("checked that a front chunk exists");
140            let bytes = front.split_to(len);
141            self.remaining -= len;
142            if front.is_empty() {
143                self.chunks.pop_front();
144            }
145            return bytes;
146        }
147        // Straddles a chunk boundary: a copy is unavoidable.
148        let mut out = Vec::with_capacity(len);
149        let mut left = len;
150        while left > 0 {
151            let chunk = self.chunk();
152            let take = min(left, chunk.len());
153            out.extend_from_slice(&chunk[..take]);
154            self.advance(take);
155            left -= take;
156        }
157        Bytes::from(out)
158    }
159}
160
161/// Read a protobuf message at file position 'pos'.
162///
163/// We write protobuf by first writing the length of the message as a u32,
164/// followed by the message itself.
165pub async fn read_message<M: Message + Default>(reader: &dyn Reader, pos: usize) -> Result<M> {
166    let file_size = reader.size().await?;
167    // A message is a u32 length prefix followed by its body; both must lie before
168    // the end. A `pos` too close to the end means the reader size is too small
169    // (e.g. a stale cached size). Reject it rather than slice a short buffer and
170    // panic.
171    if pos + 4 > file_size {
172        return Err(Error::io("file size is too small".to_string()));
173    }
174
175    let range = pos..min(pos + reader.block_size(), file_size);
176    let buf = reader.get_range(range.clone()).await?;
177    let msg_len = LittleEndian::read_u32(&buf) as usize;
178
179    if msg_len + 4 > buf.len() {
180        let remaining_range = range.end..min(4 + pos + msg_len, file_size);
181        // Fetching the remainder as concurrent chunks lifts the
182        // single-connection throughput cap on large messages (e.g. manifests of
183        // datasets with many fragments); decoding straight from the chunks
184        // avoids re-copying the whole message into one buffer.
185        let mut full = ChunkedBuf::default();
186        full.push(buf.slice(4..));
187        let mut chunks = read_range_in_chunks(reader, remaining_range, METADATA_READ_CHUNK_SIZE);
188        while let Some(chunk) = chunks.try_next().await? {
189            full.push(chunk);
190        }
191        if full.remaining() < msg_len {
192            return Err(Error::io("file size is too small".to_string()));
193        }
194        full.truncate(msg_len);
195        Ok(M::decode(full)?)
196    } else {
197        Ok(M::decode(buf.slice(4..4 + msg_len))?)
198    }
199}
200
201/// Read a Protobuf-backed struct at file position: `pos`.
202// TODO: pub(crate)
203pub async fn read_struct<
204    M: Message + Default + 'static,
205    T: ProtoStruct<Proto = M> + TryFrom<M, Error = Error>,
206>(
207    reader: &dyn Reader,
208    pos: usize,
209) -> Result<T> {
210    let msg = read_message::<M>(reader, pos).await?;
211    T::try_from(msg)
212}
213
214pub async fn read_last_block(reader: &dyn Reader) -> object_store::Result<Bytes> {
215    let file_size = reader.size().await?;
216    let block_size = reader.block_size();
217    let begin = file_size.saturating_sub(block_size);
218    reader.get_range(begin..file_size).await
219}
220
221pub fn read_metadata_offset(bytes: &Bytes) -> Result<usize> {
222    let len = bytes.len();
223    if len < 16 {
224        return Err(Error::io(format!(
225            "does not have sufficient data, len: {}, bytes: {:?}",
226            len, bytes
227        )));
228    }
229    let offset_bytes = bytes.slice(len - 16..len - 8);
230    Ok(LittleEndian::read_u64(offset_bytes.as_ref()) as usize)
231}
232
233/// Read the version from the footer bytes
234pub fn read_version(bytes: &Bytes) -> Result<(u16, u16)> {
235    let len = bytes.len();
236    if len < 8 {
237        return Err(Error::io(format!(
238            "does not have sufficient data, len: {}, bytes: {:?}",
239            len, bytes
240        )));
241    }
242
243    let major_version = LittleEndian::read_u16(bytes.slice(len - 8..len - 6).as_ref());
244    let minor_version = LittleEndian::read_u16(bytes.slice(len - 6..len - 4).as_ref());
245    Ok((major_version, minor_version))
246}
247
248/// Read protobuf from a buffer.
249pub fn read_message_from_buf<M: Message + Default>(buf: &Bytes) -> Result<M> {
250    let msg_len = LittleEndian::read_u32(buf) as usize;
251    Ok(M::decode(&buf[4..4 + msg_len])?)
252}
253
254/// Read a Protobuf-backed struct from a buffer.
255pub fn read_struct_from_buf<
256    M: Message + Default,
257    T: ProtoStruct<Proto = M> + TryFrom<M, Error = Error>,
258>(
259    buf: &Bytes,
260) -> Result<T> {
261    let msg: M = read_message_from_buf(buf)?;
262    T::try_from(msg)
263}
264
265/// A cached file size.
266///
267/// This wraps an atomic u64 to allow setting the cached file size without
268/// needed a mutable reference.
269///
270/// Zero is interpreted as unknown.
271#[derive(Debug, DeepSizeOf)]
272pub struct CachedFileSize(AtomicU64);
273
274impl<'de> Deserialize<'de> for CachedFileSize {
275    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
276    where
277        D: serde::Deserializer<'de>,
278    {
279        let size = Option::<u64>::deserialize(deserializer)?.unwrap_or(0);
280        Ok(Self::new(size))
281    }
282}
283
284impl Serialize for CachedFileSize {
285    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
286    where
287        S: serde::Serializer,
288    {
289        let size = self.0.load(std::sync::atomic::Ordering::Relaxed);
290        if size == 0 {
291            serializer.serialize_none()
292        } else {
293            serializer.serialize_u64(size)
294        }
295    }
296}
297
298impl From<Option<NonZero<u64>>> for CachedFileSize {
299    fn from(size: Option<NonZero<u64>>) -> Self {
300        match size {
301            Some(size) => Self(AtomicU64::new(size.into())),
302            None => Self(AtomicU64::new(0)),
303        }
304    }
305}
306
307impl Default for CachedFileSize {
308    fn default() -> Self {
309        Self(AtomicU64::new(0))
310    }
311}
312
313impl Clone for CachedFileSize {
314    fn clone(&self) -> Self {
315        Self(AtomicU64::new(
316            self.0.load(std::sync::atomic::Ordering::Relaxed),
317        ))
318    }
319}
320
321impl PartialEq for CachedFileSize {
322    fn eq(&self, other: &Self) -> bool {
323        self.0.load(std::sync::atomic::Ordering::Relaxed)
324            == other.0.load(std::sync::atomic::Ordering::Relaxed)
325    }
326}
327
328impl Eq for CachedFileSize {}
329
330impl CachedFileSize {
331    /// Create a `CachedFileSize` from a raw byte count.
332    ///
333    /// Passing `0` is equivalent to calling [`unknown`](Self::unknown): the
334    /// type interprets zero as "size not yet known".
335    pub fn new(size: u64) -> Self {
336        Self(AtomicU64::new(size))
337    }
338
339    pub fn unknown() -> Self {
340        Self(AtomicU64::new(0))
341    }
342
343    pub fn get(&self) -> Option<NonZero<u64>> {
344        NonZero::new(self.0.load(std::sync::atomic::Ordering::Relaxed))
345    }
346
347    pub fn set(&self, size: NonZero<u64>) {
348        self.0
349            .store(size.into(), std::sync::atomic::Ordering::Relaxed);
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use std::cmp::min;
356
357    use bytes::{Buf, Bytes, BytesMut};
358    use futures::TryStreamExt;
359    use object_store::path::Path;
360    use prost::Message;
361
362    use super::{ChunkedBuf, read_message};
363
364    use crate::{
365        Error, Result,
366        object_reader::CloudObjectReader,
367        object_store::{DEFAULT_DOWNLOAD_RETRY_COUNT, ObjectStore},
368        object_writer::ObjectWriter,
369        traits::{ProtoStruct, WriteExt, Writer},
370        utils::{METADATA_READ_CHUNK_SIZE, read_range_in_chunks, read_struct},
371    };
372
373    // Bytes is a prost::Message, since we don't have any .proto files in this crate we
374    // can use it to simulate a real message object.
375    #[derive(Debug, PartialEq)]
376    struct BytesWrapper(Bytes);
377
378    impl ProtoStruct for BytesWrapper {
379        type Proto = Bytes;
380    }
381
382    impl From<&BytesWrapper> for Bytes {
383        fn from(value: &BytesWrapper) -> Self {
384            value.0.clone()
385        }
386    }
387
388    impl TryFrom<Bytes> for BytesWrapper {
389        type Error = Error;
390        fn try_from(value: Bytes) -> Result<Self> {
391            Ok(Self(value))
392        }
393    }
394
395    #[tokio::test]
396    async fn test_write_proto_structs() {
397        let store = ObjectStore::memory();
398        let path = Path::from("/foo");
399
400        let mut object_writer = ObjectWriter::new(&store, &path).await.unwrap();
401        assert_eq!(object_writer.tell().await.unwrap(), 0);
402
403        let some_message = BytesWrapper(Bytes::from(vec![10, 20, 30]));
404
405        let pos = object_writer.write_struct(&some_message).await.unwrap();
406        assert_eq!(pos, 0);
407        object_writer.shutdown().await.unwrap();
408
409        let object_reader =
410            CloudObjectReader::new(store.inner, path, 1024, None, DEFAULT_DOWNLOAD_RETRY_COUNT)
411                .unwrap();
412        let actual: BytesWrapper = read_struct(&object_reader, pos).await.unwrap();
413        assert_eq!(some_message, actual);
414    }
415
416    #[tokio::test]
417    async fn test_read_range_in_chunks_reassembles_in_order() {
418        let store = ObjectStore::memory();
419        let path = Path::from("/chunked");
420        // Patterned data with a range that neither starts nor ends on a chunk
421        // boundary, so ordering or off-by-one mistakes change the bytes.
422        let data: Vec<u8> = (0..10 * 1024 + 37).map(|i| (i % 251) as u8).collect();
423        store.put(&path, &data).await.unwrap();
424        let reader = store.open(&path).await.unwrap();
425
426        let range = 5..data.len() - 3;
427        let mut assembled = BytesMut::new();
428        let mut chunks = read_range_in_chunks(reader.as_ref(), range.clone(), 1024);
429        while let Some(chunk) = chunks.try_next().await.unwrap() {
430            assembled.extend_from_slice(&chunk);
431        }
432        assert_eq!(assembled.as_ref(), &data[range]);
433    }
434
435    #[tokio::test]
436    async fn test_read_range_in_chunks_zero_parallelism_reader() {
437        // A reader advertising io_parallelism 0 (e.g. LANCE_URING_IO_PARALLELISM=0)
438        // must not hang the chunked read: the window is clamped to at least 1.
439        let store = ObjectStore::memory();
440        let path = Path::from("/zero_parallelism");
441        let data: Vec<u8> = (0..4096).map(|i| (i % 249) as u8).collect();
442        store.put(&path, &data).await.unwrap();
443        let reader =
444            CloudObjectReader::new(store.inner, path, 1024, None, DEFAULT_DOWNLOAD_RETRY_COUNT)
445                .unwrap()
446                .with_io_parallelism(0);
447
448        let assembled = tokio::time::timeout(std::time::Duration::from_secs(5), async {
449            let mut buf = BytesMut::new();
450            let mut chunks = read_range_in_chunks(&reader, 0..data.len(), 1024);
451            while let Some(chunk) = chunks.try_next().await.unwrap() {
452                buf.extend_from_slice(&chunk);
453            }
454            buf
455        })
456        .await
457        .expect("chunked read with a zero-parallelism reader must not hang");
458        assert_eq!(assembled.as_ref(), &data[..]);
459    }
460
461    #[tokio::test]
462    async fn test_read_message_larger_than_chunk_size() {
463        // A message body crossing METADATA_READ_CHUNK_SIZE forces read_message
464        // to fetch the remainder as multiple concurrent chunks.
465        let store = ObjectStore::memory();
466        let path = Path::from("/large_message");
467
468        let mut object_writer = ObjectWriter::new(&store, &path).await.unwrap();
469        let payload: Vec<u8> = (0..METADATA_READ_CHUNK_SIZE + 5 * 1024 * 1024)
470            .map(|i| (i % 253) as u8)
471            .collect();
472        let message = BytesWrapper(Bytes::from(payload));
473        let pos = object_writer.write_struct(&message).await.unwrap();
474        object_writer.shutdown().await.unwrap();
475
476        let object_reader =
477            CloudObjectReader::new(store.inner, path, 4096, None, DEFAULT_DOWNLOAD_RETRY_COUNT)
478                .unwrap();
479        let actual: BytesWrapper = read_struct(&object_reader, pos).await.unwrap();
480        assert_eq!(message, actual);
481    }
482
483    #[tokio::test]
484    async fn test_copy_reader_to_writer() {
485        let store = ObjectStore::memory();
486        let src = Path::from("/src");
487        let dst = Path::from("/dst");
488        store.put(&src, b"abcdef").await.unwrap();
489
490        let reader = store.open(&src).await.unwrap();
491        let mut writer = store.create(&dst).await.unwrap();
492        let copied = writer.copy_from_reader(reader.as_ref()).await.unwrap();
493        writer.shutdown().await.unwrap();
494
495        assert_eq!(copied, 6);
496        assert_eq!(store.read_one_all(&dst).await.unwrap().as_ref(), b"abcdef");
497    }
498
499    #[tokio::test]
500    async fn test_copy_reader_range_to_writer() {
501        let store = ObjectStore::memory();
502        let src = Path::from("/src-range");
503        let dst = Path::from("/dst-range");
504        store.put(&src, b"abcdef").await.unwrap();
505
506        let reader = store.open(&src).await.unwrap();
507        let mut writer = store.create(&dst).await.unwrap();
508        let copied = writer
509            .copy_range_from_reader(reader.as_ref(), 2..5)
510            .await
511            .unwrap();
512        writer.shutdown().await.unwrap();
513
514        assert_eq!(copied, 3);
515        assert_eq!(store.read_one_all(&dst).await.unwrap().as_ref(), b"cde");
516    }
517
518    #[test]
519    fn chunked_buf_advances_and_copies_across_chunks() {
520        let mut buf = ChunkedBuf::default();
521        buf.push(Bytes::from_static(b"abc"));
522        buf.push(Bytes::new());
523        buf.push(Bytes::from_static(b"defgh"));
524        buf.push(Bytes::from_static(b"ij"));
525        assert_eq!(buf.remaining(), 10);
526        assert_eq!(buf.chunk(), b"abc");
527
528        // Within one chunk: zero-copy slice of that chunk.
529        let first_chunk_ptr = buf.chunk().as_ptr();
530        let head = buf.copy_to_bytes(2);
531        assert_eq!(head.as_ref(), b"ab");
532        assert_eq!(head.as_ptr(), first_chunk_ptr);
533
534        // Straddling a boundary: copied, contents preserved, cursor advanced.
535        let cross = buf.copy_to_bytes(4);
536        assert_eq!(cross.as_ref(), b"cdef");
537        assert_eq!(buf.remaining(), 4);
538        assert_eq!(buf.chunk(), b"gh");
539
540        buf.advance(3);
541        assert_eq!(buf.chunk(), b"j");
542        assert_eq!(buf.copy_to_bytes(1).as_ref(), b"j");
543        assert_eq!(buf.remaining(), 0);
544        assert_eq!(buf.chunk(), b"");
545    }
546
547    #[test]
548    fn chunked_buf_truncate_drops_trailing_bytes() {
549        let mut buf = ChunkedBuf::default();
550        buf.push(Bytes::from_static(b"0123"));
551        buf.push(Bytes::from_static(b"4567"));
552        buf.push(Bytes::from_static(b"89"));
553        buf.truncate(20);
554        assert_eq!(buf.remaining(), 10);
555        buf.truncate(5);
556        assert_eq!(buf.remaining(), 5);
557        assert_eq!(buf.copy_to_bytes(5).as_ref(), b"01234");
558        assert_eq!(buf.remaining(), 0);
559    }
560
561    #[test]
562    fn chunked_buf_decodes_message_split_across_chunks() {
563        // `Bytes` implements `prost::Message` (one bytes field), which is
564        // exactly the shape whose decoding should be zero-copy within a chunk.
565        let payload: Vec<u8> = (0..20_000u32).map(|i| (i % 251) as u8).collect();
566        let msg = Bytes::from(payload);
567        let encoded = Bytes::from(msg.encode_to_vec());
568        for chunk_size in [1usize, 7, 64, 4096, usize::MAX] {
569            let mut buf = ChunkedBuf::default();
570            for start in (0..encoded.len()).step_by(chunk_size) {
571                buf.push(encoded.slice(start..min(start + chunk_size, encoded.len())));
572            }
573            // Trailing bytes (a footer) must be cut off before decoding.
574            buf.push(Bytes::from_static(b"footer"));
575            buf.truncate(encoded.len());
576            let decoded = Bytes::decode(buf).unwrap();
577            assert_eq!(decoded, msg, "chunk_size={chunk_size}");
578        }
579    }
580
581    #[tokio::test]
582    async fn read_message_decodes_body_larger_than_one_block_from_chunks() {
583        // A message larger than the reader block size takes the chunked path.
584        let store = ObjectStore::memory();
585        let path = Path::from("/large_message");
586        let payload = Bytes::from(
587            (0..300_000u32)
588                .map(|i| (i % 253) as u8)
589                .collect::<Vec<u8>>(),
590        );
591        let mut writer = store.create(&path).await.unwrap();
592        let pos = writer.write_protobuf(&payload).await.unwrap();
593        writer.write_magics(pos, 0, 1, b"LANC").await.unwrap();
594        writer.shutdown().await.unwrap();
595
596        let reader = store.open(&path).await.unwrap();
597        assert!(payload.len() > reader.block_size());
598        let decoded: Bytes = read_message(reader.as_ref(), pos).await.unwrap();
599        assert_eq!(decoded, payload);
600    }
601}