use std::io;
#[cfg(feature = "persist")]
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
#[cfg(feature = "persist")]
use std::time::Instant;
use kevy_hash::KevyHash;
#[cfg(feature = "persist")]
use kevy_persist::reshard::{ShardLayout, commit_reshard, merge_sources, recover_journal};
#[cfg(feature = "persist")]
use kevy_persist::{Aof, Routing, ShardsMeta, layout, layout::infer_files_n};
use kevy_store::Store as Keyspace;
use crate::config::Config;
#[cfg(feature = "persist")]
use crate::metric::KevyMetric;
use crate::metric::OpenReport;
#[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
pub(crate) use crate::shard_tier::{resolve_tier_budget, tier_tick_upkeep};
use crate::store::Inner;
#[inline]
pub(crate) fn shard_idx(key: &[u8], n: usize) -> usize {
if n == 1 {
return 0;
}
let h = key.kevy_hash() as usize;
if n.is_power_of_two() { h & (n - 1) } else { h % n }
}
#[cfg(feature = "persist")]
struct EmbLayout;
#[cfg(feature = "persist")]
impl ShardLayout for EmbLayout {
fn snapshot_path(&self, dir: &Path, i: usize, _n: usize) -> PathBuf {
layout::snapshot_path(dir, i)
}
fn aof_path(&self, dir: &Path, i: usize, _n: usize) -> PathBuf {
layout::aof_path(dir, i)
}
}
fn fresh_keyspace(config: &Config) -> Keyspace {
let mut s = Keyspace::new();
s.set_max_memory(config.maxmemory, config.eviction_policy);
s
}
pub(crate) type Built = (Vec<Arc<RwLock<Inner>>>, OpenReport, Option<kevy_resp::Argv>);
pub(crate) fn build_shards(config: &Config) -> io::Result<Built> {
let n = config.shards.max(1);
#[allow(unused_mut)] let mut stores: Vec<Keyspace> = (0..n).map(|_| fresh_keyspace(config)).collect();
#[cfg(not(feature = "persist"))]
return Ok((into_inners_mem(stores), OpenReport::default(), None));
#[cfg(feature = "persist")]
build_shards_persist(config, n, stores)
}
#[cfg(feature = "persist")]
fn build_shards_persist(config: &Config, n: usize, mut stores: Vec<Keyspace>) -> io::Result<Built> {
let Some(dir) = crate::store_wire::disk_dir(config).cloned() else {
#[cfg(feature = "tier")]
if config.tier_budget.is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"tiering requires a disk data dir (with_persist); a memory-only store has no cold tier",
));
}
let inners = into_inners(stores, (0..n).map(|_| None).collect());
return Ok((inners, OpenReport::default(), None));
};
std::fs::create_dir_all(&dir)?;
recover_journal(&dir, &EmbLayout)?;
enable_tiering(config, &dir, &mut stores)?;
let mut catalog = None;
let (mut report, walked) = load_or_reshard(&dir, config, n, &mut stores, &mut catalog)?;
let mut aofs = open_live_aofs(config, &dir, &walked, &mut report)?;
prepare_aofs(config, &dir, &mut stores, &mut aofs, &mut report, &mut catalog)?;
Ok((into_inners(stores, aofs), report, catalog))
}
#[cfg(feature = "persist")]
fn prepare_aofs(
config: &Config,
dir: &Path,
stores: &mut [Keyspace],
aofs: &mut [Option<Aof>],
report: &mut OpenReport,
catalog: &mut Option<kevy_resp::Argv>,
) -> io::Result<()> {
#[cfg(target_arch = "wasm32")]
let _ = (dir, &report, &catalog);
#[cfg(not(target_arch = "wasm32"))]
for (i, (aof, store)) in aofs.iter_mut().zip(stores.iter_mut()).enumerate() {
if let Some(aof) = aof {
crate::shard_restore::open_stage(dir, config, i, store, aof, report, catalog)?;
}
store.sweep_orphan_row_segs();
}
let policy = config.rewrite_policy();
for (aof, store) in aofs.iter_mut().zip(stores.iter()) {
if let Some(aof) = aof
&& policy.baseline_matters(aof.size_bytes())
{
aof.anchor_rewrite_baseline(kevy_persist::estimate_rewrite_size(store));
}
}
Ok(())
}
#[cfg(feature = "persist")]
fn open_live_aofs(
config: &Config,
dir: &Path,
walked: &[Option<u64>],
report: &mut OpenReport,
) -> io::Result<Vec<Option<Aof>>> {
let aofs: Vec<Option<Aof>> = if config.aof {
walked
.iter()
.enumerate()
.map(|(i, &whole)| {
Aof::open_after_replay(
&layout::aof_path(dir, i),
config.appendfsync,
config.replay_mode,
whole,
)
.map(Some)
})
.collect::<io::Result<_>>()?
} else {
walked.iter().map(|_| None).collect()
};
for aof in aofs.iter().flatten() {
if let Some(q) = aof.open_quarantine() {
report.quarantine_paths.push(q.to_path_buf());
}
}
Ok(aofs)
}
#[cfg(feature = "persist")]
fn enable_tiering(config: &Config, dir: &Path, stores: &mut [Keyspace]) -> io::Result<()> {
#[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
if config.tier_budget.is_some() {
let per_shard = resolve_tier_budget(config, stores.len())?;
for (i, store) in stores.iter_mut().enumerate() {
store.enable_tiering(&dir.join("tier").join(i.to_string()), per_shard)?;
store.set_tier_max_spill(config.max_spill_value);
}
}
#[cfg(not(all(feature = "tier", not(target_arch = "wasm32"))))]
let _ = (config, dir, stores);
Ok(())
}
#[cfg(feature = "persist")]
fn load_or_reshard(
dir: &Path,
config: &Config,
n: usize,
stores: &mut [Keyspace],
catalog: &mut Option<kevy_resp::Argv>,
) -> io::Result<(OpenReport, Vec<Option<u64>>)> {
let meta_path = layout::shards_meta_path(dir);
let prev = ShardsMeta::read(&meta_path);
let same_layout = match prev {
Some(m) => m.n == n && m.routing == Routing::KevyHash,
None => n == 1 && infer_files_n(dir) <= 1,
};
if same_layout {
let loaded = load_in_place(dir, config, stores, catalog)?;
ShardsMeta::new(n, Routing::KevyHash).write(&meta_path)?;
return Ok(loaded);
}
{
let src_n = prev.map(|m| m.n).or_else(|| {
let k = infer_files_n(dir);
(k > 1).then_some(k)
});
Ok((reshard(dir, config, n, src_n, stores, catalog)?, vec![None; n]))
}
}
#[cfg(feature = "persist")]
fn load_in_place(
dir: &Path,
config: &Config,
stores: &mut [Keyspace],
catalog: &mut Option<kevy_resp::Argv>,
) -> io::Result<(OpenReport, Vec<Option<u64>>)> {
let mut report = OpenReport::default();
let start = Instant::now();
let mut walked = Vec::with_capacity(stores.len());
for (i, store) in stores.iter_mut().enumerate() {
let restored =
crate::shard_restore::restore_one_shard(dir, config, i, store, &mut report, catalog);
walked.push(restored?);
}
report.elapsed_ms = start.elapsed().as_millis() as u64;
emit_replay(config, &report);
Ok((report, walked))
}
#[cfg(all(feature = "persist", not(target_arch = "wasm32")))]
fn settle_stages(dir: &Path, config: &Config, src_n: usize) -> io::Result<()> {
for i in 0..src_n {
let stage = layout::stage_path(dir, i);
if stage.exists() {
let mut aof = Aof::open(&layout::aof_path(dir, i), config.appendfsync)?;
aof.settle_stage(&stage, |_| {})?;
}
}
Ok(())
}
#[cfg(feature = "persist")]
fn reshard(
dir: &Path,
config: &Config,
n: usize,
prev_n: Option<usize>,
stores: &mut [Keyspace],
catalog: &mut Option<kevy_resp::Argv>,
) -> io::Result<OpenReport> {
let lay = EmbLayout;
let src_n = prev_n.unwrap_or(1);
#[cfg(not(target_arch = "wasm32"))]
settle_stages(dir, config, src_n)?;
let (temp, report) = merge_into_temp(dir, config, src_n, catalog)?;
redistribute(&temp, n, stores);
let target = ShardsMeta::new(n, Routing::KevyHash);
commit_reshard(dir, src_n, target, stores, catalog.as_ref(), &lay)?;
#[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
#[expect(clippy::let_underscore_must_use, reason = "the scratch dir is already dead")]
if config.tier_budget.is_some() {
drop(temp);
let _ = std::fs::remove_dir_all(dir.join("tier").join(".reshard-merge"));
}
Ok(report)
}
#[cfg(feature = "persist")]
fn merge_into_temp(
dir: &Path,
config: &Config,
src_n: usize,
catalog: &mut Option<kevy_resp::Argv>,
) -> io::Result<(Keyspace, OpenReport)> {
let lay = EmbLayout;
let mut temp = fresh_keyspace(config);
#[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
if config.tier_budget.is_some() {
let budget = resolve_tier_budget(config, 1)?;
temp.enable_tiering(&dir.join("tier").join(".reshard-merge"), budget)?;
temp.set_tier_max_spill(config.max_spill_value);
}
let mut total_cmds = 0u64;
let start = Instant::now();
merge_sources(dir, src_n, &lay, &mut temp, |store, args| {
if crate::shard_restore::is_catalog(&args) {
crate::shard_restore::keep_newest(catalog, args);
return;
}
total_cmds += 1;
crate::replay::apply(store, &args);
if total_cmds.is_multiple_of(kevy_persist::REPLAY_DEMOTE_INTERVAL) {
store.demote_to_watermark();
}
})?;
let total_bytes = (0..src_n)
.map(|i| lay.aof_path(dir, i, src_n))
.filter_map(|p| std::fs::metadata(p).ok())
.map(|m| m.len())
.sum();
let report = OpenReport {
replayed_commands: total_cmds,
replayed_bytes: total_bytes,
elapsed_ms: start.elapsed().as_millis() as u64,
..OpenReport::default()
};
emit_replay(config, &report);
Ok((temp, report))
}
#[cfg(feature = "persist")]
fn redistribute(temp: &Keyspace, n: usize, stores: &mut [Keyspace]) {
temp.snapshot_each(|key, value, ttl_ms| {
let hot;
let value = match temp.materialize_cold(key, value) {
Some(v) => {
hot = v;
&hot
}
None => value,
};
let target = &mut stores[shard_idx(key, n)];
target.load_value(key, value, ttl_ms);
target.try_demote_after_write();
});
}
#[cfg(feature = "persist")]
fn emit_replay(config: &Config, report: &OpenReport) {
if let Some(sink) = &config.metric_sink {
sink.emit(KevyMetric::Replay {
commands: report.replayed_commands,
bytes: report.replayed_bytes + report.dropped_bytes,
elapsed_ms: report.elapsed_ms,
dropped_bytes: report.dropped_bytes,
corrupt: report.corrupt,
});
}
}
#[cfg(feature = "persist")]
fn into_inners(stores: Vec<Keyspace>, aofs: Vec<Option<Aof>>) -> Vec<Arc<RwLock<Inner>>> {
stores
.into_iter()
.zip(aofs)
.map(|(store, aof)| Arc::new(RwLock::new(Inner::new(store, aof))))
.collect()
}
#[cfg(not(feature = "persist"))]
fn into_inners_mem(stores: Vec<Keyspace>) -> Vec<Arc<RwLock<Inner>>> {
stores.into_iter().map(|store| Arc::new(RwLock::new(Inner::new(store)))).collect()
}