1use std::collections::HashSet;
33use std::marker::PhantomData;
34use std::sync::Arc;
35use std::time::{Duration, Instant};
36
37use futures::future::Either;
38use tokio::sync::{Semaphore, mpsc, watch};
39use tokio::task::JoinHandle;
40
41use crate::leader::InstanceLeader;
42use crate::object::ObjectBlockOps;
43use crate::{BlockId, SequenceHash};
44use kvbm_common::LogicalLayoutHandle;
45use kvbm_logical::blocks::{BlockMetadata, BlockRegistry, ImmutableBlock};
46use kvbm_logical::manager::BlockManager;
47use kvbm_physical::transfer::TransferOptions;
48
49use super::batch::{
50 BatchCollector, BatchConfig, BatchOutputRx, EvalResult, QueuedBlock, TimingTrace, TransferBatch,
51};
52use super::handle::{TransferId, TransferState, TransferStatus};
53use super::pending::PendingTracker;
54use super::policy::{EvalContext, OffloadPolicy};
55use super::queue::CancellableQueue;
56use super::source::{SourceBlock, SourceBlocks};
57use crate::object::ObjectLockManager;
58
59#[derive(Clone)]
61pub struct PipelineConfig<Src: BlockMetadata, Dst: BlockMetadata> {
62 pub policies: Vec<Arc<dyn OffloadPolicy<Src>>>,
64 pub batch_config: BatchConfig,
66 pub policy_timeout: Duration,
68 pub auto_chain: bool,
70 pub eval_input_capacity: usize,
72 pub batch_input_capacity: usize,
74 pub transfer_input_capacity: usize,
76 pub sweep_interval: Duration,
78 pub skip_transfers: bool,
80 pub max_concurrent_transfers: usize,
86 pub pending_tracker: Option<Arc<PendingTracker>>,
91 pub max_concurrent_precondition_awaits: usize,
97 _marker: PhantomData<(Src, Dst)>,
99}
100
101impl<Src: BlockMetadata, Dst: BlockMetadata> Default for PipelineConfig<Src, Dst> {
102 fn default() -> Self {
103 Self {
104 policies: Vec::new(),
105 batch_config: BatchConfig::default(),
106 policy_timeout: Duration::from_millis(100),
107 auto_chain: false,
108 eval_input_capacity: 128,
109 batch_input_capacity: 256,
110 transfer_input_capacity: 8,
111 sweep_interval: Duration::from_millis(10),
112 skip_transfers: false,
113 max_concurrent_transfers: 1,
114 pending_tracker: None,
115 max_concurrent_precondition_awaits: 8,
116 _marker: PhantomData,
117 }
118 }
119}
120
121pub struct PipelineBuilder<Src: BlockMetadata, Dst: BlockMetadata> {
123 config: PipelineConfig<Src, Dst>,
124}
125
126impl<Src: BlockMetadata, Dst: BlockMetadata> PipelineBuilder<Src, Dst> {
127 pub fn new() -> Self {
129 Self {
130 config: PipelineConfig::default(),
131 }
132 }
133
134 pub fn policy(mut self, policy: Arc<dyn OffloadPolicy<Src>>) -> Self {
136 self.config.policies.push(policy);
137 self
138 }
139
140 pub fn batch_size(mut self, size: usize) -> Self {
142 self.config.batch_config.max_batch_size = size;
143 self
144 }
145
146 pub fn min_batch_size(mut self, size: usize) -> Self {
148 self.config.batch_config.min_batch_size = size;
149 self
150 }
151
152 pub fn flush_interval(mut self, interval: Duration) -> Self {
154 self.config.batch_config.flush_interval = interval;
155 self
156 }
157
158 pub fn policy_timeout(mut self, timeout: Duration) -> Self {
160 self.config.policy_timeout = timeout;
161 self
162 }
163
164 pub fn auto_chain(mut self, enabled: bool) -> Self {
166 self.config.auto_chain = enabled;
167 self
168 }
169
170 pub fn sweep_interval(mut self, interval: Duration) -> Self {
172 self.config.sweep_interval = interval;
173 self
174 }
175
176 pub fn skip_transfers(mut self, skip: bool) -> Self {
181 self.config.skip_transfers = skip;
182 self
183 }
184
185 pub fn max_concurrent_transfers(mut self, n: usize) -> Self {
193 self.config.max_concurrent_transfers = n.max(1);
194 self
195 }
196
197 pub fn pending_tracker(mut self, tracker: Arc<PendingTracker>) -> Self {
202 self.config.pending_tracker = Some(tracker);
203 self
204 }
205
206 pub fn build(self) -> PipelineConfig<Src, Dst> {
208 self.config
209 }
210}
211
212impl<Src: BlockMetadata, Dst: BlockMetadata> Default for PipelineBuilder<Src, Dst> {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218pub(crate) struct PipelineInput<T: BlockMetadata> {
220 pub(crate) transfer_id: TransferId,
221 pub(crate) source: SourceBlocks<T>,
223 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
224}
225
226pub struct PipelineOutput {
228 pub transfer_id: TransferId,
229 pub completed_hashes: Vec<SequenceHash>,
230}
231
232pub struct ChainOutput<T: BlockMetadata> {
238 pub transfer_id: TransferId,
239 pub blocks: Vec<ImmutableBlock<T>>,
240 #[allow(dead_code)]
242 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
243}
244
245pub type ChainOutputRx<T> = mpsc::Receiver<ChainOutput<T>>;
247
248pub struct Pipeline<Src: BlockMetadata, Dst: BlockMetadata> {
250 config: PipelineConfig<Src, Dst>,
251 pub(crate) eval_queue: Arc<CancellableQueue<PipelineInput<Src>>>,
253 output_tx: Option<mpsc::Sender<PipelineOutput>>,
255 chain_rx: Option<ChainOutputRx<Dst>>,
257 cancel_tx: watch::Sender<HashSet<TransferId>>,
259 pending_tracker: Arc<PendingTracker>,
261 _task_handles: Vec<JoinHandle<()>>,
263 _marker: PhantomData<Dst>,
265}
266
267impl<Src: BlockMetadata, Dst: BlockMetadata> Pipeline<Src, Dst> {
268 #[allow(clippy::too_many_arguments)]
279 pub fn new(
280 config: PipelineConfig<Src, Dst>,
281 _registry: Arc<BlockRegistry>,
282 dst_manager: Arc<BlockManager<Dst>>,
283 leader: Arc<InstanceLeader>,
284 src_layout: LogicalLayoutHandle,
285 dst_layout: LogicalLayoutHandle,
286 runtime: tokio::runtime::Handle,
287 ) -> Self {
288 let eval_queue: Arc<CancellableQueue<PipelineInput<Src>>> =
290 Arc::new(CancellableQueue::new());
291 let batch_queue: Arc<CancellableQueue<EvalResult<Src>>> = Arc::new(CancellableQueue::new());
292
293 let (output_tx, _output_rx) = mpsc::channel(64);
295
296 let (cancel_tx, cancel_rx) = watch::channel(HashSet::new());
298
299 let (batch_tx, batch_rx) = mpsc::channel(config.transfer_input_capacity);
301
302 let (precond_tx, precond_rx) = mpsc::channel(config.transfer_input_capacity);
304
305 let (chain_tx, chain_rx) = if config.auto_chain {
307 let (tx, rx) = mpsc::channel(64);
308 (Some(tx), Some(rx))
309 } else {
310 (None, None)
311 };
312
313 let pending_tracker = config
315 .pending_tracker
316 .clone()
317 .unwrap_or_else(|| Arc::new(PendingTracker::new()));
318
319 let evaluator = PolicyEvaluator {
321 policies: config.policies.clone(),
322 timeout: config.policy_timeout,
323 input_queue: eval_queue.clone(),
324 output_queue: batch_queue.clone(),
325 cancel_rx: cancel_rx.clone(),
326 pending_tracker: pending_tracker.clone(),
327 };
328 let eval_handle = runtime.spawn(async move {
329 evaluator.run().await;
330 });
331
332 let collector_input_queue = batch_queue.clone();
334 let batch_config = config.batch_config.clone();
335 let collector_cancel_rx = cancel_rx.clone();
336 let batch_handle = runtime.spawn(async move {
337 let collector = BatchCollector::new(
338 batch_config,
339 collector_input_queue,
340 batch_tx,
341 collector_cancel_rx,
342 );
343 collector.run().await;
344 });
345
346 let awaiter_leader = leader.clone();
348 let precond_handle = runtime.spawn(async move {
349 let awaiter = PreconditionAwaiter {
350 input_rx: batch_rx,
351 output_tx: precond_tx,
352 leader: awaiter_leader,
353 };
354 awaiter.run().await;
355 });
356
357 let executor = BlockTransferExecutor {
359 input_rx: precond_rx,
360 leader,
361 dst_manager,
362 src_layout,
363 dst_layout,
364 skip_transfers: config.skip_transfers,
365 max_concurrent_transfers: config.max_concurrent_transfers,
366 chain_tx,
367 _src_marker: PhantomData::<Src>,
368 };
369 let transfer_handle = runtime.spawn(async move {
370 executor.run().await;
371 });
372
373 let sweeper_queues = vec![eval_queue.clone()];
375 let sweeper_batch_queue = batch_queue;
376 let sweeper_interval = config.sweep_interval;
377 let sweeper_cancel_rx = cancel_rx;
378 let sweeper_handle = runtime.spawn(async move {
379 cancel_sweeper(
380 sweeper_queues,
381 sweeper_batch_queue,
382 sweeper_cancel_rx,
383 sweeper_interval,
384 )
385 .await;
386 });
387
388 Self {
389 config,
390 eval_queue,
391 output_tx: Some(output_tx),
392 chain_rx,
393 cancel_tx,
394 pending_tracker,
395 _task_handles: vec![
396 eval_handle,
397 batch_handle,
398 precond_handle,
399 transfer_handle,
400 sweeper_handle,
401 ],
402 _marker: PhantomData,
403 }
404 }
405
406 pub(crate) fn enqueue(
408 &self,
409 transfer_id: TransferId,
410 source: SourceBlocks<Src>,
411 state: Arc<std::sync::Mutex<TransferState>>,
412 ) -> bool {
413 tracing::debug!(%transfer_id, num_blocks = source.len(), "Pipeline: enqueueing blocks");
414 let input = PipelineInput {
415 transfer_id,
416 source,
417 state,
418 };
419 self.eval_queue.push(transfer_id, input)
420 }
421
422 pub fn request_cancel(&self, transfer_id: TransferId) {
427 self.eval_queue.mark_cancelled(transfer_id);
429
430 self.cancel_tx.send_modify(|set| {
432 set.insert(transfer_id);
433 });
434 }
435
436 pub fn auto_chain(&self) -> bool {
438 self.config.auto_chain
439 }
440
441 pub fn output_tx(&self) -> Option<mpsc::Sender<PipelineOutput>> {
443 self.output_tx.clone()
444 }
445
446 pub fn take_chain_rx(&mut self) -> Option<ChainOutputRx<Dst>> {
456 self.chain_rx.take()
457 }
458
459 pub fn pending_tracker(&self) -> &Arc<PendingTracker> {
464 &self.pending_tracker
465 }
466}
467
468#[derive(Clone)]
477pub struct ObjectPipelineConfig<Src: BlockMetadata> {
478 pub policies: Vec<Arc<dyn OffloadPolicy<Src>>>,
480 pub batch_config: BatchConfig,
482 pub policy_timeout: Duration,
484 pub eval_input_capacity: usize,
486 pub batch_input_capacity: usize,
488 pub transfer_input_capacity: usize,
490 pub sweep_interval: Duration,
492 pub skip_transfers: bool,
494 pub max_concurrent_transfers: usize,
496 pub pending_tracker: Option<Arc<PendingTracker>>,
498 pub max_concurrent_precondition_awaits: usize,
500 pub lock_manager: Option<Arc<dyn ObjectLockManager>>,
506 _marker: PhantomData<Src>,
508}
509
510impl<Src: BlockMetadata> Default for ObjectPipelineConfig<Src> {
511 fn default() -> Self {
512 Self {
513 policies: Vec::new(),
514 batch_config: BatchConfig::default(),
515 policy_timeout: Duration::from_millis(100),
516 eval_input_capacity: 128,
517 batch_input_capacity: 256,
518 transfer_input_capacity: 8,
519 sweep_interval: Duration::from_millis(10),
520 skip_transfers: false,
521 max_concurrent_transfers: 1,
522 pending_tracker: None,
523 max_concurrent_precondition_awaits: 8,
524 lock_manager: None,
525 _marker: PhantomData,
526 }
527 }
528}
529
530pub struct ObjectPipelineBuilder<Src: BlockMetadata> {
532 config: ObjectPipelineConfig<Src>,
533}
534
535impl<Src: BlockMetadata> ObjectPipelineBuilder<Src> {
536 pub fn new() -> Self {
538 Self {
539 config: ObjectPipelineConfig::default(),
540 }
541 }
542
543 pub fn policy(mut self, policy: Arc<dyn OffloadPolicy<Src>>) -> Self {
545 self.config.policies.push(policy);
546 self
547 }
548
549 pub fn batch_size(mut self, size: usize) -> Self {
551 self.config.batch_config.max_batch_size = size;
552 self
553 }
554
555 pub fn min_batch_size(mut self, size: usize) -> Self {
557 self.config.batch_config.min_batch_size = size;
558 self
559 }
560
561 pub fn flush_interval(mut self, interval: Duration) -> Self {
563 self.config.batch_config.flush_interval = interval;
564 self
565 }
566
567 pub fn policy_timeout(mut self, timeout: Duration) -> Self {
569 self.config.policy_timeout = timeout;
570 self
571 }
572
573 pub fn sweep_interval(mut self, interval: Duration) -> Self {
575 self.config.sweep_interval = interval;
576 self
577 }
578
579 pub fn skip_transfers(mut self, skip: bool) -> Self {
581 self.config.skip_transfers = skip;
582 self
583 }
584
585 pub fn max_concurrent_transfers(mut self, n: usize) -> Self {
587 self.config.max_concurrent_transfers = n.max(1);
588 self
589 }
590
591 pub fn pending_tracker(mut self, tracker: Arc<PendingTracker>) -> Self {
593 self.config.pending_tracker = Some(tracker);
594 self
595 }
596
597 pub fn lock_manager(mut self, manager: Arc<dyn ObjectLockManager>) -> Self {
602 self.config.lock_manager = Some(manager);
603 self
604 }
605
606 pub fn build(self) -> ObjectPipelineConfig<Src> {
608 self.config
609 }
610}
611
612impl<Src: BlockMetadata> Default for ObjectPipelineBuilder<Src> {
613 fn default() -> Self {
614 Self::new()
615 }
616}
617
618#[allow(dead_code)]
623pub struct ObjectPipeline<Src: BlockMetadata> {
624 config: ObjectPipelineConfig<Src>,
625 pub(crate) eval_queue: Arc<CancellableQueue<PipelineInput<Src>>>,
627 output_tx: Option<mpsc::Sender<PipelineOutput>>,
629 cancel_tx: watch::Sender<HashSet<TransferId>>,
631 pending_tracker: Arc<PendingTracker>,
633 _task_handles: Vec<JoinHandle<()>>,
635}
636
637impl<Src: BlockMetadata> ObjectPipeline<Src> {
638 #[allow(clippy::too_many_arguments)]
647 pub fn new(
648 config: ObjectPipelineConfig<Src>,
649 object_ops: Arc<dyn ObjectBlockOps>,
650 src_layout: LogicalLayoutHandle,
651 leader: Arc<InstanceLeader>,
652 runtime: tokio::runtime::Handle,
653 ) -> Self {
654 let eval_queue: Arc<CancellableQueue<PipelineInput<Src>>> =
656 Arc::new(CancellableQueue::new());
657 let batch_queue: Arc<CancellableQueue<EvalResult<Src>>> = Arc::new(CancellableQueue::new());
658
659 let (output_tx, _output_rx) = mpsc::channel(64);
661
662 let (cancel_tx, cancel_rx) = watch::channel(HashSet::new());
664
665 let (batch_tx, batch_rx) = mpsc::channel(config.transfer_input_capacity);
667
668 let (precond_tx, precond_rx) = mpsc::channel(config.transfer_input_capacity);
670
671 let pending_tracker = config
673 .pending_tracker
674 .clone()
675 .unwrap_or_else(|| Arc::new(PendingTracker::new()));
676
677 let evaluator = PolicyEvaluator {
679 policies: config.policies.clone(),
680 timeout: config.policy_timeout,
681 input_queue: eval_queue.clone(),
682 output_queue: batch_queue.clone(),
683 cancel_rx: cancel_rx.clone(),
684 pending_tracker: pending_tracker.clone(),
685 };
686 let eval_handle = runtime.spawn(async move {
687 evaluator.run().await;
688 });
689
690 let collector_input_queue = batch_queue.clone();
692 let batch_config = config.batch_config.clone();
693 let collector_cancel_rx = cancel_rx.clone();
694 let batch_handle = runtime.spawn(async move {
695 let collector = BatchCollector::new(
696 batch_config,
697 collector_input_queue,
698 batch_tx,
699 collector_cancel_rx,
700 );
701 collector.run().await;
702 });
703
704 let awaiter_leader = leader.clone();
706 let precond_handle = runtime.spawn(async move {
707 let awaiter = PreconditionAwaiter {
708 input_rx: batch_rx,
709 output_tx: precond_tx,
710 leader: awaiter_leader,
711 };
712 awaiter.run().await;
713 });
714
715 let executor = ObjectTransferExecutor::new(
717 precond_rx,
718 object_ops,
719 src_layout,
720 config.skip_transfers,
721 config.max_concurrent_transfers,
722 config.lock_manager.clone(),
723 );
724 let transfer_handle = runtime.spawn(async move {
725 executor.run().await;
726 });
727
728 let sweeper_queues = vec![eval_queue.clone()];
730 let sweeper_batch_queue = batch_queue;
731 let sweeper_interval = config.sweep_interval;
732 let sweeper_cancel_rx = cancel_rx;
733 let sweeper_handle = runtime.spawn(async move {
734 cancel_sweeper(
735 sweeper_queues,
736 sweeper_batch_queue,
737 sweeper_cancel_rx,
738 sweeper_interval,
739 )
740 .await;
741 });
742
743 Self {
744 config,
745 eval_queue,
746 output_tx: Some(output_tx),
747 cancel_tx,
748 pending_tracker,
749 _task_handles: vec![
750 eval_handle,
751 batch_handle,
752 precond_handle,
753 transfer_handle,
754 sweeper_handle,
755 ],
756 }
757 }
758
759 pub(crate) fn enqueue(
761 &self,
762 transfer_id: TransferId,
763 source: SourceBlocks<Src>,
764 state: Arc<std::sync::Mutex<TransferState>>,
765 ) -> bool {
766 tracing::debug!(%transfer_id, num_blocks = source.len(), "ObjectPipeline: enqueueing blocks");
767 let input = PipelineInput {
768 transfer_id,
769 source,
770 state,
771 };
772 self.eval_queue.push(transfer_id, input)
773 }
774
775 pub fn request_cancel(&self, transfer_id: TransferId) {
777 self.eval_queue.mark_cancelled(transfer_id);
778 self.cancel_tx.send_modify(|set| {
779 set.insert(transfer_id);
780 });
781 }
782
783 #[allow(dead_code)]
785 pub fn output_tx(&self) -> Option<mpsc::Sender<PipelineOutput>> {
786 self.output_tx.clone()
787 }
788
789 pub fn pending_tracker(&self) -> &Arc<PendingTracker> {
791 &self.pending_tracker
792 }
793}
794
795async fn cancel_sweeper<Src: BlockMetadata>(
797 input_queues: Vec<Arc<CancellableQueue<PipelineInput<Src>>>>,
798 batch_queue: Arc<CancellableQueue<EvalResult<Src>>>,
799 mut cancel_rx: watch::Receiver<HashSet<TransferId>>,
800 interval: Duration,
801) {
802 let mut ticker = tokio::time::interval(interval);
803 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
804
805 loop {
806 tokio::select! {
807 _ = ticker.tick() => {
808 for queue in &input_queues {
810 let removed = queue.sweep();
811 if removed > 0 {
812 tracing::debug!("Sweeper removed {} cancelled input items", removed);
813 }
814 }
815
816 let batch_removed = batch_queue.sweep();
817 if batch_removed > 0 {
818 tracing::debug!("Sweeper removed {} cancelled batch items", batch_removed);
819 }
820 }
821 result = cancel_rx.changed() => {
822 if result.is_err() {
823 break;
825 }
826 for queue in &input_queues {
828 queue.sweep();
829 }
830 batch_queue.sweep();
831 }
832 }
833 }
834}
835
836struct PolicyEvaluator<T: BlockMetadata> {
838 policies: Vec<Arc<dyn OffloadPolicy<T>>>,
839 timeout: Duration,
840 input_queue: Arc<CancellableQueue<PipelineInput<T>>>,
841 output_queue: Arc<CancellableQueue<EvalResult<T>>>,
842 cancel_rx: watch::Receiver<HashSet<TransferId>>,
843 pending_tracker: Arc<PendingTracker>,
845}
846
847impl<T: BlockMetadata> PolicyEvaluator<T> {
848 async fn run(mut self) {
849 loop {
850 while let Some(item) = self.input_queue.pop_valid() {
851 self.evaluate(item.data).await;
852 }
853
854 tokio::select! {
855 _ = self.input_queue.notified() => {}
856 result = self.cancel_rx.changed() => {
857 if result.is_err() {
858 break;
859 }
860 }
861 }
862 }
863 }
864
865 async fn evaluate(&self, input: PipelineInput<T>) {
866 nvtx_range!("offload::policy");
867 let transfer_id = input.transfer_id;
868
869 let total_blocks = input.source.len();
871 {
872 let mut state = input.state.lock().unwrap();
873 state.total_expected_blocks = total_blocks;
874 }
875
876 {
878 let state = input.state.lock().unwrap();
879 if state.is_cancel_requested() {
880 drop(state); tracing::debug!(%transfer_id, "Transfer cancelled before evaluation");
882 let mut state = input.state.lock().unwrap();
883 state.set_cancelled();
884 return;
885 }
886 }
887
888 let mut passed = Vec::new();
889 let mut filtered = Vec::new();
890
891 match input.source {
893 SourceBlocks::External(external_blocks) => {
894 for ext in external_blocks {
897 if self.check_cancelled(&input.state, transfer_id) {
899 return;
900 }
901
902 let ctx = EvalContext::from_external(ext.block_id, ext.sequence_hash);
904 let pass = self.evaluate_policies(&ctx).await;
905
906 if pass {
907 let pending_guard = self.pending_tracker.guard(ext.sequence_hash);
909 passed.push(QueuedBlock {
910 transfer_id,
911 block_id: Some(ext.block_id),
912 sequence_hash: ext.sequence_hash,
913 source: SourceBlock::External(ext),
914 state: input.state.clone(),
915 pending_guard: Some(pending_guard),
916 });
917 } else {
918 filtered.push(ext.block_id);
919 }
920 }
921 tracing::debug!(%transfer_id, passed = passed.len(), filtered = filtered.len(), "External blocks evaluated");
922 }
923 SourceBlocks::Strong(strong_blocks) => {
924 for block in strong_blocks {
926 if self.check_cancelled(&input.state, transfer_id) {
928 return;
929 }
930
931 let ctx = EvalContext::new(block);
932 let pass = self.evaluate_policies(&ctx).await;
933
934 if pass {
935 let block = ctx.block.expect("Strong block context always has block");
936 let pending_guard = self.pending_tracker.guard(ctx.sequence_hash);
938 passed.push(QueuedBlock {
939 transfer_id,
940 block_id: Some(ctx.block_id),
941 sequence_hash: ctx.sequence_hash,
942 source: SourceBlock::Strong(block),
943 state: input.state.clone(),
944 pending_guard: Some(pending_guard),
945 });
946 } else {
947 filtered.push(ctx.block_id);
948 }
949 }
950 }
951 SourceBlocks::Weak(weak_blocks) => {
952 for weak in weak_blocks {
955 if self.check_cancelled(&input.state, transfer_id) {
957 return;
958 }
959
960 let sequence_hash = weak.sequence_hash();
961 let ctx = EvalContext::from_weak(BlockId::default(), sequence_hash);
962 let pass = self.evaluate_policies(&ctx).await;
963
964 if pass {
965 let pending_guard = self.pending_tracker.guard(sequence_hash);
967 passed.push(QueuedBlock {
968 transfer_id,
969 block_id: None, sequence_hash,
971 source: SourceBlock::Weak(weak),
972 state: input.state.clone(),
973 pending_guard: Some(pending_guard),
974 });
975 } else {
976 tracing::debug!(%transfer_id, ?sequence_hash, "Weak block filtered by policy");
979 }
980 }
981 }
982 }
983
984 {
986 let state = input.state.lock().unwrap();
987 if state.is_cancel_requested() {
988 drop(state);
989 tracing::debug!(%transfer_id, "Transfer cancelled after evaluation");
990 let mut state = input.state.lock().unwrap();
991 state.set_cancelled();
992 return;
993 }
994 }
995
996 tracing::debug!(%transfer_id, passed = passed.len(), filtered = filtered.len(), "Policy evaluation complete");
997
998 {
1000 let mut state = input.state.lock().unwrap();
1001 state.add_passed(passed.iter().filter_map(|b| b.block_id));
1004 state.add_filtered(filtered.iter().copied());
1005 state.set_status(TransferStatus::Queued);
1006 }
1007
1008 if passed.is_empty() {
1010 tracing::debug!(%transfer_id, "All blocks filtered, completing transfer");
1011 let mut state = input.state.lock().unwrap();
1012 state.set_complete();
1013 return;
1014 }
1015
1016 let result = EvalResult {
1018 transfer_id,
1019 passed_blocks: passed,
1020 filtered_ids: filtered,
1021 state: input.state,
1022 };
1023
1024 if !self.output_queue.push(transfer_id, result) {
1025 tracing::debug!(%transfer_id, "Push to output queue failed (cancelled)");
1026 }
1027 }
1028
1029 fn check_cancelled(
1031 &self,
1032 state: &Arc<std::sync::Mutex<TransferState>>,
1033 transfer_id: TransferId,
1034 ) -> bool {
1035 let state_guard = state.lock().unwrap();
1036 if state_guard.is_cancel_requested() {
1037 drop(state_guard);
1038 tracing::debug!(%transfer_id, "Transfer cancelled mid-evaluation");
1039 let mut state_guard = state.lock().unwrap();
1040 state_guard.set_cancelled();
1041 true
1042 } else {
1043 false
1044 }
1045 }
1046
1047 async fn evaluate_policies(&self, ctx: &EvalContext<T>) -> bool {
1048 for policy in &self.policies {
1049 let eval_future = policy.evaluate(ctx);
1050 let timed_result = tokio::time::timeout(self.timeout, async {
1051 match eval_future {
1052 Either::Left(ready) => ready.await,
1053 Either::Right(boxed) => boxed.await,
1054 }
1055 })
1056 .await;
1057
1058 match timed_result {
1059 Ok(Ok(true)) => continue,
1060 Ok(Ok(false)) => return false,
1061 Ok(Err(e)) => {
1062 tracing::warn!("Policy {} error: {}", policy.name(), e);
1063 return false;
1064 }
1065 Err(_) => {
1066 tracing::warn!("Policy {} timed out", policy.name());
1067 return false;
1068 }
1069 }
1070 }
1071 true
1072 }
1073}
1074
1075pub struct ResolvedBlock<T: BlockMetadata> {
1085 pub transfer_id: TransferId,
1087 pub block_id: BlockId,
1089 pub sequence_hash: SequenceHash,
1091 #[allow(dead_code)]
1094 pub guard: Option<ImmutableBlock<T>>,
1095 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
1097}
1098
1099pub struct ResolvedBatch<T: BlockMetadata> {
1103 pub blocks: Vec<ResolvedBlock<T>>,
1105 #[allow(dead_code)]
1107 pub evicted: Vec<SequenceHash>,
1108 pub timing: TimingTrace,
1110}
1111
1112impl<T: BlockMetadata> ResolvedBatch<T> {
1113 pub fn is_empty(&self) -> bool {
1115 self.blocks.is_empty()
1116 }
1117
1118 #[allow(dead_code)]
1120 pub fn len(&self) -> usize {
1121 self.blocks.len()
1122 }
1123}
1124
1125pub fn upgrade_batch<T: BlockMetadata>(batch: TransferBatch<T>) -> ResolvedBatch<T> {
1140 let mut resolved: Vec<ResolvedBlock<T>> = Vec::with_capacity(batch.len());
1141 let mut evicted: Vec<SequenceHash> = Vec::new();
1142
1143 let mut timing = batch.timing;
1145 timing.mark_transfer_start();
1146
1147 for queued in batch.blocks {
1148 match queued.source {
1152 SourceBlock::Strong(block) => {
1153 resolved.push(ResolvedBlock {
1154 transfer_id: queued.transfer_id,
1155 block_id: block.block_id(),
1156 sequence_hash: queued.sequence_hash,
1157 guard: Some(block),
1158 state: queued.state,
1159 });
1160 }
1161 SourceBlock::External(ext) => {
1162 resolved.push(ResolvedBlock {
1163 transfer_id: queued.transfer_id,
1164 block_id: ext.block_id,
1165 sequence_hash: ext.sequence_hash,
1166 guard: None,
1167 state: queued.state,
1168 });
1169 }
1170 SourceBlock::Weak(weak) => match weak.upgrade() {
1171 Some(block) => {
1172 resolved.push(ResolvedBlock {
1173 transfer_id: queued.transfer_id,
1174 block_id: block.block_id(),
1175 sequence_hash: queued.sequence_hash,
1176 guard: Some(block),
1177 state: queued.state,
1178 });
1179 }
1180 None => {
1181 tracing::debug!(
1182 sequence_hash = ?queued.sequence_hash,
1183 "Weak block evicted before transfer"
1184 );
1185 evicted.push(queued.sequence_hash);
1186 }
1187 },
1188 }
1189 }
1190
1191 ResolvedBatch {
1192 blocks: resolved,
1193 evicted,
1194 timing,
1195 }
1196}
1197
1198struct PreconditionAwaiter<T: BlockMetadata> {
1209 input_rx: BatchOutputRx<T>,
1210 output_tx: mpsc::Sender<TransferBatch<T>>,
1211 leader: Arc<InstanceLeader>,
1212}
1213
1214impl<T: BlockMetadata> PreconditionAwaiter<T> {
1215 async fn run(mut self) {
1216 while let Some(mut batch) = self.input_rx.recv().await {
1219 let output_tx = self.output_tx.clone();
1220 let nova = self.leader.messenger().clone();
1221
1222 tokio::spawn(async move {
1224 nvtx_range!("offload::precondition");
1225 if let Some(event_handle) = batch.precondition {
1226 tracing::debug!(?event_handle, "Awaiting precondition for batch");
1227
1228 let awaiter_result = nova.events().awaiter(event_handle);
1230
1231 match awaiter_result {
1232 Ok(awaiter) => {
1233 match tokio::time::timeout(Duration::from_secs(300), awaiter).await {
1235 Ok(Ok(())) => {
1236 tracing::debug!(?event_handle, "Precondition satisfied");
1237 }
1238 Ok(Err(poison)) => {
1239 tracing::error!(
1240 ?event_handle,
1241 ?poison,
1242 "Precondition poisoned, marking all blocks as failed"
1243 );
1244 for queued in batch.blocks {
1246 let mut state = queued.state.lock().unwrap();
1247 state.set_error(format!(
1248 "precondition poisoned: {:?}",
1249 poison
1250 ));
1251 }
1252 return;
1253 }
1254 Err(_) => {
1255 tracing::error!(
1256 ?event_handle,
1257 "Precondition timeout after 30s"
1258 );
1259 for queued in batch.blocks {
1261 let mut state = queued.state.lock().unwrap();
1262 state.set_error("precondition timeout".to_string());
1263 }
1264 return;
1265 }
1266 }
1267 }
1268 Err(e) => {
1269 tracing::error!(?event_handle, ?e, "Failed to create awaiter");
1270 for queued in batch.blocks {
1272 let mut state = queued.state.lock().unwrap();
1273 state.set_error(format!("failed to create awaiter: {}", e));
1274 }
1275 return;
1276 }
1277 }
1278 }
1279
1280 batch.timing.mark_precondition_complete();
1282
1283 if let Err(e) = output_tx.send(batch).await {
1285 tracing::error!("Failed to forward batch after precondition: {}", e);
1286 }
1287 });
1288 }
1289 }
1290}
1291
1292struct BlockTransferExecutor<Src: BlockMetadata, Dst: BlockMetadata> {
1303 input_rx: BatchOutputRx<Src>,
1304 leader: Arc<InstanceLeader>,
1305 dst_manager: Arc<BlockManager<Dst>>,
1306 src_layout: LogicalLayoutHandle,
1307 dst_layout: LogicalLayoutHandle,
1308 skip_transfers: bool,
1310 max_concurrent_transfers: usize,
1312 chain_tx: Option<mpsc::Sender<ChainOutput<Dst>>>,
1314 _src_marker: PhantomData<Src>,
1315}
1316
1317struct SharedBlockExecutorState<Dst: BlockMetadata> {
1319 leader: Arc<InstanceLeader>,
1320 dst_manager: Arc<BlockManager<Dst>>,
1321 src_layout: LogicalLayoutHandle,
1322 dst_layout: LogicalLayoutHandle,
1323 skip_transfers: bool,
1324 chain_tx: Option<mpsc::Sender<ChainOutput<Dst>>>,
1325}
1326
1327impl<Src: BlockMetadata, Dst: BlockMetadata> BlockTransferExecutor<Src, Dst> {
1328 async fn run(mut self) {
1329 let transfer_semaphore = Arc::new(Semaphore::new(self.max_concurrent_transfers));
1331 let prepare_semaphore = Arc::new(Semaphore::new(1));
1333
1334 let shared = Arc::new(SharedBlockExecutorState {
1336 leader: self.leader.clone(),
1337 dst_manager: self.dst_manager.clone(),
1338 src_layout: self.src_layout,
1339 dst_layout: self.dst_layout,
1340 skip_transfers: self.skip_transfers,
1341 chain_tx: self.chain_tx.take(),
1342 });
1343
1344 while let Some(batch) = self.input_rx.recv().await {
1345 if batch.is_empty() {
1346 continue;
1347 }
1348
1349 let prepare_permit = prepare_semaphore.clone().acquire_owned().await;
1352 if prepare_permit.is_err() {
1353 break; }
1355 let prepare_permit = prepare_permit.unwrap();
1356
1357 let upgraded = upgrade_batch(batch);
1360
1361 drop(prepare_permit);
1363
1364 if upgraded.is_empty() {
1365 tracing::debug!("All blocks in batch evicted, skipping transfer");
1366 continue;
1367 }
1368
1369 let transfer_permit = transfer_semaphore.clone().acquire_owned().await;
1371 if transfer_permit.is_err() {
1372 break; }
1374 let transfer_permit = transfer_permit.unwrap();
1375
1376 let shared_clone = shared.clone();
1378 tokio::spawn(async move {
1379 let _permit = transfer_permit; if let Err(e) = Self::execute_transfer(&shared_clone, upgraded).await {
1381 tracing::error!("BlockTransferExecutor: transfer failed: {}", e);
1382 }
1383 });
1384 }
1385
1386 let _ = transfer_semaphore
1388 .acquire_many(self.max_concurrent_transfers as u32)
1389 .await;
1390 }
1391
1392 async fn execute_transfer(
1396 shared: &SharedBlockExecutorState<Dst>,
1397 mut batch: ResolvedBatch<Src>,
1398 ) -> anyhow::Result<()> {
1399 nvtx_range!("offload::transfer");
1400 if batch.is_empty() {
1401 return Ok(());
1402 }
1403
1404 let resolved = &batch.blocks;
1405
1406 let src_block_ids: Vec<BlockId> = resolved.iter().map(|b| b.block_id).collect();
1408 let sequence_hashes: Vec<SequenceHash> = resolved.iter().map(|b| b.sequence_hash).collect();
1409
1410 let mut transfer_states: std::collections::HashMap<
1412 TransferId,
1413 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1414 > = std::collections::HashMap::new();
1415 for block in resolved {
1416 transfer_states
1417 .entry(block.transfer_id)
1418 .or_insert_with(|| (block.state.clone(), Vec::new()))
1419 .1
1420 .push(block.block_id);
1421 }
1422
1423 if !shared.skip_transfers {
1425 let dst_blocks = shared
1427 .dst_manager
1428 .allocate_blocks(resolved.len())
1429 .ok_or_else(|| {
1430 anyhow::anyhow!("Failed to allocate {} destination blocks", resolved.len())
1431 })?;
1432
1433 let dst_block_ids: Vec<BlockId> = dst_blocks.iter().map(|b| b.block_id()).collect();
1434
1435 let start_xfer = Instant::now();
1437 let notification = shared.leader.execute_local_transfer(
1438 shared.src_layout,
1439 shared.dst_layout,
1440 src_block_ids.clone(),
1441 dst_block_ids.clone(),
1442 TransferOptions::default(),
1443 )?;
1444
1445 for (state, block_ids) in transfer_states.values() {
1450 let mut state_guard = state.lock().unwrap();
1451 state_guard.set_status(TransferStatus::Transferring);
1452 state_guard.mark_in_flight(block_ids.iter().copied());
1453 }
1454
1455 notification.await?;
1457 let end_xfer = Instant::now();
1458
1459 let registered_blocks: Vec<ImmutableBlock<Dst>> = dst_blocks
1461 .into_iter()
1462 .zip(sequence_hashes.iter())
1463 .map(|(dst_block, seq_hash)| {
1464 let complete = dst_block
1465 .stage(*seq_hash, shared.dst_manager.block_size())
1466 .expect("block size mismatch");
1467 shared.dst_manager.register_block(complete)
1468 })
1469 .collect();
1470
1471 let registration_timepoint = Instant::now();
1472
1473 let unique_transfer_ids: std::collections::HashSet<_> =
1475 resolved.iter().map(|b| b.transfer_id).collect();
1476
1477 let policy_ms = batch
1478 .timing
1479 .policy_duration()
1480 .map(|d| d.as_millis() as u64)
1481 .unwrap_or(0);
1482 let precondition_ms = batch
1483 .timing
1484 .precondition_duration()
1485 .map(|d| d.as_millis() as u64)
1486 .unwrap_or(0);
1487 let total_ms = batch
1488 .timing
1489 .total_duration()
1490 .map(|d| d.as_millis() as u64)
1491 .unwrap_or(0);
1492
1493 tracing::info!(
1494 blocks = resolved.len(),
1495 containers = unique_transfer_ids.len(),
1496 policy_ms,
1497 precondition_ms,
1498 xfer_ms = end_xfer.duration_since(start_xfer).as_millis() as u64,
1499 registration_ms =
1500 registration_timepoint.duration_since(end_xfer).as_millis() as u64,
1501 total_ms,
1502 src = std::any::type_name::<Src>(),
1503 dst = std::any::type_name::<Dst>(),
1504 "Batch transfer complete"
1505 );
1506
1507 if let Some(chain_tx) = &shared.chain_tx {
1509 #[allow(clippy::type_complexity)]
1510 let mut chain_outputs: std::collections::HashMap<
1511 TransferId,
1512 (
1513 Arc<std::sync::Mutex<TransferState>>,
1514 Vec<ImmutableBlock<Dst>>,
1515 ),
1516 > = std::collections::HashMap::new();
1517
1518 for (registered, resolved_block) in
1519 registered_blocks.into_iter().zip(resolved.iter())
1520 {
1521 chain_outputs
1522 .entry(resolved_block.transfer_id)
1523 .or_insert_with(|| (resolved_block.state.clone(), Vec::new()))
1524 .1
1525 .push(registered);
1526 }
1527
1528 for (transfer_id, (state, blocks)) in chain_outputs {
1529 let output = ChainOutput {
1530 transfer_id,
1531 blocks,
1532 state,
1533 };
1534 if chain_tx.send(output).await.is_err() {
1535 tracing::warn!(
1536 %transfer_id,
1537 "Chain channel closed, downstream pipeline unavailable"
1538 );
1539 } else {
1540 tracing::debug!(
1541 %transfer_id,
1542 "Sent blocks to chain output for downstream processing"
1543 );
1544 }
1545 }
1546 }
1547 } else {
1548 for (state, block_ids) in transfer_states.values() {
1549 let mut state_guard = state.lock().unwrap();
1550 state_guard.set_status(TransferStatus::Transferring);
1551 state_guard.mark_in_flight(block_ids.iter().copied());
1552 }
1553 }
1554
1555 batch.timing.mark_transfer_complete();
1557
1558 for (transfer_id, (state, block_ids)) in transfer_states {
1560 let mut state_guard = state.lock().unwrap();
1561 state_guard.mark_completed(block_ids);
1562
1563 let total = state_guard.passed_blocks.len() + state_guard.filtered_out.len();
1564 let done = state_guard.completed.len() + state_guard.filtered_out.len();
1565 tracing::debug!(
1566 %transfer_id,
1567 total,
1568 done,
1569 passed = state_guard.passed_blocks.len(),
1570 filtered = state_guard.filtered_out.len(),
1571 completed = state_guard.completed.len(),
1572 "Transfer batch progress"
1573 );
1574 if done >= total && total > 0 {
1575 state_guard.set_complete();
1576 }
1577 }
1578
1579 Ok(())
1580 }
1581}
1582
1583pub struct ObjectTransferExecutor<Src: BlockMetadata> {
1610 input_rx: BatchOutputRx<Src>,
1612 object_ops: Arc<dyn ObjectBlockOps>,
1614 src_layout: LogicalLayoutHandle,
1617 skip_transfers: bool,
1619 max_concurrent_transfers: usize,
1621 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1623}
1624
1625struct SharedObjectExecutorState {
1627 object_ops: Arc<dyn ObjectBlockOps>,
1628 src_layout: LogicalLayoutHandle,
1629 skip_transfers: bool,
1630 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1631}
1632
1633impl<Src: BlockMetadata> ObjectTransferExecutor<Src> {
1634 #[allow(dead_code)]
1636 pub fn new(
1637 input_rx: BatchOutputRx<Src>,
1638 object_ops: Arc<dyn ObjectBlockOps>,
1639 src_layout: LogicalLayoutHandle,
1640 skip_transfers: bool,
1641 max_concurrent_transfers: usize,
1642 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1643 ) -> Self {
1644 Self {
1645 input_rx,
1646 object_ops,
1647 src_layout,
1648 skip_transfers,
1649 max_concurrent_transfers,
1650 lock_manager,
1651 }
1652 }
1653
1654 pub async fn run(mut self) {
1656 let transfer_semaphore = Arc::new(Semaphore::new(self.max_concurrent_transfers));
1658 let prepare_semaphore = Arc::new(Semaphore::new(1));
1660
1661 let shared = Arc::new(SharedObjectExecutorState {
1663 object_ops: self.object_ops.clone(),
1664 src_layout: self.src_layout,
1665 skip_transfers: self.skip_transfers,
1666 lock_manager: self.lock_manager.clone(),
1667 });
1668
1669 while let Some(batch) = self.input_rx.recv().await {
1670 if batch.is_empty() {
1671 continue;
1672 }
1673
1674 let prepare_permit = prepare_semaphore.clone().acquire_owned().await;
1676 if prepare_permit.is_err() {
1677 break; }
1679 let prepare_permit = prepare_permit.unwrap();
1680
1681 let upgraded = upgrade_batch(batch);
1683
1684 drop(prepare_permit);
1686
1687 if upgraded.is_empty() {
1688 tracing::debug!("All blocks in batch evicted, skipping object transfer");
1689 continue;
1690 }
1691
1692 let transfer_permit = transfer_semaphore.clone().acquire_owned().await;
1694 if transfer_permit.is_err() {
1695 break; }
1697 let transfer_permit = transfer_permit.unwrap();
1698
1699 let shared_clone = shared.clone();
1701 tokio::spawn(async move {
1702 let _permit = transfer_permit; if let Err(e) = Self::execute_transfer(&shared_clone, upgraded).await {
1704 tracing::error!("ObjectTransferExecutor: transfer failed: {}", e);
1705 }
1706 });
1707 }
1708
1709 let _ = transfer_semaphore
1711 .acquire_many(self.max_concurrent_transfers as u32)
1712 .await;
1713 }
1714
1715 async fn execute_transfer(
1717 shared: &SharedObjectExecutorState,
1718 mut batch: ResolvedBatch<Src>,
1719 ) -> anyhow::Result<()> {
1720 nvtx_range!("offload::transfer");
1721 if batch.is_empty() {
1722 return Ok(());
1723 }
1724
1725 let resolved = &batch.blocks;
1726
1727 let keys: Vec<SequenceHash> = resolved.iter().map(|b| b.sequence_hash).collect();
1729 let block_ids: Vec<BlockId> = resolved.iter().map(|b| b.block_id).collect();
1730
1731 let mut transfer_states: std::collections::HashMap<
1733 TransferId,
1734 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1735 > = std::collections::HashMap::new();
1736 for block in resolved {
1737 transfer_states
1738 .entry(block.transfer_id)
1739 .or_insert_with(|| (block.state.clone(), Vec::new()))
1740 .1
1741 .push(block.block_id);
1742 }
1743
1744 let mut successful_hashes: Vec<SequenceHash> = Vec::new();
1746
1747 if !shared.skip_transfers {
1749 let results = shared
1751 .object_ops
1752 .put_blocks(keys.clone(), shared.src_layout, block_ids)
1753 .await;
1754
1755 if results.len() != keys.len() {
1758 tracing::error!(
1759 expected = keys.len(),
1760 actual = results.len(),
1761 "put_blocks returned mismatched result count"
1762 );
1763 for (_transfer_id, (state, block_ids)) in transfer_states {
1764 let mut state_guard = state.lock().unwrap();
1765 state_guard.mark_failed(block_ids);
1766 state_guard
1767 .set_error("put_blocks returned mismatched result count".to_string());
1768 }
1769 return Ok(());
1770 }
1771
1772 let mut success_count = 0;
1774 let mut fail_count = 0;
1775
1776 for result in results {
1777 match result {
1778 Ok(hash) => {
1779 success_count += 1;
1780 successful_hashes.push(hash);
1781 }
1782 Err(hash) => {
1783 fail_count += 1;
1784 tracing::warn!(?hash, "Failed to transfer block to object storage");
1785 }
1786 }
1787 }
1788
1789 if fail_count > 0 {
1790 tracing::warn!(
1791 success = success_count,
1792 failed = fail_count,
1793 "Object transfer partially failed"
1794 );
1795 } else {
1796 tracing::debug!(
1797 num_blocks = success_count,
1798 "Successfully transferred blocks to object storage"
1799 );
1800 }
1801
1802 if let Some(lock_manager) = &shared.lock_manager {
1808 for hash in &successful_hashes {
1809 if let Err(e) = lock_manager.create_meta(*hash).await {
1811 tracing::error!(?hash, error = %e, "Failed to create meta file");
1812 }
1813
1814 if let Err(e) = lock_manager.release_lock(*hash).await {
1816 tracing::error!(?hash, error = %e, "Failed to release lock");
1817 }
1818 }
1819 tracing::debug!(
1820 num_blocks = successful_hashes.len(),
1821 "Created meta files and released locks"
1822 );
1823 }
1824 } else {
1825 if let Some(lock_manager) = &shared.lock_manager {
1827 for hash in &keys {
1828 if let Err(e) = lock_manager.create_meta(*hash).await {
1829 tracing::error!(?hash, error = %e, "Failed to create meta file");
1830 }
1831 if let Err(e) = lock_manager.release_lock(*hash).await {
1832 tracing::error!(?hash, error = %e, "Failed to release lock");
1833 }
1834 }
1835 }
1836 }
1837
1838 batch.timing.mark_transfer_complete();
1840
1841 let unique_transfer_ids: std::collections::HashSet<_> =
1843 resolved.iter().map(|b| b.transfer_id).collect();
1844
1845 let policy_ms = batch
1846 .timing
1847 .policy_duration()
1848 .map(|d| d.as_millis() as u64)
1849 .unwrap_or(0);
1850 let precondition_ms = batch
1851 .timing
1852 .precondition_duration()
1853 .map(|d| d.as_millis() as u64)
1854 .unwrap_or(0);
1855 let transfer_ms = batch
1856 .timing
1857 .transfer_duration()
1858 .map(|d| d.as_millis() as u64)
1859 .unwrap_or(0);
1860 let total_ms = batch
1861 .timing
1862 .total_duration()
1863 .map(|d| d.as_millis() as u64)
1864 .unwrap_or(0);
1865
1866 tracing::info!(
1867 blocks = resolved.len(),
1868 containers = unique_transfer_ids.len(),
1869 policy_ms,
1870 precondition_ms,
1871 transfer_ms,
1872 total_ms,
1873 src = std::any::type_name::<Src>(),
1874 dst = "G4-object",
1875 "Object batch transfer complete"
1876 );
1877
1878 let block_to_hash: std::collections::HashMap<BlockId, SequenceHash> = resolved
1887 .iter()
1888 .map(|b| (b.block_id, b.sequence_hash))
1889 .collect();
1890 let success_set: std::collections::HashSet<SequenceHash> =
1891 successful_hashes.into_iter().collect();
1892
1893 debug_assert_eq!(
1894 block_to_hash.len(),
1895 resolved.len(),
1896 "duplicate BlockId in batch — block_to_hash would lose entries"
1897 );
1898 debug_assert_eq!(
1899 resolved
1900 .iter()
1901 .map(|b| b.sequence_hash)
1902 .collect::<std::collections::HashSet<_>>()
1903 .len(),
1904 resolved.len(),
1905 "duplicate SequenceHash in batch — hash-based success correlation is ambiguous"
1906 );
1907
1908 for (transfer_id, (state, block_ids)) in transfer_states {
1910 let mut state_guard = state.lock().unwrap();
1911
1912 if shared.skip_transfers {
1913 state_guard.mark_completed(block_ids);
1915 } else {
1916 let (succeeded, failed): (Vec<_>, Vec<_>) = block_ids.into_iter().partition(|id| {
1917 block_to_hash
1918 .get(id)
1919 .is_some_and(|h| success_set.contains(h))
1920 });
1921 state_guard.mark_completed(succeeded);
1922 if !failed.is_empty() {
1923 tracing::warn!(
1924 %transfer_id,
1925 failed_count = failed.len(),
1926 "Marking blocks as failed in transfer state"
1927 );
1928 state_guard.mark_failed(failed);
1929 }
1930 }
1931
1932 let total = state_guard.passed_blocks.len() + state_guard.filtered_out.len();
1933 let done = state_guard.completed.len()
1934 + state_guard.failed.len()
1935 + state_guard.filtered_out.len();
1936 tracing::debug!(
1937 %transfer_id,
1938 total,
1939 done,
1940 passed = state_guard.passed_blocks.len(),
1941 filtered = state_guard.filtered_out.len(),
1942 completed = state_guard.completed.len(),
1943 failed = state_guard.failed.len(),
1944 "Object transfer batch progress"
1945 );
1946 if done >= total && total > 0 {
1947 let failed_count = state_guard.failed.len();
1948 if failed_count == 0 {
1949 state_guard.set_complete();
1950 } else {
1951 state_guard.set_error(format!(
1952 "{failed_count} blocks failed to transfer to object storage",
1953 ));
1954 }
1955 }
1956 }
1957
1958 Ok(())
1959 }
1960}
1961
1962#[cfg(test)]
1963mod tests {
1964 use super::*;
1965
1966 #[test]
1967 fn test_pipeline_builder() {
1968 let config = PipelineBuilder::<(), ()>::new()
1969 .batch_size(32)
1970 .min_batch_size(8)
1971 .policy_timeout(Duration::from_millis(50))
1972 .auto_chain(true)
1973 .sweep_interval(Duration::from_millis(5))
1974 .build();
1975
1976 assert_eq!(config.batch_config.max_batch_size, 32);
1977 assert_eq!(config.batch_config.min_batch_size, 8);
1978 assert_eq!(config.policy_timeout, Duration::from_millis(50));
1979 assert!(config.auto_chain);
1980 assert_eq!(config.sweep_interval, Duration::from_millis(5));
1981 }
1982
1983 #[test]
1984 fn test_pipeline_config_default() {
1985 let config = PipelineConfig::<(), ()>::default();
1986 assert!(config.policies.is_empty());
1987 assert!(!config.auto_chain);
1988 assert_eq!(config.sweep_interval, Duration::from_millis(10));
1989 }
1990
1991 struct FailableObjectBlockOps {
1993 fail_hashes: std::collections::HashSet<SequenceHash>,
1994 }
1995
1996 impl crate::object::ObjectBlockOps for FailableObjectBlockOps {
1997 fn has_blocks(
1998 &self,
1999 keys: Vec<SequenceHash>,
2000 ) -> futures::future::BoxFuture<'static, Vec<(SequenceHash, Option<usize>)>> {
2001 Box::pin(async move { keys.into_iter().map(|h| (h, Some(1))).collect() })
2002 }
2003
2004 fn put_blocks(
2005 &self,
2006 keys: Vec<SequenceHash>,
2007 _layout: LogicalLayoutHandle,
2008 _block_ids: Vec<BlockId>,
2009 ) -> futures::future::BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
2010 let fail_set = self.fail_hashes.clone();
2011 Box::pin(async move {
2012 keys.into_iter()
2013 .map(|h| if fail_set.contains(&h) { Err(h) } else { Ok(h) })
2014 .collect()
2015 })
2016 }
2017
2018 fn get_blocks(
2019 &self,
2020 keys: Vec<SequenceHash>,
2021 _layout: LogicalLayoutHandle,
2022 _block_ids: Vec<BlockId>,
2023 ) -> futures::future::BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
2024 Box::pin(async move { keys.into_iter().map(Ok).collect() })
2025 }
2026 }
2027
2028 fn test_hash(n: u64) -> SequenceHash {
2029 SequenceHash::new(n, None, 0)
2030 }
2031
2032 #[tokio::test]
2033 async fn test_execute_transfer_partial_failure() {
2034 use crate::offload::handle::{TransferState, TransferStatus};
2035
2036 let hash_ok_1 = test_hash(1);
2037 let hash_fail = test_hash(2);
2038 let hash_ok_2 = test_hash(3);
2039
2040 let fail_hashes = [hash_fail].into_iter().collect();
2041 let object_ops: Arc<dyn crate::object::ObjectBlockOps> =
2042 Arc::new(FailableObjectBlockOps { fail_hashes });
2043
2044 let shared = SharedObjectExecutorState {
2045 object_ops,
2046 src_layout: LogicalLayoutHandle::G2,
2047 skip_transfers: false,
2048 lock_manager: None,
2049 };
2050
2051 let transfer_id = crate::offload::handle::TransferId::new();
2052 let (mut state, handle) = TransferState::new(transfer_id, vec![10, 20, 30]);
2053 state.add_passed(vec![10, 20, 30]);
2054 state.mark_in_flight(vec![10, 20, 30]);
2055 let state_arc = Arc::new(std::sync::Mutex::new(state));
2056
2057 let blocks = vec![
2058 ResolvedBlock::<crate::G2> {
2059 transfer_id,
2060 block_id: 10,
2061 sequence_hash: hash_ok_1,
2062 guard: None,
2063 state: state_arc.clone(),
2064 },
2065 ResolvedBlock::<crate::G2> {
2066 transfer_id,
2067 block_id: 20,
2068 sequence_hash: hash_fail,
2069 guard: None,
2070 state: state_arc.clone(),
2071 },
2072 ResolvedBlock::<crate::G2> {
2073 transfer_id,
2074 block_id: 30,
2075 sequence_hash: hash_ok_2,
2076 guard: None,
2077 state: state_arc.clone(),
2078 },
2079 ];
2080
2081 let mut timing = TimingTrace::new();
2082 timing.mark_policy_complete();
2083 timing.mark_precondition_complete();
2084
2085 let batch = ResolvedBatch {
2086 blocks,
2087 evicted: Vec::new(),
2088 timing,
2089 };
2090
2091 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch)
2092 .await
2093 .expect("execute_transfer should succeed");
2094
2095 let state_guard = state_arc.lock().unwrap();
2097 assert_eq!(state_guard.completed, vec![10, 30]);
2098 assert_eq!(state_guard.failed, vec![20]);
2099 assert_eq!(state_guard.in_flight.len(), 0);
2100 assert_eq!(state_guard.status, TransferStatus::Failed);
2101 assert!(state_guard.error.is_some());
2102
2103 drop(state_guard);
2105 assert_eq!(handle.completed_blocks(), vec![10, 30]);
2106 assert_eq!(handle.failed_blocks(), vec![20]);
2107 }
2108
2109 #[tokio::test]
2110 async fn test_execute_transfer_all_success() {
2111 use crate::offload::handle::{TransferState, TransferStatus};
2112
2113 let hash1 = test_hash(1);
2114 let hash2 = test_hash(2);
2115
2116 let object_ops: Arc<dyn crate::object::ObjectBlockOps> = Arc::new(FailableObjectBlockOps {
2117 fail_hashes: std::collections::HashSet::new(),
2118 });
2119
2120 let shared = SharedObjectExecutorState {
2121 object_ops,
2122 src_layout: LogicalLayoutHandle::G2,
2123 skip_transfers: false,
2124 lock_manager: None,
2125 };
2126
2127 let transfer_id = crate::offload::handle::TransferId::new();
2128 let (mut state, handle) = TransferState::new(transfer_id, vec![10, 20]);
2129 state.add_passed(vec![10, 20]);
2130 state.mark_in_flight(vec![10, 20]);
2131 let state_arc = Arc::new(std::sync::Mutex::new(state));
2132
2133 let blocks = vec![
2134 ResolvedBlock::<crate::G2> {
2135 transfer_id,
2136 block_id: 10,
2137 sequence_hash: hash1,
2138 guard: None,
2139 state: state_arc.clone(),
2140 },
2141 ResolvedBlock::<crate::G2> {
2142 transfer_id,
2143 block_id: 20,
2144 sequence_hash: hash2,
2145 guard: None,
2146 state: state_arc.clone(),
2147 },
2148 ];
2149
2150 let mut timing = TimingTrace::new();
2151 timing.mark_policy_complete();
2152 timing.mark_precondition_complete();
2153
2154 let batch = ResolvedBatch {
2155 blocks,
2156 evicted: Vec::new(),
2157 timing,
2158 };
2159
2160 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch)
2161 .await
2162 .expect("execute_transfer should succeed");
2163
2164 let state_guard = state_arc.lock().unwrap();
2165 assert_eq!(state_guard.completed, vec![10, 20]);
2166 assert!(state_guard.failed.is_empty());
2167 assert_eq!(state_guard.status, TransferStatus::Complete);
2168
2169 drop(state_guard);
2170 assert_eq!(handle.completed_blocks(), vec![10, 20]);
2171 assert!(handle.failed_blocks().is_empty());
2172 }
2173
2174 #[tokio::test]
2176 async fn test_execute_transfer_mixed_transfers() {
2177 use crate::offload::handle::{TransferState, TransferStatus};
2178
2179 let hash_a1 = test_hash(10);
2180 let hash_a2_fail = test_hash(20); let hash_b1 = test_hash(30);
2182 let hash_b2 = test_hash(40);
2183
2184 let fail_hashes = [hash_a2_fail].into_iter().collect();
2185 let object_ops: Arc<dyn crate::object::ObjectBlockOps> =
2186 Arc::new(FailableObjectBlockOps { fail_hashes });
2187
2188 let shared = SharedObjectExecutorState {
2189 object_ops,
2190 src_layout: LogicalLayoutHandle::G2,
2191 skip_transfers: false,
2192 lock_manager: None,
2193 };
2194
2195 let tid_a = crate::offload::handle::TransferId::new();
2197 let (mut state_a, handle_a) = TransferState::new(tid_a, vec![100, 200]);
2198 state_a.add_passed(vec![100, 200]);
2199 state_a.mark_in_flight(vec![100, 200]);
2200 let state_a_arc = Arc::new(std::sync::Mutex::new(state_a));
2201
2202 let tid_b = crate::offload::handle::TransferId::new();
2204 let (mut state_b, handle_b) = TransferState::new(tid_b, vec![300, 400]);
2205 state_b.add_passed(vec![300, 400]);
2206 state_b.mark_in_flight(vec![300, 400]);
2207 let state_b_arc = Arc::new(std::sync::Mutex::new(state_b));
2208
2209 let blocks = vec![
2210 ResolvedBlock::<crate::G2> {
2211 transfer_id: tid_a,
2212 block_id: 100,
2213 sequence_hash: hash_a1,
2214 guard: None,
2215 state: state_a_arc.clone(),
2216 },
2217 ResolvedBlock::<crate::G2> {
2218 transfer_id: tid_a,
2219 block_id: 200,
2220 sequence_hash: hash_a2_fail,
2221 guard: None,
2222 state: state_a_arc.clone(),
2223 },
2224 ResolvedBlock::<crate::G2> {
2225 transfer_id: tid_b,
2226 block_id: 300,
2227 sequence_hash: hash_b1,
2228 guard: None,
2229 state: state_b_arc.clone(),
2230 },
2231 ResolvedBlock::<crate::G2> {
2232 transfer_id: tid_b,
2233 block_id: 400,
2234 sequence_hash: hash_b2,
2235 guard: None,
2236 state: state_b_arc.clone(),
2237 },
2238 ];
2239
2240 let mut timing = TimingTrace::new();
2241 timing.mark_policy_complete();
2242 timing.mark_precondition_complete();
2243
2244 let batch = ResolvedBatch {
2245 blocks,
2246 evicted: Vec::new(),
2247 timing,
2248 };
2249
2250 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch)
2251 .await
2252 .expect("execute_transfer should succeed");
2253
2254 let sa = state_a_arc.lock().unwrap();
2256 assert_eq!(sa.completed, vec![100]);
2257 assert_eq!(sa.failed, vec![200]);
2258 assert_eq!(sa.status, TransferStatus::Failed);
2259 assert!(sa.error.is_some());
2260 drop(sa);
2261
2262 assert_eq!(handle_a.completed_blocks(), vec![100]);
2263 assert_eq!(handle_a.failed_blocks(), vec![200]);
2264
2265 let sb = state_b_arc.lock().unwrap();
2267 assert_eq!(sb.completed, vec![300, 400]);
2268 assert!(sb.failed.is_empty());
2269 assert_eq!(sb.status, TransferStatus::Complete);
2270 drop(sb);
2271
2272 assert_eq!(handle_b.completed_blocks(), vec![300, 400]);
2273 assert!(handle_b.failed_blocks().is_empty());
2274 }
2275}