-
Notifications
You must be signed in to change notification settings - Fork 0
fix(storage): policy-aware eviction fail-close for disk-offload without a durability backstop #273
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b044025
6683efe
c89bac4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -931,6 +931,54 @@ impl ServerConfig { | |
| } | ||
| } | ||
|
|
||
| /// True when `--disk-offload enable` is configured but neither AOF | ||
| /// (`--appendonly yes`) nor RDB (`--save`) durability is on (GCP | ||
| /// benchmark finding, 2026-07-10). | ||
| /// | ||
| /// The durable-spill eviction path (`evict_batch_durable_no_aof`) needs | ||
| /// a `ShardManifest`, which today is only threaded through the | ||
| /// tick-driven memory-pressure cascade | ||
| /// (`shard::persistence_tick::handle_memory_pressure`) — itself gated on | ||
| /// `persistence_dir`, which `main.rs` only constructs when | ||
| /// `appendonly == "yes" || save.is_some()` (see `main.rs`'s | ||
| /// `persistence_dir` binding). The inline per-connection write-path | ||
| /// eviction gate has no manifest access and, under this combination, | ||
| /// cannot durably spill — cold data is NOT tiered to disk regardless of | ||
| /// policy. What happens to the *write* itself is now policy-aware | ||
| /// (`src/storage/eviction.rs`, the `manifest is None` branch of | ||
| /// `try_evict_if_needed_async_spill_with_total_budget`): an evicting | ||
| /// policy (`allkeys-*`/`volatile-*`) still honors `--maxmemory` by | ||
| /// DROPPING victims outright (Redis cache semantics, no tiering, no | ||
| /// crash-durability claim needed since nothing needs to survive a | ||
| /// restart); `noeviction` — and any evicting policy once no eligible | ||
| /// victim remains (e.g. `volatile-*` with no TTL keys left) — rejects | ||
| /// writes with OOM at the cap. This | ||
| /// predicate stays orthogonal to `maxmemory_policy` — spill IS still | ||
| /// inert either way — and exists only to make that degradation loud. | ||
| pub fn disk_offload_spill_inert(&self) -> bool { | ||
| self.disk_offload_enabled() && self.appendonly != "yes" && self.save.is_none() | ||
| } | ||
|
Comment on lines
+934
to
+960
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift Split The final file reaches Line 2808, so this change adds more configuration surface to a Rust file already well beyond the 1500-line limit. Move related implementation/tests into modules before continuing to grow this file. As per coding guidelines, “No single Rust file should exceed 1500 lines.” 🤖 Prompt for AI AgentsSource: Coding guidelines
Comment on lines
+958
to
+960
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
ast-grep outline src/config.rs --items all --type function
rg -n -C6 -- '--save|parse_save_rules|save\.is_some|save\.is_none' \
src/config.rs src/main.rsRepository: pilotspace/moon Length of output: 15227 🏁 Script executed: #!/bin/bash
set -eu
# Inspect the save field definition and relevant parsing logic.
ast-grep outline src/config.rs --match 'struct ServerConfig' --view expanded
rg -n -C4 'save:|--save|parse_save_rules|save\.is_some|save\.is_none|disable form|empty.*save|snapshot rule' src/config.rs src/main.rs src/persistence -g '!target'
# Inspect auto-save rule parsing implementation.
ast-grep outline src/persistence/auto_save.rs --view expanded
sed -n '1,260p' src/persistence/auto_save.rsRepository: pilotspace/moon Length of output: 28129 🏁 Script executed: #!/bin/bash
set -eu
# Show the CLI field annotations and any normalization of save.
sed -n '130,170p' src/config.rs
sed -n '1230,1335p' src/config.rs
# Search for explicit tests or docs about disabling save snapshots.
rg -n -C3 'save.*""|--save ""|empty string|disable.*save|save is none|save rules empty|parse_save_rules\(&input\)' src tests .Repository: pilotspace/moon Length of output: 46382 Use active save rules here
🤖 Prompt for AI Agents |
||
|
|
||
| /// Warn once at startup when [`disk_offload_spill_inert`] holds — see | ||
| /// that method's docs for the full mechanism. Behavior is unchanged; | ||
| /// this only surfaces the pre-existing silent degradation. | ||
| /// | ||
| /// [`disk_offload_spill_inert`]: Self::disk_offload_spill_inert | ||
| pub fn warn_disk_offload_without_durability(&self) { | ||
| if self.disk_offload_spill_inert() { | ||
| tracing::warn!( | ||
| "--disk-offload is enabled but persistence is off (appendonly=no and no \ | ||
| --save). The disk-offload cold-spill tier requires a durability backstop \ | ||
| to function: without one, cold data is NOT spilled to disk. Evicting \ | ||
| policies (allkeys-*/volatile-*) fall back to DROPPING eligible \ | ||
| victims with no tiering; noeviction — and any evicting policy once \ | ||
| no eligible victim remains (e.g. volatile-* with no TTL keys) — \ | ||
| rejects writes with OOM at the cap. Enable \ | ||
| --appendonly yes or --save to activate durable spill." | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// Validate `--databases` fits the `u8` db_index tag used by vector/text | ||
| /// index scoping (WS5a round 2). Returns an error message (not a | ||
| /// `Frame`/`anyhow::Error` — kept plain so both `main.rs`'s early-boot | ||
|
|
@@ -2369,6 +2417,33 @@ mod tests { | |
| assert_eq!(config.pagecache_size, None); | ||
| } | ||
|
|
||
| #[test] | ||
| fn disk_offload_spill_inert_true_without_durability_backstop() { | ||
| // disk-offload defaults to enabled; explicitly turning off both AOF | ||
| // and RDB durability with no backstop leaves cold-spill inert. | ||
| let config = ServerConfig::parse_from(["moon", "--appendonly", "no"]); | ||
| assert!(config.disk_offload_spill_inert()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn disk_offload_spill_inert_false_when_appendonly_yes() { | ||
| let config = ServerConfig::parse_from(["moon", "--appendonly", "yes"]); | ||
| assert!(!config.disk_offload_spill_inert()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn disk_offload_spill_inert_false_when_save_configured() { | ||
| let config = ServerConfig::parse_from(["moon", "--appendonly", "no", "--save", "3600 1"]); | ||
| assert!(!config.disk_offload_spill_inert()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn disk_offload_spill_inert_false_when_disk_offload_disabled() { | ||
| let config = | ||
| ServerConfig::parse_from(["moon", "--appendonly", "no", "--disk-offload", "disable"]); | ||
| assert!(!config.disk_offload_spill_inert()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_parse_size() { | ||
| assert_eq!(ServerConfig::parse_size("256mb"), Some(268_435_456)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -140,6 +140,12 @@ fn main() -> anyhow::Result<()> { | |||||||||||||||||||||
| // once if an operator still sets any of them away from their default. | ||||||||||||||||||||||
| config.warn_deprecated_cold_tier_flags(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // GCP benchmark finding (2026-07-10): --disk-offload enable without a | ||||||||||||||||||||||
| // durability backstop (--appendonly yes or --save) leaves the cold-spill | ||||||||||||||||||||||
| // tier silently inert — --maxmemory becomes a hard reject-at-cap instead | ||||||||||||||||||||||
| // of spilling. Warn once at startup; behavior is intentionally unchanged. | ||||||||||||||||||||||
| config.warn_disk_offload_without_durability(); | ||||||||||||||||||||||
|
Comment on lines
+143
to
+147
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift Split The final file reaches Line 1862, exceeding the repository’s 1500-line Rust-file limit. As per coding guidelines, “No single Rust file should exceed 1500 lines.” 🤖 Prompt for AI AgentsSource: Coding guidelines 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Update the startup comment to match the new eviction behavior. This comment still says Suggested wording- // tier silently inert — --maxmemory becomes a hard reject-at-cap instead
- // of spilling. Warn once at startup; behavior is intentionally unchanged.
+ // tier silently inert — evicting policies drop eligible victims instead
+ // of spilling, while noeviction rejects writes at the cap. Warn once.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // ── --profile tuning presets (WS4, v0.6.0) ────────────────────────────── | ||||||||||||||||||||||
| // Fill-only: a preset only sets flags the operator left at their default | ||||||||||||||||||||||
| // (see `ServerConfig::apply_profile`); anything explicitly passed on the | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.