Skip to main content

kvbm_logical/blocks/
immutable.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! RAII guards for blocks in the **Registered** state.
5//!
6//! [`ImmutableBlock`] is a cheap-to-clone strong handle backed by an
7//! `Arc<ImmutableBlockInner<T>>`. The inner carries the slot's identity
8//! ([`BlockId`], [`SequenceHash`], [`BlockRegistrationHandle`]) plus an
9//! `is_primary` flag that decides the slot's drop transition:
10//!
11//! - **Primary** (`is_primary = true`): the canonical holder of a sequence
12//!   hash. Drop of the last clone moves the slot to `Inactive` so it can
13//!   be evicted later.
14//! - **Duplicate** (`is_primary = false`): a second physical block sharing
15//!   the same hash. It carries a strong [`Arc`] reference to the primary
16//!   inner so the primary cannot be evicted while a duplicate exists. Drop
17//!   of the last clone resets the slot via `mark_absent`.
18//!
19//! [`WeakBlock`] is a non-owning handle that can be upgraded back to an
20//! [`ImmutableBlock`] either via `Weak::upgrade` (fast path) or by
21//! resurrecting an evicted block from the store's inactive pool through
22//! the registry (slow path).
23
24use std::sync::{Arc, Weak};
25
26use crate::ManagerId;
27use crate::blocks::pin::{LifecyclePin, LifecyclePinRef};
28use crate::blocks::{BlockId, BlockMetadata, BlockRegistrationHandle, SequenceHash};
29use crate::pools::{BlockStore, store::upgrade_or_resurrect};
30
31/// Internal owner of a registered slot. Every clone of an
32/// [`ImmutableBlock`] shares an `Arc<ImmutableBlockInner<T>>`. When the
33/// last `Arc` is dropped the slot transitions per `is_primary`.
34///
35/// The per-block "reset on release" override is *not* stored here —
36/// it lives in `BlockStore::reset_on_release[block_id]` and is read by
37/// `release_primary` under the store mutex. This keeps the override
38/// visible to the lookup-driven eager `Primary → Inactive` path even
39/// when this Inner is mid-drop.
40pub(crate) struct ImmutableBlockInner<T: BlockMetadata> {
41    store: Arc<BlockStore<T>>,
42    block_id: BlockId,
43    seq_hash: SequenceHash,
44    handle: BlockRegistrationHandle,
45    is_primary: bool,
46    /// For duplicates, holds a strong reference to the primary's inner so
47    /// the primary cannot transition to `Inactive` (and thus be evicted)
48    /// while any duplicate is alive.
49    _primary_keepalive: Option<Arc<ImmutableBlockInner<T>>>,
50}
51
52impl<T: BlockMetadata + Sync> ImmutableBlockInner<T> {
53    pub(crate) fn new_primary(
54        store: Arc<BlockStore<T>>,
55        block_id: BlockId,
56        seq_hash: SequenceHash,
57        handle: BlockRegistrationHandle,
58    ) -> Arc<Self> {
59        Arc::new(Self {
60            store,
61            block_id,
62            seq_hash,
63            handle,
64            is_primary: true,
65            _primary_keepalive: None,
66        })
67    }
68
69    pub(crate) fn new_duplicate(
70        store: Arc<BlockStore<T>>,
71        block_id: BlockId,
72        seq_hash: SequenceHash,
73        handle: BlockRegistrationHandle,
74        primary: Arc<ImmutableBlockInner<T>>,
75    ) -> Arc<Self> {
76        Arc::new(Self {
77            store,
78            block_id,
79            seq_hash,
80            handle,
81            is_primary: false,
82            _primary_keepalive: Some(primary),
83        })
84    }
85
86    pub(crate) fn block_id(&self) -> BlockId {
87        self.block_id
88    }
89
90    /// Crate-private inherent accessor for the block's [`SequenceHash`].
91    ///
92    /// `LifecyclePin::sequence_hash` exposes the same value, but the
93    /// inherent method lets callers (e.g. `BlockManager::match_blocks`'
94    /// batched frequency-tracker touch) read it without importing the
95    /// `LifecyclePin` trait.
96    pub(crate) fn sequence_hash(&self) -> SequenceHash {
97        self.seq_hash
98    }
99}
100
101impl<T: BlockMetadata + Sync> LifecyclePin for ImmutableBlockInner<T> {
102    fn block_id(&self) -> BlockId {
103        self.block_id
104    }
105    fn sequence_hash(&self) -> SequenceHash {
106        self.seq_hash
107    }
108    fn manager_id(&self) -> ManagerId {
109        self.store.id()
110    }
111    fn registration_handle(&self) -> BlockRegistrationHandle {
112        self.handle.clone()
113    }
114}
115
116impl<T: BlockMetadata> Drop for ImmutableBlockInner<T> {
117    fn drop(&mut self) {
118        // self_ptr identifies *this* Inner so the store can verify slot
119        // identity before transitioning. If a concurrent
120        // `acquire_for_hash` already eagerly completed the transition,
121        // the store call is a no-op. The destination decision (Inactive
122        // vs Reset) is taken inside `release_primary` from the
123        // store-owned per-slot atomic.
124        let self_ptr = self as *const ImmutableBlockInner<T> as *const ();
125        if self.is_primary {
126            self.store.release_primary(self.block_id, self_ptr);
127        } else {
128            self.store.release_duplicate(self.block_id, self_ptr);
129        }
130    }
131}
132
133/// RAII guard for a block in the **Registered** state.
134///
135/// `Clone` increments an `Arc` and the `inflight_immutable` metric;
136/// dropping a clone decrements the metric. Dropping the last strong
137/// reference triggers the slot's transition to `Inactive` (primary) or
138/// `Reset` (duplicate).
139pub struct ImmutableBlock<T: BlockMetadata> {
140    inner: Arc<ImmutableBlockInner<T>>,
141}
142
143impl<T: BlockMetadata + Sync> ImmutableBlock<T> {
144    pub(crate) fn from_inner(inner: Arc<ImmutableBlockInner<T>>) -> Self {
145        inner.store.metrics().inc_inflight_immutable();
146        Self { inner }
147    }
148
149    /// Creates a [`WeakBlock`] that does not prevent the block from being
150    /// evicted.
151    pub fn downgrade(&self) -> WeakBlock<T> {
152        WeakBlock {
153            sequence_hash: self.inner.seq_hash,
154            inner: Arc::downgrade(&self.inner),
155            handle: self.inner.handle.clone(),
156            store: self.inner.store.clone(),
157        }
158    }
159
160    /// Returns the [`BlockId`] assigned to this block.
161    pub fn block_id(&self) -> BlockId {
162        self.inner.block_id
163    }
164
165    /// Returns the [`SequenceHash`] that identifies this block's content.
166    pub fn sequence_hash(&self) -> SequenceHash {
167        self.inner.seq_hash
168    }
169
170    /// Returns a clone of the [`BlockRegistrationHandle`] for this block.
171    pub fn registration_handle(&self) -> BlockRegistrationHandle {
172        self.inner.handle.clone()
173    }
174
175    /// Returns the number of strong [`Arc`] references to the underlying
176    /// inner.
177    pub fn use_count(&self) -> usize {
178        Arc::strong_count(&self.inner)
179    }
180
181    /// Set the per-block "reset on release" override.
182    ///
183    /// When the last clone of this block drops, the slot transitions to
184    /// the reset/free list (`true`) or to the inactive cache (`false`),
185    /// overriding the store-wide default set by
186    /// `BlockManagerConfigBuilder::with_default_reset_on_release`.
187    ///
188    /// The override is sticky across cache-hit resurrections: if the
189    /// block lands in the inactive pool and is later matched, the
190    /// resurrected `ImmutableBlock` inherits this value. The override
191    /// is reset to the store-wide default only when the slot truly
192    /// leaves the inactive pool (eviction back to `Mutable`).
193    ///
194    /// Briefly acquires the store mutex to publish the write. Not a hot
195    /// path — typically called at most once per block. Concurrent
196    /// setters race with last-writer-wins semantics under the mutex.
197    ///
198    /// Race-window guarantee: the override is preserved even when a
199    /// concurrent `match_blocks` drives the eager `Primary → Inactive`
200    /// transition (because this `Inner`'s `Arc` strong-count went to 0
201    /// before `release_primary` ran). The eager path leaves the
202    /// per-slot value untouched, and every reader of that value also
203    /// goes through the same store mutex — so visibility comes from
204    /// release-acquire on the mutex, not from any assumption about
205    /// `Arc::drop` / `Weak::upgrade` ordering.
206    pub fn set_evict_on_reset(&self, value: bool) {
207        self.inner
208            .store
209            .store_reset_on_release(self.inner.block_id, value);
210    }
211
212    /// Type-erased lifecycle pin for cross-policy use.
213    ///
214    /// Returns an [`LifecyclePinRef`] that:
215    /// - Bumps the underlying `Arc<ImmutableBlockInner<T>>` once (the
216    ///   only allocation cost is one `Arc::clone` — no `Box` and no new
217    ///   heap node).
218    /// - Keeps the slot alive (preventing the `Active → Inactive`
219    ///   transition) while the pin is live, identical to holding an
220    ///   `ImmutableBlock` clone.
221    /// - Exposes `(manager_id, block_id, sequence_hash)` so callers that
222    ///   stash a heterogeneous list of pins (different `T`s) can still
223    ///   address each slot unambiguously at runtime.
224    ///
225    /// See [`crate::blocks::pin`] for the rationale.
226    pub fn pin(&self) -> LifecyclePinRef {
227        LifecyclePinRef::new(self.inner.clone() as Arc<dyn LifecyclePin>)
228    }
229}
230
231impl<T: BlockMetadata + Sync> Clone for ImmutableBlock<T> {
232    fn clone(&self) -> Self {
233        self.inner.store.metrics().inc_inflight_immutable();
234        Self {
235            inner: self.inner.clone(),
236        }
237    }
238}
239
240impl<T: BlockMetadata> Drop for ImmutableBlock<T> {
241    #[inline]
242    fn drop(&mut self) {
243        self.inner.store.metrics().dec_inflight_immutable();
244    }
245}
246
247impl<T: BlockMetadata> std::fmt::Debug for ImmutableBlock<T> {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        f.debug_struct("ImmutableBlock")
250            .field("block_id", &self.inner.block_id)
251            .field("sequence_hash", &self.inner.seq_hash)
252            .finish()
253    }
254}
255
256/// Non-owning reference to a registered block.
257///
258/// Created via [`ImmutableBlock::downgrade`]. Cheap to clone. Calling
259/// [`upgrade`](Self::upgrade) tries `Weak::upgrade` first (fast path) and
260/// falls back to resurrecting the block from the store's inactive pool
261/// via the registry (slow path).
262pub struct WeakBlock<T: BlockMetadata> {
263    sequence_hash: SequenceHash,
264    inner: Weak<ImmutableBlockInner<T>>,
265    handle: BlockRegistrationHandle,
266    store: Arc<BlockStore<T>>,
267}
268
269impl<T: BlockMetadata + Sync> WeakBlock<T> {
270    /// Attempt to upgrade this weak reference to a strong [`ImmutableBlock`].
271    pub fn upgrade(&self) -> Option<ImmutableBlock<T>> {
272        if let Some(strong) = self.inner.upgrade() {
273            return Some(ImmutableBlock::from_inner(strong));
274        }
275        let inner = upgrade_or_resurrect::<T>(&self.handle, &self.store, false)?;
276        Some(ImmutableBlock::from_inner(inner))
277    }
278
279    /// Returns the [`SequenceHash`] for the block this weak reference
280    /// points to.
281    pub fn sequence_hash(&self) -> SequenceHash {
282        self.sequence_hash
283    }
284}
285
286impl<T: BlockMetadata> Clone for WeakBlock<T> {
287    fn clone(&self) -> Self {
288        Self {
289            sequence_hash: self.sequence_hash,
290            inner: self.inner.clone(),
291            handle: self.handle.clone(),
292            store: self.store.clone(),
293        }
294    }
295}
296
297impl<T: BlockMetadata> std::fmt::Debug for WeakBlock<T> {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        f.debug_struct("WeakBlock")
300            .field("sequence_hash", &self.sequence_hash)
301            .finish()
302    }
303}