Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
130 changes: 109 additions & 21 deletions src/command/vector_search/tests.rs

Large diffs are not rendered by default.

3,912 changes: 0 additions & 3,912 deletions src/persistence/aof.rs

This file was deleted.

1,078 changes: 1,078 additions & 0 deletions src/persistence/aof/mod.rs

Large diffs are not rendered by default.

1,382 changes: 1,382 additions & 0 deletions src/persistence/aof/pool.rs

Large diffs are not rendered by default.

941 changes: 941 additions & 0 deletions src/persistence/aof/rewrite.rs

Large diffs are not rendered by default.

1,017 changes: 1,017 additions & 0 deletions src/persistence/aof/writer_task.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"
);
}
}
29 changes: 24 additions & 5 deletions src/shard/conn_accept.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,13 @@ pub(crate) fn spawn_tokio_connection(
///
/// TLS connections cannot be migrated because TLS session state lives in userspace and
/// cannot be reconstructed from a raw FD. Only plain TCP connections should be migrated.
#[cfg(feature = "runtime-tokio")]
// `unix`-gated alongside `runtime-tokio`: the signature takes a
// `std::os::unix::io::RawFd` and reconstructs the socket via `from_raw_fd`,
// both of which only exist on Unix. Moon targets Linux + macOS (both Unix), so
// on every supported build `unix` is always true; the extra predicate just makes
// the platform coupling explicit (CodeRabbit PR #144). Full non-Unix support
// would also require gating `MigrateConnectionPayload.fd` — out of scope.
#[cfg(all(feature = "runtime-tokio", unix))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_migrated_tokio_connection(
fd: std::os::unix::io::RawFd,
Expand Down Expand Up @@ -301,6 +307,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 +374,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 +406,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 Expand Up @@ -739,7 +755,10 @@ pub(crate) fn spawn_monoio_connection(
/// # Limitations
///
/// TLS connections cannot be migrated (TLS session state is in userspace).
#[cfg(feature = "runtime-monoio")]
// `unix`-gated alongside `runtime-monoio` for the same reason as the tokio twin
// above: the `RawFd` signature + `from_raw_fd` are Unix-only. Always true on
// supported targets (Linux + macOS).
#[cfg(all(feature = "runtime-monoio", unix))]
#[allow(clippy::too_many_arguments)]
pub(crate) fn spawn_migrated_monoio_connection(
fd: std::os::unix::io::RawFd,
Expand Down
14 changes: 10 additions & 4 deletions src/shard/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1233,7 +1233,7 @@ impl super::Shard {
"Shard {}: accepting migrated connection (fd={}, client_id={}, from={})",
shard_id, fd, state.client_id, state.peer_addr
);
#[cfg(feature = "runtime-tokio")]
#[cfg(all(feature = "runtime-tokio", unix))]
{
conn_accept::spawn_migrated_tokio_connection(
fd, state,
Expand All @@ -1244,9 +1244,10 @@ 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")]
#[cfg(all(feature = "runtime-monoio", unix))]
{
conn_accept::spawn_migrated_monoio_connection(
fd, state,
Expand Down Expand Up @@ -1332,7 +1333,7 @@ impl super::Shard {
"Shard {}: accepting migrated connection (fd={}, client_id={}, from={})",
shard_id, fd, state.client_id, state.peer_addr
);
#[cfg(feature = "runtime-tokio")]
#[cfg(all(feature = "runtime-tokio", unix))]
{
conn_accept::spawn_migrated_tokio_connection(
fd, state,
Expand All @@ -1343,9 +1344,10 @@ 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")]
#[cfg(all(feature = "runtime-monoio", unix))]
{
conn_accept::spawn_migrated_monoio_connection(
fd, state,
Expand Down Expand Up @@ -1975,6 +1977,10 @@ impl super::Shard {
state.client_id,
state.peer_addr
);
// `unix`-gated to match `spawn_migrated_monoio_connection`'s
// Unix-only `RawFd` signature (CodeRabbit PR #144). Always
// compiled on supported targets (Linux + macOS).
#[cfg(unix)]
conn_accept::spawn_migrated_monoio_connection(
fd,
state,
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