use std::{
io::Write,
path::{Path, PathBuf},
};
use eyre::Result;
use opendal::{Operator, services::Memory};
use sqlx::PgPool;
use tantivy::directory::{Directory, RamDirectory, TerminatingWrite};
use tokio::task;
use uuid::Uuid;
use crate::{FullDirectory, LightDirectory, empty::Empty};
async fn pool() -> Result<PgPool> {
let pool = PgPool::connect("postgresql://postgres:postgres@localhost:15432/postgres").await?;
Ok(pool)
}
fn operator() -> Result<Operator> {
let operator = Operator::new(Memory::default())?.finish();
Ok(operator)
}
async fn file_is_empty(pool: &PgPool, index: Uuid, path: &str) -> Result<Option<bool>> {
let is_empty = sqlx::query_scalar(
r#"
SELECT is_empty
FROM tantivy.files
WHERE index = $1
AND path = $2
AND deleted_at IS NULL
"#,
)
.bind(index)
.bind(path)
.fetch_optional(pool)
.await?;
Ok(is_empty)
}
fn write_file(dir: &impl Directory, path: &Path, data: &[u8]) -> Result<()> {
let mut writer = dir.open_write(path)?;
writer.write_all(data)?;
writer.terminate()?;
dir.sync_directory()?;
Ok(())
}
fn read_file(dir: &impl Directory, path: &Path) -> Result<Vec<u8>> {
let handle = dir.get_file_handle(path)?;
let len = handle.len();
Ok(handle.read_bytes(0..len)?.to_vec())
}
#[tokio::test(flavor = "multi_thread")]
async fn full_skips_and_reconstructs_empty_file() -> Result<()> {
let index = Uuid::new_v4();
let pool = pool().await?;
let operator = operator()?;
let dir = FullDirectory::open(index, operator.clone(), pool.clone()).await?;
let path = PathBuf::from("seg.idx");
let empty = Empty::Composite.bytes().to_vec();
let dir_ = dir.clone();
let empty_ = empty.clone();
let path_ = path.clone();
task::spawn_blocking(move || write_file(&dir_, &path_, &empty_)).await??;
assert_eq!(file_is_empty(&pool, index, "seg.idx").await?, Some(true));
let remote = format!("idx-{index}/seg.idx");
assert!(
!operator.exists(&remote).await?,
"empty file must not be stored in the object store",
);
let dir_ = dir.clone();
let path_ = path.clone();
let read = task::spawn_blocking(move || read_file(&dir_, &path_)).await??;
assert_eq!(read, empty);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn full_stores_non_empty_file() -> Result<()> {
let index = Uuid::new_v4();
let pool = pool().await?;
let operator = operator()?;
let dir = FullDirectory::open(index, operator.clone(), pool.clone()).await?;
let path = PathBuf::from("seg.idx");
let mut data = Empty::Composite.bytes().to_vec();
data.push(0xff);
let dir_ = dir.clone();
let data_ = data.clone();
let path_ = path.clone();
task::spawn_blocking(move || write_file(&dir_, &path_, &data_)).await??;
assert_eq!(file_is_empty(&pool, index, "seg.idx").await?, Some(false));
let remote = format!("idx-{index}/seg.idx");
assert!(
operator.exists(&remote).await?,
"non-empty file must be stored in the object store",
);
let dir_ = dir.clone();
let read = task::spawn_blocking(move || read_file(&dir_, &path)).await??;
assert_eq!(read, data);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn light_skips_empty_and_forwards_non_empty() -> Result<()> {
let index = Uuid::new_v4();
let pool = pool().await?;
let operator = operator()?;
let inner = RamDirectory::create();
let dir = LightDirectory::open(inner.clone(), index, operator, pool.clone()).await?;
let empty_path = PathBuf::from("seg.fast");
let empty = Empty::Fast.bytes().to_vec();
let full_path = PathBuf::from("seg.store");
let data = b"this is a real doc store payload".to_vec();
let dir_ = dir.clone();
let empty_ = empty.clone();
let empty_path_ = empty_path.clone();
let data_ = data.clone();
let full_path_ = full_path.clone();
task::spawn_blocking(move || -> Result<()> {
write_file(&dir_, &empty_path_, &empty_)?;
write_file(&dir_, &full_path_, &data_)?;
Ok(())
})
.await??;
assert_eq!(file_is_empty(&pool, index, "seg.fast").await?, Some(true));
assert_eq!(file_is_empty(&pool, index, "seg.store").await?, None);
assert!(
!inner.exists(&empty_path)?,
"empty file must not be written to the inner directory",
);
assert!(
inner.exists(&full_path)?,
"non-empty file must be written to the inner directory",
);
let dir_ = dir.clone();
let (empty_read, full_read) = task::spawn_blocking(move || -> Result<(Vec<u8>, Vec<u8>)> {
Ok((
read_file(&dir_, &empty_path)?,
read_file(&dir_, &full_path)?,
))
})
.await??;
assert_eq!(empty_read, empty);
assert_eq!(full_read, data);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn light_exists_and_delete_empty_file() -> Result<()> {
let index = Uuid::new_v4();
let pool = pool().await?;
let operator = operator()?;
let inner = RamDirectory::create();
let dir = LightDirectory::open(inner, index, operator, pool.clone()).await?;
let path = PathBuf::from("seg.pos");
let empty = Empty::Composite.bytes().to_vec();
let dir_ = dir.clone();
let path_ = path.clone();
let exists_after_write = task::spawn_blocking(move || -> Result<(bool, bool)> {
write_file(&dir_, &path_, &empty)?;
let exists = dir_.exists(&path_)?;
dir_.delete(&path_)?;
Ok((exists, dir_.exists(&path_)?))
})
.await??;
assert_eq!(exists_after_write, (true, false));
assert_eq!(file_is_empty(&pool, index, "seg.pos").await?, None);
Ok(())
}