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}