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::settlement::{
57 BatchPhaseGuard, PipelineFailure, PipelineFailureKind, PipelineRunGuard,
58 PipelineSettlementTracker, QueuedBatchGuard,
59};
60use super::source::{SourceBlock, SourceBlocks};
61use crate::object::ObjectLockManager;
62
63#[derive(Clone)]
65pub struct PipelineConfig<Src: BlockMetadata, Dst: BlockMetadata> {
66 pub policies: Vec<Arc<dyn OffloadPolicy<Src>>>,
68 pub batch_config: BatchConfig,
70 pub policy_timeout: Duration,
72 pub auto_chain: bool,
74 pub eval_input_capacity: usize,
76 pub batch_input_capacity: usize,
78 pub transfer_input_capacity: usize,
80 pub sweep_interval: Duration,
82 pub skip_transfers: bool,
84 pub max_concurrent_transfers: usize,
90 pub pending_tracker: Option<Arc<PendingTracker>>,
95 pub max_concurrent_precondition_awaits: usize,
101 _marker: PhantomData<(Src, Dst)>,
103}
104
105impl<Src: BlockMetadata, Dst: BlockMetadata> Default for PipelineConfig<Src, Dst> {
106 fn default() -> Self {
107 Self {
108 policies: Vec::new(),
109 batch_config: BatchConfig::default(),
110 policy_timeout: Duration::from_millis(100),
111 auto_chain: false,
112 eval_input_capacity: 128,
113 batch_input_capacity: 256,
114 transfer_input_capacity: 8,
115 sweep_interval: Duration::from_millis(10),
116 skip_transfers: false,
117 max_concurrent_transfers: 1,
118 pending_tracker: None,
119 max_concurrent_precondition_awaits: 8,
120 _marker: PhantomData,
121 }
122 }
123}
124
125pub struct PipelineBuilder<Src: BlockMetadata, Dst: BlockMetadata> {
127 config: PipelineConfig<Src, Dst>,
128}
129
130impl<Src: BlockMetadata, Dst: BlockMetadata> PipelineBuilder<Src, Dst> {
131 pub fn new() -> Self {
133 Self {
134 config: PipelineConfig::default(),
135 }
136 }
137
138 pub fn policy(mut self, policy: Arc<dyn OffloadPolicy<Src>>) -> Self {
140 self.config.policies.push(policy);
141 self
142 }
143
144 pub fn batch_size(mut self, size: usize) -> Self {
146 self.config.batch_config.max_batch_size = size;
147 self
148 }
149
150 pub fn min_batch_size(mut self, size: usize) -> Self {
152 self.config.batch_config.min_batch_size = size;
153 self
154 }
155
156 pub fn flush_interval(mut self, interval: Duration) -> Self {
158 self.config.batch_config.flush_interval = interval;
159 self
160 }
161
162 pub fn policy_timeout(mut self, timeout: Duration) -> Self {
164 self.config.policy_timeout = timeout;
165 self
166 }
167
168 pub fn auto_chain(mut self, enabled: bool) -> Self {
170 self.config.auto_chain = enabled;
171 self
172 }
173
174 pub fn sweep_interval(mut self, interval: Duration) -> Self {
176 self.config.sweep_interval = interval;
177 self
178 }
179
180 pub fn skip_transfers(mut self, skip: bool) -> Self {
185 self.config.skip_transfers = skip;
186 self
187 }
188
189 pub fn max_concurrent_transfers(mut self, n: usize) -> Self {
197 self.config.max_concurrent_transfers = n.max(1);
198 self
199 }
200
201 pub fn pending_tracker(mut self, tracker: Arc<PendingTracker>) -> Self {
206 self.config.pending_tracker = Some(tracker);
207 self
208 }
209
210 pub fn build(self) -> PipelineConfig<Src, Dst> {
212 self.config
213 }
214}
215
216impl<Src: BlockMetadata, Dst: BlockMetadata> Default for PipelineBuilder<Src, Dst> {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222pub(crate) struct PipelineInput<T: BlockMetadata> {
224 pub(crate) transfer_id: TransferId,
225 pub(crate) source: SourceBlocks<T>,
227 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
228}
229
230pub struct PipelineOutput {
232 pub transfer_id: TransferId,
233 pub completed_hashes: Vec<SequenceHash>,
234}
235
236pub struct ChainOutput<T: BlockMetadata> {
242 pub transfer_id: TransferId,
243 pub blocks: Vec<ImmutableBlock<T>>,
244 #[allow(dead_code)]
246 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
247}
248
249pub type ChainOutputRx<T> = mpsc::Receiver<ChainOutput<T>>;
251
252pub struct Pipeline<Src: BlockMetadata, Dst: BlockMetadata> {
254 config: PipelineConfig<Src, Dst>,
255 pub(crate) eval_queue: Arc<CancellableQueue<PipelineInput<Src>>>,
257 output_tx: Option<mpsc::Sender<PipelineOutput>>,
259 chain_rx: Option<ChainOutputRx<Dst>>,
261 cancel_tx: watch::Sender<HashSet<TransferId>>,
263 pending_tracker: Arc<PendingTracker>,
265 pub(crate) settlement: PipelineSettlementTracker,
267 _task_handles: Vec<JoinHandle<()>>,
269 _marker: PhantomData<Dst>,
271}
272
273impl<Src: BlockMetadata, Dst: BlockMetadata> Pipeline<Src, Dst> {
274 #[allow(clippy::too_many_arguments)]
285 pub fn new(
286 config: PipelineConfig<Src, Dst>,
287 _registry: Arc<BlockRegistry>,
288 dst_manager: Arc<BlockManager<Dst>>,
289 leader: Arc<InstanceLeader>,
290 src_layout: LogicalLayoutHandle,
291 dst_layout: LogicalLayoutHandle,
292 runtime: tokio::runtime::Handle,
293 ) -> Self {
294 let eval_queue: Arc<CancellableQueue<PipelineInput<Src>>> =
296 Arc::new(CancellableQueue::new());
297 let batch_queue: Arc<CancellableQueue<EvalResult<Src>>> = Arc::new(CancellableQueue::new());
298
299 let (output_tx, _output_rx) = mpsc::channel(64);
301
302 let (cancel_tx, cancel_rx) = watch::channel(HashSet::new());
304
305 let (batch_tx, batch_rx) = mpsc::channel(config.transfer_input_capacity);
307
308 let (precond_tx, precond_rx) = mpsc::channel(config.transfer_input_capacity);
310
311 let (chain_tx, chain_rx) = if config.auto_chain {
313 let (tx, rx) = mpsc::channel(64);
314 (Some(tx), Some(rx))
315 } else {
316 (None, None)
317 };
318
319 let pending_tracker = config
321 .pending_tracker
322 .clone()
323 .unwrap_or_else(|| Arc::new(PendingTracker::new()));
324 let settlement = PipelineSettlementTracker::new(config.max_concurrent_transfers);
325
326 let evaluator = PolicyEvaluator {
328 policies: config.policies.clone(),
329 timeout: config.policy_timeout,
330 input_queue: eval_queue.clone(),
331 output_queue: batch_queue.clone(),
332 cancel_rx: cancel_rx.clone(),
333 pending_tracker: pending_tracker.clone(),
334 };
335 let eval_handle = runtime.spawn(async move {
336 evaluator.run().await;
337 });
338
339 let collector_input_queue = batch_queue.clone();
341 let batch_config = config.batch_config.clone();
342 let collector_cancel_rx = cancel_rx.clone();
343 let batch_handle = runtime.spawn(async move {
344 let collector = BatchCollector::new(
345 batch_config,
346 collector_input_queue,
347 batch_tx,
348 collector_cancel_rx,
349 );
350 collector.run().await;
351 });
352
353 let awaiter_leader = leader.clone();
355 let awaiter_settlement = settlement.clone();
356 let precond_handle = runtime.spawn(async move {
357 let awaiter = PreconditionAwaiter {
358 input_rx: batch_rx,
359 output_tx: precond_tx,
360 leader: awaiter_leader,
361 settlement: awaiter_settlement,
362 };
363 awaiter.run().await;
364 });
365
366 let executor = BlockTransferExecutor {
368 input_rx: precond_rx,
369 leader,
370 dst_manager,
371 src_layout,
372 dst_layout,
373 skip_transfers: config.skip_transfers,
374 max_concurrent_transfers: config.max_concurrent_transfers,
375 chain_tx,
376 settlement: settlement.clone(),
377 _src_marker: PhantomData::<Src>,
378 };
379 let transfer_handle = runtime.spawn(async move {
380 executor.run().await;
381 });
382
383 let sweeper_queues = vec![eval_queue.clone()];
385 let sweeper_batch_queue = batch_queue;
386 let sweeper_interval = config.sweep_interval;
387 let sweeper_cancel_rx = cancel_rx;
388 let sweeper_handle = runtime.spawn(async move {
389 cancel_sweeper(
390 sweeper_queues,
391 sweeper_batch_queue,
392 sweeper_cancel_rx,
393 sweeper_interval,
394 )
395 .await;
396 });
397
398 Self {
399 config,
400 eval_queue,
401 output_tx: Some(output_tx),
402 chain_rx,
403 cancel_tx,
404 pending_tracker,
405 settlement,
406 _task_handles: vec![
407 eval_handle,
408 batch_handle,
409 precond_handle,
410 transfer_handle,
411 sweeper_handle,
412 ],
413 _marker: PhantomData,
414 }
415 }
416
417 pub(crate) fn enqueue(
419 &self,
420 transfer_id: TransferId,
421 source: SourceBlocks<Src>,
422 state: Arc<std::sync::Mutex<TransferState>>,
423 ) -> bool {
424 tracing::debug!(%transfer_id, num_blocks = source.len(), "Pipeline: enqueueing blocks");
425 let input = PipelineInput {
426 transfer_id,
427 source,
428 state,
429 };
430 self.eval_queue.push(transfer_id, input)
431 }
432
433 pub fn request_cancel(&self, transfer_id: TransferId) {
438 self.eval_queue.mark_cancelled(transfer_id);
440
441 self.cancel_tx.send_modify(|set| {
443 set.insert(transfer_id);
444 });
445 }
446
447 pub fn auto_chain(&self) -> bool {
449 self.config.auto_chain
450 }
451
452 pub fn output_tx(&self) -> Option<mpsc::Sender<PipelineOutput>> {
454 self.output_tx.clone()
455 }
456
457 pub fn take_chain_rx(&mut self) -> Option<ChainOutputRx<Dst>> {
467 self.chain_rx.take()
468 }
469
470 pub fn pending_tracker(&self) -> &Arc<PendingTracker> {
475 &self.pending_tracker
476 }
477}
478
479#[derive(Clone)]
488pub struct ObjectPipelineConfig<Src: BlockMetadata> {
489 pub policies: Vec<Arc<dyn OffloadPolicy<Src>>>,
491 pub batch_config: BatchConfig,
493 pub policy_timeout: Duration,
495 pub eval_input_capacity: usize,
497 pub batch_input_capacity: usize,
499 pub transfer_input_capacity: usize,
501 pub sweep_interval: Duration,
503 pub skip_transfers: bool,
505 pub max_concurrent_transfers: usize,
507 pub pending_tracker: Option<Arc<PendingTracker>>,
509 pub max_concurrent_precondition_awaits: usize,
511 pub lock_manager: Option<Arc<dyn ObjectLockManager>>,
517 _marker: PhantomData<Src>,
519}
520
521impl<Src: BlockMetadata> Default for ObjectPipelineConfig<Src> {
522 fn default() -> Self {
523 Self {
524 policies: Vec::new(),
525 batch_config: BatchConfig::default(),
526 policy_timeout: Duration::from_millis(100),
527 eval_input_capacity: 128,
528 batch_input_capacity: 256,
529 transfer_input_capacity: 8,
530 sweep_interval: Duration::from_millis(10),
531 skip_transfers: false,
532 max_concurrent_transfers: 1,
533 pending_tracker: None,
534 max_concurrent_precondition_awaits: 8,
535 lock_manager: None,
536 _marker: PhantomData,
537 }
538 }
539}
540
541pub struct ObjectPipelineBuilder<Src: BlockMetadata> {
543 config: ObjectPipelineConfig<Src>,
544}
545
546impl<Src: BlockMetadata> ObjectPipelineBuilder<Src> {
547 pub fn new() -> Self {
549 Self {
550 config: ObjectPipelineConfig::default(),
551 }
552 }
553
554 pub fn policy(mut self, policy: Arc<dyn OffloadPolicy<Src>>) -> Self {
556 self.config.policies.push(policy);
557 self
558 }
559
560 pub fn batch_size(mut self, size: usize) -> Self {
562 self.config.batch_config.max_batch_size = size;
563 self
564 }
565
566 pub fn min_batch_size(mut self, size: usize) -> Self {
568 self.config.batch_config.min_batch_size = size;
569 self
570 }
571
572 pub fn flush_interval(mut self, interval: Duration) -> Self {
574 self.config.batch_config.flush_interval = interval;
575 self
576 }
577
578 pub fn policy_timeout(mut self, timeout: Duration) -> Self {
580 self.config.policy_timeout = timeout;
581 self
582 }
583
584 pub fn sweep_interval(mut self, interval: Duration) -> Self {
586 self.config.sweep_interval = interval;
587 self
588 }
589
590 pub fn skip_transfers(mut self, skip: bool) -> Self {
592 self.config.skip_transfers = skip;
593 self
594 }
595
596 pub fn max_concurrent_transfers(mut self, n: usize) -> Self {
598 self.config.max_concurrent_transfers = n.max(1);
599 self
600 }
601
602 pub fn pending_tracker(mut self, tracker: Arc<PendingTracker>) -> Self {
604 self.config.pending_tracker = Some(tracker);
605 self
606 }
607
608 pub fn lock_manager(mut self, manager: Arc<dyn ObjectLockManager>) -> Self {
613 self.config.lock_manager = Some(manager);
614 self
615 }
616
617 pub fn build(self) -> ObjectPipelineConfig<Src> {
619 self.config
620 }
621}
622
623impl<Src: BlockMetadata> Default for ObjectPipelineBuilder<Src> {
624 fn default() -> Self {
625 Self::new()
626 }
627}
628
629#[allow(dead_code)]
634pub struct ObjectPipeline<Src: BlockMetadata> {
635 config: ObjectPipelineConfig<Src>,
636 pub(crate) eval_queue: Arc<CancellableQueue<PipelineInput<Src>>>,
638 output_tx: Option<mpsc::Sender<PipelineOutput>>,
640 cancel_tx: watch::Sender<HashSet<TransferId>>,
642 pending_tracker: Arc<PendingTracker>,
644 pub(crate) settlement: PipelineSettlementTracker,
646 _task_handles: Vec<JoinHandle<()>>,
648}
649
650impl<Src: BlockMetadata> ObjectPipeline<Src> {
651 #[allow(clippy::too_many_arguments)]
660 pub fn new(
661 config: ObjectPipelineConfig<Src>,
662 object_ops: Arc<dyn ObjectBlockOps>,
663 src_layout: LogicalLayoutHandle,
664 leader: Arc<InstanceLeader>,
665 runtime: tokio::runtime::Handle,
666 ) -> Self {
667 let eval_queue: Arc<CancellableQueue<PipelineInput<Src>>> =
669 Arc::new(CancellableQueue::new());
670 let batch_queue: Arc<CancellableQueue<EvalResult<Src>>> = Arc::new(CancellableQueue::new());
671
672 let (output_tx, _output_rx) = mpsc::channel(64);
674
675 let (cancel_tx, cancel_rx) = watch::channel(HashSet::new());
677
678 let (batch_tx, batch_rx) = mpsc::channel(config.transfer_input_capacity);
680
681 let (precond_tx, precond_rx) = mpsc::channel(config.transfer_input_capacity);
683
684 let pending_tracker = config
686 .pending_tracker
687 .clone()
688 .unwrap_or_else(|| Arc::new(PendingTracker::new()));
689 let settlement = PipelineSettlementTracker::new(config.max_concurrent_transfers);
690
691 let evaluator = PolicyEvaluator {
693 policies: config.policies.clone(),
694 timeout: config.policy_timeout,
695 input_queue: eval_queue.clone(),
696 output_queue: batch_queue.clone(),
697 cancel_rx: cancel_rx.clone(),
698 pending_tracker: pending_tracker.clone(),
699 };
700 let eval_handle = runtime.spawn(async move {
701 evaluator.run().await;
702 });
703
704 let collector_input_queue = batch_queue.clone();
706 let batch_config = config.batch_config.clone();
707 let collector_cancel_rx = cancel_rx.clone();
708 let batch_handle = runtime.spawn(async move {
709 let collector = BatchCollector::new(
710 batch_config,
711 collector_input_queue,
712 batch_tx,
713 collector_cancel_rx,
714 );
715 collector.run().await;
716 });
717
718 let awaiter_leader = leader.clone();
720 let awaiter_settlement = settlement.clone();
721 let precond_handle = runtime.spawn(async move {
722 let awaiter = PreconditionAwaiter {
723 input_rx: batch_rx,
724 output_tx: precond_tx,
725 leader: awaiter_leader,
726 settlement: awaiter_settlement,
727 };
728 awaiter.run().await;
729 });
730
731 let executor = ObjectTransferExecutor::new(
733 precond_rx,
734 object_ops,
735 src_layout,
736 config.skip_transfers,
737 config.max_concurrent_transfers,
738 config.lock_manager.clone(),
739 settlement.clone(),
740 );
741 let transfer_handle = runtime.spawn(async move {
742 executor.run().await;
743 });
744
745 let sweeper_queues = vec![eval_queue.clone()];
747 let sweeper_batch_queue = batch_queue;
748 let sweeper_interval = config.sweep_interval;
749 let sweeper_cancel_rx = cancel_rx;
750 let sweeper_handle = runtime.spawn(async move {
751 cancel_sweeper(
752 sweeper_queues,
753 sweeper_batch_queue,
754 sweeper_cancel_rx,
755 sweeper_interval,
756 )
757 .await;
758 });
759
760 Self {
761 config,
762 eval_queue,
763 output_tx: Some(output_tx),
764 cancel_tx,
765 pending_tracker,
766 settlement,
767 _task_handles: vec![
768 eval_handle,
769 batch_handle,
770 precond_handle,
771 transfer_handle,
772 sweeper_handle,
773 ],
774 }
775 }
776
777 pub(crate) fn enqueue(
779 &self,
780 transfer_id: TransferId,
781 source: SourceBlocks<Src>,
782 state: Arc<std::sync::Mutex<TransferState>>,
783 ) -> bool {
784 tracing::debug!(%transfer_id, num_blocks = source.len(), "ObjectPipeline: enqueueing blocks");
785 let input = PipelineInput {
786 transfer_id,
787 source,
788 state,
789 };
790 self.eval_queue.push(transfer_id, input)
791 }
792
793 pub fn request_cancel(&self, transfer_id: TransferId) {
795 self.eval_queue.mark_cancelled(transfer_id);
796 self.cancel_tx.send_modify(|set| {
797 set.insert(transfer_id);
798 });
799 }
800
801 #[allow(dead_code)]
803 pub fn output_tx(&self) -> Option<mpsc::Sender<PipelineOutput>> {
804 self.output_tx.clone()
805 }
806
807 pub fn pending_tracker(&self) -> &Arc<PendingTracker> {
809 &self.pending_tracker
810 }
811}
812
813async fn cancel_sweeper<Src: BlockMetadata>(
815 input_queues: Vec<Arc<CancellableQueue<PipelineInput<Src>>>>,
816 batch_queue: Arc<CancellableQueue<EvalResult<Src>>>,
817 mut cancel_rx: watch::Receiver<HashSet<TransferId>>,
818 interval: Duration,
819) {
820 let mut ticker = tokio::time::interval(interval);
821 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
822
823 loop {
824 tokio::select! {
825 _ = ticker.tick() => {
826 for queue in &input_queues {
828 let removed = queue.sweep();
829 if removed > 0 {
830 tracing::debug!("Sweeper removed {} cancelled input items", removed);
831 }
832 }
833
834 let batch_removed = batch_queue.sweep();
835 if batch_removed > 0 {
836 tracing::debug!("Sweeper removed {} cancelled batch items", batch_removed);
837 }
838 }
839 result = cancel_rx.changed() => {
840 if result.is_err() {
841 break;
843 }
844 for queue in &input_queues {
846 queue.sweep();
847 }
848 batch_queue.sweep();
849 }
850 }
851 }
852}
853
854struct PolicyEvaluator<T: BlockMetadata> {
856 policies: Vec<Arc<dyn OffloadPolicy<T>>>,
857 timeout: Duration,
858 input_queue: Arc<CancellableQueue<PipelineInput<T>>>,
859 output_queue: Arc<CancellableQueue<EvalResult<T>>>,
860 cancel_rx: watch::Receiver<HashSet<TransferId>>,
861 pending_tracker: Arc<PendingTracker>,
863}
864
865impl<T: BlockMetadata> PolicyEvaluator<T> {
866 async fn run(mut self) {
867 loop {
868 while let Some(item) = self.input_queue.pop_valid() {
869 self.evaluate(item.data).await;
870 }
871
872 tokio::select! {
873 _ = self.input_queue.notified() => {}
874 result = self.cancel_rx.changed() => {
875 if result.is_err() {
876 break;
877 }
878 }
879 }
880 }
881 }
882
883 async fn evaluate(&self, input: PipelineInput<T>) {
884 nvtx_range!("offload::policy");
885 let transfer_id = input.transfer_id;
886
887 let total_blocks = input.source.len();
889 {
890 let mut state = input.state.lock().unwrap();
891 state.total_expected_blocks = total_blocks;
892 }
893
894 {
896 let state = input.state.lock().unwrap();
897 if state.is_cancel_requested() {
898 drop(state); tracing::debug!(%transfer_id, "Transfer cancelled before evaluation");
900 let mut state = input.state.lock().unwrap();
901 state.set_cancelled();
902 return;
903 }
904 }
905
906 let mut passed = Vec::new();
907 let mut filtered = Vec::new();
908
909 match input.source {
911 SourceBlocks::External(external_blocks) => {
912 for ext in external_blocks {
915 if self.check_cancelled(&input.state, transfer_id) {
917 return;
918 }
919
920 let ctx = EvalContext::from_external(ext.block_id, ext.sequence_hash);
922 let pass = self.evaluate_policies(&ctx).await;
923
924 if pass {
925 let pending_guard = self.pending_tracker.guard(ext.sequence_hash);
927 passed.push(QueuedBlock {
928 transfer_id,
929 block_id: Some(ext.block_id),
930 sequence_hash: ext.sequence_hash,
931 source: SourceBlock::External(ext),
932 state: input.state.clone(),
933 pending_guard: Some(pending_guard),
934 });
935 } else {
936 filtered.push(ext.block_id);
937 }
938 }
939 tracing::debug!(%transfer_id, passed = passed.len(), filtered = filtered.len(), "External blocks evaluated");
940 }
941 SourceBlocks::Strong(strong_blocks) => {
942 for block in strong_blocks {
944 if self.check_cancelled(&input.state, transfer_id) {
946 return;
947 }
948
949 let ctx = EvalContext::new(block);
950 let pass = self.evaluate_policies(&ctx).await;
951
952 if pass {
953 let block = ctx.block.expect("Strong block context always has block");
954 let pending_guard = self.pending_tracker.guard(ctx.sequence_hash);
956 passed.push(QueuedBlock {
957 transfer_id,
958 block_id: Some(ctx.block_id),
959 sequence_hash: ctx.sequence_hash,
960 source: SourceBlock::Strong(block),
961 state: input.state.clone(),
962 pending_guard: Some(pending_guard),
963 });
964 } else {
965 filtered.push(ctx.block_id);
966 }
967 }
968 }
969 SourceBlocks::Weak(weak_blocks) => {
970 for weak in weak_blocks {
973 if self.check_cancelled(&input.state, transfer_id) {
975 return;
976 }
977
978 let sequence_hash = weak.sequence_hash();
979 let ctx = EvalContext::from_weak(BlockId::default(), sequence_hash);
980 let pass = self.evaluate_policies(&ctx).await;
981
982 if pass {
983 let pending_guard = self.pending_tracker.guard(sequence_hash);
985 passed.push(QueuedBlock {
986 transfer_id,
987 block_id: None, sequence_hash,
989 source: SourceBlock::Weak(weak),
990 state: input.state.clone(),
991 pending_guard: Some(pending_guard),
992 });
993 } else {
994 tracing::debug!(%transfer_id, ?sequence_hash, "Weak block filtered by policy");
997 }
998 }
999 }
1000 }
1001
1002 {
1004 let state = input.state.lock().unwrap();
1005 if state.is_cancel_requested() {
1006 drop(state);
1007 tracing::debug!(%transfer_id, "Transfer cancelled after evaluation");
1008 let mut state = input.state.lock().unwrap();
1009 state.set_cancelled();
1010 return;
1011 }
1012 }
1013
1014 tracing::debug!(%transfer_id, passed = passed.len(), filtered = filtered.len(), "Policy evaluation complete");
1015
1016 {
1018 let mut state = input.state.lock().unwrap();
1019 state.add_passed(passed.iter().filter_map(|b| b.block_id));
1022 state.add_filtered(filtered.iter().copied());
1023 state.set_status(TransferStatus::Queued);
1024 }
1025
1026 if passed.is_empty() {
1028 tracing::debug!(%transfer_id, "All blocks filtered, completing transfer");
1029 let mut state = input.state.lock().unwrap();
1030 state.set_complete();
1031 return;
1032 }
1033
1034 let result = EvalResult {
1036 transfer_id,
1037 passed_blocks: passed,
1038 filtered_ids: filtered,
1039 state: input.state,
1040 };
1041
1042 if !self.output_queue.push(transfer_id, result) {
1043 tracing::debug!(%transfer_id, "Push to output queue failed (cancelled)");
1044 }
1045 }
1046
1047 fn check_cancelled(
1049 &self,
1050 state: &Arc<std::sync::Mutex<TransferState>>,
1051 transfer_id: TransferId,
1052 ) -> bool {
1053 let state_guard = state.lock().unwrap();
1054 if state_guard.is_cancel_requested() {
1055 drop(state_guard);
1056 tracing::debug!(%transfer_id, "Transfer cancelled mid-evaluation");
1057 let mut state_guard = state.lock().unwrap();
1058 state_guard.set_cancelled();
1059 true
1060 } else {
1061 false
1062 }
1063 }
1064
1065 async fn evaluate_policies(&self, ctx: &EvalContext<T>) -> bool {
1066 for policy in &self.policies {
1067 let eval_future = policy.evaluate(ctx);
1068 let timed_result = tokio::time::timeout(self.timeout, async {
1069 match eval_future {
1070 Either::Left(ready) => ready.await,
1071 Either::Right(boxed) => boxed.await,
1072 }
1073 })
1074 .await;
1075
1076 match timed_result {
1077 Ok(Ok(true)) => continue,
1078 Ok(Ok(false)) => return false,
1079 Ok(Err(e)) => {
1080 tracing::warn!("Policy {} error: {}", policy.name(), e);
1081 return false;
1082 }
1083 Err(_) => {
1084 tracing::warn!("Policy {} timed out", policy.name());
1085 return false;
1086 }
1087 }
1088 }
1089 true
1090 }
1091}
1092
1093pub struct ResolvedBlock<T: BlockMetadata> {
1103 pub transfer_id: TransferId,
1105 pub block_id: BlockId,
1107 pub sequence_hash: SequenceHash,
1109 #[allow(dead_code)]
1112 pub guard: Option<ImmutableBlock<T>>,
1113 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
1115}
1116
1117pub struct ResolvedBatch<T: BlockMetadata> {
1121 pub blocks: Vec<ResolvedBlock<T>>,
1123 #[allow(dead_code)]
1125 pub evicted: Vec<SequenceHash>,
1126 pub timing: TimingTrace,
1128}
1129
1130impl<T: BlockMetadata> ResolvedBatch<T> {
1131 pub fn is_empty(&self) -> bool {
1133 self.blocks.is_empty()
1134 }
1135
1136 #[allow(dead_code)]
1138 pub fn len(&self) -> usize {
1139 self.blocks.len()
1140 }
1141}
1142
1143pub fn upgrade_batch<T: BlockMetadata>(batch: TransferBatch<T>) -> ResolvedBatch<T> {
1158 let mut resolved: Vec<ResolvedBlock<T>> = Vec::with_capacity(batch.len());
1159 let mut evicted: Vec<SequenceHash> = Vec::new();
1160
1161 let mut timing = batch.timing;
1163 timing.mark_transfer_start();
1164
1165 for queued in batch.blocks {
1166 match queued.source {
1170 SourceBlock::Strong(block) => {
1171 resolved.push(ResolvedBlock {
1172 transfer_id: queued.transfer_id,
1173 block_id: block.block_id(),
1174 sequence_hash: queued.sequence_hash,
1175 guard: Some(block),
1176 state: queued.state,
1177 });
1178 }
1179 SourceBlock::External(ext) => {
1180 resolved.push(ResolvedBlock {
1181 transfer_id: queued.transfer_id,
1182 block_id: ext.block_id,
1183 sequence_hash: ext.sequence_hash,
1184 guard: None,
1185 state: queued.state,
1186 });
1187 }
1188 SourceBlock::Weak(weak) => match weak.upgrade() {
1189 Some(block) => {
1190 resolved.push(ResolvedBlock {
1191 transfer_id: queued.transfer_id,
1192 block_id: block.block_id(),
1193 sequence_hash: queued.sequence_hash,
1194 guard: Some(block),
1195 state: queued.state,
1196 });
1197 }
1198 None => {
1199 tracing::debug!(
1200 sequence_hash = ?queued.sequence_hash,
1201 "Weak block evicted before transfer"
1202 );
1203 evicted.push(queued.sequence_hash);
1204 }
1205 },
1206 }
1207 }
1208
1209 ResolvedBatch {
1210 blocks: resolved,
1211 evicted,
1212 timing,
1213 }
1214}
1215
1216struct PreconditionAwaiter<T: BlockMetadata> {
1227 input_rx: BatchOutputRx<T>,
1228 output_tx: mpsc::Sender<TransferBatch<T>>,
1229 leader: Arc<InstanceLeader>,
1230 settlement: PipelineSettlementTracker,
1231}
1232
1233impl<T: BlockMetadata> PreconditionAwaiter<T> {
1234 async fn run(mut self) {
1235 while let Some(mut batch) = self.input_rx.recv().await {
1238 let output_tx = self.output_tx.clone();
1239 let nova = self.leader.messenger().clone();
1240 let settlement = self.settlement.clone();
1241
1242 tokio::spawn(async move {
1244 nvtx_range!("offload::precondition");
1245 if let Some(event_handle) = batch.precondition {
1246 tracing::debug!(?event_handle, "Awaiting precondition for batch");
1247
1248 let awaiter_result = nova.events().awaiter(event_handle);
1250
1251 match awaiter_result {
1252 Ok(awaiter) => {
1253 match tokio::time::timeout(Duration::from_secs(300), awaiter).await {
1255 Ok(Ok(())) => {
1256 tracing::debug!(?event_handle, "Precondition satisfied");
1257 }
1258 Ok(Err(poison)) => {
1259 tracing::error!(
1260 ?event_handle,
1261 ?poison,
1262 "Precondition poisoned, marking all blocks as failed"
1263 );
1264 for queued in batch.blocks {
1266 let mut state = queued.state.lock().unwrap();
1267 state.set_error(format!(
1268 "precondition poisoned: {:?}",
1269 poison
1270 ));
1271 }
1272 return;
1273 }
1274 Err(_) => {
1275 tracing::error!(
1276 ?event_handle,
1277 "Precondition timeout after 30s"
1278 );
1279 for queued in batch.blocks {
1281 let mut state = queued.state.lock().unwrap();
1282 state.set_error("precondition timeout".to_string());
1283 }
1284 return;
1285 }
1286 }
1287 }
1288 Err(e) => {
1289 tracing::error!(?event_handle, ?e, "Failed to create awaiter");
1290 for queued in batch.blocks {
1292 let mut state = queued.state.lock().unwrap();
1293 state.set_error(format!("failed to create awaiter: {}", e));
1294 }
1295 return;
1296 }
1297 }
1298 }
1299
1300 batch.timing.mark_precondition_complete();
1302
1303 let queued = QueuedBatchGuard::new(settlement);
1305 if let Err(e) = output_tx.send(batch).await {
1306 tracing::error!("Failed to forward batch after precondition: {}", e);
1307 queued.finish_failure(PipelineFailure::new(
1308 PipelineFailureKind::Shutdown,
1309 "transfer executor input channel closed",
1310 ));
1311 } else {
1312 queued.sent();
1313 }
1314 });
1315 }
1316 }
1317}
1318
1319struct BlockTransferExecutor<Src: BlockMetadata, Dst: BlockMetadata> {
1330 input_rx: BatchOutputRx<Src>,
1331 leader: Arc<InstanceLeader>,
1332 dst_manager: Arc<BlockManager<Dst>>,
1333 src_layout: LogicalLayoutHandle,
1334 dst_layout: LogicalLayoutHandle,
1335 skip_transfers: bool,
1337 max_concurrent_transfers: usize,
1339 chain_tx: Option<mpsc::Sender<ChainOutput<Dst>>>,
1341 settlement: PipelineSettlementTracker,
1342 _src_marker: PhantomData<Src>,
1343}
1344
1345struct SharedBlockExecutorState<Dst: BlockMetadata> {
1347 leader: Arc<InstanceLeader>,
1348 dst_manager: Arc<BlockManager<Dst>>,
1349 src_layout: LogicalLayoutHandle,
1350 dst_layout: LogicalLayoutHandle,
1351 skip_transfers: bool,
1352 chain_tx: Option<mpsc::Sender<ChainOutput<Dst>>>,
1353}
1354
1355impl<Src: BlockMetadata, Dst: BlockMetadata> BlockTransferExecutor<Src, Dst> {
1356 async fn run(mut self) {
1357 let transfer_semaphore = Arc::new(Semaphore::new(self.max_concurrent_transfers));
1359 let prepare_semaphore = Arc::new(Semaphore::new(1));
1361
1362 let shared = Arc::new(SharedBlockExecutorState {
1364 leader: self.leader.clone(),
1365 dst_manager: self.dst_manager.clone(),
1366 src_layout: self.src_layout,
1367 dst_layout: self.dst_layout,
1368 skip_transfers: self.skip_transfers,
1369 chain_tx: self.chain_tx.take(),
1370 });
1371 let settlement = self.settlement.clone();
1372 let run_guard = PipelineRunGuard::new(settlement.clone(), "block transfer executor");
1373
1374 while let Some(batch) = self.input_rx.recv().await {
1375 if batch.is_empty() {
1376 settlement.discard_queued();
1377 continue;
1378 }
1379
1380 let prepare_permit = prepare_semaphore.clone().acquire_owned().await;
1383 if prepare_permit.is_err() {
1384 settlement.fail_queued(PipelineFailure::new(
1385 PipelineFailureKind::Shutdown,
1386 "block preparation semaphore closed",
1387 ));
1388 break; }
1390 let prepare_permit = prepare_permit.unwrap();
1391
1392 let upgraded = upgrade_batch(batch);
1395
1396 drop(prepare_permit);
1398
1399 if upgraded.is_empty() {
1400 tracing::debug!("All blocks in batch evicted, skipping transfer");
1401 settlement.discard_queued();
1402 continue;
1403 }
1404
1405 let transfer_permit = transfer_semaphore.clone().acquire_owned().await;
1407 if transfer_permit.is_err() {
1408 settlement.fail_queued(PipelineFailure::new(
1409 PipelineFailureKind::Shutdown,
1410 "block transfer semaphore closed",
1411 ));
1412 break; }
1414 let transfer_permit = transfer_permit.unwrap();
1415
1416 let shared_clone = shared.clone();
1418 let mut phase = BatchPhaseGuard::starting(settlement.clone(), transfer_permit);
1419 tokio::spawn(async move {
1420 match Self::execute_transfer(&shared_clone, upgraded, &mut phase).await {
1421 Ok(()) => phase.finish_success(),
1422 Err(error) => {
1423 tracing::error!("BlockTransferExecutor: transfer failed: {}", error);
1424 phase.finish_failure(PipelineFailure::new(
1425 PipelineFailureKind::Executor,
1426 error.to_string(),
1427 ));
1428 }
1429 }
1430 });
1431 }
1432
1433 let _ = transfer_semaphore
1435 .acquire_many(self.max_concurrent_transfers as u32)
1436 .await;
1437 run_guard.finish_shutdown();
1438 }
1439
1440 fn fail_transfer_states(
1441 transfer_states: &std::collections::HashMap<
1442 TransferId,
1443 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1444 >,
1445 error: &anyhow::Error,
1446 ) {
1447 let message = format!("block transfer failed: {error}");
1448 for (state, block_ids) in transfer_states.values() {
1449 let mut state = state.lock().unwrap();
1450 state.mark_failed(block_ids.iter().copied());
1451 state.set_error(message.clone());
1452 }
1453 }
1454
1455 async fn execute_transfer(
1459 shared: &SharedBlockExecutorState<Dst>,
1460 mut batch: ResolvedBatch<Src>,
1461 phase: &mut BatchPhaseGuard,
1462 ) -> anyhow::Result<()> {
1463 nvtx_range!("offload::transfer");
1464 if batch.is_empty() {
1465 return Ok(());
1466 }
1467
1468 let resolved = &batch.blocks;
1469
1470 let src_block_ids: Vec<BlockId> = resolved.iter().map(|b| b.block_id).collect();
1472 let sequence_hashes: Vec<SequenceHash> = resolved.iter().map(|b| b.sequence_hash).collect();
1473
1474 let mut transfer_states: std::collections::HashMap<
1476 TransferId,
1477 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1478 > = std::collections::HashMap::new();
1479 for block in resolved {
1480 transfer_states
1481 .entry(block.transfer_id)
1482 .or_insert_with(|| (block.state.clone(), Vec::new()))
1483 .1
1484 .push(block.block_id);
1485 }
1486
1487 if !shared.skip_transfers {
1489 let Some(dst_blocks) = shared.dst_manager.allocate_blocks(resolved.len()) else {
1491 let error =
1492 anyhow::anyhow!("failed to allocate {} destination blocks", resolved.len());
1493 Self::fail_transfer_states(&transfer_states, &error);
1494 return Err(error);
1495 };
1496
1497 let dst_block_ids: Vec<BlockId> = dst_blocks.iter().map(|b| b.block_id()).collect();
1498
1499 let start_xfer = Instant::now();
1501 let notification = match shared.leader.execute_local_transfer(
1502 shared.src_layout,
1503 shared.dst_layout,
1504 src_block_ids.clone(),
1505 dst_block_ids.clone(),
1506 TransferOptions::default(),
1507 ) {
1508 Ok(notification) => notification,
1509 Err(error) => {
1510 Self::fail_transfer_states(&transfer_states, &error);
1511 return Err(error);
1512 }
1513 };
1514
1515 for (state, block_ids) in transfer_states.values() {
1520 let mut state_guard = state.lock().unwrap();
1521 state_guard.set_status(TransferStatus::Transferring);
1522 state_guard.mark_in_flight(block_ids.iter().copied());
1523 }
1524 phase.mark_in_flight();
1525
1526 if let Err(error) = notification.await {
1528 Self::fail_transfer_states(&transfer_states, &error);
1529 return Err(error);
1530 }
1531 phase.mark_settling();
1532 let end_xfer = Instant::now();
1533
1534 let completed_blocks = dst_blocks
1536 .into_iter()
1537 .zip(sequence_hashes.iter())
1538 .map(|(dst_block, seq_hash)| {
1539 dst_block
1540 .stage(*seq_hash, shared.dst_manager.block_size())
1541 .expect("block size mismatch")
1542 })
1543 .collect();
1544 let registered_blocks: Vec<ImmutableBlock<Dst>> =
1545 shared.dst_manager.register_blocks(completed_blocks);
1546
1547 let registration_timepoint = Instant::now();
1548
1549 let unique_transfer_ids: std::collections::HashSet<_> =
1551 resolved.iter().map(|b| b.transfer_id).collect();
1552
1553 let policy_ms = batch
1554 .timing
1555 .policy_duration()
1556 .map(|d| d.as_millis() as u64)
1557 .unwrap_or(0);
1558 let precondition_ms = batch
1559 .timing
1560 .precondition_duration()
1561 .map(|d| d.as_millis() as u64)
1562 .unwrap_or(0);
1563 let total_ms = batch
1564 .timing
1565 .total_duration()
1566 .map(|d| d.as_millis() as u64)
1567 .unwrap_or(0);
1568
1569 tracing::info!(
1570 blocks = resolved.len(),
1571 containers = unique_transfer_ids.len(),
1572 policy_ms,
1573 precondition_ms,
1574 xfer_ms = end_xfer.duration_since(start_xfer).as_millis() as u64,
1575 registration_ms =
1576 registration_timepoint.duration_since(end_xfer).as_millis() as u64,
1577 total_ms,
1578 src = std::any::type_name::<Src>(),
1579 dst = std::any::type_name::<Dst>(),
1580 "Batch transfer complete"
1581 );
1582
1583 if let Some(chain_tx) = &shared.chain_tx {
1585 #[allow(clippy::type_complexity)]
1586 let mut chain_outputs: std::collections::HashMap<
1587 TransferId,
1588 (
1589 Arc<std::sync::Mutex<TransferState>>,
1590 Vec<ImmutableBlock<Dst>>,
1591 ),
1592 > = std::collections::HashMap::new();
1593
1594 for (registered, resolved_block) in
1595 registered_blocks.into_iter().zip(resolved.iter())
1596 {
1597 chain_outputs
1598 .entry(resolved_block.transfer_id)
1599 .or_insert_with(|| (resolved_block.state.clone(), Vec::new()))
1600 .1
1601 .push(registered);
1602 }
1603
1604 for (transfer_id, (state, blocks)) in chain_outputs {
1605 let output = ChainOutput {
1606 transfer_id,
1607 blocks,
1608 state,
1609 };
1610 if chain_tx.send(output).await.is_err() {
1611 tracing::warn!(
1612 %transfer_id,
1613 "Chain channel closed, downstream pipeline unavailable"
1614 );
1615 } else {
1616 tracing::debug!(
1617 %transfer_id,
1618 "Sent blocks to chain output for downstream processing"
1619 );
1620 }
1621 }
1622 }
1623 } else {
1624 phase.mark_in_flight();
1625 phase.mark_settling();
1626 for (state, block_ids) in transfer_states.values() {
1627 let mut state_guard = state.lock().unwrap();
1628 state_guard.set_status(TransferStatus::Transferring);
1629 state_guard.mark_in_flight(block_ids.iter().copied());
1630 }
1631 }
1632
1633 batch.timing.mark_transfer_complete();
1635
1636 for (transfer_id, (state, block_ids)) in transfer_states {
1638 let mut state_guard = state.lock().unwrap();
1639 state_guard.mark_completed(block_ids);
1640
1641 let progress = state_guard.progress_counts();
1642 let total = progress.passed + state_guard.filtered_out.len();
1643 let done = progress.completed + state_guard.filtered_out.len();
1644 tracing::debug!(
1645 %transfer_id,
1646 total,
1647 done,
1648 passed = progress.passed,
1649 filtered = state_guard.filtered_out.len(),
1650 completed = progress.completed,
1651 "Transfer batch progress"
1652 );
1653 if done >= total && total > 0 {
1654 state_guard.set_complete();
1655 }
1656 }
1657
1658 Ok(())
1659 }
1660}
1661
1662pub struct ObjectTransferExecutor<Src: BlockMetadata> {
1689 input_rx: BatchOutputRx<Src>,
1691 object_ops: Arc<dyn ObjectBlockOps>,
1693 src_layout: LogicalLayoutHandle,
1696 skip_transfers: bool,
1698 max_concurrent_transfers: usize,
1700 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1702 settlement: PipelineSettlementTracker,
1703}
1704
1705struct SharedObjectExecutorState {
1707 object_ops: Arc<dyn ObjectBlockOps>,
1708 src_layout: LogicalLayoutHandle,
1709 skip_transfers: bool,
1710 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1711}
1712
1713impl<Src: BlockMetadata> ObjectTransferExecutor<Src> {
1714 #[allow(dead_code)]
1716 pub fn new(
1717 input_rx: BatchOutputRx<Src>,
1718 object_ops: Arc<dyn ObjectBlockOps>,
1719 src_layout: LogicalLayoutHandle,
1720 skip_transfers: bool,
1721 max_concurrent_transfers: usize,
1722 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1723 settlement: PipelineSettlementTracker,
1724 ) -> Self {
1725 Self {
1726 input_rx,
1727 object_ops,
1728 src_layout,
1729 skip_transfers,
1730 max_concurrent_transfers,
1731 lock_manager,
1732 settlement,
1733 }
1734 }
1735
1736 pub async fn run(mut self) {
1738 let transfer_semaphore = Arc::new(Semaphore::new(self.max_concurrent_transfers));
1740 let prepare_semaphore = Arc::new(Semaphore::new(1));
1742
1743 let shared = Arc::new(SharedObjectExecutorState {
1745 object_ops: self.object_ops.clone(),
1746 src_layout: self.src_layout,
1747 skip_transfers: self.skip_transfers,
1748 lock_manager: self.lock_manager.clone(),
1749 });
1750 let settlement = self.settlement.clone();
1751 let run_guard = PipelineRunGuard::new(settlement.clone(), "object transfer executor");
1752
1753 while let Some(batch) = self.input_rx.recv().await {
1754 if batch.is_empty() {
1755 settlement.discard_queued();
1756 continue;
1757 }
1758
1759 let prepare_permit = prepare_semaphore.clone().acquire_owned().await;
1761 if prepare_permit.is_err() {
1762 settlement.fail_queued(PipelineFailure::new(
1763 PipelineFailureKind::Shutdown,
1764 "object preparation semaphore closed",
1765 ));
1766 break; }
1768 let prepare_permit = prepare_permit.unwrap();
1769
1770 let upgraded = upgrade_batch(batch);
1772
1773 drop(prepare_permit);
1775
1776 if upgraded.is_empty() {
1777 tracing::debug!("All blocks in batch evicted, skipping object transfer");
1778 settlement.discard_queued();
1779 continue;
1780 }
1781
1782 let transfer_permit = transfer_semaphore.clone().acquire_owned().await;
1784 if transfer_permit.is_err() {
1785 settlement.fail_queued(PipelineFailure::new(
1786 PipelineFailureKind::Shutdown,
1787 "object transfer semaphore closed",
1788 ));
1789 break; }
1791 let transfer_permit = transfer_permit.unwrap();
1792
1793 let shared_clone = shared.clone();
1795 let mut phase = BatchPhaseGuard::starting(settlement.clone(), transfer_permit);
1796 tokio::spawn(async move {
1797 match Self::execute_transfer(&shared_clone, upgraded, &mut phase).await {
1798 Ok(()) => phase.finish_success(),
1799 Err(error) => {
1800 tracing::error!("ObjectTransferExecutor: transfer failed: {}", error);
1801 phase.finish_failure(PipelineFailure::new(
1802 PipelineFailureKind::Executor,
1803 error.to_string(),
1804 ));
1805 }
1806 }
1807 });
1808 }
1809
1810 let _ = transfer_semaphore
1812 .acquire_many(self.max_concurrent_transfers as u32)
1813 .await;
1814 run_guard.finish_shutdown();
1815 }
1816
1817 async fn execute_transfer(
1819 shared: &SharedObjectExecutorState,
1820 mut batch: ResolvedBatch<Src>,
1821 phase: &mut BatchPhaseGuard,
1822 ) -> anyhow::Result<()> {
1823 nvtx_range!("offload::transfer");
1824 if batch.is_empty() {
1825 return Ok(());
1826 }
1827
1828 let resolved = &batch.blocks;
1829
1830 let keys: Vec<SequenceHash> = resolved.iter().map(|b| b.sequence_hash).collect();
1832 let block_ids: Vec<BlockId> = resolved.iter().map(|b| b.block_id).collect();
1833
1834 let mut transfer_states: std::collections::HashMap<
1836 TransferId,
1837 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1838 > = std::collections::HashMap::new();
1839 for block in resolved {
1840 transfer_states
1841 .entry(block.transfer_id)
1842 .or_insert_with(|| (block.state.clone(), Vec::new()))
1843 .1
1844 .push(block.block_id);
1845 }
1846
1847 let mut successful_hashes: Vec<SequenceHash> = Vec::new();
1849
1850 if !shared.skip_transfers {
1852 let mut put = shared
1854 .object_ops
1855 .put_blocks(keys.clone(), shared.src_layout, block_ids);
1856 let mut first_poll = true;
1857 let results = std::future::poll_fn(|cx| {
1858 if first_poll {
1859 first_poll = false;
1860 phase.mark_in_flight();
1861 }
1862 put.as_mut().poll(cx)
1863 })
1864 .await;
1865 phase.mark_settling();
1866
1867 if results.len() != keys.len() {
1870 tracing::error!(
1871 expected = keys.len(),
1872 actual = results.len(),
1873 "put_blocks returned mismatched result count"
1874 );
1875 for (_transfer_id, (state, block_ids)) in transfer_states {
1876 let mut state_guard = state.lock().unwrap();
1877 state_guard.mark_failed(block_ids);
1878 state_guard
1879 .set_error("put_blocks returned mismatched result count".to_string());
1880 }
1881 return Ok(());
1882 }
1883
1884 let mut success_count = 0;
1886 let mut fail_count = 0;
1887
1888 for result in results {
1889 match result {
1890 Ok(hash) => {
1891 success_count += 1;
1892 successful_hashes.push(hash);
1893 }
1894 Err(hash) => {
1895 fail_count += 1;
1896 tracing::warn!(?hash, "Failed to transfer block to object storage");
1897 }
1898 }
1899 }
1900
1901 if fail_count > 0 {
1902 tracing::warn!(
1903 success = success_count,
1904 failed = fail_count,
1905 "Object transfer partially failed"
1906 );
1907 } else {
1908 tracing::debug!(
1909 num_blocks = success_count,
1910 "Successfully transferred blocks to object storage"
1911 );
1912 }
1913
1914 if let Some(lock_manager) = &shared.lock_manager {
1920 for hash in &successful_hashes {
1921 if let Err(e) = lock_manager.create_meta(*hash).await {
1923 tracing::error!(?hash, error = %e, "Failed to create meta file");
1924 }
1925
1926 if let Err(e) = lock_manager.release_lock(*hash).await {
1928 tracing::error!(?hash, error = %e, "Failed to release lock");
1929 }
1930 }
1931 tracing::debug!(
1932 num_blocks = successful_hashes.len(),
1933 "Created meta files and released locks"
1934 );
1935 }
1936 } else {
1937 phase.mark_in_flight();
1938 phase.mark_settling();
1939 if let Some(lock_manager) = &shared.lock_manager {
1941 for hash in &keys {
1942 if let Err(e) = lock_manager.create_meta(*hash).await {
1943 tracing::error!(?hash, error = %e, "Failed to create meta file");
1944 }
1945 if let Err(e) = lock_manager.release_lock(*hash).await {
1946 tracing::error!(?hash, error = %e, "Failed to release lock");
1947 }
1948 }
1949 }
1950 }
1951
1952 batch.timing.mark_transfer_complete();
1954
1955 let unique_transfer_ids: std::collections::HashSet<_> =
1957 resolved.iter().map(|b| b.transfer_id).collect();
1958
1959 let policy_ms = batch
1960 .timing
1961 .policy_duration()
1962 .map(|d| d.as_millis() as u64)
1963 .unwrap_or(0);
1964 let precondition_ms = batch
1965 .timing
1966 .precondition_duration()
1967 .map(|d| d.as_millis() as u64)
1968 .unwrap_or(0);
1969 let transfer_ms = batch
1970 .timing
1971 .transfer_duration()
1972 .map(|d| d.as_millis() as u64)
1973 .unwrap_or(0);
1974 let total_ms = batch
1975 .timing
1976 .total_duration()
1977 .map(|d| d.as_millis() as u64)
1978 .unwrap_or(0);
1979
1980 tracing::info!(
1981 blocks = resolved.len(),
1982 containers = unique_transfer_ids.len(),
1983 policy_ms,
1984 precondition_ms,
1985 transfer_ms,
1986 total_ms,
1987 src = std::any::type_name::<Src>(),
1988 dst = "G4-object",
1989 "Object batch transfer complete"
1990 );
1991
1992 let block_to_hash: std::collections::HashMap<BlockId, SequenceHash> = resolved
2001 .iter()
2002 .map(|b| (b.block_id, b.sequence_hash))
2003 .collect();
2004 let success_set: std::collections::HashSet<SequenceHash> =
2005 successful_hashes.into_iter().collect();
2006
2007 debug_assert_eq!(
2008 block_to_hash.len(),
2009 resolved.len(),
2010 "duplicate BlockId in batch — block_to_hash would lose entries"
2011 );
2012 debug_assert_eq!(
2013 resolved
2014 .iter()
2015 .map(|b| b.sequence_hash)
2016 .collect::<std::collections::HashSet<_>>()
2017 .len(),
2018 resolved.len(),
2019 "duplicate SequenceHash in batch — hash-based success correlation is ambiguous"
2020 );
2021
2022 for (transfer_id, (state, block_ids)) in transfer_states {
2024 let mut state_guard = state.lock().unwrap();
2025
2026 if shared.skip_transfers {
2027 state_guard.mark_completed(block_ids);
2029 } else {
2030 let (succeeded, failed): (Vec<_>, Vec<_>) = block_ids.into_iter().partition(|id| {
2031 block_to_hash
2032 .get(id)
2033 .is_some_and(|h| success_set.contains(h))
2034 });
2035 state_guard.mark_completed(succeeded);
2036 if !failed.is_empty() {
2037 tracing::warn!(
2038 %transfer_id,
2039 failed_count = failed.len(),
2040 "Marking blocks as failed in transfer state"
2041 );
2042 state_guard.mark_failed(failed);
2043 }
2044 }
2045
2046 let progress = state_guard.progress_counts();
2047 let total = progress.passed + state_guard.filtered_out.len();
2048 let done = progress.settled() + state_guard.filtered_out.len();
2049 tracing::debug!(
2050 %transfer_id,
2051 total,
2052 done,
2053 passed = progress.passed,
2054 filtered = state_guard.filtered_out.len(),
2055 completed = progress.completed,
2056 failed = progress.failed,
2057 "Object transfer batch progress"
2058 );
2059 if done >= total && total > 0 {
2060 let failed_count = progress.failed;
2061 if failed_count == 0 {
2062 state_guard.set_complete();
2063 } else {
2064 state_guard.set_error(format!(
2065 "{failed_count} blocks failed to transfer to object storage",
2066 ));
2067 }
2068 }
2069 }
2070
2071 Ok(())
2072 }
2073}
2074
2075#[cfg(test)]
2076mod tests {
2077 use futures::FutureExt;
2078
2079 use super::*;
2080
2081 #[test]
2082 fn test_pipeline_builder() {
2083 let config = PipelineBuilder::<(), ()>::new()
2084 .batch_size(32)
2085 .min_batch_size(8)
2086 .policy_timeout(Duration::from_millis(50))
2087 .auto_chain(true)
2088 .sweep_interval(Duration::from_millis(5))
2089 .build();
2090
2091 assert_eq!(config.batch_config.max_batch_size, 32);
2092 assert_eq!(config.batch_config.min_batch_size, 8);
2093 assert_eq!(config.policy_timeout, Duration::from_millis(50));
2094 assert!(config.auto_chain);
2095 assert_eq!(config.sweep_interval, Duration::from_millis(5));
2096 }
2097
2098 #[test]
2099 fn test_pipeline_config_default() {
2100 let config = PipelineConfig::<(), ()>::default();
2101 assert!(config.policies.is_empty());
2102 assert!(!config.auto_chain);
2103 assert_eq!(config.sweep_interval, Duration::from_millis(10));
2104 }
2105
2106 struct FailableObjectBlockOps {
2108 fail_hashes: std::collections::HashSet<SequenceHash>,
2109 }
2110
2111 impl crate::object::ObjectBlockOps for FailableObjectBlockOps {
2112 fn has_blocks(
2113 &self,
2114 keys: Vec<SequenceHash>,
2115 ) -> futures::future::BoxFuture<'static, Vec<(SequenceHash, Option<usize>)>> {
2116 Box::pin(async move { keys.into_iter().map(|h| (h, Some(1))).collect() })
2117 }
2118
2119 fn put_blocks(
2120 &self,
2121 keys: Vec<SequenceHash>,
2122 _layout: LogicalLayoutHandle,
2123 _block_ids: Vec<BlockId>,
2124 ) -> futures::future::BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
2125 let fail_set = self.fail_hashes.clone();
2126 Box::pin(async move {
2127 keys.into_iter()
2128 .map(|h| if fail_set.contains(&h) { Err(h) } else { Ok(h) })
2129 .collect()
2130 })
2131 }
2132
2133 fn get_blocks(
2134 &self,
2135 keys: Vec<SequenceHash>,
2136 _layout: LogicalLayoutHandle,
2137 _block_ids: Vec<BlockId>,
2138 ) -> futures::future::BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
2139 Box::pin(async move { keys.into_iter().map(Ok).collect() })
2140 }
2141 }
2142
2143 fn test_hash(n: u64) -> SequenceHash {
2144 SequenceHash::new(n, None, 0)
2145 }
2146
2147 async fn test_phase_guard() -> BatchPhaseGuard {
2148 let tracker = PipelineSettlementTracker::new(1);
2149 tracker.queue_batch();
2150 let permit = Arc::new(Semaphore::new(1))
2151 .acquire_owned()
2152 .await
2153 .expect("test semaphore should remain open");
2154 BatchPhaseGuard::starting(tracker, permit)
2155 }
2156
2157 #[test]
2158 fn block_transfer_failure_finalizes_queued_and_in_flight_handles() {
2159 let mut transfer_states = std::collections::HashMap::new();
2160 let mut handles = Vec::new();
2161
2162 for (block_id, status) in [
2163 (7, TransferStatus::Queued),
2164 (11, TransferStatus::Transferring),
2165 ] {
2166 let transfer_id = TransferId::new();
2167 let (mut state, handle) = TransferState::new(transfer_id, vec![block_id]);
2168 state.add_passed([block_id]);
2169 state.set_status(status);
2170 if status == TransferStatus::Transferring {
2171 state.mark_in_flight([block_id]);
2172 }
2173 let state = Arc::new(std::sync::Mutex::new(state));
2174 transfer_states.insert(transfer_id, (state.clone(), vec![block_id]));
2175 handles.push((block_id, state, handle));
2176 }
2177
2178 let error = anyhow::anyhow!("injected block transfer failure");
2179 BlockTransferExecutor::<crate::G1, crate::G2>::fail_transfer_states(
2180 &transfer_states,
2181 &error,
2182 );
2183
2184 for (block_id, state, mut handle) in handles {
2185 let result = handle
2186 .wait()
2187 .now_or_never()
2188 .expect("failed transfer handle must be ready")
2189 .expect("failed transfer must publish a result");
2190 assert_eq!(result.status, TransferStatus::Failed);
2191 assert_eq!(result.failed_blocks, vec![block_id]);
2192 assert!(result.completed_blocks.is_empty());
2193 assert_eq!(
2194 result.error.as_deref(),
2195 Some("block transfer failed: injected block transfer failure")
2196 );
2197 assert!(state.lock().unwrap().in_flight.is_empty());
2198 }
2199 }
2200
2201 #[tokio::test]
2202 async fn test_execute_transfer_partial_failure() {
2203 use crate::offload::handle::{TransferState, TransferStatus};
2204
2205 let hash_ok_1 = test_hash(1);
2206 let hash_fail = test_hash(2);
2207 let hash_ok_2 = test_hash(3);
2208
2209 let fail_hashes = [hash_fail].into_iter().collect();
2210 let object_ops: Arc<dyn crate::object::ObjectBlockOps> =
2211 Arc::new(FailableObjectBlockOps { fail_hashes });
2212
2213 let shared = SharedObjectExecutorState {
2214 object_ops,
2215 src_layout: LogicalLayoutHandle::G2,
2216 skip_transfers: false,
2217 lock_manager: None,
2218 };
2219
2220 let transfer_id = crate::offload::handle::TransferId::new();
2221 let (mut state, handle) = TransferState::new(transfer_id, vec![10, 20, 30]);
2222 state.add_passed(vec![10, 20, 30]);
2223 state.mark_in_flight(vec![10, 20, 30]);
2224 let state_arc = Arc::new(std::sync::Mutex::new(state));
2225
2226 let blocks = vec![
2227 ResolvedBlock::<crate::G2> {
2228 transfer_id,
2229 block_id: 10,
2230 sequence_hash: hash_ok_1,
2231 guard: None,
2232 state: state_arc.clone(),
2233 },
2234 ResolvedBlock::<crate::G2> {
2235 transfer_id,
2236 block_id: 20,
2237 sequence_hash: hash_fail,
2238 guard: None,
2239 state: state_arc.clone(),
2240 },
2241 ResolvedBlock::<crate::G2> {
2242 transfer_id,
2243 block_id: 30,
2244 sequence_hash: hash_ok_2,
2245 guard: None,
2246 state: state_arc.clone(),
2247 },
2248 ];
2249
2250 let mut timing = TimingTrace::new();
2251 timing.mark_policy_complete();
2252 timing.mark_precondition_complete();
2253
2254 let batch = ResolvedBatch {
2255 blocks,
2256 evicted: Vec::new(),
2257 timing,
2258 };
2259
2260 let mut phase = test_phase_guard().await;
2261 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch, &mut phase)
2262 .await
2263 .expect("execute_transfer should succeed");
2264 phase.finish_success();
2265
2266 let state_guard = state_arc.lock().unwrap();
2268 assert_eq!(handle.completed_blocks(), vec![10, 30]);
2269 assert_eq!(handle.failed_blocks(), vec![20]);
2270 assert_eq!(state_guard.in_flight.len(), 0);
2271 assert_eq!(state_guard.status, TransferStatus::Failed);
2272 assert!(state_guard.error.is_some());
2273
2274 drop(state_guard);
2276 assert_eq!(handle.completed_blocks(), vec![10, 30]);
2277 assert_eq!(handle.failed_blocks(), vec![20]);
2278 }
2279
2280 #[tokio::test]
2281 async fn test_execute_transfer_all_success() {
2282 use crate::offload::handle::{TransferState, TransferStatus};
2283
2284 let hash1 = test_hash(1);
2285 let hash2 = test_hash(2);
2286
2287 let object_ops: Arc<dyn crate::object::ObjectBlockOps> = Arc::new(FailableObjectBlockOps {
2288 fail_hashes: std::collections::HashSet::new(),
2289 });
2290
2291 let shared = SharedObjectExecutorState {
2292 object_ops,
2293 src_layout: LogicalLayoutHandle::G2,
2294 skip_transfers: false,
2295 lock_manager: None,
2296 };
2297
2298 let transfer_id = crate::offload::handle::TransferId::new();
2299 let (mut state, handle) = TransferState::new(transfer_id, vec![10, 20]);
2300 state.add_passed(vec![10, 20]);
2301 state.mark_in_flight(vec![10, 20]);
2302 let state_arc = Arc::new(std::sync::Mutex::new(state));
2303
2304 let blocks = vec![
2305 ResolvedBlock::<crate::G2> {
2306 transfer_id,
2307 block_id: 10,
2308 sequence_hash: hash1,
2309 guard: None,
2310 state: state_arc.clone(),
2311 },
2312 ResolvedBlock::<crate::G2> {
2313 transfer_id,
2314 block_id: 20,
2315 sequence_hash: hash2,
2316 guard: None,
2317 state: state_arc.clone(),
2318 },
2319 ];
2320
2321 let mut timing = TimingTrace::new();
2322 timing.mark_policy_complete();
2323 timing.mark_precondition_complete();
2324
2325 let batch = ResolvedBatch {
2326 blocks,
2327 evicted: Vec::new(),
2328 timing,
2329 };
2330
2331 let mut phase = test_phase_guard().await;
2332 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch, &mut phase)
2333 .await
2334 .expect("execute_transfer should succeed");
2335 phase.finish_success();
2336
2337 let state_guard = state_arc.lock().unwrap();
2338 assert_eq!(handle.completed_blocks(), vec![10, 20]);
2339 assert!(handle.failed_blocks().is_empty());
2340 assert_eq!(state_guard.status, TransferStatus::Complete);
2341
2342 drop(state_guard);
2343 assert_eq!(handle.completed_blocks(), vec![10, 20]);
2344 assert!(handle.failed_blocks().is_empty());
2345 }
2346
2347 #[tokio::test]
2349 async fn test_execute_transfer_mixed_transfers() {
2350 use crate::offload::handle::{TransferState, TransferStatus};
2351
2352 let hash_a1 = test_hash(10);
2353 let hash_a2_fail = test_hash(20); let hash_b1 = test_hash(30);
2355 let hash_b2 = test_hash(40);
2356
2357 let fail_hashes = [hash_a2_fail].into_iter().collect();
2358 let object_ops: Arc<dyn crate::object::ObjectBlockOps> =
2359 Arc::new(FailableObjectBlockOps { fail_hashes });
2360
2361 let shared = SharedObjectExecutorState {
2362 object_ops,
2363 src_layout: LogicalLayoutHandle::G2,
2364 skip_transfers: false,
2365 lock_manager: None,
2366 };
2367
2368 let tid_a = crate::offload::handle::TransferId::new();
2370 let (mut state_a, handle_a) = TransferState::new(tid_a, vec![100, 200]);
2371 state_a.add_passed(vec![100, 200]);
2372 state_a.mark_in_flight(vec![100, 200]);
2373 let state_a_arc = Arc::new(std::sync::Mutex::new(state_a));
2374
2375 let tid_b = crate::offload::handle::TransferId::new();
2377 let (mut state_b, handle_b) = TransferState::new(tid_b, vec![300, 400]);
2378 state_b.add_passed(vec![300, 400]);
2379 state_b.mark_in_flight(vec![300, 400]);
2380 let state_b_arc = Arc::new(std::sync::Mutex::new(state_b));
2381
2382 let blocks = vec![
2383 ResolvedBlock::<crate::G2> {
2384 transfer_id: tid_a,
2385 block_id: 100,
2386 sequence_hash: hash_a1,
2387 guard: None,
2388 state: state_a_arc.clone(),
2389 },
2390 ResolvedBlock::<crate::G2> {
2391 transfer_id: tid_a,
2392 block_id: 200,
2393 sequence_hash: hash_a2_fail,
2394 guard: None,
2395 state: state_a_arc.clone(),
2396 },
2397 ResolvedBlock::<crate::G2> {
2398 transfer_id: tid_b,
2399 block_id: 300,
2400 sequence_hash: hash_b1,
2401 guard: None,
2402 state: state_b_arc.clone(),
2403 },
2404 ResolvedBlock::<crate::G2> {
2405 transfer_id: tid_b,
2406 block_id: 400,
2407 sequence_hash: hash_b2,
2408 guard: None,
2409 state: state_b_arc.clone(),
2410 },
2411 ];
2412
2413 let mut timing = TimingTrace::new();
2414 timing.mark_policy_complete();
2415 timing.mark_precondition_complete();
2416
2417 let batch = ResolvedBatch {
2418 blocks,
2419 evicted: Vec::new(),
2420 timing,
2421 };
2422
2423 let mut phase = test_phase_guard().await;
2424 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch, &mut phase)
2425 .await
2426 .expect("execute_transfer should succeed");
2427 phase.finish_success();
2428
2429 let sa = state_a_arc.lock().unwrap();
2431 assert_eq!(handle_a.completed_blocks(), vec![100]);
2432 assert_eq!(handle_a.failed_blocks(), vec![200]);
2433 assert_eq!(sa.status, TransferStatus::Failed);
2434 assert!(sa.error.is_some());
2435 drop(sa);
2436
2437 assert_eq!(handle_a.completed_blocks(), vec![100]);
2438 assert_eq!(handle_a.failed_blocks(), vec![200]);
2439
2440 let sb = state_b_arc.lock().unwrap();
2442 assert_eq!(handle_b.completed_blocks(), vec![300, 400]);
2443 assert!(handle_b.failed_blocks().is_empty());
2444 assert_eq!(sb.status, TransferStatus::Complete);
2445 drop(sb);
2446
2447 assert_eq!(handle_b.completed_blocks(), vec![300, 400]);
2448 assert!(handle_b.failed_blocks().is_empty());
2449 }
2450}