Skip to main content

kvbm_logical/manager/
mod.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Block lifecycle orchestration over the unified [`BlockStore`].
5//!
6//! [`BlockManager`] owns a single [`BlockStore`] and the [`BlockRegistry`].
7//! All pool transitions go through the store's single mutex; the manager
8//! adds the registry coordination, allocation eviction policy, and metrics.
9
10mod builder;
11
12#[cfg(test)]
13mod tests;
14
15pub use builder::{
16    BlockManagerBuilderError, BlockManagerConfigBuilder, BlockManagerResetError,
17    FrequencyTrackingCapacity, InactiveBackendConfig, LineageEviction,
18};
19
20use std::collections::HashMap;
21use std::sync::Arc;
22
23use crate::blocks::{BlockMetadata, CompleteBlock, ImmutableBlock, MutableBlock};
24use crate::metrics::BlockPoolMetrics;
25use crate::pools::{BlockDuplicationPolicy, BlockStore, SequenceHash};
26use crate::registry::BlockRegistry;
27
28/// Manages the full block lifecycle over the unified [`BlockStore`].
29///
30/// Construct via [`BlockManager::builder()`].
31pub struct BlockManager<T: BlockMetadata> {
32    pub(crate) store: Arc<BlockStore<T>>,
33    pub(crate) block_registry: BlockRegistry,
34    pub(crate) duplication_policy: BlockDuplicationPolicy,
35    pub(crate) total_blocks: usize,
36    pub(crate) block_size: usize,
37    pub(crate) metrics: Arc<BlockPoolMetrics>,
38}
39
40impl<T: BlockMetadata + Sync> BlockManager<T> {
41    /// Create a new builder for `BlockManager`.
42    pub fn builder() -> BlockManagerConfigBuilder<T> {
43        BlockManagerConfigBuilder::default()
44    }
45
46    /// Stable, process-unique identifier for this manager's underlying
47    /// [`BlockStore`](crate::pools::BlockStore). See [`crate::ManagerId`].
48    /// Cheap (one field load via the store).
49    ///
50    /// Together with a [`BlockId`](crate::BlockId) this names a specific
51    /// physical pool slot — the disambiguating runtime address that
52    /// downstream consumers need after the policy parameter `T` has been
53    /// type-erased through [`crate::LifecyclePinRef`].
54    pub fn id(&self) -> crate::ManagerId {
55        self.store.id()
56    }
57
58    /// Allocate `count` mutable blocks, drawing first from the reset pool
59    /// and then evicting from the inactive pool if needed.
60    ///
61    /// Returns `None` if fewer than `count` blocks are available across both pools.
62    pub fn allocate_blocks(&self, count: usize) -> Option<Vec<MutableBlock<T>>> {
63        self.allocate_blocks_with_evictions(count)
64            .map(|(blocks, _evicted)| blocks)
65    }
66
67    /// Like [`allocate_blocks`](Self::allocate_blocks) but also reports the
68    /// [`SequenceHash`] of each block evicted from the inactive pool.
69    pub fn allocate_blocks_with_evictions(
70        &self,
71        count: usize,
72    ) -> Option<(Vec<MutableBlock<T>>, Vec<SequenceHash>)> {
73        self.store.allocate_atomic(count)
74    }
75
76    /// Drain the inactive pool, returning all blocks to the reset pool.
77    pub fn reset_inactive_pool(&self) -> Result<(), BlockManagerResetError> {
78        let blocks = self.store.drain_inactive_to_mutable();
79        drop(blocks);
80
81        let reset_count = self.store.reset_len();
82        if reset_count != self.total_blocks {
83            return Err(BlockManagerResetError::BlockCountMismatch {
84                expected: self.total_blocks,
85                actual: reset_count,
86            });
87        }
88
89        Ok(())
90    }
91
92    /// Register a batch of completed blocks.
93    pub fn register_blocks(&self, blocks: Vec<CompleteBlock<T>>) -> Vec<ImmutableBlock<T>> {
94        if blocks.is_empty() {
95            return Vec::new();
96        }
97
98        let handles = self
99            .block_registry
100            .register_sequence_hashes(blocks.iter().map(CompleteBlock::sequence_hash));
101        let batch_size = blocks.len();
102        let registered =
103            self.store
104                .register_completed_blocks(blocks, handles, self.duplication_policy);
105        // The offline settlement bridge observes this counter as a
106        // publication watermark, so publish only after every store transition
107        // and presence marker in the batch is complete.
108        self.metrics
109            .inc_registrations_by(u64::try_from(batch_size).unwrap_or(u64::MAX));
110        registered
111            .into_iter()
112            .map(ImmutableBlock::from_inner)
113            .collect()
114    }
115
116    /// Register a single completed block and return an immutable handle.
117    pub fn register_block(&self, block: CompleteBlock<T>) -> ImmutableBlock<T> {
118        let handle = self
119            .block_registry
120            .register_sequence_hash(block.sequence_hash());
121        let inner = handle.register_block(block, self.duplication_policy, &self.store);
122        self.metrics.inc_registrations();
123        ImmutableBlock::from_inner(inner)
124    }
125
126    /// Linear prefix match: walks `seq_hash` left-to-right, stopping on
127    /// the first hash that hits neither the active nor the inactive pool.
128    ///
129    /// The whole active-or-inactive prefix is resolved under a **single**
130    /// store-mutex acquisition via [`BlockStore::match_prefix_locked_batch`]
131    /// — no per-hash registry radix-tree lookup, no per-hash store lock.
132    /// Frequency-tracker touches are batched and applied *after* the store
133    /// lock is released: every returned block is touched exactly once
134    /// (including inactive resurrections).
135    pub fn match_blocks(&self, seq_hash: &[SequenceHash]) -> Vec<ImmutableBlock<T>> {
136        self.metrics
137            .inc_match_hashes_requested(seq_hash.len() as u64);
138
139        if seq_hash.is_empty() {
140            self.metrics.inc_match_blocks_returned(0);
141            return Vec::new();
142        }
143
144        // ONE store-lock acquisition for the whole active+inactive prefix.
145        let inners = self.store.match_prefix_locked_batch(seq_hash);
146
147        // Frequency-tracker touches, batched, AFTER the store lock is
148        // released. Touches every returned hit exactly once — including
149        // inactive resurrections, which the old `find_inactive_primaries`
150        // path never touched.
151        if self.block_registry.has_frequency_tracking() {
152            for inner in &inners {
153                self.block_registry.touch(inner.sequence_hash());
154            }
155        }
156
157        let matched: Vec<ImmutableBlock<T>> =
158            inners.into_iter().map(ImmutableBlock::from_inner).collect();
159
160        self.metrics.inc_match_blocks_returned(matched.len() as u64);
161        tracing::debug!(
162            num_hashes = seq_hash.len(),
163            total_matched = matched.len(),
164            "match_blocks result"
165        );
166        tracing::trace!(matched = ?matched, "matched blocks");
167        matched
168    }
169
170    /// Scattered batch match: resolves every input hash against the active or
171    /// inactive pool without stopping at a miss.
172    ///
173    /// The returned vector is aligned with `seq_hash`: each hit is `Some`,
174    /// each miss is `None`, and input order and duplicates are preserved. The
175    /// complete batch is resolved under one store-mutex acquisition. Frequency
176    /// tracking is applied after releasing that lock, exactly once per hit
177    /// (including repeated hashes and inactive resurrections).
178    ///
179    /// This operation contributes to the existing match metrics. Requested
180    /// and returned values are counted as occurrences, so repeated input
181    /// hashes and their repeated hits are counted repeatedly.
182    pub fn match_blocks_scattered(
183        &self,
184        seq_hash: &[SequenceHash],
185    ) -> Vec<Option<ImmutableBlock<T>>> {
186        self.metrics
187            .inc_match_hashes_requested(seq_hash.len() as u64);
188
189        if seq_hash.is_empty() {
190            self.metrics.inc_match_blocks_returned(0);
191            return Vec::new();
192        }
193
194        // ONE store-lock acquisition for all active+inactive probes, including
195        // misses and repeated hashes.
196        let inners = self.store.match_scattered_locked_batch(seq_hash);
197
198        // Keep TinyLFU work outside the store critical section. A duplicate
199        // input is a duplicate access, so each returned occurrence is touched.
200        if self.block_registry.has_frequency_tracking() {
201            for inner in inners.iter().flatten() {
202                self.block_registry.touch(inner.sequence_hash());
203            }
204        }
205
206        let hit_count = inners.iter().filter(|inner| inner.is_some()).count();
207        let matched = inners
208            .into_iter()
209            .map(|inner| inner.map(ImmutableBlock::from_inner))
210            .collect();
211
212        self.metrics.inc_match_blocks_returned(hit_count as u64);
213        tracing::debug!(
214            num_hashes = seq_hash.len(),
215            total_matched = hit_count,
216            "match_blocks_scattered result"
217        );
218        matched
219    }
220
221    /// Scatter-gather scan: finds all blocks matching any hash, without
222    /// stopping on misses. Requested hashes are counted as input occurrences,
223    /// while returned blocks are counted as distinct hashes in the result map.
224    pub fn scan_matches(
225        &self,
226        seq_hashes: &[SequenceHash],
227        touch: bool,
228    ) -> HashMap<SequenceHash, ImmutableBlock<T>> {
229        self.metrics
230            .inc_scan_hashes_requested(seq_hashes.len() as u64);
231
232        let mut result = HashMap::new();
233
234        let active_found = self.scan_active_matches(seq_hashes, touch);
235        for (hash, inner) in active_found {
236            result.insert(hash, ImmutableBlock::from_inner(inner));
237        }
238
239        let remaining: Vec<SequenceHash> = seq_hashes
240            .iter()
241            .filter(|h| !result.contains_key(h))
242            .copied()
243            .collect();
244
245        if !remaining.is_empty() {
246            let inactive_found = self.store.scan_inactive_primaries(&remaining, touch);
247            for (hash, inner) in inactive_found {
248                result.insert(hash, ImmutableBlock::from_inner(inner));
249            }
250        }
251
252        self.metrics.inc_scan_blocks_returned(result.len() as u64);
253
254        result
255    }
256
257    /// Scan-style active lookup by sequence hash via the registry's
258    /// stored Weak references — does not stop on miss.
259    fn scan_active_matches(
260        &self,
261        hashes: &[SequenceHash],
262        touch: bool,
263    ) -> Vec<(SequenceHash, Arc<crate::blocks::ImmutableBlockInner<T>>)> {
264        hashes
265            .iter()
266            .filter_map(|hash| {
267                self.block_registry
268                    .match_sequence_hash(*hash, touch)
269                    .and_then(|handle| {
270                        handle
271                            .try_get_inner::<T>(&self.store, touch)
272                            .map(|inner| (*hash, inner))
273                    })
274            })
275            .collect()
276    }
277
278    /// Total number of blocks managed (constant after construction).
279    pub fn total_blocks(&self) -> usize {
280        self.total_blocks
281    }
282
283    /// Blocks available for allocation (reset + inactive pools).
284    ///
285    /// Reads both pool sizes under a single store-lock acquisition so the
286    /// returned value is a coherent snapshot, never an over- or under-count
287    /// produced by a concurrent reset↔inactive transition.
288    pub fn available_blocks(&self) -> usize {
289        self.store.available_len()
290    }
291
292    /// Tokens per block (constant after construction).
293    pub fn block_size(&self) -> usize {
294        self.block_size
295    }
296
297    /// Current duplication policy.
298    pub fn duplication_policy(&self) -> &BlockDuplicationPolicy {
299        &self.duplication_policy
300    }
301
302    /// Reference to the shared block registry.
303    pub fn block_registry(&self) -> &BlockRegistry {
304        &self.block_registry
305    }
306
307    /// Reference to the block pool metrics.
308    pub fn metrics(&self) -> &Arc<BlockPoolMetrics> {
309        &self.metrics
310    }
311
312    /// Test-only accessor for the underlying [`BlockStore`]. Used to
313    /// reach test hooks like `BlockStore::pause_release_primary` from
314    /// race-window tests.
315    #[cfg(test)]
316    pub(crate) fn store_for_test(&self) -> &Arc<BlockStore<T>> {
317        &self.store
318    }
319}