Skip to main content

kvbm_engine/leader/
instance.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5
6use ::velo::Messenger;
7use anyhow::Result;
8use dashmap::DashMap;
9use tokio::sync::{Mutex, mpsc, watch};
10use uuid::Uuid;
11
12use std::sync::Arc;
13
14use crate::{
15    BlockId, G2, G3, InstanceId, SequenceHash, object::ObjectBlockOps, worker::RemoteDescriptor,
16};
17use kvbm_common::LogicalLayoutHandle;
18use kvbm_logical::{
19    blocks::{BlockRegistry, ImmutableBlock},
20    manager::BlockManager,
21};
22use kvbm_physical::transfer::{TransferCompleteNotification, TransferOptions};
23
24use kvbm_physical::manager::{LayoutHandle, SerializedLayout};
25
26use super::{
27    super::worker::Worker,
28    super::worker::group::{ParallelWorkers, SpmdParallelWorkers},
29    AsyncSessionResult,
30    FindMatchesOptions,
31    FindMatchesResult,
32    Leader,
33    OnboardingStatus,
34    ReadyResult,
35    // Legacy SessionHandle for deferred operations
36    SessionHandle as LegacySessionHandle,
37    SessionId,
38    StagingMode,
39    accessor::{BlockAccessor, PolicyContext},
40    session::{
41        BlockHolder, ControlRole, ControllableSessionOptions, ControllableSessionResult,
42        InitiatorSession, MessageTransport, OnboardMessage, OnboardSessionTx, ResponderSession,
43        ServerSession, ServerSessionHandle, ServerSessionOptions, SessionHandle, SessionMessage,
44        SessionMessageTx, SessionPhase, create_server_session, session_handle_state_channel,
45        session_message_channel,
46    },
47    velo::{ExportMetadataCallback, VeloLeaderService},
48};
49
50/// Primary leader implementation for the distributed KVBM system.
51///
52/// `InstanceLeader` coordinates block onboarding across local and remote
53/// instances. It owns a G2 (host memory) `BlockManager` and an optional G3
54/// (disk) `BlockManager`, a set of workers for executing physical transfers,
55/// and a parallel worker abstraction for multi-rank RDMA operations.
56///
57/// Key responsibilities:
58/// - **Block matching**: finding which requested sequence hashes are already
59///   cached locally (via `BlockAccessor` policies).
60/// - **Session management**: creating, attaching, and driving onboard sessions
61///   between endpoint (source) and controller (destination) roles.
62/// - **Remote connectivity**: exchanging serialized layout metadata with peer
63///   instances so workers can perform RDMA transfers.
64/// - **Velo RPC**: registering handlers via `VeloLeaderService` so remote
65///   leaders can initiate sessions and exchange metadata.
66#[derive(Clone)]
67pub struct InstanceLeader {
68    /// Nova instance for distributed communication.
69    messenger: Arc<Messenger>,
70
71    /// Block registry for deduplication.
72    #[allow(dead_code)]
73    pub(crate) registry: BlockRegistry,
74
75    /// G2 (host memory) block manager (wrapped in Arc since BlockManager doesn't implement Clone).
76    pub(crate) g2_manager: Arc<BlockManager<G2>>,
77
78    /// Optional G3 (disk) block manager
79    pub(crate) g3_manager: Option<Arc<BlockManager<G3>>>,
80
81    /// Workers for executing transfers (at least 1 required).
82    /// Multiple workers enable parallel transfers and redundancy.
83    workers: Vec<Arc<dyn Worker>>,
84
85    /// Parallel worker abstraction wrapping the workers.
86    /// Used for RDMA transfers with proper handle mapping storage.
87    parallel_worker: Option<Arc<dyn ParallelWorkers>>,
88
89    /// Map of active sessions (session_id -> message channel).
90    sessions: Arc<DashMap<SessionId, OnboardSessionTx>>,
91
92    /// Cached worker metadata (avoids querying workers repeatedly).
93    cached_worker_metadata: Option<Vec<SerializedLayout>>,
94
95    /// Map of session states for holding blocks alive (RAII).
96    session_states: Arc<DashMap<SessionId, SessionState>>,
97
98    /// List of remote leader instance IDs (mutable for post-construction configuration).
99    remote_leaders: Arc<std::sync::RwLock<Vec<InstanceId>>>,
100
101    /// Message transport for session communication.
102    transport: Arc<MessageTransport>,
103
104    // ========================================================================
105    // Unified Session Protocol
106    // ========================================================================
107    /// Map of session message receivers.
108    /// Used by SessionHandle/SessionEndpoint/ControllableSession.
109    session_sessions: Arc<DashMap<SessionId, SessionMessageTx>>,
110
111    // ========================================================================
112    // G4/Object Storage
113    // ========================================================================
114    /// Object storage client for G4 search and load operations.
115    /// Leader calls has_blocks on S3 directly, coordinates workers for get_blocks.
116    object_client: Option<Arc<dyn ObjectBlockOps>>,
117}
118
119/// Builder for InstanceLeader.
120#[derive(Default)]
121pub struct InstanceLeaderBuilder {
122    messenger: Option<Arc<Messenger>>,
123    registry: Option<BlockRegistry>,
124    g2_manager: Option<Arc<BlockManager<G2>>>,
125    g3_manager: Option<Arc<BlockManager<G3>>>,
126    workers: Vec<Arc<dyn Worker>>,
127    sessions: Option<Arc<DashMap<SessionId, OnboardSessionTx>>>,
128    remote_leaders: Option<Vec<InstanceId>>,
129    cached_worker_metadata: Option<Vec<SerializedLayout>>,
130    object_client: Option<Arc<dyn ObjectBlockOps>>,
131}
132
133impl InstanceLeaderBuilder {
134    /// Initialize builder with components from KvbmRuntime.
135    ///
136    /// This extracts Nova from the runtime. Use this when the runtime
137    /// has already been constructed and you want the leader to share
138    /// the same Nova instance for distributed communication.
139    ///
140    /// # Example
141    /// ```ignore
142    /// let runtime = KvbmRuntime::from_env_leader().await?;
143    /// let leader = InstanceLeaderBuilder::default()
144    ///     .with_runtime(&runtime)
145    ///     .g2_manager(g2_manager)
146    ///     .build()?;
147    /// ```
148    pub fn with_runtime(self, runtime: &crate::KvbmRuntime) -> Self {
149        self.messenger(runtime.messenger().clone())
150    }
151
152    pub fn messenger(mut self, messenger: Arc<Messenger>) -> Self {
153        self.messenger = Some(messenger);
154        self
155    }
156
157    pub fn registry(mut self, registry: BlockRegistry) -> Self {
158        self.registry = Some(registry);
159        self
160    }
161
162    pub fn with_g2_manager(mut self, manager: Option<BlockManager<G2>>) -> Self {
163        self.g2_manager = manager.map(Arc::new);
164        self
165    }
166
167    pub fn with_g3_manager(mut self, manager: Option<BlockManager<G3>>) -> Self {
168        self.g3_manager = manager.map(Arc::new);
169        self
170    }
171
172    pub fn g2_manager(mut self, manager: Arc<BlockManager<G2>>) -> Self {
173        self.g2_manager = Some(manager);
174        self
175    }
176
177    pub fn g3_manager(mut self, manager: Arc<BlockManager<G3>>) -> Self {
178        self.g3_manager = Some(manager);
179        self
180    }
181
182    /// Add a single worker (convenience method).
183    pub fn worker(mut self, worker: Arc<dyn Worker>) -> Self {
184        self.workers.push(worker);
185        self
186    }
187
188    /// Set all workers at once.
189    pub fn workers(mut self, workers: Vec<Arc<dyn Worker>>) -> Self {
190        self.workers = workers;
191        self
192    }
193
194    pub fn remote_leaders(mut self, leaders: Vec<InstanceId>) -> Self {
195        self.remote_leaders = Some(leaders);
196        self
197    }
198
199    /// Cache worker metadata upfront to avoid querying workers later.
200    ///
201    /// This is useful when workers have already exported metadata during initialization
202    /// (e.g., in the connector pattern where workers return metadata in their init response).
203    pub fn with_cached_worker_metadata(mut self, metadata: Vec<SerializedLayout>) -> Self {
204        self.cached_worker_metadata = Some(metadata);
205        self
206    }
207
208    /// Set the object storage client for G4 search and load operations.
209    ///
210    /// The leader uses this client to:
211    /// - Query S3 for block presence via `has_blocks`
212    /// - Coordinate workers to load blocks from S3 via `get_blocks`
213    pub fn object_client(mut self, client: Arc<dyn ObjectBlockOps>) -> Self {
214        self.object_client = Some(client);
215        self
216    }
217
218    pub fn build(self) -> Result<InstanceLeader> {
219        let messenger = self
220            .messenger
221            .ok_or_else(|| anyhow::anyhow!("Nova instance required"))?;
222        let transport = Arc::new(MessageTransport::velo(messenger.clone()));
223
224        // Create event system for notification aggregation
225        let events = Arc::new(messenger.event_manager());
226
227        // Get current tokio runtime handle
228        let runtime = tokio::runtime::Handle::current();
229
230        // // Validate at least one worker
231        // if self.workers.is_empty() {
232        //     anyhow::bail!("At least one worker required");
233        // }
234
235        // todo: we will need a common builder pattern for creating "general" parallel workers
236        // - we could also use an enum and match as the number of types will be limited
237
238        // Create parallel worker if workers are provided
239        let parallel_worker: Option<Arc<dyn ParallelWorkers>> = if !self.workers.is_empty() {
240            Some(Arc::new(SpmdParallelWorkers::new(
241                self.workers.to_vec(),
242                events.clone(),
243                runtime.clone(),
244            )))
245        } else {
246            None
247        };
248
249        Ok(InstanceLeader {
250            messenger,
251            registry: self
252                .registry
253                .ok_or_else(|| anyhow::anyhow!("block registry required"))?,
254            g2_manager: self
255                .g2_manager
256                .ok_or_else(|| anyhow::anyhow!("g2_manager required"))?,
257            g3_manager: self.g3_manager,
258            workers: self.workers,
259            parallel_worker,
260            cached_worker_metadata: self.cached_worker_metadata,
261            sessions: self.sessions.unwrap_or_else(|| Arc::new(DashMap::new())),
262            session_states: Arc::new(DashMap::new()),
263            remote_leaders: Arc::new(std::sync::RwLock::new(
264                self.remote_leaders.unwrap_or_default(),
265            )),
266            transport,
267            session_sessions: Arc::new(DashMap::new()),
268            object_client: self.object_client,
269        })
270    }
271}
272
273/// Internal session state for holding matched blocks.
274#[allow(dead_code)] // Used for RAII block lifetime management
275struct SessionState {
276    session_id: SessionId,
277    matched_g2_blocks: Vec<ImmutableBlock<G2>>,
278    matched_g3_blocks: Vec<ImmutableBlock<G3>>,
279    status_tx: watch::Sender<OnboardingStatus>,
280}
281
282/// Result of scanning for blocks across tiers.
283///
284/// Unlike `FindMatchesResult`, this scans all given hashes without stopping on first miss.
285/// Returns blocks found in each tier along with their sorted positions.
286pub struct ScanBlocksResult {
287    /// Blocks found in G2 (host memory).
288    pub g2_blocks: HashMap<SequenceHash, ImmutableBlock<G2>>,
289
290    /// Blocks found in G3 (disk).
291    pub g3_blocks: HashMap<SequenceHash, ImmutableBlock<G3>>,
292
293    /// All found blocks sorted by position (lowest to highest).
294    /// Each entry indicates which tier (G2/G3) the block was found in.
295    pub sorted_matches: Vec<(SequenceHash, LogicalLayoutHandle)>,
296}
297
298impl InstanceLeader {
299    /// Get a reference to the G2 BlockManager.
300    pub fn g2_manager(&self) -> &Arc<BlockManager<G2>> {
301        &self.g2_manager
302    }
303
304    /// Get a reference to the optional G3 BlockManager.
305    pub fn g3_manager(&self) -> Option<&Arc<BlockManager<G3>>> {
306        self.g3_manager.as_ref()
307    }
308
309    /// Get the block registry.
310    pub fn registry(&self) -> &BlockRegistry {
311        &self.registry
312    }
313
314    /// Get a reference to the Nova instance.
315    ///
316    /// This provides access to the Nova distributed system for features
317    /// like event coordination and cross-instance communication.
318    pub fn messenger(&self) -> &Arc<Messenger> {
319        &self.messenger
320    }
321
322    /// Get the tokio runtime handle from Nova.
323    ///
324    /// This handle should be used for spawning background tasks that need to
325    /// run on the KVBM runtime's executor (e.g., offload engine pipelines).
326    pub fn runtime(&self) -> tokio::runtime::Handle {
327        self.messenger.runtime().clone()
328    }
329
330    /// Check if a parallel_worker is configured.
331    ///
332    /// The parallel_worker is required for local transfer operations
333    /// (e.g., offloading blocks between tiers).
334    pub fn has_parallel_worker(&self) -> bool {
335        self.parallel_worker.is_some()
336    }
337
338    /// Get the parallel worker for distributed operations.
339    ///
340    /// The parallel worker fans out operations to all workers and aggregates results.
341    /// It implements `ObjectBlockOps` for coordinated object storage uploads.
342    pub fn parallel_worker(&self) -> Option<Arc<dyn ParallelWorkers>> {
343        self.parallel_worker.clone()
344    }
345
346    /// Get the object storage client for G4 operations.
347    ///
348    /// Returns `Some` if object storage is configured, `None` otherwise.
349    /// The client is used by InitiatorSession for G4 parallel search.
350    pub fn object_client(&self) -> Option<Arc<dyn ObjectBlockOps>> {
351        self.object_client.clone()
352    }
353
354    /// Add a remote leader to the search list.
355    ///
356    /// Remote leaders are queried during `find_matches_with_options` when
357    /// `search_remote == true`. This method allows adding remote leaders
358    /// after construction (e.g., when instance IDs are only known after
359    /// cluster setup).
360    pub fn add_remote_leader(&self, instance_id: InstanceId) {
361        let mut remote_leaders = self.remote_leaders.write().unwrap();
362        if !remote_leaders.contains(&instance_id) {
363            remote_leaders.push(instance_id);
364        }
365    }
366
367    /// Set all remote leaders at once.
368    pub fn set_remote_leaders(&self, instance_ids: Vec<InstanceId>) {
369        let mut remote_leaders = self.remote_leaders.write().unwrap();
370        *remote_leaders = instance_ids;
371    }
372
373    /// Get the list of remote leader instance IDs.
374    pub fn remote_leaders(&self) -> Vec<InstanceId> {
375        self.remote_leaders.read().unwrap().clone()
376    }
377
378    /// Scan for all blocks matching any of the given sequence hashes.
379    ///
380    /// Unlike `find_matches`, this:
381    /// - Does NOT stop on first miss
382    /// - Returns blocks from both G2 and G3 tiers separately
383    /// - Acquires blocks from pools (caller owns until dropped via RAII)
384    /// - Returns `sorted_matches` ordered by `SequenceHash::position()`
385    ///
386    /// # Arguments
387    /// * `sequence_hashes` - Hashes to scan for
388    /// * `touch` - Whether to update frequency tracking (for MultiLRU eviction policy)
389    ///
390    /// # Algorithm
391    /// 1. Scan G2 manager for candidates
392    /// 2. Scan G3 manager for remaining candidates
393    /// 3. Build sorted_matches from both, sorted by position (lowest to highest)
394    pub fn scan_blocks(&self, sequence_hashes: &[SequenceHash], touch: bool) -> ScanBlocksResult {
395        // Step 1: Scan G2 for all candidates
396        let g2_blocks = self.g2_manager.scan_matches(sequence_hashes, touch);
397
398        // Step 2: Find remaining hashes not in G2
399        let remaining: Vec<SequenceHash> = sequence_hashes
400            .iter()
401            .filter(|h| !g2_blocks.contains_key(h))
402            .copied()
403            .collect();
404
405        // Step 3: Scan G3 for remaining (if G3 exists)
406        let g3_blocks = if let Some(ref g3_manager) = self.g3_manager {
407            if !remaining.is_empty() {
408                g3_manager.scan_matches(&remaining, touch)
409            } else {
410                HashMap::new()
411            }
412        } else {
413            HashMap::new()
414        };
415
416        // Step 4: Build sorted_matches from both tiers
417        let mut sorted_matches: Vec<(SequenceHash, LogicalLayoutHandle)> =
418            Vec::with_capacity(g2_blocks.len() + g3_blocks.len());
419
420        // Add G2 matches
421        for hash in g2_blocks.keys() {
422            sorted_matches.push((*hash, LogicalLayoutHandle::G2));
423        }
424
425        // Add G3 matches
426        for hash in g3_blocks.keys() {
427            sorted_matches.push((*hash, LogicalLayoutHandle::G3));
428        }
429
430        // Sort by SequenceHash position (lowest to highest)
431        sorted_matches.sort_by_key(|(hash, _)| hash.position());
432
433        ScanBlocksResult {
434            g2_blocks,
435            g3_blocks,
436            sorted_matches,
437        }
438    }
439
440    /// Scan blocks using a custom policy that controls iteration and yields results.
441    ///
442    /// This provides maximum flexibility for implementing custom scanning strategies.
443    /// The policy receives access to a `BlockAccessor` for acquiring blocks and a
444    /// `PolicyContext` for yielding results incrementally.
445    ///
446    /// # Arguments
447    /// * `hashes` - Sequence hashes to scan
448    /// * `touch` - Whether to update frequency tracking on block access
449    /// * `policy` - Function that implements the scanning strategy
450    ///
451    /// # Design
452    ///
453    /// The accessor does NOT hold locks between calls. Each `.find()` call is
454    /// independent. This enables:
455    /// - Custom iteration patterns (sorted, BTree scan, binary search, etc.)
456    /// - Yielding results incrementally (e.g., contiguous subsequences)
457    /// - Future parallel execution (accessor is Send + Sync)
458    ///
459    /// # Example: Simple linear scan
460    /// ```ignore
461    /// let blocks = leader.scan_with_policy(&hashes, true, |hashes, ctx| {
462    ///     for hash in hashes {
463    ///         if let Some(block) = ctx.accessor().find(*hash) {
464    ///             ctx.yield_item(block);
465    ///         }
466    ///     }
467    /// });
468    /// ```
469    ///
470    /// # Example: Find contiguous subsequences
471    /// ```ignore
472    /// let runs: Vec<Vec<TieredBlock>> = leader.scan_with_policy(&hashes, true, |hashes, ctx| {
473    ///     let mut run = Vec::new();
474    ///     let mut last_pos: Option<u64> = None;
475    ///
476    ///     for hash in hashes.iter().sorted_by_key(|h| h.position()) {
477    ///         if let Some(block) = ctx.accessor().find(*hash) {
478    ///             let pos = block.position();
479    ///             if last_pos.map_or(true, |p| pos == p + 1) {
480    ///                 run.push(block);
481    ///             } else {
482    ///                 if !run.is_empty() { ctx.yield_item(std::mem::take(&mut run)); }
483    ///                 run.push(block);
484    ///             }
485    ///             last_pos = Some(pos);
486    ///         } else if !run.is_empty() {
487    ///             ctx.yield_item(std::mem::take(&mut run));
488    ///             last_pos = None;
489    ///         }
490    ///     }
491    ///     if !run.is_empty() { ctx.yield_item(run); }
492    /// });
493    /// ```
494    pub fn scan_with_policy<F, T>(&self, hashes: &[SequenceHash], touch: bool, policy: F) -> Vec<T>
495    where
496        F: FnOnce(&[SequenceHash], &mut PolicyContext<T>),
497    {
498        let accessor = BlockAccessor::new(self, touch);
499        let mut ctx = PolicyContext {
500            accessor,
501            results: Vec::new(),
502        };
503        policy(hashes, &mut ctx);
504        ctx.results
505    }
506
507    pub fn builder() -> InstanceLeaderBuilder {
508        InstanceLeaderBuilder::default()
509    }
510
511    /// Register Nova handlers for leader-to-leader communication.
512    ///
513    /// This must be called after construction to enable distributed onboarding.
514    pub fn register_handlers(&self) -> Result<()> {
515        let instance_id = self.messenger.instance_id();
516        let g2_manager = self.g2_manager.clone();
517        let g3_manager = self.g3_manager.clone();
518        let parallel_worker = self.parallel_worker.clone();
519        let transport = self.transport.clone();
520        let sessions = self.sessions.clone();
521
522        let spawn_responder = move |msg: OnboardMessage| -> Result<()> {
523            if let OnboardMessage::CreateSession {
524                requester,
525                session_id,
526                sequence_hashes,
527            } = msg
528            {
529                let (tx, rx) = mpsc::channel(100);
530                sessions.insert(session_id, tx);
531
532                let session = ResponderSession::new(
533                    session_id,
534                    instance_id,
535                    requester,
536                    g2_manager.clone(),
537                    g3_manager.clone(),
538                    parallel_worker.clone(),
539                    transport.clone(),
540                );
541
542                tokio::spawn(async move {
543                    if let Err(e) = session.run(rx, sequence_hashes).await {
544                        tracing::warn!(error = %e, "ResponderSession error");
545                    }
546                });
547
548                Ok(())
549            } else {
550                anyhow::bail!("spawn_responder called with non-CreateSession message")
551            }
552        };
553
554        // Create export_metadata callback if we have workers or cached metadata
555        let export_metadata_callback: Option<ExportMetadataCallback> =
556            if !self.workers.is_empty() || self.cached_worker_metadata.is_some() {
557                let workers = self.workers.clone();
558                let cached_metadata = self.cached_worker_metadata.clone();
559                Some(Arc::new(move || {
560                    let workers = workers.clone();
561                    let cached_metadata = cached_metadata.clone();
562                    Box::pin(async move {
563                        // Return cached metadata if available
564                        if let Some(cached) = cached_metadata {
565                            return Ok(cached);
566                        }
567                        // Otherwise, query workers
568                        let mut metadata = Vec::with_capacity(workers.len());
569                        for worker in &workers {
570                            let serialized = worker.export_metadata()?.await?;
571                            metadata.push(serialized);
572                        }
573                        Ok(metadata)
574                    })
575                }))
576            } else {
577                None
578            };
579
580        let mut service = VeloLeaderService::new(self.messenger.clone(), self.sessions.clone())
581            .with_spawn_responder(spawn_responder)
582            .with_session_sessions(self.session_sessions.clone());
583
584        if let Some(callback) = export_metadata_callback {
585            service = service.with_export_metadata(callback);
586        }
587
588        service.register_handlers()?;
589
590        Ok(())
591    }
592
593    /// Store session state (held blocks and status channel).
594    ///
595    /// Blocks are kept alive via RAII until the session is removed from storage.
596    fn store_session_state(&self, state: SessionState) {
597        self.session_states.insert(state.session_id, state);
598    }
599
600    /// Release a completed session, dropping any held blocks.
601    ///
602    /// This is optional - sessions will naturally be cleaned up when the InstanceLeader
603    /// is dropped. Call this explicitly if you need to release blocks earlier.
604    pub fn release_session(&self, session_id: SessionId) {
605        self.session_states.remove(&session_id);
606        self.sessions.remove(&session_id);
607        self.session_sessions.remove(&session_id);
608    }
609
610    // ========================================================================
611    // Inverted Control Pattern (Prefill-Decode) Methods
612    // ========================================================================
613
614    /// Create a controllable session for local blocks.
615    ///
616    /// This is the "Decode side" of the inverted control pattern:
617    /// 1. Search local G2 and G3 for matches
618    /// 2. Create a ControllableSession that holds the blocks
619    /// 3. Return session_id to be sent to Prefill out-of-band
620    ///
621    /// By default, G3→G2 staging starts immediately (auto_stage=true).
622    pub fn create_controllable_session(
623        &self,
624        sequence_hashes: &[SequenceHash],
625    ) -> Result<ControllableSessionResult> {
626        self.create_controllable_session_with_options(
627            sequence_hashes,
628            ControllableSessionOptions::default(),
629        )
630    }
631
632    /// Create a controllable session with custom options.
633    ///
634    /// Use this when you need to control auto-staging behavior.
635    pub fn create_controllable_session_with_options(
636        &self,
637        sequence_hashes: &[SequenceHash],
638        options: ControllableSessionOptions,
639    ) -> Result<ControllableSessionResult> {
640        let session_id = SessionId::from(Uuid::new_v4());
641
642        // Local search only
643        let matched_g2_blocks = self.g2_manager.match_blocks(sequence_hashes);
644
645        // Find remaining hashes not in G2
646        let remaining_hashes: Vec<_> = sequence_hashes
647            .iter()
648            .filter(|h| !matched_g2_blocks.iter().any(|b| b.sequence_hash() == **h))
649            .copied()
650            .collect();
651
652        // Search G3 for remaining hashes
653        let matched_g3_blocks = if let Some(ref g3_manager) = self.g3_manager {
654            g3_manager.match_blocks(&remaining_hashes)
655        } else {
656            Vec::new()
657        };
658
659        let local_g2_count = matched_g2_blocks.len();
660        let local_g3_count = matched_g3_blocks.len();
661
662        // Create session channel using unified SessionMessage protocol
663        let (tx, rx) = session_message_channel(100);
664        self.session_sessions.insert(session_id, tx);
665
666        // Collect G2 layout handles from workers for round-robin block allocation
667        let worker_g2_handles: Vec<LayoutHandle> = self
668            .parallel_worker
669            .as_ref()
670            .map(|pw| pw.workers().iter().filter_map(|w| w.g2_handle()).collect())
671            .unwrap_or_default();
672
673        let endpoint = super::session::SessionEndpoint::new(
674            session_id,
675            self.messenger.instance_id(),
676            self.transport.clone(),
677            rx,
678        );
679
680        let (cmd_tx, cmd_rx) = mpsc::channel(16);
681
682        let session = ServerSession::new_with_staging(
683            endpoint,
684            BlockHolder::new(matched_g2_blocks),
685            BlockHolder::new(matched_g3_blocks),
686            worker_g2_handles,
687            self.g2_manager.clone(),
688            self.parallel_worker.clone(),
689            cmd_rx,
690            ServerSessionOptions {
691                auto_stage: options.auto_stage,
692            },
693        );
694
695        // Keep handle alive to prevent cmd channel from closing
696        let _handle = ServerSessionHandle::new(session_id, self.messenger.instance_id(), cmd_tx);
697
698        // Spawn session task
699        let session_sessions = self.session_sessions.clone();
700        tokio::spawn(async move {
701            let _handle = _handle; // move handle into task to keep cmd channel open
702            if let Err(e) = session.run().await {
703                tracing::warn!(error = %e, "ServerSession error");
704            }
705            // Clean up when session completes
706            session_sessions.remove(&session_id);
707        });
708
709        Ok(ControllableSessionResult {
710            session_id,
711            local_g2_count,
712            local_g3_count,
713        })
714    }
715
716    // ========================================================================
717    // Unified Session Protocol
718    // ========================================================================
719
720    /// Attach to a remote session.
721    /// Returns a `SessionHandle` that uses `SessionMessage` for communication.
722    ///
723    /// # Arguments
724    /// * `remote_instance` - The instance hosting the session
725    /// * `session_id` - The session to attach to
726    ///
727    /// # Example
728    /// ```ignore
729    /// let handle = leader.attach_session(remote_id, session_id).await?;
730    /// let state = handle.wait_for_ready().await?;
731    /// handle.trigger_staging().await?;
732    /// ```
733    pub async fn attach_session(
734        &self,
735        remote_instance: InstanceId,
736        session_id: SessionId,
737    ) -> Result<SessionHandle> {
738        // Create local channel for receiving state updates
739        let (state_tx, state_rx) = session_handle_state_channel();
740
741        // Register handler for this session's messages
742        let (msg_tx, msg_rx) = session_message_channel(100);
743        self.session_sessions.insert(session_id, msg_tx);
744
745        // Spawn receiver task to update state
746        tokio::spawn(Self::run_session_receiver(msg_rx, state_tx));
747
748        // Send attach message using new protocol
749        let msg = SessionMessage::Attach {
750            peer: self.messenger.instance_id(),
751            session_id,
752            as_role: ControlRole::Controller,
753        };
754        self.transport.send_session(remote_instance, msg).await?;
755
756        let mut handle = SessionHandle::new(
757            session_id,
758            remote_instance,
759            self.messenger.instance_id(),
760            self.transport.clone(),
761            state_rx,
762        );
763
764        // Add RDMA support if parallel worker is configured
765        if let Some(parallel_worker) = &self.parallel_worker {
766            handle = handle.with_rdma_support(parallel_worker.clone());
767        }
768
769        Ok(handle)
770    }
771
772    // ========================================================================
773    // Endpoint Session Creation (Server-Side)
774    // ========================================================================
775
776    /// Create an endpoint session that a remote peer can attach to.
777    ///
778    /// This searches local G2/G3 for blocks matching the given sequence hashes
779    /// and creates a session that exposes them for remote RDMA pull.
780    ///
781    /// Returns `(session_id, handle)` where:
782    /// - `session_id` - Send to remote peer for attachment
783    /// - `handle` - Use to control the session (send layer notifications, close)
784    ///
785    /// # Example
786    /// ```ignore
787    /// // Create session for sequence hashes
788    /// let (session_id, handle) = leader.create_endpoint_session(&hashes)?;
789    ///
790    /// // Send session_id to remote peer out-of-band
791    /// // Remote attaches via: remote_leader.attach_session(local_id, session_id)
792    ///
793    /// // For layerwise transfer, notify when layers are ready
794    /// handle.notify_layers_ready(0..1).await?;
795    /// ```
796    pub fn create_endpoint_session(
797        &self,
798        sequence_hashes: &[SequenceHash],
799    ) -> Result<(SessionId, ServerSessionHandle)> {
800        let session_id = SessionId::from(uuid::Uuid::new_v4());
801
802        // Local search
803        let matched_g2_blocks = self.g2_manager.match_blocks(sequence_hashes);
804
805        // Collect layout handles from workers
806        // Note: For single-worker setups, all blocks use the same handle
807        // For multi-worker (SPMD), each block gets the handle from its assigned worker
808        let worker_g2_handles: Vec<LayoutHandle> = self
809            .parallel_worker
810            .as_ref()
811            .map(|pw| pw.workers().iter().filter_map(|w| w.g2_handle()).collect())
812            .unwrap_or_default();
813
814        // Assign layout handle to each matched block
815        // For now, use the first worker's handle for all blocks (single-worker assumption)
816        // TODO: For SPMD, map blocks to worker handles based on block assignment
817        let layout_handle = worker_g2_handles
818            .first()
819            .copied()
820            .ok_or_else(|| anyhow::anyhow!("No G2 layout handle available from workers"))?;
821        let layout_handles: Vec<LayoutHandle> = vec![layout_handle; matched_g2_blocks.len()];
822
823        // Get sequence hashes from matched blocks
824        let matched_hashes: Vec<SequenceHash> = matched_g2_blocks
825            .iter()
826            .map(|b| b.sequence_hash())
827            .collect();
828
829        // Create the session channel
830        let (msg_tx, msg_rx) = session_message_channel(100);
831        self.session_sessions.insert(session_id, msg_tx);
832
833        // Create BlockHolder from matched blocks
834        let block_holder = BlockHolder::new(matched_g2_blocks);
835
836        // Create the session and handle
837        let (session, handle) = create_server_session(
838            session_id,
839            self.messenger.instance_id(),
840            block_holder,
841            layout_handles,
842            matched_hashes,
843            self.transport.clone(),
844            msg_rx,
845        );
846
847        // Spawn the session task
848        let session_sessions = self.session_sessions.clone();
849        tokio::spawn(async move {
850            if let Err(e) = session.run().await {
851                tracing::warn!(error = %e, "ServerSession error");
852            }
853            // Clean up when session completes
854            session_sessions.remove(&session_id);
855        });
856
857        Ok((session_id, handle))
858    }
859
860    /// Create an endpoint session for specific pre-allocated blocks.
861    ///
862    /// Unlike `create_endpoint_session`, this doesn't search - it uses the
863    /// provided blocks directly. Useful when the caller already has blocks
864    /// to expose (e.g., after prefill computation).
865    ///
866    /// # Arguments
867    /// * `blocks` - Blocks to expose for RDMA pull
868    /// * `sequence_hashes` - Sequence hashes for the blocks (must match block count)
869    /// * `layout_handles` - Layout handles for the blocks (must match block count)
870    ///
871    /// # Example
872    /// ```ignore
873    /// // After prefill computation, expose blocks for Decode to pull
874    /// let (session_id, handle) = leader.create_endpoint_session_for_blocks(
875    ///     prefill_blocks,
876    ///     &hashes,
877    ///     &layout_handles,
878    /// )?;
879    /// ```
880    pub fn create_endpoint_session_for_blocks(
881        &self,
882        blocks: BlockHolder<G2>,
883        sequence_hashes: &[SequenceHash],
884        layout_handles: &[LayoutHandle],
885    ) -> Result<(SessionId, ServerSessionHandle)> {
886        let session_id = SessionId::from(uuid::Uuid::new_v4());
887
888        // Create the session channel
889        let (msg_tx, msg_rx) = session_message_channel(100);
890        self.session_sessions.insert(session_id, msg_tx);
891
892        // Create the session and handle
893        let (session, handle) = create_server_session(
894            session_id,
895            self.messenger.instance_id(),
896            blocks,
897            layout_handles.to_vec(),
898            sequence_hashes.to_vec(),
899            self.transport.clone(),
900            msg_rx,
901        );
902
903        // Spawn the session task
904        let session_sessions = self.session_sessions.clone();
905        tokio::spawn(async move {
906            if let Err(e) = session.run().await {
907                tracing::warn!(error = %e, "ServerSession error");
908            }
909            // Clean up when session completes
910            session_sessions.remove(&session_id);
911        });
912
913        Ok((session_id, handle))
914    }
915
916    /// Internal: Process incoming SessionMessage for a session.
917    async fn run_session_receiver(
918        mut rx: mpsc::Receiver<SessionMessage>,
919        state_tx: super::session::SessionHandleStateTx,
920    ) {
921        while let Some(msg) = rx.recv().await {
922            match msg {
923                SessionMessage::StateResponse { state, .. } => {
924                    state_tx.update(state);
925                }
926                SessionMessage::BlocksStaged {
927                    staged_blocks,
928                    remaining,
929                    layer_range,
930                    ..
931                } => {
932                    state_tx.add_staged_blocks(staged_blocks, remaining, layer_range);
933                }
934                SessionMessage::Error { message, .. } => {
935                    tracing::warn!(%message, "Session error");
936                    state_tx.set_failed();
937                    break;
938                }
939                SessionMessage::Close { .. } => {
940                    state_tx.set_phase(SessionPhase::Complete);
941                    break;
942                }
943                _ => {
944                    // Ignore control commands (sent by controller, not received)
945                }
946            }
947        }
948    }
949
950    /// Get the session sessions map (for Nova handler registration).
951    #[expect(dead_code)]
952    pub(crate) fn session_sessions(&self) -> Arc<DashMap<SessionId, SessionMessageTx>> {
953        self.session_sessions.clone()
954    }
955
956    // ========================================================================
957    // RDMA Metadata Management
958    // These methods handle layout metadata export/import for remote RDMA transfers.
959    // ========================================================================
960
961    /// Check if metadata for a remote instance has been loaded.
962    ///
963    /// Returns true if `import_remote_metadata` has been successfully called
964    /// for the given instance.
965    pub fn has_remote_metadata(&self, instance: InstanceId) -> bool {
966        self.parallel_worker
967            .as_ref()
968            .map(|pw| pw.has_remote_metadata(instance))
969            .unwrap_or(false)
970    }
971
972    /// Get the number of workers attached to this leader.
973    pub fn worker_count(&self) -> usize {
974        self.workers.len()
975    }
976
977    /// Export metadata from all workers.
978    ///
979    /// Returns a `Vec<SerializedLayout>` where each element corresponds to a worker
980    /// in rank order. This metadata can be sent to remote instances to enable
981    /// RDMA transfers.
982    ///
983    /// # Returns
984    /// Vector of serialized layouts, one per worker
985    pub async fn export_worker_metadata(&self) -> Result<Vec<SerializedLayout>> {
986        // Return cached metadata if available
987        if let Some(cached) = &self.cached_worker_metadata {
988            return Ok(cached.clone());
989        }
990
991        // Otherwise, query workers
992        let mut metadata = Vec::with_capacity(self.workers.len());
993
994        for worker in &self.workers {
995            let serialized = worker.export_metadata()?.await?;
996            metadata.push(serialized);
997        }
998
999        Ok(metadata)
1000    }
1001
1002    /// Import metadata from a remote instance's workers.
1003    ///
1004    /// This imports layout metadata from a remote instance, enabling RDMA transfers
1005    /// to pull data from that instance. Metadata is imported rank-by-rank:
1006    /// - local worker 0 imports remote worker 0's metadata
1007    /// - local worker 1 imports remote worker 1's metadata
1008    /// - etc.
1009    ///
1010    /// # Arguments
1011    /// * `remote_instance` - The instance ID of the remote leader
1012    /// * `metadata` - Vector of SerializedLayout from remote workers (one per worker)
1013    ///
1014    /// # Errors
1015    /// Returns an error if:
1016    /// - No parallel worker configured
1017    /// - Metadata was already imported for this instance
1018    /// - Worker count mismatch between local and remote
1019    /// - Individual worker metadata import fails
1020    pub async fn import_remote_metadata(
1021        &self,
1022        remote_instance: InstanceId,
1023        metadata: Vec<SerializedLayout>,
1024    ) -> Result<()> {
1025        let parallel_worker = self
1026            .parallel_worker
1027            .as_ref()
1028            .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1029
1030        // Check if already loaded
1031        if parallel_worker.has_remote_metadata(remote_instance) {
1032            anyhow::bail!("Metadata already imported for instance {}", remote_instance);
1033        }
1034
1035        // Connect to remote - this imports metadata and stores handle mappings
1036        parallel_worker
1037            .connect_remote(remote_instance, metadata)?
1038            .await?;
1039
1040        Ok(())
1041    }
1042
1043    // ========================================================================
1044    // Private Worker Mirror Methods
1045    // These methods execute operations across all workers and aggregate results.
1046    // ========================================================================
1047
1048    /// Execute local transfer across all workers, returning aggregated notification.
1049    ///
1050    /// Delegates to the parallel_worker which fans out to all workers and
1051    /// aggregates their notifications into a single composite notification.
1052    #[allow(dead_code)]
1053    pub(crate) fn execute_local_transfer(
1054        &self,
1055        src: LogicalLayoutHandle,
1056        dst: LogicalLayoutHandle,
1057        src_block_ids: Vec<BlockId>,
1058        dst_block_ids: Vec<BlockId>,
1059        options: TransferOptions,
1060    ) -> Result<TransferCompleteNotification> {
1061        let parallel_worker = self
1062            .parallel_worker
1063            .as_ref()
1064            .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1065
1066        parallel_worker.execute_local_transfer(
1067            src,
1068            dst,
1069            Arc::from(src_block_ids),
1070            Arc::from(dst_block_ids),
1071            options,
1072        )
1073    }
1074
1075    /// Execute remote onboard across all workers, returning aggregated notification.
1076    ///
1077    /// Delegates to the parallel_worker which fans out to all workers and
1078    /// aggregates their notifications into a single composite notification.
1079    #[allow(dead_code)]
1080    pub(crate) fn execute_remote_onboard(
1081        &self,
1082        src: RemoteDescriptor,
1083        dst: LogicalLayoutHandle,
1084        dst_block_ids: Vec<BlockId>,
1085        options: TransferOptions,
1086    ) -> Result<TransferCompleteNotification> {
1087        let parallel_worker = self
1088            .parallel_worker
1089            .as_ref()
1090            .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1091
1092        parallel_worker.execute_remote_onboard(src, dst, Arc::from(dst_block_ids), options)
1093    }
1094
1095    /// Execute remote offload across all workers, returning aggregated notification.
1096    ///
1097    /// Delegates to the parallel_worker which fans out to all workers and
1098    /// aggregates their notifications into a single composite notification.
1099    #[allow(dead_code)]
1100    pub(crate) fn execute_remote_offload(
1101        &self,
1102        src: LogicalLayoutHandle,
1103        dst: RemoteDescriptor,
1104        src_block_ids: Vec<BlockId>,
1105        options: TransferOptions,
1106    ) -> Result<TransferCompleteNotification> {
1107        let parallel_worker = self
1108            .parallel_worker
1109            .as_ref()
1110            .ok_or_else(|| anyhow::anyhow!("No parallel worker configured"))?;
1111
1112        parallel_worker.execute_remote_offload(src, Arc::from(src_block_ids), dst, options)
1113    }
1114}
1115
1116impl Leader for InstanceLeader {
1117    fn find_matches_with_options(
1118        &self,
1119        sequence_hashes: &[SequenceHash],
1120        options: FindMatchesOptions,
1121    ) -> Result<FindMatchesResult> {
1122        // Search G2 (host memory) for matches
1123        // Uses match_blocks which stops at first miss (implements "first hole" policy).
1124        // This ensures we only find contiguous blocks from the start of the sequence.
1125        // For distributed search, remote instances use scan_matches for broad coverage,
1126        // then first-hole filtering is applied in InitiatorSession after aggregation.
1127
1128        // todo: add explicit timing tracing here
1129        // let start_time = Instant::now();
1130        let matched_g2_blocks = self.g2_manager.match_blocks(sequence_hashes);
1131        //let g2_search_time = Instant::now().duration_since(start_time);
1132
1133        // Search G3 (disk) for remaining hashes if G3 is available
1134        let remaining_hashes: Vec<_> = sequence_hashes
1135            .iter()
1136            .filter(|h| !matched_g2_blocks.iter().any(|b| b.sequence_hash() == **h))
1137            .copied()
1138            .collect();
1139
1140        let matched_g3_blocks = if let Some(ref g3_manager) = self.g3_manager {
1141            // Uses match_blocks on remaining hashes (those not found in G2).
1142            // Since G2 already applied first-hole policy, G3 search continues from where G2 stopped.
1143            g3_manager.match_blocks(&remaining_hashes)
1144        } else {
1145            Vec::new()
1146        };
1147
1148        // Determine if we can return immediately (Ready) or need async session
1149        // Ready if:
1150        //   - g3 blocks is empty
1151        //   - AND NOT (search_remote AND has_remote_leaders)
1152        //   - AND NOT (search_remote AND has_object_client)
1153        //
1154        // AsyncSession (is_ready=false) if:
1155        //   - g3 is not empty, or
1156        //   - search_remote is true AND (has_remote_leaders OR has_object_client)
1157        let has_remote_leaders = !self.remote_leaders.read().unwrap().is_empty();
1158        let has_object_client = self.object_client.is_some();
1159        let needs_remote_search =
1160            options.search_remote && (has_remote_leaders || has_object_client);
1161        let is_ready = matched_g3_blocks.is_empty() && !needs_remote_search;
1162
1163        if is_ready {
1164            // No session needed - blocks owned directly by ReadyResult (RAII)
1165            return Ok(FindMatchesResult::Ready(ReadyResult::new(
1166                matched_g2_blocks,
1167            )));
1168        }
1169
1170        // AsyncSession path: G3 blocks found or remote search enabled
1171        let session_id = SessionId::from(Uuid::new_v4());
1172        let local_g2_count = matched_g2_blocks.len();
1173        let local_g3_count = matched_g3_blocks.len();
1174
1175        // AsyncSession: staging locally and/or remote searching
1176        let (status_tx, status_rx) = watch::channel(OnboardingStatus::Searching);
1177        let all_g2_blocks = Arc::new(Mutex::new(None));
1178
1179        // Store session state to keep blocks alive
1180        let state = SessionState {
1181            session_id,
1182            matched_g2_blocks,
1183            matched_g3_blocks,
1184            status_tx: status_tx.clone(),
1185        };
1186        self.store_session_state(state);
1187
1188        // If no remote search, handle local-only staging
1189        if !options.search_remote {
1190            // Local-only staging (Prepare or Full mode)
1191            // TODO: Implement local G3→G2 staging
1192            let total_matched = local_g2_count + local_g3_count;
1193            status_tx
1194                .send(OnboardingStatus::Complete {
1195                    matched_blocks: total_matched,
1196                })
1197                .ok();
1198
1199            return Ok(FindMatchesResult::AsyncSession(AsyncSessionResult::new(
1200                session_id,
1201                status_rx,
1202                all_g2_blocks,
1203                None, // No session handle for local-only staging (yet)
1204            )));
1205        }
1206
1207        // Remote search path
1208        let (tx, rx) = mpsc::channel(100);
1209        self.sessions.insert(session_id, tx);
1210
1211        // Create control channel for Hold/Prepare modes
1212        let (session_handle, control_rx) = if matches!(
1213            options.staging_mode,
1214            StagingMode::Hold | StagingMode::Prepare
1215        ) {
1216            let (control_tx, control_rx) = mpsc::channel(10);
1217            let handle = LegacySessionHandle::new(session_id, options.staging_mode, control_tx);
1218            (Some(handle), Some(control_rx))
1219        } else {
1220            (None, None)
1221        };
1222
1223        let session = InitiatorSession::new(
1224            session_id,
1225            self.messenger.instance_id(),
1226            options.staging_mode,
1227            self.g2_manager.clone(),
1228            self.g3_manager.clone(),
1229            self.parallel_worker.clone(),
1230            self.transport.clone(),
1231            status_tx.clone(),
1232            all_g2_blocks.clone(),
1233            control_rx.unwrap_or_else(|| {
1234                let (_, rx) = mpsc::channel(1);
1235                rx
1236            }),
1237            self.object_client.clone(),
1238        );
1239
1240        let remote_leaders = self.remote_leaders.read().unwrap().clone();
1241        let sequence_hashes = sequence_hashes.to_vec();
1242
1243        let handle = self.messenger.runtime();
1244
1245        handle.spawn(async move {
1246            if let Err(e) = session.run(rx, remote_leaders, sequence_hashes).await {
1247                tracing::warn!(error = %e, "InitiatorSession error");
1248                // Try to update status to indicate error
1249                status_tx
1250                    .send(OnboardingStatus::Complete { matched_blocks: 0 })
1251                    .ok();
1252            }
1253        });
1254
1255        Ok(FindMatchesResult::AsyncSession(AsyncSessionResult::new(
1256            session_id,
1257            status_rx,
1258            all_g2_blocks,
1259            session_handle,
1260        )))
1261    }
1262}