Skip to main content

kvbm_engine/leader/session/
endpoint.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! SessionEndpoint: Point-to-point session primitive.
5//!
6//! This is the core building block for unified sessions. It handles:
7//! - State machine (control role + attachment state + phase)
8//! - Message receive channel for incoming [`SessionMessage`]
9//! - State publication via watch channel for observers
10//! - Transport for sending messages to peer
11//!
12//! It does NOT handle:
13//! - Block holding (use [`BlockHolder`] for that)
14//! - Staging logic (caller invokes staging)
15//! - Multi-peer orchestration (that's [`InitiatorSession`]'s job)
16//!
17//! # Usage
18//!
19//! ```ignore
20//! // Create an endpoint
21//! let (tx, rx) = mpsc::channel(32);
22//! let endpoint = SessionEndpoint::new(
23//!     SessionId::new_v4(),
24//!     my_instance_id,
25//!     transport,
26//!     rx,
27//! );
28//!
29//! // Process messages
30//! while let Some(msg) = endpoint.recv().await {
31//!     match msg {
32//!         SessionMessage::Attach { peer, as_role, .. } => {
33//!             endpoint.accept_attachment(peer, as_role);
34//!             // ... handle attachment
35//!         }
36//!         // ... other messages
37//!     }
38//! }
39//! ```
40
41use std::sync::Arc;
42use tokio::sync::{mpsc, watch};
43
44use anyhow::Result;
45
46use crate::InstanceId;
47
48use super::{
49    SessionId,
50    messages::{BlockInfo, SessionMessage, SessionStateSnapshot},
51    state::{AttachmentState, ControlRole, SessionPhase},
52    transport::MessageTransport,
53};
54
55/// A point-to-point session endpoint.
56///
57/// This is the common building block for all session types. It encapsulates:
58/// - Identity (session_id, instance_id)
59/// - State machine (control role, attachment, phase)
60/// - Communication (message receive, state publication)
61///
62/// The endpoint starts in `Neutral + Unattached` state by default.
63pub struct SessionEndpoint {
64    session_id: SessionId,
65    instance_id: InstanceId,
66
67    // State
68    control_role: ControlRole,
69    attachment: AttachmentState,
70    phase: SessionPhase,
71
72    // Communication
73    transport: Arc<MessageTransport>,
74    msg_rx: mpsc::Receiver<SessionMessage>,
75    state_tx: watch::Sender<SessionStateSnapshot>,
76}
77
78impl SessionEndpoint {
79    /// Create a new endpoint in `Neutral + Unattached` state.
80    pub fn new(
81        session_id: SessionId,
82        instance_id: InstanceId,
83        transport: Arc<MessageTransport>,
84        msg_rx: mpsc::Receiver<SessionMessage>,
85    ) -> Self {
86        let initial_state = SessionStateSnapshot {
87            phase: SessionPhase::default(),
88            control_role: ControlRole::default(),
89            g2_blocks: Vec::new(),
90            g3_pending: 0,
91            ready_layer_range: None,
92        };
93        let (state_tx, _) = watch::channel(initial_state);
94
95        Self {
96            session_id,
97            instance_id,
98            control_role: ControlRole::default(),
99            attachment: AttachmentState::default(),
100            phase: SessionPhase::default(),
101            transport,
102            msg_rx,
103            state_tx,
104        }
105    }
106
107    /// Create a new endpoint with pre-attached state.
108    ///
109    /// Used when creating a session that is already attached to a peer
110    /// (e.g., ResponderSession which is pre-attached to the initiator).
111    pub fn new_attached(
112        session_id: SessionId,
113        instance_id: InstanceId,
114        peer: InstanceId,
115        role: ControlRole,
116        phase: SessionPhase,
117        transport: Arc<MessageTransport>,
118        msg_rx: mpsc::Receiver<SessionMessage>,
119    ) -> Self {
120        let initial_state = SessionStateSnapshot {
121            phase,
122            control_role: role,
123            g2_blocks: Vec::new(),
124            g3_pending: 0,
125            ready_layer_range: None,
126        };
127        let (state_tx, _) = watch::channel(initial_state);
128
129        Self {
130            session_id,
131            instance_id,
132            control_role: role,
133            attachment: AttachmentState::Attached { peer },
134            phase,
135            transport,
136            msg_rx,
137            state_tx,
138        }
139    }
140
141    // =========================================================================
142    // State Accessors
143    // =========================================================================
144
145    /// Get the session ID.
146    pub fn session_id(&self) -> SessionId {
147        self.session_id
148    }
149
150    /// Get this endpoint's instance ID.
151    pub fn instance_id(&self) -> InstanceId {
152        self.instance_id
153    }
154
155    /// Get the current control role.
156    pub fn control_role(&self) -> ControlRole {
157        self.control_role
158    }
159
160    /// Check if a peer is attached.
161    pub fn is_attached(&self) -> bool {
162        self.attachment.is_attached()
163    }
164
165    /// Get the attached peer's instance ID.
166    pub fn peer(&self) -> Option<InstanceId> {
167        self.attachment.peer()
168    }
169
170    /// Get the current session phase.
171    pub fn phase(&self) -> SessionPhase {
172        self.phase
173    }
174
175    /// Check if the session is in a terminal state.
176    pub fn is_complete(&self) -> bool {
177        self.phase.is_terminal()
178    }
179
180    // =========================================================================
181    // State Transitions
182    // =========================================================================
183
184    /// Set the session phase.
185    pub fn set_phase(&mut self, phase: SessionPhase) {
186        self.phase = phase;
187    }
188
189    /// Set the control role.
190    ///
191    /// Use this for direct role changes (e.g., when processing YieldControl
192    /// or AcquireControl messages).
193    pub fn set_control_role(&mut self, role: ControlRole) {
194        self.control_role = role;
195    }
196
197    /// Accept an attachment from a peer.
198    ///
199    /// Transitions from `Unattached` to `Attached` and sets the control role.
200    pub fn accept_attachment(&mut self, peer: InstanceId, role: ControlRole) {
201        self.attachment = AttachmentState::Attached { peer };
202        self.control_role = role;
203    }
204
205    /// Detach from the current peer.
206    ///
207    /// Returns the detached peer's instance ID if there was one.
208    pub fn detach(&mut self) -> Option<InstanceId> {
209        let peer = self.attachment.peer();
210        self.attachment = AttachmentState::Unattached;
211        self.control_role = ControlRole::Neutral;
212        peer
213    }
214
215    /// Yield control to peer.
216    ///
217    /// Transitions from `Controller` to `Neutral`.
218    /// Returns `Err` if not currently `Controller`.
219    pub fn yield_control(&mut self) -> Result<()> {
220        if self.control_role != ControlRole::Controller {
221            anyhow::bail!("Cannot yield control: not currently Controller");
222        }
223        self.control_role = ControlRole::Neutral;
224        Ok(())
225    }
226
227    /// Acquire control from peer.
228    ///
229    /// Transitions from `Neutral` or `Controllee` to `Controller`.
230    /// The peer must be in `Neutral` state for this to succeed.
231    pub fn acquire_control(&mut self) -> Result<()> {
232        if self.control_role == ControlRole::Controller {
233            // Already controller, no-op
234            return Ok(());
235        }
236        self.control_role = ControlRole::Controller;
237        Ok(())
238    }
239
240    /// Handle a peer yielding control to us.
241    ///
242    /// Transitions to `Neutral` (peer has yielded, we can now acquire if we want).
243    pub fn peer_yielded_control(&mut self) {
244        // When peer yields, we stay in our current role or become neutral
245        // The peer is now Neutral, so we can acquire if desired
246        if self.control_role == ControlRole::Controllee {
247            self.control_role = ControlRole::Neutral;
248        }
249    }
250
251    /// Handle a peer acquiring control.
252    ///
253    /// Transitions from `Neutral` to `Controllee`.
254    pub fn peer_acquired_control(&mut self) -> Result<()> {
255        if self.control_role == ControlRole::Controller {
256            anyhow::bail!("Cannot transition to Controllee: currently Controller");
257        }
258        self.control_role = ControlRole::Controllee;
259        Ok(())
260    }
261
262    // =========================================================================
263    // Message Handling
264    // =========================================================================
265
266    /// Receive the next message.
267    ///
268    /// Returns `None` when the channel is closed.
269    pub async fn recv(&mut self) -> Option<SessionMessage> {
270        self.msg_rx.recv().await
271    }
272
273    /// Try to receive a message without blocking.
274    pub fn try_recv(&mut self) -> Result<SessionMessage, mpsc::error::TryRecvError> {
275        self.msg_rx.try_recv()
276    }
277
278    // =========================================================================
279    // Outbound Messages
280    // =========================================================================
281
282    /// Send an attach message to a peer.
283    pub async fn send_attach(&self, peer: InstanceId, as_role: ControlRole) -> Result<()> {
284        let msg = SessionMessage::Attach {
285            peer: self.instance_id,
286            session_id: self.session_id,
287            as_role,
288        };
289        self.send_to(peer, msg).await
290    }
291
292    /// Send a detach message to the current peer.
293    pub async fn send_detach(&self) -> Result<()> {
294        let peer = self
295            .peer()
296            .ok_or_else(|| anyhow::anyhow!("Cannot detach: not attached"))?;
297
298        let msg = SessionMessage::Detach {
299            peer: self.instance_id,
300            session_id: self.session_id,
301        };
302        self.send_to(peer, msg).await
303    }
304
305    /// Send yield control message to peer.
306    pub async fn send_yield_control(&self) -> Result<()> {
307        let peer = self
308            .peer()
309            .ok_or_else(|| anyhow::anyhow!("Cannot yield: not attached"))?;
310
311        let msg = SessionMessage::YieldControl {
312            peer: self.instance_id,
313            session_id: self.session_id,
314        };
315        self.send_to(peer, msg).await
316    }
317
318    /// Send acquire control message to peer.
319    pub async fn send_acquire_control(&self) -> Result<()> {
320        let peer = self
321            .peer()
322            .ok_or_else(|| anyhow::anyhow!("Cannot acquire: not attached"))?;
323
324        let msg = SessionMessage::AcquireControl {
325            peer: self.instance_id,
326            session_id: self.session_id,
327        };
328        self.send_to(peer, msg).await
329    }
330
331    /// Send a message to a specific peer.
332    pub async fn send_to(&self, peer: InstanceId, msg: SessionMessage) -> Result<()> {
333        self.transport.send_session(peer, msg).await
334    }
335
336    /// Send a message to the currently attached peer.
337    pub async fn send(&self, msg: SessionMessage) -> Result<()> {
338        let peer = self
339            .peer()
340            .ok_or_else(|| anyhow::anyhow!("Cannot send: not attached"))?;
341        self.send_to(peer, msg).await
342    }
343
344    // =========================================================================
345    // State Publication
346    // =========================================================================
347
348    /// Publish the current state snapshot.
349    ///
350    /// This updates all watchers with the new state.
351    pub fn publish_state(&self, g2_blocks: Vec<BlockInfo>, g3_pending: usize) {
352        let _ = self.state_tx.send(SessionStateSnapshot {
353            phase: self.phase,
354            control_role: self.control_role,
355            g2_blocks,
356            g3_pending,
357            ready_layer_range: None,
358        });
359    }
360
361    /// Publish state with layer range information.
362    ///
363    /// Used for layerwise transfer where specific layers are ready.
364    pub fn publish_state_with_layer_range(
365        &self,
366        g2_blocks: Vec<BlockInfo>,
367        g3_pending: usize,
368        layer_range: Option<std::ops::Range<usize>>,
369    ) {
370        let _ = self.state_tx.send(SessionStateSnapshot {
371            phase: self.phase,
372            control_role: self.control_role,
373            g2_blocks,
374            g3_pending,
375            ready_layer_range: layer_range,
376        });
377    }
378
379    /// Get a receiver for state updates.
380    ///
381    /// Returns a watch receiver that will receive state snapshots
382    /// whenever they are published.
383    pub fn state_rx(&self) -> watch::Receiver<SessionStateSnapshot> {
384        self.state_tx.subscribe()
385    }
386
387    /// Get the transport for direct access (for legacy interop).
388    pub fn transport(&self) -> &Arc<MessageTransport> {
389        &self.transport
390    }
391}
392
393/// Channel type for sending SessionMessages to an endpoint.
394pub type SessionMessageTx = mpsc::Sender<SessionMessage>;
395
396/// Create a new session message channel.
397pub fn session_message_channel(
398    buffer: usize,
399) -> (SessionMessageTx, mpsc::Receiver<SessionMessage>) {
400    mpsc::channel(buffer)
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use dashmap::DashMap;
407
408    fn create_test_transport() -> Arc<MessageTransport> {
409        Arc::new(MessageTransport::local(
410            Arc::new(DashMap::new()),
411            Arc::new(DashMap::new()),
412        ))
413    }
414
415    #[test]
416    fn test_endpoint_initial_state() {
417        let (_, rx) = mpsc::channel(32);
418        let transport = create_test_transport();
419        let session_id = SessionId::new_v4();
420        let instance_id = InstanceId::new_v4();
421
422        let endpoint = SessionEndpoint::new(session_id, instance_id, transport, rx);
423
424        assert_eq!(endpoint.session_id(), session_id);
425        assert_eq!(endpoint.instance_id(), instance_id);
426        assert_eq!(endpoint.control_role(), ControlRole::Neutral);
427        assert!(!endpoint.is_attached());
428        assert!(endpoint.peer().is_none());
429        assert_eq!(endpoint.phase(), SessionPhase::Searching);
430        assert!(!endpoint.is_complete());
431    }
432
433    #[test]
434    fn test_endpoint_attachment() {
435        let (_, rx) = mpsc::channel(32);
436        let transport = create_test_transport();
437        let session_id = SessionId::new_v4();
438        let instance_id = InstanceId::new_v4();
439        let peer_id = InstanceId::new_v4();
440
441        let mut endpoint = SessionEndpoint::new(session_id, instance_id, transport, rx);
442
443        // Accept attachment
444        endpoint.accept_attachment(peer_id, ControlRole::Controllee);
445
446        assert!(endpoint.is_attached());
447        assert_eq!(endpoint.peer(), Some(peer_id));
448        assert_eq!(endpoint.control_role(), ControlRole::Controllee);
449
450        // Detach
451        let detached = endpoint.detach();
452        assert_eq!(detached, Some(peer_id));
453        assert!(!endpoint.is_attached());
454        assert_eq!(endpoint.control_role(), ControlRole::Neutral);
455    }
456
457    #[test]
458    fn test_endpoint_pre_attached() {
459        let (_, rx) = mpsc::channel(32);
460        let transport = create_test_transport();
461        let session_id = SessionId::new_v4();
462        let instance_id = InstanceId::new_v4();
463        let peer_id = InstanceId::new_v4();
464
465        let endpoint = SessionEndpoint::new_attached(
466            session_id,
467            instance_id,
468            peer_id,
469            ControlRole::Controller,
470            SessionPhase::Holding,
471            transport,
472            rx,
473        );
474
475        assert!(endpoint.is_attached());
476        assert_eq!(endpoint.peer(), Some(peer_id));
477        assert_eq!(endpoint.control_role(), ControlRole::Controller);
478        assert_eq!(endpoint.phase(), SessionPhase::Holding);
479    }
480
481    #[test]
482    fn test_control_transitions() {
483        let (_, rx) = mpsc::channel(32);
484        let transport = create_test_transport();
485        let session_id = SessionId::new_v4();
486        let instance_id = InstanceId::new_v4();
487        let peer_id = InstanceId::new_v4();
488
489        let mut endpoint = SessionEndpoint::new(session_id, instance_id, transport, rx);
490        endpoint.accept_attachment(peer_id, ControlRole::Controller);
491
492        // Yield control
493        assert!(endpoint.yield_control().is_ok());
494        assert_eq!(endpoint.control_role(), ControlRole::Neutral);
495
496        // Can't yield again
497        assert!(endpoint.yield_control().is_err());
498
499        // Acquire control
500        assert!(endpoint.acquire_control().is_ok());
501        assert_eq!(endpoint.control_role(), ControlRole::Controller);
502    }
503
504    #[test]
505    fn test_phase_transitions() {
506        let (_, rx) = mpsc::channel(32);
507        let transport = create_test_transport();
508        let session_id = SessionId::new_v4();
509        let instance_id = InstanceId::new_v4();
510
511        let mut endpoint = SessionEndpoint::new(session_id, instance_id, transport, rx);
512
513        assert_eq!(endpoint.phase(), SessionPhase::Searching);
514        assert!(!endpoint.is_complete());
515
516        endpoint.set_phase(SessionPhase::Holding);
517        assert_eq!(endpoint.phase(), SessionPhase::Holding);
518
519        endpoint.set_phase(SessionPhase::Complete);
520        assert!(endpoint.is_complete());
521    }
522
523    #[tokio::test]
524    async fn test_state_publication() {
525        let (_, rx) = mpsc::channel(32);
526        let transport = create_test_transport();
527        let session_id = SessionId::new_v4();
528        let instance_id = InstanceId::new_v4();
529
530        let endpoint = SessionEndpoint::new(session_id, instance_id, transport, rx);
531        let mut state_rx = endpoint.state_rx();
532
533        // Initial state
534        let state = state_rx.borrow().clone();
535        assert_eq!(state.phase, SessionPhase::Searching);
536        assert_eq!(state.g2_blocks.len(), 0);
537
538        // Publish new state
539        endpoint.publish_state(vec![], 5);
540
541        // Wait for change
542        state_rx.changed().await.unwrap();
543        let state = state_rx.borrow().clone();
544        assert_eq!(state.g3_pending, 5);
545    }
546}