Skip to main content

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}