use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use futures::stream::{BoxStream, FuturesUnordered};
use futures::{Future, StreamExt};
use lance_core::Result;
use lance_table::format::{IndexMetadata, Manifest};
use lance_table::io::commit::ManifestLocation;
use lance_table::io::manifest::{read_manifest, read_manifest_indexes};
use object_store::path::Path;
use super::remove_prefix;
use crate::Dataset;
const MANIFEST_MEMORY_BUDGET: usize = 1024 * 1024 * 1024; const MANIFEST_DECOMPRESSION_RATIO: usize = 4;
const MAX_BUFFERED_LOCATIONS: usize = 50_000;
struct MemoryPermit {
bytes: usize,
inflight: Arc<AtomicUsize>,
notify: Arc<tokio::sync::Notify>,
}
impl Drop for MemoryPermit {
fn drop(&mut self) {
self.inflight.fetch_sub(self.bytes, Ordering::AcqRel);
self.notify.notify_one();
}
}
pub struct ScannedManifest {
pub manifest: Arc<Manifest>,
pub manifest_path: String,
pub indexes: Vec<IndexMetadata>,
_permit: MemoryPermit,
}
pub struct ManifestScan {
pub stream: BoxStream<'static, Result<ScannedManifest>>,
pub total: Arc<std::sync::OnceLock<usize>>,
#[cfg(test)]
inflight_bytes: Arc<AtomicUsize>,
}
#[cfg(test)]
impl ManifestScan {
fn inflight_bytes(&self) -> usize {
self.inflight_bytes.load(Ordering::Acquire)
}
fn inflight_handle(&self) -> Arc<AtomicUsize> {
self.inflight_bytes.clone()
}
}
pub fn scan_manifests(dataset: &Dataset, min_version: Option<u64>) -> ManifestScan {
let base = dataset.base.clone();
let object_store = dataset.object_store.clone();
let commit_handler = dataset.commit_handler.clone();
let (tx_manifest, rx_manifest) = tokio::sync::mpsc::channel::<Result<ScannedManifest>>(2);
let (tx_locations, rx_locations) =
tokio::sync::mpsc::channel::<ManifestLocation>(MAX_BUFFERED_LOCATIONS);
let inflight_mem = Arc::new(AtomicUsize::new(0));
let mem_notify = Arc::new(tokio::sync::Notify::new());
let total: Arc<std::sync::OnceLock<usize>> = Arc::new(std::sync::OnceLock::new());
spawn_lister(
commit_handler,
object_store.clone(),
base.clone(),
min_version,
tx_locations,
total.clone(),
tx_manifest.clone(),
);
spawn_reader(
object_store,
base,
rx_locations,
tx_manifest,
&inflight_mem,
mem_notify,
);
ManifestScan {
stream: tokio_stream::wrappers::ReceiverStream::new(rx_manifest).boxed(),
total,
#[cfg(test)]
inflight_bytes: inflight_mem,
}
}
fn spawn_lister(
commit_handler: Arc<dyn lance_table::io::commit::CommitHandler>,
object_store: Arc<lance_io::object_store::ObjectStore>,
base: Path,
min_version: Option<u64>,
tx_locations: tokio::sync::mpsc::Sender<ManifestLocation>,
total: Arc<std::sync::OnceLock<usize>>,
tx_err: tokio::sync::mpsc::Sender<Result<ScannedManifest>>,
) {
tokio::spawn(async move {
let result: Result<()> = async {
let mut locations = commit_handler.list_manifest_locations(&base, &object_store, false);
let mut count = 0usize;
while let Some(location) = locations.next().await {
let location = location?;
if let Some(min_version) = min_version
&& location.version < min_version
{
continue;
}
count += 1;
if tx_locations.send(location).await.is_err() {
return Ok(());
}
}
let _ = total.set(count);
Ok(())
}
.await;
if let Err(error) = result {
let _ = tx_err.send(Err(error)).await;
}
});
}
fn spawn_reader(
object_store: Arc<lance_io::object_store::ObjectStore>,
base: Path,
mut rx_locations: tokio::sync::mpsc::Receiver<ManifestLocation>,
tx_manifest: tokio::sync::mpsc::Sender<Result<ScannedManifest>>,
inflight_mem: &Arc<AtomicUsize>,
mem_notify: Arc<tokio::sync::Notify>,
) {
let inflight_mem = inflight_mem.clone();
tokio::spawn(async move {
let max_parallelism = object_store.io_parallelism();
type ScanResult = Result<ScannedManifest>;
let mut in_flight: FuturesUnordered<
std::pin::Pin<Box<dyn Future<Output = ScanResult> + Send>>,
> = FuturesUnordered::new();
let mut locations_exhausted = false;
loop {
let can_launch = !locations_exhausted
&& in_flight.len() < max_parallelism
&& (in_flight.is_empty()
|| inflight_mem.load(Ordering::Acquire) < MANIFEST_MEMORY_BUDGET);
if in_flight.is_empty() && !can_launch {
break;
}
tokio::select! {
biased;
Some(scanned) = in_flight.next(), if !in_flight.is_empty() => {
if tx_manifest.send(scanned).await.is_err() {
return;
}
}
location = rx_locations.recv(), if can_launch => {
match location {
Some(location) => {
let estimated = location.size.unwrap_or(0) as usize
* MANIFEST_DECOMPRESSION_RATIO;
inflight_mem.fetch_add(estimated, Ordering::AcqRel);
let permit = MemoryPermit {
bytes: estimated,
inflight: inflight_mem.clone(),
notify: mem_notify.clone(),
};
let object_store = object_store.clone();
let base = base.clone();
in_flight.push(Box::pin(async move {
let manifest = read_manifest(
&object_store,
&location.path,
location.size,
)
.await?;
let indexes = read_manifest_indexes(
&object_store,
&location,
&manifest,
)
.await?;
Ok(ScannedManifest {
manifest: Arc::new(manifest),
manifest_path: remove_prefix(&location.path, &base)
.to_string(),
indexes,
_permit: permit,
})
}));
}
None => locations_exhausted = true,
}
}
_ = mem_notify.notified(), if !can_launch && !in_flight.is_empty() => {}
}
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator};
use arrow_schema::{DataType, Field, Schema as ArrowSchema};
fn simple_batch() -> impl arrow_array::RecordBatchReader {
let schema = Arc::new(ArrowSchema::new(vec![Field::new(
"id",
DataType::Int32,
false,
)]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
)
.unwrap();
RecordBatchIterator::new(vec![Ok(batch)], schema)
}
async fn dataset_with_three_versions(uri: &str) -> Dataset {
let mut dataset = Dataset::write(simple_batch(), uri, None).await.unwrap();
dataset.append(simple_batch(), None).await.unwrap();
dataset.append(simple_batch(), None).await.unwrap();
dataset
}
#[tokio::test]
async fn budget_returns_to_zero_after_consuming() {
let dataset = dataset_with_three_versions("memory://scan_budget_zero").await;
let mut scan = scan_manifests(&dataset, None);
let mut seen = 0usize;
while let Some(scanned) = scan.stream.next().await {
scanned.unwrap();
seen += 1;
}
assert_eq!(seen, 3, "expected every present manifest");
assert_eq!(
scan.inflight_bytes(),
0,
"dropping every ScannedManifest must return the whole budget"
);
}
#[tokio::test]
async fn holding_manifests_keeps_budget_charged() {
let dataset = dataset_with_three_versions("memory://scan_budget_held").await;
let mut scan = scan_manifests(&dataset, None);
let mut held = Vec::new();
while let Some(scanned) = scan.stream.next().await {
held.push(scanned.unwrap());
}
assert!(
scan.inflight_bytes() > 0,
"held manifests must still be charged against the budget"
);
drop(held);
assert_eq!(
scan.inflight_bytes(),
0,
"the budget must come back when the consumer lets go"
);
}
#[tokio::test]
async fn min_version_skips_older_manifests() {
let dataset = dataset_with_three_versions("memory://scan_min_version").await;
let mut versions = Vec::new();
let ManifestScan { mut stream, .. } = scan_manifests(&dataset, Some(3));
while let Some(scanned) = stream.next().await {
versions.push(scanned.unwrap().manifest.version);
}
assert_eq!(versions, vec![3], "min_version must drop versions 1 and 2");
}
#[tokio::test]
async fn dropping_the_stream_releases_every_permit() {
let dataset = dataset_with_three_versions("memory://scan_drop_early").await;
let scan = scan_manifests(&dataset, None);
let inflight = scan.inflight_handle();
let mut stream = scan.stream;
let first = stream.next().await.expect("at least one manifest").unwrap();
assert!(
inflight.load(Ordering::Acquire) > 0,
"holding a manifest must charge the budget"
);
drop(stream);
drop(first);
let mut released = false;
for _ in 0..1000 {
if inflight.load(Ordering::Acquire) == 0 {
released = true;
break;
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
assert!(
released,
"dropping the stream must release every in-flight permit; \
timed out waiting for the reader to unwind"
);
}
#[tokio::test]
async fn read_failure_surfaces_as_an_error_item() {
use crate::dataset::builder::DatasetBuilder;
use crate::dataset::{ObjectStoreParams, ReadParams};
use crate::utils::test::FailingProxyStore;
let dir = tempfile::tempdir().unwrap();
let uri = dir.path().to_str().unwrap();
drop(dataset_with_three_versions(uri).await);
let failing = Arc::new(FailingProxyStore::new());
let dataset = DatasetBuilder::from_uri(uri)
.with_read_params(ReadParams {
store_options: Some(ObjectStoreParams {
object_store_wrapper: Some(failing.clone()),
..Default::default()
}),
..Default::default()
})
.load()
.await
.unwrap();
failing.fail_when("get_opts", "_versions", "injected manifest read failure");
let mut scan = scan_manifests(&dataset, None);
let mut errors = 0usize;
let mut successes = 0usize;
while let Some(scanned) = scan.stream.next().await {
match scanned {
Ok(_) => successes += 1,
Err(_) => errors += 1,
}
}
assert_eq!(
successes, 0,
"no manifest read can succeed while every `_versions` read fails"
);
assert_eq!(
errors, 3,
"each of the three manifests must surface its own read error"
);
assert_eq!(
scan.inflight_bytes(),
0,
"a failed read must return its share of the budget"
);
}
#[tokio::test]
async fn total_counts_every_yielded_manifest() {
let dataset = dataset_with_three_versions("memory://scan_total").await;
let ManifestScan {
mut stream, total, ..
} = scan_manifests(&dataset, None);
let mut seen = 0usize;
while let Some(scanned) = stream.next().await {
scanned.unwrap();
seen += 1;
}
assert_eq!(total.get().copied(), Some(seen));
}
}