use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use hermes_core::directories::{
Directory, DirectoryWriter, FileHandle, OwnedBytes, RamDirectory, StreamingWriter,
};
use hermes_core::{Document, IndexConfig, IndexWriter, SchemaBuilder};
#[derive(Clone, Default)]
struct RecordingDirectory {
inner: RamDirectory,
plain_writes: Arc<Mutex<HashSet<PathBuf>>>,
streaming_writes: Arc<Mutex<HashSet<PathBuf>>>,
}
#[async_trait]
impl Directory for RecordingDirectory {
async fn exists(&self, path: &Path) -> std::io::Result<bool> {
self.inner.exists(path).await
}
async fn file_size(&self, path: &Path) -> std::io::Result<u64> {
self.inner.file_size(path).await
}
async fn open_read(&self, path: &Path) -> std::io::Result<FileHandle> {
self.inner.open_read(path).await
}
async fn read_range(
&self,
path: &Path,
range: std::ops::Range<u64>,
) -> std::io::Result<OwnedBytes> {
self.inner.read_range(path, range).await
}
async fn list_files(&self, prefix: &Path) -> std::io::Result<Vec<PathBuf>> {
self.inner.list_files(prefix).await
}
async fn open_lazy(&self, path: &Path) -> std::io::Result<FileHandle> {
self.inner.open_lazy(path).await
}
}
#[async_trait]
impl DirectoryWriter for RecordingDirectory {
async fn write(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
self.plain_writes.lock().unwrap().insert(path.to_path_buf());
self.inner.write(path, data).await
}
async fn delete(&self, path: &Path) -> std::io::Result<()> {
self.inner.delete(path).await
}
async fn rename(&self, from: &Path, to: &Path) -> std::io::Result<()> {
self.inner.rename(from, to).await
}
async fn sync(&self) -> std::io::Result<()> {
self.inner.sync().await
}
async fn streaming_writer(&self, path: &Path) -> std::io::Result<Box<dyn StreamingWriter>> {
self.streaming_writes
.lock()
.unwrap()
.insert(path.to_path_buf());
self.inner.streaming_writer(path).await
}
}
fn is_segment_meta(path: &Path) -> bool {
path.extension().is_some_and(|e| e == "meta")
}
#[tokio::test(flavor = "multi_thread")]
async fn test_segment_meta_written_through_durable_path() {
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let schema = schema_builder.build();
let dir = RecordingDirectory::default();
let mut writer = IndexWriter::create(dir.clone(), schema, IndexConfig::default())
.await
.unwrap();
for batch in 0..2 {
for i in 0..50 {
let mut doc = Document::new();
doc.add_text(title, format!("hello world batch {batch} doc {i}"));
writer.add_document(doc).unwrap();
}
writer.commit().await.unwrap();
}
writer.force_merge().await.unwrap();
writer.reorder().await.unwrap();
writer.wait_for_all_merges().await;
let streaming = dir.streaming_writes.lock().unwrap();
let metas_durable: Vec<_> = streaming.iter().filter(|p| is_segment_meta(p)).collect();
assert!(
!metas_durable.is_empty(),
"expected at least one segment .meta to be produced"
);
let plain = dir.plain_writes.lock().unwrap();
let metas_plain: Vec<_> = plain.iter().filter(|p| is_segment_meta(p)).collect();
assert!(
metas_plain.is_empty(),
"segment .meta written via non-durable DirectoryWriter::write \
(page cache only, lost on power failure): {metas_plain:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_segment_meta_files_are_produced_at_all() {
let mut schema_builder = SchemaBuilder::default();
let title = schema_builder.add_text_field("title", true, true);
let schema = schema_builder.build();
let dir = RecordingDirectory::default();
let mut writer = IndexWriter::create(dir.clone(), schema, IndexConfig::default())
.await
.unwrap();
let mut doc = Document::new();
doc.add_text(title, "hello");
writer.add_document(doc).unwrap();
writer.commit().await.unwrap();
let all: Vec<PathBuf> = {
let plain = dir.plain_writes.lock().unwrap();
let streaming = dir.streaming_writes.lock().unwrap();
plain.iter().chain(streaming.iter()).cloned().collect()
};
assert!(
all.iter().any(|p| is_segment_meta(p)),
"no segment .meta produced by commit at all; files seen: {all:?}"
);
}