Skip to main content

qdrant_edge/edge/edge_shard/
mod.rs

1mod optimize;
2mod shard_read;
3mod snapshots;
4mod update;
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::sync::atomic::AtomicBool;
10
11use crate::wal::WalOptions;
12use crate::common::save_on_disk::SaveOnDisk;
13use fs_err as fs;
14use parking_lot::Mutex;
15use crate::segment::common::operation_error::{OperationError, OperationResult};
16use crate::segment::entry::ReadSegmentEntry as _;
17use crate::segment::segment_constructor::{load_segment, normalize_segment_dir};
18use crate::shard::files::{PAYLOAD_INDEX_CONFIG_FILE, SEGMENTS_PATH, segment_manifest_path};
19use crate::shard::operations::CollectionUpdateOperations;
20use crate::shard::segment_holder::locked::LockedSegmentHolder;
21use crate::shard::segment_holder::{FlushMode, SegmentHolder};
22use crate::shard::segment_manifest::SegmentsManifest;
23use crate::shard::wal::SerdeWal;
24use uuid::Uuid;
25
26use crate::edge::config::optimizers::EdgeOptimizersConfig;
27use crate::edge::config::shard::{EDGE_CONFIG_FILE, EdgeConfig};
28use crate::edge::read_view::build_segment_pool;
29
30#[derive(Debug)]
31pub struct EdgeShard {
32    path: PathBuf,
33    /// Shared so long-lived closures (e.g. the optimizer's live vector-name provider) can read the
34    /// current config after `&self` borrows expire.
35    config: Arc<SaveOnDisk<EdgeConfig>>,
36    wal: Mutex<SerdeWal<CollectionUpdateOperations>>,
37    segments: LockedSegmentHolder,
38    /// Segment manifest (`segments/manifest.json`), kept in sync with the live segment set so a
39    /// read-only follower can discover segments without scanning. `Some` only when the
40    /// `write_segment_manifest` feature flag is enabled.
41    segment_manifest: Option<SaveOnDisk<SegmentsManifest>>,
42    /// Fixed-size pool used to run per-segment reads in parallel. Sized from
43    /// [`EdgeConfig::max_search_threads`].
44    search_pool: Arc<rayon::ThreadPool>,
45}
46
47const WAL_PATH: &str = "wal";
48impl EdgeShard {
49    /// Create a new edge shard at `path` with the given configuration.
50    ///
51    /// Fails if the shard already exists (i.e. the segments directory contains any segment).
52    /// Configuration is required and is persisted to `edge_config.json`. WAL
53    /// behavior follows `config.wal_options` (defaults to 32 MiB segments
54    /// when unset).
55    pub fn new(path: &Path, config: EdgeConfig) -> OperationResult<Self> {
56        if has_existing_segments(path) {
57            return Err(OperationError::service_error(
58                "cannot create edge shard: path already contains segment data",
59            ));
60        }
61
62        let wal_options = config.wal_options.clone().unwrap_or_default();
63        let (wal, segments_path) = ensure_dirs_and_open_wal(path, wal_options)?;
64        config.save(path)?;
65
66        let mut segments = SegmentHolder::default();
67        ensure_appendable_segment(&mut segments, path, &segments_path, &config)?;
68
69        let search_pool = build_segment_pool(
70            "edge-search",
71            config.search_thread_count(),
72            config.search_pool_core,
73        )?;
74
75        let config_path = path.join(EDGE_CONFIG_FILE);
76        let config = Arc::new(
77            SaveOnDisk::new(&config_path, config)
78                .map_err(|e| OperationError::service_error(e.to_string()))?,
79        );
80
81        let segment_manifest = init_segment_manifest(path, &segments)?;
82
83        Ok(Self {
84            path: path.into(),
85            config,
86            wal: parking_lot::Mutex::new(wal),
87            segments: LockedSegmentHolder::new(segments),
88            segment_manifest,
89            search_pool,
90        })
91    }
92
93    /// Load an edge shard from existing files at `path`.
94    ///
95    /// Every tunable parameter resolves through the fallback chain
96    /// **provided → persisted (`edge_config.json`) → derived from segments → default**, so a
97    /// parameter left unspecified (`None`) keeps whatever the shard already has, while an
98    /// explicitly provided value overwrites it and existing segments converge to it through the
99    /// optimizers. The resolved config is persisted to `edge_config.json`.
100    ///
101    /// `vectors` and `sparse_vectors` define the stored data and cannot be changed here: if
102    /// provided (non-empty), they are validated for compatibility against the loaded segments;
103    /// if not, they are taken from the persisted config or the segments themselves.
104    ///
105    /// Fails if no segments exist and no config can be loaded or inferred.
106    ///
107    /// To override WAL options (e.g. for embedded/mobile deployments where
108    /// the default 32 MiB segment capacity is too large), set
109    /// [`EdgeConfig::wal_options`] on the supplied config.
110    pub fn load(path: &Path, config: Option<EdgeConfig>) -> OperationResult<Self> {
111        let resolved = resolve_initial_config(path, config)?;
112
113        let wal_options = resolved
114            .as_ref()
115            .and_then(|c| c.wal_options.clone())
116            .unwrap_or_default();
117        let (wal, segments_path) = ensure_dirs_and_open_wal(path, wal_options)?;
118
119        let (mut segments, derived) = load_segments(&segments_path)?;
120
121        let config = match (resolved, derived) {
122            (Some(resolved), Some(derived)) => {
123                let merged = resolved.fill_unspecified_from(&derived);
124                // The tunables converge to the merged config via the optimizers, but the vector
125                // definitions must actually match the stored data.
126                merged
127                    .check_compatible_with_segment_config(&derived.plain_segment_config())
128                    .map_err(|err| {
129                        OperationError::service_error(format!(
130                            "config is incompatible with existing segments: {err}"
131                        ))
132                    })?;
133                merged
134            }
135            (Some(resolved), None) => resolved,
136            (None, Some(derived)) => derived,
137            (None, None) => {
138                return Err(OperationError::service_error(
139                    "edge config is not provided and no segments were loaded",
140                ));
141            }
142        };
143
144        ensure_appendable_segment(&mut segments, path, &segments_path, &config)?;
145
146        let search_pool = build_segment_pool(
147            "edge-search",
148            config.search_thread_count(),
149            config.search_pool_core,
150        )?;
151
152        let config_path = path.join(EDGE_CONFIG_FILE);
153        let config = Arc::new(
154            SaveOnDisk::new(&config_path, config)
155                .map_err(|e| OperationError::service_error(e.to_string()))?,
156        );
157
158        let segment_manifest = init_segment_manifest(path, &segments)?;
159
160        Ok(Self {
161            path: path.into(),
162            config,
163            wal: parking_lot::Mutex::new(wal),
164            segments: LockedSegmentHolder::new(segments),
165            segment_manifest,
166            search_pool,
167        })
168    }
169
170    /// Rebuild and persist the segment manifest from the current live segment set, when enabled.
171    /// Cheap and idempotent: only writes when the set differs from what's persisted. Preserves an
172    /// out-of-process optimizer's marks for still-live segments.
173    pub(crate) fn update_segment_manifest(&self) -> OperationResult<()> {
174        let Some(manifest) = &self.segment_manifest else {
175            return Ok(());
176        };
177
178        let rebuilt = {
179            let holder = self.segments.read();
180            SegmentsManifest::from_segment_holder(&holder)
181        };
182        // Merge under the write lock (see `SegmentsManifest::sync`).
183        manifest
184            .write_optional(|previous| {
185                let current = rebuilt.preserving(previous);
186                (*previous != current).then_some(current)
187            })
188            .map_err(|err| OperationError::service_error(err.to_string()))?;
189        Ok(())
190    }
191
192    pub fn config(&self) -> parking_lot::RwLockReadGuard<'_, EdgeConfig> {
193        self.config.read()
194    }
195
196    pub fn path(&self) -> &Path {
197        &self.path
198    }
199
200    /// Update global HNSW config and persist. Does not change per-vector HNSW.
201    pub fn set_hnsw_config(&self, hnsw_config: crate::segment::types::HnswConfig) -> OperationResult<()> {
202        self.config
203            .write(|cfg| cfg.set_hnsw_config(hnsw_config))
204            .map_err(|e| OperationError::service_error(e.to_string()))
205    }
206
207    /// Update HNSW config for a named vector and persist.
208    /// Fails if the vector does not exist. Immutable fields (e.g. size, distance) cannot be changed.
209    pub fn set_vector_hnsw_config(
210        &self,
211        vector_name: &str,
212        hnsw_config: crate::segment::types::HnswConfig,
213    ) -> OperationResult<()> {
214        // Run the fallible mutation on a clone *inside* the config lock via
215        // write_optional, rather than read()-clone-mutate-then-write(). The
216        // latter releases the read lock before writing, so a concurrent config
217        // update between the two would be silently overwritten (a lost-update
218        // TOCTOU). Returning None on failure aborts the persist+swap.
219        let mut mutation = Ok(());
220        self.config
221            .write_optional(|cfg| {
222                let mut updated = cfg.clone();
223                match updated.set_vector_hnsw_config(vector_name, hnsw_config) {
224                    Ok(()) => Some(updated),
225                    Err(e) => {
226                        mutation = Err(e);
227                        None
228                    }
229                }
230            })
231            .map_err(|e| OperationError::service_error(e.to_string()))
232            .and(mutation)
233    }
234
235    /// Update optimizer config and persist.
236    pub fn set_optimizers_config(&self, optimizers: EdgeOptimizersConfig) -> OperationResult<()> {
237        self.config
238            .write(|cfg| cfg.set_optimizers_config(optimizers))
239            .map_err(|e| OperationError::service_error(e.to_string()))
240    }
241
242    /// Persist the WAL and all segments to disk.
243    ///
244    /// Blocks until the WAL and segment locks are free, so a flush issued
245    /// concurrently with an in-flight `update`/`optimize` waits for it and then
246    /// persists, rather than spuriously failing with a "lock busy" error — the
247    /// same blocking-lock semantics those operations already use. Still fallible:
248    /// a genuine WAL/segment flush I/O error is surfaced instead of panicking.
249    ///
250    /// Must not be called while already holding the `wal` mutex or a `segments`
251    /// guard on the same thread — parking_lot locks are non-reentrant and would
252    /// self-deadlock. No current caller does (the FFI boundary, `Drop`, the
253    /// Python bindings, and tests all invoke it without holding those locks).
254    pub fn flush(&self) -> OperationResult<()> {
255        self.wal
256            .lock()
257            .flush()
258            .map_err(|e| OperationError::service_error(format!("WAL flush failed: {e}")))?;
259
260        self.segments.read().flush_all(FlushMode::Sync, true)?;
261
262        Ok(())
263    }
264}
265
266impl Drop for EdgeShard {
267    fn drop(&mut self) {
268        if let Err(e) = self.flush() {
269            log::error!("EdgeShard flush during drop failed: {e}");
270        }
271    }
272}
273
274/// Initialize the segment manifest from the current segments, when the `write_segment_manifest`
275/// feature flag is enabled. Returns `None` (and writes nothing) when disabled.
276fn init_segment_manifest(
277    path: &Path,
278    segments: &SegmentHolder,
279) -> OperationResult<Option<SaveOnDisk<SegmentsManifest>>> {
280    if !crate::common::flags::feature_flags().write_segment_manifest {
281        return Ok(None);
282    }
283
284    // `SaveOnDisk::new` persists immediately — read the existing manifest first so an
285    // optimizer's marks survive a (re)load. An unreadable manifest is replaced, as before.
286    let manifest_path = segment_manifest_path(path);
287    let rebuilt = SegmentsManifest::from_segment_holder(segments);
288    let manifest = match fs::read(&manifest_path)
289        .ok()
290        .and_then(|bytes| serde_json::from_slice::<SegmentsManifest>(&bytes).ok())
291    {
292        Some(previous) => rebuilt.preserving(&previous),
293        None => rebuilt,
294    };
295    let manifest = SaveOnDisk::new(manifest_path, manifest)
296        .map_err(|err| OperationError::service_error(err.to_string()))?;
297    Ok(Some(manifest))
298}
299
300fn has_existing_segments(path: &Path) -> bool {
301    let segments_path = path.join(SEGMENTS_PATH);
302    let Ok(entries) = fs::read_dir(&segments_path) else {
303        return false;
304    };
305    for entry in entries.flatten() {
306        let p = entry.path();
307        if !p.is_dir() {
308            continue;
309        }
310        if p.file_name()
311            .and_then(|n| n.to_str())
312            .is_some_and(|n| n.starts_with('.'))
313        {
314            continue;
315        }
316        if normalize_segment_dir(&p).ok().flatten().is_some() {
317            return true;
318        }
319    }
320    false
321}
322
323fn ensure_dirs_and_open_wal(
324    path: &Path,
325    wal_options: WalOptions,
326) -> OperationResult<(SerdeWal<CollectionUpdateOperations>, PathBuf)> {
327    let wal_path = path.join(WAL_PATH);
328    if !wal_path.exists() {
329        fs::create_dir(&wal_path).map_err(|err| {
330            OperationError::service_error(format!("failed to create WAL directory: {err}"))
331        })?;
332    }
333
334    let wal = SerdeWal::new(&wal_path, wal_options).map_err(|err| {
335        OperationError::service_error(format!("failed to open WAL {}: {err}", wal_path.display(),))
336    })?;
337
338    let segments_path = path.join(SEGMENTS_PATH);
339    if !segments_path.exists() {
340        fs::create_dir(&segments_path).map_err(|err| {
341            OperationError::service_error(format!("failed to create segments directory: {err}"))
342        })?;
343    }
344
345    Ok((wal, segments_path))
346}
347
348/// The provided → persisted layers of the config fallback chain (the derived-from-segments layer
349/// is applied by [`EdgeShard::load`] once the segments are loaded).
350fn resolve_initial_config(
351    path: &Path,
352    config: Option<EdgeConfig>,
353) -> OperationResult<Option<EdgeConfig>> {
354    let persisted = match EdgeConfig::load(path) {
355        Some(Ok(c)) => Some(c),
356        Some(Err(e)) => return Err(e),
357        None => None,
358    };
359    Ok(match (config, persisted) {
360        // Provided config wins, but parameters it leaves unspecified keep their persisted values
361        (Some(provided), Some(persisted)) => Some(provided.fill_unspecified_from(&persisted)),
362        (Some(provided), None) => Some(provided),
363        (None, persisted) => persisted,
364    })
365}
366
367/// Scan a `segments/` directory and return the valid, complete segment directories keyed by UUID.
368///
369/// Skips non-directories, hidden (`.`-prefixed) entries, and (via [`normalize_segment_dir`])
370/// `.deleted` leftovers and segments without a written `version.info`. Shared by [`EdgeShard`]
371/// loading and by the read-only follower's refresh, so both observe the same segment set.
372pub(crate) fn scan_segment_dirs(segments_path: &Path) -> OperationResult<HashMap<Uuid, PathBuf>> {
373    let segments_dir = fs::read_dir(segments_path).map_err(|err| {
374        OperationError::service_error(format!("failed to read segments directory: {err}"))
375    })?;
376
377    let mut result = HashMap::new();
378
379    for entry in segments_dir {
380        let entry = entry.map_err(|err| {
381            OperationError::service_error(format!(
382                "failed to read entry in segments directory: {err}",
383            ))
384        })?;
385
386        let segment_path = entry.path();
387
388        if !segment_path.is_dir() {
389            log::warn!(
390                "Skipping non-directory segment entry {}",
391                segment_path.display(),
392            );
393            continue;
394        }
395
396        if segment_path
397            .file_name()
398            .and_then(|n| n.to_str())
399            .is_some_and(|n| n.starts_with('.'))
400        {
401            log::warn!(
402                "Skipping hidden segment directory {}",
403                segment_path.display(),
404            );
405            continue;
406        }
407
408        let Some((segment_path, segment_uuid)) = normalize_segment_dir(&segment_path)? else {
409            continue;
410        };
411
412        result.insert(segment_uuid, segment_path);
413    }
414
415    Ok(result)
416}
417
418/// Load all segments and fold their configs into a single derived [`EdgeConfig`] — the
419/// derived-from-segments layer of the config fallback chain. Segments are folded in UUID order so
420/// the derivation is deterministic, and each segment is checked for compatibility against the
421/// previously loaded ones.
422fn load_segments(segments_path: &Path) -> OperationResult<(SegmentHolder, Option<EdgeConfig>)> {
423    let mut segments = SegmentHolder::default();
424    let mut derived: Option<EdgeConfig> = None;
425
426    let mut segment_dirs: Vec<_> = scan_segment_dirs(segments_path)?.into_iter().collect();
427    segment_dirs.sort_unstable_by_key(|(segment_uuid, _)| *segment_uuid);
428
429    for (segment_uuid, segment_path) in segment_dirs {
430        let mut segment = load_segment(&segment_path, segment_uuid, None, &AtomicBool::new(false))
431            .map_err(|err| {
432                OperationError::service_error(format!(
433                    "failed to load segment {}: {err}",
434                    segment_path.display(),
435                ))
436            })?;
437
438        let segment_cfg = segment.config();
439        if let Some(acc) = derived.as_ref() {
440            acc.check_compatible_with_segment_config(segment_cfg)
441                .map_err(|err| {
442                    OperationError::service_error(format!(
443                        "segment {} is incompatible with previously loaded segments: {err}",
444                        segment_path.display(),
445                    ))
446                })?;
447        }
448        derived = Some(EdgeConfig::fold_from_segment_config(derived, segment_cfg));
449
450        segment.check_consistency_and_repair().map_err(|err| {
451            OperationError::service_error(format!(
452                "failed to repair segment {}: {err}",
453                segment_path.display(),
454            ))
455        })?;
456
457        segments.add_new(segment);
458    }
459
460    Ok((segments, derived))
461}
462
463fn ensure_appendable_segment(
464    segments: &mut SegmentHolder,
465    path: &Path,
466    segments_path: &Path,
467    config: &EdgeConfig,
468) -> OperationResult<()> {
469    if segments.has_appendable_segment() {
470        return Ok(());
471    }
472
473    let payload_index_schema_path = path.join(PAYLOAD_INDEX_CONFIG_FILE);
474    let payload_index_schema = SaveOnDisk::load_or_init_default(&payload_index_schema_path)
475        .map_err(|err| {
476            OperationError::service_error(format!(
477                "failed to initialize payload index schema file {}: {err}",
478                payload_index_schema_path.display(),
479            ))
480        })?;
481
482    segments.create_appendable_segment(
483        segments_path,
484        config.plain_segment_config(),
485        Arc::new(payload_index_schema),
486        None,
487    )?;
488
489    debug_assert!(segments.has_appendable_segment());
490    Ok(())
491}