Skip to main content

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}