Skip to main content

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// =============================================================================