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}