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}