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