Skip to main content

kvbm_engine/leader/session/
messages.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::ops::Range;
5use std::sync::Arc;
6
7use serde::{Deserialize, Serialize};
8
9use crate::{BlockId, G2, InstanceId, SequenceHash};
10use kvbm_logical::blocks::ImmutableBlock;
11use kvbm_physical::manager::LayoutHandle;
12
13use super::SessionId;
14
15/// Messages exchanged between leaders during onboarding sessions.
16///
17/// Phase 2 protocol (G2-only):
18/// 1. Initiator sends CreateSession to multiple responders
19/// 2. Each responder searches local G2 and sends G2Results back
20/// 3. Initiator applies first-responder-wins and sends HoldBlocks to each
21/// 4. Responders send Acknowledged after releasing unwanted blocks
22///
23/// Phase 3 protocol (G3 staging):
24/// 5. Responders search G3 and send G3Results
25/// 6. Initiator sends StageBlocks with blocks to stage G3->G2
26/// 7. Responders stage blocks and send BlocksReady when complete
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub enum OnboardMessage {
29    /// Initiator creates a new onboarding session.
30    CreateSession {
31        requester: InstanceId,
32        session_id: SessionId,
33        sequence_hashes: Vec<SequenceHash>,
34    },
35
36    /// Responder signals local search (G2 and G3) is complete.
37    SearchComplete {
38        responder: InstanceId,
39        session_id: SessionId,
40    },
41
42    /// Responder reports G2 search results.
43    /// - sequence_hashes: ordered list of matched sequence hashes
44    /// - block_ids: parallel list of block IDs (can be zipped with sequence_hashes)
45    G2Results {
46        responder: InstanceId,
47        session_id: SessionId,
48        sequence_hashes: Vec<SequenceHash>,
49        block_ids: Vec<BlockId>,
50    },
51
52    /// Responder reports G3 search results.
53    /// - sequence_hashes: ordered list of matched sequence hashes (no block IDs)
54    G3Results {
55        responder: InstanceId,
56        session_id: SessionId,
57        sequence_hashes: Vec<SequenceHash>,
58    },
59
60    /// Initiator tells responder which sequence hashes to hold/drop.
61    /// Works across G2 and G3 tiers.
62    HoldBlocks {
63        requester: InstanceId,
64        session_id: SessionId,
65        hold_hashes: Vec<SequenceHash>,
66        drop_hashes: Vec<SequenceHash>,
67    },
68
69    /// Initiator tells responder which G3 sequence hashes to stage to G2.
70    /// Any G3 blocks with these hashes should be staged to G2.
71    StageBlocks {
72        requester: InstanceId,
73        session_id: SessionId,
74        stage_hashes: Vec<SequenceHash>,
75    },
76
77    /// Responder reports newly staged blocks are ready in G2 (after G3->G2 staging).
78    /// Only reports blocks that were just staged, not all G2 blocks.
79    /// - sequence_hashes: newly staged blocks
80    /// - block_ids: parallel to sequence_hashes
81    BlocksReady {
82        responder: InstanceId,
83        session_id: SessionId,
84        sequence_hashes: Vec<SequenceHash>,
85        block_ids: Vec<BlockId>,
86    },
87
88    /// Responder acknowledges hold/drop request.
89    Acknowledged {
90        responder: InstanceId,
91        session_id: SessionId,
92    },
93
94    /// Initiator tells responder to release specific sequence hashes that weren't selected.
95    /// Works across G2 and G3 tiers.
96    ReleaseBlocks {
97        requester: InstanceId,
98        session_id: SessionId,
99        release_hashes: Vec<SequenceHash>,
100    },
101
102    /// Initiator tells responder session is complete, responder can cleanup.
103    CloseSession {
104        requester: InstanceId,
105        session_id: SessionId,
106    },
107
108    // =========================================================================
109    // G4/Object Storage Messages (Internal - not sent over network)
110    // =========================================================================
111    /// G4 search results from object storage `has_blocks`.
112    ///
113    /// Internal message sent via mpsc channel from the G4 search task
114    /// to the initiator session. Contains hashes found in object storage
115    /// with their sizes.
116    G4Results {
117        session_id: SessionId,
118        /// Hashes found in G4 with their sizes in bytes
119        found_hashes: Vec<(SequenceHash, usize)>,
120    },
121
122    /// G4 load completion results from object storage `get_blocks`.
123    ///
124    /// Internal message sent via mpsc channel from the G4 load task
125    /// to the initiator session. Contains per-block success/failure.
126    G4LoadComplete {
127        session_id: SessionId,
128        /// Successfully loaded hashes
129        success: Vec<SequenceHash>,
130        /// Failed hashes with error messages
131        failures: Vec<(SequenceHash, String)>,
132        /// Successfully loaded and registered G2 blocks.
133        /// These are ready to be added to local_g2_blocks.
134        /// Wrapped in Arc for Clone derivation (internal message only).
135        #[serde(skip)]
136        blocks: Arc<Vec<ImmutableBlock<G2>>>,
137    },
138    // TODO: Add heartbeat/TTL mechanism for handling unresponsive initiators
139    // Heartbeat {
140    //     requester: InstanceId,
141    //     session_id: SessionId,
142    //     timestamp: u64,
143    // },
144    // TTL resets with each heartbeat. If TTL expires:
145    // - Responder releases all held blocks
146    // - Responder cleans up session state
147    // - Session task exits
148}
149
150/// Represents a block match found during search.
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct BlockMatch {
153    pub sequence_hash: SequenceHash,
154    pub block_id: BlockId,
155}
156
157impl OnboardMessage {
158    /// Extract the session ID from any message variant.
159    pub fn session_id(&self) -> SessionId {
160        match self {
161            OnboardMessage::CreateSession { session_id, .. }
162            | OnboardMessage::SearchComplete { session_id, .. }
163            | OnboardMessage::G2Results { session_id, .. }
164            | OnboardMessage::G3Results { session_id, .. }
165            | OnboardMessage::HoldBlocks { session_id, .. }
166            | OnboardMessage::StageBlocks { session_id, .. }
167            | OnboardMessage::BlocksReady { session_id, .. }
168            | OnboardMessage::Acknowledged { session_id, .. }
169            | OnboardMessage::ReleaseBlocks { session_id, .. }
170            | OnboardMessage::CloseSession { session_id, .. }
171            | OnboardMessage::G4Results { session_id, .. }
172            | OnboardMessage::G4LoadComplete { session_id, .. } => *session_id,
173        }
174    }
175
176    /// Extract the requester/responder instance ID from the message.
177    ///
178    /// # Panics
179    /// Panics if called on G4 messages (internal only, no instance ID).
180    pub fn instance_id(&self) -> InstanceId {
181        match self {
182            OnboardMessage::CreateSession { requester, .. }
183            | OnboardMessage::HoldBlocks { requester, .. }
184            | OnboardMessage::StageBlocks { requester, .. }
185            | OnboardMessage::ReleaseBlocks { requester, .. }
186            | OnboardMessage::CloseSession { requester, .. } => *requester,
187            OnboardMessage::SearchComplete { responder, .. }
188            | OnboardMessage::G2Results { responder, .. }
189            | OnboardMessage::G3Results { responder, .. }
190            | OnboardMessage::BlocksReady { responder, .. }
191            | OnboardMessage::Acknowledged { responder, .. } => *responder,
192            OnboardMessage::G4Results { .. } | OnboardMessage::G4LoadComplete { .. } => {
193                panic!("G4 messages are internal and do not have an instance ID")
194            }
195        }
196    }
197
198    /// Get the variant name as a string for logging.
199    pub fn variant_name(&self) -> &'static str {
200        match self {
201            OnboardMessage::CreateSession { .. } => "CreateSession",
202            OnboardMessage::SearchComplete { .. } => "SearchComplete",
203            OnboardMessage::G2Results { .. } => "G2Results",
204            OnboardMessage::G3Results { .. } => "G3Results",
205            OnboardMessage::HoldBlocks { .. } => "HoldBlocks",
206            OnboardMessage::StageBlocks { .. } => "StageBlocks",
207            OnboardMessage::BlocksReady { .. } => "BlocksReady",
208            OnboardMessage::Acknowledged { .. } => "Acknowledged",
209            OnboardMessage::ReleaseBlocks { .. } => "ReleaseBlocks",
210            OnboardMessage::CloseSession { .. } => "CloseSession",
211            OnboardMessage::G4Results { .. } => "G4Results",
212            OnboardMessage::G4LoadComplete { .. } => "G4LoadComplete",
213        }
214    }
215}
216
217// =============================================================================
218// Unified Session Protocol
219// =============================================================================
220//
221// These types support the unified session model where sessions can dynamically
222// transition between control roles.
223
224use super::state::{ControlRole, SessionPhase};
225
226/// Unified session message protocol.
227///
228/// Unified, bidirectional protocol that supports dynamic control transfer.
229///
230/// # Protocol Overview
231///
232/// 1. **Connection**: `Attach`/`Detach` for peer management
233/// 2. **Control Transfer**: `YieldControl`/`AcquireControl` for bidirectional role changes
234/// 3. **Block Operations**: Commands from controller to controllee
235/// 4. **State Sync**: Responses from controllee to controller
236/// 5. **Lifecycle**: `Close`/`Error` for termination
237#[derive(Debug, Clone, Serialize, Deserialize)]
238pub enum SessionMessage {
239    // =========================================================================
240    // Connection Management
241    // =========================================================================
242    /// Attach to a session as a specific role.
243    ///
244    /// Sent by a peer to establish the session relationship.
245    Attach {
246        /// The instance ID of the attaching peer.
247        peer: InstanceId,
248        /// The session to attach to.
249        session_id: SessionId,
250        /// The role this peer will assume (typically `Controllee` or `Controller`).
251        as_role: ControlRole,
252    },
253
254    /// Detach from a session.
255    ///
256    /// Graceful disconnection. The session may continue if other peers are attached.
257    Detach {
258        /// The instance ID of the detaching peer.
259        peer: InstanceId,
260        /// The session to detach from.
261        session_id: SessionId,
262    },
263
264    // =========================================================================
265    // Control Transfer (Bidirectional)
266    // =========================================================================
267    /// Yield control to peer.
268    ///
269    /// The sender transitions from `Controller` to `Neutral`.
270    /// The receiver (if in `Controllee`) can then `AcquireControl` or remain passive.
271    YieldControl {
272        /// The instance ID of the yielding peer.
273        peer: InstanceId,
274        /// The session.
275        session_id: SessionId,
276    },
277
278    /// Acquire control from peer.
279    ///
280    /// The sender attempts to become `Controller`.
281    /// Valid when sender is `Neutral` or `Controllee` and peer is `Neutral`.
282    AcquireControl {
283        /// The instance ID of the peer acquiring control.
284        peer: InstanceId,
285        /// The session.
286        session_id: SessionId,
287    },
288
289    // =========================================================================
290    // Block Operations (Controller → Controllee)
291    // =========================================================================
292    /// Trigger staging of blocks (e.g., G3→G2).
293    TriggerStaging {
294        /// The session.
295        session_id: SessionId,
296    },
297
298    /// Request that specific blocks be held (kept alive).
299    HoldBlocks {
300        /// The session.
301        session_id: SessionId,
302        /// Sequence hashes of blocks to hold.
303        hold_hashes: Vec<SequenceHash>,
304    },
305
306    /// Release specific blocks (they can now be evicted).
307    ReleaseBlocks {
308        /// The session.
309        session_id: SessionId,
310        /// Sequence hashes of blocks to release.
311        release_hashes: Vec<SequenceHash>,
312    },
313
314    /// Notify that blocks have been pulled via RDMA.
315    ///
316    /// The controllee can release these blocks from its hold.
317    BlocksPulled {
318        /// The session.
319        session_id: SessionId,
320        /// Sequence hashes of blocks that were pulled.
321        pulled_hashes: Vec<SequenceHash>,
322    },
323
324    // =========================================================================
325    // State Synchronization (Controllee → Controller)
326    // =========================================================================
327    /// Full state snapshot.
328    ///
329    /// Sent after attachment and periodically on state changes.
330    StateResponse {
331        /// The session.
332        session_id: SessionId,
333        /// Complete state snapshot.
334        state: SessionStateSnapshot,
335    },
336
337    /// Notification that blocks have been staged.
338    ///
339    /// This message supports layerwise transfer by optionally specifying
340    /// which layer range is ready. When `layer_range` is `None`, all layers
341    /// of the staged blocks are ready for transfer.
342    BlocksStaged {
343        /// The session.
344        session_id: SessionId,
345        /// Newly staged blocks (now in target tier).
346        staged_blocks: Vec<BlockInfo>,
347        /// Count of blocks remaining to stage.
348        remaining: usize,
349        /// Layer range that is ready for transfer.
350        ///
351        /// - `None`: All layers are ready (default behavior)
352        /// - `Some(0..1)`: Only layer 0 is ready
353        /// - `Some(0..60)`: Layers 0-59 are ready
354        ///
355        /// This enables layerwise streaming where the sender computes
356        /// layer-by-layer and notifies the receiver as each layer completes.
357        layer_range: Option<Range<usize>>,
358    },
359
360    // =========================================================================
361    // Lifecycle
362    // =========================================================================
363    /// Close the session gracefully.
364    Close {
365        /// The session.
366        session_id: SessionId,
367    },
368
369    /// Report an error.
370    Error {
371        /// The session.
372        session_id: SessionId,
373        /// Error description.
374        message: String,
375    },
376}
377
378impl SessionMessage {
379    /// Extract the session ID from any message variant.
380    pub fn session_id(&self) -> SessionId {
381        match self {
382            SessionMessage::Attach { session_id, .. }
383            | SessionMessage::Detach { session_id, .. }
384            | SessionMessage::YieldControl { session_id, .. }
385            | SessionMessage::AcquireControl { session_id, .. }
386            | SessionMessage::TriggerStaging { session_id, .. }
387            | SessionMessage::HoldBlocks { session_id, .. }
388            | SessionMessage::ReleaseBlocks { session_id, .. }
389            | SessionMessage::BlocksPulled { session_id, .. }
390            | SessionMessage::StateResponse { session_id, .. }
391            | SessionMessage::BlocksStaged { session_id, .. }
392            | SessionMessage::Close { session_id, .. }
393            | SessionMessage::Error { session_id, .. } => *session_id,
394        }
395    }
396
397    /// Extract the peer instance ID if present.
398    pub fn peer(&self) -> Option<InstanceId> {
399        match self {
400            SessionMessage::Attach { peer, .. }
401            | SessionMessage::Detach { peer, .. }
402            | SessionMessage::YieldControl { peer, .. }
403            | SessionMessage::AcquireControl { peer, .. } => Some(*peer),
404            _ => None,
405        }
406    }
407
408    /// Check if this is a control command (sent by controller).
409    pub fn is_control_command(&self) -> bool {
410        matches!(
411            self,
412            SessionMessage::TriggerStaging { .. }
413                | SessionMessage::HoldBlocks { .. }
414                | SessionMessage::ReleaseBlocks { .. }
415                | SessionMessage::BlocksPulled { .. }
416        )
417    }
418
419    /// Check if this is a state response (sent by controllee).
420    pub fn is_state_response(&self) -> bool {
421        matches!(
422            self,
423            SessionMessage::StateResponse { .. } | SessionMessage::BlocksStaged { .. }
424        )
425    }
426
427    /// Get the variant name as a string for logging.
428    pub fn variant_name(&self) -> &'static str {
429        match self {
430            SessionMessage::Attach { .. } => "Attach",
431            SessionMessage::Detach { .. } => "Detach",
432            SessionMessage::YieldControl { .. } => "YieldControl",
433            SessionMessage::AcquireControl { .. } => "AcquireControl",
434            SessionMessage::TriggerStaging { .. } => "TriggerStaging",
435            SessionMessage::HoldBlocks { .. } => "HoldBlocks",
436            SessionMessage::ReleaseBlocks { .. } => "ReleaseBlocks",
437            SessionMessage::BlocksPulled { .. } => "BlocksPulled",
438            SessionMessage::StateResponse { .. } => "StateResponse",
439            SessionMessage::BlocksStaged { .. } => "BlocksStaged",
440            SessionMessage::Close { .. } => "Close",
441            SessionMessage::Error { .. } => "Error",
442        }
443    }
444}
445
446/// Complete session state snapshot.
447///
448/// Sent in `SessionMessage::StateResponse` to provide the controller
449/// with full visibility into the controllee's state.
450#[derive(Debug, Clone, Serialize, Deserialize)]
451pub struct SessionStateSnapshot {
452    /// Current session phase.
453    pub phase: SessionPhase,
454    /// Current control role of the sender.
455    pub control_role: ControlRole,
456    /// Blocks currently in G2 (ready for RDMA pull).
457    pub g2_blocks: Vec<BlockInfo>,
458    /// Count of blocks pending staging to G2.
459    pub g3_pending: usize,
460    /// Layer range that is ready for transfer.
461    ///
462    /// - `None`: All layers are ready (or not applicable)
463    /// - `Some(0..1)`: Only layer 0 is ready
464    /// - `Some(0..60)`: Layers 0-59 are ready
465    ///
466    /// This is updated when receiving `BlocksStaged` messages with `layer_range`.
467    /// The controller can use this to know which layers can be pulled.
468    #[serde(default)]
469    pub ready_layer_range: Option<Range<usize>>,
470}
471
472/// Block information for session messages.
473///
474/// Contains the metadata needed to identify and transfer a block.
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct BlockInfo {
477    /// Physical block ID in the layout.
478    pub block_id: BlockId,
479    /// Logical sequence hash.
480    pub sequence_hash: SequenceHash,
481    /// Layout handle for RDMA operations.
482    pub layout_handle: LayoutHandle,
483}