aria2_core/engine/bt_message_handler.rs
1//! BT Message Handler - Block request and receive logic
2//!
3//! This module handles the low-level BitTorrent protocol message processing
4//! for block requests and data reception during piece download.
5//!
6//! Extracted from `bt_download_command.rs` to improve modularity and
7//! follow the single responsibility principle.
8//!
9//! # Architecture Reference
10//!
11//! Based on original aria2 C++ structure:
12//! - `src/BtMessageDispatcher.h` - Message dispatching
13//! - `src/PeerInteractionCommand.h` - Peer interaction
14
15use crate::constants;
16use crate::engine::bt_download_execute::EndgameState;
17use crate::engine::bt_peer_connection::BtPeerConn;
18use crate::error::{Aria2Error, FatalError, RecoverableError, Result};
19use tracing::{debug, info, warn};
20
21/// Block size for each piece block request (16 KB)
22pub const BLOCK_SIZE: u32 = constants::BT_BLOCK_SIZE as u32;
23
24/// Maximum number of retries for a failed piece download
25pub const MAX_RETRIES: u32 = constants::BT_MAX_RETRIES;
26
27/// Timeout for each block request (seconds)
28pub const BLOCK_REQUEST_TIMEOUT_SECS: u64 = constants::BT_BLOCK_REQUEST_TIMEOUT_SECS;
29
30/// Maximum messages to read while waiting for a specific block
31pub const MAX_BLOCK_READ_MESSAGES: u32 = constants::BT_MAX_BLOCK_READ_MESSAGES as u32;
32
33/// Result of a block download attempt
34pub struct BlockDownloadResult {
35 /// Whether the block was successfully received
36 pub success: bool,
37 /// The received data (if successful)
38 pub data: Option<Vec<u8>>,
39 /// Number of bytes received (for statistics)
40 pub bytes_received: u64,
41}
42
43/// BT Message Handler for block-level operations
44///
45/// Manages the process of requesting and receiving individual blocks
46/// from peers during piece download.
47pub struct BtMessageHandler;
48
49impl BtMessageHandler {
50 /// Request and receive a single block from available peers
51 ///
52 /// This method implements the core block request/receive loop:
53 /// 1. Send the block request to a peer
54 /// 2. Wait for the response with timeout
55 /// 3. Handle various message types while waiting
56 /// 4. Return the block data on success
57 ///
58 /// # Arguments
59 /// * `connections` - Mutable slice of active peer connections
60 /// * `piece_index` - The index of the piece this block belongs to
61 /// * `block_offset` - The byte offset within the piece
62 /// * `block_length` - The length of this block in bytes
63 ///
64 /// # Returns
65 /// * `Ok(BlockDownloadResult)` - Result containing success status and data
66 /// * `Err(Aria2Error)` - If all peers fail to respond
67 pub async fn request_block(
68 connections: &mut [BtPeerConn],
69 piece_index: u32,
70 block_offset: u32,
71 block_length: u32,
72 ) -> Result<BlockDownloadResult> {
73 let req = aria2_protocol::bittorrent::message::types::PieceBlockRequest {
74 index: piece_index,
75 begin: block_offset,
76 length: block_length,
77 };
78
79 debug!(
80 "[BT] Requesting block {} offset={} len={}",
81 block_offset / BLOCK_SIZE,
82 block_offset,
83 block_length
84 );
85
86 let mut total_bytes = 0u64;
87
88 // Try each peer in order until we get the block
89 for (conn_idx, conn) in connections.iter_mut().enumerate() {
90 debug!("[BT] Trying peer {} for block request", conn_idx);
91
92 // Send request to this peer
93 if conn.send_request(req.clone()).await.is_err() {
94 warn!("[BT] Failed to send request to peer {}", conn_idx);
95 continue;
96 }
97
98 // Wait for response with timeout
99 match tokio::time::timeout(
100 std::time::Duration::from_secs(BLOCK_REQUEST_TIMEOUT_SECS),
101 Self::wait_for_piece_block(conn, piece_index, block_offset),
102 )
103 .await
104 {
105 Ok(Ok(data)) => {
106 debug!(
107 "[BT] Got block {} data len={} from peer {}",
108 block_offset / BLOCK_SIZE,
109 data.len(),
110 conn_idx
111 );
112 total_bytes += data.len() as u64;
113
114 return Ok(BlockDownloadResult {
115 success: true,
116 data: Some(data),
117 bytes_received: total_bytes,
118 });
119 }
120 Ok(Err(e)) => {
121 warn!(
122 "[BT] No PIECE message received from peer {}: {}",
123 conn_idx, e
124 );
125 }
126 Err(_) => {
127 warn!(
128 "[BT] Block request timed out after {}s",
129 BLOCK_REQUEST_TIMEOUT_SECS
130 );
131 }
132 }
133 }
134
135 // All peers failed
136 warn!("[BT] Failed to get block from any peer");
137 Ok(BlockDownloadResult {
138 success: false,
139 data: None,
140 bytes_received: total_bytes,
141 })
142 }
143
144 /// Wait for a specific PIECE message from a peer
145 ///
146 /// Reads messages from the connection until we receive the expected
147 /// piece block or exhaust our message limit.
148 async fn wait_for_piece_block(
149 conn: &mut BtPeerConn,
150 expected_index: u32,
151 expected_begin: u32,
152 ) -> Result<Vec<u8>> {
153 for _ in 0..MAX_BLOCK_READ_MESSAGES {
154 match conn.read_message().await {
155 Ok(Some(msg)) => {
156 use aria2_protocol::bittorrent::message::types::BtMessage;
157
158 match msg {
159 BtMessage::Piece {
160 index,
161 begin,
162 ref data,
163 } => {
164 if index == expected_index && begin == expected_begin {
165 return Ok(data.clone());
166 }
167 // Not the block we're waiting for, continue reading
168 debug!(
169 "[BT] Received unexpected PIECE (index={}, begin={}), waiting for ({}, {})",
170 index, begin, expected_index, expected_begin
171 );
172 }
173 other => {
174 use aria2_protocol::bittorrent::message::types::BtMessage;
175 match &other {
176 BtMessage::AllowedFast { index } => {
177 debug!("[BT] Received AllowedFast for piece {}", index);
178 conn.add_allowed_fast(*index);
179 }
180 BtMessage::Reject {
181 index,
182 offset,
183 length,
184 } => {
185 debug!(
186 "[BT] Received Reject for piece {} offset {} len {}",
187 index, offset, length
188 );
189 }
190 BtMessage::Suggest { index } => {
191 debug!("[BT] Received Suggest for piece {}", index);
192 // Note: Priority boost would be applied here if we had
193 // access to the piece picker. For now, just log it.
194 debug!(
195 "[BT] Suggest received for piece {} — would boost priority",
196 index
197 );
198 }
199 BtMessage::HaveAll => {
200 debug!("[BT] Received HaveAll");
201 }
202 BtMessage::HaveNone => {
203 debug!("[BT] Received HaveNone");
204 }
205 _ => {
206 debug!(
207 "[BT] Received non-PIECE message while waiting: {:?}",
208 other
209 );
210 }
211 }
212 }
213 }
214 }
215 Ok(None) => {
216 warn!("[BT] Connection closed by peer while waiting for block");
217 return Err(Aria2Error::Recoverable(
218 RecoverableError::TemporaryNetworkFailure {
219 message: "Peer connection closed".into(),
220 },
221 ));
222 }
223 Err(e) => {
224 warn!("[BT] Error reading from peer: {}", e);
225 return Err(Aria2Error::Recoverable(
226 RecoverableError::TemporaryNetworkFailure {
227 message: format!("Read error: {}", e),
228 },
229 ));
230 }
231 }
232 }
233
234 Err(Aria2Error::Recoverable(
235 RecoverableError::TemporaryNetworkFailure {
236 message: format!(
237 "Exceeded max messages ({}) without receiving expected block",
238 MAX_BLOCK_READ_MESSAGES
239 ),
240 },
241 ))
242 }
243
244 /// Download all blocks for a piece with retry logic
245 ///
246 /// Coordinates the download of all blocks that make up a piece,
247 /// implementing retry logic for failed pieces.
248 ///
249 /// # Arguments
250 /// * `connections` - Mutable slice of active peer connections
251 /// * `piece_index` - Index of the piece to download
252 /// * `piece_length` - Total length of this piece in bytes
253 /// * `num_blocks` - Number of blocks in this piece
254 ///
255 /// # Returns
256 /// * `Ok(Vec<u8>)` - Complete piece data if all blocks downloaded successfully
257 /// * `Err(Aria2Error)` - If piece download fails after all retries
258 pub async fn download_piece_blocks(
259 connections: &mut [BtPeerConn],
260 piece_index: u32,
261 piece_length: u32,
262 num_blocks: u32,
263 ) -> Result<Vec<u8>> {
264 // Retry the entire piece multiple times
265 for _retry in 0..MAX_RETRIES {
266 info!(
267 "[BT] Piece download attempt {} for piece {}",
268 _retry + 1,
269 piece_index
270 );
271
272 // Ensure clean state for each retry attempt
273 let mut piece_data = Vec::with_capacity(piece_length as usize);
274 piece_data.clear();
275 let mut all_blocks_ok = true;
276
277 // Download each block in sequence
278 for block_idx in 0..num_blocks {
279 let offset = block_idx * BLOCK_SIZE;
280 let len = if offset + BLOCK_SIZE > piece_length {
281 piece_length - offset
282 } else {
283 BLOCK_SIZE
284 };
285
286 debug!(
287 "[BT] Requesting block {}/{} (offset={}, len={})",
288 block_idx + 1,
289 num_blocks,
290 offset,
291 len
292 );
293
294 // Try to get this block from any peer
295 match Self::request_block(connections, piece_index, offset, len).await {
296 Ok(result) if result.success => {
297 if let Some(data) = result.data {
298 piece_data.extend_from_slice(&data);
299 } else {
300 all_blocks_ok = false;
301 break;
302 }
303 }
304 Ok(_) => {
305 warn!("[BT] Block {} request returned no data", block_idx);
306 all_blocks_ok = false;
307 break;
308 }
309 Err(e) => {
310 warn!("[BT] Block {} request error: {}", block_idx, e);
311 all_blocks_ok = false;
312 break;
313 }
314 }
315 }
316
317 // Check if we got all blocks
318 if all_blocks_ok && piece_data.len() == piece_length as usize {
319 info!(
320 "[BT] All {} blocks downloaded for piece {} ({} bytes)",
321 num_blocks,
322 piece_index,
323 piece_data.len()
324 );
325 return Ok(piece_data);
326 }
327
328 warn!(
329 "[BT] Incomplete piece {} (attempt {}/{}), retrying...",
330 piece_index,
331 _retry + 1,
332 MAX_RETRIES
333 );
334
335 // Small delay before retry
336 tokio::time::sleep(std::time::Duration::from_millis(
337 constants::BT_RETRY_DELAY_MS,
338 ))
339 .await;
340 }
341
342 Err(Aria2Error::Fatal(FatalError::Config(format!(
343 "Failed to download piece {} after {} attempts",
344 piece_index, MAX_RETRIES
345 ))))
346 }
347
348 /// Download all blocks for a piece using endgame mode (duplicate request strategy).
349 ///
350 /// In endgame mode, when few pieces remain, we request each block from ALL available
351 /// peers simultaneously. When any peer responds first, we immediately send Cancel
352 /// messages to the other peers to stop them from sending redundant data.
353 ///
354 /// # Phase 14 - B1/B2: Endgame Duplicate Request Strategy + Cancel on Block Arrival
355 ///
356 /// # Arguments
357 /// * `connections` - Mutable slice of active peer connections
358 /// * `piece_index` - Index of the piece to download
359 /// * `piece_length` - Total length of this piece in bytes
360 /// * `num_blocks` - Number of blocks in this piece
361 /// * `endgame_state` - Mutable reference to EndgameState for tracking duplicate requests
362 ///
363 /// # Returns
364 /// * `Ok(Vec<u8>)` - Complete piece data if all blocks downloaded successfully
365 /// * `Err(Aria2Error)` - If piece download fails after all retries
366 pub async fn download_piece_blocks_endgame(
367 connections: &mut [BtPeerConn],
368 piece_index: u32,
369 piece_length: u32,
370 num_blocks: u32,
371 endgame_state: &mut EndgameState,
372 ) -> Result<Vec<u8>> {
373 // Retry the entire piece multiple times (same as normal mode)
374 for _retry in 0..MAX_RETRIES {
375 info!(
376 "[BT] Endgame piece download attempt {} for piece {} ({} peers)",
377 _retry + 1,
378 piece_index,
379 connections.len()
380 );
381
382 let mut piece_data = Vec::with_capacity(piece_length as usize);
383 piece_data.clear();
384 let mut all_blocks_ok = true;
385
386 // Download each block using endgame strategy
387 for block_idx in 0..num_blocks {
388 let offset = block_idx * BLOCK_SIZE;
389 let len = if offset + BLOCK_SIZE > piece_length {
390 piece_length - offset
391 } else {
392 BLOCK_SIZE
393 };
394
395 debug!(
396 "[BT] Endgame: requesting block {}/{} (offset={}, len={}) from all {} peers",
397 block_idx + 1,
398 num_blocks,
399 offset,
400 len,
401 connections.len()
402 );
403
404 // Phase 14 - B1: Request this block from ALL peers and track duplicates
405 match Self::request_block_endgame(
406 connections,
407 piece_index,
408 offset,
409 len,
410 endgame_state,
411 )
412 .await
413 {
414 Ok(result) if result.success => {
415 if let Some(data) = result.data {
416 // Phase 14 - B2: Cancel redundant requests now that we have the block
417 Self::cancel_redundant_requests(
418 connections,
419 piece_index,
420 offset,
421 len,
422 endgame_state,
423 )
424 .await;
425
426 piece_data.extend_from_slice(&data);
427 } else {
428 all_blocks_ok = false;
429 break;
430 }
431 }
432 Ok(_) => {
433 warn!("[BT] Endgame: Block {} request returned no data", block_idx);
434 all_blocks_ok = false;
435 break;
436 }
437 Err(e) => {
438 warn!("[BT] Endgame: Block {} request error: {}", block_idx, e);
439 all_blocks_ok = false;
440 break;
441 }
442 }
443 }
444
445 // Check if we got all blocks
446 if all_blocks_ok && piece_data.len() == piece_length as usize {
447 info!(
448 "[BT] Endgame: All {} blocks downloaded for piece {} ({} bytes)",
449 num_blocks,
450 piece_index,
451 piece_data.len()
452 );
453 return Ok(piece_data);
454 }
455
456 warn!(
457 "[BT] Endgame: Incomplete piece {} (attempt {}/{}), retrying...",
458 piece_index,
459 _retry + 1,
460 MAX_RETRIES
461 );
462
463 // Small delay before retry
464 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
465 }
466
467 Err(Aria2Error::Fatal(FatalError::Config(format!(
468 "Failed to download piece {} in endgame mode after {} attempts",
469 piece_index, MAX_RETRIES
470 ))))
471 }
472
473 /// Request a single block from all peers during endgame mode.
474 ///
475 /// Sends the same block request to every connected peer simultaneously.
476 /// Tracks each request in the EndgameState so we can cancel redundant ones later.
477 ///
478 /// # Phase 14 - B1: Endgame Duplicate Request Strategy
479 async fn request_block_endgame(
480 connections: &mut [BtPeerConn],
481 piece_index: u32,
482 block_offset: u32,
483 block_length: u32,
484 endgame_state: &mut EndgameState,
485 ) -> Result<BlockDownloadResult> {
486 let req = aria2_protocol::bittorrent::message::types::PieceBlockRequest {
487 index: piece_index,
488 begin: block_offset,
489 length: block_length,
490 };
491
492 let mut total_bytes = 0u64;
493
494 // Phase 14 - B1: Send request to ALL peers (not just one)
495 for (conn_idx, conn) in connections.iter_mut().enumerate() {
496 debug!(
497 "[BT] Endgame: Sending duplicate request for block {} to peer {}",
498 block_offset / BLOCK_SIZE,
499 conn_idx
500 );
501
502 // Send request to this peer
503 if conn.send_request(req.clone()).await.is_err() {
504 warn!(
505 "[BT] Endgame: Failed to send request to peer {}, skipping",
506 conn_idx
507 );
508 continue;
509 }
510
511 // Track this duplicate request in endgame state
512 endgame_state.track_request(piece_index, block_offset, block_length, conn_idx);
513 }
514
515 // Now wait for the FIRST response from any peer (others will be cancelled later)
516 match tokio::time::timeout(
517 std::time::Duration::from_secs(BLOCK_REQUEST_TIMEOUT_SECS),
518 Self::wait_for_any_piece_block(connections, piece_index, block_offset),
519 )
520 .await
521 {
522 Ok(Ok((data, _peer_idx))) => {
523 debug!(
524 "[BT] Endgame: Got block {} data len={} (will cancel {} duplicates)",
525 block_offset / BLOCK_SIZE,
526 data.len(),
527 endgame_state
528 .get_cancel_targets(piece_index, block_offset, block_length)
529 .len()
530 .saturating_sub(1)
531 );
532 total_bytes += data.len() as u64;
533
534 return Ok(BlockDownloadResult {
535 success: true,
536 data: Some(data),
537 bytes_received: total_bytes,
538 });
539 }
540 Ok(Err(e)) => {
541 warn!(
542 "[BT] Endgame: No PIECE message received from any peer: {}",
543 e
544 );
545 }
546 Err(_) => {
547 warn!(
548 "[BT] Endgame: Block request timed out after {}s",
549 BLOCK_REQUEST_TIMEOUT_SECS
550 );
551 }
552 }
553
554 // All peers failed or timed out
555 warn!("[BT] Endgame: Failed to get block from any peer");
556 Ok(BlockDownloadResult {
557 success: false,
558 data: None,
559 bytes_received: total_bytes,
560 })
561 }
562
563 /// Wait for a specific PIECE message from ANY peer.
564 ///
565 /// Unlike `wait_for_piece_block` which waits on a single connection,
566 /// this polls all connections until the expected block arrives.
567 async fn wait_for_any_piece_block(
568 connections: &mut [BtPeerConn],
569 expected_index: u32,
570 expected_begin: u32,
571 ) -> Result<(Vec<u8>, usize)> {
572 // Poll each connection in round-robin fashion
573 for _ in 0..MAX_BLOCK_READ_MESSAGES {
574 for (conn_idx, conn) in connections.iter_mut().enumerate() {
575 match conn.read_message().await {
576 Ok(Some(msg)) => {
577 use aria2_protocol::bittorrent::message::types::BtMessage;
578
579 match msg {
580 BtMessage::Piece {
581 index,
582 begin,
583 ref data,
584 } => {
585 if index == expected_index && begin == expected_begin {
586 return Ok((data.clone(), conn_idx));
587 }
588 // Not the block we're waiting for, continue
589 debug!(
590 "[BT] Endgame: Received unexpected PIECE (index={}, begin={}) from peer {}, waiting for ({}, {})",
591 index, begin, conn_idx, expected_index, expected_begin
592 );
593 }
594 BtMessage::AllowedFast { index } => {
595 debug!("[BT] Received AllowedFast for piece {}", index);
596 conn.add_allowed_fast(index);
597 }
598 other => {
599 debug!(
600 "[BT] Endgame: Received non-PIECE message while waiting: {:?}",
601 other
602 );
603 }
604 }
605 }
606 Ok(None) => {
607 // Connection closed by this peer, try next
608 debug!("[BT] Endgame: Peer {} connection closed", conn_idx);
609 }
610 Err(e) => {
611 debug!("[BT] Endgame: Error reading from peer {}: {}", conn_idx, e);
612 }
613 }
614 }
615 }
616
617 Err(Aria2Error::Recoverable(
618 RecoverableError::TemporaryNetworkFailure {
619 message: format!(
620 "Exceeded max messages ({}) without receiving expected block from any peer",
621 MAX_BLOCK_READ_MESSAGES
622 ),
623 },
624 ))
625 }
626
627 /// Cancel redundant requests for a completed block.
628 ///
629 /// After receiving a block from one peer during endgame mode, sends Cancel
630 /// messages to all other peers that were sent duplicate requests for the same block.
631 ///
632 /// # Phase 14 - B2: Cancel Redundant Requests on Block Arrival
633 async fn cancel_redundant_requests(
634 connections: &mut [BtPeerConn],
635 piece_index: u32,
636 offset: u32,
637 len: u32,
638 endgame_state: &mut EndgameState,
639 ) {
640 // Get list of peers that have pending requests for this block
641 let targets = endgame_state.get_cancel_targets(piece_index, offset, len);
642
643 if targets.is_empty() {
644 debug!(
645 "[BT] Endgame: No redundant requests to cancel for piece {} block {}",
646 piece_index,
647 offset / BLOCK_SIZE
648 );
649 return;
650 }
651
652 let cancel_req = aria2_protocol::bittorrent::message::types::PieceBlockRequest {
653 index: piece_index,
654 begin: offset,
655 length: len,
656 };
657
658 debug!(
659 "[BT] Endgame: Cancelling {} redundant requests for piece {} block offset={}",
660 targets.len(),
661 piece_index,
662 offset
663 );
664
665 // Send Cancel to each peer that had a pending request
666 for peer_id in targets {
667 if let Some(conn) = connections.get_mut(peer_id) {
668 match conn.send_cancel(&cancel_req).await {
669 Ok(()) => {
670 debug!(
671 "[BT] Endgame: Sent Cancel to peer {} for piece {} offset={} len={}",
672 peer_id, piece_index, offset, len
673 );
674 }
675 Err(e) => {
676 warn!(
677 "[BT] Endgame: Failed to send Cancel to peer {}: {}",
678 peer_id, e
679 );
680 }
681 }
682 }
683 }
684
685 // Remove the tracked request since we've handled it
686 endgame_state.remove_request(piece_index, offset, len);
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693
694 #[test]
695 fn test_block_size_constant() {
696 assert_eq!(BLOCK_SIZE, 16384);
697 assert_eq!(BLOCK_SIZE, 16 * 1024); // 16 KB
698 }
699
700 #[test]
701 fn test_constants_are_reasonable() {
702 const _: () = {
703 assert!(MAX_RETRIES >= 1);
704 assert!(MAX_RETRIES <= 10);
705 assert!(BLOCK_REQUEST_TIMEOUT_SECS >= 1);
706 assert!(BLOCK_REQUEST_TIMEOUT_SECS <= 30);
707 assert!(MAX_BLOCK_READ_MESSAGES >= 100);
708 };
709 }
710
711 #[test]
712 fn test_block_download_result_default() {
713 let result = BlockDownloadResult {
714 success: false,
715 data: None,
716 bytes_received: 0,
717 };
718 assert!(!result.success);
719 assert!(result.data.is_none());
720 assert_eq!(result.bytes_received, 0);
721 }
722}