Skip to main content

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}