Skip to main content

qdrant_edge/edge/read_only/
lifecycle.rs

1use std::path::Path;
2use std::sync::Arc;
3
4use crate::common::universal_io::{MmapFile, MmapFs};
5use parking_lot::RwLock;
6use crate::segment::common::operation_error::OperationResult;
7use crate::segment::data_types::load_profile::LoadProfile;
8use crate::segment::index::UniversalReadExt;
9
10use crate::edge::EdgeConfig;
11use crate::edge::read_only::ReadOnlyEdgeShard;
12use crate::edge::read_only::enumerate::{ManifestSegmentEnumerator, SegmentEnumerator};
13use crate::edge::read_only::holder::ReadOnlySegmentHolder;
14
15impl ReadOnlyEdgeShard<MmapFile> {
16    /// Open a read-only follower over local memory-mapped files, discovering segments from the
17    /// leader's segment manifest. Requires the leader to write a manifest (the
18    /// `write_segment_manifest` feature flag).
19    pub fn open_mmap(path: &Path) -> OperationResult<Self> {
20        Self::open(MmapFs, path, None, None)
21    }
22}
23
24/// Effective follower config: tunables prefer the caller-`provided` config and fall back to the
25/// segment-`derived` one (via [`EdgeConfig::fill_unspecified_from`]), while `vectors` and
26/// `sparse_vectors` always come from `derived` — the segments are the follower's source of truth
27/// for the stored data.
28pub(super) fn merge_follower_config(provided: EdgeConfig, mut derived: EdgeConfig) -> EdgeConfig {
29    // Take the vector params out of `derived` up front, so caller-provided ones cannot win in
30    // the fill below (a non-empty map would count as "specified").
31    let vectors = std::mem::take(&mut derived.vectors);
32    let sparse_vectors = std::mem::take(&mut derived.sparse_vectors);
33
34    let mut config = provided.fill_unspecified_from(&derived);
35    config.vectors = vectors;
36    config.sparse_vectors = sparse_vectors;
37    config
38}
39
40impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
41    /// Open a read-only follower over the edge-shard directory at `path` using read backend `fs`.
42    ///
43    /// Segments are discovered through the leader's segment manifest (read via `fs`) — a read-only
44    /// follower always uses the manifest, so there is no discovery to configure.
45    ///
46    /// A follower has no `edge_config.json` — the segments are the source of truth, so the config is
47    /// derived from the segments themselves (see [`EdgeConfig::from_segment_config`]), mirroring the
48    /// read-write [`EdgeShard`](crate::EdgeShard)'s fallback. A provided `config` overrides tunable
49    /// parameters at open (its `Some` values win over the derived ones; `vectors`/`sparse_vectors`
50    /// are ignored); a [`refresh`](Self::refresh) re-derives the config from the segments alone.
51    /// An empty shard (no segments yet) starts from the provided config (or a default one).
52    ///
53    /// A `load_profile` — derived from the request this shard is being opened to serve (see
54    /// [`LoadProfile`]) — parks the segment components that request won't touch cold instead of
55    /// warming them per the persisted segment configs, cutting the cold-start cost. Without one,
56    /// loading follows the segment configs alone. The profile also applies to segments a later
57    /// [`refresh`](Self::refresh) discovers: the shard was opened for that one request, so new
58    /// segments shouldn't load any warmer.
59    pub fn open(
60        fs: S::Fs,
61        path: &Path,
62        config: Option<EdgeConfig>,
63        load_profile: Option<LoadProfile>,
64    ) -> OperationResult<Self>
65    where
66        S::Fs: Send + Sync + Clone + 'static,
67    {
68        let enumerator = ManifestSegmentEnumerator::new(fs.clone(), path);
69        Self::open_with_enumerator(fs, path, enumerator, config, load_profile)
70    }
71
72    /// Open with an explicit segment [`enumerator`](SegmentEnumerator).
73    ///
74    /// Internal seam: [`open`](Self::open) always discovers via the manifest, but tests inject other
75    /// discovery strategies (e.g. a directory scan) here.
76    ///
77    /// The initial load is a [`refresh`](Self::refresh) over an empty shard: same discovery, same
78    /// parallel load, same handling of segments that change while being loaded. The manifest is
79    /// superset-biased, so segments that cannot be loaded (not yet finalized, already deleted, or
80    /// appendable) are skipped rather than failing the open.
81    pub(crate) fn open_with_enumerator(
82        fs: S::Fs,
83        path: &Path,
84        enumerator: impl SegmentEnumerator + 'static,
85        config: Option<EdgeConfig>,
86        load_profile: Option<LoadProfile>,
87    ) -> OperationResult<Self>
88    where
89        S::Fs: Send + Sync + Clone + 'static,
90    {
91        let provided_config = config.unwrap_or_default();
92
93        // Segments never carry `max_search_threads` / `search_pool_core`, so the pool is sized and
94        // pinned from the caller-provided config alone: the CPU-derived default unless set.
95        let search_pool = crate::edge::read_view::build_segment_pool(
96            "edge-search",
97            provided_config.search_thread_count(),
98            provided_config.search_pool_core,
99        )?;
100
101        let shard = Self {
102            path: path.to_path_buf(),
103            fs,
104            config: RwLock::new(Arc::new(provided_config.clone())),
105            segments: RwLock::new(ReadOnlySegmentHolder::default()),
106            enumerator: Box::new(enumerator),
107            search_pool,
108            load_profile,
109        };
110        shard.refresh()?;
111
112        // Open-only config overlay: caller-provided tunables win over the segment-derived ones
113        // (`refresh` re-derives from the segments alone — see the `config` field docs). For an
114        // empty shard the refresh left the provided config in place, and the overlay is a no-op.
115        let derived = shard.config.read().clone();
116        *shard.config.write() = Arc::new(merge_follower_config(
117            provided_config,
118            EdgeConfig::clone(&derived),
119        ));
120
121        Ok(shard)
122    }
123}