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, oneshot, 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 ordered_transfer_starts: bool,
95 pub pending_tracker: Option<Arc<PendingTracker>>,
100 pub max_concurrent_precondition_awaits: usize,
106 _marker: PhantomData<(Src, Dst)>,
108}
109
110impl<Src: BlockMetadata, Dst: BlockMetadata> Default for PipelineConfig<Src, Dst> {
111 fn default() -> Self {
112 Self {
113 policies: Vec::new(),
114 batch_config: BatchConfig::default(),
115 policy_timeout: Duration::from_millis(100),
116 auto_chain: false,
117 eval_input_capacity: 128,
118 batch_input_capacity: 256,
119 transfer_input_capacity: 8,
120 sweep_interval: Duration::from_millis(10),
121 skip_transfers: false,
122 max_concurrent_transfers: 1,
123 ordered_transfer_starts: false,
124 pending_tracker: None,
125 max_concurrent_precondition_awaits: 8,
126 _marker: PhantomData,
127 }
128 }
129}
130
131pub struct PipelineBuilder<Src: BlockMetadata, Dst: BlockMetadata> {
133 config: PipelineConfig<Src, Dst>,
134}
135
136impl<Src: BlockMetadata, Dst: BlockMetadata> PipelineBuilder<Src, Dst> {
137 pub fn new() -> Self {
139 Self {
140 config: PipelineConfig::default(),
141 }
142 }
143
144 pub fn policy(mut self, policy: Arc<dyn OffloadPolicy<Src>>) -> Self {
146 self.config.policies.push(policy);
147 self
148 }
149
150 pub fn batch_size(mut self, size: usize) -> Self {
152 self.config.batch_config.max_batch_size = size;
153 self
154 }
155
156 pub fn min_batch_size(mut self, size: usize) -> Self {
158 self.config.batch_config.min_batch_size = size;
159 self
160 }
161
162 pub fn flush_interval(mut self, interval: Duration) -> Self {
164 self.config.batch_config.flush_interval = interval;
165 self
166 }
167
168 pub fn policy_timeout(mut self, timeout: Duration) -> Self {
170 self.config.policy_timeout = timeout;
171 self
172 }
173
174 pub fn auto_chain(mut self, enabled: bool) -> Self {
176 self.config.auto_chain = enabled;
177 self
178 }
179
180 pub fn sweep_interval(mut self, interval: Duration) -> Self {
182 self.config.sweep_interval = interval;
183 self
184 }
185
186 pub fn skip_transfers(mut self, skip: bool) -> Self {
191 self.config.skip_transfers = skip;
192 self
193 }
194
195 pub fn max_concurrent_transfers(mut self, n: usize) -> Self {
203 self.config.max_concurrent_transfers = n.max(1);
204 self
205 }
206
207 pub fn ordered_transfer_starts(mut self, ordered: bool) -> Self {
209 self.config.ordered_transfer_starts = ordered;
210 self
211 }
212
213 pub fn pending_tracker(mut self, tracker: Arc<PendingTracker>) -> Self {
218 self.config.pending_tracker = Some(tracker);
219 self
220 }
221
222 pub fn build(self) -> PipelineConfig<Src, Dst> {
224 self.config
225 }
226}
227
228impl<Src: BlockMetadata, Dst: BlockMetadata> Default for PipelineBuilder<Src, Dst> {
229 fn default() -> Self {
230 Self::new()
231 }
232}
233
234pub(crate) struct PipelineInput<T: BlockMetadata> {
236 pub(crate) transfer_id: TransferId,
237 pub(crate) source: SourceBlocks<T>,
239 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
240}
241
242pub struct PipelineOutput {
244 pub transfer_id: TransferId,
245 pub completed_hashes: Vec<SequenceHash>,
246}
247
248pub struct ChainOutput<T: BlockMetadata> {
254 pub transfer_id: TransferId,
255 pub blocks: Vec<ImmutableBlock<T>>,
256 #[allow(dead_code)]
258 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
259}
260
261pub type ChainOutputRx<T> = mpsc::Receiver<ChainOutput<T>>;
263
264pub struct Pipeline<Src: BlockMetadata, Dst: BlockMetadata> {
266 config: PipelineConfig<Src, Dst>,
267 pub(crate) eval_queue: Arc<CancellableQueue<PipelineInput<Src>>>,
269 output_tx: Option<mpsc::Sender<PipelineOutput>>,
271 chain_rx: Option<ChainOutputRx<Dst>>,
273 cancel_tx: watch::Sender<HashSet<TransferId>>,
275 pending_tracker: Arc<PendingTracker>,
277 pub(crate) settlement: PipelineSettlementTracker,
279 _task_handles: Vec<JoinHandle<()>>,
281 _marker: PhantomData<Dst>,
283}
284
285impl<Src: BlockMetadata, Dst: BlockMetadata> Pipeline<Src, Dst> {
286 #[allow(clippy::too_many_arguments)]
297 pub fn new(
298 config: PipelineConfig<Src, Dst>,
299 _registry: Arc<BlockRegistry>,
300 dst_manager: Arc<BlockManager<Dst>>,
301 leader: Arc<InstanceLeader>,
302 src_layout: LogicalLayoutHandle,
303 dst_layout: LogicalLayoutHandle,
304 runtime: tokio::runtime::Handle,
305 ) -> Self {
306 let eval_queue: Arc<CancellableQueue<PipelineInput<Src>>> =
308 Arc::new(CancellableQueue::new());
309 let batch_queue: Arc<CancellableQueue<EvalResult<Src>>> = Arc::new(CancellableQueue::new());
310
311 let (output_tx, _output_rx) = mpsc::channel(64);
313
314 let (cancel_tx, cancel_rx) = watch::channel(HashSet::new());
316
317 let (batch_tx, batch_rx) = mpsc::channel(config.transfer_input_capacity);
319
320 let (precond_tx, precond_rx) = mpsc::channel(config.transfer_input_capacity);
322
323 let (chain_tx, chain_rx) = if config.auto_chain {
325 let (tx, rx) = mpsc::channel(64);
326 (Some(tx), Some(rx))
327 } else {
328 (None, None)
329 };
330
331 let pending_tracker = config
333 .pending_tracker
334 .clone()
335 .unwrap_or_else(|| Arc::new(PendingTracker::new()));
336 let settlement = PipelineSettlementTracker::new(config.max_concurrent_transfers);
337
338 let evaluator = PolicyEvaluator {
340 policies: config.policies.clone(),
341 timeout: config.policy_timeout,
342 input_queue: eval_queue.clone(),
343 output_queue: batch_queue.clone(),
344 cancel_rx: cancel_rx.clone(),
345 pending_tracker: pending_tracker.clone(),
346 };
347 let eval_handle = runtime.spawn(async move {
348 evaluator.run().await;
349 });
350
351 let collector_input_queue = batch_queue.clone();
353 let batch_config = config.batch_config.clone();
354 let collector_cancel_rx = cancel_rx.clone();
355 let batch_handle = runtime.spawn(async move {
356 let collector = BatchCollector::new(
357 batch_config,
358 collector_input_queue,
359 batch_tx,
360 collector_cancel_rx,
361 );
362 collector.run().await;
363 });
364
365 let awaiter_leader = leader.clone();
367 let awaiter_settlement = settlement.clone();
368 let precond_handle = runtime.spawn(async move {
369 let awaiter = PreconditionAwaiter {
370 input_rx: batch_rx,
371 output_tx: precond_tx,
372 leader: awaiter_leader,
373 settlement: awaiter_settlement,
374 };
375 awaiter.run().await;
376 });
377
378 let executor = BlockTransferExecutor {
380 input_rx: precond_rx,
381 leader,
382 dst_manager,
383 src_layout,
384 dst_layout,
385 skip_transfers: config.skip_transfers,
386 max_concurrent_transfers: config.max_concurrent_transfers,
387 ordered_transfer_starts: config.ordered_transfer_starts,
388 chain_tx,
389 settlement: settlement.clone(),
390 _src_marker: PhantomData::<Src>,
391 };
392 let transfer_handle = runtime.spawn(async move {
393 executor.run().await;
394 });
395
396 let sweeper_queues = vec![eval_queue.clone()];
398 let sweeper_batch_queue = batch_queue;
399 let sweeper_interval = config.sweep_interval;
400 let sweeper_cancel_rx = cancel_rx;
401 let sweeper_handle = runtime.spawn(async move {
402 cancel_sweeper(
403 sweeper_queues,
404 sweeper_batch_queue,
405 sweeper_cancel_rx,
406 sweeper_interval,
407 )
408 .await;
409 });
410
411 Self {
412 config,
413 eval_queue,
414 output_tx: Some(output_tx),
415 chain_rx,
416 cancel_tx,
417 pending_tracker,
418 settlement,
419 _task_handles: vec![
420 eval_handle,
421 batch_handle,
422 precond_handle,
423 transfer_handle,
424 sweeper_handle,
425 ],
426 _marker: PhantomData,
427 }
428 }
429
430 pub(crate) fn enqueue(
432 &self,
433 transfer_id: TransferId,
434 source: SourceBlocks<Src>,
435 state: Arc<std::sync::Mutex<TransferState>>,
436 ) -> bool {
437 tracing::debug!(%transfer_id, num_blocks = source.len(), "Pipeline: enqueueing blocks");
438 let input = PipelineInput {
439 transfer_id,
440 source,
441 state,
442 };
443 self.eval_queue.push(transfer_id, input)
444 }
445
446 pub fn request_cancel(&self, transfer_id: TransferId) {
451 self.eval_queue.mark_cancelled(transfer_id);
453
454 self.cancel_tx.send_modify(|set| {
456 set.insert(transfer_id);
457 });
458 }
459
460 pub fn auto_chain(&self) -> bool {
462 self.config.auto_chain
463 }
464
465 pub fn output_tx(&self) -> Option<mpsc::Sender<PipelineOutput>> {
467 self.output_tx.clone()
468 }
469
470 pub fn take_chain_rx(&mut self) -> Option<ChainOutputRx<Dst>> {
480 self.chain_rx.take()
481 }
482
483 pub fn pending_tracker(&self) -> &Arc<PendingTracker> {
488 &self.pending_tracker
489 }
490}
491
492#[derive(Clone)]
501pub struct ObjectPipelineConfig<Src: BlockMetadata> {
502 pub policies: Vec<Arc<dyn OffloadPolicy<Src>>>,
504 pub batch_config: BatchConfig,
506 pub policy_timeout: Duration,
508 pub eval_input_capacity: usize,
510 pub batch_input_capacity: usize,
512 pub transfer_input_capacity: usize,
514 pub sweep_interval: Duration,
516 pub skip_transfers: bool,
518 pub max_concurrent_transfers: usize,
520 pub pending_tracker: Option<Arc<PendingTracker>>,
522 pub max_concurrent_precondition_awaits: usize,
524 pub lock_manager: Option<Arc<dyn ObjectLockManager>>,
530 _marker: PhantomData<Src>,
532}
533
534impl<Src: BlockMetadata> Default for ObjectPipelineConfig<Src> {
535 fn default() -> Self {
536 Self {
537 policies: Vec::new(),
538 batch_config: BatchConfig::default(),
539 policy_timeout: Duration::from_millis(100),
540 eval_input_capacity: 128,
541 batch_input_capacity: 256,
542 transfer_input_capacity: 8,
543 sweep_interval: Duration::from_millis(10),
544 skip_transfers: false,
545 max_concurrent_transfers: 1,
546 pending_tracker: None,
547 max_concurrent_precondition_awaits: 8,
548 lock_manager: None,
549 _marker: PhantomData,
550 }
551 }
552}
553
554pub struct ObjectPipelineBuilder<Src: BlockMetadata> {
556 config: ObjectPipelineConfig<Src>,
557}
558
559impl<Src: BlockMetadata> ObjectPipelineBuilder<Src> {
560 pub fn new() -> Self {
562 Self {
563 config: ObjectPipelineConfig::default(),
564 }
565 }
566
567 pub fn policy(mut self, policy: Arc<dyn OffloadPolicy<Src>>) -> Self {
569 self.config.policies.push(policy);
570 self
571 }
572
573 pub fn batch_size(mut self, size: usize) -> Self {
575 self.config.batch_config.max_batch_size = size;
576 self
577 }
578
579 pub fn min_batch_size(mut self, size: usize) -> Self {
581 self.config.batch_config.min_batch_size = size;
582 self
583 }
584
585 pub fn flush_interval(mut self, interval: Duration) -> Self {
587 self.config.batch_config.flush_interval = interval;
588 self
589 }
590
591 pub fn policy_timeout(mut self, timeout: Duration) -> Self {
593 self.config.policy_timeout = timeout;
594 self
595 }
596
597 pub fn sweep_interval(mut self, interval: Duration) -> Self {
599 self.config.sweep_interval = interval;
600 self
601 }
602
603 pub fn skip_transfers(mut self, skip: bool) -> Self {
605 self.config.skip_transfers = skip;
606 self
607 }
608
609 pub fn max_concurrent_transfers(mut self, n: usize) -> Self {
611 self.config.max_concurrent_transfers = n.max(1);
612 self
613 }
614
615 pub fn pending_tracker(mut self, tracker: Arc<PendingTracker>) -> Self {
617 self.config.pending_tracker = Some(tracker);
618 self
619 }
620
621 pub fn lock_manager(mut self, manager: Arc<dyn ObjectLockManager>) -> Self {
626 self.config.lock_manager = Some(manager);
627 self
628 }
629
630 pub fn build(self) -> ObjectPipelineConfig<Src> {
632 self.config
633 }
634}
635
636impl<Src: BlockMetadata> Default for ObjectPipelineBuilder<Src> {
637 fn default() -> Self {
638 Self::new()
639 }
640}
641
642#[allow(dead_code)]
647pub struct ObjectPipeline<Src: BlockMetadata> {
648 config: ObjectPipelineConfig<Src>,
649 pub(crate) eval_queue: Arc<CancellableQueue<PipelineInput<Src>>>,
651 output_tx: Option<mpsc::Sender<PipelineOutput>>,
653 cancel_tx: watch::Sender<HashSet<TransferId>>,
655 pending_tracker: Arc<PendingTracker>,
657 pub(crate) settlement: PipelineSettlementTracker,
659 _task_handles: Vec<JoinHandle<()>>,
661}
662
663impl<Src: BlockMetadata> ObjectPipeline<Src> {
664 #[allow(clippy::too_many_arguments)]
673 pub fn new(
674 config: ObjectPipelineConfig<Src>,
675 object_ops: Arc<dyn ObjectBlockOps>,
676 src_layout: LogicalLayoutHandle,
677 leader: Arc<InstanceLeader>,
678 runtime: tokio::runtime::Handle,
679 ) -> Self {
680 let eval_queue: Arc<CancellableQueue<PipelineInput<Src>>> =
682 Arc::new(CancellableQueue::new());
683 let batch_queue: Arc<CancellableQueue<EvalResult<Src>>> = Arc::new(CancellableQueue::new());
684
685 let (output_tx, _output_rx) = mpsc::channel(64);
687
688 let (cancel_tx, cancel_rx) = watch::channel(HashSet::new());
690
691 let (batch_tx, batch_rx) = mpsc::channel(config.transfer_input_capacity);
693
694 let (precond_tx, precond_rx) = mpsc::channel(config.transfer_input_capacity);
696
697 let pending_tracker = config
699 .pending_tracker
700 .clone()
701 .unwrap_or_else(|| Arc::new(PendingTracker::new()));
702 let settlement = PipelineSettlementTracker::new(config.max_concurrent_transfers);
703
704 let evaluator = PolicyEvaluator {
706 policies: config.policies.clone(),
707 timeout: config.policy_timeout,
708 input_queue: eval_queue.clone(),
709 output_queue: batch_queue.clone(),
710 cancel_rx: cancel_rx.clone(),
711 pending_tracker: pending_tracker.clone(),
712 };
713 let eval_handle = runtime.spawn(async move {
714 evaluator.run().await;
715 });
716
717 let collector_input_queue = batch_queue.clone();
719 let batch_config = config.batch_config.clone();
720 let collector_cancel_rx = cancel_rx.clone();
721 let batch_handle = runtime.spawn(async move {
722 let collector = BatchCollector::new(
723 batch_config,
724 collector_input_queue,
725 batch_tx,
726 collector_cancel_rx,
727 );
728 collector.run().await;
729 });
730
731 let awaiter_leader = leader.clone();
733 let awaiter_settlement = settlement.clone();
734 let precond_handle = runtime.spawn(async move {
735 let awaiter = PreconditionAwaiter {
736 input_rx: batch_rx,
737 output_tx: precond_tx,
738 leader: awaiter_leader,
739 settlement: awaiter_settlement,
740 };
741 awaiter.run().await;
742 });
743
744 let executor = ObjectTransferExecutor::new(
746 precond_rx,
747 object_ops,
748 src_layout,
749 config.skip_transfers,
750 config.max_concurrent_transfers,
751 config.lock_manager.clone(),
752 settlement.clone(),
753 );
754 let transfer_handle = runtime.spawn(async move {
755 executor.run().await;
756 });
757
758 let sweeper_queues = vec![eval_queue.clone()];
760 let sweeper_batch_queue = batch_queue;
761 let sweeper_interval = config.sweep_interval;
762 let sweeper_cancel_rx = cancel_rx;
763 let sweeper_handle = runtime.spawn(async move {
764 cancel_sweeper(
765 sweeper_queues,
766 sweeper_batch_queue,
767 sweeper_cancel_rx,
768 sweeper_interval,
769 )
770 .await;
771 });
772
773 Self {
774 config,
775 eval_queue,
776 output_tx: Some(output_tx),
777 cancel_tx,
778 pending_tracker,
779 settlement,
780 _task_handles: vec![
781 eval_handle,
782 batch_handle,
783 precond_handle,
784 transfer_handle,
785 sweeper_handle,
786 ],
787 }
788 }
789
790 pub(crate) fn enqueue(
792 &self,
793 transfer_id: TransferId,
794 source: SourceBlocks<Src>,
795 state: Arc<std::sync::Mutex<TransferState>>,
796 ) -> bool {
797 tracing::debug!(%transfer_id, num_blocks = source.len(), "ObjectPipeline: enqueueing blocks");
798 let input = PipelineInput {
799 transfer_id,
800 source,
801 state,
802 };
803 self.eval_queue.push(transfer_id, input)
804 }
805
806 pub fn request_cancel(&self, transfer_id: TransferId) {
808 self.eval_queue.mark_cancelled(transfer_id);
809 self.cancel_tx.send_modify(|set| {
810 set.insert(transfer_id);
811 });
812 }
813
814 #[allow(dead_code)]
816 pub fn output_tx(&self) -> Option<mpsc::Sender<PipelineOutput>> {
817 self.output_tx.clone()
818 }
819
820 pub fn pending_tracker(&self) -> &Arc<PendingTracker> {
822 &self.pending_tracker
823 }
824}
825
826async fn cancel_sweeper<Src: BlockMetadata>(
828 input_queues: Vec<Arc<CancellableQueue<PipelineInput<Src>>>>,
829 batch_queue: Arc<CancellableQueue<EvalResult<Src>>>,
830 mut cancel_rx: watch::Receiver<HashSet<TransferId>>,
831 interval: Duration,
832) {
833 let mut ticker = tokio::time::interval(interval);
834 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
835
836 loop {
837 tokio::select! {
838 _ = ticker.tick() => {
839 for queue in &input_queues {
841 let removed = queue.sweep();
842 if removed > 0 {
843 tracing::debug!("Sweeper removed {} cancelled input items", removed);
844 }
845 }
846
847 let batch_removed = batch_queue.sweep();
848 if batch_removed > 0 {
849 tracing::debug!("Sweeper removed {} cancelled batch items", batch_removed);
850 }
851 }
852 result = cancel_rx.changed() => {
853 if result.is_err() {
854 break;
856 }
857 for queue in &input_queues {
859 queue.sweep();
860 }
861 batch_queue.sweep();
862 }
863 }
864 }
865}
866
867struct PolicyEvaluator<T: BlockMetadata> {
869 policies: Vec<Arc<dyn OffloadPolicy<T>>>,
870 timeout: Duration,
871 input_queue: Arc<CancellableQueue<PipelineInput<T>>>,
872 output_queue: Arc<CancellableQueue<EvalResult<T>>>,
873 cancel_rx: watch::Receiver<HashSet<TransferId>>,
874 pending_tracker: Arc<PendingTracker>,
876}
877
878impl<T: BlockMetadata> PolicyEvaluator<T> {
879 async fn run(mut self) {
880 loop {
881 while let Some(item) = self.input_queue.pop_valid() {
882 self.evaluate(item.data).await;
883 }
884
885 tokio::select! {
886 _ = self.input_queue.notified() => {}
887 result = self.cancel_rx.changed() => {
888 if result.is_err() {
889 break;
890 }
891 }
892 }
893 }
894 }
895
896 async fn evaluate(&self, input: PipelineInput<T>) {
897 nvtx_range!("offload::policy");
898 let transfer_id = input.transfer_id;
899
900 let total_blocks = input.source.len();
902 {
903 let mut state = input.state.lock().unwrap();
904 state.total_expected_blocks = total_blocks;
905 }
906
907 {
909 let state = input.state.lock().unwrap();
910 if state.is_cancel_requested() {
911 drop(state); tracing::debug!(%transfer_id, "Transfer cancelled before evaluation");
913 let mut state = input.state.lock().unwrap();
914 state.set_cancelled();
915 return;
916 }
917 }
918
919 let mut passed = Vec::new();
920 let mut filtered = Vec::new();
921
922 match input.source {
924 SourceBlocks::External(external_blocks) => {
925 for ext in external_blocks {
928 if self.check_cancelled(&input.state, transfer_id) {
930 return;
931 }
932
933 let ctx = EvalContext::from_external(ext.block_id, ext.sequence_hash);
935 let pass = self.evaluate_policies(&ctx).await;
936
937 if pass {
938 let pending_guard = self.pending_tracker.guard(ext.sequence_hash);
940 passed.push(QueuedBlock {
941 transfer_id,
942 block_id: Some(ext.block_id),
943 sequence_hash: ext.sequence_hash,
944 source: SourceBlock::External(ext),
945 state: input.state.clone(),
946 pending_guard: Some(pending_guard),
947 });
948 } else {
949 filtered.push(ext.block_id);
950 }
951 }
952 tracing::debug!(%transfer_id, passed = passed.len(), filtered = filtered.len(), "External blocks evaluated");
953 }
954 SourceBlocks::Strong(strong_blocks) => {
955 for block in strong_blocks {
957 if self.check_cancelled(&input.state, transfer_id) {
959 return;
960 }
961
962 let ctx = EvalContext::new(block);
963 let pass = self.evaluate_policies(&ctx).await;
964
965 if pass {
966 let block = ctx.block.expect("Strong block context always has block");
967 let pending_guard = self.pending_tracker.guard(ctx.sequence_hash);
969 passed.push(QueuedBlock {
970 transfer_id,
971 block_id: Some(ctx.block_id),
972 sequence_hash: ctx.sequence_hash,
973 source: SourceBlock::Strong(block),
974 state: input.state.clone(),
975 pending_guard: Some(pending_guard),
976 });
977 } else {
978 filtered.push(ctx.block_id);
979 }
980 }
981 }
982 SourceBlocks::Weak(weak_blocks) => {
983 for weak in weak_blocks {
986 if self.check_cancelled(&input.state, transfer_id) {
988 return;
989 }
990
991 let sequence_hash = weak.sequence_hash();
992 let ctx = EvalContext::from_weak(BlockId::default(), sequence_hash);
993 let pass = self.evaluate_policies(&ctx).await;
994
995 if pass {
996 let pending_guard = self.pending_tracker.guard(sequence_hash);
998 passed.push(QueuedBlock {
999 transfer_id,
1000 block_id: None, sequence_hash,
1002 source: SourceBlock::Weak(weak),
1003 state: input.state.clone(),
1004 pending_guard: Some(pending_guard),
1005 });
1006 } else {
1007 tracing::debug!(%transfer_id, ?sequence_hash, "Weak block filtered by policy");
1010 }
1011 }
1012 }
1013 }
1014
1015 {
1017 let state = input.state.lock().unwrap();
1018 if state.is_cancel_requested() {
1019 drop(state);
1020 tracing::debug!(%transfer_id, "Transfer cancelled after evaluation");
1021 let mut state = input.state.lock().unwrap();
1022 state.set_cancelled();
1023 return;
1024 }
1025 }
1026
1027 tracing::debug!(%transfer_id, passed = passed.len(), filtered = filtered.len(), "Policy evaluation complete");
1028
1029 {
1031 let mut state = input.state.lock().unwrap();
1032 state.add_passed(passed.iter().filter_map(|b| b.block_id));
1035 state.add_filtered(filtered.iter().copied());
1036 state.set_status(TransferStatus::Queued);
1037 }
1038
1039 if passed.is_empty() {
1041 tracing::debug!(%transfer_id, "All blocks filtered, completing transfer");
1042 let mut state = input.state.lock().unwrap();
1043 state.set_complete();
1044 return;
1045 }
1046
1047 let result = EvalResult {
1049 transfer_id,
1050 passed_blocks: passed,
1051 filtered_ids: filtered,
1052 state: input.state,
1053 };
1054
1055 if !self.output_queue.push(transfer_id, result) {
1056 tracing::debug!(%transfer_id, "Push to output queue failed (cancelled)");
1057 }
1058 }
1059
1060 fn check_cancelled(
1062 &self,
1063 state: &Arc<std::sync::Mutex<TransferState>>,
1064 transfer_id: TransferId,
1065 ) -> bool {
1066 let state_guard = state.lock().unwrap();
1067 if state_guard.is_cancel_requested() {
1068 drop(state_guard);
1069 tracing::debug!(%transfer_id, "Transfer cancelled mid-evaluation");
1070 let mut state_guard = state.lock().unwrap();
1071 state_guard.set_cancelled();
1072 true
1073 } else {
1074 false
1075 }
1076 }
1077
1078 async fn evaluate_policies(&self, ctx: &EvalContext<T>) -> bool {
1079 for policy in &self.policies {
1080 let eval_future = policy.evaluate(ctx);
1081 let timed_result = tokio::time::timeout(self.timeout, async {
1082 match eval_future {
1083 Either::Left(ready) => ready.await,
1084 Either::Right(boxed) => boxed.await,
1085 }
1086 })
1087 .await;
1088
1089 match timed_result {
1090 Ok(Ok(true)) => continue,
1091 Ok(Ok(false)) => return false,
1092 Ok(Err(e)) => {
1093 tracing::warn!("Policy {} error: {}", policy.name(), e);
1094 return false;
1095 }
1096 Err(_) => {
1097 tracing::warn!("Policy {} timed out", policy.name());
1098 return false;
1099 }
1100 }
1101 }
1102 true
1103 }
1104}
1105
1106pub struct ResolvedBlock<T: BlockMetadata> {
1116 pub transfer_id: TransferId,
1118 pub block_id: BlockId,
1120 pub sequence_hash: SequenceHash,
1122 #[allow(dead_code)]
1125 pub guard: Option<ImmutableBlock<T>>,
1126 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
1128}
1129
1130pub struct ResolvedBatch<T: BlockMetadata> {
1134 pub blocks: Vec<ResolvedBlock<T>>,
1136 #[allow(dead_code)]
1138 pub evicted: Vec<SequenceHash>,
1139 pub timing: TimingTrace,
1141}
1142
1143impl<T: BlockMetadata> ResolvedBatch<T> {
1144 pub fn is_empty(&self) -> bool {
1146 self.blocks.is_empty()
1147 }
1148
1149 #[allow(dead_code)]
1151 pub fn len(&self) -> usize {
1152 self.blocks.len()
1153 }
1154}
1155
1156pub fn upgrade_batch<T: BlockMetadata>(batch: TransferBatch<T>) -> ResolvedBatch<T> {
1171 let mut resolved: Vec<ResolvedBlock<T>> = Vec::with_capacity(batch.len());
1172 let mut evicted: Vec<SequenceHash> = Vec::new();
1173
1174 let mut timing = batch.timing;
1176 timing.mark_transfer_start();
1177
1178 for queued in batch.blocks {
1179 match queued.source {
1183 SourceBlock::Strong(block) => {
1184 resolved.push(ResolvedBlock {
1185 transfer_id: queued.transfer_id,
1186 block_id: block.block_id(),
1187 sequence_hash: queued.sequence_hash,
1188 guard: Some(block),
1189 state: queued.state,
1190 });
1191 }
1192 SourceBlock::External(ext) => {
1193 resolved.push(ResolvedBlock {
1194 transfer_id: queued.transfer_id,
1195 block_id: ext.block_id,
1196 sequence_hash: ext.sequence_hash,
1197 guard: None,
1198 state: queued.state,
1199 });
1200 }
1201 SourceBlock::Weak(weak) => match weak.upgrade() {
1202 Some(block) => {
1203 resolved.push(ResolvedBlock {
1204 transfer_id: queued.transfer_id,
1205 block_id: block.block_id(),
1206 sequence_hash: queued.sequence_hash,
1207 guard: Some(block),
1208 state: queued.state,
1209 });
1210 }
1211 None => {
1212 tracing::debug!(
1213 sequence_hash = ?queued.sequence_hash,
1214 "Weak block evicted before transfer"
1215 );
1216 evicted.push(queued.sequence_hash);
1217 }
1218 },
1219 }
1220 }
1221
1222 ResolvedBatch {
1223 blocks: resolved,
1224 evicted,
1225 timing,
1226 }
1227}
1228
1229struct PreconditionAwaiter<T: BlockMetadata> {
1240 input_rx: BatchOutputRx<T>,
1241 output_tx: mpsc::Sender<TransferBatch<T>>,
1242 leader: Arc<InstanceLeader>,
1243 settlement: PipelineSettlementTracker,
1244}
1245
1246impl<T: BlockMetadata> PreconditionAwaiter<T> {
1247 async fn run(mut self) {
1248 while let Some(mut batch) = self.input_rx.recv().await {
1251 let output_tx = self.output_tx.clone();
1252 let nova = self.leader.messenger().clone();
1253 let settlement = self.settlement.clone();
1254
1255 tokio::spawn(async move {
1257 nvtx_range!("offload::precondition");
1258 if let Some(event_handle) = batch.precondition {
1259 tracing::debug!(?event_handle, "Awaiting precondition for batch");
1260
1261 let awaiter_result = nova.events().awaiter(event_handle);
1263
1264 match awaiter_result {
1265 Ok(awaiter) => {
1266 match tokio::time::timeout(Duration::from_secs(300), awaiter).await {
1268 Ok(Ok(())) => {
1269 tracing::debug!(?event_handle, "Precondition satisfied");
1270 }
1271 Ok(Err(poison)) => {
1272 tracing::error!(
1273 ?event_handle,
1274 ?poison,
1275 "Precondition poisoned, marking all blocks as failed"
1276 );
1277 for queued in batch.blocks {
1279 let mut state = queued.state.lock().unwrap();
1280 state.set_error(format!(
1281 "precondition poisoned: {:?}",
1282 poison
1283 ));
1284 }
1285 return;
1286 }
1287 Err(_) => {
1288 tracing::error!(
1289 ?event_handle,
1290 "Precondition timeout after 30s"
1291 );
1292 for queued in batch.blocks {
1294 let mut state = queued.state.lock().unwrap();
1295 state.set_error("precondition timeout".to_string());
1296 }
1297 return;
1298 }
1299 }
1300 }
1301 Err(e) => {
1302 tracing::error!(?event_handle, ?e, "Failed to create awaiter");
1303 for queued in batch.blocks {
1305 let mut state = queued.state.lock().unwrap();
1306 state.set_error(format!("failed to create awaiter: {}", e));
1307 }
1308 return;
1309 }
1310 }
1311 }
1312
1313 batch.timing.mark_precondition_complete();
1315
1316 let queued = QueuedBatchGuard::new(settlement);
1318 if let Err(e) = output_tx.send(batch).await {
1319 tracing::error!("Failed to forward batch after precondition: {}", e);
1320 queued.finish_failure(PipelineFailure::new(
1321 PipelineFailureKind::Shutdown,
1322 "transfer executor input channel closed",
1323 ));
1324 } else {
1325 queued.sent();
1326 }
1327 });
1328 }
1329 }
1330}
1331
1332struct BlockTransferExecutor<Src: BlockMetadata, Dst: BlockMetadata> {
1343 input_rx: BatchOutputRx<Src>,
1344 leader: Arc<InstanceLeader>,
1345 dst_manager: Arc<BlockManager<Dst>>,
1346 src_layout: LogicalLayoutHandle,
1347 dst_layout: LogicalLayoutHandle,
1348 skip_transfers: bool,
1350 max_concurrent_transfers: usize,
1352 ordered_transfer_starts: bool,
1354 chain_tx: Option<mpsc::Sender<ChainOutput<Dst>>>,
1356 settlement: PipelineSettlementTracker,
1357 _src_marker: PhantomData<Src>,
1358}
1359
1360struct SharedBlockExecutorState<Dst: BlockMetadata> {
1362 leader: Arc<InstanceLeader>,
1363 dst_manager: Arc<BlockManager<Dst>>,
1364 src_layout: LogicalLayoutHandle,
1365 dst_layout: LogicalLayoutHandle,
1366 skip_transfers: bool,
1367 chain_tx: Option<mpsc::Sender<ChainOutput<Dst>>>,
1368}
1369
1370struct OrderedTransferStart {
1371 predecessor: Option<oneshot::Receiver<()>>,
1372 successor: Option<oneshot::Sender<()>>,
1373}
1374
1375impl OrderedTransferStart {
1376 fn next(predecessor: &mut Option<oneshot::Receiver<()>>) -> Self {
1377 let (successor, next_predecessor) = oneshot::channel();
1378 Self {
1379 predecessor: predecessor.replace(next_predecessor),
1380 successor: Some(successor),
1381 }
1382 }
1383
1384 async fn wait(&mut self) {
1385 if let Some(predecessor) = self.predecessor.take() {
1386 let _ = predecessor.await;
1387 }
1388 }
1389
1390 fn release(&mut self) {
1391 if let Some(successor) = self.successor.take() {
1392 let _ = successor.send(());
1393 }
1394 }
1395}
1396
1397impl Drop for OrderedTransferStart {
1398 fn drop(&mut self) {
1399 self.release();
1400 }
1401}
1402
1403impl<Src: BlockMetadata, Dst: BlockMetadata> BlockTransferExecutor<Src, Dst> {
1404 async fn run(mut self) {
1405 let transfer_semaphore = Arc::new(Semaphore::new(self.max_concurrent_transfers));
1407 let prepare_semaphore = Arc::new(Semaphore::new(1));
1409
1410 let shared = Arc::new(SharedBlockExecutorState {
1412 leader: self.leader.clone(),
1413 dst_manager: self.dst_manager.clone(),
1414 src_layout: self.src_layout,
1415 dst_layout: self.dst_layout,
1416 skip_transfers: self.skip_transfers,
1417 chain_tx: self.chain_tx.take(),
1418 });
1419 let settlement = self.settlement.clone();
1420 let run_guard = PipelineRunGuard::new(settlement.clone(), "block transfer executor");
1421 let mut transfer_start_predecessor = None;
1422
1423 while let Some(batch) = self.input_rx.recv().await {
1424 if batch.is_empty() {
1425 settlement.discard_queued();
1426 continue;
1427 }
1428
1429 let prepare_permit = prepare_semaphore.clone().acquire_owned().await;
1432 if prepare_permit.is_err() {
1433 settlement.fail_queued(PipelineFailure::new(
1434 PipelineFailureKind::Shutdown,
1435 "block preparation semaphore closed",
1436 ));
1437 break; }
1439 let prepare_permit = prepare_permit.unwrap();
1440
1441 let upgraded = upgrade_batch(batch);
1444
1445 drop(prepare_permit);
1447
1448 if upgraded.is_empty() {
1449 tracing::debug!("All blocks in batch evicted, skipping transfer");
1450 settlement.discard_queued();
1451 continue;
1452 }
1453
1454 let transfer_permit = transfer_semaphore.clone().acquire_owned().await;
1456 if transfer_permit.is_err() {
1457 settlement.fail_queued(PipelineFailure::new(
1458 PipelineFailureKind::Shutdown,
1459 "block transfer semaphore closed",
1460 ));
1461 break; }
1463 let transfer_permit = transfer_permit.unwrap();
1464
1465 let shared_clone = shared.clone();
1467 let mut phase = BatchPhaseGuard::starting(settlement.clone(), transfer_permit);
1468 let mut transfer_start = self
1469 .ordered_transfer_starts
1470 .then(|| OrderedTransferStart::next(&mut transfer_start_predecessor));
1471 tokio::spawn(async move {
1472 if let Some(transfer_start) = &mut transfer_start {
1473 transfer_start.wait().await;
1474 }
1475 match Self::execute_transfer(
1476 &shared_clone,
1477 upgraded,
1478 &mut phase,
1479 transfer_start.as_mut(),
1480 )
1481 .await
1482 {
1483 Ok(()) => phase.finish_success(),
1484 Err(error) => {
1485 tracing::error!("BlockTransferExecutor: transfer failed: {}", error);
1486 phase.finish_failure(PipelineFailure::new(
1487 PipelineFailureKind::Executor,
1488 error.to_string(),
1489 ));
1490 }
1491 }
1492 });
1493 }
1494
1495 let _ = transfer_semaphore
1497 .acquire_many(self.max_concurrent_transfers as u32)
1498 .await;
1499 run_guard.finish_shutdown();
1500 }
1501
1502 fn fail_transfer_states(
1503 transfer_states: &std::collections::HashMap<
1504 TransferId,
1505 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1506 >,
1507 error: &anyhow::Error,
1508 ) {
1509 let message = format!("block transfer failed: {error}");
1510 for (state, block_ids) in transfer_states.values() {
1511 let mut state = state.lock().unwrap();
1512 state.mark_failed(block_ids.iter().copied());
1513 state.set_error(message.clone());
1514 }
1515 }
1516
1517 async fn execute_transfer(
1521 shared: &SharedBlockExecutorState<Dst>,
1522 mut batch: ResolvedBatch<Src>,
1523 phase: &mut BatchPhaseGuard,
1524 mut transfer_start: Option<&mut OrderedTransferStart>,
1525 ) -> anyhow::Result<()> {
1526 nvtx_range!("offload::transfer");
1527 if batch.is_empty() {
1528 return Ok(());
1529 }
1530
1531 let resolved = &batch.blocks;
1532
1533 let src_block_ids: Vec<BlockId> = resolved.iter().map(|b| b.block_id).collect();
1535 let sequence_hashes: Vec<SequenceHash> = resolved.iter().map(|b| b.sequence_hash).collect();
1536
1537 let mut transfer_states: std::collections::HashMap<
1539 TransferId,
1540 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1541 > = std::collections::HashMap::new();
1542 for block in resolved {
1543 transfer_states
1544 .entry(block.transfer_id)
1545 .or_insert_with(|| (block.state.clone(), Vec::new()))
1546 .1
1547 .push(block.block_id);
1548 }
1549
1550 if !shared.skip_transfers {
1552 let Some(dst_blocks) = shared.dst_manager.allocate_blocks(resolved.len()) else {
1554 let error =
1555 anyhow::anyhow!("failed to allocate {} destination blocks", resolved.len());
1556 Self::fail_transfer_states(&transfer_states, &error);
1557 return Err(error);
1558 };
1559
1560 let dst_block_ids: Vec<BlockId> = dst_blocks.iter().map(|b| b.block_id()).collect();
1561
1562 let start_xfer = Instant::now();
1564 let notification = match shared.leader.execute_local_transfer(
1565 shared.src_layout,
1566 shared.dst_layout,
1567 src_block_ids.clone(),
1568 dst_block_ids.clone(),
1569 TransferOptions::default(),
1570 ) {
1571 Ok(notification) => notification,
1572 Err(error) => {
1573 Self::fail_transfer_states(&transfer_states, &error);
1574 return Err(error);
1575 }
1576 };
1577
1578 for (state, block_ids) in transfer_states.values() {
1583 let mut state_guard = state.lock().unwrap();
1584 state_guard.set_status(TransferStatus::Transferring);
1585 state_guard.mark_in_flight(block_ids.iter().copied());
1586 }
1587 phase.mark_in_flight();
1588 if let Some(transfer_start) = &mut transfer_start {
1589 transfer_start.release();
1590 }
1591
1592 if let Err(error) = notification.await {
1594 Self::fail_transfer_states(&transfer_states, &error);
1595 return Err(error);
1596 }
1597 phase.mark_settling();
1598 let end_xfer = Instant::now();
1599
1600 let completed_blocks = dst_blocks
1602 .into_iter()
1603 .zip(sequence_hashes.iter())
1604 .map(|(dst_block, seq_hash)| {
1605 dst_block
1606 .stage(*seq_hash, shared.dst_manager.block_size())
1607 .expect("block size mismatch")
1608 })
1609 .collect();
1610 let registered_blocks: Vec<ImmutableBlock<Dst>> =
1611 shared.dst_manager.register_blocks(completed_blocks);
1612
1613 let registration_timepoint = Instant::now();
1614
1615 let unique_transfer_ids: std::collections::HashSet<_> =
1617 resolved.iter().map(|b| b.transfer_id).collect();
1618
1619 let policy_ms = batch
1620 .timing
1621 .policy_duration()
1622 .map(|d| d.as_millis() as u64)
1623 .unwrap_or(0);
1624 let precondition_ms = batch
1625 .timing
1626 .precondition_duration()
1627 .map(|d| d.as_millis() as u64)
1628 .unwrap_or(0);
1629 let total_ms = batch
1630 .timing
1631 .total_duration()
1632 .map(|d| d.as_millis() as u64)
1633 .unwrap_or(0);
1634
1635 tracing::info!(
1636 blocks = resolved.len(),
1637 containers = unique_transfer_ids.len(),
1638 policy_ms,
1639 precondition_ms,
1640 xfer_ms = end_xfer.duration_since(start_xfer).as_millis() as u64,
1641 registration_ms =
1642 registration_timepoint.duration_since(end_xfer).as_millis() as u64,
1643 total_ms,
1644 src = std::any::type_name::<Src>(),
1645 dst = std::any::type_name::<Dst>(),
1646 "Batch transfer complete"
1647 );
1648
1649 if let Some(chain_tx) = &shared.chain_tx {
1651 #[allow(clippy::type_complexity)]
1652 let mut chain_outputs: std::collections::HashMap<
1653 TransferId,
1654 (
1655 Arc<std::sync::Mutex<TransferState>>,
1656 Vec<ImmutableBlock<Dst>>,
1657 ),
1658 > = std::collections::HashMap::new();
1659
1660 for (registered, resolved_block) in
1661 registered_blocks.into_iter().zip(resolved.iter())
1662 {
1663 chain_outputs
1664 .entry(resolved_block.transfer_id)
1665 .or_insert_with(|| (resolved_block.state.clone(), Vec::new()))
1666 .1
1667 .push(registered);
1668 }
1669
1670 for (transfer_id, (state, blocks)) in chain_outputs {
1671 let output = ChainOutput {
1672 transfer_id,
1673 blocks,
1674 state,
1675 };
1676 if chain_tx.send(output).await.is_err() {
1677 tracing::warn!(
1678 %transfer_id,
1679 "Chain channel closed, downstream pipeline unavailable"
1680 );
1681 } else {
1682 tracing::debug!(
1683 %transfer_id,
1684 "Sent blocks to chain output for downstream processing"
1685 );
1686 }
1687 }
1688 }
1689 } else {
1690 phase.mark_in_flight();
1691 if let Some(transfer_start) = &mut transfer_start {
1692 transfer_start.release();
1693 }
1694 phase.mark_settling();
1695 for (state, block_ids) in transfer_states.values() {
1696 let mut state_guard = state.lock().unwrap();
1697 state_guard.set_status(TransferStatus::Transferring);
1698 state_guard.mark_in_flight(block_ids.iter().copied());
1699 }
1700 }
1701
1702 batch.timing.mark_transfer_complete();
1704
1705 for (transfer_id, (state, block_ids)) in transfer_states {
1707 let mut state_guard = state.lock().unwrap();
1708 state_guard.mark_completed(block_ids);
1709
1710 let progress = state_guard.progress_counts();
1711 let total = progress.passed + state_guard.filtered_out.len();
1712 let done = progress.completed + state_guard.filtered_out.len();
1713 tracing::debug!(
1714 %transfer_id,
1715 total,
1716 done,
1717 passed = progress.passed,
1718 filtered = state_guard.filtered_out.len(),
1719 completed = progress.completed,
1720 "Transfer batch progress"
1721 );
1722 if done >= total && total > 0 {
1723 state_guard.set_complete();
1724 }
1725 }
1726
1727 Ok(())
1728 }
1729}
1730
1731pub struct ObjectTransferExecutor<Src: BlockMetadata> {
1758 input_rx: BatchOutputRx<Src>,
1760 object_ops: Arc<dyn ObjectBlockOps>,
1762 src_layout: LogicalLayoutHandle,
1765 skip_transfers: bool,
1767 max_concurrent_transfers: usize,
1769 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1771 settlement: PipelineSettlementTracker,
1772}
1773
1774struct SharedObjectExecutorState {
1776 object_ops: Arc<dyn ObjectBlockOps>,
1777 src_layout: LogicalLayoutHandle,
1778 skip_transfers: bool,
1779 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1780}
1781
1782impl<Src: BlockMetadata> ObjectTransferExecutor<Src> {
1783 #[allow(dead_code)]
1785 pub fn new(
1786 input_rx: BatchOutputRx<Src>,
1787 object_ops: Arc<dyn ObjectBlockOps>,
1788 src_layout: LogicalLayoutHandle,
1789 skip_transfers: bool,
1790 max_concurrent_transfers: usize,
1791 lock_manager: Option<Arc<dyn ObjectLockManager>>,
1792 settlement: PipelineSettlementTracker,
1793 ) -> Self {
1794 Self {
1795 input_rx,
1796 object_ops,
1797 src_layout,
1798 skip_transfers,
1799 max_concurrent_transfers,
1800 lock_manager,
1801 settlement,
1802 }
1803 }
1804
1805 pub async fn run(mut self) {
1807 let transfer_semaphore = Arc::new(Semaphore::new(self.max_concurrent_transfers));
1809 let prepare_semaphore = Arc::new(Semaphore::new(1));
1811
1812 let shared = Arc::new(SharedObjectExecutorState {
1814 object_ops: self.object_ops.clone(),
1815 src_layout: self.src_layout,
1816 skip_transfers: self.skip_transfers,
1817 lock_manager: self.lock_manager.clone(),
1818 });
1819 let settlement = self.settlement.clone();
1820 let run_guard = PipelineRunGuard::new(settlement.clone(), "object transfer executor");
1821
1822 while let Some(batch) = self.input_rx.recv().await {
1823 if batch.is_empty() {
1824 settlement.discard_queued();
1825 continue;
1826 }
1827
1828 let prepare_permit = prepare_semaphore.clone().acquire_owned().await;
1830 if prepare_permit.is_err() {
1831 settlement.fail_queued(PipelineFailure::new(
1832 PipelineFailureKind::Shutdown,
1833 "object preparation semaphore closed",
1834 ));
1835 break; }
1837 let prepare_permit = prepare_permit.unwrap();
1838
1839 let upgraded = upgrade_batch(batch);
1841
1842 drop(prepare_permit);
1844
1845 if upgraded.is_empty() {
1846 tracing::debug!("All blocks in batch evicted, skipping object transfer");
1847 settlement.discard_queued();
1848 continue;
1849 }
1850
1851 let transfer_permit = transfer_semaphore.clone().acquire_owned().await;
1853 if transfer_permit.is_err() {
1854 settlement.fail_queued(PipelineFailure::new(
1855 PipelineFailureKind::Shutdown,
1856 "object transfer semaphore closed",
1857 ));
1858 break; }
1860 let transfer_permit = transfer_permit.unwrap();
1861
1862 let shared_clone = shared.clone();
1864 let mut phase = BatchPhaseGuard::starting(settlement.clone(), transfer_permit);
1865 tokio::spawn(async move {
1866 match Self::execute_transfer(&shared_clone, upgraded, &mut phase).await {
1867 Ok(()) => phase.finish_success(),
1868 Err(error) => {
1869 tracing::error!("ObjectTransferExecutor: transfer failed: {}", error);
1870 phase.finish_failure(PipelineFailure::new(
1871 PipelineFailureKind::Executor,
1872 error.to_string(),
1873 ));
1874 }
1875 }
1876 });
1877 }
1878
1879 let _ = transfer_semaphore
1881 .acquire_many(self.max_concurrent_transfers as u32)
1882 .await;
1883 run_guard.finish_shutdown();
1884 }
1885
1886 async fn execute_transfer(
1888 shared: &SharedObjectExecutorState,
1889 mut batch: ResolvedBatch<Src>,
1890 phase: &mut BatchPhaseGuard,
1891 ) -> anyhow::Result<()> {
1892 nvtx_range!("offload::transfer");
1893 if batch.is_empty() {
1894 return Ok(());
1895 }
1896
1897 let resolved = &batch.blocks;
1898
1899 let keys: Vec<SequenceHash> = resolved.iter().map(|b| b.sequence_hash).collect();
1901 let block_ids: Vec<BlockId> = resolved.iter().map(|b| b.block_id).collect();
1902
1903 let mut transfer_states: std::collections::HashMap<
1905 TransferId,
1906 (Arc<std::sync::Mutex<TransferState>>, Vec<BlockId>),
1907 > = std::collections::HashMap::new();
1908 for block in resolved {
1909 transfer_states
1910 .entry(block.transfer_id)
1911 .or_insert_with(|| (block.state.clone(), Vec::new()))
1912 .1
1913 .push(block.block_id);
1914 }
1915
1916 let mut successful_hashes: Vec<SequenceHash> = Vec::new();
1918
1919 if !shared.skip_transfers {
1921 let mut put = shared
1923 .object_ops
1924 .put_blocks(keys.clone(), shared.src_layout, block_ids);
1925 let mut first_poll = true;
1926 let results = std::future::poll_fn(|cx| {
1927 if first_poll {
1928 first_poll = false;
1929 phase.mark_in_flight();
1930 }
1931 put.as_mut().poll(cx)
1932 })
1933 .await;
1934 phase.mark_settling();
1935
1936 if results.len() != keys.len() {
1939 tracing::error!(
1940 expected = keys.len(),
1941 actual = results.len(),
1942 "put_blocks returned mismatched result count"
1943 );
1944 for (_transfer_id, (state, block_ids)) in transfer_states {
1945 let mut state_guard = state.lock().unwrap();
1946 state_guard.mark_failed(block_ids);
1947 state_guard
1948 .set_error("put_blocks returned mismatched result count".to_string());
1949 }
1950 return Ok(());
1951 }
1952
1953 let mut success_count = 0;
1955 let mut fail_count = 0;
1956
1957 for result in results {
1958 match result {
1959 Ok(hash) => {
1960 success_count += 1;
1961 successful_hashes.push(hash);
1962 }
1963 Err(hash) => {
1964 fail_count += 1;
1965 tracing::warn!(?hash, "Failed to transfer block to object storage");
1966 }
1967 }
1968 }
1969
1970 if fail_count > 0 {
1971 tracing::warn!(
1972 success = success_count,
1973 failed = fail_count,
1974 "Object transfer partially failed"
1975 );
1976 } else {
1977 tracing::debug!(
1978 num_blocks = success_count,
1979 "Successfully transferred blocks to object storage"
1980 );
1981 }
1982
1983 if let Some(lock_manager) = &shared.lock_manager {
1989 for hash in &successful_hashes {
1990 if let Err(e) = lock_manager.create_meta(*hash).await {
1992 tracing::error!(?hash, error = %e, "Failed to create meta file");
1993 }
1994
1995 if let Err(e) = lock_manager.release_lock(*hash).await {
1997 tracing::error!(?hash, error = %e, "Failed to release lock");
1998 }
1999 }
2000 tracing::debug!(
2001 num_blocks = successful_hashes.len(),
2002 "Created meta files and released locks"
2003 );
2004 }
2005 } else {
2006 phase.mark_in_flight();
2007 phase.mark_settling();
2008 if let Some(lock_manager) = &shared.lock_manager {
2010 for hash in &keys {
2011 if let Err(e) = lock_manager.create_meta(*hash).await {
2012 tracing::error!(?hash, error = %e, "Failed to create meta file");
2013 }
2014 if let Err(e) = lock_manager.release_lock(*hash).await {
2015 tracing::error!(?hash, error = %e, "Failed to release lock");
2016 }
2017 }
2018 }
2019 }
2020
2021 batch.timing.mark_transfer_complete();
2023
2024 let unique_transfer_ids: std::collections::HashSet<_> =
2026 resolved.iter().map(|b| b.transfer_id).collect();
2027
2028 let policy_ms = batch
2029 .timing
2030 .policy_duration()
2031 .map(|d| d.as_millis() as u64)
2032 .unwrap_or(0);
2033 let precondition_ms = batch
2034 .timing
2035 .precondition_duration()
2036 .map(|d| d.as_millis() as u64)
2037 .unwrap_or(0);
2038 let transfer_ms = batch
2039 .timing
2040 .transfer_duration()
2041 .map(|d| d.as_millis() as u64)
2042 .unwrap_or(0);
2043 let total_ms = batch
2044 .timing
2045 .total_duration()
2046 .map(|d| d.as_millis() as u64)
2047 .unwrap_or(0);
2048
2049 tracing::info!(
2050 blocks = resolved.len(),
2051 containers = unique_transfer_ids.len(),
2052 policy_ms,
2053 precondition_ms,
2054 transfer_ms,
2055 total_ms,
2056 src = std::any::type_name::<Src>(),
2057 dst = "G4-object",
2058 "Object batch transfer complete"
2059 );
2060
2061 let block_to_hash: std::collections::HashMap<BlockId, SequenceHash> = resolved
2070 .iter()
2071 .map(|b| (b.block_id, b.sequence_hash))
2072 .collect();
2073 let success_set: std::collections::HashSet<SequenceHash> =
2074 successful_hashes.into_iter().collect();
2075
2076 debug_assert_eq!(
2077 block_to_hash.len(),
2078 resolved.len(),
2079 "duplicate BlockId in batch — block_to_hash would lose entries"
2080 );
2081 debug_assert_eq!(
2082 resolved
2083 .iter()
2084 .map(|b| b.sequence_hash)
2085 .collect::<std::collections::HashSet<_>>()
2086 .len(),
2087 resolved.len(),
2088 "duplicate SequenceHash in batch — hash-based success correlation is ambiguous"
2089 );
2090
2091 for (transfer_id, (state, block_ids)) in transfer_states {
2093 let mut state_guard = state.lock().unwrap();
2094
2095 if shared.skip_transfers {
2096 state_guard.mark_completed(block_ids);
2098 } else {
2099 let (succeeded, failed): (Vec<_>, Vec<_>) = block_ids.into_iter().partition(|id| {
2100 block_to_hash
2101 .get(id)
2102 .is_some_and(|h| success_set.contains(h))
2103 });
2104 state_guard.mark_completed(succeeded);
2105 if !failed.is_empty() {
2106 tracing::warn!(
2107 %transfer_id,
2108 failed_count = failed.len(),
2109 "Marking blocks as failed in transfer state"
2110 );
2111 state_guard.mark_failed(failed);
2112 }
2113 }
2114
2115 let progress = state_guard.progress_counts();
2116 let total = progress.passed + state_guard.filtered_out.len();
2117 let done = progress.settled() + state_guard.filtered_out.len();
2118 tracing::debug!(
2119 %transfer_id,
2120 total,
2121 done,
2122 passed = progress.passed,
2123 filtered = state_guard.filtered_out.len(),
2124 completed = progress.completed,
2125 failed = progress.failed,
2126 "Object transfer batch progress"
2127 );
2128 if done >= total && total > 0 {
2129 let failed_count = progress.failed;
2130 if failed_count == 0 {
2131 state_guard.set_complete();
2132 } else {
2133 state_guard.set_error(format!(
2134 "{failed_count} blocks failed to transfer to object storage",
2135 ));
2136 }
2137 }
2138 }
2139
2140 Ok(())
2141 }
2142}
2143
2144#[cfg(test)]
2145mod tests {
2146 use futures::FutureExt;
2147
2148 use super::*;
2149
2150 #[test]
2151 fn test_pipeline_builder() {
2152 let config = PipelineBuilder::<(), ()>::new()
2153 .batch_size(32)
2154 .min_batch_size(8)
2155 .policy_timeout(Duration::from_millis(50))
2156 .auto_chain(true)
2157 .sweep_interval(Duration::from_millis(5))
2158 .build();
2159
2160 assert_eq!(config.batch_config.max_batch_size, 32);
2161 assert_eq!(config.batch_config.min_batch_size, 8);
2162 assert_eq!(config.policy_timeout, Duration::from_millis(50));
2163 assert!(config.auto_chain);
2164 assert_eq!(config.sweep_interval, Duration::from_millis(5));
2165 }
2166
2167 #[test]
2168 fn test_pipeline_config_default() {
2169 let config = PipelineConfig::<(), ()>::default();
2170 assert!(config.policies.is_empty());
2171 assert!(!config.auto_chain);
2172 assert_eq!(config.sweep_interval, Duration::from_millis(10));
2173 assert!(!config.ordered_transfer_starts);
2174 }
2175
2176 #[tokio::test]
2177 async fn ordered_transfer_starts_ignore_task_schedule_order() {
2178 let observed = Arc::new(tokio::sync::Mutex::new(Vec::new()));
2179 let mut predecessor = None;
2180 let starts = (0..3)
2181 .map(|_| OrderedTransferStart::next(&mut predecessor))
2182 .collect::<Vec<_>>();
2183 let mut tasks = Vec::new();
2184
2185 for (index, mut start) in starts.into_iter().enumerate().rev() {
2186 let observed = observed.clone();
2187 tasks.push(tokio::spawn(async move {
2188 start.wait().await;
2189 observed.lock().await.push(index);
2190 start.release();
2191 }));
2192 }
2193 for task in tasks {
2194 task.await.unwrap();
2195 }
2196
2197 assert_eq!(*observed.lock().await, vec![0, 1, 2]);
2198 }
2199
2200 struct FailableObjectBlockOps {
2202 fail_hashes: std::collections::HashSet<SequenceHash>,
2203 }
2204
2205 impl crate::object::ObjectBlockOps for FailableObjectBlockOps {
2206 fn has_blocks(
2207 &self,
2208 keys: Vec<SequenceHash>,
2209 ) -> futures::future::BoxFuture<'static, Vec<(SequenceHash, Option<usize>)>> {
2210 Box::pin(async move { keys.into_iter().map(|h| (h, Some(1))).collect() })
2211 }
2212
2213 fn put_blocks(
2214 &self,
2215 keys: Vec<SequenceHash>,
2216 _layout: LogicalLayoutHandle,
2217 _block_ids: Vec<BlockId>,
2218 ) -> futures::future::BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
2219 let fail_set = self.fail_hashes.clone();
2220 Box::pin(async move {
2221 keys.into_iter()
2222 .map(|h| if fail_set.contains(&h) { Err(h) } else { Ok(h) })
2223 .collect()
2224 })
2225 }
2226
2227 fn get_blocks(
2228 &self,
2229 keys: Vec<SequenceHash>,
2230 _layout: LogicalLayoutHandle,
2231 _block_ids: Vec<BlockId>,
2232 ) -> futures::future::BoxFuture<'static, Vec<Result<SequenceHash, SequenceHash>>> {
2233 Box::pin(async move { keys.into_iter().map(Ok).collect() })
2234 }
2235 }
2236
2237 fn test_hash(n: u64) -> SequenceHash {
2238 SequenceHash::new(n, None, 0)
2239 }
2240
2241 async fn test_phase_guard() -> BatchPhaseGuard {
2242 let tracker = PipelineSettlementTracker::new(1);
2243 tracker.queue_batch();
2244 let permit = Arc::new(Semaphore::new(1))
2245 .acquire_owned()
2246 .await
2247 .expect("test semaphore should remain open");
2248 BatchPhaseGuard::starting(tracker, permit)
2249 }
2250
2251 #[test]
2252 fn block_transfer_failure_finalizes_queued_and_in_flight_handles() {
2253 let mut transfer_states = std::collections::HashMap::new();
2254 let mut handles = Vec::new();
2255
2256 for (block_id, status) in [
2257 (7, TransferStatus::Queued),
2258 (11, TransferStatus::Transferring),
2259 ] {
2260 let transfer_id = TransferId::new();
2261 let (mut state, handle) = TransferState::new(transfer_id, vec![block_id]);
2262 state.add_passed([block_id]);
2263 state.set_status(status);
2264 if status == TransferStatus::Transferring {
2265 state.mark_in_flight([block_id]);
2266 }
2267 let state = Arc::new(std::sync::Mutex::new(state));
2268 transfer_states.insert(transfer_id, (state.clone(), vec![block_id]));
2269 handles.push((block_id, state, handle));
2270 }
2271
2272 let error = anyhow::anyhow!("injected block transfer failure");
2273 BlockTransferExecutor::<crate::G1, crate::G2>::fail_transfer_states(
2274 &transfer_states,
2275 &error,
2276 );
2277
2278 for (block_id, state, mut handle) in handles {
2279 let result = handle
2280 .wait()
2281 .now_or_never()
2282 .expect("failed transfer handle must be ready")
2283 .expect("failed transfer must publish a result");
2284 assert_eq!(result.status, TransferStatus::Failed);
2285 assert_eq!(result.failed_blocks, vec![block_id]);
2286 assert!(result.completed_blocks.is_empty());
2287 assert_eq!(
2288 result.error.as_deref(),
2289 Some("block transfer failed: injected block transfer failure")
2290 );
2291 assert!(state.lock().unwrap().in_flight.is_empty());
2292 }
2293 }
2294
2295 #[tokio::test]
2296 async fn test_execute_transfer_partial_failure() {
2297 use crate::offload::handle::{TransferState, TransferStatus};
2298
2299 let hash_ok_1 = test_hash(1);
2300 let hash_fail = test_hash(2);
2301 let hash_ok_2 = test_hash(3);
2302
2303 let fail_hashes = [hash_fail].into_iter().collect();
2304 let object_ops: Arc<dyn crate::object::ObjectBlockOps> =
2305 Arc::new(FailableObjectBlockOps { fail_hashes });
2306
2307 let shared = SharedObjectExecutorState {
2308 object_ops,
2309 src_layout: LogicalLayoutHandle::G2,
2310 skip_transfers: false,
2311 lock_manager: None,
2312 };
2313
2314 let transfer_id = crate::offload::handle::TransferId::new();
2315 let (mut state, handle) = TransferState::new(transfer_id, vec![10, 20, 30]);
2316 state.add_passed(vec![10, 20, 30]);
2317 state.mark_in_flight(vec![10, 20, 30]);
2318 let state_arc = Arc::new(std::sync::Mutex::new(state));
2319
2320 let blocks = vec![
2321 ResolvedBlock::<crate::G2> {
2322 transfer_id,
2323 block_id: 10,
2324 sequence_hash: hash_ok_1,
2325 guard: None,
2326 state: state_arc.clone(),
2327 },
2328 ResolvedBlock::<crate::G2> {
2329 transfer_id,
2330 block_id: 20,
2331 sequence_hash: hash_fail,
2332 guard: None,
2333 state: state_arc.clone(),
2334 },
2335 ResolvedBlock::<crate::G2> {
2336 transfer_id,
2337 block_id: 30,
2338 sequence_hash: hash_ok_2,
2339 guard: None,
2340 state: state_arc.clone(),
2341 },
2342 ];
2343
2344 let mut timing = TimingTrace::new();
2345 timing.mark_policy_complete();
2346 timing.mark_precondition_complete();
2347
2348 let batch = ResolvedBatch {
2349 blocks,
2350 evicted: Vec::new(),
2351 timing,
2352 };
2353
2354 let mut phase = test_phase_guard().await;
2355 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch, &mut phase)
2356 .await
2357 .expect("execute_transfer should succeed");
2358 phase.finish_success();
2359
2360 let state_guard = state_arc.lock().unwrap();
2362 assert_eq!(handle.completed_blocks(), vec![10, 30]);
2363 assert_eq!(handle.failed_blocks(), vec![20]);
2364 assert_eq!(state_guard.in_flight.len(), 0);
2365 assert_eq!(state_guard.status, TransferStatus::Failed);
2366 assert!(state_guard.error.is_some());
2367
2368 drop(state_guard);
2370 assert_eq!(handle.completed_blocks(), vec![10, 30]);
2371 assert_eq!(handle.failed_blocks(), vec![20]);
2372 }
2373
2374 #[tokio::test]
2375 async fn test_execute_transfer_all_success() {
2376 use crate::offload::handle::{TransferState, TransferStatus};
2377
2378 let hash1 = test_hash(1);
2379 let hash2 = test_hash(2);
2380
2381 let object_ops: Arc<dyn crate::object::ObjectBlockOps> = Arc::new(FailableObjectBlockOps {
2382 fail_hashes: std::collections::HashSet::new(),
2383 });
2384
2385 let shared = SharedObjectExecutorState {
2386 object_ops,
2387 src_layout: LogicalLayoutHandle::G2,
2388 skip_transfers: false,
2389 lock_manager: None,
2390 };
2391
2392 let transfer_id = crate::offload::handle::TransferId::new();
2393 let (mut state, handle) = TransferState::new(transfer_id, vec![10, 20]);
2394 state.add_passed(vec![10, 20]);
2395 state.mark_in_flight(vec![10, 20]);
2396 let state_arc = Arc::new(std::sync::Mutex::new(state));
2397
2398 let blocks = vec![
2399 ResolvedBlock::<crate::G2> {
2400 transfer_id,
2401 block_id: 10,
2402 sequence_hash: hash1,
2403 guard: None,
2404 state: state_arc.clone(),
2405 },
2406 ResolvedBlock::<crate::G2> {
2407 transfer_id,
2408 block_id: 20,
2409 sequence_hash: hash2,
2410 guard: None,
2411 state: state_arc.clone(),
2412 },
2413 ];
2414
2415 let mut timing = TimingTrace::new();
2416 timing.mark_policy_complete();
2417 timing.mark_precondition_complete();
2418
2419 let batch = ResolvedBatch {
2420 blocks,
2421 evicted: Vec::new(),
2422 timing,
2423 };
2424
2425 let mut phase = test_phase_guard().await;
2426 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch, &mut phase)
2427 .await
2428 .expect("execute_transfer should succeed");
2429 phase.finish_success();
2430
2431 let state_guard = state_arc.lock().unwrap();
2432 assert_eq!(handle.completed_blocks(), vec![10, 20]);
2433 assert!(handle.failed_blocks().is_empty());
2434 assert_eq!(state_guard.status, TransferStatus::Complete);
2435
2436 drop(state_guard);
2437 assert_eq!(handle.completed_blocks(), vec![10, 20]);
2438 assert!(handle.failed_blocks().is_empty());
2439 }
2440
2441 #[tokio::test]
2443 async fn test_execute_transfer_mixed_transfers() {
2444 use crate::offload::handle::{TransferState, TransferStatus};
2445
2446 let hash_a1 = test_hash(10);
2447 let hash_a2_fail = test_hash(20); let hash_b1 = test_hash(30);
2449 let hash_b2 = test_hash(40);
2450
2451 let fail_hashes = [hash_a2_fail].into_iter().collect();
2452 let object_ops: Arc<dyn crate::object::ObjectBlockOps> =
2453 Arc::new(FailableObjectBlockOps { fail_hashes });
2454
2455 let shared = SharedObjectExecutorState {
2456 object_ops,
2457 src_layout: LogicalLayoutHandle::G2,
2458 skip_transfers: false,
2459 lock_manager: None,
2460 };
2461
2462 let tid_a = crate::offload::handle::TransferId::new();
2464 let (mut state_a, handle_a) = TransferState::new(tid_a, vec![100, 200]);
2465 state_a.add_passed(vec![100, 200]);
2466 state_a.mark_in_flight(vec![100, 200]);
2467 let state_a_arc = Arc::new(std::sync::Mutex::new(state_a));
2468
2469 let tid_b = crate::offload::handle::TransferId::new();
2471 let (mut state_b, handle_b) = TransferState::new(tid_b, vec![300, 400]);
2472 state_b.add_passed(vec![300, 400]);
2473 state_b.mark_in_flight(vec![300, 400]);
2474 let state_b_arc = Arc::new(std::sync::Mutex::new(state_b));
2475
2476 let blocks = vec![
2477 ResolvedBlock::<crate::G2> {
2478 transfer_id: tid_a,
2479 block_id: 100,
2480 sequence_hash: hash_a1,
2481 guard: None,
2482 state: state_a_arc.clone(),
2483 },
2484 ResolvedBlock::<crate::G2> {
2485 transfer_id: tid_a,
2486 block_id: 200,
2487 sequence_hash: hash_a2_fail,
2488 guard: None,
2489 state: state_a_arc.clone(),
2490 },
2491 ResolvedBlock::<crate::G2> {
2492 transfer_id: tid_b,
2493 block_id: 300,
2494 sequence_hash: hash_b1,
2495 guard: None,
2496 state: state_b_arc.clone(),
2497 },
2498 ResolvedBlock::<crate::G2> {
2499 transfer_id: tid_b,
2500 block_id: 400,
2501 sequence_hash: hash_b2,
2502 guard: None,
2503 state: state_b_arc.clone(),
2504 },
2505 ];
2506
2507 let mut timing = TimingTrace::new();
2508 timing.mark_policy_complete();
2509 timing.mark_precondition_complete();
2510
2511 let batch = ResolvedBatch {
2512 blocks,
2513 evicted: Vec::new(),
2514 timing,
2515 };
2516
2517 let mut phase = test_phase_guard().await;
2518 ObjectTransferExecutor::<crate::G2>::execute_transfer(&shared, batch, &mut phase)
2519 .await
2520 .expect("execute_transfer should succeed");
2521 phase.finish_success();
2522
2523 let sa = state_a_arc.lock().unwrap();
2525 assert_eq!(handle_a.completed_blocks(), vec![100]);
2526 assert_eq!(handle_a.failed_blocks(), vec![200]);
2527 assert_eq!(sa.status, TransferStatus::Failed);
2528 assert!(sa.error.is_some());
2529 drop(sa);
2530
2531 assert_eq!(handle_a.completed_blocks(), vec![100]);
2532 assert_eq!(handle_a.failed_blocks(), vec![200]);
2533
2534 let sb = state_b_arc.lock().unwrap();
2536 assert_eq!(handle_b.completed_blocks(), vec![300, 400]);
2537 assert!(handle_b.failed_blocks().is_empty());
2538 assert_eq!(sb.status, TransferStatus::Complete);
2539 drop(sb);
2540
2541 assert_eq!(handle_b.completed_blocks(), vec![300, 400]);
2542 assert!(handle_b.failed_blocks().is_empty());
2543 }
2544}