Skip to main content

kvbm_engine/leader/session/
responder.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 tokio::sync::mpsc;
6
7use std::collections::HashSet;
8use std::sync::Arc;
9
10use crate::{BlockId, G2, G3, InstanceId, SequenceHash, worker::group::ParallelWorkers};
11use kvbm_logical::manager::BlockManager;
12
13use super::{BlockHolder, SessionId, messages::OnboardMessage, transport::MessageTransport};
14
15/// Responder-side session for handling block onboarding requests.
16///
17/// Lifecycle:
18/// 1. Spawned when receiving CreateSession
19/// 2. Searches local G2 for matches
20/// 3. Holds `ImmutableBlock<G2>` references (RAII)
21/// 4. Sends G2Results immediately
22/// 5. Searches local G3 for remaining matches (if G3 available)
23/// 6. Sends G3Results
24/// 7. Receives HoldBlocks and filters held G2 blocks
25/// 8. Receives StageBlocks and executes G3->G2 transfers
26/// 9. Sends BlocksReady when staging completes
27/// 10. Sends Acknowledged
28/// 11. Completes and drops (releases blocks)
29pub struct ResponderSession {
30    session_id: SessionId,
31    instance_id: InstanceId,
32    requester: InstanceId,
33    g2_manager: Arc<BlockManager<G2>>,
34    g3_manager: Option<Arc<BlockManager<G3>>>,
35    parallel_worker: Option<Arc<dyn ParallelWorkers>>,
36    transport: Arc<MessageTransport>,
37    // Held blocks using BlockHolder for RAII semantics
38    // Blocks are automatically released when the session drops
39    held_g2_blocks: BlockHolder<G2>,
40    held_g3_blocks: BlockHolder<G3>,
41}
42
43impl ResponderSession {
44    /// Create a new responder session.
45    pub fn new(
46        session_id: SessionId,
47        instance_id: InstanceId,
48        requester: InstanceId,
49        g2_manager: Arc<BlockManager<G2>>,
50        g3_manager: Option<Arc<BlockManager<G3>>>,
51        parallel_worker: Option<Arc<dyn ParallelWorkers>>,
52        transport: Arc<MessageTransport>,
53    ) -> Self {
54        Self {
55            session_id,
56            instance_id,
57            requester,
58            g2_manager,
59            g3_manager,
60            parallel_worker,
61            transport,
62            held_g2_blocks: BlockHolder::empty(),
63            held_g3_blocks: BlockHolder::empty(),
64        }
65    }
66
67    /// Run the responder session task.
68    ///
69    /// This is the main session loop that processes messages from the channel.
70    pub async fn run(
71        mut self,
72        mut rx: mpsc::Receiver<OnboardMessage>,
73        sequence_hashes: Vec<SequenceHash>,
74    ) -> Result<()> {
75        // Phase 1: Immediate G2 search
76        // Use scan_matches instead of match_blocks to find all matching blocks
77        // without stopping on first miss (supports partial sequence matching)
78        let g2_matches_map = self.g2_manager.scan_matches(&sequence_hashes, true);
79        let mut g2_matches: Vec<_> = g2_matches_map.into_values().collect();
80
81        // Sort by position to ensure G2Results are in position order
82        // HashMap iteration order is arbitrary, so we must sort explicitly
83        g2_matches.sort_by_key(|block| block.sequence_hash().position());
84
85        // Hold the G2 blocks using BlockHolder (RAII semantics)
86        self.held_g2_blocks = BlockHolder::new(g2_matches);
87
88        // Send G2 results immediately (fire-and-forget) with parallel arrays
89        let g2_sequence_hashes: Vec<SequenceHash> = self.held_g2_blocks.sequence_hashes();
90        let g2_block_ids: Vec<BlockId> = self
91            .held_g2_blocks
92            .blocks()
93            .iter()
94            .map(|b| b.block_id())
95            .collect();
96
97        let g2_msg = OnboardMessage::G2Results {
98            responder: self.instance_id,
99            session_id: self.session_id,
100            sequence_hashes: g2_sequence_hashes,
101            block_ids: g2_block_ids,
102        };
103        self.transport.send(self.requester, g2_msg).await?;
104
105        // Phase 2: Search G3 for remaining hashes (if G3 available)
106        let g2_matched_hashes: HashSet<SequenceHash> =
107            self.held_g2_blocks.sequence_hashes().into_iter().collect();
108
109        let remaining_hashes: Vec<SequenceHash> = sequence_hashes
110            .iter()
111            .filter(|h| !g2_matched_hashes.contains(h))
112            .copied()
113            .collect();
114
115        if !remaining_hashes.is_empty()
116            && let Some(ref g3_manager) = self.g3_manager
117        {
118            // Use scan_matches instead of match_blocks to find all matching blocks
119            // without stopping on first miss (supports partial sequence matching)
120            let g3_matches_map = g3_manager.scan_matches(&remaining_hashes, true);
121            let mut g3_matches: Vec<_> = g3_matches_map.into_values().collect();
122
123            // Sort by position to ensure G3Results are in position order
124            g3_matches.sort_by_key(|block| block.sequence_hash().position());
125
126            if !g3_matches.is_empty() {
127                // Hold the G3 blocks using BlockHolder
128                self.held_g3_blocks = BlockHolder::new(g3_matches);
129
130                // Send G3 results (sequence hashes only, keep order)
131                let g3_sequence_hashes: Vec<SequenceHash> = self.held_g3_blocks.sequence_hashes();
132
133                let g3_msg = OnboardMessage::G3Results {
134                    responder: self.instance_id,
135                    session_id: self.session_id,
136                    sequence_hashes: g3_sequence_hashes,
137                };
138                self.transport.send(self.requester, g3_msg).await?;
139            }
140        }
141
142        // Send SearchComplete to signal we're done searching
143        let complete_msg = OnboardMessage::SearchComplete {
144            responder: self.instance_id,
145            session_id: self.session_id,
146        };
147        self.transport.send(self.requester, complete_msg).await?;
148
149        // Phase 3: Process incoming messages
150        while let Some(msg) = rx.recv().await {
151            match msg {
152                OnboardMessage::HoldBlocks {
153                    hold_hashes,
154                    drop_hashes: _,
155                    ..
156                } => {
157                    // Filter by sequence hash - BlockHolder's retain keeps only matching hashes
158                    self.held_g2_blocks.retain(&hold_hashes);
159                    self.held_g3_blocks.retain(&hold_hashes);
160
161                    // Send acknowledgment
162                    let ack = OnboardMessage::Acknowledged {
163                        responder: self.instance_id,
164                        session_id: self.session_id,
165                    };
166                    self.transport.send(self.requester, ack).await?;
167
168                    // Always wait for CloseSession, even if no G3 blocks
169                    // This ensures proper session lifecycle and avoids race conditions
170                    // where initiator sends CloseSession after we've already exited
171                }
172
173                OnboardMessage::StageBlocks { stage_hashes, .. } => {
174                    // Filter G3 blocks to only keep blocks to be staged
175                    // BlockHolder's retain keeps only matching hashes
176                    self.held_g3_blocks.retain(&stage_hashes);
177
178                    if !self.held_g3_blocks.is_empty() {
179                        if self.parallel_worker.is_some() {
180                            // Execute G3->G2 transfer
181                            self.stage_g3_to_g2().await?;
182                        } else {
183                            tracing::warn!(
184                                session_id = %self.session_id,
185                                g3_blocks = self.held_g3_blocks.count(),
186                                "G3 blocks cannot be staged: no parallel worker configured"
187                            );
188                        }
189                    }
190
191                    // Don't exit - wait for CloseSession in Hold/Prepare modes
192                }
193
194                OnboardMessage::ReleaseBlocks { release_hashes, .. } => {
195                    // Release specific blocks by sequence hash
196                    // BlockHolder's release removes blocks with given hashes
197                    self.held_g2_blocks.release(&release_hashes);
198                    self.held_g3_blocks.release(&release_hashes);
199                }
200
201                // todo: how does close session drop the session from the dashmap?
202                // todo: do we need to handle this in the handler rather than the session responder loop?
203                OnboardMessage::CloseSession { .. } => {
204                    // Session complete - release all blocks and exit
205                    // take_all() explicitly releases the blocks
206                    let _ = self.held_g2_blocks.take_all();
207                    let _ = self.held_g3_blocks.take_all();
208                    break;
209                }
210
211                OnboardMessage::CreateSession { .. } => {
212                    // Duplicate CreateSession - ignore
213                }
214
215                // todo: be explicit about what messages are expected and what messages are unexpected
216                //       on the responder session - avoid using the wildcard match
217                _ => {
218                    // Unexpected message - log and ignore
219                    tracing::warn!(
220                        session_id = %self.session_id,
221                        msg = ?msg,
222                        "ResponderSession: unexpected message"
223                    );
224                }
225            }
226
227            // TODO: Add heartbeat/TTL timeout handling
228            // If no message received within TTL duration:
229            // - Release all held blocks
230            // - Exit session
231            // Implementation:
232            //   tokio::select! {
233            //       msg = rx.recv() => { /* process message */ }
234            //       _ = tokio::time::sleep_until(ttl_deadline) => {
235            //           eprintln!("Session {} TTL expired, releasing blocks", self.session_id);
236            //           break;
237            //       }
238            //   }
239        }
240
241        Ok(())
242    }
243
244    /// Stage G3 blocks to G2.
245    async fn stage_g3_to_g2(&mut self) -> Result<()> {
246        let parallel_worker = self
247            .parallel_worker
248            .as_ref()
249            .ok_or_else(|| anyhow::anyhow!("ParallelWorker required for G3->G2 staging"))?;
250
251        let result = super::staging::stage_g3_to_g2(
252            &self.held_g3_blocks,
253            &self.g2_manager,
254            &**parallel_worker,
255        )
256        .await?;
257
258        // Extract sequence hashes and block IDs for newly staged blocks
259        let new_sequence_hashes: Vec<SequenceHash> = result
260            .new_g2_blocks
261            .iter()
262            .map(|b| b.sequence_hash())
263            .collect();
264        let new_block_ids: Vec<BlockId> =
265            result.new_g2_blocks.iter().map(|b| b.block_id()).collect();
266
267        // Release G3 blocks (take_all releases them) and hold new G2 blocks
268        let _ = self.held_g3_blocks.take_all();
269        self.held_g2_blocks.extend(result.new_g2_blocks);
270
271        // Send BlocksReady with only newly staged blocks
272        let ready_msg = OnboardMessage::BlocksReady {
273            responder: self.instance_id,
274            session_id: self.session_id,
275            sequence_hashes: new_sequence_hashes,
276            block_ids: new_block_ids,
277        };
278        self.transport.send(self.requester, ready_msg).await?;
279
280        Ok(())
281    }
282}