kvbm_engine/leader/accessor.rs
1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Block accessor for policy-based scanning.
5//!
6//! Provides a stateless interface for acquiring blocks from G2/G3 tiers.
7//! Designed for use with custom scanning policies that control iteration
8//! and can yield results incrementally.
9
10use crate::{BlockId, G2, G3, SequenceHash};
11use kvbm_common::LogicalLayoutHandle;
12use kvbm_logical::blocks::ImmutableBlock;
13
14use super::InstanceLeader;
15
16/// A block from either G2 or G3 tier.
17///
18/// Provides RAII ownership - blocks are released when dropped.
19#[derive(Debug)]
20pub enum TieredBlock {
21 /// Block from G2 (host memory) tier.
22 G2(ImmutableBlock<G2>),
23 /// Block from G3 (disk) tier.
24 G3(ImmutableBlock<G3>),
25}
26
27impl TieredBlock {
28 /// Get the storage tier of this block.
29 pub fn tier(&self) -> LogicalLayoutHandle {
30 match self {
31 TieredBlock::G2(_) => LogicalLayoutHandle::G2,
32 TieredBlock::G3(_) => LogicalLayoutHandle::G3,
33 }
34 }
35
36 /// Get the sequence hash.
37 pub fn sequence_hash(&self) -> SequenceHash {
38 match self {
39 TieredBlock::G2(b) => b.sequence_hash(),
40 TieredBlock::G3(b) => b.sequence_hash(),
41 }
42 }
43
44 /// Get the block ID.
45 pub fn block_id(&self) -> BlockId {
46 match self {
47 TieredBlock::G2(b) => b.block_id(),
48 TieredBlock::G3(b) => b.block_id(),
49 }
50 }
51
52 /// Get the position in the sequence (for ordering).
53 pub fn position(&self) -> u64 {
54 self.sequence_hash().position()
55 }
56
57 /// Check if this is a G2 block.
58 pub fn is_g2(&self) -> bool {
59 matches!(self, TieredBlock::G2(_))
60 }
61
62 /// Check if this is a G3 block.
63 pub fn is_g3(&self) -> bool {
64 matches!(self, TieredBlock::G3(_))
65 }
66
67 /// Convert to G2 block, consuming self.
68 pub fn into_g2(self) -> Option<ImmutableBlock<G2>> {
69 match self {
70 TieredBlock::G2(b) => Some(b),
71 TieredBlock::G3(_) => None,
72 }
73 }
74
75 /// Convert to G3 block, consuming self.
76 pub fn into_g3(self) -> Option<ImmutableBlock<G3>> {
77 match self {
78 TieredBlock::G3(b) => Some(b),
79 TieredBlock::G2(_) => None,
80 }
81 }
82}
83
84/// Stateless accessor for block acquisition.
85///
86/// Each method call is independent - no locks are held between calls.
87/// This enables parallel policy execution (e.g., with rayon).
88///
89/// # Thread Safety
90///
91/// `BlockAccessor` is `Send + Sync` because:
92/// - It only holds a shared reference to `InstanceLeader`
93/// - `InstanceLeader` contains `Arc<BlockManager<T>>` which is `Send + Sync`
94/// - All operations use internal locking per call
95/// - No mutable state is held between method calls
96pub struct BlockAccessor<'a> {
97 instance: &'a InstanceLeader,
98 touch: bool,
99}
100
101impl<'a> BlockAccessor<'a> {
102 /// Create a new accessor.
103 pub(crate) fn new(instance: &'a InstanceLeader, touch: bool) -> Self {
104 Self { instance, touch }
105 }
106
107 /// Find and take a block from G2 or G3.
108 ///
109 /// Searches G2 first, then G3 if not found. The block is acquired/removed
110 /// from the pool - caller owns via RAII until dropped.
111 ///
112 /// Returns `None` if the block is not found in either tier.
113 pub fn find(&self, hash: SequenceHash) -> Option<TieredBlock> {
114 // Try G2 first (match_blocks acquires the block)
115 let g2_matches = self.instance.g2_manager.match_blocks(&[hash]);
116 if let Some(block) = g2_matches.into_iter().next() {
117 return Some(TieredBlock::G2(block));
118 }
119
120 // Try G3 if available
121 if let Some(ref g3) = self.instance.g3_manager {
122 let g3_matches = g3.match_blocks(&[hash]);
123 if let Some(block) = g3_matches.into_iter().next() {
124 return Some(TieredBlock::G3(block));
125 }
126 }
127
128 None
129 }
130
131 /// Get the touch setting for this accessor.
132 ///
133 /// When `true`, frequency tracking is updated on block access
134 /// (affects MultiLRU eviction priority).
135 pub fn touch(&self) -> bool {
136 self.touch
137 }
138}
139
140// Safety: BlockAccessor is Send + Sync because:
141// - It only holds a shared reference to InstanceLeader
142// - InstanceLeader contains Arc<BlockManager<T>> which is Send + Sync
143// - All operations use internal locking per call (RwLock in InactivePool)
144// - No mutable state is held between method calls
145unsafe impl Send for BlockAccessor<'_> {}
146unsafe impl Sync for BlockAccessor<'_> {}
147
148/// Context for policy execution with result collection.
149///
150/// Provides access to the `BlockAccessor` for block lookups and a
151/// `yield_item` method for streaming results back to the caller.
152pub struct PolicyContext<'a, T> {
153 pub(crate) accessor: BlockAccessor<'a>,
154 pub(crate) results: Vec<T>,
155}
156
157impl<'a, T> PolicyContext<'a, T> {
158 /// Get access to the block accessor.
159 pub fn accessor(&self) -> &BlockAccessor<'a> {
160 &self.accessor
161 }
162
163 /// Yield a result item.
164 ///
165 /// Items are collected and returned as a `Vec<T>` when the policy completes.
166 pub fn yield_item(&mut self, item: T) {
167 self.results.push(item);
168 }
169
170 /// Yield multiple result items at once.
171 pub fn yield_items(&mut self, items: impl IntoIterator<Item = T>) {
172 self.results.extend(items);
173 }
174}
175
176// =============================================================================
177// TODO: Parallel policy support via rayon::scope
178//
179// Requirements to enable:
180// 1. Add `rayon` to Cargo.toml dependencies
181// 2. Ensure BlockAccessor is truly Send+Sync (verify internal locking is correct)
182// 3. Add feature flag `parallel` to gate this code
183// 4. Test thread-safety of concurrent BlockManager::match_blocks calls
184// 5. Benchmark to ensure parallel overhead is worth it (likely only for large hash sets)
185//
186// The design uses rayon::scope instead of par_chunks because:
187// - par_chunks could split across logical boundaries (e.g., middle of a contiguous run)
188// - rayon::scope lets the policy control parallelism granularity
189// - Policy can identify natural split points (e.g., gaps in position sequence)
190//
191// use std::sync::Mutex;
192// use rayon;
193//
194// /// Context for parallel policy execution.
195// /// Provides thread-safe result collection via Mutex.
196// pub struct ParallelPolicyContext<'a, 's, T> {
197// pub(crate) accessor: &'a BlockAccessor<'a>,
198// pub(crate) scope: &'s rayon::Scope<'s>,
199// pub(crate) results: &'a Mutex<Vec<T>>,
200// }
201//
202// impl<'a, 's, T: Send> ParallelPolicyContext<'a, 's, T> {
203// /// Get access to the block accessor.
204// pub fn accessor(&self) -> &BlockAccessor<'a> {
205// self.accessor
206// }
207//
208// /// Yield a result item (thread-safe).
209// pub fn yield_item(&self, item: T) {
210// self.results.lock().unwrap().push(item);
211// }
212//
213// /// Yield multiple result items (thread-safe, single lock acquisition).
214// pub fn yield_items(&self, items: impl IntoIterator<Item = T>) {
215// self.results.lock().unwrap().extend(items);
216// }
217//
218// /// Spawn parallel work within the rayon scope.
219// ///
220// /// The closure receives the accessor and results mutex, allowing it to
221// /// perform lookups and yield items from a separate thread.
222// ///
223// /// # Example
224// /// ```ignore
225// /// ctx.spawn(|accessor, results| {
226// /// for hash in my_segment {
227// /// if let Some(block) = accessor.find(hash) {
228// /// results.lock().unwrap().push(block);
229// /// }
230// /// }
231// /// });
232// /// ```
233// pub fn spawn<F>(&self, f: F)
234// where
235// F: FnOnce(&BlockAccessor, &Mutex<Vec<T>>) + Send + 'a,
236// {
237// let accessor = self.accessor;
238// let results = self.results;
239// self.scope.spawn(move |_| {
240// f(accessor, results);
241// });
242// }
243// }
244// =============================================================================