1use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
13use lru::LruCache;
14use parking_lot::RwLock;
15use rustc_hash::FxHashMap;
16#[cfg(feature = "native")]
17use std::collections::BTreeMap;
18use std::io::{self, Write};
19use std::sync::Arc;
20#[cfg(feature = "native")]
21use std::sync::mpsc::{Receiver, SyncSender};
22#[cfg(feature = "native")]
23use std::thread::JoinHandle;
24
25use crate::DocId;
26use crate::compression::CompressionDict;
27#[cfg(feature = "native")]
28use crate::compression::CompressionLevel;
29use crate::directories::FileHandle;
30use crate::dsl::{Document, Schema};
31
32const STORE_MAGIC: u32 = 0x53544F52; const STORE_VERSION: u32 = 2; pub const STORE_BLOCK_SIZE: usize = 16 * 1024;
40
41pub const DEFAULT_DICT_SIZE: usize = 4 * 1024;
43
44const MAX_STORE_BLOCK_BYTES: usize = 64 * 1024 * 1024;
48const MAX_STORE_DICTIONARY_BYTES: u64 = 16 * 1024 * 1024;
49
50#[cfg(feature = "native")]
52const DEFAULT_COMPRESSION_LEVEL: CompressionLevel = CompressionLevel(3);
53
54fn write_store_index_and_footer(
58 writer: &mut (impl Write + ?Sized),
59 index: &[StoreBlockIndex],
60 data_end_offset: u64,
61 dict_offset: u64,
62 num_docs: u32,
63 has_dict: bool,
64) -> io::Result<()> {
65 writer.write_u32::<LittleEndian>(u32::try_from(index.len()).map_err(|_| {
66 io::Error::new(
67 io::ErrorKind::InvalidInput,
68 "too many document store blocks",
69 )
70 })?)?;
71 for entry in index {
72 writer.write_u32::<LittleEndian>(entry.first_doc_id)?;
73 writer.write_u64::<LittleEndian>(entry.offset)?;
74 writer.write_u32::<LittleEndian>(entry.length)?;
75 writer.write_u32::<LittleEndian>(entry.num_docs)?;
76 }
77 writer.write_u64::<LittleEndian>(data_end_offset)?;
78 writer.write_u64::<LittleEndian>(dict_offset)?;
79 writer.write_u32::<LittleEndian>(num_docs)?;
80 writer.write_u32::<LittleEndian>(if has_dict { 1 } else { 0 })?;
81 writer.write_u32::<LittleEndian>(STORE_VERSION)?;
82 writer.write_u32::<LittleEndian>(STORE_MAGIC)?;
83 Ok(())
84}
85
86pub fn serialize_document(doc: &Document, schema: &Schema) -> io::Result<Vec<u8>> {
98 let mut buf = Vec::with_capacity(256);
99 serialize_document_into(doc, schema, &mut buf)?;
100 Ok(buf)
101}
102
103pub fn serialize_document_into(
106 doc: &Document,
107 schema: &Schema,
108 buf: &mut Vec<u8>,
109) -> io::Result<()> {
110 use crate::dsl::FieldValue;
111
112 buf.clear();
113
114 let is_stored = |field: &crate::dsl::Field, value: &FieldValue| -> bool {
116 if matches!(
118 value,
119 FieldValue::DenseVector(_) | FieldValue::BinaryDenseVector(_)
120 ) {
121 return false;
122 }
123 schema.get_field_entry(*field).is_some_and(|e| e.stored)
124 };
125
126 let stored_count = doc
127 .field_values()
128 .iter()
129 .filter(|(field, value)| is_stored(field, value))
130 .count();
131
132 buf.write_u16::<LittleEndian>(
133 u16::try_from(stored_count)
134 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "too many stored fields"))?,
135 )?;
136
137 for (field, value) in doc.field_values().iter().filter(|(f, v)| is_stored(f, v)) {
138 buf.write_u16::<LittleEndian>(u16::try_from(field.0).map_err(|_| {
139 io::Error::new(io::ErrorKind::InvalidInput, "stored field id exceeds u16")
140 })?)?;
141 match value {
142 FieldValue::Text(s) => {
143 buf.push(0);
144 let bytes = s.as_bytes();
145 buf.write_u32::<LittleEndian>(u32::try_from(bytes.len()).map_err(|_| {
146 io::Error::new(io::ErrorKind::InvalidInput, "stored text is too large")
147 })?)?;
148 buf.extend_from_slice(bytes);
149 }
150 FieldValue::U64(v) => {
151 buf.push(1);
152 buf.write_u64::<LittleEndian>(*v)?;
153 }
154 FieldValue::I64(v) => {
155 buf.push(2);
156 buf.write_i64::<LittleEndian>(*v)?;
157 }
158 FieldValue::F64(v) => {
159 buf.push(3);
160 buf.write_f64::<LittleEndian>(*v)?;
161 }
162 FieldValue::Bytes(b) => {
163 buf.push(4);
164 buf.write_u32::<LittleEndian>(u32::try_from(b.len()).map_err(|_| {
165 io::Error::new(
166 io::ErrorKind::InvalidInput,
167 "stored byte field is too large",
168 )
169 })?)?;
170 buf.extend_from_slice(b);
171 }
172 FieldValue::SparseVector(entries) => {
173 buf.push(5);
174 buf.write_u32::<LittleEndian>(u32::try_from(entries.len()).map_err(|_| {
175 io::Error::new(
176 io::ErrorKind::InvalidInput,
177 "stored sparse vector is too large",
178 )
179 })?)?;
180 for (idx, val) in entries {
181 buf.write_u32::<LittleEndian>(*idx)?;
182 buf.write_f32::<LittleEndian>(*val)?;
183 }
184 }
185 FieldValue::DenseVector(values) => {
186 buf.push(6);
187 buf.write_u32::<LittleEndian>(u32::try_from(values.len()).map_err(|_| {
188 io::Error::new(
189 io::ErrorKind::InvalidInput,
190 "stored dense vector is too large",
191 )
192 })?)?;
193 let byte_slice = unsafe {
195 std::slice::from_raw_parts(values.as_ptr() as *const u8, values.len() * 4)
196 };
197 buf.extend_from_slice(byte_slice);
198 }
199 FieldValue::Json(v) => {
200 buf.push(7);
201 let json_bytes = serde_json::to_vec(v)
202 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
203 buf.write_u32::<LittleEndian>(u32::try_from(json_bytes.len()).map_err(|_| {
204 io::Error::new(io::ErrorKind::InvalidInput, "stored JSON is too large")
205 })?)?;
206 buf.extend_from_slice(&json_bytes);
207 }
208 FieldValue::BinaryDenseVector(b) => {
209 buf.push(8);
210 buf.write_u32::<LittleEndian>(u32::try_from(b.len()).map_err(|_| {
211 io::Error::new(
212 io::ErrorKind::InvalidInput,
213 "stored binary dense vector is too large",
214 )
215 })?)?;
216 buf.extend_from_slice(b);
217 }
218 }
219 }
220
221 Ok(())
222}
223
224#[cfg(feature = "native")]
226struct CompressedBlock {
227 seq: usize,
228 first_doc_id: DocId,
229 num_docs: u32,
230 compressed: Vec<u8>,
231}
232
233#[cfg(feature = "native")]
234struct CompressionJob {
235 seq: usize,
236 first_doc_id: DocId,
237 num_docs: u32,
238 data: Vec<u8>,
239}
240
241#[cfg(feature = "native")]
248struct StoreCompressionPool {
249 jobs: Option<SyncSender<CompressionJob>>,
250 results: Receiver<io::Result<CompressedBlock>>,
251 workers: Vec<JoinHandle<()>>,
252 num_threads: usize,
253}
254
255#[cfg(feature = "native")]
256impl StoreCompressionPool {
257 fn new(
258 num_threads: usize,
259 dict: Option<Arc<CompressionDict>>,
260 compression_level: CompressionLevel,
261 ) -> Self {
262 let num_threads = num_threads.max(1);
266 let (job_sender, job_receiver) =
267 std::sync::mpsc::sync_channel::<CompressionJob>(num_threads);
268 let job_receiver = Arc::new(std::sync::Mutex::new(job_receiver));
269 let (result_sender, result_receiver) = std::sync::mpsc::channel();
270 let mut workers = Vec::with_capacity(num_threads);
271
272 for worker_id in 0..num_threads {
273 let jobs = Arc::clone(&job_receiver);
274 let results = result_sender.clone();
275 let dict = dict.clone();
276 let worker = std::thread::Builder::new()
277 .name(format!("hermes-store-compress-{worker_id}"))
278 .spawn(move || {
279 loop {
280 let job = {
285 let receiver = jobs
286 .lock()
287 .unwrap_or_else(std::sync::PoisonError::into_inner);
288 receiver.recv()
289 };
290 let Ok(job) = job else {
291 break;
292 };
293
294 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
299 let compressed = if let Some(ref dict) = dict {
300 crate::compression::compress_with_dict(
301 &job.data,
302 compression_level,
303 dict,
304 )
305 } else {
306 crate::compression::compress(&job.data, compression_level)
307 }?;
308 Ok(CompressedBlock {
309 seq: job.seq,
310 first_doc_id: job.first_doc_id,
311 num_docs: job.num_docs,
312 compressed,
313 })
314 }))
315 .unwrap_or_else(|_| {
316 Err(io::Error::other(
317 "document-store compression worker panicked",
318 ))
319 });
320
321 if results.send(result).is_err() {
322 break;
323 }
324 }
325 })
326 .expect("failed to spawn document-store compression worker");
327 workers.push(worker);
328 }
329 drop(result_sender);
330
331 Self {
332 jobs: Some(job_sender),
333 results: result_receiver,
334 workers,
335 num_threads,
336 }
337 }
338
339 #[inline]
340 fn num_threads(&self) -> usize {
341 self.num_threads
342 }
343
344 fn submit(&self, job: CompressionJob) -> io::Result<()> {
345 self.jobs
346 .as_ref()
347 .ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "compression pool is closed"))?
348 .send(job)
349 .map_err(|_| {
350 io::Error::new(
351 io::ErrorKind::BrokenPipe,
352 "document-store compression workers stopped",
353 )
354 })
355 }
356
357 fn receive(&self) -> io::Result<CompressedBlock> {
358 self.results.recv().map_err(|_| {
359 io::Error::new(
360 io::ErrorKind::BrokenPipe,
361 "document-store compression result channel closed",
362 )
363 })?
364 }
365
366 fn shutdown(&mut self) -> io::Result<()> {
367 self.jobs.take();
370 let mut panicked = false;
371 for worker in self.workers.drain(..) {
372 panicked |= worker.join().is_err();
373 }
374 if panicked {
375 Err(io::Error::other(
376 "document-store compression worker panicked",
377 ))
378 } else {
379 Ok(())
380 }
381 }
382}
383
384#[cfg(feature = "native")]
385impl Drop for StoreCompressionPool {
386 fn drop(&mut self) {
387 self.jobs.take();
391 for worker in self.workers.drain(..) {
392 let _ = worker.join();
393 }
394 }
395}
396
397#[cfg(feature = "native")]
403pub struct EagerParallelStoreWriter<'a> {
404 writer: &'a mut dyn Write,
405 block_buffer: Vec<u8>,
406 serialize_buf: Vec<u8>,
408 workers: StoreCompressionPool,
409 ready_blocks: BTreeMap<usize, CompressedBlock>,
411 pending_results: usize,
413 max_in_flight: usize,
415 next_seq: usize,
416 next_write_seq: usize,
417 next_doc_id: DocId,
418 block_first_doc: DocId,
419 index: Vec<StoreBlockIndex>,
420 current_offset: u64,
421 dict: Option<Arc<CompressionDict>>,
422}
423
424#[cfg(feature = "native")]
425impl<'a> EagerParallelStoreWriter<'a> {
426 pub fn new(writer: &'a mut dyn Write, num_threads: usize) -> Self {
428 Self::with_compression_level(writer, num_threads, DEFAULT_COMPRESSION_LEVEL)
429 }
430
431 pub fn with_compression_level(
433 writer: &'a mut dyn Write,
434 num_threads: usize,
435 compression_level: CompressionLevel,
436 ) -> Self {
437 Self::with_optional_dict(writer, None, num_threads, compression_level)
438 }
439
440 pub fn with_dict(writer: &'a mut dyn Write, dict: CompressionDict, num_threads: usize) -> Self {
442 Self::with_dict_and_level(writer, dict, num_threads, DEFAULT_COMPRESSION_LEVEL)
443 }
444
445 pub fn with_dict_and_level(
447 writer: &'a mut dyn Write,
448 dict: CompressionDict,
449 num_threads: usize,
450 compression_level: CompressionLevel,
451 ) -> Self {
452 Self::with_optional_dict(writer, Some(Arc::new(dict)), num_threads, compression_level)
453 }
454
455 fn with_optional_dict(
456 writer: &'a mut dyn Write,
457 dict: Option<Arc<CompressionDict>>,
458 num_threads: usize,
459 compression_level: CompressionLevel,
460 ) -> Self {
461 let workers = StoreCompressionPool::new(num_threads, dict.clone(), compression_level);
462 let max_in_flight = workers.num_threads().saturating_mul(2).max(1);
465 Self {
466 writer,
467 block_buffer: Vec::with_capacity(STORE_BLOCK_SIZE),
468 serialize_buf: Vec::with_capacity(512),
469 workers,
470 ready_blocks: BTreeMap::new(),
471 pending_results: 0,
472 max_in_flight,
473 next_seq: 0,
474 next_write_seq: 0,
475 next_doc_id: 0,
476 block_first_doc: 0,
477 index: Vec::new(),
478 current_offset: 0,
479 dict,
480 }
481 }
482
483 pub fn store(&mut self, doc: &Document, schema: &Schema) -> io::Result<DocId> {
484 serialize_document_into(doc, schema, &mut self.serialize_buf)?;
485 if self.serialize_buf.len() > MAX_STORE_BLOCK_BYTES.saturating_sub(4) {
486 return Err(io::Error::new(
487 io::ErrorKind::InvalidInput,
488 "serialized document exceeds store block limit",
489 ));
490 }
491 let doc_id = self.next_doc_id;
492 self.next_doc_id = self
493 .next_doc_id
494 .checked_add(1)
495 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "document id overflow"))?;
496 self.block_buffer
497 .write_u32::<LittleEndian>(self.serialize_buf.len() as u32)?;
498 self.block_buffer.extend_from_slice(&self.serialize_buf);
499 if self.block_buffer.len() >= STORE_BLOCK_SIZE {
500 self.queue_compression()?;
501 }
502 Ok(doc_id)
503 }
504
505 pub fn store_raw(&mut self, doc_bytes: &[u8]) -> io::Result<DocId> {
507 if doc_bytes.len() > MAX_STORE_BLOCK_BYTES.saturating_sub(4) {
508 return Err(io::Error::new(
509 io::ErrorKind::InvalidInput,
510 "serialized document exceeds store block limit",
511 ));
512 }
513 let doc_id = self.next_doc_id;
514 self.next_doc_id = self
515 .next_doc_id
516 .checked_add(1)
517 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "document id overflow"))?;
518
519 self.block_buffer
520 .write_u32::<LittleEndian>(doc_bytes.len() as u32)?;
521 self.block_buffer.extend_from_slice(doc_bytes);
522
523 if self.block_buffer.len() >= STORE_BLOCK_SIZE {
524 self.queue_compression()?;
525 }
526
527 Ok(doc_id)
528 }
529
530 fn queue_compression(&mut self) -> io::Result<()> {
533 if self.block_buffer.is_empty() {
534 return Ok(());
535 }
536
537 let num_docs = self.next_doc_id - self.block_first_doc;
538 let data = std::mem::replace(&mut self.block_buffer, Vec::with_capacity(STORE_BLOCK_SIZE));
539 let seq = self.next_seq;
540 let first_doc_id = self.block_first_doc;
541
542 self.workers.submit(CompressionJob {
543 seq,
544 first_doc_id,
545 num_docs,
546 data,
547 })?;
548 self.next_seq = self
549 .next_seq
550 .checked_add(1)
551 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store block overflow"))?;
552 self.pending_results += 1;
553 self.block_first_doc = self.next_doc_id;
554
555 while self.outstanding_blocks() >= self.max_in_flight {
556 self.receive_and_write_ready()?;
557 }
558
559 Ok(())
560 }
561
562 #[inline]
563 fn outstanding_blocks(&self) -> usize {
564 self.next_seq - self.next_write_seq
565 }
566
567 fn receive_and_write_ready(&mut self) -> io::Result<()> {
568 if self.pending_results == 0 {
569 return Err(io::Error::new(
570 io::ErrorKind::BrokenPipe,
571 "missing document-store compression result",
572 ));
573 }
574 let block = self.workers.receive()?;
575 self.pending_results -= 1;
576 if block.seq < self.next_write_seq || self.ready_blocks.insert(block.seq, block).is_some() {
577 return Err(io::Error::new(
578 io::ErrorKind::InvalidData,
579 "duplicate document-store compression result",
580 ));
581 }
582 self.write_ready_blocks()
583 }
584
585 fn write_ready_blocks(&mut self) -> io::Result<()> {
586 while let Some(block) = self.ready_blocks.remove(&self.next_write_seq) {
587 let length = u32::try_from(block.compressed.len()).map_err(|_| {
588 io::Error::new(
589 io::ErrorKind::InvalidData,
590 "compressed store block too large",
591 )
592 })?;
593 self.writer.write_all(&block.compressed)?;
594 self.index.push(StoreBlockIndex {
595 first_doc_id: block.first_doc_id,
596 offset: self.current_offset,
597 length,
598 num_docs: block.num_docs,
599 });
600 self.current_offset = self
601 .current_offset
602 .checked_add(u64::from(length))
603 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
604 self.next_write_seq += 1;
605 }
606 Ok(())
607 }
608
609 pub fn finish(mut self) -> io::Result<u32> {
610 self.queue_compression()?;
613 while self.next_write_seq < self.next_seq {
614 self.receive_and_write_ready()?;
615 }
616 debug_assert_eq!(self.pending_results, 0);
617 debug_assert!(self.ready_blocks.is_empty());
618 self.workers.shutdown()?;
619
620 if self.index.is_empty() {
621 write_store_index_and_footer(&mut self.writer, &[], 0, 0, 0, false)?;
622 return Ok(0);
623 }
624
625 let dict_offset = if let Some(ref dict) = self.dict {
627 let offset = self.current_offset;
628 let dict_bytes = dict.as_bytes();
629 self.writer
630 .write_u32::<LittleEndian>(dict_bytes.len() as u32)?;
631 self.writer.write_all(dict_bytes)?;
632 Some(offset)
633 } else {
634 None
635 };
636
637 write_store_index_and_footer(
639 &mut self.writer,
640 &self.index,
641 self.current_offset,
642 dict_offset.unwrap_or(0),
643 self.next_doc_id,
644 self.dict.is_some(),
645 )?;
646
647 Ok(self.next_doc_id)
648 }
649}
650
651#[derive(Debug, Clone)]
653pub(crate) struct StoreBlockIndex {
654 pub(crate) first_doc_id: DocId,
655 pub(crate) offset: u64,
656 pub(crate) length: u32,
657 pub(crate) num_docs: u32,
658}
659
660pub struct AsyncStoreReader {
662 data_slice: FileHandle,
664 index: Vec<StoreBlockIndex>,
666 num_docs: u32,
667 dict: Option<CompressionDict>,
669 cache: Arc<SharedStoreCache>,
671 cache_namespace: StoreCacheNamespace,
673}
674
675struct CachedBlock {
681 data: Vec<u8>,
682 offsets: Vec<u32>,
685}
686
687impl CachedBlock {
688 fn build(data: Vec<u8>, num_docs: u32) -> io::Result<Self> {
689 if num_docs as usize > data.len() / 4 {
690 return Err(io::Error::new(
691 io::ErrorKind::InvalidData,
692 "store block document count exceeds block length",
693 ));
694 }
695 let mut offsets = Vec::new();
696 offsets.try_reserve_exact(num_docs as usize).map_err(|_| {
697 io::Error::new(
698 io::ErrorKind::InvalidData,
699 "store block has too many documents",
700 )
701 })?;
702 let mut pos = 0usize;
703 for _ in 0..num_docs {
704 let length_end = pos.checked_add(4).ok_or_else(|| {
705 io::Error::new(io::ErrorKind::InvalidData, "store block offset overflow")
706 })?;
707 if length_end > data.len() {
708 return Err(io::Error::new(
709 io::ErrorKind::InvalidData,
710 "truncated block while building offset table",
711 ));
712 }
713 offsets.push(u32::try_from(pos).map_err(|_| {
714 io::Error::new(io::ErrorKind::InvalidData, "store block offset exceeds u32")
715 })?);
716 let doc_len =
717 u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]])
718 as usize;
719 pos = length_end.checked_add(doc_len).ok_or_else(|| {
720 io::Error::new(io::ErrorKind::InvalidData, "store document length overflow")
721 })?;
722 if pos > data.len() {
723 return Err(io::Error::new(
724 io::ErrorKind::UnexpectedEof,
725 "store document is truncated",
726 ));
727 }
728 }
729 if pos != data.len() {
730 return Err(io::Error::new(
731 io::ErrorKind::InvalidData,
732 "store block contains trailing data",
733 ));
734 }
735 Ok(Self { data, offsets })
736 }
737
738 fn doc_bytes(&self, doc_offset_in_block: u32) -> io::Result<&[u8]> {
740 let idx = doc_offset_in_block as usize;
741 if idx >= self.offsets.len() {
742 return Err(io::Error::new(
743 io::ErrorKind::InvalidData,
744 "doc offset out of range",
745 ));
746 }
747 let start = self.offsets[idx] as usize;
748 let data_start = start.checked_add(4).ok_or_else(|| {
749 io::Error::new(io::ErrorKind::InvalidData, "store document offset overflow")
750 })?;
751 if data_start > self.data.len() {
752 return Err(io::Error::new(
753 io::ErrorKind::InvalidData,
754 "truncated doc length",
755 ));
756 }
757 let doc_len = u32::from_le_bytes([
758 self.data[start],
759 self.data[start + 1],
760 self.data[start + 2],
761 self.data[start + 3],
762 ]) as usize;
763 let data_end = data_start.checked_add(doc_len).ok_or_else(|| {
764 io::Error::new(io::ErrorKind::InvalidData, "store document length overflow")
765 })?;
766 if data_end > self.data.len() {
767 return Err(io::Error::new(
768 io::ErrorKind::InvalidData,
769 "doc data overflow",
770 ));
771 }
772 Ok(&self.data[data_start..data_end])
773 }
774
775 #[inline]
776 fn retained_bytes(&self) -> usize {
777 self.data.capacity() + self.offsets.capacity() * std::mem::size_of::<u32>()
778 }
779}
780
781#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
782struct StoreCacheNamespace {
783 directory: usize,
784 segment: u128,
785}
786
787#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
788struct StoreCacheKey {
789 namespace: StoreCacheNamespace,
790 first_doc_id: DocId,
791}
792
793struct SharedStoreCacheState {
794 blocks: LruCache<StoreCacheKey, Arc<CachedBlock>>,
795 retained_bytes: usize,
796 namespace_bytes: FxHashMap<StoreCacheNamespace, usize>,
797 namespace_readers: FxHashMap<StoreCacheNamespace, usize>,
798}
799
800pub(crate) struct SharedStoreCache {
809 state: RwLock<SharedStoreCacheState>,
810 max_bytes: usize,
811 max_entry_bytes: usize,
815}
816
817impl std::fmt::Debug for SharedStoreCache {
818 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
819 formatter
820 .debug_struct("SharedStoreCache")
821 .field("max_bytes", &self.max_bytes)
822 .field("max_entry_bytes", &self.max_entry_bytes)
823 .field("retained_bytes", &self.total_bytes())
824 .finish()
825 }
826}
827
828impl SharedStoreCache {
829 const MAX_ADMITTED_ENTRY_BYTES: usize = 8 * 1024 * 1024;
830
831 pub(crate) fn new(max_bytes: usize) -> Self {
832 Self::with_limits(max_bytes, max_bytes.min(Self::MAX_ADMITTED_ENTRY_BYTES))
833 }
834
835 fn with_limits(max_bytes: usize, max_entry_bytes: usize) -> Self {
836 Self {
837 state: RwLock::new(SharedStoreCacheState {
838 blocks: LruCache::unbounded(),
839 retained_bytes: 0,
840 namespace_bytes: FxHashMap::default(),
841 namespace_readers: FxHashMap::default(),
842 }),
843 max_bytes,
844 max_entry_bytes: max_entry_bytes.min(max_bytes),
845 }
846 }
847
848 fn register(&self, namespace: StoreCacheNamespace) {
849 if self.max_bytes == 0 {
850 return;
851 }
852 let mut state = self.state.write();
853 *state.namespace_readers.entry(namespace).or_default() += 1;
854 }
855
856 fn unregister(&self, namespace: StoreCacheNamespace) {
857 if self.max_bytes == 0 {
858 return;
859 }
860 let mut state = self.state.write();
861 let Some(readers) = state.namespace_readers.get_mut(&namespace) else {
862 return;
863 };
864 *readers -= 1;
865 if *readers > 0 {
866 return;
867 }
868 state.namespace_readers.remove(&namespace);
869
870 let keys: Vec<_> = state
874 .blocks
875 .iter()
876 .filter_map(|(key, _)| (key.namespace == namespace).then_some(*key))
877 .collect();
878 for key in keys {
879 if let Some(block) = state.blocks.pop(&key) {
880 state.retained_bytes = state.retained_bytes.saturating_sub(block.retained_bytes());
881 }
882 }
883 state.namespace_bytes.remove(&namespace);
884 }
885
886 fn get(&self, key: StoreCacheKey) -> Option<Arc<CachedBlock>> {
887 self.state.read().blocks.peek(&key).map(Arc::clone)
891 }
892
893 fn insert(&self, key: StoreCacheKey, block: Arc<CachedBlock>) -> Arc<CachedBlock> {
896 let bytes = block.retained_bytes();
897 if self.max_bytes == 0 || bytes == 0 || bytes > self.max_entry_bytes {
898 return block;
899 }
900
901 let mut state = self.state.write();
902 if let Some(existing) = state.blocks.get(&key) {
903 return Arc::clone(existing);
904 }
905
906 state.retained_bytes = state.retained_bytes.saturating_add(bytes);
907 *state.namespace_bytes.entry(key.namespace).or_default() = state
908 .namespace_bytes
909 .get(&key.namespace)
910 .copied()
911 .unwrap_or(0)
912 .saturating_add(bytes);
913 state.blocks.put(key, Arc::clone(&block));
914
915 while state.retained_bytes > self.max_bytes {
916 let Some((evicted_key, evicted)) = state.blocks.pop_lru() else {
917 state.retained_bytes = 0;
918 state.namespace_bytes.clear();
919 break;
920 };
921 let evicted_bytes = evicted.retained_bytes();
922 state.retained_bytes = state.retained_bytes.saturating_sub(evicted_bytes);
923 if let Some(namespace_bytes) = state.namespace_bytes.get_mut(&evicted_key.namespace) {
924 *namespace_bytes = namespace_bytes.saturating_sub(evicted_bytes);
925 if *namespace_bytes == 0 {
926 state.namespace_bytes.remove(&evicted_key.namespace);
927 }
928 }
929 }
930 block
931 }
932
933 pub(crate) fn total_bytes(&self) -> usize {
934 self.state.read().retained_bytes
935 }
936
937 pub(crate) fn total_blocks(&self) -> usize {
938 self.state.read().blocks.len()
939 }
940
941 fn namespace_bytes(&self, namespace: StoreCacheNamespace) -> usize {
942 self.state
943 .read()
944 .namespace_bytes
945 .get(&namespace)
946 .copied()
947 .unwrap_or(0)
948 }
949
950 fn namespace_blocks(&self, namespace: StoreCacheNamespace) -> usize {
951 self.state
952 .read()
953 .blocks
954 .iter()
955 .filter(|(key, _)| key.namespace == namespace)
956 .count()
957 }
958}
959
960impl Drop for AsyncStoreReader {
961 fn drop(&mut self) {
962 self.cache.unregister(self.cache_namespace);
963 }
964}
965
966impl AsyncStoreReader {
967 pub(crate) async fn open(
970 file_handle: FileHandle,
971 directory_namespace: usize,
972 segment_namespace: u128,
973 cache: Arc<SharedStoreCache>,
974 ) -> io::Result<Self> {
975 let file_len = file_handle.len();
976 if file_len < 32 {
978 return Err(io::Error::new(
979 io::ErrorKind::InvalidData,
980 "Store too small",
981 ));
982 }
983
984 let footer = file_handle
986 .read_bytes_range(file_len - 32..file_len)
987 .await?;
988 let mut reader = footer.as_slice();
989 let data_end_offset = reader.read_u64::<LittleEndian>()?;
990 let dict_offset = reader.read_u64::<LittleEndian>()?;
991 let num_docs = reader.read_u32::<LittleEndian>()?;
992 let has_dict = reader.read_u32::<LittleEndian>()? != 0;
993 let version = reader.read_u32::<LittleEndian>()?;
994 let magic = reader.read_u32::<LittleEndian>()?;
995
996 if magic != STORE_MAGIC {
997 return Err(io::Error::new(
998 io::ErrorKind::InvalidData,
999 "Invalid store magic",
1000 ));
1001 }
1002 if version != STORE_VERSION {
1003 return Err(io::Error::new(
1004 io::ErrorKind::InvalidData,
1005 format!("Unsupported store version: {}", version),
1006 ));
1007 }
1008
1009 let index_end = file_len - 32;
1010 if data_end_offset > index_end {
1011 return Err(io::Error::new(
1012 io::ErrorKind::InvalidData,
1013 "store data section extends past its footer",
1014 ));
1015 }
1016
1017 let (dict, index_start) = if has_dict {
1019 if dict_offset < data_end_offset || dict_offset >= index_end {
1020 return Err(io::Error::new(
1021 io::ErrorKind::InvalidData,
1022 "store dictionary offset is out of bounds",
1023 ));
1024 }
1025 let dict_start = dict_offset;
1026 let dict_header_end = dict_start.checked_add(4).ok_or_else(|| {
1027 io::Error::new(
1028 io::ErrorKind::InvalidData,
1029 "store dictionary range overflow",
1030 )
1031 })?;
1032 if dict_header_end > index_end {
1033 return Err(io::Error::new(
1034 io::ErrorKind::UnexpectedEof,
1035 "store dictionary length is truncated",
1036 ));
1037 }
1038 let dict_len_bytes = file_handle
1039 .read_bytes_range(dict_start..dict_header_end)
1040 .await?;
1041 let dict_len = (&dict_len_bytes[..]).read_u32::<LittleEndian>()? as u64;
1042 if dict_len > MAX_STORE_DICTIONARY_BYTES {
1043 return Err(io::Error::new(
1044 io::ErrorKind::InvalidData,
1045 "store dictionary exceeds safety limit",
1046 ));
1047 }
1048 let dict_end = dict_header_end.checked_add(dict_len).ok_or_else(|| {
1049 io::Error::new(
1050 io::ErrorKind::InvalidData,
1051 "store dictionary range overflow",
1052 )
1053 })?;
1054 if dict_end > index_end {
1055 return Err(io::Error::new(
1056 io::ErrorKind::UnexpectedEof,
1057 "store dictionary is truncated",
1058 ));
1059 }
1060 let dict_bytes = file_handle
1061 .read_bytes_range(dict_header_end..dict_end)
1062 .await?;
1063 (
1064 Some(CompressionDict::from_owned_bytes(dict_bytes)),
1065 dict_end,
1066 )
1067 } else {
1068 if dict_offset != 0 {
1069 return Err(io::Error::new(
1070 io::ErrorKind::InvalidData,
1071 "store without a dictionary has a dictionary offset",
1072 ));
1073 }
1074 (None, data_end_offset)
1075 };
1076
1077 if index_start > index_end {
1078 return Err(io::Error::new(
1079 io::ErrorKind::InvalidData,
1080 "store index offset is out of bounds",
1081 ));
1082 }
1083
1084 let index_bytes = file_handle.read_bytes_range(index_start..index_end).await?;
1085 let mut reader = index_bytes.as_slice();
1086
1087 let num_blocks = reader.read_u32::<LittleEndian>()? as usize;
1088 let required_index_bytes = num_blocks.checked_mul(20).ok_or_else(|| {
1089 io::Error::new(io::ErrorKind::InvalidData, "store index size overflow")
1090 })?;
1091 if reader.len() != required_index_bytes {
1092 return Err(io::Error::new(
1093 io::ErrorKind::InvalidData,
1094 "store index length is inconsistent",
1095 ));
1096 }
1097 let mut index = Vec::new();
1098 index
1099 .try_reserve_exact(num_blocks)
1100 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "too many store blocks"))?;
1101
1102 let mut expected_doc = 0u32;
1103 let mut expected_offset = 0u64;
1104
1105 for _ in 0..num_blocks {
1106 let first_doc_id = reader.read_u32::<LittleEndian>()?;
1107 let offset = reader.read_u64::<LittleEndian>()?;
1108 let length = reader.read_u32::<LittleEndian>()?;
1109 let num_docs_in_block = reader.read_u32::<LittleEndian>()?;
1110
1111 let end = offset.checked_add(length as u64).ok_or_else(|| {
1112 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1113 })?;
1114 if first_doc_id != expected_doc
1115 || num_docs_in_block == 0
1116 || offset != expected_offset
1117 || end > data_end_offset
1118 {
1119 return Err(io::Error::new(
1120 io::ErrorKind::InvalidData,
1121 "store block index is inconsistent",
1122 ));
1123 }
1124 expected_doc = expected_doc.checked_add(num_docs_in_block).ok_or_else(|| {
1125 io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1126 })?;
1127 expected_offset = end;
1128
1129 index.push(StoreBlockIndex {
1130 first_doc_id,
1131 offset,
1132 length,
1133 num_docs: num_docs_in_block,
1134 });
1135 }
1136
1137 if expected_doc != num_docs || expected_offset != data_end_offset {
1138 return Err(io::Error::new(
1139 io::ErrorKind::InvalidData,
1140 "store footer totals do not match its block index",
1141 ));
1142 }
1143
1144 let data_slice = file_handle.slice(0..data_end_offset);
1146
1147 let cache_namespace = StoreCacheNamespace {
1148 directory: directory_namespace,
1149 segment: segment_namespace,
1150 };
1151 cache.register(cache_namespace);
1152 Ok(Self {
1153 data_slice,
1154 index,
1155 num_docs,
1156 dict,
1157 cache,
1158 cache_namespace,
1159 })
1160 }
1161
1162 pub fn num_docs(&self) -> u32 {
1164 self.num_docs
1165 }
1166
1167 pub fn cached_blocks(&self) -> usize {
1169 self.cache.namespace_blocks(self.cache_namespace)
1170 }
1171
1172 pub fn cached_bytes(&self) -> usize {
1174 self.cache.namespace_bytes(self.cache_namespace)
1175 }
1176
1177 pub async fn get(&self, doc_id: DocId, schema: &Schema) -> io::Result<Option<Document>> {
1179 if doc_id >= self.num_docs {
1180 return Ok(None);
1181 }
1182
1183 let t = crate::observe::Timer::start();
1184 let (entry, block) = self.find_and_load_block(doc_id).await?;
1185 let doc_bytes = block.doc_bytes(doc_id - entry.first_doc_id)?;
1186 let result = deserialize_document(doc_bytes, schema).map(Some);
1187 crate::observe::store_get(schema.index_label(), t.secs());
1188 result
1189 }
1190
1191 pub async fn get_fields(
1197 &self,
1198 doc_id: DocId,
1199 schema: &Schema,
1200 field_ids: &[u32],
1201 ) -> io::Result<Option<Document>> {
1202 if doc_id >= self.num_docs {
1203 return Ok(None);
1204 }
1205
1206 let t = crate::observe::Timer::start();
1207 let (entry, block) = self.find_and_load_block(doc_id).await?;
1208 let doc_bytes = block.doc_bytes(doc_id - entry.first_doc_id)?;
1209 let result = deserialize_document_fields(doc_bytes, schema, field_ids).map(Some);
1210 crate::observe::store_get(schema.index_label(), t.secs());
1211 result
1212 }
1213
1214 async fn find_and_load_block(
1216 &self,
1217 doc_id: DocId,
1218 ) -> io::Result<(&StoreBlockIndex, Arc<CachedBlock>)> {
1219 let block_idx = self
1220 .index
1221 .binary_search_by(|entry| {
1222 if doc_id < entry.first_doc_id {
1223 std::cmp::Ordering::Greater
1224 } else if doc_id >= entry.first_doc_id + entry.num_docs {
1225 std::cmp::Ordering::Less
1226 } else {
1227 std::cmp::Ordering::Equal
1228 }
1229 })
1230 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Doc not found in index"))?;
1231
1232 let entry = &self.index[block_idx];
1233 let block = self.load_block(entry).await?;
1234 Ok((entry, block))
1235 }
1236
1237 async fn load_block(&self, entry: &StoreBlockIndex) -> io::Result<Arc<CachedBlock>> {
1238 let key = StoreCacheKey {
1239 namespace: self.cache_namespace,
1240 first_doc_id: entry.first_doc_id,
1241 };
1242 if let Some(block) = self.cache.get(key) {
1243 return Ok(block);
1244 }
1245
1246 let start = entry.offset;
1248 let end = start.checked_add(entry.length as u64).ok_or_else(|| {
1249 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1250 })?;
1251 let compressed = self.data_slice.read_bytes_range(start..end).await?;
1252
1253 let decompressed = if let Some(ref dict) = self.dict {
1255 crate::compression::decompress_with_dict_limited(
1256 compressed.as_slice(),
1257 dict,
1258 MAX_STORE_BLOCK_BYTES,
1259 )?
1260 } else {
1261 crate::compression::decompress_limited(compressed.as_slice(), MAX_STORE_BLOCK_BYTES)?
1262 };
1263
1264 let cached = CachedBlock::build(decompressed, entry.num_docs)?;
1266 Ok(self.cache.insert(key, Arc::new(cached)))
1267 }
1268}
1269
1270pub fn deserialize_document_fields(
1277 data: &[u8],
1278 schema: &Schema,
1279 field_ids: &[u32],
1280) -> io::Result<Document> {
1281 deserialize_document_inner(data, schema, Some(field_ids))
1282}
1283
1284pub fn deserialize_document(data: &[u8], schema: &Schema) -> io::Result<Document> {
1288 deserialize_document_inner(data, schema, None)
1289}
1290
1291fn deserialize_document_inner(
1293 data: &[u8],
1294 _schema: &Schema,
1295 field_filter: Option<&[u32]>,
1296) -> io::Result<Document> {
1297 use crate::dsl::Field;
1298
1299 let mut reader = data;
1300 let num_fields = reader.read_u16::<LittleEndian>()? as usize;
1301 let mut doc = Document::new();
1302
1303 for _ in 0..num_fields {
1304 let field_id = reader.read_u16::<LittleEndian>()?;
1305 let type_tag = reader.read_u8()?;
1306
1307 let wanted = field_filter.is_none_or(|ids| ids.contains(&(field_id as u32)));
1308
1309 match type_tag {
1310 0 => {
1311 let len = reader.read_u32::<LittleEndian>()? as usize;
1313 let bytes = take_document_bytes(&mut reader, len, "text field")?;
1314 if wanted {
1315 let s = std::str::from_utf8(bytes)
1316 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
1317 doc.add_text(Field(field_id as u32), s);
1318 }
1319 }
1320 1 => {
1321 let v = reader.read_u64::<LittleEndian>()?;
1323 if wanted {
1324 doc.add_u64(Field(field_id as u32), v);
1325 }
1326 }
1327 2 => {
1328 let v = reader.read_i64::<LittleEndian>()?;
1330 if wanted {
1331 doc.add_i64(Field(field_id as u32), v);
1332 }
1333 }
1334 3 => {
1335 let v = reader.read_f64::<LittleEndian>()?;
1337 if wanted {
1338 doc.add_f64(Field(field_id as u32), v);
1339 }
1340 }
1341 4 => {
1342 let len = reader.read_u32::<LittleEndian>()? as usize;
1344 let bytes = take_document_bytes(&mut reader, len, "byte field")?;
1345 if wanted {
1346 doc.add_bytes(Field(field_id as u32), bytes.to_vec());
1347 }
1348 }
1349 5 => {
1350 let count = reader.read_u32::<LittleEndian>()? as usize;
1352 let byte_len = count.checked_mul(8).ok_or_else(|| {
1353 io::Error::new(io::ErrorKind::InvalidData, "sparse vector size overflow")
1354 })?;
1355 let bytes = take_document_bytes(&mut reader, byte_len, "sparse vector")?;
1356 if wanted {
1357 let mut entries = Vec::new();
1358 entries.try_reserve_exact(count).map_err(|_| {
1359 io::Error::new(io::ErrorKind::InvalidData, "sparse vector is too large")
1360 })?;
1361 let mut vector_reader = bytes;
1362 for _ in 0..count {
1363 let idx = vector_reader.read_u32::<LittleEndian>()?;
1364 let val = vector_reader.read_f32::<LittleEndian>()?;
1365 entries.push((idx, val));
1366 }
1367 doc.add_sparse_vector(Field(field_id as u32), entries);
1368 }
1369 }
1370 6 => {
1371 let count = reader.read_u32::<LittleEndian>()? as usize;
1373 let byte_len = count.checked_mul(4).ok_or_else(|| {
1374 io::Error::new(io::ErrorKind::InvalidData, "dense vector size overflow")
1375 })?;
1376 let bytes = take_document_bytes(&mut reader, byte_len, "dense vector")?;
1377 if wanted {
1378 let mut values = vec![0.0f32; count];
1379 unsafe {
1380 std::ptr::copy_nonoverlapping(
1381 bytes.as_ptr(),
1382 values.as_mut_ptr() as *mut u8,
1383 byte_len,
1384 );
1385 }
1386 doc.add_dense_vector(Field(field_id as u32), values);
1387 }
1388 }
1389 7 => {
1390 let len = reader.read_u32::<LittleEndian>()? as usize;
1392 let bytes = take_document_bytes(&mut reader, len, "JSON field")?;
1393 if wanted {
1394 let v: serde_json::Value = serde_json::from_slice(bytes)
1395 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
1396 doc.add_json(Field(field_id as u32), v);
1397 }
1398 }
1399 8 => {
1400 let len = reader.read_u32::<LittleEndian>()? as usize;
1402 let bytes = take_document_bytes(&mut reader, len, "binary dense vector")?;
1403 if wanted {
1404 doc.add_binary_dense_vector(Field(field_id as u32), bytes.to_vec());
1405 }
1406 }
1407 _ => {
1408 return Err(io::Error::new(
1409 io::ErrorKind::InvalidData,
1410 format!("Unknown field type tag: {}", type_tag),
1411 ));
1412 }
1413 }
1414 }
1415
1416 Ok(doc)
1417}
1418
1419fn take_document_bytes<'a>(reader: &mut &'a [u8], len: usize, field: &str) -> io::Result<&'a [u8]> {
1420 if len > reader.len() {
1421 return Err(io::Error::new(
1422 io::ErrorKind::UnexpectedEof,
1423 format!("{field} is truncated"),
1424 ));
1425 }
1426 let (value, remaining) = reader.split_at(len);
1427 *reader = remaining;
1428 Ok(value)
1429}
1430
1431#[derive(Debug, Clone)]
1433pub struct RawStoreBlock {
1434 pub first_doc_id: DocId,
1435 pub num_docs: u32,
1436 pub offset: u64,
1437 pub length: u32,
1438}
1439
1440pub struct StoreMerger<'a, W: Write> {
1451 writer: &'a mut W,
1452 index: Vec<StoreBlockIndex>,
1453 current_offset: u64,
1454 next_doc_id: DocId,
1455}
1456
1457impl<'a, W: Write> StoreMerger<'a, W> {
1458 pub fn new(writer: &'a mut W) -> Self {
1459 Self {
1460 writer,
1461 index: Vec::new(),
1462 current_offset: 0,
1463 next_doc_id: 0,
1464 }
1465 }
1466
1467 pub async fn append_store(
1472 &mut self,
1473 data_slice: &FileHandle,
1474 blocks: &[RawStoreBlock],
1475 cancellation: Option<&std::sync::atomic::AtomicBool>,
1476 ) -> io::Result<()> {
1477 for block in blocks {
1478 if cancellation
1479 .is_some_and(|cancelled| cancelled.load(std::sync::atomic::Ordering::Relaxed))
1480 {
1481 return Err(io::Error::new(
1482 io::ErrorKind::Interrupted,
1483 "store merge cancelled",
1484 ));
1485 }
1486 let start = block.offset;
1488 let end = start.checked_add(block.length as u64).ok_or_else(|| {
1489 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1490 })?;
1491 if end > data_slice.len() {
1492 return Err(io::Error::new(
1493 io::ErrorKind::UnexpectedEof,
1494 "store block range is out of bounds",
1495 ));
1496 }
1497 let compressed_data = data_slice.read_bytes_range(start..end).await?;
1498
1499 self.writer.write_all(compressed_data.as_slice())?;
1501
1502 self.index.push(StoreBlockIndex {
1504 first_doc_id: self.next_doc_id,
1505 offset: self.current_offset,
1506 length: block.length,
1507 num_docs: block.num_docs,
1508 });
1509
1510 self.current_offset = self
1511 .current_offset
1512 .checked_add(block.length as u64)
1513 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
1514 self.next_doc_id = self
1515 .next_doc_id
1516 .checked_add(block.num_docs)
1517 .ok_or_else(|| {
1518 io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1519 })?;
1520 }
1521
1522 Ok(())
1523 }
1524
1525 pub async fn append_store_recompressing(
1532 &mut self,
1533 store: &AsyncStoreReader,
1534 cancellation: Option<&std::sync::atomic::AtomicBool>,
1535 ) -> io::Result<()> {
1536 let dict = store.dict();
1537 let data_slice = store.data_slice();
1538 let blocks = store.block_index();
1539
1540 for block in blocks {
1541 if cancellation
1542 .is_some_and(|cancelled| cancelled.load(std::sync::atomic::Ordering::Relaxed))
1543 {
1544 return Err(io::Error::new(
1545 io::ErrorKind::Interrupted,
1546 "store merge cancelled",
1547 ));
1548 }
1549 let start = block.offset;
1550 let end = start.checked_add(block.length as u64).ok_or_else(|| {
1551 io::Error::new(io::ErrorKind::InvalidData, "store block range overflow")
1552 })?;
1553 if end > data_slice.len() {
1554 return Err(io::Error::new(
1555 io::ErrorKind::UnexpectedEof,
1556 "store block range is out of bounds",
1557 ));
1558 }
1559 let compressed = data_slice.read_bytes_range(start..end).await?;
1560
1561 let decompressed = if let Some(d) = dict {
1563 crate::compression::decompress_with_dict_limited(
1564 compressed.as_slice(),
1565 d,
1566 MAX_STORE_BLOCK_BYTES,
1567 )?
1568 } else {
1569 crate::compression::decompress_limited(
1570 compressed.as_slice(),
1571 MAX_STORE_BLOCK_BYTES,
1572 )?
1573 };
1574
1575 let recompressed = crate::compression::compress(
1577 &decompressed,
1578 crate::compression::CompressionLevel::default(),
1579 )?;
1580
1581 self.writer.write_all(&recompressed)?;
1582
1583 self.index.push(StoreBlockIndex {
1584 first_doc_id: self.next_doc_id,
1585 offset: self.current_offset,
1586 length: u32::try_from(recompressed.len()).map_err(|_| {
1587 io::Error::new(
1588 io::ErrorKind::InvalidData,
1589 "compressed store block too large",
1590 )
1591 })?,
1592 num_docs: block.num_docs,
1593 });
1594
1595 self.current_offset = self
1596 .current_offset
1597 .checked_add(recompressed.len() as u64)
1598 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "store size overflow"))?;
1599 self.next_doc_id = self
1600 .next_doc_id
1601 .checked_add(block.num_docs)
1602 .ok_or_else(|| {
1603 io::Error::new(io::ErrorKind::InvalidData, "store document count overflow")
1604 })?;
1605 }
1606
1607 Ok(())
1608 }
1609
1610 pub fn finish(self) -> io::Result<u32> {
1612 let data_end_offset = self.current_offset;
1613
1614 let dict_offset = 0u64;
1616
1617 write_store_index_and_footer(
1619 self.writer,
1620 &self.index,
1621 data_end_offset,
1622 dict_offset,
1623 self.next_doc_id,
1624 false,
1625 )?;
1626
1627 Ok(self.next_doc_id)
1628 }
1629}
1630
1631impl AsyncStoreReader {
1632 pub fn raw_blocks(&self) -> Vec<RawStoreBlock> {
1634 self.index
1635 .iter()
1636 .map(|entry| RawStoreBlock {
1637 first_doc_id: entry.first_doc_id,
1638 num_docs: entry.num_docs,
1639 offset: entry.offset,
1640 length: entry.length,
1641 })
1642 .collect()
1643 }
1644
1645 pub fn data_slice(&self) -> &FileHandle {
1647 &self.data_slice
1648 }
1649
1650 pub fn has_dict(&self) -> bool {
1652 self.dict.is_some()
1653 }
1654
1655 pub fn dict(&self) -> Option<&CompressionDict> {
1657 self.dict.as_ref()
1658 }
1659
1660 pub(crate) fn block_index(&self) -> &[StoreBlockIndex] {
1662 &self.index
1663 }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668 use super::*;
1669
1670 #[cfg(feature = "native")]
1671 fn raw_store_bytes(num_threads: usize, documents: &[Vec<u8>]) -> Vec<u8> {
1672 let mut output = Vec::new();
1673 let mut store = EagerParallelStoreWriter::with_compression_level(
1674 &mut output,
1675 num_threads,
1676 CompressionLevel::FAST,
1677 );
1678 for document in documents {
1679 store.store_raw(document).unwrap();
1680 }
1681 assert_eq!(store.finish().unwrap() as usize, documents.len());
1682 output
1683 }
1684
1685 fn cached_test_block(byte: u8) -> Arc<CachedBlock> {
1686 Arc::new(CachedBlock::build(vec![4, 0, 0, 0, byte, byte, byte, byte], 1).unwrap())
1687 }
1688
1689 #[cfg(feature = "native")]
1690 #[test]
1691 fn parallel_store_is_byte_identical_across_worker_counts() {
1692 let documents: Vec<Vec<u8>> = (0..64usize)
1695 .map(|doc| {
1696 let len = 1_000 + (doc * 7_919 % 40_000);
1697 (0..len)
1698 .map(|offset| ((doc * 17 + offset * 31 + offset / 7) % 251) as u8)
1699 .collect()
1700 })
1701 .collect();
1702
1703 let single_worker = raw_store_bytes(1, &documents);
1704 let four_workers = raw_store_bytes(4, &documents);
1705 assert_eq!(four_workers, single_worker);
1706 }
1707
1708 #[cfg(feature = "native")]
1709 #[test]
1710 fn parallel_store_bounds_in_flight_blocks_and_streams_before_finish() {
1711 use std::sync::atomic::{AtomicUsize, Ordering};
1712
1713 struct CountingWriter(Arc<AtomicUsize>);
1714 impl Write for CountingWriter {
1715 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
1716 self.0.fetch_add(bytes.len(), Ordering::Relaxed);
1717 Ok(bytes.len())
1718 }
1719
1720 fn flush(&mut self) -> io::Result<()> {
1721 Ok(())
1722 }
1723 }
1724
1725 let bytes_written = Arc::new(AtomicUsize::new(0));
1726 let mut output = CountingWriter(Arc::clone(&bytes_written));
1727 let mut store = EagerParallelStoreWriter::new(&mut output, 3);
1728 assert_eq!(store.workers.num_threads(), 3);
1729 assert_eq!(store.max_in_flight, 6);
1730
1731 let document = vec![7u8; STORE_BLOCK_SIZE];
1732 for _ in 0..24 {
1733 store.store_raw(&document).unwrap();
1734 assert!(store.outstanding_blocks() < store.max_in_flight);
1735 assert_eq!(
1736 store.outstanding_blocks(),
1737 store.pending_results + store.ready_blocks.len()
1738 );
1739 }
1740
1741 assert!(bytes_written.load(Ordering::Relaxed) > 0);
1744 assert_eq!(store.finish().unwrap(), 24);
1745 }
1746
1747 #[cfg(feature = "native")]
1748 #[test]
1749 fn parallel_store_zero_threads_falls_back_to_one_worker() {
1750 let mut output = Vec::new();
1751 let store = EagerParallelStoreWriter::new(&mut output, 0);
1752 assert_eq!(store.workers.num_threads(), 1);
1753 assert_eq!(store.max_in_flight, 2);
1754 assert_eq!(store.finish().unwrap(), 0);
1755 assert_eq!(output.len(), 36);
1757 }
1758
1759 #[test]
1760 fn cached_block_rejects_truncated_and_trailing_documents() {
1761 assert!(CachedBlock::build(vec![8, 0, 0, 0, 1], 1).is_err());
1762 assert!(CachedBlock::build(vec![0, 0, 0, 0, 1], 1).is_err());
1763 }
1764
1765 #[test]
1766 fn document_deserializer_rejects_length_prefixed_slice_overrun() {
1767 let schema = Schema::builder().build();
1768 let truncated_text = [1, 0, 0, 0, 0, 5, 0, 0, 0, b'x'];
1769 assert!(deserialize_document(&truncated_text, &schema).is_err());
1770
1771 let truncated_sparse = [1, 0, 0, 0, 5, 2, 0, 0, 0, 1, 0, 0, 0];
1772 assert!(deserialize_document(&truncated_sparse, &schema).is_err());
1773 }
1774
1775 #[test]
1776 fn shared_store_cache_is_byte_bounded_and_read_concurrent() {
1777 let block_bytes = cached_test_block(1).retained_bytes();
1778 let cache = SharedStoreCache::with_limits(block_bytes * 2, block_bytes);
1779 let key = |first_doc_id| StoreCacheKey {
1780 namespace: StoreCacheNamespace {
1781 directory: 1,
1782 segment: 7,
1783 },
1784 first_doc_id,
1785 };
1786
1787 cache.insert(key(1), cached_test_block(1));
1788 cache.insert(key(2), cached_test_block(2));
1789 assert!(cache.get(key(1)).is_some());
1790 cache.insert(key(3), cached_test_block(3));
1791
1792 assert!(cache.get(key(1)).is_none());
1793 assert!(cache.get(key(2)).is_some());
1794 assert!(cache.get(key(3)).is_some());
1795 assert!(cache.total_bytes() <= block_bytes * 2);
1796 }
1797
1798 #[test]
1799 fn shared_store_cache_bypasses_oversized_entries() {
1800 let block = cached_test_block(1);
1801 let cache = SharedStoreCache::with_limits(1024, block.retained_bytes() - 1);
1802 let key = StoreCacheKey {
1803 namespace: StoreCacheNamespace {
1804 directory: 1,
1805 segment: 9,
1806 },
1807 first_doc_id: 0,
1808 };
1809 cache.insert(key, block);
1810 assert_eq!(cache.total_bytes(), 0);
1811 assert!(cache.get(key).is_none());
1812 }
1813
1814 #[test]
1815 fn shared_store_cache_purges_closed_segment_namespace() {
1816 let block = cached_test_block(1);
1817 let cache = SharedStoreCache::with_limits(1024, 1024);
1818 let key = StoreCacheKey {
1819 namespace: StoreCacheNamespace {
1820 directory: 1,
1821 segment: 11,
1822 },
1823 first_doc_id: 0,
1824 };
1825 cache.register(key.namespace);
1826 cache.insert(key, block);
1827 assert!(cache.total_bytes() > 0);
1828 cache.unregister(key.namespace);
1829 assert_eq!(cache.total_bytes(), 0);
1830 assert!(cache.get(key).is_none());
1831 }
1832
1833 #[test]
1834 fn shared_store_cache_isolates_equal_segment_ids_across_directories() {
1835 let cache = SharedStoreCache::with_limits(1024, 1024);
1836 let key = |directory| StoreCacheKey {
1837 namespace: StoreCacheNamespace {
1838 directory,
1839 segment: 42,
1840 },
1841 first_doc_id: 0,
1842 };
1843 let left = cache.insert(key(1), cached_test_block(1));
1844 let right = cache.insert(key(2), cached_test_block(2));
1845
1846 assert!(!Arc::ptr_eq(&left, &right));
1847 assert_eq!(cache.total_blocks(), 2);
1848 }
1849}