1use 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#[allow(dead_code)]
74pub struct OffloadEngine {
75 leader: Arc<InstanceLeader>,
77 registry: Arc<BlockRegistry>,
79 g1_to_g2: Option<Pipeline<G1, G2>>,
81 g2_to_g3: Option<Pipeline<G2, G3>>,
83 g2_to_g4: Option<ObjectPipeline<G2>>,
85 transfers: Arc<DashMap<TransferId, Arc<std::sync::Mutex<TransferState>>>>,
87 _chain_router_handle: Option<JoinHandle<()>>,
89 _remote_g4_offload_handle: Option<JoinHandle<()>>,
91}
92
93impl OffloadEngine {
94 pub fn builder(leader: Arc<InstanceLeader>) -> OffloadEngineBuilder {
96 OffloadEngineBuilder::new(leader)
97 }
98
99 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 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 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 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 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 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 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 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 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(), }
222 }
223
224 pub fn release_transfer(&self, transfer_id: TransferId) {
229 self.transfers.remove(&transfer_id);
230 }
231
232 pub fn active_transfer_count(&self) -> usize {
234 self.transfers.len()
235 }
236
237 pub fn has_g1_to_g2(&self) -> bool {
239 self.g1_to_g2.is_some()
240 }
241
242 pub fn has_g2_to_g3(&self) -> bool {
244 self.g2_to_g3.is_some()
245 }
246
247 pub fn has_g2_to_g4(&self) -> bool {
249 self.g2_to_g4.is_some()
250 }
251}
252
253pub 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_ops: Option<Arc<dyn ObjectBlockOps>>,
262 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_to_g4_config: Option<ObjectPipelineConfig<G2>>,
268 runtime: Option<tokio::runtime::Handle>,
270 enable_remote_g4: bool,
272}
273
274impl OffloadEngineBuilder {
275 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 pub fn with_runtime(mut self, runtime: tokio::runtime::Handle) -> Self {
298 self.runtime = Some(runtime);
299 self
300 }
301
302 pub fn with_registry(mut self, registry: Arc<BlockRegistry>) -> Self {
304 self.registry = Some(registry);
305 self
306 }
307
308 pub fn with_g1_manager(mut self, manager: Arc<BlockManager<G1>>) -> Self {
310 self.g1_manager = Some(manager);
311 self
312 }
313
314 pub fn with_g2_manager(mut self, manager: Arc<BlockManager<G2>>) -> Self {
316 self.g2_manager = Some(manager);
317 self
318 }
319
320 pub fn with_g3_manager(mut self, manager: Arc<BlockManager<G3>>) -> Self {
322 self.g3_manager = Some(manager);
323 self
324 }
325
326 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 pub fn with_g2_physical_layout(mut self, layout: PhysicalLayout) -> Self {
340 self.g2_physical_layout = Some(layout);
341 self
342 }
343
344 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 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 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 pub fn with_enable_remote_g4(mut self, enable: bool) -> Self {
379 self.enable_remote_g4 = enable;
380 self
381 }
382
383 pub fn build(self) -> Result<OffloadEngine> {
385 let registry = self
386 .registry
387 .ok_or_else(|| anyhow::anyhow!("Block registry required"))?;
388
389 let runtime = self.runtime.unwrap_or_else(|| self.leader.runtime());
392
393 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 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 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 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 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 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 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 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 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 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
525struct RemoteG4OffloadRequest {
529 transfer_id: TransferId,
531 keys: Vec<SequenceHash>,
533 block_ids: Vec<BlockId>,
535}
536
537async 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 let weak_blocks: Vec<WeakBlock<G2>> =
562 blocks.iter().map(|block| block.downgrade()).collect();
563
564 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(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 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 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 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
623async 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 let result = leader.execute_remote_offload(
646 LogicalLayoutHandle::G2, 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 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 #[test]
697 fn test_transfer_id_generation() {
698 let id1 = TransferId::new();
699 let id2 = TransferId::new();
700 assert_ne!(id1, id2);
701 }
702}