Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
6cac58f
fix(disk-offload): block instead of dropping spill completions (data …
tindangtts Jun 4, 2026
deae60d
fix(disk-offload): salvage inline-batch spill failures per-entry, not…
tindangtts Jun 4, 2026
cf90800
fix(disk-offload): preserve spill context across tokio connection mig…
tindangtts Jun 4, 2026
4828b3d
fix(embedded): recover cold tier under appendonly=no + disable per-sh…
tindangtts Jun 4, 2026
fa9afe9
fix(persistence): clear rewrite flag when per-shard fan-out fails par…
tindangtts Jun 4, 2026
c919ff5
fix(persistence): roll per-shard rewrite writers back to committed ge…
tindangtts Jun 4, 2026
73d9514
fix(persistence): ack drained AppendSync only after the boundary fsync
tindangtts Jun 4, 2026
14c572b
refactor(persistence): split aof_manifest.rs into submodules under 15…
tindangtts Jun 4, 2026
566869d
chore(storage): correct stale async-spill doc, drop dead remove-first…
tindangtts Jun 4, 2026
798ce09
fix(shard): gate migrated-connection spawn fns behind cfg(unix)
tindangtts Jun 4, 2026
1a10aab
test(storage): lock fail-safe send-before-remove async-spill eviction
tindangtts Jun 4, 2026
5f5ecb3
refactor(persistence): split aof.rs into submodules under 1500-line cap
tindangtts Jun 4, 2026
edffce3
test(vector): fix VECTOR_INDEXES counter race via RwLock test isolation
tindangtts Jun 4, 2026
839ba8c
docs(changelog): add PR #144 durability + decomposition + test-isolat…
tindangtts Jun 4, 2026
e41f065
test(vector): use parking_lot::RwLock for METRICS_LOCK (no poison cas…
tindangtts Jun 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
601 changes: 534 additions & 67 deletions src/persistence/aof.rs

Large diffs are not rendered by default.

3,058 changes: 0 additions & 3,058 deletions src/persistence/aof_manifest.rs

This file was deleted.

1,388 changes: 1,388 additions & 0 deletions src/persistence/aof_manifest/mod.rs

Large diffs are not rendered by default.

1,157 changes: 1,157 additions & 0 deletions src/persistence/aof_manifest/shard_replay.rs

Large diffs are not rendered by default.

577 changes: 577 additions & 0 deletions src/persistence/aof_manifest/shard_rewrite.rs

Large diffs are not rendered by default.

49 changes: 45 additions & 4 deletions src/server/embedded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ use crate::shard::Shard;
use crate::shard::mesh::{CHANNEL_BUFFER_SIZE, ChannelMesh};
use crate::shard::shared_databases::ShardDatabases;

/// Whether pre-loop recovery (`Shard::restore_from_persistence`) must run.
///
/// Recovery is required when EITHER durable persistence (`persistence_dir`) OR
/// disk-offload is configured. The disk-offload case is the subtle one: under
/// `--appendonly no` + disk-offload, `persistence_dir` is `None`, yet the cold
/// tier still has on-disk state whose baseline must be restored on restart —
/// skipping it leaves cold keys unrecoverable. Mirrors the `main.rs` gate.
#[inline]
fn should_run_recovery(persistence_dir: Option<&str>, disk_offload_enabled: bool) -> bool {
persistence_dir.is_some() || disk_offload_enabled
}

/// Run an embedded sharded Moon server until `cancel` is fired.
///
/// Behaves like the production `main.rs` startup path but with cluster,
Expand Down Expand Up @@ -218,8 +230,13 @@ pub async fn run_embedded(
config.initial_keyspace_hint,
config.to_runtime_config(),
);
if let Some(ref dir) = persistence_dir {
shard.restore_from_persistence(dir, disk_offload_base.as_deref());
// Recover whenever persistence OR disk-offload is configured. Under
// `--appendonly no` + disk-offload, persistence_dir is None but the
// cold tier still needs its baseline restored on restart, else cold
// keys are unrecoverable (mirrors main.rs).
if should_run_recovery(persistence_dir.as_deref(), disk_offload_base.is_some()) {
let recover_dir = persistence_dir.as_deref().unwrap_or(config.dir.as_str());
shard.restore_from_persistence(recover_dir, disk_offload_base.as_deref());
}
if let Some(ref offload_base) = disk_offload_base {
let shard_dir = offload_base.join(format!("shard-{}", id));
Expand Down Expand Up @@ -355,8 +372,10 @@ pub async fn run_embedded(
}
let _ = snap_tx; // keep the watch sender alive for the shard's snap_rx clones

// Run the sharded listener until cancelled.
let per_shard_accept = cfg!(target_os = "linux");
// Run the sharded listener until cancelled. Use the central accept path
// (per_shard_accept = false) like main.rs: the per-shard tokio/Linux accept
// loop has a bind-order race that can hang serving (fixed in PR #136).
let per_shard_accept = false;
let listener_result = server::listener::run_sharded(
config,
conn_txs,
Expand Down Expand Up @@ -463,3 +482,25 @@ fn panic_message<'a>(payload: &'a Box<dyn std::any::Any + Send + 'static>) -> &'
"<non-string panic payload>"
}
}

#[cfg(test)]
mod tests {
use super::should_run_recovery;

#[test]
fn recovery_runs_for_disk_offload_without_persistence() {
// The regression: `--appendonly no` + disk-offload means persistence_dir
// is None but the cold tier must still be recovered on restart.
assert!(
should_run_recovery(None, true),
"disk-offload-only (appendonly=no) must still run cold-tier recovery"
);
// Other combinations behave as before.
assert!(should_run_recovery(Some("/var/lib/moon"), false));
assert!(should_run_recovery(Some("/var/lib/moon"), true));
assert!(
!should_run_recovery(None, false),
"no persistence and no disk-offload: nothing to recover"
);
}
}
16 changes: 13 additions & 3 deletions src/shard/conn_accept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ pub(crate) fn spawn_migrated_tokio_connection(
shard_id: usize,
num_shards: usize,
config_port: u16,
spill_sender: &Option<flume::Sender<crate::storage::tiered::spill_thread::SpillRequest>>,
spill_file_id: &Rc<std::cell::Cell<u64>>,
disk_offload_dir: &Option<std::path::PathBuf>,
) {
use std::os::unix::io::FromRawFd;

Expand Down Expand Up @@ -365,6 +368,13 @@ pub(crate) fn spawn_migrated_tokio_connection(

// Pool is built by the spawn site and threaded through here.
let pool_for_ctx = aof_pool.as_ref().map(Arc::clone);
// Preserve the disk-offload spill context across migration — without
// this a migrated connection silently falls back to the non-spilling
// eviction path even when disk-offload is enabled, so cold-tier
// durability would depend on whether the connection had migrated.
let spill_tx = spill_sender.clone();
let spill_fid = spill_file_id.clone();
let do_dir = disk_offload_dir.clone();
let conn_ctx = crate::server::conn::ConnectionContext::new(
sdbs,
shard_id,
Expand All @@ -390,9 +400,9 @@ pub(crate) fn spawn_migrated_tokio_connection(
all_regs,
all_rsm,
aff,
None, // spill_sender
Rc::new(std::cell::Cell::new(0)), // spill_file_id
None, // disk_offload_dir
spill_tx, // spill_sender (preserved across migration)
spill_fid, // spill_file_id
do_dir, // disk_offload_dir
);

// State restoration happens directly via migrated_state parameter —
Expand Down
2 changes: 2 additions & 0 deletions src/shard/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,7 @@ impl super::Shard {
&cached_clock, &remote_sub_map_arc, &all_pubsub_registries,
&all_remote_sub_maps, &affinity_tracker,
shard_id, num_shards, config_port,
&spill_sender, &spill_file_id, &disk_offload_dir,
);
}
#[cfg(feature = "runtime-monoio")]
Expand Down Expand Up @@ -1343,6 +1344,7 @@ impl super::Shard {
&cached_clock, &remote_sub_map_arc, &all_pubsub_registries,
&all_remote_sub_maps, &affinity_tracker,
shard_id, num_shards, config_port,
&spill_sender, &spill_file_id, &disk_offload_dir,
);
}
#[cfg(feature = "runtime-monoio")]
Expand Down
18 changes: 17 additions & 1 deletion src/shard/persistence_tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,11 @@ pub(crate) fn drain_and_shutdown_spill(
apply_spill_completions(spill_t, shard_manifest, shard_databases, shard_id);
}
if let Some(st) = spill_thread.take() {
st.shutdown();
// shutdown() returns any completions from the thread's final buffer
// flush that the drain above did not see; apply them so those cold keys
// are not lost (file on disk but never recorded in the manifest).
let leftover = st.shutdown();
apply_completion_vec(leftover, shard_manifest, shard_databases, shard_id);
tracing::info!("Shard {}: spill background thread shut down", shard_id);
}
}
Expand All @@ -387,6 +391,18 @@ pub(crate) fn apply_spill_completions(
shard_id: usize,
) {
let completions = spill_thread.drain_completions();
apply_completion_vec(completions, shard_manifest, shard_databases, shard_id);
}

/// Apply a batch of spill completions: ONE manifest `add_file`+commit per file,
/// one `cold_index` insert per KV entry within it. Shared by the live drain
/// (`apply_spill_completions`) and the shutdown final-flush drain.
fn apply_completion_vec(
completions: Vec<crate::storage::tiered::spill_thread::SpillCompletion>,
shard_manifest: &mut Option<crate::persistence::manifest::ShardManifest>,
shard_databases: &std::sync::Arc<super::shared_databases::ShardDatabases>,
shard_id: usize,
) {
if completions.is_empty() {
return;
}
Expand Down
Loading
Loading