Skip to main content

kvbm_engine/offload/
engine.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Main offload engine coordinating pipelines.
5//!
6//! The `OffloadEngine` is a standalone component that manages block offloading
7//! between storage tiers (G1→G2, G2→G3, G2→G4).
8//!
9//! # Example
10//! ```ignore
11//! let engine = OffloadEngine::builder(leader.clone())
12//!     .with_registry(registry.clone())
13//!     .with_g1_to_g2_pipeline(
14//!         PipelineBuilder::<G1, G2>::new()
15//!             .policy(Arc::new(PresenceFilter::new(registry.clone())))
16//!             .batch_size(32)
17//!             .auto_chain(true)
18//!             .build()
19//!     )
20//!     .with_g2_to_g3_pipeline(
21//!         PipelineBuilder::<G2, G3>::new()
22//!             .policy(Arc::new(PresenceAndLFUFilter::with_default_threshold(registry.clone())))
23//!             .batch_size(64)
24//!             .build()
25//!     )
26//!     .build()?;
27//!
28//! let handle = engine.enqueue_g2_to_g3(blocks);
29//! handle.wait().await?;
30//! ```
31
32use std::sync::Arc;
33
34use anyhow::Result;
35use dashmap::DashMap;
36use tokio::sync::mpsc;
37use tokio::task::JoinHandle;
38use uuid::Uuid;
39
40use crate::leader::InstanceLeader;
41use crate::object::ObjectBlockOps;
42use crate::worker::RemoteDescriptor;
43use crate::{BlockId, G1, G2, G3, SequenceHash};
44use kvbm_common::LogicalLayoutHandle;
45use kvbm_logical::blocks::{BlockMetadata, BlockRegistry, WeakBlock};
46use kvbm_logical::manager::BlockManager;
47use kvbm_physical::transfer::{PhysicalLayout, TransferOptions};
48
49use super::handle::{TransferHandle, TransferId, TransferState};
50use super::pipeline::{
51    ChainOutput, ChainOutputRx, ObjectPipeline, ObjectPipelineConfig, Pipeline, PipelineConfig,
52    PipelineInput,
53};
54use super::queue::CancellableQueue;
55use super::settlement::{
56    PipelineLane, PipelineSettlementTracker, SettlementError, SettlementTarget, SettlementToken,
57    SettlementWaiter, wait_for_settlement,
58};
59use super::source::SourceBlocks;
60
61/// Central coordinator for offload pipelines.
62///
63/// The engine manages multiple pipelines (G1→G2, G2→G3, G2→G4) and provides
64/// a unified interface for enqueueing blocks for offload.
65///
66/// # Storage Tier Model
67///
68/// - G1→G2: `BlockManager<G2>` destination (host memory)
69/// - G2→G3: `BlockManager<G3>` destination (disk/NVMe)
70/// - G2→G4: ObjectBlockOps destination (object storage like S3)
71///
72/// # Distributed G2→G4 Offloading
73///
74/// For distributed setups where the leader doesn't have physical layouts (only workers do),
75/// use `with_enable_remote_g4(true)` instead of `with_g2_to_g4_pipeline()`. This enables
76/// remote G4 offloading where workers execute object storage uploads via their local
77/// `ObjectBlockOps` implementations.
78#[allow(dead_code)]
79pub struct OffloadEngine {
80    /// Identity carried by settlement tokens to reject cross-engine use.
81    engine_id: Uuid,
82    /// Reference to the instance leader for transfers
83    leader: Arc<InstanceLeader>,
84    /// Block registry for policy evaluation
85    registry: Arc<BlockRegistry>,
86    /// G1→G2 pipeline (BlockManager destination)
87    g1_to_g2: Option<Pipeline<G1, G2>>,
88    /// G2→G3 pipeline (BlockManager destination)
89    g2_to_g3: Option<Pipeline<G2, G3>>,
90    /// G2→G4 pipeline (Object storage destination) - for local mode only
91    g2_to_g4: Option<ObjectPipeline<G2>>,
92    /// Active transfer tracking
93    transfers: Arc<DashMap<TransferId, Arc<std::sync::Mutex<TransferState>>>>,
94    /// Chain router task handle (routes G1→G2 output to downstream pipelines)
95    _chain_router_handle: Option<JoinHandle<()>>,
96    /// Remote G4 offload task handle (for distributed mode)
97    _remote_g4_offload_handle: Option<JoinHandle<()>>,
98}
99
100impl OffloadEngine {
101    /// Create a new builder for the offload engine.
102    pub fn builder(leader: Arc<InstanceLeader>) -> OffloadEngineBuilder {
103        OffloadEngineBuilder::new(leader)
104    }
105
106    /// Enqueue blocks for G1→G2 offload.
107    ///
108    /// Returns a `TransferHandle` for tracking progress and cancellation.
109    pub fn enqueue_g1_to_g2(&self, blocks: impl Into<SourceBlocks<G1>>) -> Result<TransferHandle> {
110        let pipeline = self
111            .g1_to_g2
112            .as_ref()
113            .ok_or_else(|| anyhow::anyhow!("G1→G2 pipeline not configured"))?;
114
115        self.enqueue_to_pipeline(pipeline, blocks.into())
116    }
117
118    /// Enqueue blocks for G1→G2 offload with a precondition event.
119    ///
120    /// The precondition event must be satisfied before the batch is processed
121    /// by the transfer executor. This enables coordination with worker forward passes.
122    ///
123    /// Returns a `TransferHandle` for tracking progress and cancellation.
124    pub fn enqueue_g1_to_g2_with_precondition(
125        &self,
126        blocks: impl Into<SourceBlocks<G1>>,
127        precondition: Option<velo::EventHandle>,
128    ) -> Result<TransferHandle> {
129        let pipeline = self
130            .g1_to_g2
131            .as_ref()
132            .ok_or_else(|| anyhow::anyhow!("G1→G2 pipeline not configured"))?;
133
134        self.enqueue_to_pipeline_with_precondition(pipeline, blocks.into(), precondition)
135    }
136
137    /// Enqueue blocks for G2→G3 offload.
138    ///
139    /// Returns a `TransferHandle` for tracking progress and cancellation.
140    pub fn enqueue_g2_to_g3(&self, blocks: impl Into<SourceBlocks<G2>>) -> Result<TransferHandle> {
141        let pipeline = self
142            .g2_to_g3
143            .as_ref()
144            .ok_or_else(|| anyhow::anyhow!("G2→G3 pipeline not configured"))?;
145
146        self.enqueue_to_pipeline(pipeline, blocks.into())
147    }
148
149    /// Enqueue blocks for G2→G4 offload (object storage).
150    ///
151    /// Returns a `TransferHandle` for tracking progress and cancellation.
152    pub fn enqueue_g2_to_g4(&self, blocks: impl Into<SourceBlocks<G2>>) -> Result<TransferHandle> {
153        let pipeline = self
154            .g2_to_g4
155            .as_ref()
156            .ok_or_else(|| anyhow::anyhow!("G2→G4 pipeline not configured"))?;
157
158        self.enqueue_to_object_pipeline(pipeline, blocks.into())
159    }
160
161    /// Create transfer state, store it, and return the components needed for enqueueing.
162    fn create_transfer<T: BlockMetadata>(
163        &self,
164        source: &SourceBlocks<T>,
165    ) -> (
166        TransferId,
167        Arc<std::sync::Mutex<TransferState>>,
168        TransferHandle,
169    ) {
170        let input_block_ids = self.extract_block_ids(source);
171        let transfer_id = TransferId::new();
172        let (state, handle) = TransferState::new(transfer_id, input_block_ids);
173        let state = Arc::new(std::sync::Mutex::new(state));
174        self.transfers.insert(transfer_id, state.clone());
175        (transfer_id, state, handle)
176    }
177
178    /// Internal: enqueue to a specific pipeline.
179    fn enqueue_to_pipeline<Src: BlockMetadata, Dst: BlockMetadata>(
180        &self,
181        pipeline: &Pipeline<Src, Dst>,
182        source: SourceBlocks<Src>,
183    ) -> Result<TransferHandle> {
184        let (transfer_id, state, handle) = self.create_transfer(&source);
185        if !pipeline.enqueue(transfer_id, source, state) {
186            tracing::warn!("Transfer {} was cancelled before enqueueing", transfer_id);
187        }
188        Ok(handle)
189    }
190
191    /// Internal: enqueue to a specific pipeline with a precondition.
192    fn enqueue_to_pipeline_with_precondition<Src: BlockMetadata, Dst: BlockMetadata>(
193        &self,
194        pipeline: &Pipeline<Src, Dst>,
195        source: SourceBlocks<Src>,
196        precondition: Option<velo::EventHandle>,
197    ) -> Result<TransferHandle> {
198        let (transfer_id, state, handle) = self.create_transfer(&source);
199        state.lock().unwrap().precondition = precondition;
200        if !pipeline.enqueue(transfer_id, source, state) {
201            tracing::warn!("Transfer {} was cancelled before enqueueing", transfer_id);
202        }
203        Ok(handle)
204    }
205
206    /// Internal: enqueue to an object pipeline (G2→G4).
207    fn enqueue_to_object_pipeline(
208        &self,
209        pipeline: &ObjectPipeline<G2>,
210        source: SourceBlocks<G2>,
211    ) -> Result<TransferHandle> {
212        let (transfer_id, state, handle) = self.create_transfer(&source);
213        if !pipeline.enqueue(transfer_id, source, state) {
214            tracing::warn!("Transfer {} was cancelled before enqueueing", transfer_id);
215        }
216        Ok(handle)
217    }
218
219    /// Extract block IDs from source blocks.
220    ///
221    /// For External/Strong blocks, returns the known block IDs.
222    /// For Weak blocks, returns empty vec (IDs determined at upgrade time).
223    fn extract_block_ids<T: BlockMetadata>(&self, source: &SourceBlocks<T>) -> Vec<BlockId> {
224        match source {
225            SourceBlocks::External(blocks) => blocks.iter().map(|b| b.block_id).collect(),
226            SourceBlocks::Strong(blocks) => blocks.iter().map(|b| b.block_id()).collect(),
227            SourceBlocks::Weak(_) => Vec::new(), // IDs not available without upgrade
228        }
229    }
230
231    /// Release a completed transfer's resources.
232    ///
233    /// This is optional - transfers are automatically cleaned up,
234    /// but call this to release resources earlier.
235    pub fn release_transfer(&self, transfer_id: TransferId) {
236        self.transfers.remove(&transfer_id);
237    }
238
239    /// Get the number of active transfers.
240    pub fn active_transfer_count(&self) -> usize {
241        self.transfers.len()
242    }
243
244    /// Check if G1→G2 pipeline is configured.
245    pub fn has_g1_to_g2(&self) -> bool {
246        self.g1_to_g2.is_some()
247    }
248
249    /// Check if G2→G3 pipeline is configured.
250    pub fn has_g2_to_g3(&self) -> bool {
251        self.g2_to_g3.is_some()
252    }
253
254    /// Check if G2→G4 pipeline is configured.
255    pub fn has_g2_to_g4(&self) -> bool {
256        self.g2_to_g4.is_some()
257    }
258
259    /// Capture a causal checkpoint for every configured transfer lane.
260    pub fn settlement_token(&self) -> SettlementToken {
261        let mut checkpoints = [None; 3];
262        for lane in PipelineLane::ALL {
263            if let Some((tracker, _)) = self.settlement_tracker(lane) {
264                checkpoints[lane.index()] = Some(tracker.snapshot().checkpoint());
265            }
266        }
267        SettlementToken {
268            engine_id: self.engine_id,
269            checkpoints,
270        }
271    }
272
273    /// Wait until the targeted post-token batches are causally settled.
274    ///
275    /// Settlement includes synchronous executor publication and semaphore handoff to
276    /// every immediately runnable successor. It deliberately does not include
277    /// auto-chained downstream topology in this implementation slice.
278    pub async fn settle_after(
279        &self,
280        token: SettlementToken,
281        target: SettlementTarget,
282    ) -> Result<(), SettlementError> {
283        token.validate_engine(self.engine_id)?;
284        if target.is_empty() {
285            return Ok(());
286        }
287
288        let mut waiters = Vec::new();
289        for lane in PipelineLane::ALL {
290            let delta = target.completed_batches(lane);
291            if delta == 0 {
292                continue;
293            }
294
295            let (tracker, auto_chain) = self
296                .settlement_tracker(lane)
297                .ok_or(SettlementError::LaneUnavailable { lane })?;
298            if auto_chain {
299                return Err(SettlementError::UnsupportedAutoChain { lane });
300            }
301            let checkpoint = token.checkpoint(lane)?;
302
303            // Subscribe every targeted lane before the first state read below.
304            waiters.push(SettlementWaiter::new(lane, tracker, checkpoint, delta)?);
305        }
306        wait_for_settlement(waiters).await
307    }
308
309    fn settlement_tracker(&self, lane: PipelineLane) -> Option<(&PipelineSettlementTracker, bool)> {
310        match lane {
311            PipelineLane::G1ToG2 => self
312                .g1_to_g2
313                .as_ref()
314                .map(|pipeline| (&pipeline.settlement, pipeline.auto_chain())),
315            PipelineLane::G2ToG3 => self
316                .g2_to_g3
317                .as_ref()
318                .map(|pipeline| (&pipeline.settlement, pipeline.auto_chain())),
319            PipelineLane::G2ToG4 => self
320                .g2_to_g4
321                .as_ref()
322                .map(|pipeline| (&pipeline.settlement, false)),
323        }
324    }
325}
326
327/// Builder for OffloadEngine.
328pub struct OffloadEngineBuilder {
329    leader: Arc<InstanceLeader>,
330    registry: Option<Arc<BlockRegistry>>,
331    g1_manager: Option<Arc<BlockManager<G1>>>,
332    g2_manager: Option<Arc<BlockManager<G2>>>,
333    g3_manager: Option<Arc<BlockManager<G3>>>,
334    /// Object storage operations for G4 (replaces `BlockManager<G4>`)
335    object_ops: Option<Arc<dyn ObjectBlockOps>>,
336    /// G2 physical layout for object transfers (needed by ObjectTransferExecutor)
337    g2_physical_layout: Option<PhysicalLayout>,
338    g1_to_g2_config: Option<PipelineConfig<G1, G2>>,
339    g2_to_g3_config: Option<PipelineConfig<G2, G3>>,
340    /// G2→G4 uses ObjectPipelineConfig (no destination BlockManager)
341    g2_to_g4_config: Option<ObjectPipelineConfig<G2>>,
342    /// Optional runtime handle override (defaults to leader.runtime())
343    runtime: Option<tokio::runtime::Handle>,
344    /// Enable remote G4 offloading via workers' ObjectBlockOps (for distributed mode)
345    enable_remote_g4: bool,
346}
347
348impl OffloadEngineBuilder {
349    /// Create a new builder with the given instance leader.
350    pub fn new(leader: Arc<InstanceLeader>) -> Self {
351        Self {
352            leader,
353            registry: None,
354            g1_manager: None,
355            g2_manager: None,
356            g3_manager: None,
357            object_ops: None,
358            g2_physical_layout: None,
359            g1_to_g2_config: None,
360            g2_to_g3_config: None,
361            g2_to_g4_config: None,
362            runtime: None,
363            enable_remote_g4: false,
364        }
365    }
366
367    /// Set an explicit runtime handle for spawning pipeline tasks.
368    ///
369    /// If not set, defaults to `leader.runtime()`. Use this when you need
370    /// pipeline tasks to run on a specific runtime (e.g., in tests).
371    pub fn with_runtime(mut self, runtime: tokio::runtime::Handle) -> Self {
372        self.runtime = Some(runtime);
373        self
374    }
375
376    /// Set the block registry.
377    pub fn with_registry(mut self, registry: Arc<BlockRegistry>) -> Self {
378        self.registry = Some(registry);
379        self
380    }
381
382    /// Set the G1 block manager.
383    pub fn with_g1_manager(mut self, manager: Arc<BlockManager<G1>>) -> Self {
384        self.g1_manager = Some(manager);
385        self
386    }
387
388    /// Set the G2 block manager.
389    pub fn with_g2_manager(mut self, manager: Arc<BlockManager<G2>>) -> Self {
390        self.g2_manager = Some(manager);
391        self
392    }
393
394    /// Set the G3 block manager.
395    pub fn with_g3_manager(mut self, manager: Arc<BlockManager<G3>>) -> Self {
396        self.g3_manager = Some(manager);
397        self
398    }
399
400    /// Set object storage operations for G4.
401    ///
402    /// G4 is object storage (S3, MinIO, etc.) and uses `ObjectBlockOps`
403    /// instead of a `BlockManager`. This replaces `with_g4_manager`.
404    pub fn with_object_ops(mut self, object_ops: Arc<dyn ObjectBlockOps>) -> Self {
405        self.object_ops = Some(object_ops);
406        self
407    }
408
409    /// Set the G2 physical layout for object transfers.
410    ///
411    /// Required when using G2→G4 pipeline. The ObjectTransferExecutor needs
412    /// the physical layout to read block data for upload to object storage.
413    pub fn with_g2_physical_layout(mut self, layout: PhysicalLayout) -> Self {
414        self.g2_physical_layout = Some(layout);
415        self
416    }
417
418    /// Configure G1→G2 pipeline.
419    pub fn with_g1_to_g2_pipeline(mut self, config: PipelineConfig<G1, G2>) -> Self {
420        self.g1_to_g2_config = Some(config);
421        self
422    }
423
424    /// Configure G2→G3 pipeline.
425    pub fn with_g2_to_g3_pipeline(mut self, config: PipelineConfig<G2, G3>) -> Self {
426        self.g2_to_g3_config = Some(config);
427        self
428    }
429
430    /// Configure G2→G4 pipeline (object storage).
431    ///
432    /// Uses `ObjectPipelineConfig` instead of `PipelineConfig` since G4
433    /// is object storage, not a BlockManager destination.
434    ///
435    /// For distributed setups where the leader doesn't have physical layouts,
436    /// use `with_enable_remote_g4(true)` instead.
437    pub fn with_g2_to_g4_pipeline(mut self, config: ObjectPipelineConfig<G2>) -> Self {
438        self.g2_to_g4_config = Some(config);
439        self
440    }
441
442    /// Enable remote G4 offloading via workers' ObjectBlockOps.
443    ///
444    /// In distributed setups, the leader doesn't have physical layouts (only workers do).
445    /// This enables G2→G4 offloading where:
446    /// 1. G1→G2 chain output is routed to a remote offload task
447    /// 2. The task calls workers' ObjectBlockOps::put_blocks() via RPC
448    /// 3. Workers upload blocks from their local G2 to object storage
449    /// 4. Per-block results are returned and logged
450    ///
451    /// This is mutually exclusive with `with_g2_to_g4_pipeline()` - use one or the other.
452    pub fn with_enable_remote_g4(mut self, enable: bool) -> Self {
453        self.enable_remote_g4 = enable;
454        self
455    }
456
457    /// Build the offload engine.
458    pub fn build(self) -> Result<OffloadEngine> {
459        let registry = self
460            .registry
461            .ok_or_else(|| anyhow::anyhow!("Block registry required"))?;
462
463        // Get the runtime handle for spawning background tasks
464        // Use explicit override if provided, otherwise get from leader
465        let runtime = self.runtime.unwrap_or_else(|| self.leader.runtime());
466
467        // Build G1→G2 pipeline if configured
468        // Note: G1 is externally owned (vLLM GPU cache), so no G1 manager needed.
469        // Pipeline works with ExternalBlock<G1> which contains block_id + sequence_hash.
470        let mut g1_to_g2 = if let Some(config) = self.g1_to_g2_config {
471            let g2_manager = self
472                .g2_manager
473                .clone()
474                .ok_or_else(|| anyhow::anyhow!("G2 manager required for G1→G2 pipeline"))?;
475
476            Some(Pipeline::new(
477                config,
478                registry.clone(),
479                g2_manager,
480                self.leader.clone(),
481                LogicalLayoutHandle::G1,
482                LogicalLayoutHandle::G2,
483                runtime.clone(),
484            ))
485        } else {
486            None
487        };
488
489        // Build G2→G3 pipeline if configured
490        let g2_to_g3 = if let Some(config) = self.g2_to_g3_config {
491            let g3_manager = self
492                .g3_manager
493                .ok_or_else(|| anyhow::anyhow!("G3 manager required for G2→G3 pipeline"))?;
494
495            Some(Pipeline::new(
496                config,
497                registry.clone(),
498                g3_manager,
499                self.leader.clone(),
500                LogicalLayoutHandle::G2,
501                LogicalLayoutHandle::G3,
502                runtime.clone(),
503            ))
504        } else {
505            None
506        };
507
508        // Build G2→G4 pipeline if configured (object storage destination)
509        // Note: For distributed mode, use enable_remote_g4 instead
510        let g2_to_g4 = if let Some(config) = self.g2_to_g4_config {
511            let object_ops = self
512                .object_ops
513                .ok_or_else(|| anyhow::anyhow!("ObjectBlockOps required for G2→G4 pipeline"))?;
514
515            // ObjectPipeline takes LogicalLayoutHandle - the ObjectBlockOps implementation
516            // resolves this to a physical layout internally
517            Some(ObjectPipeline::new(
518                config,
519                object_ops,
520                LogicalLayoutHandle::G2,
521                self.leader.clone(),
522                runtime.clone(),
523            ))
524        } else {
525            None
526        };
527
528        // Create channel for remote G4 offload if enabled
529        let (remote_g4_tx, remote_g4_rx) = if self.enable_remote_g4 {
530            let (tx, rx) = mpsc::channel::<RemoteG4OffloadRequest>(64);
531            (Some(tx), Some(rx))
532        } else {
533            (None, None)
534        };
535
536        // Wire up auto-chaining from G1→G2 to downstream G2→G3/G2→G4 pipelines
537        let chain_router_handle = if let Some(ref mut g1_to_g2_pipeline) = g1_to_g2 {
538            if g1_to_g2_pipeline.auto_chain() {
539                if let Some(chain_rx) = g1_to_g2_pipeline.take_chain_rx() {
540                    // Get references to downstream pipeline queues
541                    let g2_to_g3_queue = g2_to_g3.as_ref().map(|p| p.eval_queue.clone());
542                    let g2_to_g4_queue = g2_to_g4.as_ref().map(|p| p.eval_queue.clone());
543
544                    // Check if we have any downstream target (local pipelines or remote G4)
545                    let has_g2_to_g4_local = g2_to_g4_queue.is_some();
546                    let has_g2_to_g4_remote = remote_g4_tx.is_some();
547
548                    // Only spawn if there's at least one downstream target
549                    if g2_to_g3_queue.is_some() || has_g2_to_g4_local || has_g2_to_g4_remote {
550                        tracing::debug!(
551                            has_g2_to_g3 = g2_to_g3_queue.is_some(),
552                            has_g2_to_g4_local,
553                            has_g2_to_g4_remote,
554                            "Spawning chain router for G1→G2 auto-chaining"
555                        );
556                        Some(runtime.spawn(chain_router_task(
557                            chain_rx,
558                            g2_to_g3_queue,
559                            g2_to_g4_queue,
560                            remote_g4_tx,
561                        )))
562                    } else {
563                        tracing::debug!(
564                            "G1→G2 auto_chain enabled but no downstream pipelines configured"
565                        );
566                        None
567                    }
568                } else {
569                    None
570                }
571            } else {
572                None
573            }
574        } else {
575            None
576        };
577
578        // Spawn remote G4 offload task if enabled
579        let remote_g4_offload_handle = if let Some(rx) = remote_g4_rx {
580            tracing::info!("Enabling remote G4 offload via workers' ObjectBlockOps");
581            Some(runtime.spawn(remote_g4_offload_task(rx, self.leader.clone())))
582        } else {
583            None
584        };
585
586        Ok(OffloadEngine {
587            engine_id: Uuid::new_v4(),
588            leader: self.leader,
589            registry,
590            g1_to_g2,
591            g2_to_g3,
592            g2_to_g4,
593            transfers: Arc::new(DashMap::new()),
594            _chain_router_handle: chain_router_handle,
595            _remote_g4_offload_handle: remote_g4_offload_handle,
596        })
597    }
598}
599
600/// Request for remote G4 offload (distributed mode).
601///
602/// Contains the information needed to call workers' ObjectBlockOps::put_blocks().
603struct RemoteG4OffloadRequest {
604    /// Transfer ID for tracking
605    transfer_id: TransferId,
606    /// Sequence hashes (keys for object storage)
607    keys: Vec<SequenceHash>,
608    /// Block IDs in G2 layout
609    block_ids: Vec<BlockId>,
610}
611
612/// Routes chain output from G1→G2 to downstream G2→G3/G2→G4 pipelines.
613///
614/// Blocks are converted to WeakBlocks for best-effort offloading - if they're
615/// evicted before the downstream pipeline processes them, that's acceptable.
616/// This enables graceful degradation under memory pressure.
617async fn chain_router_task(
618    mut chain_rx: ChainOutputRx<G2>,
619    g2_to_g3_queue: Option<Arc<CancellableQueue<PipelineInput<G2>>>>,
620    g2_to_g4_queue: Option<Arc<CancellableQueue<PipelineInput<G2>>>>,
621    remote_g4_tx: Option<mpsc::Sender<RemoteG4OffloadRequest>>,
622) {
623    while let Some(output) = chain_rx.recv().await {
624        let ChainOutput {
625            transfer_id,
626            blocks,
627            state,
628        } = output;
629
630        if blocks.is_empty() {
631            continue;
632        }
633
634        // Convert strong blocks to weak blocks for best-effort downstream processing
635        // This allows blocks to be evicted if memory pressure requires it
636        let weak_blocks: Vec<WeakBlock<G2>> =
637            blocks.iter().map(|block| block.downgrade()).collect();
638
639        // Extract sequence hashes and block IDs for remote G4 offload before dropping
640        let remote_g4_data: Option<(Vec<SequenceHash>, Vec<BlockId>)> = if remote_g4_tx.is_some() {
641            Some((
642                blocks.iter().map(|b| b.sequence_hash()).collect(),
643                blocks.iter().map(|b| b.block_id()).collect(),
644            ))
645        } else {
646            None
647        };
648
649        // Drop strong references - blocks can now be evicted if needed
650        drop(blocks);
651
652        tracing::debug!(
653            %transfer_id,
654            num_blocks = weak_blocks.len(),
655            "Routing chain output to downstream pipelines as WeakBlocks"
656        );
657
658        // Enqueue to G2→G3 if available
659        if let Some(ref queue) = g2_to_g3_queue {
660            let input = PipelineInput {
661                transfer_id,
662                source: SourceBlocks::Weak(weak_blocks.clone()),
663                state: state.clone(),
664            };
665            if !queue.push(transfer_id, input) {
666                tracing::debug!(%transfer_id, "G2→G3 chain enqueue skipped (cancelled)");
667            }
668        }
669
670        // Enqueue to local G2→G4 pipeline if available
671        if let Some(ref queue) = g2_to_g4_queue {
672            let input = PipelineInput {
673                transfer_id,
674                source: SourceBlocks::Weak(weak_blocks.clone()),
675                state: state.clone(),
676            };
677            if !queue.push(transfer_id, input) {
678                tracing::debug!(%transfer_id, "G2→G4 chain enqueue skipped (cancelled)");
679            }
680        }
681
682        // Send to remote G4 offload if enabled (distributed mode)
683        if let (Some(tx), Some((keys, block_ids))) = (&remote_g4_tx, remote_g4_data) {
684            let request = RemoteG4OffloadRequest {
685                transfer_id,
686                keys,
687                block_ids,
688            };
689            if tx.send(request).await.is_err() {
690                tracing::debug!(%transfer_id, "Remote G4 offload channel closed");
691            }
692        }
693    }
694
695    tracing::debug!("Chain router task shutting down");
696}
697
698/// Task that processes remote G4 offload requests.
699///
700/// In distributed mode, this task receives requests from the chain router
701/// and calls workers' ObjectBlockOps to upload blocks to object storage.
702/// Uses execute_remote_offload with RemoteDescriptor::Object to coordinate
703/// workers uploading their local G2 data to S3.
704async fn remote_g4_offload_task(
705    mut rx: mpsc::Receiver<RemoteG4OffloadRequest>,
706    leader: Arc<InstanceLeader>,
707) {
708    tracing::info!("Remote G4 offload task started");
709
710    while let Some(request) = rx.recv().await {
711        let num_blocks = request.keys.len();
712        tracing::debug!(
713            %request.transfer_id,
714            num_blocks,
715            "Processing remote G4 offload request"
716        );
717
718        // Use the leader's execute_remote_offload with RemoteDescriptor::Object
719        // This coordinates all workers to upload from their local G2 to object storage
720        let result = leader.execute_remote_offload(
721            LogicalLayoutHandle::G2, // Source is G2 (host memory)
722            RemoteDescriptor::Object {
723                keys: request.keys.clone(),
724            },
725            request.block_ids.clone(),
726            TransferOptions::default(),
727        );
728
729        match result {
730            Ok(notification) => {
731                // Wait for all workers to complete
732                match notification.await {
733                    Ok(()) => {
734                        tracing::info!(
735                            %request.transfer_id,
736                            num_blocks,
737                            "Remote G4 offload completed successfully"
738                        );
739                    }
740                    Err(e) => {
741                        tracing::warn!(
742                            %request.transfer_id,
743                            num_blocks,
744                            error = %e,
745                            "Remote G4 offload failed"
746                        );
747                    }
748                }
749            }
750            Err(e) => {
751                tracing::warn!(
752                    %request.transfer_id,
753                    num_blocks,
754                    error = %e,
755                    "Failed to initiate remote G4 offload"
756                );
757            }
758        }
759    }
760
761    tracing::info!("Remote G4 offload task shutting down");
762}
763
764#[cfg(test)]
765mod tests {
766    use super::*;
767
768    // Note: Full tests require complex infrastructure setup (InstanceLeader, BlockManagers, etc.)
769    // Basic API tests here.
770
771    #[test]
772    fn test_transfer_id_generation() {
773        let id1 = TransferId::new();
774        let id2 = TransferId::new();
775        assert_ne!(id1, id2);
776    }
777}