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}