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:
qdrant-cloud-bot
2026-09-03 12:46:00 +02:00
committed by timvisee
parent 1c93470beb
commit cf2fd822ee
8 changed files with 83 additions and 80 deletions
+1 -1
View File
@@ -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}"))
+1 -1
View File
@@ -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.
///
+7 -7
View File
@@ -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 {
+9 -9
View File
@@ -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> {
+24 -24
View File
@@ -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);
}
+4 -4
View File
@@ -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
+9 -9
View File
@@ -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;
}