···77repository = "https://tangled.org/@microcosm.blue/repo-stream"
8899[dependencies]
1010+bincode = { version = "2.0.1", features = ["serde"] }
1011futures = "0.3.31"
1112futures-core = "0.3.31"
1213ipld-core = { version = "0.4.2", features = ["serde"] }
1314iroh-car = "0.5.1"
1415log = "0.4.28"
1516multibase = "0.9.2"
1717+rusqlite = "0.37.0"
1618serde = { version = "1.0.228", features = ["derive"] }
1719serde_bytes = "0.11.19"
1820serde_ipld_dagcbor = "0.6.4"
2121+sha2 = "0.10.9"
1922thiserror = "2.0.17"
2020-tokio = "1.47.1"
2323+tokio = { version = "1.47.1", features = ["rt", "sync"] }
21242225[dev-dependencies]
2326clap = { version = "4.5.48", features = ["derive"] }
2427criterion = { version = "0.7.0", features = ["async_tokio"] }
2528env_logger = "0.11.8"
2629multibase = "0.9.2"
3030+tempfile = "3.23.0"
2731tokio = { version = "1.47.1", features = ["full"] }
28322933[profile.profiling]
3034inherits = "release"
3135debug = true
3636+3737+# [profile.release]
3838+# debug = true
32393340[[bench]]
3441name = "non-huge-cars"
+12-21
benches/huge-car.rs
···11extern crate repo_stream;
22-use futures::TryStreamExt;
33-use iroh_car::CarReader;
44-use std::convert::Infallible;
22+use repo_stream::Driver;
53use std::path::{Path, PathBuf};
6475use criterion::{Criterion, criterion_group, criterion_main};
···2018 });
2119}
22202323-async fn drive_car(filename: impl AsRef<Path>) {
2121+async fn drive_car(filename: impl AsRef<Path>) -> usize {
2422 let reader = tokio::fs::File::open(filename).await.unwrap();
2523 let reader = tokio::io::BufReader::new(reader);
2626- let reader = CarReader::new(reader).await.unwrap();
27242828- let root = reader
2929- .header()
3030- .roots()
3131- .first()
3232- .ok_or("missing root")
2525+ let mut driver = match Driver::load_car(reader, |block| block.len(), 1024)
2626+ .await
3327 .unwrap()
3434- .clone();
3535-3636- let stream = std::pin::pin!(reader.stream());
3737-3838- let (_commit, v) =
3939- repo_stream::drive::Vehicle::init(root, stream, |block| Ok::<_, Infallible>(block.len()))
4040- .await
4141- .unwrap();
4242- let mut record_stream = std::pin::pin!(v.stream());
2828+ {
2929+ Driver::Memory(_, mem_driver) => mem_driver,
3030+ Driver::Disk(_) => panic!("not doing disk for benchmark"),
3131+ };
43324444- while let Some(_) = record_stream.try_next().await.unwrap() {
4545- // just here for the drive
3333+ let mut n = 0;
3434+ while let Some(pairs) = driver.next_chunk(256).await.unwrap() {
3535+ n += pairs.len();
4636 }
3737+ n
4738}
48394940criterion_group!(benches, criterion_benchmark);
+12-22
benches/non-huge-cars.rs
···11extern crate repo_stream;
22-use futures::TryStreamExt;
33-use iroh_car::CarReader;
44-use std::convert::Infallible;
22+use repo_stream::Driver;
5364use criterion::{Criterion, criterion_group, criterion_main};
75···2624 });
2725}
28262929-async fn drive_car(bytes: &[u8]) {
3030- let reader = CarReader::new(bytes).await.unwrap();
3131-3232- let root = reader
3333- .header()
3434- .roots()
3535- .first()
3636- .ok_or("missing root")
2727+async fn drive_car(bytes: &[u8]) -> usize {
2828+ let mut driver = match Driver::load_car(bytes, |block| block.len(), 32)
2929+ .await
3730 .unwrap()
3838- .clone();
3939-4040- let stream = std::pin::pin!(reader.stream());
3131+ {
3232+ Driver::Memory(_, mem_driver) => mem_driver,
3333+ Driver::Disk(_) => panic!("not benching big cars here"),
3434+ };
41354242- let (_commit, v) =
4343- repo_stream::drive::Vehicle::init(root, stream, |block| Ok::<_, Infallible>(block.len()))
4444- .await
4545- .unwrap();
4646- let mut record_stream = std::pin::pin!(v.stream());
4747-4848- while let Some(_) = record_stream.try_next().await.unwrap() {
4949- // just here for the drive
3636+ let mut n = 0;
3737+ while let Some(pairs) = driver.next_chunk(256).await.unwrap() {
3838+ n += pairs.len();
5039 }
4040+ n
5141}
52425343criterion_group!(benches, criterion_benchmark);
+91
examples/disk-read-file/main.rs
···11+/*!
22+Read a CAR file by spilling to disk
33+*/
44+55+extern crate repo_stream;
66+use clap::Parser;
77+use repo_stream::{DiskBuilder, Driver, DriverBuilder};
88+use std::path::PathBuf;
99+1010+#[derive(Debug, Parser)]
1111+struct Args {
1212+ #[arg()]
1313+ car: PathBuf,
1414+ #[arg()]
1515+ tmpfile: PathBuf,
1616+}
1717+1818+#[tokio::main]
1919+async fn main() -> Result<(), Box<dyn std::error::Error>> {
2020+ env_logger::init();
2121+2222+ let Args { car, tmpfile } = Args::parse();
2323+2424+ // repo-stream takes an AsyncRead as input. wrapping a filesystem read in
2525+ // BufReader can provide a really significant performance win.
2626+ let reader = tokio::fs::File::open(car).await?;
2727+ let reader = tokio::io::BufReader::new(reader);
2828+2929+ log::info!("hello! reading the car...");
3030+3131+ // in this example we only bother handling CARs that are too big for memory
3232+ // `noop` helper means: do no block processing, store the raw blocks
3333+ let driver = match DriverBuilder::new()
3434+ .with_mem_limit_mb(10) // how much memory can be used before disk spill
3535+ .load_car(reader)
3636+ .await?
3737+ {
3838+ Driver::Memory(_, _) => panic!("try this on a bigger car"),
3939+ Driver::Disk(big_stuff) => {
4040+ // we reach here if the repo was too big and needs to be spilled to
4141+ // disk to continue
4242+4343+ // set up a disk store we can spill to
4444+ let disk_store = DiskBuilder::new().open(tmpfile).await?;
4545+4646+ // do the spilling, get back a (similar) driver
4747+ let (commit, driver) = big_stuff.finish_loading(disk_store).await?;
4848+4949+ // at this point you might want to fetch the account's signing key
5050+ // via the DID from the commit, and then verify the signature.
5151+ log::warn!("big's comit: {:?}", commit);
5252+5353+ // pop the driver back out to get some code indentation relief
5454+ driver
5555+ }
5656+ };
5757+5858+ // collect some random stats about the blocks
5959+ let mut n = 0;
6060+ let mut zeros = 0;
6161+6262+ log::info!("walking...");
6363+6464+ // this example uses the disk driver's channel mode: the tree walking is
6565+ // spawned onto a blocking thread, and we get chunks of rkey+blocks back
6666+ let (mut rx, join) = driver.to_channel(512);
6767+ while let Some(r) = rx.recv().await {
6868+ let pairs = r?;
6969+7070+ // keep a count of the total number of blocks seen
7171+ n += pairs.len();
7272+7373+ for (_, block) in pairs {
7474+ // for each block, count how many bytes are equal to '0'
7575+ // (this is just an example, you probably want to do something more
7676+ // interesting)
7777+ zeros += block.into_iter().filter(|&b| b == b'0').count()
7878+ }
7979+ }
8080+8181+ log::info!("arrived! joining rx...");
8282+8383+ // clean up the database. would be nice to do this in drop so it happens
8484+ // automatically, but some blocking work happens, so that's not allowed in
8585+ // async rust. 🤷♀️
8686+ join.await?.reset_store().await?;
8787+8888+ log::info!("done. n={n} zeros={zeros}");
8989+9090+ Ok(())
9191+}
+18-25
examples/read-file/main.rs
···11+/*!
22+Read a CAR file with in-memory processing
33+*/
44+15extern crate repo_stream;
26use clap::Parser;
33-use futures::TryStreamExt;
44-use iroh_car::CarReader;
55-use std::convert::Infallible;
77+use repo_stream::{Driver, DriverBuilder};
68use std::path::PathBuf;
79810type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
···2123 let reader = tokio::fs::File::open(file).await?;
2224 let reader = tokio::io::BufReader::new(reader);
23252424- println!("hello!");
2525-2626- let reader = CarReader::new(reader).await?;
2727-2828- let root = reader
2929- .header()
3030- .roots()
3131- .first()
3232- .ok_or("missing root")?
3333- .clone();
3434- log::debug!("root: {root:?}");
3535-3636- // let stream = Box::pin(reader.stream());
3737- let stream = std::pin::pin!(reader.stream());
3838-3939- let (commit, v) =
4040- repo_stream::drive::Vehicle::init(root, stream, |block| Ok::<_, Infallible>(block.len()))
4141- .await?;
4242- let mut record_stream = std::pin::pin!(v.stream());
2626+ let (commit, mut driver) = match DriverBuilder::new()
2727+ .with_block_processor(|block| block.len())
2828+ .load_car(reader)
2929+ .await?
3030+ {
3131+ Driver::Memory(commit, mem_driver) => (commit, mem_driver),
3232+ Driver::Disk(_) => panic!("this example doesn't handle big CARs"),
3333+ };
43344435 log::info!("got commit: {commit:?}");
45364646- while let Some((rkey, _rec)) = record_stream.try_next().await? {
4747- log::info!("got {rkey:?}");
3737+ let mut n = 0;
3838+ while let Some(pairs) = driver.next_chunk(256).await? {
3939+ n += pairs.len();
4040+ // log::info!("got {rkey:?}");
4841 }
4949- log::info!("bye!");
4242+ log::info!("bye! total records={n}");
50435144 Ok(())
5245}
+12-2
readme.md
···11# repo-stream
2233-Fast and (aspirationally) robust atproto CAR file processing in rust
33+Efficient and robust atproto CAR file processing in rust
44+55+todo
66+77+- [ ] get an *emtpy* car for the test suite
88+- [ ] implement a max size on disk limit
99+1010+1111+-----
1212+1313+older stuff (to clean up):
414515616current car processing times (records processed into their length usize, phil's dev machine):
···2737 -> yeah the commit is returned from init
2838- [ ] spec compliance todos
2939 - [x] assert that keys are ordered and fail if not
3030- - [ ] verify node mst depth from key (possibly pending [interop test fixes](https://github.com/bluesky-social/atproto-interop-tests/issues/5))
4040+ - [x] verify node mst depth from key (possibly pending [interop test fixes](https://github.com/bluesky-social/atproto-interop-tests/issues/5))
3141- [ ] performance todos
3242 - [x] consume the serialized nodes into a mutable efficient format
3343 - [ ] maybe customize the deserialize impl to do that directly?
+220
src/disk.rs
···11+/*!
22+Disk storage for blocks on disk
33+44+Currently this uses sqlite. In testing sqlite wasn't the fastest, but it seemed
55+to be the best behaved in terms of both on-disk space usage and memory usage.
66+77+```no_run
88+# use repo_stream::{DiskBuilder, DiskError};
99+# #[tokio::main]
1010+# async fn main() -> Result<(), DiskError> {
1111+let store = DiskBuilder::new()
1212+ .with_cache_size_mb(32)
1313+ .with_max_stored_mb(1024) // errors when >1GiB of processed blocks are inserted
1414+ .open("/some/path.db".into()).await?;
1515+# Ok(())
1616+# }
1717+```
1818+*/
1919+2020+use crate::drive::DriveError;
2121+use rusqlite::OptionalExtension;
2222+use std::path::PathBuf;
2323+2424+#[derive(Debug, thiserror::Error)]
2525+pub enum DiskError {
2626+ /// A wrapped database error
2727+ ///
2828+ /// (The wrapped err should probably be obscured to remove public-facing
2929+ /// sqlite bits)
3030+ #[error(transparent)]
3131+ DbError(#[from] rusqlite::Error),
3232+ /// A tokio blocking task failed to join
3333+ #[error("Failed to join a tokio blocking task: {0}")]
3434+ JoinError(#[from] tokio::task::JoinError),
3535+ /// The total size of stored blocks exceeded the allowed size
3636+ ///
3737+ /// If you need to process *really* big CARs, you can configure a higher
3838+ /// limit.
3939+ #[error("Maximum disk size reached")]
4040+ MaxSizeExceeded,
4141+ #[error("this error was replaced, seeing this is a bug.")]
4242+ #[doc(hidden)]
4343+ Stolen,
4444+}
4545+4646+impl DiskError {
4747+ /// hack for ownership challenges with the disk driver
4848+ pub(crate) fn steal(&mut self) -> Self {
4949+ let mut swapped = DiskError::Stolen;
5050+ std::mem::swap(self, &mut swapped);
5151+ swapped
5252+ }
5353+}
5454+5555+/// Builder-style disk store setup
5656+pub struct DiskBuilder {
5757+ /// Database in-memory cache allowance
5858+ ///
5959+ /// Default: 32 MiB
6060+ pub cache_size_mb: usize,
6161+ /// Database stored block size limit
6262+ ///
6363+ /// Default: 10 GiB
6464+ ///
6565+ /// Note: actual size on disk may be more, but should approximately scale
6666+ /// with this limit
6767+ pub max_stored_mb: usize,
6868+}
6969+7070+impl Default for DiskBuilder {
7171+ fn default() -> Self {
7272+ Self {
7373+ cache_size_mb: 32,
7474+ max_stored_mb: 10 * 1024, // 10 GiB
7575+ }
7676+ }
7777+}
7878+7979+impl DiskBuilder {
8080+ /// Begin configuring the storage with defaults
8181+ pub fn new() -> Self {
8282+ Default::default()
8383+ }
8484+ /// Set the in-memory cache allowance for the database
8585+ ///
8686+ /// Default: 32 MiB
8787+ pub fn with_cache_size_mb(mut self, size: usize) -> Self {
8888+ self.cache_size_mb = size;
8989+ self
9090+ }
9191+ /// Set the approximate stored block size limit
9292+ ///
9393+ /// Default: 10 GiB
9494+ pub fn with_max_stored_mb(mut self, max: usize) -> Self {
9595+ self.max_stored_mb = max;
9696+ self
9797+ }
9898+ /// Open and initialize the actual disk storage
9999+ pub async fn open(self, path: PathBuf) -> Result<DiskStore, DiskError> {
100100+ DiskStore::new(path, self.cache_size_mb, self.max_stored_mb).await
101101+ }
102102+}
103103+104104+/// On-disk block storage
105105+pub struct DiskStore {
106106+ conn: rusqlite::Connection,
107107+ max_stored: usize,
108108+ stored: usize,
109109+}
110110+111111+impl DiskStore {
112112+ /// Initialize a new disk store
113113+ pub async fn new(
114114+ path: PathBuf,
115115+ cache_mb: usize,
116116+ max_stored_mb: usize,
117117+ ) -> Result<Self, DiskError> {
118118+ let max_stored = max_stored_mb * 2_usize.pow(20);
119119+ let conn = tokio::task::spawn_blocking(move || {
120120+ let conn = rusqlite::Connection::open(path)?;
121121+122122+ let sqlite_one_mb = -(2_i64.pow(10)); // negative is kibibytes for sqlite cache_size
123123+124124+ // conn.pragma_update(None, "journal_mode", "OFF")?;
125125+ // conn.pragma_update(None, "journal_mode", "MEMORY")?;
126126+ conn.pragma_update(None, "journal_mode", "WAL")?;
127127+ // conn.pragma_update(None, "wal_autocheckpoint", "0")?; // this lets things get a bit big on disk
128128+ conn.pragma_update(None, "synchronous", "OFF")?;
129129+ conn.pragma_update(
130130+ None,
131131+ "cache_size",
132132+ (cache_mb as i64 * sqlite_one_mb).to_string(),
133133+ )?;
134134+ Self::reset_tables(&conn)?;
135135+136136+ Ok::<_, DiskError>(conn)
137137+ })
138138+ .await??;
139139+140140+ Ok(Self {
141141+ conn,
142142+ max_stored,
143143+ stored: 0,
144144+ })
145145+ }
146146+ pub(crate) fn get_writer(&'_ mut self) -> Result<SqliteWriter<'_>, DiskError> {
147147+ let tx = self.conn.transaction()?;
148148+ Ok(SqliteWriter {
149149+ tx,
150150+ stored: &mut self.stored,
151151+ max: self.max_stored,
152152+ })
153153+ }
154154+ pub(crate) fn get_reader<'conn>(&'conn self) -> Result<SqliteReader<'conn>, DiskError> {
155155+ let select_stmt = self.conn.prepare("SELECT val FROM blocks WHERE key = ?1")?;
156156+ Ok(SqliteReader { select_stmt })
157157+ }
158158+ /// Drop and recreate the kv table
159159+ pub async fn reset(self) -> Result<Self, DiskError> {
160160+ tokio::task::spawn_blocking(move || {
161161+ Self::reset_tables(&self.conn)?;
162162+ Ok(self)
163163+ })
164164+ .await?
165165+ }
166166+ fn reset_tables(conn: &rusqlite::Connection) -> Result<(), DiskError> {
167167+ conn.execute("DROP TABLE IF EXISTS blocks", ())?;
168168+ conn.execute(
169169+ "CREATE TABLE blocks (
170170+ key BLOB PRIMARY KEY NOT NULL,
171171+ val BLOB NOT NULL
172172+ ) WITHOUT ROWID",
173173+ (),
174174+ )?;
175175+ Ok(())
176176+ }
177177+}
178178+179179+pub(crate) struct SqliteWriter<'conn> {
180180+ tx: rusqlite::Transaction<'conn>,
181181+ stored: &'conn mut usize,
182182+ max: usize,
183183+}
184184+185185+impl SqliteWriter<'_> {
186186+ pub(crate) fn put_many(
187187+ &mut self,
188188+ kv: impl Iterator<Item = Result<(Vec<u8>, Vec<u8>), DriveError>>,
189189+ ) -> Result<(), DriveError> {
190190+ let mut insert_stmt = self
191191+ .tx
192192+ .prepare_cached("INSERT INTO blocks (key, val) VALUES (?1, ?2)")
193193+ .map_err(DiskError::DbError)?;
194194+ for pair in kv {
195195+ let (k, v) = pair?;
196196+ *self.stored += v.len();
197197+ if *self.stored > self.max {
198198+ return Err(DiskError::MaxSizeExceeded.into());
199199+ }
200200+ insert_stmt.execute((k, v)).map_err(DiskError::DbError)?;
201201+ }
202202+ Ok(())
203203+ }
204204+ pub fn commit(self) -> Result<(), DiskError> {
205205+ self.tx.commit()?;
206206+ Ok(())
207207+ }
208208+}
209209+210210+pub(crate) struct SqliteReader<'conn> {
211211+ select_stmt: rusqlite::Statement<'conn>,
212212+}
213213+214214+impl SqliteReader<'_> {
215215+ pub(crate) fn get(&mut self, key: Vec<u8>) -> rusqlite::Result<Option<Vec<u8>>> {
216216+ self.select_stmt
217217+ .query_one((&key,), |row| row.get(0))
218218+ .optional()
219219+ }
220220+}
+554-109
src/drive.rs
···11-//! Consume an MST block stream, producing an ordered stream of records
11+//! Consume a CAR from an AsyncRead, producing an ordered stream of records
2233-use futures::{Stream, TryStreamExt};
33+use crate::disk::{DiskError, DiskStore};
44+use crate::process::Processable;
45use ipld_core::cid::Cid;
66+use iroh_car::CarReader;
77+use serde::{Deserialize, Serialize};
58use std::collections::HashMap;
66-use std::error::Error;
99+use std::convert::Infallible;
1010+use tokio::{io::AsyncRead, sync::mpsc};
711812use crate::mst::{Commit, Node};
99-use crate::walk::{Step, Trip, Walker};
1313+use crate::walk::{Step, WalkError, Walker};
10141115/// Errors that can happen while consuming and emitting blocks and records
1216#[derive(Debug, thiserror::Error)]
1313-pub enum DriveError<E: Error> {
1414- #[error("Failed to initialize CarReader: {0}")]
1717+pub enum DriveError {
1818+ #[error("Error from iroh_car: {0}")]
1519 CarReader(#[from] iroh_car::Error),
1616- #[error("Car block stream error: {0}")]
1717- CarBlockError(Box<dyn Error>),
1820 #[error("Failed to decode commit block: {0}")]
1919- BadCommit(Box<dyn Error>),
2121+ BadBlock(#[from] serde_ipld_dagcbor::DecodeError<Infallible>),
2022 #[error("The Commit block reference by the root was not found")]
2123 MissingCommit,
2224 #[error("The MST block {0} could not be found")]
2325 MissingBlock(Cid),
2426 #[error("Failed to walk the mst tree: {0}")]
2525- Tripped(#[from] Trip<E>),
2727+ WalkError(#[from] WalkError),
2828+ #[error("CAR file had no roots")]
2929+ MissingRoot,
3030+ #[error("Storage error")]
3131+ StorageError(#[from] DiskError),
3232+ #[error("Encode error: {0}")]
3333+ BincodeEncodeError(#[from] bincode::error::EncodeError),
3434+ #[error("Tried to send on a closed channel")]
3535+ ChannelSendError, // SendError takes <T> which we don't need
3636+ #[error("Failed to join a task: {0}")]
3737+ JoinError(#[from] tokio::task::JoinError),
2638}
27392828-type CarBlock<E> = Result<(Cid, Vec<u8>), E>;
4040+#[derive(Debug, thiserror::Error)]
4141+pub enum DecodeError {
4242+ #[error(transparent)]
4343+ BincodeDecodeError(#[from] bincode::error::DecodeError),
4444+ #[error("extra bytes remained after decoding")]
4545+ ExtraGarbage,
4646+}
4747+4848+/// An in-order chunk of Rkey + (processed) Block pairs
4949+pub type BlockChunk<T> = Vec<(String, T)>;
29503030-#[derive(Debug)]
3131-pub enum MaybeProcessedBlock<T, E> {
5151+#[derive(Debug, Clone, Serialize, Deserialize)]
5252+pub(crate) enum MaybeProcessedBlock<T> {
3253 /// A block that's *probably* a Node (but we can't know yet)
3354 ///
3455 /// It *can be* a record that suspiciously looks a lot like a node, so we
···5071 /// There's an alternative here, which would be to kick unprocessable blocks
5172 /// back to Raw, or maybe even a new RawUnprocessable variant. Then we could
5273 /// surface the typed error later if needed by trying to reprocess.
5353- Processed(Result<T, E>),
7474+ Processed(T),
5475}
55765656-/// The core driver between the block stream and MST walker
5757-pub struct Vehicle<SE, S, T, P, PE>
5858-where
5959- S: Stream<Item = CarBlock<SE>>,
6060- P: Fn(&[u8]) -> Result<T, PE>,
6161- PE: Error,
6262-{
6363- block_stream: S,
6464- blocks: HashMap<Cid, MaybeProcessedBlock<T, PE>>,
6565- walker: Walker,
6666- process: P,
7777+impl<T: Processable> Processable for MaybeProcessedBlock<T> {
7878+ /// TODO this is probably a little broken
7979+ fn get_size(&self) -> usize {
8080+ use std::{cmp::max, mem::size_of};
8181+8282+ // enum is always as big as its biggest member?
8383+ let base_size = max(size_of::<Vec<u8>>(), size_of::<T>());
8484+8585+ let extra = match self {
8686+ Self::Raw(bytes) => bytes.len(),
8787+ Self::Processed(t) => t.get_size(),
8888+ };
8989+9090+ base_size + extra
9191+ }
6792}
68936969-impl<SE, S, T: Clone, P, PE> Vehicle<SE, S, T, P, PE>
7070-where
7171- SE: Error + 'static,
7272- S: Stream<Item = CarBlock<SE>> + Unpin,
7373- P: Fn(&[u8]) -> Result<T, PE>,
7474- PE: Error,
7575-{
7676- /// Set up the stream
9494+impl<T> MaybeProcessedBlock<T> {
9595+ fn maybe(process: fn(Vec<u8>) -> T, data: Vec<u8>) -> Self {
9696+ if Node::could_be(&data) {
9797+ MaybeProcessedBlock::Raw(data)
9898+ } else {
9999+ MaybeProcessedBlock::Processed(process(data))
100100+ }
101101+ }
102102+}
103103+104104+/// Read a CAR file, buffering blocks in memory or to disk
105105+pub enum Driver<R: AsyncRead + Unpin, T: Processable> {
106106+ /// All blocks fit within the memory limit
77107 ///
7878- /// This will eagerly consume blocks until the `Commit` object is found.
7979- /// *Usually* the it's the first block, but there is no guarantee.
108108+ /// You probably want to check the commit's signature. You can go ahead and
109109+ /// walk the MST right away.
110110+ Memory(Commit, MemDriver<T>),
111111+ /// Blocks exceed the memory limit
80112 ///
8181- /// ### Parameters
113113+ /// You'll need to provide a disk storage to continue. The commit will be
114114+ /// returned and can be validated only once all blocks are loaded.
115115+ Disk(NeedDisk<R, T>),
116116+}
117117+118118+/// Builder-style driver setup
119119+pub struct DriverBuilder {
120120+ pub mem_limit_mb: usize,
121121+}
122122+123123+impl Default for DriverBuilder {
124124+ fn default() -> Self {
125125+ Self { mem_limit_mb: 16 }
126126+ }
127127+}
128128+129129+impl DriverBuilder {
130130+ /// Begin configuring the driver with defaults
131131+ pub fn new() -> Self {
132132+ Default::default()
133133+ }
134134+ /// Set the in-memory size limit, in MiB
82135 ///
8383- /// `root`: CID of the commit object that is the root of the MST
136136+ /// Default: 16 MiB
137137+ pub fn with_mem_limit_mb(self, new_limit: usize) -> Self {
138138+ Self {
139139+ mem_limit_mb: new_limit,
140140+ }
141141+ }
142142+ /// Set the block processor
84143 ///
8585- /// `block_stream`: Input stream of raw CAR blocks
144144+ /// Default: noop, raw blocks will be emitted
145145+ pub fn with_block_processor<T: Processable>(
146146+ self,
147147+ p: fn(Vec<u8>) -> T,
148148+ ) -> DriverBuilderWithProcessor<T> {
149149+ DriverBuilderWithProcessor {
150150+ mem_limit_mb: self.mem_limit_mb,
151151+ block_processor: p,
152152+ }
153153+ }
154154+ /// Begin processing an atproto MST from a CAR file
155155+ pub async fn load_car<R: AsyncRead + Unpin>(
156156+ self,
157157+ reader: R,
158158+ ) -> Result<Driver<R, Vec<u8>>, DriveError> {
159159+ Driver::load_car(reader, crate::process::noop, self.mem_limit_mb).await
160160+ }
161161+}
162162+163163+/// Builder-style driver intermediate step
164164+///
165165+/// start from `DriverBuilder`
166166+pub struct DriverBuilderWithProcessor<T: Processable> {
167167+ pub mem_limit_mb: usize,
168168+ pub block_processor: fn(Vec<u8>) -> T,
169169+}
170170+171171+impl<T: Processable> DriverBuilderWithProcessor<T> {
172172+ /// Set the in-memory size limit, in MiB
86173 ///
8787- /// `process`: record-transforming callback:
174174+ /// Default: 16 MiB
175175+ pub fn with_mem_limit_mb(mut self, new_limit: usize) -> Self {
176176+ self.mem_limit_mb = new_limit;
177177+ self
178178+ }
179179+ /// Begin processing an atproto MST from a CAR file
180180+ pub async fn load_car<R: AsyncRead + Unpin>(
181181+ self,
182182+ reader: R,
183183+ ) -> Result<Driver<R, T>, DriveError> {
184184+ Driver::load_car(reader, self.block_processor, self.mem_limit_mb).await
185185+ }
186186+}
187187+188188+impl<R: AsyncRead + Unpin, T: Processable> Driver<R, T> {
189189+ /// Begin processing an atproto MST from a CAR file
88190 ///
8989- /// For tasks where records can be quickly processed into a *smaller*
9090- /// useful representation, you can do that eagerly as blocks come in by
9191- /// passing the processor as a callback here. This can reduce overall
9292- /// memory usage.
9393- pub async fn init(
9494- root: Cid,
9595- mut block_stream: S,
9696- process: P,
9797- ) -> Result<(Commit, Self), DriveError<PE>> {
9898- let mut blocks = HashMap::new();
191191+ /// Blocks will be loaded, processed, and buffered in memory. If the entire
192192+ /// processed size is under the `mem_limit_mb` limit, a `Driver::Memory`
193193+ /// will be returned along with a `Commit` ready for validation.
194194+ ///
195195+ /// If the `mem_limit_mb` limit is reached before loading all blocks, the
196196+ /// partial state will be returned as `Driver::Disk(needed)`, which can be
197197+ /// resumed by providing a `SqliteStorage` for on-disk block storage.
198198+ pub async fn load_car(
199199+ reader: R,
200200+ process: fn(Vec<u8>) -> T,
201201+ mem_limit_mb: usize,
202202+ ) -> Result<Driver<R, T>, DriveError> {
203203+ let max_size = mem_limit_mb * 2_usize.pow(20);
204204+ let mut mem_blocks = HashMap::new();
205205+206206+ let mut car = CarReader::new(reader).await?;
207207+208208+ let root = *car
209209+ .header()
210210+ .roots()
211211+ .first()
212212+ .ok_or(DriveError::MissingRoot)?;
213213+ log::debug!("root: {root:?}");
99214100215 let mut commit = None;
101216102102- while let Some((cid, data)) = block_stream
103103- .try_next()
104104- .await
105105- .map_err(|e| DriveError::CarBlockError(e.into()))?
106106- {
217217+ // try to load all the blocks into memory
218218+ let mut mem_size = 0;
219219+ while let Some((cid, data)) = car.next_block().await? {
220220+ // the root commit is a Special Third Kind of block that we need to make
221221+ // sure not to optimistically send to the processing function
107222 if cid == root {
108108- let c: Commit = serde_ipld_dagcbor::from_slice(&data)
109109- .map_err(|e| DriveError::BadCommit(e.into()))?;
223223+ let c: Commit = serde_ipld_dagcbor::from_slice(&data)?;
110224 commit = Some(c);
111111- break;
112112- } else {
113113- blocks.insert(
114114- cid,
115115- if Node::could_be(&data) {
116116- MaybeProcessedBlock::Raw(data)
117117- } else {
118118- MaybeProcessedBlock::Processed(process(&data))
119119- },
120120- );
225225+ continue;
226226+ }
227227+228228+ // remaining possible types: node, record, other. optimistically process
229229+ let maybe_processed = MaybeProcessedBlock::maybe(process, data);
230230+231231+ // stash (maybe processed) blocks in memory as long as we have room
232232+ mem_size += std::mem::size_of::<Cid>() + maybe_processed.get_size();
233233+ mem_blocks.insert(cid, maybe_processed);
234234+ if mem_size >= max_size {
235235+ return Ok(Driver::Disk(NeedDisk {
236236+ car,
237237+ root,
238238+ process,
239239+ max_size,
240240+ mem_blocks,
241241+ commit,
242242+ }));
121243 }
122244 }
123245124124- // we either broke out or read all the blocks without finding the commit...
246246+ // all blocks loaded and we fit in memory! hopefully we found the commit...
125247 let commit = commit.ok_or(DriveError::MissingCommit)?;
126248127249 let walker = Walker::new(commit.data);
128250129129- let me = Self {
130130- block_stream,
131131- blocks,
132132- walker,
133133- process,
134134- };
135135- Ok((commit, me))
251251+ Ok(Driver::Memory(
252252+ commit,
253253+ MemDriver {
254254+ blocks: mem_blocks,
255255+ walker,
256256+ process,
257257+ },
258258+ ))
259259+ }
260260+}
261261+262262+/// The core driver between the block stream and MST walker
263263+///
264264+/// In the future, PDSs will export CARs in a stream-friendly order that will
265265+/// enable processing them with tiny memory overhead. But that future is not
266266+/// here yet.
267267+///
268268+/// CARs are almost always in a stream-unfriendly order, so I'm reverting the
269269+/// optimistic stream features: we load all block first, then walk the MST.
270270+///
271271+/// This makes things much simpler: we only need to worry about spilling to disk
272272+/// in one place, and we always have a reasonable expecatation about how much
273273+/// work the init function will do. We can drop the CAR reader before walking,
274274+/// so the sync/async boundaries become a little easier to work around.
275275+#[derive(Debug)]
276276+pub struct MemDriver<T: Processable> {
277277+ blocks: HashMap<Cid, MaybeProcessedBlock<T>>,
278278+ walker: Walker,
279279+ process: fn(Vec<u8>) -> T,
280280+}
281281+282282+impl<T: Processable> MemDriver<T> {
283283+ /// Step through the record outputs, in rkey order
284284+ pub async fn next_chunk(&mut self, n: usize) -> Result<Option<BlockChunk<T>>, DriveError> {
285285+ let mut out = Vec::with_capacity(n);
286286+ for _ in 0..n {
287287+ // walk as far as we can until we run out of blocks or find a record
288288+ match self.walker.step(&mut self.blocks, self.process)? {
289289+ Step::Missing(cid) => return Err(DriveError::MissingBlock(cid)),
290290+ Step::Finish => break,
291291+ Step::Found { rkey, data } => {
292292+ out.push((rkey, data));
293293+ continue;
294294+ }
295295+ };
296296+ }
297297+298298+ if out.is_empty() {
299299+ Ok(None)
300300+ } else {
301301+ Ok(Some(out))
302302+ }
303303+ }
304304+}
305305+306306+/// A partially memory-loaded car file that needs disk spillover to continue
307307+pub struct NeedDisk<R: AsyncRead + Unpin, T: Processable> {
308308+ car: CarReader<R>,
309309+ root: Cid,
310310+ process: fn(Vec<u8>) -> T,
311311+ max_size: usize,
312312+ mem_blocks: HashMap<Cid, MaybeProcessedBlock<T>>,
313313+ pub commit: Option<Commit>,
314314+}
315315+316316+fn encode(v: impl Serialize) -> Result<Vec<u8>, bincode::error::EncodeError> {
317317+ bincode::serde::encode_to_vec(v, bincode::config::standard())
318318+}
319319+320320+pub(crate) fn decode<T: Processable>(bytes: &[u8]) -> Result<T, DecodeError> {
321321+ let (t, n) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())?;
322322+ if n != bytes.len() {
323323+ return Err(DecodeError::ExtraGarbage);
136324 }
325325+ Ok(t)
326326+}
137327138138- async fn drive_until(&mut self, cid_needed: Cid) -> Result<(), DriveError<PE>> {
139139- while let Some((cid, data)) = self
140140- .block_stream
141141- .try_next()
142142- .await
143143- .map_err(|e| DriveError::CarBlockError(e.into()))?
144144- {
145145- self.blocks.insert(
146146- cid,
147147- if Node::could_be(&data) {
148148- MaybeProcessedBlock::Raw(data)
149149- } else {
150150- MaybeProcessedBlock::Processed((self.process)(&data))
151151- },
152152- );
153153- if cid == cid_needed {
154154- return Ok(());
328328+impl<R: AsyncRead + Unpin, T: Processable + Send + 'static> NeedDisk<R, T> {
329329+ pub async fn finish_loading(
330330+ mut self,
331331+ mut store: DiskStore,
332332+ ) -> Result<(Commit, DiskDriver<T>), DriveError> {
333333+ // move store in and back out so we can manage lifetimes
334334+ // dump mem blocks into the store
335335+ store = tokio::task::spawn(async move {
336336+ let mut writer = store.get_writer()?;
337337+338338+ let kvs = self
339339+ .mem_blocks
340340+ .into_iter()
341341+ .map(|(k, v)| Ok(encode(v).map(|v| (k.to_bytes(), v))?));
342342+343343+ writer.put_many(kvs)?;
344344+ writer.commit()?;
345345+ Ok::<_, DriveError>(store)
346346+ })
347347+ .await??;
348348+349349+ let (tx, mut rx) = mpsc::channel::<Vec<(Cid, MaybeProcessedBlock<T>)>>(2);
350350+351351+ let store_worker = tokio::task::spawn_blocking(move || {
352352+ let mut writer = store.get_writer()?;
353353+354354+ while let Some(chunk) = rx.blocking_recv() {
355355+ let kvs = chunk
356356+ .into_iter()
357357+ .map(|(k, v)| Ok(encode(v).map(|v| (k.to_bytes(), v))?));
358358+ writer.put_many(kvs)?;
155359 }
360360+361361+ writer.commit()?;
362362+ Ok::<_, DriveError>(store)
363363+ }); // await later
364364+365365+ // dump the rest to disk (in chunks)
366366+ log::debug!("dumping the rest of the stream...");
367367+ loop {
368368+ let mut mem_size = 0;
369369+ let mut chunk = vec![];
370370+ loop {
371371+ let Some((cid, data)) = self.car.next_block().await? else {
372372+ break;
373373+ };
374374+ // we still gotta keep checking for the root since we might not have it
375375+ if cid == self.root {
376376+ let c: Commit = serde_ipld_dagcbor::from_slice(&data)?;
377377+ self.commit = Some(c);
378378+ continue;
379379+ }
380380+ // remaining possible types: node, record, other. optimistically process
381381+ // TODO: get the actual in-memory size to compute disk spill
382382+ let maybe_processed = MaybeProcessedBlock::maybe(self.process, data);
383383+ mem_size += std::mem::size_of::<Cid>() + maybe_processed.get_size();
384384+ chunk.push((cid, maybe_processed));
385385+ if mem_size >= self.max_size {
386386+ // soooooo if we're setting the db cache to max_size and then letting
387387+ // multiple chunks in the queue that are >= max_size, then at any time
388388+ // we might be using some multiple of max_size?
389389+ break;
390390+ }
391391+ }
392392+ if chunk.is_empty() {
393393+ break;
394394+ }
395395+ tx.send(chunk)
396396+ .await
397397+ .map_err(|_| DriveError::ChannelSendError)?;
156398 }
399399+ drop(tx);
400400+ log::debug!("done. waiting for worker to finish...");
157401158158- // if we never found the block
159159- Err(DriveError::MissingBlock(cid_needed))
402402+ store = store_worker.await??;
403403+404404+ log::debug!("worker finished.");
405405+406406+ let commit = self.commit.ok_or(DriveError::MissingCommit)?;
407407+408408+ let walker = Walker::new(commit.data);
409409+410410+ Ok((
411411+ commit,
412412+ DiskDriver {
413413+ process: self.process,
414414+ state: Some(BigState { store, walker }),
415415+ },
416416+ ))
417417+ }
418418+}
419419+420420+struct BigState {
421421+ store: DiskStore,
422422+ walker: Walker,
423423+}
424424+425425+/// MST walker that reads from disk instead of an in-memory hashmap
426426+pub struct DiskDriver<T: Clone> {
427427+ process: fn(Vec<u8>) -> T,
428428+ state: Option<BigState>,
429429+}
430430+431431+// for doctests only
432432+#[doc(hidden)]
433433+pub fn _get_fake_disk_driver() -> DiskDriver<Vec<u8>> {
434434+ use crate::process::noop;
435435+ DiskDriver {
436436+ process: noop,
437437+ state: None,
438438+ }
439439+}
440440+441441+impl<T: Processable + Send + 'static> DiskDriver<T> {
442442+ /// Walk the MST returning up to `n` rkey + record pairs
443443+ ///
444444+ /// ```no_run
445445+ /// # use repo_stream::{drive::{DiskDriver, DriveError, _get_fake_disk_driver}, process::noop};
446446+ /// # #[tokio::main]
447447+ /// # async fn main() -> Result<(), DriveError> {
448448+ /// # let mut disk_driver = _get_fake_disk_driver();
449449+ /// while let Some(pairs) = disk_driver.next_chunk(256).await? {
450450+ /// for (rkey, record) in pairs {
451451+ /// println!("{rkey}: size={}", record.len());
452452+ /// }
453453+ /// }
454454+ /// let store = disk_driver.reset_store().await?;
455455+ /// # Ok(())
456456+ /// # }
457457+ /// ```
458458+ pub async fn next_chunk(&mut self, n: usize) -> Result<Option<BlockChunk<T>>, DriveError> {
459459+ let process = self.process;
460460+461461+ // state should only *ever* be None transiently while inside here
462462+ let mut state = self.state.take().expect("DiskDriver must have Some(state)");
463463+464464+ // the big pain here is that we don't want to leave self.state in an
465465+ // invalid state (None), so all the error paths have to make sure it
466466+ // comes out again.
467467+ let (state, res) = tokio::task::spawn_blocking(
468468+ move || -> (BigState, Result<BlockChunk<T>, DriveError>) {
469469+ let mut reader_res = state.store.get_reader();
470470+ let reader: &mut _ = match reader_res {
471471+ Ok(ref mut r) => r,
472472+ Err(ref mut e) => {
473473+ // unfortunately we can't return the error directly because
474474+ // (for some reason) it's attached to the lifetime of the
475475+ // reader?
476476+ // hack a mem::swap so we can get it out :/
477477+ let e_swapped = e.steal();
478478+ // the pain: `state` *has to* outlive the reader
479479+ drop(reader_res);
480480+ return (state, Err(e_swapped.into()));
481481+ }
482482+ };
483483+484484+ let mut out = Vec::with_capacity(n);
485485+486486+ for _ in 0..n {
487487+ // walk as far as we can until we run out of blocks or find a record
488488+ let step = match state.walker.disk_step(reader, process) {
489489+ Ok(s) => s,
490490+ Err(e) => {
491491+ // the pain: `state` *has to* outlive the reader
492492+ drop(reader_res);
493493+ return (state, Err(e.into()));
494494+ }
495495+ };
496496+ match step {
497497+ Step::Missing(cid) => {
498498+ // the pain: `state` *has to* outlive the reader
499499+ drop(reader_res);
500500+ return (state, Err(DriveError::MissingBlock(cid)));
501501+ }
502502+ Step::Finish => break,
503503+ Step::Found { rkey, data } => out.push((rkey, data)),
504504+ };
505505+ }
506506+507507+ // `state` *has to* outlive the reader
508508+ drop(reader_res);
509509+510510+ (state, Ok::<_, DriveError>(out))
511511+ },
512512+ )
513513+ .await?; // on tokio JoinError, we'll be left with invalid state :(
514514+515515+ // *must* restore state before dealing with the actual result
516516+ self.state = Some(state);
517517+518518+ let out = res?;
519519+520520+ if out.is_empty() {
521521+ Ok(None)
522522+ } else {
523523+ Ok(Some(out))
524524+ }
160525 }
161526162162- /// Manually step through the record outputs
163163- pub async fn next_record(&mut self) -> Result<Option<(String, T)>, DriveError<PE>> {
527527+ fn read_tx_blocking(
528528+ &mut self,
529529+ n: usize,
530530+ tx: mpsc::Sender<Result<BlockChunk<T>, DriveError>>,
531531+ ) -> Result<(), mpsc::error::SendError<Result<BlockChunk<T>, DriveError>>> {
532532+ let BigState { store, walker } = self.state.as_mut().expect("valid state");
533533+ let mut reader = match store.get_reader() {
534534+ Ok(r) => r,
535535+ Err(e) => return tx.blocking_send(Err(e.into())),
536536+ };
537537+164538 loop {
165165- // walk as far as we can until we run out of blocks or find a record
166166- let cid_needed = match self.walker.step(&mut self.blocks, &self.process)? {
167167- Step::Rest(cid) => cid,
168168- Step::Finish => return Ok(None),
169169- Step::Step { rkey, data } => return Ok(Some((rkey, data))),
170170- };
539539+ let mut out: BlockChunk<T> = Vec::with_capacity(n);
540540+541541+ for _ in 0..n {
542542+ // walk as far as we can until we run out of blocks or find a record
543543+544544+ let step = match walker.disk_step(&mut reader, self.process) {
545545+ Ok(s) => s,
546546+ Err(e) => return tx.blocking_send(Err(e.into())),
547547+ };
548548+549549+ match step {
550550+ Step::Missing(cid) => {
551551+ return tx.blocking_send(Err(DriveError::MissingBlock(cid)));
552552+ }
553553+ Step::Finish => return Ok(()),
554554+ Step::Found { rkey, data } => {
555555+ out.push((rkey, data));
556556+ continue;
557557+ }
558558+ };
559559+ }
171560172172- // load blocks until we reach that cid
173173- self.drive_until(cid_needed).await?;
561561+ if out.is_empty() {
562562+ break;
563563+ }
564564+ tx.blocking_send(Ok(out))?;
174565 }
566566+567567+ Ok(())
175568 }
176569177177- /// Convert to a futures::stream of record outputs
178178- pub fn stream(self) -> impl Stream<Item = Result<(String, T), DriveError<PE>>> {
179179- futures::stream::try_unfold(self, |mut this| async move {
180180- let maybe_record = this.next_record().await?;
181181- Ok(maybe_record.map(|b| (b, this)))
182182- })
570570+ /// Spawn the disk reading task into a tokio blocking thread
571571+ ///
572572+ /// The idea is to avoid so much sending back and forth to the blocking
573573+ /// thread, letting a blocking task do all the disk reading work and sending
574574+ /// records and rkeys back through an `mpsc` channel instead.
575575+ ///
576576+ /// This might also allow the disk work to continue while processing the
577577+ /// records. It's still not yet clear if this method actually has much
578578+ /// benefit over just using `.next_chunk(n)`.
579579+ ///
580580+ /// ```no_run
581581+ /// # use repo_stream::{drive::{DiskDriver, DriveError, _get_fake_disk_driver}, process::noop};
582582+ /// # #[tokio::main]
583583+ /// # async fn main() -> Result<(), DriveError> {
584584+ /// # let mut disk_driver = _get_fake_disk_driver();
585585+ /// let (mut rx, join) = disk_driver.to_channel(512);
586586+ /// while let Some(recvd) = rx.recv().await {
587587+ /// let pairs = recvd?;
588588+ /// for (rkey, record) in pairs {
589589+ /// println!("{rkey}: size={}", record.len());
590590+ /// }
591591+ ///
592592+ /// }
593593+ /// let store = join.await?.reset_store().await?;
594594+ /// # Ok(())
595595+ /// # }
596596+ /// ```
597597+ pub fn to_channel(
598598+ mut self,
599599+ n: usize,
600600+ ) -> (
601601+ mpsc::Receiver<Result<BlockChunk<T>, DriveError>>,
602602+ tokio::task::JoinHandle<Self>,
603603+ ) {
604604+ let (tx, rx) = mpsc::channel::<Result<BlockChunk<T>, DriveError>>(1);
605605+606606+ // sketch: this worker is going to be allowed to execute without a join handle
607607+ let chan_task = tokio::task::spawn_blocking(move || {
608608+ if let Err(mpsc::error::SendError(_)) = self.read_tx_blocking(n, tx) {
609609+ log::debug!("big car reader exited early due to dropped receiver channel");
610610+ }
611611+ self
612612+ });
613613+614614+ (rx, chan_task)
615615+ }
616616+617617+ /// Reset the disk storage so it can be reused. You must call this.
618618+ ///
619619+ /// Ideally we'd put this in an `impl Drop`, but since it makes blocking
620620+ /// calls, that would be risky in an async context. For now you just have to
621621+ /// carefully make sure you call it.
622622+ ///
623623+ /// The sqlite store is returned, so it can be reused for another
624624+ /// `DiskDriver`.
625625+ pub async fn reset_store(mut self) -> Result<DiskStore, DriveError> {
626626+ let BigState { store, .. } = self.state.take().expect("valid state");
627627+ Ok(store.reset().await?)
183628 }
184629}
+85-5
src/lib.rs
···11-//! Fast and robust atproto CAR file processing in rust
22-//!
33-//! For now see the [examples](https://tangled.org/@microcosm.blue/repo-stream/tree/main/examples)
11+/*!
22+A robust CAR file -> MST walker for atproto
33+44+Small CARs have their blocks buffered in memory. If a configurable memory limit
55+is reached while reading blocks, CAR reading is suspended, and can be continued
66+by providing disk storage to buffer the CAR blocks instead.
77+88+A `process` function can be provided for tasks where records are transformed
99+into a smaller representation, to save memory (and disk) during block reading.
1010+1111+Once blocks are loaded, the MST is walked and emitted as chunks of pairs of
1212+`(rkey, processed_block)` pairs, in order (depth first, left-to-right).
1313+1414+Some MST validations are applied
1515+- Keys must appear in order
1616+- Keys must be at the correct MST tree depth
1717+1818+`iroh_car` additionally applies a block size limit of `2MiB`.
1919+2020+```
2121+use repo_stream::{Driver, DriverBuilder, DiskBuilder};
2222+2323+# #[tokio::main]
2424+# async fn main() -> Result<(), Box<dyn std::error::Error>> {
2525+# let reader = include_bytes!("../car-samples/tiny.car").as_slice();
2626+let mut total_size = 0;
42755-pub mod drive;
2828+match DriverBuilder::new()
2929+ .with_mem_limit_mb(10)
3030+ .with_block_processor(|rec| rec.len()) // block processing: just extract the raw record size
3131+ .load_car(reader)
3232+ .await?
3333+{
3434+3535+ // if all blocks fit within memory
3636+ Driver::Memory(_commit, mut driver) => {
3737+ while let Some(chunk) = driver.next_chunk(256).await? {
3838+ for (_rkey, size) in chunk {
3939+ total_size += size;
4040+ }
4141+ }
4242+ },
4343+4444+ // if the CAR was too big for in-memory processing
4545+ Driver::Disk(paused) => {
4646+ // set up a disk store we can spill to
4747+ let store = DiskBuilder::new().open("some/path.db".into()).await?;
4848+ // do the spilling, get back a (similar) driver
4949+ let (_commit, mut driver) = paused.finish_loading(store).await?;
5050+5151+ while let Some(chunk) = driver.next_chunk(256).await? {
5252+ for (_rkey, size) in chunk {
5353+ total_size += size;
5454+ }
5555+ }
5656+5757+ // clean up the disk store (drop tables etc)
5858+ driver.reset_store().await?;
5959+ }
6060+};
6161+println!("sum of size of all records: {total_size}");
6262+# Ok(())
6363+# }
6464+```
6565+6666+Disk spilling suspends and returns a `Driver::Disk(paused)` instead of going
6767+ahead and eagerly using disk I/O. This means you have to write a bit more code
6868+to handle both cases, but it allows you to have finer control over resource
6969+usage. For example, you can drive a number of parallel memory CAR workers, and
7070+separately have a different number of disk workers picking up suspended disk
7171+tasks from a queue.
7272+7373+Find more [examples in the repo](https://tangled.org/@microcosm.blue/repo-stream/tree/main/examples).
7474+7575+*/
7676+677pub mod mst;
77-pub mod walk;
7878+mod walk;
7979+8080+pub mod disk;
8181+pub mod drive;
8282+pub mod process;
8383+8484+pub use disk::{DiskBuilder, DiskError, DiskStore};
8585+pub use drive::{DriveError, Driver, DriverBuilder};
8686+pub use mst::Commit;
8787+pub use process::Processable;
+4-8
src/mst.rs
···3939/// MST node data schema
4040#[derive(Debug, Deserialize, PartialEq)]
4141#[serde(deny_unknown_fields)]
4242-pub struct Node {
4242+pub(crate) struct Node {
4343 /// link to sub-tree Node on a lower level and with all keys sorting before
4444 /// keys at this node
4545 #[serde(rename = "l")]
···6262 /// so if a block *could be* a node, any record converter must postpone
6363 /// processing. if it turns out it happens to be a very node-looking record,
6464 /// well, sorry, it just has to only be processed later when that's known.
6565- pub fn could_be(bytes: impl AsRef<[u8]>) -> bool {
6565+ pub(crate) fn could_be(bytes: impl AsRef<[u8]>) -> bool {
6666 const NODE_FINGERPRINT: [u8; 3] = [
6767 0xA2, // map length 2 (for "l" and "e" keys)
6868 0x61, // text length 1
···8383 /// with an empty array of entries. This is the only situation in which a
8484 /// tree may contain an empty leaf node which does not either contain keys
8585 /// ("entries") or point to a sub-tree containing entries.
8686- ///
8787- /// TODO: to me this is slightly unclear with respect to `l` (ask someone).
8888- /// ...is that what "The top of the tree must not be a an empty node which
8989- /// only points to a sub-tree." is referring to?
9090- pub fn is_empty(&self) -> bool {
8686+ pub(crate) fn is_empty(&self) -> bool {
9187 self.left.is_none() && self.entries.is_empty()
9288 }
9389}
···9591/// TreeEntry object
9692#[derive(Debug, Deserialize, PartialEq)]
9793#[serde(deny_unknown_fields)]
9898-pub struct Entry {
9494+pub(crate) struct Entry {
9995 /// count of bytes shared with previous TreeEntry in this Node (if any)
10096 #[serde(rename = "p")]
10197 pub prefix_len: usize,
+87
src/process.rs
···11+/*!
22+Record processor function output trait
33+44+The return type must satisfy the `Processable` trait, which requires:
55+66+- `Clone` because two rkeys can refer to the same record by CID, which may
77+ only appear once in the CAR file.
88+- `Serialize + DeserializeOwned` so it can be spilled to disk.
99+1010+One required function must be implemented, `get_size()`: this should return the
1111+approximate total off-stack size of the type. (the on-stack size will be added
1212+automatically via `std::mem::get_size`).
1313+1414+Note that it is **not guaranteed** that the `process` function will run on a
1515+block before storing it in memory or on disk: it's not possible to know if a
1616+block is a record without actually walking the MST, so the best we can do is
1717+apply `process` to any block that we know *cannot* be an MST node, and otherwise
1818+store the raw block bytes.
1919+2020+Here's a silly processing function that just collects 'eyy's found in the raw
2121+record bytes
2222+2323+```
2424+# use repo_stream::Processable;
2525+# use serde::{Serialize, Deserialize};
2626+#[derive(Debug, Clone, Serialize, Deserialize)]
2727+struct Eyy(usize, String);
2828+2929+impl Processable for Eyy {
3030+ fn get_size(&self) -> usize {
3131+ // don't need to compute the usize, it's on the stack
3232+ self.1.capacity() // in-mem size from the string's capacity, in bytes
3333+ }
3434+}
3535+3636+fn process(raw: Vec<u8>) -> Vec<Eyy> {
3737+ let mut out = Vec::new();
3838+ let to_find = "eyy".as_bytes();
3939+ for i in 0..(raw.len() - 3) {
4040+ if &raw[i..(i+3)] == to_find {
4141+ out.push(Eyy(i, "eyy".to_string()));
4242+ }
4343+ }
4444+ out
4545+}
4646+```
4747+4848+The memory sizing stuff is a little sketch but probably at least approximately
4949+works.
5050+*/
5151+5252+use serde::{Serialize, de::DeserializeOwned};
5353+5454+/// Output trait for record processing
5555+pub trait Processable: Clone + Serialize + DeserializeOwned {
5656+ /// Any additional in-memory size taken by the processed type
5757+ ///
5858+ /// Do not include stack size (`std::mem::size_of`)
5959+ fn get_size(&self) -> usize;
6060+}
6161+6262+/// Processor that just returns the raw blocks
6363+#[inline]
6464+pub fn noop(block: Vec<u8>) -> Vec<u8> {
6565+ block
6666+}
6767+6868+impl Processable for u8 {
6969+ fn get_size(&self) -> usize {
7070+ 0
7171+ }
7272+}
7373+7474+impl Processable for usize {
7575+ fn get_size(&self) -> usize {
7676+ 0 // no additional space taken, just its stack size (newtype is free)
7777+ }
7878+}
7979+8080+impl<Item: Sized + Processable> Processable for Vec<Item> {
8181+ fn get_size(&self) -> usize {
8282+ let slot_size = std::mem::size_of::<Item>();
8383+ let direct_size = slot_size * self.capacity();
8484+ let items_referenced_size: usize = self.iter().map(|item| item.get_size()).sum();
8585+ direct_size + items_referenced_size
8686+ }
8787+}
+257-259
src/walk.rs
···11//! Depth-first MST traversal
2233-use crate::drive::MaybeProcessedBlock;
33+use crate::disk::SqliteReader;
44+use crate::drive::{DecodeError, MaybeProcessedBlock};
45use crate::mst::Node;
66+use crate::process::Processable;
57use ipld_core::cid::Cid;
88+use sha2::{Digest, Sha256};
69use std::collections::HashMap;
77-use std::error::Error;
1010+use std::convert::Infallible;
811912/// Errors that can happen while walking
1013#[derive(Debug, thiserror::Error)]
1111-pub enum Trip<E: Error> {
1212- #[error("empty mst nodes are not allowed")]
1313- NodeEmpty,
1414+pub enum WalkError {
1515+ #[error("Failed to fingerprint commit block")]
1616+ BadCommitFingerprint,
1417 #[error("Failed to decode commit block: {0}")]
1515- BadCommit(Box<dyn std::error::Error>),
1818+ BadCommit(#[from] serde_ipld_dagcbor::DecodeError<Infallible>),
1619 #[error("Action node error: {0}")]
1717- RkeyError(#[from] RkeyError),
1818- #[error("Process failed: {0}")]
1919- ProcessFailed(E),
2020- #[error("Encountered an rkey out of order while walking the MST")]
2121- RkeyOutOfOrder,
2020+ MstError(#[from] MstError),
2121+ #[error("storage error: {0}")]
2222+ StorageError(#[from] rusqlite::Error),
2323+ #[error("Decode error: {0}")]
2424+ DecodeError(#[from] DecodeError),
2225}
23262427/// Errors from invalid Rkeys
2525-#[derive(Debug, thiserror::Error)]
2626-pub enum RkeyError {
2828+#[derive(Debug, PartialEq, thiserror::Error)]
2929+pub enum MstError {
2730 #[error("Failed to compute an rkey due to invalid prefix_len")]
2831 EntryPrefixOutOfbounds,
2932 #[error("RKey was not utf-8")]
3033 EntryRkeyNotUtf8(#[from] std::string::FromUtf8Error),
3434+ #[error("Nodes cannot be empty (except for an entirely empty MST)")]
3535+ EmptyNode,
3636+ #[error("Found an entry with rkey at the wrong depth")]
3737+ WrongDepth,
3838+ #[error("Lost track of our depth (possible bug?)")]
3939+ LostDepth,
4040+ #[error("MST depth underflow: depth-0 node with child trees")]
4141+ DepthUnderflow,
4242+ #[error("Encountered an rkey out of order while walking the MST")]
4343+ RkeyOutOfOrder,
3144}
32453346/// Walker outputs
3447#[derive(Debug)]
3548pub enum Step<T> {
3636- /// We need a CID but it's not in the block store
3737- ///
3838- /// Give the needed CID to the driver so it can load blocks until it's found
3939- Rest(Cid),
4949+ /// We needed this CID but it's not in the block store
5050+ Missing(Cid),
4051 /// Reached the end of the MST! yay!
4152 Finish,
4253 /// A record was found!
4343- Step { rkey: String, data: T },
5454+ Found { rkey: String, data: T },
4455}
45564657#[derive(Debug, Clone, PartialEq)]
4758enum Need {
4848- Node(Cid),
5959+ Node { depth: Depth, cid: Cid },
4960 Record { rkey: String, cid: Cid },
5061}
51625252-fn push_from_node(stack: &mut Vec<Need>, node: &Node) -> Result<(), RkeyError> {
5353- let mut entries = Vec::with_capacity(node.entries.len());
6363+#[derive(Debug, Clone, Copy, PartialEq)]
6464+enum Depth {
6565+ Root,
6666+ Depth(u32),
6767+}
6868+6969+impl Depth {
7070+ fn from_key(key: &[u8]) -> Self {
7171+ let mut zeros = 0;
7272+ for byte in Sha256::digest(key) {
7373+ let leading = byte.leading_zeros();
7474+ zeros += leading;
7575+ if leading < 8 {
7676+ break;
7777+ }
7878+ }
7979+ Self::Depth(zeros / 2) // truncating divide (rounds down)
8080+ }
8181+ fn next_expected(&self) -> Result<Option<u32>, MstError> {
8282+ match self {
8383+ Self::Root => Ok(None),
8484+ Self::Depth(d) => d.checked_sub(1).ok_or(MstError::DepthUnderflow).map(Some),
8585+ }
8686+ }
8787+}
54888989+fn push_from_node(stack: &mut Vec<Need>, node: &Node, parent_depth: Depth) -> Result<(), MstError> {
9090+ // empty nodes are not allowed in the MST
9191+ // ...except for a single one for empty MST, but we wouldn't be pushing that
9292+ if node.is_empty() {
9393+ return Err(MstError::EmptyNode);
9494+ }
9595+9696+ let mut entries = Vec::with_capacity(node.entries.len());
5597 let mut prefix = vec![];
9898+ let mut this_depth = parent_depth.next_expected()?;
9999+56100 for entry in &node.entries {
57101 let mut rkey = vec![];
58102 let pre_checked = prefix
59103 .get(..entry.prefix_len)
6060- .ok_or(RkeyError::EntryPrefixOutOfbounds)?;
104104+ .ok_or(MstError::EntryPrefixOutOfbounds)?;
61105 rkey.extend_from_slice(pre_checked);
62106 rkey.extend_from_slice(&entry.keysuffix);
107107+108108+ let Depth::Depth(key_depth) = Depth::from_key(&rkey) else {
109109+ return Err(MstError::WrongDepth);
110110+ };
111111+112112+ // this_depth is `none` if we are the deepest child (directly below root)
113113+ // in that case we accept whatever highest depth is claimed
114114+ let expected_depth = match this_depth {
115115+ Some(d) => d,
116116+ None => {
117117+ this_depth = Some(key_depth);
118118+ key_depth
119119+ }
120120+ };
121121+122122+ // all keys we find should be this depth
123123+ if key_depth != expected_depth {
124124+ return Err(MstError::DepthUnderflow);
125125+ }
126126+63127 prefix = rkey.clone();
6412865129 entries.push(Need::Record {
···67131 cid: entry.value,
68132 });
69133 if let Some(ref tree) = entry.tree {
7070- entries.push(Need::Node(*tree));
134134+ entries.push(Need::Node {
135135+ depth: Depth::Depth(key_depth),
136136+ cid: *tree,
137137+ });
71138 }
72139 }
7314074141 entries.reverse();
75142 stack.append(&mut entries);
76143144144+ let d = this_depth.ok_or(MstError::LostDepth)?;
145145+77146 if let Some(tree) = node.left {
7878- stack.push(Need::Node(tree));
147147+ stack.push(Need::Node {
148148+ depth: Depth::Depth(d),
149149+ cid: tree,
150150+ });
79151 }
80152 Ok(())
81153}
···92164impl Walker {
93165 pub fn new(tree_root_cid: Cid) -> Self {
94166 Self {
9595- stack: vec![Need::Node(tree_root_cid)],
167167+ stack: vec![Need::Node {
168168+ depth: Depth::Root,
169169+ cid: tree_root_cid,
170170+ }],
96171 prev: "".to_string(),
97172 }
98173 }
99174100175 /// Advance through nodes until we find a record or can't go further
101101- pub fn step<T: Clone, E: Error>(
176176+ pub fn step<T: Processable>(
102177 &mut self,
103103- blocks: &mut HashMap<Cid, MaybeProcessedBlock<T, E>>,
104104- process: impl Fn(&[u8]) -> Result<T, E>,
105105- ) -> Result<Step<T>, Trip<E>> {
178178+ blocks: &mut HashMap<Cid, MaybeProcessedBlock<T>>,
179179+ process: impl Fn(Vec<u8>) -> T,
180180+ ) -> Result<Step<T>, WalkError> {
106181 loop {
107107- let Some(mut need) = self.stack.last() else {
182182+ let Some(need) = self.stack.last_mut() else {
108183 log::trace!("tried to walk but we're actually done.");
109184 return Ok(Step::Finish);
110185 };
111186112112- match &mut need {
113113- Need::Node(cid) => {
187187+ match need {
188188+ &mut Need::Node { depth, cid } => {
114189 log::trace!("need node {cid:?}");
115115- let Some(block) = blocks.remove(cid) else {
190190+ let Some(block) = blocks.remove(&cid) else {
116191 log::trace!("node not found, resting");
117117- return Ok(Step::Rest(*cid));
192192+ return Ok(Step::Missing(cid));
118193 };
119194120195 let MaybeProcessedBlock::Raw(data) = block else {
121121- return Err(Trip::BadCommit("failed commit fingerprint".into()));
196196+ return Err(WalkError::BadCommitFingerprint);
122197 };
123198 let node = serde_ipld_dagcbor::from_slice::<Node>(&data)
124124- .map_err(|e| Trip::BadCommit(e.into()))?;
199199+ .map_err(WalkError::BadCommit)?;
125200126201 // found node, make sure we remember
127202 self.stack.pop();
128203129204 // queue up work on the found node next
130130- push_from_node(&mut self.stack, &node)?;
205205+ push_from_node(&mut self.stack, &node, depth)?;
131206 }
132207 Need::Record { rkey, cid } => {
133208 log::trace!("need record {cid:?}");
209209+ // note that we cannot *remove* a record block, sadly, since
210210+ // there can be multiple rkeys pointing to the same cid.
134211 let Some(data) = blocks.get_mut(cid) else {
212212+ return Ok(Step::Missing(*cid));
213213+ };
214214+ let rkey = rkey.clone();
215215+ let data = match data {
216216+ MaybeProcessedBlock::Raw(data) => process(data.to_vec()),
217217+ MaybeProcessedBlock::Processed(t) => t.clone(),
218218+ };
219219+220220+ // found node, make sure we remember
221221+ self.stack.pop();
222222+223223+ // rkeys *must* be in order or else the tree is invalid (or
224224+ // we have a bug)
225225+ if rkey <= self.prev {
226226+ return Err(MstError::RkeyOutOfOrder)?;
227227+ }
228228+ self.prev = rkey.clone();
229229+230230+ return Ok(Step::Found { rkey, data });
231231+ }
232232+ }
233233+ }
234234+ }
235235+236236+ /// blocking!!!!!!
237237+ pub fn disk_step<T: Processable>(
238238+ &mut self,
239239+ reader: &mut SqliteReader,
240240+ process: impl Fn(Vec<u8>) -> T,
241241+ ) -> Result<Step<T>, WalkError> {
242242+ loop {
243243+ let Some(need) = self.stack.last_mut() else {
244244+ log::trace!("tried to walk but we're actually done.");
245245+ return Ok(Step::Finish);
246246+ };
247247+248248+ match need {
249249+ &mut Need::Node { depth, cid } => {
250250+ let cid_bytes = cid.to_bytes();
251251+ log::trace!("need node {cid:?}");
252252+ let Some(block_bytes) = reader.get(cid_bytes)? else {
253253+ log::trace!("node not found, resting");
254254+ return Ok(Step::Missing(cid));
255255+ };
256256+257257+ let block: MaybeProcessedBlock<T> = crate::drive::decode(&block_bytes)?;
258258+259259+ let MaybeProcessedBlock::Raw(data) = block else {
260260+ return Err(WalkError::BadCommitFingerprint);
261261+ };
262262+ let node = serde_ipld_dagcbor::from_slice::<Node>(&data)
263263+ .map_err(WalkError::BadCommit)?;
264264+265265+ // found node, make sure we remember
266266+ self.stack.pop();
267267+268268+ // queue up work on the found node next
269269+ push_from_node(&mut self.stack, &node, depth).map_err(WalkError::MstError)?;
270270+ }
271271+ Need::Record { rkey, cid } => {
272272+ log::trace!("need record {cid:?}");
273273+ let cid_bytes = cid.to_bytes();
274274+ let Some(data_bytes) = reader.get(cid_bytes)? else {
135275 log::trace!("record block not found, resting");
136136- return Ok(Step::Rest(*cid));
276276+ return Ok(Step::Missing(*cid));
137277 };
278278+ let data: MaybeProcessedBlock<T> = crate::drive::decode(&data_bytes)?;
138279 let rkey = rkey.clone();
139280 let data = match data {
140281 MaybeProcessedBlock::Raw(data) => process(data),
141141- MaybeProcessedBlock::Processed(Ok(t)) => Ok(t.clone()),
142142- bad => {
143143- // big hack to pull the error out -- this corrupts
144144- // a block, so we should not continue trying to work
145145- let mut steal = MaybeProcessedBlock::Raw(vec![]);
146146- std::mem::swap(&mut steal, bad);
147147- let MaybeProcessedBlock::Processed(Err(e)) = steal else {
148148- unreachable!();
149149- };
150150- return Err(Trip::ProcessFailed(e));
151151- }
282282+ MaybeProcessedBlock::Processed(t) => t.clone(),
152283 };
153284154285 // found node, make sure we remember
155286 self.stack.pop();
156287157288 log::trace!("emitting a block as a step. depth={}", self.stack.len());
158158- let data = data.map_err(Trip::ProcessFailed)?;
159289160290 // rkeys *must* be in order or else the tree is invalid (or
161291 // we have a bug)
162292 if rkey <= self.prev {
163163- return Err(Trip::RkeyOutOfOrder);
293293+ return Err(MstError::RkeyOutOfOrder)?;
164294 }
165295 self.prev = rkey.clone();
166296167167- return Ok(Step::Step { rkey, data });
297297+ return Ok(Step::Found { rkey, data });
168298 }
169299 }
170300 }
···174304#[cfg(test)]
175305mod test {
176306 use super::*;
177177- // use crate::mst::Entry;
178307179308 fn cid1() -> Cid {
180309 "bafyreihixenvk3ahqbytas4hk4a26w43bh6eo3w6usjqtxkpzsvi655a3m"
181310 .parse()
182311 .unwrap()
183312 }
184184- // fn cid2() -> Cid {
185185- // "QmY7Yh4UquoXHLPFo2XbhXkhBvFoPwmQUSa92pxnxjQuPU"
186186- // .parse()
187187- // .unwrap()
188188- // }
189189- // fn cid3() -> Cid {
190190- // "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
191191- // .parse()
192192- // .unwrap()
193193- // }
194194- // fn cid4() -> Cid {
195195- // "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
196196- // .parse()
197197- // .unwrap()
198198- // }
199199- // fn cid5() -> Cid {
200200- // "QmSnuWmxptJZdLJpKRarxBMS2Ju2oANVrgbr2xWbie9b2D"
201201- // .parse()
202202- // .unwrap()
203203- // }
204204- // fn cid6() -> Cid {
205205- // "QmdmQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7RgQm"
206206- // .parse()
207207- // .unwrap()
208208- // }
209209- // fn cid7() -> Cid {
210210- // "bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze"
211211- // .parse()
212212- // .unwrap()
213213- // }
214214- // fn cid8() -> Cid {
215215- // "bafyreif3tfdpr5n4jdrbielmcapwvbpcthepfkwq2vwonmlhirbjmotedi"
216216- // .parse()
217217- // .unwrap()
218218- // }
219219- // fn cid9() -> Cid {
220220- // "bafyreicnokmhmrnlp2wjhyk2haep4tqxiptwfrp2rrs7rzq7uk766chqvq"
221221- // .parse()
222222- // .unwrap()
223223- // }
313313+314314+ #[test]
315315+ fn test_depth_spec_0() {
316316+ let d = Depth::from_key(b"2653ae71");
317317+ assert_eq!(d, Depth::Depth(0))
318318+ }
319319+320320+ #[test]
321321+ fn test_depth_spec_1() {
322322+ let d = Depth::from_key(b"blue");
323323+ assert_eq!(d, Depth::Depth(1))
324324+ }
325325+326326+ #[test]
327327+ fn test_depth_spec_4() {
328328+ let d = Depth::from_key(b"app.bsky.feed.post/454397e440ec");
329329+ assert_eq!(d, Depth::Depth(4))
330330+ }
331331+332332+ #[test]
333333+ fn test_depth_spec_8() {
334334+ let d = Depth::from_key(b"app.bsky.feed.post/9adeb165882c");
335335+ assert_eq!(d, Depth::Depth(8))
336336+ }
337337+338338+ #[test]
339339+ fn test_depth_ietf_draft_0() {
340340+ let d = Depth::from_key(b"key1");
341341+ assert_eq!(d, Depth::Depth(0))
342342+ }
343343+344344+ #[test]
345345+ fn test_depth_ietf_draft_1() {
346346+ let d = Depth::from_key(b"key7");
347347+ assert_eq!(d, Depth::Depth(1))
348348+ }
349349+350350+ #[test]
351351+ fn test_depth_ietf_draft_4() {
352352+ let d = Depth::from_key(b"key515");
353353+ assert_eq!(d, Depth::Depth(4))
354354+ }
355355+356356+ #[test]
357357+ fn test_depth_interop() {
358358+ // examples from https://github.com/bluesky-social/atproto-interop-tests/blob/main/mst/key_heights.json
359359+ for (k, expected) in [
360360+ ("", 0),
361361+ ("asdf", 0),
362362+ ("blue", 1),
363363+ ("2653ae71", 0),
364364+ ("88bfafc7", 2),
365365+ ("2a92d355", 4),
366366+ ("884976f5", 6),
367367+ ("app.bsky.feed.post/454397e440ec", 4),
368368+ ("app.bsky.feed.post/9adeb165882c", 8),
369369+ ] {
370370+ let d = Depth::from_key(k.as_bytes());
371371+ assert_eq!(d, Depth::Depth(expected), "key: {}", k);
372372+ }
373373+ }
224374225375 #[test]
226226- fn test_next_from_node_empty() {
227227- let node = Node {
376376+ fn test_push_empty_fails() {
377377+ let empty_node = Node {
228378 left: None,
229379 entries: vec![],
230380 };
231381 let mut stack = vec![];
232232- push_from_node(&mut stack, &node).unwrap();
233233- assert_eq!(stack.last(), None);
382382+ let err = push_from_node(&mut stack, &empty_node, Depth::Depth(4));
383383+ assert_eq!(err, Err(MstError::EmptyNode));
234384 }
235385236386 #[test]
237237- fn test_needs_from_node_just_left() {
387387+ fn test_push_one_node() {
238388 let node = Node {
239389 left: Some(cid1()),
240390 entries: vec![],
241391 };
242392 let mut stack = vec![];
243243- push_from_node(&mut stack, &node).unwrap();
244244- assert_eq!(stack.last(), Some(Need::Node(cid1())).as_ref());
393393+ push_from_node(&mut stack, &node, Depth::Depth(4)).unwrap();
394394+ assert_eq!(
395395+ stack.last(),
396396+ Some(Need::Node {
397397+ depth: Depth::Depth(3),
398398+ cid: cid1()
399399+ })
400400+ .as_ref()
401401+ );
245402 }
246246-247247- // #[test]
248248- // fn test_needs_from_node_just_one_record() {
249249- // let node = Node {
250250- // left: None,
251251- // entries: vec![Entry {
252252- // keysuffix: "asdf".into(),
253253- // prefix_len: 0,
254254- // value: cid1(),
255255- // tree: None,
256256- // }],
257257- // };
258258- // assert_eq!(
259259- // needs_from_node(node).unwrap(),
260260- // vec![Need::Record {
261261- // rkey: "asdf".into(),
262262- // cid: cid1(),
263263- // },]
264264- // );
265265- // }
266266-267267- // #[test]
268268- // fn test_needs_from_node_two_records() {
269269- // let node = Node {
270270- // left: None,
271271- // entries: vec![
272272- // Entry {
273273- // keysuffix: "asdf".into(),
274274- // prefix_len: 0,
275275- // value: cid1(),
276276- // tree: None,
277277- // },
278278- // Entry {
279279- // keysuffix: "gh".into(),
280280- // prefix_len: 2,
281281- // value: cid2(),
282282- // tree: None,
283283- // },
284284- // ],
285285- // };
286286- // assert_eq!(
287287- // needs_from_node(node).unwrap(),
288288- // vec![
289289- // Need::Record {
290290- // rkey: "asdf".into(),
291291- // cid: cid1(),
292292- // },
293293- // Need::Record {
294294- // rkey: "asgh".into(),
295295- // cid: cid2(),
296296- // },
297297- // ]
298298- // );
299299- // }
300300-301301- // #[test]
302302- // fn test_needs_from_node_with_both() {
303303- // let node = Node {
304304- // left: None,
305305- // entries: vec![Entry {
306306- // keysuffix: "asdf".into(),
307307- // prefix_len: 0,
308308- // value: cid1(),
309309- // tree: Some(cid2()),
310310- // }],
311311- // };
312312- // assert_eq!(
313313- // needs_from_node(node).unwrap(),
314314- // vec![
315315- // Need::Record {
316316- // rkey: "asdf".into(),
317317- // cid: cid1(),
318318- // },
319319- // Need::Node(cid2()),
320320- // ]
321321- // );
322322- // }
323323-324324- // #[test]
325325- // fn test_needs_from_node_left_and_record() {
326326- // let node = Node {
327327- // left: Some(cid1()),
328328- // entries: vec![Entry {
329329- // keysuffix: "asdf".into(),
330330- // prefix_len: 0,
331331- // value: cid2(),
332332- // tree: None,
333333- // }],
334334- // };
335335- // assert_eq!(
336336- // needs_from_node(node).unwrap(),
337337- // vec![
338338- // Need::Node(cid1()),
339339- // Need::Record {
340340- // rkey: "asdf".into(),
341341- // cid: cid2(),
342342- // },
343343- // ]
344344- // );
345345- // }
346346-347347- // #[test]
348348- // fn test_needs_from_full_node() {
349349- // let node = Node {
350350- // left: Some(cid1()),
351351- // entries: vec![
352352- // Entry {
353353- // keysuffix: "asdf".into(),
354354- // prefix_len: 0,
355355- // value: cid2(),
356356- // tree: Some(cid3()),
357357- // },
358358- // Entry {
359359- // keysuffix: "ghi".into(),
360360- // prefix_len: 1,
361361- // value: cid4(),
362362- // tree: Some(cid5()),
363363- // },
364364- // Entry {
365365- // keysuffix: "jkl".into(),
366366- // prefix_len: 2,
367367- // value: cid6(),
368368- // tree: Some(cid7()),
369369- // },
370370- // Entry {
371371- // keysuffix: "mno".into(),
372372- // prefix_len: 4,
373373- // value: cid8(),
374374- // tree: Some(cid9()),
375375- // },
376376- // ],
377377- // };
378378- // assert_eq!(
379379- // needs_from_node(node).unwrap(),
380380- // vec![
381381- // Need::Node(cid1()),
382382- // Need::Record {
383383- // rkey: "asdf".into(),
384384- // cid: cid2(),
385385- // },
386386- // Need::Node(cid3()),
387387- // Need::Record {
388388- // rkey: "aghi".into(),
389389- // cid: cid4(),
390390- // },
391391- // Need::Node(cid5()),
392392- // Need::Record {
393393- // rkey: "agjkl".into(),
394394- // cid: cid6(),
395395- // },
396396- // Need::Node(cid7()),
397397- // Need::Record {
398398- // rkey: "agjkmno".into(),
399399- // cid: cid8(),
400400- // },
401401- // Need::Node(cid9()),
402402- // ]
403403- // );
404404- // }
405403}
+18-26
tests/non-huge-cars.rs
···11extern crate repo_stream;
22-use futures::TryStreamExt;
33-use iroh_car::CarReader;
44-use std::convert::Infallible;
22+use repo_stream::Driver;
5364const TINY_CAR: &'static [u8] = include_bytes!("../car-samples/tiny.car");
75const LITTLE_CAR: &'static [u8] = include_bytes!("../car-samples/little.car");
86const MIDSIZE_CAR: &'static [u8] = include_bytes!("../car-samples/midsize.car");
97108async fn test_car(bytes: &[u8], expected_records: usize, expected_sum: usize) {
1111- let reader = CarReader::new(bytes).await.unwrap();
1212-1313- let root = reader
1414- .header()
1515- .roots()
1616- .first()
1717- .ok_or("missing root")
99+ let mut driver = match Driver::load_car(bytes, |block| block.len(), 10 /* MiB */)
1010+ .await
1811 .unwrap()
1919- .clone();
2020-2121- let stream = std::pin::pin!(reader.stream());
2222-2323- let (_commit, v) =
2424- repo_stream::drive::Vehicle::init(root, stream, |block| Ok::<_, Infallible>(block.len()))
2525- .await
2626- .unwrap();
2727- let mut record_stream = std::pin::pin!(v.stream());
1212+ {
1313+ Driver::Memory(_commit, mem_driver) => mem_driver,
1414+ Driver::Disk(_) => panic!("too big"),
1515+ };
28162917 let mut records = 0;
3018 let mut sum = 0;
3119 let mut found_bsky_profile = false;
3220 let mut prev_rkey = "".to_string();
3333- while let Some((rkey, size)) = record_stream.try_next().await.unwrap() {
3434- records += 1;
3535- sum += size;
3636- if rkey == "app.bsky.actor.profile/self" {
3737- found_bsky_profile = true;
2121+2222+ while let Some(pairs) = driver.next_chunk(256).await.unwrap() {
2323+ for (rkey, size) in pairs {
2424+ records += 1;
2525+ sum += size;
2626+ if rkey == "app.bsky.actor.profile/self" {
2727+ found_bsky_profile = true;
2828+ }
2929+ assert!(rkey > prev_rkey, "rkeys are streamed in order");
3030+ prev_rkey = rkey;
3831 }
3939- assert!(rkey > prev_rkey, "rkeys are streamed in order");
4040- prev_rkey = rkey;
4132 }
3333+4234 assert_eq!(records, expected_records);
4335 assert_eq!(sum, expected_sum);
4436 assert!(found_bsky_profile);