spark_runtime/prefix_cache/
tier_evict.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! [`TierEvict`] — what an SSM spill-tier eviction decided to do with the
4//! victim. Split out of `prefix_cache.rs` (500-LoC cap).
5
6/// Outcome of [`crate::prefix_cache::PrefixCache::evict_snapshot_to_tier`].
7///
8/// The spill side of the tier has a cost gate (`ATLAS_SSM_SPILL_MIN_TOKENS`)
9/// mirroring the fault-in side's `ATLAS_SSM_FAULT_MIN_TOKENS`, and the gate has
10/// to be applied at VICTIM SELECTION, not at the byte move. Gating only the
11/// byte move would leave the index entry marked `tiered` — findable by
12/// `lookup_tiered` — with no blob behind it, so every warm turn would pay a
13/// full-blob allocation plus a `store.get` only to learn it misses.
14///
15/// Both arms free the victim's HBM slot, so the caller's invariant "a reclaim
16/// always yields a slot" is preserved either way.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum TierEvict {
19    /// Move the victim's bytes to the tier and keep its index entry findable
20    /// (`tiered = true`) so a warm turn faults it back instead of recomputing.
21    Spill {
22        /// The freed HBM snapshot slot (spill FROM here, then free it).
23        slot: usize,
24        /// Tier key = the entry's prefix hash.
25        key: u64,
26        /// Victim depth in tokens — what the spill's cost buys back.
27        depth: usize,
28    },
29    /// Too shallow to be worth a spill: the index entry was REMOVED, no tier
30    /// key exists, and `tier_spills` was not incremented. Identical to the
31    /// pre-tier drop path.
32    Drop {
33        /// The freed HBM snapshot slot.
34        slot: usize,
35        /// Victim depth in tokens (for the gate-skip log line).
36        depth: usize,
37    },
38}
39
40impl TierEvict {
41    /// The freed HBM slot, whichever arm was taken.
42    pub fn slot(&self) -> usize {
43        match *self {
44            TierEvict::Spill { slot, .. } | TierEvict::Drop { slot, .. } => slot,
45        }
46    }
47}