mirror of
https://github.com/qdrant/qdrant.git
synced 2026-09-21 21:47:39 -05:00
Rename ReadOnlyEdgeShard::refresh to live_reload (#10444)
Align the Edge follower API with the segment-level LiveReload naming used everywhere else, including the module, lock, and docs.
This commit is contained in:
@@ -368,7 +368,7 @@ fn resolve_initial_config(
|
||||
///
|
||||
/// Skips non-directories, hidden (`.`-prefixed) entries, and (via [`normalize_segment_dir`])
|
||||
/// `.deleted` leftovers and segments without a written `version.info`. Shared by [`EdgeShard`]
|
||||
/// loading and by the read-only follower's refresh, so both observe the same segment set.
|
||||
/// loading and by the read-only follower's live_reload, so both observe the same segment set.
|
||||
pub(crate) fn scan_segment_dirs(segments_path: &Path) -> OperationResult<HashMap<Uuid, PathBuf>> {
|
||||
let segments_dir = fs::read_dir(segments_path).map_err(|err| {
|
||||
OperationError::service_error(format!("failed to read segments directory: {err}"))
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::edge_shard::scan_segment_dirs;
|
||||
/// * [`LocalSegmentEnumerator`] scans the local `segments/` directory;
|
||||
/// * an S3 follower can supply its own (e.g. reading the manifest over object storage).
|
||||
///
|
||||
/// Called on every [`refresh`](super::ReadOnlyEdgeShard::refresh), so it must reflect the current
|
||||
/// Called on every [`live_reload`](super::ReadOnlyEdgeShard::live_reload), so it must reflect the current
|
||||
/// set. The returned paths are segment directory paths interpreted relative to the backend root
|
||||
/// (e.g. `segments/<uuid>`), matching what [`ReadOnlySegment::open`] expects.
|
||||
///
|
||||
|
||||
@@ -47,14 +47,14 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
/// derived from the segments themselves (see [`EdgeConfig::from_segment_config`]), mirroring the
|
||||
/// read-write [`EdgeShard`](crate::EdgeShard)'s fallback. A provided `config` overrides tunable
|
||||
/// parameters at open (its `Some` values win over the derived ones; `vectors`/`sparse_vectors`
|
||||
/// are ignored); a [`refresh`](Self::refresh) re-derives the config from the segments alone.
|
||||
/// are ignored); a [`live_reload`](Self::live_reload) re-derives the config from the segments alone.
|
||||
/// An empty shard (no segments yet) starts from the provided config (or a default one).
|
||||
///
|
||||
/// A `load_profile` — derived from the request this shard is being opened to serve (see
|
||||
/// [`LoadProfile`]) — parks the segment components that request won't touch cold instead of
|
||||
/// warming them per the persisted segment configs, cutting the cold-start cost. Without one,
|
||||
/// loading follows the segment configs alone. The profile also applies to segments a later
|
||||
/// [`refresh`](Self::refresh) discovers: the shard was opened for that one request, so new
|
||||
/// [`live_reload`](Self::live_reload) discovers: the shard was opened for that one request, so new
|
||||
/// segments shouldn't load any warmer.
|
||||
pub fn open(
|
||||
fs: S::Fs,
|
||||
@@ -74,7 +74,7 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
/// Internal seam: [`open`](Self::open) always discovers via the manifest, but tests inject other
|
||||
/// discovery strategies (e.g. a directory scan) here.
|
||||
///
|
||||
/// The initial load is a [`refresh`](Self::refresh) over an empty shard: same discovery, same
|
||||
/// The initial load is a [`live_reload`](Self::live_reload) over an empty shard: same discovery, same
|
||||
/// parallel load, same handling of segments that change while being loaded. The manifest is
|
||||
/// superset-biased, so segments that cannot be loaded (not yet finalized, already deleted, or
|
||||
/// appendable) are skipped rather than failing the open.
|
||||
@@ -106,13 +106,13 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
enumerator: Box::new(enumerator),
|
||||
search_pool,
|
||||
load_profile,
|
||||
refresh_lock: Default::default(),
|
||||
live_reload_lock: Default::default(),
|
||||
};
|
||||
shard.refresh()?;
|
||||
shard.live_reload()?;
|
||||
|
||||
// Open-only config overlay: caller-provided tunables win over the segment-derived ones
|
||||
// (`refresh` re-derives from the segments alone — see the `config` field docs). For an
|
||||
// empty shard the refresh left the provided config in place, and the overlay is a no-op.
|
||||
// (`live_reload` re-derives from the segments alone — see the `config` field docs). For an
|
||||
// empty shard the live_reload left the provided config in place, and the overlay is a no-op.
|
||||
let derived = shard.config.read().clone();
|
||||
*shard.config.write() = Arc::new(merge_follower_config(
|
||||
provided_config,
|
||||
|
||||
@@ -13,8 +13,8 @@ use crate::EdgeConfig;
|
||||
use crate::read_only::ReadOnlyEdgeShard;
|
||||
use crate::read_only::load::{load_segments_parallel, reload_segments_parallel};
|
||||
|
||||
/// How a single [`refresh_attempt`](ReadOnlyEdgeShard::refresh_attempt) ended.
|
||||
enum RefreshOutcome {
|
||||
/// How a single [`live_reload_attempt`](ReadOnlyEdgeShard::live_reload_attempt) ended.
|
||||
enum LiveReloadOutcome {
|
||||
/// The attempt fully converged on its manifest snapshot.
|
||||
Complete,
|
||||
/// Segments vanished benignly mid-attempt (the leader removed them, confirmed
|
||||
@@ -24,34 +24,34 @@ enum RefreshOutcome {
|
||||
}
|
||||
|
||||
impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
/// Refresh the follower to the leader's current on-disk state.
|
||||
/// Live-reload the follower to the leader's current on-disk state.
|
||||
///
|
||||
/// Caller-driven (never self-triggered), mirroring `ReadOnlySegment::live_reload`: the host
|
||||
/// owns the cadence (a timer, an explicit call after a known leader flush, or an FS watch).
|
||||
/// Eventually consistent — a point becomes visible once the leader has flushed it to the
|
||||
/// segment files and a subsequent `refresh` has run.
|
||||
/// segment files and a subsequent `live_reload` has run.
|
||||
///
|
||||
/// Leader-side segment churn is absorbed: a segment whose files vanish while its live-reload
|
||||
/// runs is checked against a re-read manifest, and if the leader indeed removed it, the segment
|
||||
/// is dropped and the refresh re-runs (bounded) to pick up its replacements. `Err` therefore
|
||||
/// is dropped and the live_reload re-runs (bounded) to pick up its replacements. `Err` therefore
|
||||
/// means the shard genuinely needs attention: a component failed to reload, or a segment's
|
||||
/// essential files are missing while the manifest still lists it. Either way the shard stays
|
||||
/// consistent — every swap is atomic, a failed segment keeps serving its pre-refresh state,
|
||||
/// and the next refresh replays its unapplied delta (see `pending_reload`).
|
||||
pub fn refresh(&self) -> OperationResult<()>
|
||||
/// consistent — every swap is atomic, a failed segment keeps serving its pre-live_reload state,
|
||||
/// and the next live_reload replays its unapplied delta (see `pending_reload`).
|
||||
pub fn live_reload(&self) -> OperationResult<()>
|
||||
where
|
||||
S::Fs: UniversalReadFsAsync + Send + Sync + Clone + 'static,
|
||||
{
|
||||
let hw_counter = HardwareCounterCell::disposable();
|
||||
self.refresh_with(&hw_counter)
|
||||
self.live_reload_with(&hw_counter)
|
||||
}
|
||||
|
||||
/// [`refresh`](Self::refresh) with a caller-supplied hardware counter.
|
||||
pub fn refresh_with(&self, hw_counter: &HardwareCounterCell) -> OperationResult<()>
|
||||
/// [`live_reload`](Self::live_reload) with a caller-supplied hardware counter.
|
||||
pub fn live_reload_with(&self, hw_counter: &HardwareCounterCell) -> OperationResult<()>
|
||||
where
|
||||
S::Fs: UniversalReadFsAsync + Send + Sync + Clone + 'static,
|
||||
{
|
||||
let _refresh_guard = self.refresh_lock.lock();
|
||||
let _live_reload_guard = self.live_reload_lock.lock();
|
||||
|
||||
// A benign mid-attempt segment removal re-runs the attempt against the fresh manifest;
|
||||
// bound the re-runs so a leader churning segments faster than the follower converges
|
||||
@@ -59,31 +59,34 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
const MAX_ATTEMPTS: usize = 3;
|
||||
|
||||
for _ in 0..MAX_ATTEMPTS {
|
||||
match self.refresh_attempt(hw_counter)? {
|
||||
RefreshOutcome::Complete => return Ok(()),
|
||||
RefreshOutcome::ManifestChanged => {}
|
||||
match self.live_reload_attempt(hw_counter)? {
|
||||
LiveReloadOutcome::Complete => return Ok(()),
|
||||
LiveReloadOutcome::ManifestChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts exhausted without converging. The shard is still consistent — every completed
|
||||
// swap was atomic — it just may not reflect the newest segments yet; the next refresh
|
||||
// swap was atomic — it just may not reflect the newest segments yet; the next live_reload
|
||||
// continues from here.
|
||||
log::warn!(
|
||||
"shard refresh did not converge after {MAX_ATTEMPTS} attempts \
|
||||
"shard live_reload did not converge after {MAX_ATTEMPTS} attempts \
|
||||
(leader keeps replacing segments); serving the state reached so far",
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// One refresh pass over a single manifest snapshot.
|
||||
/// One live_reload pass over a single manifest snapshot.
|
||||
///
|
||||
/// Completes as much as possible before reporting problems: newly-appeared segments are
|
||||
/// swapped in and every survivor is live-reloaded (they are independent) even when one of
|
||||
/// them fails. Not-found failures are then resolved against a re-read manifest — a segment
|
||||
/// the leader removed mid-attempt is dropped and reported as [`RefreshOutcome::ManifestChanged`]
|
||||
/// the leader removed mid-attempt is dropped and reported as [`LiveReloadOutcome::ManifestChanged`]
|
||||
/// so the caller re-runs against the fresh manifest; one whose files are missing while the
|
||||
/// manifest still lists it escalates. Any other reload failure escalates after the loop.
|
||||
fn refresh_attempt(&self, hw_counter: &HardwareCounterCell) -> OperationResult<RefreshOutcome>
|
||||
fn live_reload_attempt(
|
||||
&self,
|
||||
hw_counter: &HardwareCounterCell,
|
||||
) -> OperationResult<LiveReloadOutcome>
|
||||
where
|
||||
S::Fs: UniversalReadFsAsync + Send + Sync + Clone + 'static,
|
||||
{
|
||||
@@ -94,7 +97,7 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
// add them and drop removed ones, and collect the survivors to live_reload *after*
|
||||
// releasing the lock — so reads only block during the cheap add/drop, not during load
|
||||
// or reload. The manifest is superset-biased, so unloadable segments are skipped by
|
||||
// `load_segments_parallel` and simply retried on the next refresh.
|
||||
// `load_segments_parallel` and simply retried on the next live_reload.
|
||||
let new_segments: Vec<(Uuid, PathBuf)> = {
|
||||
let holder = self.segments.read();
|
||||
on_disk
|
||||
@@ -113,7 +116,7 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
let survivors: Vec<(Uuid, Arc<RwLock<ReadOnlySegment<S>>>)> = {
|
||||
let mut holder = self.segments.write();
|
||||
|
||||
// Segments present before this refresh that still exist on disk.
|
||||
// Segments present before this live_reload that still exist on disk.
|
||||
let survivor_uuids: Vec<Uuid> = holder
|
||||
.uuids()
|
||||
.into_iter()
|
||||
@@ -180,7 +183,7 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
// the leader removed the segment mid-attempt — drop it and re-run to pick up its
|
||||
// replacements; still listed means its essential files are genuinely missing — escalate.
|
||||
let outcome = if not_found.is_empty() {
|
||||
RefreshOutcome::Complete
|
||||
LiveReloadOutcome::Complete
|
||||
} else {
|
||||
let fresh = self.enumerator.list_segments()?;
|
||||
let mut gone: Vec<Uuid> = Vec::new();
|
||||
@@ -192,7 +195,7 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
);
|
||||
still_listed_error.get_or_insert(err);
|
||||
} else {
|
||||
log::debug!("segment {uuid} was removed by the leader during refresh");
|
||||
log::debug!("segment {uuid} was removed by the leader during live_reload");
|
||||
gone.push(uuid);
|
||||
}
|
||||
}
|
||||
@@ -209,7 +212,7 @@ impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
if let Some(err) = still_listed_error {
|
||||
return Err(err);
|
||||
}
|
||||
RefreshOutcome::ManifestChanged
|
||||
LiveReloadOutcome::ManifestChanged
|
||||
};
|
||||
|
||||
if let Some(err) = first_hard_error {
|
||||
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! A leader process owns a read-write `EdgeShard` and writes to an on-disk directory. One or more
|
||||
//! follower processes open the *same* directory as a [`ReadOnlyEdgeShard`] and serve reads. A
|
||||
//! follower never writes: no WAL, no optimization, no segment creation. It refreshes by
|
||||
//! [`refresh`](ReadOnlyEdgeShard::refresh)ing — rescanning the `segments/` directory to pick up
|
||||
//! follower never writes: no WAL, no optimization, no segment creation. It live-reloads by
|
||||
//! [`live_reload`](ReadOnlyEdgeShard::live_reload)ing — rescanning the `segments/` directory to pick up
|
||||
//! segments the leader created/removed, and [`live_reload`](segment::segment::read_only::ReadOnlySegment::live_reload)ing
|
||||
//! the survivors to fold in the leader's flushed in-place appends and deletes.
|
||||
//!
|
||||
@@ -15,8 +15,8 @@
|
||||
mod enumerate;
|
||||
mod holder;
|
||||
mod lifecycle;
|
||||
mod live_reload;
|
||||
mod load;
|
||||
mod refresh;
|
||||
mod shard_read;
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests;
|
||||
@@ -44,27 +44,27 @@ pub struct ReadOnlyEdgeShard<S: UniversalReadExt + 'static> {
|
||||
/// Read backend handle; passed to segment `open` and `live_reload`.
|
||||
fs: S::Fs,
|
||||
/// Config snapshot, derived from the segments (a follower has no `edge_config.json`). At open
|
||||
/// it is overlaid with the tunables of the caller-provided config; each refresh re-derives it
|
||||
/// it is overlaid with the tunables of the caller-provided config; each live_reload re-derives it
|
||||
/// from the segments alone. Stored as an `Arc` so a read view can cheaply clone the current
|
||||
/// snapshot while a refresh swaps in a new one.
|
||||
/// snapshot while a live_reload swaps in a new one.
|
||||
config: RwLock<Arc<EdgeConfig>>,
|
||||
segments: RwLock<ReadOnlySegmentHolder<S>>,
|
||||
/// Discovers the current segment directories. Injected because segment discovery is
|
||||
/// backend-specific (see [`SegmentEnumerator`]) until an on-disk manifest exists.
|
||||
enumerator: Box<dyn SegmentEnumerator>,
|
||||
/// Fixed-size pool used to open segments in parallel on open/refresh and to run per-segment
|
||||
/// Fixed-size pool used to open segments in parallel on open/live_reload and to run per-segment
|
||||
/// reads in parallel. Segments never carry `max_search_threads`, so it is sized from
|
||||
/// `provided_config` alone: the CPU-derived default unless explicitly set (see
|
||||
/// [`EdgeConfig::search_thread_count`]).
|
||||
search_pool: Arc<rayon::ThreadPool>,
|
||||
/// Request-specific load profile this shard was opened with, if any: components the request
|
||||
/// won't touch are parked cold instead of warmed per the segment configs. Kept so segments a
|
||||
/// later [`refresh`](Self::refresh) discovers load with the same placement.
|
||||
/// later [`live_reload`](Self::live_reload) discovers load with the same placement.
|
||||
load_profile: Option<LoadProfile>,
|
||||
/// Serializes [`refresh`](Self::refresh)es: concurrent refreshes would
|
||||
/// Serializes [`live_reload`](Self::live_reload)s: concurrent live_reloads would
|
||||
/// duplicate the listing and load work, and could clear each other's staged
|
||||
/// prefetches between a segment's preload and apply.
|
||||
refresh_lock: Mutex<()>,
|
||||
live_reload_lock: Mutex<()>,
|
||||
}
|
||||
|
||||
impl<S: UniversalReadExt + 'static> ReadOnlyEdgeShard<S> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Leader + follower tests: a read-write [`EdgeShard`] (leader) writes to a directory and a
|
||||
//! [`ReadOnlyEdgeShard`] (follower) opened over the same directory serves reads, converging on the
|
||||
//! leader's flushed state after a [`refresh`](ReadOnlyEdgeShard::refresh).
|
||||
//! leader's flushed state after a [`live_reload`](ReadOnlyEdgeShard::live_reload).
|
||||
#![expect(clippy::wildcard_enum_match_arm, reason = "test code")]
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
@@ -91,7 +91,7 @@ pub(crate) fn delete(shard: &EdgeShard, ids: impl IntoIterator<Item = u64>) {
|
||||
}
|
||||
|
||||
/// Open a follower that discovers segments by scanning the directory (no manifest required) — used
|
||||
/// by the read/refresh tests, whose leaders don't write a manifest.
|
||||
/// by the read/live_reload tests, whose leaders don't write a manifest.
|
||||
pub(crate) fn open_follower(path: &std::path::Path) -> ReadOnlyEdgeShard<MmapFile> {
|
||||
ReadOnlyEdgeShard::<MmapFile>::open_with_enumerator(
|
||||
MmapFs,
|
||||
@@ -171,7 +171,7 @@ fn follower_sees_flushed_data() {
|
||||
leader.flush().unwrap();
|
||||
|
||||
let follower = open_follower(dir.path());
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
|
||||
assert_eq!(exact_count(&follower), 100);
|
||||
assert_eq!(exact_count(&follower), leader_exact_count(&leader));
|
||||
@@ -213,15 +213,15 @@ fn follower_with_load_profile_serves_reads() {
|
||||
// Reads outside the profile (vectors are parked cold for a scroll) still work.
|
||||
assert_follower_vectors(&follower, &[1, 50, 100]);
|
||||
|
||||
// Segments discovered by a refresh load under the same profile.
|
||||
// Segments discovered by a live_reload load under the same profile.
|
||||
upsert(&leader, 101..=150);
|
||||
leader.flush().unwrap();
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
assert_eq!(exact_count(&follower), 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_picks_up_incremental_writes() {
|
||||
fn live_reload_picks_up_incremental_writes() {
|
||||
let dir = tempfile::Builder::new()
|
||||
.prefix("edge-ro-incremental")
|
||||
.tempdir()
|
||||
@@ -232,13 +232,13 @@ fn refresh_picks_up_incremental_writes() {
|
||||
leader.flush().unwrap();
|
||||
|
||||
let follower = open_follower(dir.path());
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
assert_eq!(exact_count(&follower), 50);
|
||||
|
||||
// Second batch: appended in place to the same (appendable) segment.
|
||||
upsert(&leader, 51..=100);
|
||||
leader.flush().unwrap();
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
|
||||
assert_eq!(exact_count(&follower), 100);
|
||||
assert_eq!(exact_count(&follower), leader_exact_count(&leader));
|
||||
@@ -257,12 +257,12 @@ fn follower_reflects_deletes() {
|
||||
leader.flush().unwrap();
|
||||
|
||||
let follower = open_follower(dir.path());
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
assert_eq!(exact_count(&follower), 100);
|
||||
|
||||
delete(&leader, 1..=40);
|
||||
leader.flush().unwrap();
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
|
||||
assert_eq!(exact_count(&follower), 60);
|
||||
assert_eq!(exact_count(&follower), leader_exact_count(&leader));
|
||||
@@ -294,14 +294,14 @@ fn follower_tracks_optimization_swap() {
|
||||
leader.flush().unwrap();
|
||||
|
||||
let follower = open_follower(dir.path());
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
assert_eq!(exact_count(&follower), 700);
|
||||
|
||||
// Vacuum rebuilds the segment under a new UUID and removes the old one.
|
||||
let optimized = leader.optimize().unwrap();
|
||||
assert!(optimized, "expected a vacuum optimization to run");
|
||||
leader.flush().unwrap();
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
|
||||
// Follower drops the old UUID, opens the new one, and serves the same data.
|
||||
assert_eq!(exact_count(&follower), 700);
|
||||
@@ -310,7 +310,7 @@ fn follower_tracks_optimization_swap() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_on_unchanged_dir_is_noop() {
|
||||
fn live_reload_on_unchanged_dir_is_noop() {
|
||||
let dir = tempfile::Builder::new()
|
||||
.prefix("edge-ro-noop")
|
||||
.tempdir()
|
||||
@@ -321,12 +321,12 @@ fn refresh_on_unchanged_dir_is_noop() {
|
||||
leader.flush().unwrap();
|
||||
|
||||
let follower = open_follower(dir.path());
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
let before = exact_count(&follower);
|
||||
|
||||
// Repeated refreshes without leader changes must be stable.
|
||||
follower.refresh().unwrap();
|
||||
follower.refresh().unwrap();
|
||||
// Repeated live_reloads without leader changes must be stable.
|
||||
follower.live_reload().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
assert_eq!(exact_count(&follower), before);
|
||||
assert_eq!(before, 10);
|
||||
}
|
||||
@@ -355,10 +355,10 @@ fn open_without_config_derives_from_segments() {
|
||||
}
|
||||
|
||||
/// A caller-provided config overrides tunables at open only: `vectors` still derive from the
|
||||
/// segments (so reads work with a vectors-less provided config), while [`refresh`] re-derives the
|
||||
/// segments (so reads work with a vectors-less provided config), while [`live_reload`] re-derives the
|
||||
/// config from the segments alone.
|
||||
///
|
||||
/// [`refresh`]: ReadOnlyEdgeShard::refresh
|
||||
/// [`live_reload`]: ReadOnlyEdgeShard::live_reload
|
||||
#[test]
|
||||
fn provided_config_overrides_tunables_at_open() {
|
||||
let dir = tempfile::Builder::new()
|
||||
@@ -400,11 +400,11 @@ fn provided_config_overrides_tunables_at_open() {
|
||||
assert_eq!(config.max_search_threads, Some(2));
|
||||
assert_eq!(exact_count(&follower), 10);
|
||||
|
||||
// Refresh re-derives the config from the segments alone: the provided tunables are dropped
|
||||
// live_reload re-derives the config from the segments alone: the provided tunables are dropped
|
||||
// (segments never carry `max_search_threads`), the segment-derived values remain.
|
||||
upsert(&leader, 11..=15);
|
||||
leader.flush().unwrap();
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
|
||||
let config = follower.config_snapshot();
|
||||
assert!(config.vectors.contains_key(VECTOR_NAME));
|
||||
@@ -475,8 +475,8 @@ fn follower_uses_injected_enumerator() {
|
||||
.unwrap();
|
||||
assert_eq!(follower.segments_count(), all_segments.len() - 1);
|
||||
|
||||
// A refresh still goes through the same enumerator, so the hidden segment stays hidden.
|
||||
follower.refresh().unwrap();
|
||||
// A live_reload still goes through the same enumerator, so the hidden segment stays hidden.
|
||||
follower.live_reload().unwrap();
|
||||
assert_eq!(follower.segments_count(), all_segments.len() - 1);
|
||||
}
|
||||
|
||||
@@ -551,6 +551,6 @@ fn leader_writes_manifest_and_follower_loads_it() {
|
||||
|
||||
// The follower discovers and serves the data through the manifest.
|
||||
let follower = ReadOnlyEdgeShard::<MmapFile>::open_mmap(dir.path()).unwrap();
|
||||
follower.refresh().unwrap();
|
||||
follower.live_reload().unwrap();
|
||||
assert_eq!(exact_count(&follower), 100);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ Results print as one JSON object per line (`id`, `payload`, `vector`, plus `scor
|
||||
|
||||
## Live reload — watching a leader's writes
|
||||
|
||||
Both flags keep the tool running after the first answer. Each iteration refreshes the shard from the backend, re-runs the **same** request, and prints a diff against the previous results:
|
||||
Both flags keep the tool running after the first answer. Each iteration live_reloads the shard from the backend, re-runs the **same** request, and prints a diff against the previous results:
|
||||
|
||||
```
|
||||
+ {...} point appeared
|
||||
@@ -111,10 +111,10 @@ Both flags keep the tool running after the first answer. Each iteration refreshe
|
||||
|
||||
A pure reordering of unchanged rows prints nothing (`no changes`). This is the fastest way to watch a leader writing into the same bucket, and to catch staleness or consistency bugs in the follower read path.
|
||||
|
||||
- `--live-reload <SECONDS>` — refresh on a timer (minimum 1).
|
||||
- `--live-reload-key` — refresh when you press Enter instead. Better when stepping through a debugger on the leader. Not compatible with `@-` (stdin) request arguments, since it reads stdin itself.
|
||||
- `--live-reload <SECONDS>` — live_reload on a timer (minimum 1).
|
||||
- `--live-reload-key` — live_reload when you press Enter instead. Better when stepping through a debugger on the leader. Not compatible with `@-` (stdin) request arguments, since it reads stdin itself.
|
||||
|
||||
The two are mutually exclusive. A failed refresh is logged and retried on the next trigger — the shard keeps serving its previous state rather than dying.
|
||||
The two are mutually exclusive. A failed live_reload is logged and retried on the next trigger — the shard keeps serving its previous state rather than dying.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
//! ```
|
||||
//!
|
||||
//! With `--live-reload <SECONDS>` the tool keeps running after the first
|
||||
//! answer: every interval it [`refresh`](ReadOnlyEdgeShard::refresh)es the
|
||||
//! answer: every interval it [`live_reload`](ReadOnlyEdgeShard::live_reload)s the
|
||||
//! shard from object storage, re-runs the same request, and prints the
|
||||
//! difference against the previous results (`+` appeared, `-` disappeared,
|
||||
//! `~` changed), so a leader writing to the same bucket can be observed live.
|
||||
@@ -141,7 +141,7 @@ struct Cli {
|
||||
connection: ConnectionArgs,
|
||||
|
||||
/// Live-reload polling interval in seconds. When set, the tool keeps
|
||||
/// running after the first answer: every interval it refreshes the shard
|
||||
/// running after the first answer: every interval it live_reloads the shard
|
||||
/// from object storage, re-runs the same request, and prints the
|
||||
/// difference against the previous results (`+` appeared, `-` disappeared,
|
||||
/// `~` changed). Must be given before the sub-command.
|
||||
@@ -161,9 +161,9 @@ struct Cli {
|
||||
|
||||
/// What triggers each live-reload iteration.
|
||||
enum ReloadTrigger {
|
||||
/// Refresh every interval (`--live-reload <SECONDS>`).
|
||||
/// Live-reload every interval (`--live-reload <SECONDS>`).
|
||||
Timer(Duration),
|
||||
/// Refresh when the user presses Enter (`--live-reload-key`).
|
||||
/// Live-reload when the user presses Enter (`--live-reload-key`).
|
||||
Key,
|
||||
}
|
||||
|
||||
@@ -966,7 +966,7 @@ where
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// Live-reload loop: on every trigger (timer tick or Enter), refresh the
|
||||
// Live-reload loop: on every trigger (timer tick or Enter), live_reload the
|
||||
// shard from the backend, re-run the same request, and print the diff
|
||||
// against the previous run. Runs until interrupted (or stdin closes, in
|
||||
// key mode).
|
||||
@@ -977,18 +977,18 @@ where
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Err(err) = shard.refresh() {
|
||||
if let Err(err) = shard.live_reload() {
|
||||
// The shard keeps serving its previous state; retry on the next trigger.
|
||||
log::error!("live-reload refresh failed (will retry on next reload): {err}");
|
||||
log::error!("live_reload failed (will retry on next reload): {err}");
|
||||
continue;
|
||||
}
|
||||
log::info!(
|
||||
"live-reload #{iteration}: refreshed shard, {} segment(s)",
|
||||
"live-reload #{iteration}: live_reloaded shard, {} segment(s)",
|
||||
shard.segments_count(),
|
||||
);
|
||||
|
||||
let (rows, _) = request.run(&shard)?;
|
||||
println!("--- refresh #{iteration}: diff vs previous results ---");
|
||||
println!("--- live_reload #{iteration}: diff vs previous results ---");
|
||||
print_diff(&previous, &rows)?;
|
||||
previous = rows;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user