kvbm_engine/leader/types.rs
1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use anyhow::Result;
5use futures::future::{BoxFuture, Either, Ready, ready};
6use serde::{Deserialize, Serialize};
7use tokio::sync::{Mutex, watch};
8
9use std::sync::Arc;
10
11use crate::G2;
12use kvbm_logical::blocks::ImmutableBlock;
13
14use super::onboarding::{OnboardingStatus, SessionHandle};
15use super::session::SessionId;
16
17/// Staging mode for matched blocks.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
19pub enum StagingMode {
20 /// Hold blocks in their current tiers (G2 and G3) without staging.
21 /// Session stays alive for future operations.
22 /// Blocks remain on their original instances (local or remote).
23 Hold,
24
25 /// Stage all G3→G2 on local and remote instances.
26 /// No RDMA pulls from remote instances.
27 /// Remote blocks stay in remote G2.
28 /// Session stays alive for future operations.
29 Prepare,
30
31 /// Full staging: G3→G2 everywhere, then RDMA pull remote G2→local G2.
32 /// Session completes after all blocks are in local G2.
33 #[default]
34 Full,
35}
36
37/// Options for find_matches operation.
38#[derive(Debug, Default, Clone, Serialize, Deserialize)]
39pub struct FindMatchesOptions {
40 /// Whether to search remote instances in addition to local search.
41 /// Default: false (local only)
42 pub search_remote: bool,
43
44 /// Staging mode controlling how blocks are staged and session lifecycle.
45 /// Default: StagingMode::Full
46 pub staging_mode: StagingMode,
47}
48
49/// Result of a find_matches operation.
50///
51/// This enum has two variants:
52/// - `Ready`: Immediate result when no async work is needed (local search with Hold mode)
53/// - `AsyncSession`: When staging or remote search is required
54#[derive(Debug)]
55pub enum FindMatchesResult {
56 /// Immediate result - blocks are held in place without staging.
57 ///
58 /// Returned when `search_remote == false` AND `staging_mode == Hold`.
59 /// Blocks remain in their original tiers (G2 or G3) on the local instance.
60 Ready(ReadyResult),
61
62 /// Async session for staging and/or remote search.
63 ///
64 /// Returned when:
65 /// - `search_remote == true` (remote searching enabled)
66 /// - OR `staging_mode` is `Prepare` or `Full` (local/remote staging)
67 AsyncSession(AsyncSessionResult),
68}
69
70/// Immediate result containing matched blocks held directly.
71///
72/// No session is created - blocks are owned directly by this struct (RAII).
73/// Dropping this struct will release the block references.
74#[derive(Debug)]
75pub struct ReadyResult {
76 /// G2 blocks held directly via RAII
77 blocks: Vec<ImmutableBlock<G2>>,
78}
79
80impl ReadyResult {
81 /// Create a new ready result with G2 blocks.
82 pub fn new(blocks: Vec<ImmutableBlock<G2>>) -> Self {
83 Self { blocks }
84 }
85
86 /// Number of G2 blocks held.
87 pub fn g2_count(&self) -> usize {
88 self.blocks.len()
89 }
90
91 /// Take ownership of the G2 blocks.
92 ///
93 /// After calling this, the ReadyResult will be empty.
94 pub fn take_g2_blocks(&mut self) -> Vec<ImmutableBlock<G2>> {
95 std::mem::take(&mut self.blocks)
96 }
97
98 /// Get a reference to the G2 blocks.
99 pub fn blocks(&self) -> &[ImmutableBlock<G2>] {
100 &self.blocks
101 }
102}
103
104/// Async session result for staging and/or remote search operations.
105#[derive(Debug)]
106pub struct AsyncSessionResult {
107 session_id: SessionId,
108 status_rx: watch::Receiver<OnboardingStatus>,
109 blocks: Arc<Mutex<Option<Vec<ImmutableBlock<G2>>>>>,
110 session_handle: Option<SessionHandle>,
111}
112
113impl AsyncSessionResult {
114 /// Create a new async session result.
115 pub fn new(
116 session_id: SessionId,
117 status_rx: watch::Receiver<OnboardingStatus>,
118 blocks: Arc<Mutex<Option<Vec<ImmutableBlock<G2>>>>>,
119 session_handle: Option<SessionHandle>,
120 ) -> Self {
121 Self {
122 session_id,
123 status_rx,
124 blocks,
125 session_handle,
126 }
127 }
128
129 /// Get the session ID for this onboarding operation.
130 pub fn session_id(&self) -> SessionId {
131 self.session_id
132 }
133
134 /// Get the current status of the onboarding operation.
135 pub fn status(&self) -> OnboardingStatus {
136 self.status_rx.borrow().clone()
137 }
138
139 /// Get session handle for deferred operations (Hold/Prepare modes only).
140 ///
141 /// Returns None for StagingMode::Full.
142 pub fn session_handle(&self) -> Option<&SessionHandle> {
143 self.session_handle.as_ref()
144 }
145
146 /// Non-blocking check if blocks are available.
147 ///
148 /// Returns Some(count) if blocks are available, None if still in progress.
149 /// Use wait_for_completion() to take ownership of blocks.
150 pub fn get_blocks_count(&self) -> Option<usize> {
151 self.blocks.try_lock().ok()?.as_ref().map(|v| v.len())
152 }
153
154 /// Wait for the operation to complete and return the matched blocks.
155 ///
156 /// For StagingMode::Full, waits for Complete status.
157 /// For Hold/Prepare modes, waits for terminal state (Holding/Prepared/Complete).
158 ///
159 /// This method returns a future that can be used with tokio::select!.
160 pub fn wait_for_completion(&self) -> BoxFuture<'static, Result<()>> {
161 let mut status_rx = self.status_rx.clone();
162 Box::pin(async move {
163 // Wait for terminal status
164 status_rx
165 .wait_for(|status| {
166 matches!(
167 status,
168 OnboardingStatus::Complete { .. }
169 | OnboardingStatus::Holding { .. }
170 | OnboardingStatus::Prepared { .. }
171 )
172 })
173 .await
174 .map_err(|e| anyhow::anyhow!("failed to wait for completion: {e}"))?;
175
176 Ok(())
177 })
178 }
179}
180
181impl FindMatchesResult {
182 /// Check if this is a ready (immediate) result.
183 pub fn is_ready(&self) -> bool {
184 matches!(self, FindMatchesResult::Ready(_))
185 }
186
187 /// Check if this is an async session result.
188 pub fn is_async(&self) -> bool {
189 matches!(self, FindMatchesResult::AsyncSession(_))
190 }
191
192 /// Get the ready result, if this is a Ready variant.
193 pub fn as_ready(&self) -> Option<&ReadyResult> {
194 match self {
195 FindMatchesResult::Ready(r) => Some(r),
196 FindMatchesResult::AsyncSession(_) => None,
197 }
198 }
199
200 /// Get the ready result mutably, if this is a Ready variant.
201 pub fn as_ready_mut(&mut self) -> Option<&mut ReadyResult> {
202 match self {
203 FindMatchesResult::Ready(r) => Some(r),
204 FindMatchesResult::AsyncSession(_) => None,
205 }
206 }
207
208 /// Get the async session result, if this is an AsyncSession variant.
209 pub fn as_async(&self) -> Option<&AsyncSessionResult> {
210 match self {
211 FindMatchesResult::Ready(_) => None,
212 FindMatchesResult::AsyncSession(a) => Some(a),
213 }
214 }
215
216 /// Get the async session result mutably, if this is an AsyncSession variant.
217 pub fn as_async_mut(&mut self) -> Option<&mut AsyncSessionResult> {
218 match self {
219 FindMatchesResult::Ready(_) => None,
220 FindMatchesResult::AsyncSession(a) => Some(a),
221 }
222 }
223
224 /// Get the number of G2 blocks available or matched.
225 ///
226 /// For Ready: returns the count of blocks held.
227 /// For AsyncSession: returns the count if blocks are available, 0 otherwise.
228 pub fn g2_count(&self) -> usize {
229 match self {
230 FindMatchesResult::Ready(r) => r.g2_count(),
231 FindMatchesResult::AsyncSession(a) => a.get_blocks_count().unwrap_or(0),
232 }
233 }
234
235 /// Take ownership of G2 blocks if available.
236 ///
237 /// For Ready: always succeeds, returns the blocks.
238 /// For AsyncSession: returns Some if blocks are available and lock succeeds.
239 pub fn take_g2_blocks(&mut self) -> Option<Vec<ImmutableBlock<G2>>> {
240 match self {
241 FindMatchesResult::Ready(r) => Some(r.take_g2_blocks()),
242 FindMatchesResult::AsyncSession(a) => a.blocks.try_lock().ok()?.take(),
243 }
244 }
245
246 pub fn session_id(&self) -> Option<SessionId> {
247 match self {
248 FindMatchesResult::Ready(_) => None,
249 FindMatchesResult::AsyncSession(a) => Some(a.session_id()),
250 }
251 }
252
253 /// Wait for the operation to complete.
254 ///
255 /// For Ready variant: returns immediately with Ok(()).
256 /// For AsyncSession variant: waits for terminal status (Complete/Holding/Prepared).
257 ///
258 /// Returns an Either future that can be used with tokio::select!.
259 pub fn wait_for_completion(&self) -> Either<Ready<Result<()>>, BoxFuture<'static, Result<()>>> {
260 match self {
261 FindMatchesResult::Ready(_) => Either::Left(ready(Ok(()))),
262 FindMatchesResult::AsyncSession(async_session) => {
263 Either::Right(async_session.wait_for_completion())
264 }
265 }
266 }
267}