qdrant_edge/edge/read_only/mod.rs
1//! Read-only follower of [`EdgeShard`](crate::EdgeShard).
2//!
3//! A leader process owns a read-write `EdgeShard` and writes to an on-disk directory. One or more
4//! follower processes open the *same* directory as a [`ReadOnlyEdgeShard`] and serve reads. A
5//! follower never writes: no WAL, no optimization, no segment creation. It refreshes by
6//! [`refresh`](ReadOnlyEdgeShard::refresh)ing — rescanning the `segments/` directory to pick up
7//! segments the leader created/removed, and [`live_reload`](segment::segment::read_only::ReadOnlySegment::live_reload)ing
8//! the survivors to fold in the leader's flushed in-place appends and deletes.
9//!
10//! This mirrors the segment-level read-only design one level up: just as `ReadOnlySegment` is the
11//! read-only counterpart of `Segment`, `ReadOnlyEdgeShard` is the read-only counterpart of
12//! `EdgeShard`. Both expose a consistent [`EdgeReadView`](crate::EdgeReadView) and share its read
13//! logic.
14
15mod enumerate;
16mod holder;
17mod lifecycle;
18mod load;
19mod refresh;
20mod shard_read;
21#[cfg(test)]
22mod tests;
23
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26
27use parking_lot::RwLock;
28use crate::segment::data_types::load_profile::LoadProfile;
29use crate::segment::index::UniversalReadExt;
30
31use crate::edge::EdgeConfig;
32pub use crate::edge::read_only::enumerate::{
33 LocalSegmentEnumerator, ManifestSegmentEnumerator, SegmentEnumerator,
34};
35use crate::edge::read_only::holder::ReadOnlySegmentHolder;
36
37/// Read-only follower view over an edge shard's on-disk directory.
38///
39/// Generic over the read backend `S` (e.g. `MmapFile` for local memory-mapped files; the same
40/// abstraction `ReadOnlySegment` uses, so blob/S3 backends are possible). Use
41/// [`open_mmap`](ReadOnlyEdgeShard::open_mmap) for the common local case.
42pub struct ReadOnlyEdgeShard<S: UniversalReadExt + 'static> {
43 path: PathBuf,
44 /// Read backend handle; passed to segment `open` and `live_reload`.
45 fs: S::Fs,
46 /// Config snapshot, derived from the segments (a follower has no `edge_config.json`). At open
47 /// it is overlaid with the tunables of the caller-provided config; each refresh re-derives it
48 /// from the segments alone. Stored as an `Arc` so a read view can cheaply clone the current
49 /// snapshot while a refresh swaps in a new one.
50 config: RwLock<Arc<EdgeConfig>>,
51 segments: RwLock<ReadOnlySegmentHolder<S>>,
52 /// Discovers the current segment directories. Injected because segment discovery is
53 /// backend-specific (see [`SegmentEnumerator`]) until an on-disk manifest exists.
54 enumerator: Box<dyn SegmentEnumerator>,
55 /// Fixed-size pool used to open segments in parallel on open/refresh and to run per-segment
56 /// reads in parallel. Segments never carry `max_search_threads`, so it is sized from
57 /// `provided_config` alone: the CPU-derived default unless explicitly set (see
58 /// [`EdgeConfig::search_thread_count`]).
59 search_pool: Arc<rayon::ThreadPool>,
60 /// Request-specific load profile this shard was opened with, if any: components the request
61 /// won't touch are parked cold instead of warmed per the segment configs. Kept so segments a
62 /// later [`refresh`](Self::refresh) discovers load with the same placement.
63 load_profile: Option<LoadProfile>,
64}
65
66impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
67 pub fn path(&self) -> &Path {
68 &self.path
69 }
70
71 /// Number of segments currently open in the follower.
72 pub fn segments_count(&self) -> usize {
73 self.segments.read().uuids().len()
74 }
75}