Skip to main content

qdrant_edge/edge/read_only/
refresh.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::common::counter::hardware_counter::HardwareCounterCell;
5use crate::common::universal_io::IsNotFound as _;
6use parking_lot::RwLock;
7use crate::segment::common::operation_error::{OperationError, OperationResult};
8use crate::segment::index::UniversalReadExt;
9use crate::segment::segment::read_only::ReadOnlySegment;
10use uuid::Uuid;
11
12use crate::edge::EdgeConfig;
13use crate::edge::read_only::ReadOnlyEdgeShard;
14use crate::edge::read_only::load::load_segments_parallel;
15
16/// How a single [`refresh_attempt`](ReadOnlyEdgeShard::refresh_attempt) ended.
17enum RefreshOutcome {
18    /// The attempt fully converged on its manifest snapshot.
19    Complete,
20    /// Segments vanished benignly mid-attempt (the leader removed them, confirmed
21    /// against a re-read manifest); re-run against the fresh manifest to pick up
22    /// their replacements.
23    ManifestChanged,
24}
25
26impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
27    /// Refresh the follower to the leader's current on-disk state.
28    ///
29    /// Caller-driven (never self-triggered), mirroring `ReadOnlySegment::live_reload`: the host
30    /// owns the cadence (a timer, an explicit call after a known leader flush, or an FS watch).
31    /// Eventually consistent — a point becomes visible once the leader has flushed it to the
32    /// segment files and a subsequent `refresh` has run.
33    ///
34    /// Leader-side segment churn is absorbed: a segment whose files vanish while its live-reload
35    /// runs is checked against a re-read manifest, and if the leader indeed removed it, the segment
36    /// is dropped and the refresh re-runs (bounded) to pick up its replacements. `Err` therefore
37    /// means the shard genuinely needs attention: a component failed to reload, or a segment's
38    /// essential files are missing while the manifest still lists it. Either way the shard stays
39    /// consistent — every swap is atomic, a failed segment keeps serving its pre-refresh state,
40    /// and the next refresh replays its unapplied delta (see `pending_reload`).
41    pub fn refresh(&self) -> OperationResult<()>
42    where
43        S::Fs: Send + Sync + Clone + 'static,
44    {
45        let hw_counter = HardwareCounterCell::disposable();
46        self.refresh_with(&hw_counter)
47    }
48
49    /// [`refresh`](Self::refresh) with a caller-supplied hardware counter.
50    pub fn refresh_with(&self, hw_counter: &HardwareCounterCell) -> OperationResult<()>
51    where
52        S::Fs: Send + Sync + Clone + 'static,
53    {
54        // A benign mid-attempt segment removal re-runs the attempt against the fresh manifest;
55        // bound the re-runs so a leader churning segments faster than the follower converges
56        // cannot spin this loop forever.
57        const MAX_ATTEMPTS: usize = 3;
58
59        for _ in 0..MAX_ATTEMPTS {
60            match self.refresh_attempt(hw_counter)? {
61                RefreshOutcome::Complete => return Ok(()),
62                RefreshOutcome::ManifestChanged => {}
63            }
64        }
65
66        // Attempts exhausted without converging. The shard is still consistent — every completed
67        // swap was atomic — it just may not reflect the newest segments yet; the next refresh
68        // continues from here.
69        log::warn!(
70            "shard refresh did not converge after {MAX_ATTEMPTS} attempts \
71             (leader keeps replacing segments); serving the state reached so far",
72        );
73        Ok(())
74    }
75
76    /// One refresh pass over a single manifest snapshot.
77    ///
78    /// Completes as much as possible before reporting problems: newly-appeared segments are
79    /// swapped in and every survivor is live-reloaded (they are independent) even when one of
80    /// them fails. Not-found failures are then resolved against a re-read manifest — a segment
81    /// the leader removed mid-attempt is dropped and reported as [`RefreshOutcome::ManifestChanged`]
82    /// so the caller re-runs against the fresh manifest; one whose files are missing while the
83    /// manifest still lists it escalates. Any other reload failure escalates after the loop.
84    fn refresh_attempt(&self, hw_counter: &HardwareCounterCell) -> OperationResult<RefreshOutcome>
85    where
86        S::Fs: Send + Sync + Clone + 'static,
87    {
88        // 1. Snapshot the current on-disk segment set (backend-specific; see `SegmentEnumerator`).
89        let on_disk = self.enumerator.list_segments()?;
90
91        // 2. Load newly-appeared segments in parallel (outside the holder lock), then under the lock
92        //    add them and drop removed ones, and collect the survivors to live_reload *after*
93        //    releasing the lock — so reads only block during the cheap add/drop, not during load
94        //    or reload. The manifest is superset-biased, so unloadable segments are skipped by
95        //    `load_segments_parallel` and simply retried on the next refresh.
96        let new_segments: Vec<(Uuid, PathBuf)> = {
97            let holder = self.segments.read();
98            on_disk
99                .iter()
100                .filter(|(uuid, _)| !holder.contains(uuid))
101                .map(|(uuid, segment_path)| (*uuid, segment_path.clone()))
102                .collect()
103        };
104        let loaded = load_segments_parallel::<S>(
105            &self.search_pool,
106            &self.fs,
107            new_segments,
108            self.load_profile.as_ref(),
109        );
110
111        let survivors: Vec<(Uuid, Arc<RwLock<ReadOnlySegment<S>>>)> = {
112            let mut holder = self.segments.write();
113
114            // Segments present before this refresh that still exist on disk.
115            let survivor_uuids: Vec<Uuid> = holder
116                .uuids()
117                .into_iter()
118                .filter(|uuid| on_disk.contains_key(uuid))
119                .collect();
120
121            // Add before drop: when an optimization migrates points from old to new segments, both
122            // must be momentarily visible so migrated points never disappear.
123            for (uuid, segment) in loaded {
124                let appendable = segment.segment_config.is_appendable();
125                holder.insert(uuid, appendable, Arc::new(RwLock::new(segment)));
126            }
127
128            holder.remove_missing(&on_disk);
129
130            survivor_uuids
131                .into_iter()
132                .filter_map(|uuid| holder.segment_arc(&uuid).map(|segment| (uuid, segment)))
133                .collect()
134        };
135
136        // 3. Re-derive the config from the current segments — a read-only follower has no
137        //    edge_config.json, so the segments are the source of truth. Folded over all segments
138        //    in UUID order, so the derivation is deterministic and a segment carrying no
139        //    information about a parameter never masks one that does. No-op for an empty shard
140        //    (the previous snapshot stays in place until segments appear).
141        let derived = {
142            let holder = self.segments.read();
143            let mut uuids = holder.uuids();
144            uuids.sort_unstable();
145            uuids
146                .into_iter()
147                .filter_map(|uuid| holder.segment_arc(&uuid))
148                .fold(None, |acc, segment| {
149                    Some(EdgeConfig::fold_from_segment_config(
150                        acc,
151                        &segment.read().segment_config,
152                    ))
153                })
154        };
155        if let Some(derived) = derived {
156            *self.config.write() = Arc::new(derived);
157        }
158
159        // 4. Live-reload survivors to fold in the leader's flushed in-place appends and deletes.
160        //    Newly-added segments are already current, so they are skipped. Survivors are
161        //    independent, so a failure does not stop the others from reloading; a failed segment
162        //    keeps serving its pre-refresh state and its unapplied delta is retained in
163        //    `pending_reload`, so a later reload replays the union and nothing is lost.
164        let mut not_found: Vec<(Uuid, OperationError)> = Vec::new();
165        let mut first_hard_error: Option<OperationError> = None;
166        for (uuid, segment) in survivors {
167            match segment.write().live_reload(&self.fs, hw_counter) {
168                Ok(()) => {}
169                // An essential file is gone; whether that is benign (the leader removed the
170                // segment while we reloaded it) is decided against a re-read manifest below.
171                Err(err) if err.is_not_found() => not_found.push((uuid, err)),
172                Err(err) => {
173                    log::error!("live_reload of segment {uuid} failed: {err}");
174                    first_hard_error.get_or_insert(err);
175                }
176            }
177        }
178
179        // 5. Resolve not-found failures against a re-read manifest: gone from the manifest means
180        //    the leader removed the segment mid-attempt — drop it and re-run to pick up its
181        //    replacements; still listed means its essential files are genuinely missing — escalate.
182        let outcome = if not_found.is_empty() {
183            RefreshOutcome::Complete
184        } else {
185            let fresh = self.enumerator.list_segments()?;
186            let mut gone: Vec<Uuid> = Vec::new();
187            let mut still_listed_error: Option<OperationError> = None;
188            for (uuid, err) in not_found {
189                if fresh.contains_key(&uuid) {
190                    log::error!(
191                        "segment {uuid} is listed in the manifest but its files are missing: {err}",
192                    );
193                    still_listed_error.get_or_insert(err);
194                } else {
195                    log::debug!("segment {uuid} was removed by the leader during refresh");
196                    gone.push(uuid);
197                }
198            }
199
200            // Drop the removed segments even when escalating below: they are confirmed gone, so
201            // keeping them would only leave handles to vanished files.
202            if !gone.is_empty() {
203                let mut holder = self.segments.write();
204                for uuid in &gone {
205                    holder.remove(uuid);
206                }
207            }
208
209            if let Some(err) = still_listed_error {
210                return Err(err);
211            }
212            RefreshOutcome::ManifestChanged
213        };
214
215        if let Some(err) = first_hard_error {
216            return Err(err);
217        }
218        Ok(outcome)
219    }
220}