1use std::collections::HashSet;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12
13use tokio::sync::{mpsc, watch};
14use velo::EventHandle;
15
16use crate::{BlockId, SequenceHash};
17use kvbm_logical::blocks::BlockMetadata;
18
19use super::handle::TransferId;
20use super::pending::PendingGuard;
21use super::queue::CancellableQueue;
22use super::source::SourceBlock;
23
24#[derive(Debug, Clone)]
29pub struct TimingTrace {
30 pub enqueued_at: Instant,
32 pub policy_complete_at: Option<Instant>,
34 pub precondition_complete_at: Option<Instant>,
36 pub batched_at: Option<Instant>,
38 pub transfer_start_at: Option<Instant>,
40 pub transfer_complete_at: Option<Instant>,
42}
43
44impl TimingTrace {
45 pub fn new() -> Self {
47 Self {
48 enqueued_at: Instant::now(),
49 policy_complete_at: None,
50 precondition_complete_at: None,
51 batched_at: None,
52 transfer_start_at: None,
53 transfer_complete_at: None,
54 }
55 }
56
57 pub fn mark_policy_complete(&mut self) {
59 self.policy_complete_at = Some(Instant::now());
60 }
61
62 pub fn mark_precondition_complete(&mut self) {
64 self.precondition_complete_at = Some(Instant::now());
65 }
66
67 pub fn mark_batched(&mut self) {
69 self.batched_at = Some(Instant::now());
70 }
71
72 pub fn mark_transfer_start(&mut self) {
74 self.transfer_start_at = Some(Instant::now());
75 }
76
77 pub fn mark_transfer_complete(&mut self) {
79 self.transfer_complete_at = Some(Instant::now());
80 }
81
82 pub fn total_duration(&self) -> Option<Duration> {
84 self.transfer_complete_at
85 .map(|end| end.duration_since(self.enqueued_at))
86 }
87
88 pub fn policy_duration(&self) -> Option<Duration> {
90 self.policy_complete_at
91 .map(|end| end.duration_since(self.enqueued_at))
92 }
93
94 pub fn precondition_duration(&self) -> Option<Duration> {
96 match (self.policy_complete_at, self.precondition_complete_at) {
97 (Some(start), Some(end)) => Some(end.duration_since(start)),
98 _ => None,
99 }
100 }
101
102 pub fn transfer_duration(&self) -> Option<Duration> {
104 match (self.transfer_start_at, self.transfer_complete_at) {
105 (Some(start), Some(end)) => Some(end.duration_since(start)),
106 _ => None,
107 }
108 }
109}
110
111impl Default for TimingTrace {
112 fn default() -> Self {
113 Self::new()
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct BatchConfig {
120 pub max_batch_size: usize,
122 pub flush_interval: Duration,
124 pub min_batch_size: usize,
126}
127
128impl Default for BatchConfig {
129 fn default() -> Self {
130 Self {
131 max_batch_size: 1024,
132 flush_interval: Duration::from_millis(10),
133 min_batch_size: 8,
134 }
135 }
136}
137
138impl BatchConfig {
139 pub fn with_max_size(mut self, size: usize) -> Self {
141 self.max_batch_size = size;
142 self
143 }
144
145 pub fn with_flush_interval(mut self, interval: Duration) -> Self {
147 self.flush_interval = interval;
148 self
149 }
150
151 pub fn with_min_size(mut self, size: usize) -> Self {
153 self.min_batch_size = size;
154 self
155 }
156}
157
158#[allow(dead_code)]
160pub struct QueuedBlock<T: BlockMetadata> {
161 pub transfer_id: TransferId,
163 pub block_id: Option<BlockId>,
165 pub sequence_hash: SequenceHash,
167 pub source: SourceBlock<T>,
169 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
171 pub pending_guard: Option<PendingGuard>,
176}
177
178impl<T: BlockMetadata> std::fmt::Debug for QueuedBlock<T> {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 f.debug_struct("QueuedBlock")
181 .field("transfer_id", &self.transfer_id)
182 .field("block_id", &self.block_id)
183 .field("sequence_hash", &self.sequence_hash)
184 .finish()
185 }
186}
187
188pub struct TransferBatch<T: BlockMetadata> {
190 pub blocks: Vec<QueuedBlock<T>>,
192 pub precondition: Option<EventHandle>,
195 pub timing: TimingTrace,
197}
198
199impl<T: BlockMetadata> TransferBatch<T> {
200 pub fn new() -> Self {
202 Self {
203 blocks: Vec::new(),
204 precondition: None,
205 timing: TimingTrace::new(),
206 }
207 }
208
209 pub fn with_capacity(capacity: usize) -> Self {
211 Self {
212 blocks: Vec::with_capacity(capacity),
213 precondition: None,
214 timing: TimingTrace::new(),
215 }
216 }
217
218 #[allow(dead_code)]
220 pub fn with_precondition(mut self, precondition: EventHandle) -> Self {
221 self.precondition = Some(precondition);
222 self
223 }
224
225 pub fn push(&mut self, block: QueuedBlock<T>) {
227 self.blocks.push(block);
228 }
229
230 pub fn len(&self) -> usize {
232 self.blocks.len()
233 }
234
235 pub fn is_empty(&self) -> bool {
237 self.blocks.is_empty()
238 }
239
240 #[allow(dead_code)]
245 pub fn block_ids(&self) -> Vec<BlockId> {
246 self.blocks.iter().filter_map(|b| b.block_id).collect()
247 }
248
249 #[allow(dead_code)]
251 pub fn sequence_hashes(&self) -> Vec<SequenceHash> {
252 self.blocks.iter().map(|b| b.sequence_hash).collect()
253 }
254
255 #[allow(dead_code)]
257 pub fn transfer_ids(&self) -> Vec<TransferId> {
258 let mut ids: Vec<TransferId> = self.blocks.iter().map(|b| b.transfer_id).collect();
259 ids.sort_by_key(|id| id.as_uuid());
260 ids.dedup();
261 ids
262 }
263
264 #[allow(dead_code)]
266 pub fn take(&mut self) -> Vec<QueuedBlock<T>> {
267 std::mem::take(&mut self.blocks)
268 }
269
270 #[allow(dead_code)]
272 pub fn drain_transfer(&mut self, transfer_id: TransferId) -> Vec<QueuedBlock<T>> {
273 let mut kept = Vec::new();
274 let mut drained = Vec::new();
275 for block in std::mem::take(&mut self.blocks) {
276 if block.transfer_id == transfer_id {
277 drained.push(block);
278 } else {
279 kept.push(block);
280 }
281 }
282 self.blocks = kept;
283 drained
284 }
285}
286
287impl<T: BlockMetadata> Default for TransferBatch<T> {
288 fn default() -> Self {
289 Self::new()
290 }
291}
292
293use super::handle::TransferState;
294
295#[allow(dead_code)]
297pub struct EvalResult<T: BlockMetadata> {
298 pub transfer_id: TransferId,
300 pub passed_blocks: Vec<QueuedBlock<T>>,
302 pub filtered_ids: Vec<BlockId>,
304 pub(crate) state: Arc<std::sync::Mutex<TransferState>>,
306}
307
308pub type BatchOutput<T> = mpsc::Sender<TransferBatch<T>>;
310pub type BatchOutputRx<T> = mpsc::Receiver<TransferBatch<T>>;
312
313fn extract_common_precondition<T: BlockMetadata>(blocks: &[QueuedBlock<T>]) -> Option<EventHandle> {
318 blocks.first().and_then(|first_block| {
319 let first_precondition = first_block.state.lock().unwrap().precondition;
320 let all_same = blocks
321 .iter()
322 .all(|block| block.state.lock().unwrap().precondition == first_precondition);
323 if all_same { first_precondition } else { None }
324 })
325}
326
327pub struct BatchCollector<T: BlockMetadata> {
335 config: BatchConfig,
336 input_queue: Arc<CancellableQueue<EvalResult<T>>>,
338 output_tx: BatchOutput<T>,
340 cancel_rx: watch::Receiver<HashSet<TransferId>>,
342 current_batch: TransferBatch<T>,
344}
345
346impl<T: BlockMetadata> BatchCollector<T> {
347 pub fn new(
349 config: BatchConfig,
350 input_queue: Arc<CancellableQueue<EvalResult<T>>>,
351 output_tx: BatchOutput<T>,
352 cancel_rx: watch::Receiver<HashSet<TransferId>>,
353 ) -> Self {
354 let max_batch_size = config.max_batch_size;
355 Self {
356 config,
357 input_queue,
358 output_tx,
359 cancel_rx,
360 current_batch: TransferBatch::with_capacity(max_batch_size),
361 }
362 }
363
364 pub async fn run(mut self) {
366 let mut flush_timer = tokio::time::interval(self.config.flush_interval);
367 flush_timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
368
369 loop {
370 while let Some(item) = self.input_queue.pop_valid() {
371 self.handle_eval_result(item.data).await;
372 }
373
374 tokio::select! {
375 _ = self.input_queue.notified() => {}
376 _ = flush_timer.tick() => {
378 self.try_flush().await;
379 }
380 result = self.cancel_rx.changed() => {
382 if result.is_err() {
383 self.flush_if_not_empty().await;
385 break;
386 }
387 }
388 }
389 }
390 }
391
392 async fn handle_eval_result(&mut self, result: EvalResult<T>) {
398 let blocks_in_eval = result.passed_blocks.len() + result.filtered_ids.len();
400
401 for block in result.passed_blocks {
403 self.current_batch.push(block);
404
405 if self.current_batch.len() >= self.config.max_batch_size {
407 self.flush().await;
408 }
409 }
410
411 let should_flush = {
413 let mut state = result.state.lock().unwrap();
414 state.blocks_processed += blocks_in_eval;
415 state.blocks_processed >= state.total_expected_blocks && state.total_expected_blocks > 0
417 };
418
419 if should_flush && !self.current_batch.is_empty() {
421 tracing::debug!(
422 transfer_id = %result.transfer_id,
423 batch_size = self.current_batch.len(),
424 "Per-transfer sentinel flush"
425 );
426 self.flush().await;
427 }
428 }
429
430 async fn try_flush(&mut self) {
432 if self.current_batch.len() >= self.config.min_batch_size {
433 self.flush().await;
434 }
435 }
436
437 async fn flush_if_not_empty(&mut self) {
439 if !self.current_batch.is_empty() {
440 self.flush().await;
441 }
442 }
443
444 async fn flush(&mut self) {
446 nvtx_range!("offload::batch");
447 if self.current_batch.is_empty() {
448 return;
449 }
450
451 let mut batch = std::mem::replace(
452 &mut self.current_batch,
453 TransferBatch::with_capacity(self.config.max_batch_size),
454 );
455
456 batch.timing.mark_batched();
458
459 batch.precondition = extract_common_precondition(&batch.blocks);
460
461 if self.output_tx.send(batch).await.is_err() {
463 tracing::warn!("Batch output channel closed");
465 }
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_batch_config_default() {
475 let config = BatchConfig::default();
476 assert_eq!(config.max_batch_size, 1024);
477 assert_eq!(config.min_batch_size, 8);
478 }
479
480 #[test]
481 fn test_batch_config_builder() {
482 let config = BatchConfig::default()
483 .with_max_size(128)
484 .with_min_size(16)
485 .with_flush_interval(Duration::from_millis(50));
486
487 assert_eq!(config.max_batch_size, 128);
488 assert_eq!(config.min_batch_size, 16);
489 assert_eq!(config.flush_interval, Duration::from_millis(50));
490 }
491
492 #[test]
493 fn test_transfer_batch() {
494 let batch: TransferBatch<()> = TransferBatch::new();
495 assert!(batch.is_empty());
496 assert_eq!(batch.len(), 0);
497 }
498
499 #[tokio::test]
500 async fn test_batch_collector_empty_input() {
501 let input_queue = Arc::new(CancellableQueue::<EvalResult<()>>::new());
502 let (output_tx, mut output_rx) = mpsc::channel::<TransferBatch<()>>(10);
503 let (cancel_tx, cancel_rx) = watch::channel(HashSet::new());
504
505 let collector =
506 BatchCollector::new(BatchConfig::default(), input_queue, output_tx, cancel_rx);
507
508 drop(cancel_tx);
510
511 tokio::spawn(async move {
513 collector.run().await;
514 });
515
516 let result = tokio::time::timeout(Duration::from_millis(50), output_rx.recv()).await;
518 assert!(result.is_err() || result.unwrap().is_none());
519 }
520
521 #[test]
522 fn test_transfer_batch_with_capacity() {
523 let batch: TransferBatch<()> = TransferBatch::with_capacity(128);
524 assert!(batch.is_empty());
525 assert_eq!(batch.len(), 0);
526 }
527
528 #[test]
529 fn test_batch_config_with_methods() {
530 let config = BatchConfig::default()
531 .with_max_size(256)
532 .with_min_size(32)
533 .with_flush_interval(Duration::from_millis(100));
534
535 assert_eq!(config.max_batch_size, 256);
536 assert_eq!(config.min_batch_size, 32);
537 assert_eq!(config.flush_interval, Duration::from_millis(100));
538 }
539
540 #[test]
541 fn test_transfer_batch_methods() {
542 let mut batch: TransferBatch<()> = TransferBatch::new();
543
544 assert!(batch.block_ids().is_empty());
547 assert!(batch.sequence_hashes().is_empty());
548 assert!(batch.transfer_ids().is_empty());
549
550 let taken = batch.take();
552 assert!(taken.is_empty());
553 assert!(batch.is_empty());
554 }
555
556 #[test]
557 fn test_batch_precondition() {
558 let batch: TransferBatch<()> = TransferBatch::new();
559 assert!(batch.precondition.is_none());
560
561 }
564}