1use std::{
2 cell::OnceCell,
3 collections::VecDeque,
4 io::{self},
5 net::{SocketAddrV4, TcpListener},
6 path::{Path, PathBuf},
7 sync::mpsc::{Receiver, Sender},
8 time::{Duration, Instant},
9};
10
11use crate::{
12 buf_pool::{Buffer, BufferPool},
13 event_loop::{ConnectionId, EventLoop},
14 file_store::DiskOpType,
15 peer_comm::{extended_protocol::MetadataProgress, peer_protocol::PeerId},
16};
17use crate::{
18 file_store::DiskOp,
19 piece_selector::{DownloadedPiece, Piece, PieceSelector, SUBPIECE_SIZE, Subpiece},
20};
21use crate::{file_store::FileStore, peer_connection::PeerConnection};
22use ahash::HashSetExt;
23use bitvec::{boxed::BitBox, order::Msb0, vec::BitVec};
24use heapless::spsc::Producer;
25use io_uring::IoUring;
26use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
27use slotmap::SlotMap;
28use thiserror::Error;
29
30use crate::peer_connection::DisconnectReason;
31
32use crate::TorrentMetadata;
33
34#[derive(Error, Debug)]
35pub enum Error {
36 #[error("Encountered IO issue: {0}")]
37 Io(#[from] io::Error),
38 #[error("Peer provider disconnected")]
39 PeerProviderDisconnect,
40}
41
42pub const CQE_WAIT_TIME_NS: u32 = 150_000_000;
43
44#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
46#[serde(default)]
47pub struct Config {
48 pub max_connections: usize,
50 pub max_reported_outstanding_requests: u64,
55 pub max_unchoked: u32,
57 pub num_ticks_before_unchoke_recalc: u32,
61 pub num_ticks_before_optimistic_unchoke_recalc: u32,
68 pub seeding_piece_quota: u32,
72 pub cq_size: u32,
74 pub sq_size: u32,
76 pub completion_event_want: usize,
80 pub network_read_buffer_size: usize,
82 pub network_write_buffer_size: usize,
84 pub write_buffer_pool_size: usize,
86 pub read_buffer_pool_size: usize,
88}
89
90impl Default for Config {
91 fn default() -> Self {
92 Self {
93 max_connections: 128,
94 max_reported_outstanding_requests: 512,
95 max_unchoked: 8,
96 num_ticks_before_unchoke_recalc: 15,
97 num_ticks_before_optimistic_unchoke_recalc: 30,
98 seeding_piece_quota: 20,
99 cq_size: 4096,
100 sq_size: 4096,
101 completion_event_want: 32,
102 network_read_buffer_size: (SUBPIECE_SIZE * 2) as usize,
103 read_buffer_pool_size: 512,
104 network_write_buffer_size: (SUBPIECE_SIZE + 4096) as usize,
105 write_buffer_pool_size: 128,
106 }
107 }
108}
109
110pub struct Torrent {
114 our_id: PeerId,
115 state: State,
116}
117
118impl Torrent {
119 pub fn new(our_id: PeerId, state: State) -> Self {
122 Self { our_id, state }
123 }
124
125 pub fn is_complete(&self) -> bool {
131 self.state
132 .torrent_state
133 .as_ref()
134 .is_some_and(|state| state.is_complete)
135 }
136
137 pub fn start(
144 &mut self,
145 event_tx: Producer<TorrentEvent>,
146 command_rc: Receiver<Command>,
147 listener: TcpListener,
148 ) -> Result<(), Error> {
149 let ring: IoUring = IoUring::builder()
151 .setup_single_issuer()
152 .setup_clamp()
153 .setup_cqsize(self.state.config.cq_size)
154 .setup_defer_taskrun()
155 .setup_coop_taskrun()
156 .build(self.state.config.sq_size)
157 .unwrap();
158 let events = SlotMap::with_capacity_and_key(self.state.config.cq_size as usize);
159 let mut event_loop = EventLoop::new(self.our_id, events, &self.state.config);
160 event_loop.run(ring, &mut self.state, event_tx, command_rc, listener)
161 }
162}
163
164#[derive(Debug)]
166pub enum Command {
167 ConnectToPeers(Vec<SocketAddrV4>),
170 Stop,
172 Pause,
174 Resume,
178}
179
180#[derive(Debug)]
182pub struct PeerMetrics {
183 pub download_throughput: u64,
185 pub upload_throughput: u64,
187 pub metadata_progress: Option<MetadataProgress>,
189}
190
191#[derive(Debug, Clone)]
199pub struct TorrentProgress {
200 completed: BitBox<u8, Msb0>,
203 completed_count: usize,
205}
206
207impl TorrentProgress {
208 pub(crate) fn new(completed: BitBox<u8, Msb0>) -> Self {
210 let completed_count = completed.count_ones();
211 Self {
212 completed,
213 completed_count,
214 }
215 }
216
217 #[inline]
219 pub fn num_pieces(&self) -> usize {
220 self.completed.len()
221 }
222
223 #[inline]
225 pub fn total_completed(&self) -> usize {
226 self.completed_count
227 }
228
229 #[inline]
232 pub fn get(&self, index: usize) -> bool {
233 self.completed.get(index).is_some_and(|bit| *bit)
234 }
235
236 #[inline]
238 pub fn iter(&self) -> ProgressIter<'_> {
239 ProgressIter {
240 inner: self.completed.iter().by_vals(),
241 }
242 }
243}
244
245impl<'a> IntoIterator for &'a TorrentProgress {
246 type Item = bool;
247 type IntoIter = ProgressIter<'a>;
248
249 fn into_iter(self) -> Self::IntoIter {
250 self.iter()
251 }
252}
253
254pub struct ProgressIter<'a> {
257 inner: bitvec::slice::BitValIter<'a, u8, Msb0>,
258}
259
260impl Iterator for ProgressIter<'_> {
261 type Item = bool;
262
263 #[inline]
264 fn next(&mut self) -> Option<bool> {
265 self.inner.next()
266 }
267
268 #[inline]
269 fn size_hint(&self) -> (usize, Option<usize>) {
270 self.inner.size_hint()
271 }
272}
273
274impl ExactSizeIterator for ProgressIter<'_> {}
275
276#[derive(Debug)]
278pub enum TorrentEvent {
279 TorrentComplete,
284 MetadataComplete(Box<TorrentMetadata>),
288 Running { port: u16 },
293 Paused,
295 TorrentMetrics {
297 pieces_allocated: usize,
300 peer_metrics: Vec<PeerMetrics>,
302 progress: Option<TorrentProgress>,
310 },
311}
312
313pub struct InitializedState {
314 pub piece_selector: PieceSelector,
315 pub num_unchoked: u32,
316 pub config: Config,
317 pub ticks_to_recalc_unchoke: u32,
318 pub ticks_to_recalc_optimistic_unchoke: u32,
319 pub downloaded_piece_rc: Receiver<DownloadedPiece>,
320 pub downloaded_piece_tx: Sender<DownloadedPiece>,
321 pub pieces: Vec<Option<Piece>>,
322 pub file_store: FileStore,
323 pub piece_buffer_pool: BufferPool,
324 pub is_complete: bool,
325}
326
327impl InitializedState {
328 pub fn new(root: &Path, metadata: &TorrentMetadata, config: Config) -> io::Result<Self> {
329 let mut pieces = Vec::with_capacity(metadata.pieces.len());
330 for _ in 0..metadata.pieces.len() {
331 pieces.push(None);
332 }
333 let (tx, rc) = std::sync::mpsc::channel();
334 Ok(Self {
335 piece_selector: PieceSelector::new(metadata),
336 num_unchoked: 0,
337 config,
338 ticks_to_recalc_unchoke: config.num_ticks_before_unchoke_recalc,
339 ticks_to_recalc_optimistic_unchoke: config.num_ticks_before_optimistic_unchoke_recalc,
340 downloaded_piece_rc: rc,
341 downloaded_piece_tx: tx,
342 pieces,
343 is_complete: false,
344 piece_buffer_pool: BufferPool::new("pieces", 256, metadata.piece_length as usize),
345 file_store: FileStore::new(root, metadata)?,
346 })
347 }
348
349 #[allow(dead_code)]
350 pub fn num_allocated(&self) -> usize {
351 self.pieces
352 .iter()
353 .filter(|piece| piece.as_ref().is_some_and(|piece| piece.ref_count > 0))
354 .count()
355 }
356
357 #[inline]
358 pub fn num_pieces(&self) -> usize {
359 self.pieces.len()
360 }
361
362 pub(crate) fn complete_piece(
363 &mut self,
364 piece_idx: i32,
365 connections: &mut SlotMap<ConnectionId, PeerConnection>,
366 event_tx: &mut Producer<'_, TorrentEvent>,
367 piece_buffer: Buffer,
368 ) {
369 self.piece_buffer_pool.return_buffer(piece_buffer);
370 self.piece_selector.mark_complete(piece_idx as usize);
371 if !self.is_complete && self.piece_selector.completed_all() {
372 log::info!("Torrent complete!");
373 self.is_complete = true;
374 if event_tx.enqueue(TorrentEvent::TorrentComplete).is_err() {
375 log::error!("Torrent completion event missed");
376 }
377 for (_, peer) in connections.iter_mut() {
380 peer.not_interested();
381 if peer.is_upload_only {
385 peer.pending_disconnect = Some(DisconnectReason::RedundantConnection);
386 }
387 for extension in peer.extensions.values_mut() {
389 extension.on_torrent_complete(&mut peer.outgoing_msgs_buffer);
390 }
391 }
392 }
393 for (conn_id, peer) in connections.iter_mut() {
394 if let Some(bitfield) = self.piece_selector.interesting_peer_pieces(conn_id)
395 && !bitfield.any()
396 && peer.is_interesting
397 && peer.queued.is_empty()
398 && peer.inflight.is_empty()
399 {
400 peer.not_interested();
402 if peer.is_upload_only {
406 peer.pending_disconnect = Some(DisconnectReason::RedundantConnection);
407 }
408 }
409 peer.have(piece_idx);
410 }
411 log::debug!("Piece {piece_idx} completed!");
412 }
413
414 pub(crate) fn queue_disk_write_for_downloaded_pieces(
416 &mut self,
417 pending_disk_operations: &mut Vec<DiskOp>,
418 ) {
419 while let Ok(completed_piece) = self.downloaded_piece_rc.try_recv() {
420 if completed_piece.hash_matched {
421 let piece_len = self.piece_selector.piece_len(completed_piece.index as i32);
422 self.file_store.queue_piece_disk_operation(
423 completed_piece.index as i32,
424 completed_piece.buffer,
425 piece_len as usize,
426 DiskOpType::Write,
427 pending_disk_operations,
428 );
429 } else {
430 self.piece_selector
433 .mark_not_downloaded(completed_piece.index);
434 self.piece_buffer_pool.return_buffer(completed_piece.buffer);
435 log::error!("Piece hash didn't match expected hash!");
438 self.piece_selector
439 .mark_not_allocated(completed_piece.index as i32, completed_piece.conn_id);
440 }
441 }
442 }
443
444 pub fn allocate_piece(&mut self, index: i32, conn_id: ConnectionId) -> VecDeque<Subpiece> {
446 log::debug!("Allocating piece: conn_id: {conn_id:?}, index: {index}");
447 self.piece_selector.mark_allocated(index, conn_id);
448 match &mut self.pieces[index as usize] {
449 Some(allocated_piece) => allocated_piece.allocate_remaining_subpieces(),
450 None => {
451 let length = self.piece_selector.piece_len(index);
452 let buffer = self.piece_buffer_pool.get_buffer();
453 let mut piece = Piece::new(index, length, buffer);
454 let subpieces = piece.allocate_remaining_subpieces();
455 self.pieces[index as usize] = Some(piece);
456 subpieces
457 }
458 }
459 }
460
461 pub fn deallocate_piece(&mut self, index: i32, conn_id: ConnectionId) {
463 log::debug!("Deallocating piece: conn_id: {conn_id:?}, index: {index}");
464 self.piece_selector
467 .update_peer_piece_intrest(conn_id, index as usize);
468 if let Some(piece) = self.pieces[index as usize].take_if(|piece| {
472 piece.ref_count = piece.ref_count.saturating_sub(1);
473 piece.ref_count == 0
474 }) {
475 log::debug!("Marked as not allocated: conn_id: {conn_id:?}, index: {index}");
476 self.piece_buffer_pool.return_buffer(piece.into_buffer());
477 self.piece_selector.mark_not_allocated(index, conn_id);
478 }
479 }
480
481 pub fn can_preemtively_unchoke(&self) -> bool {
482 self.num_unchoked < self.config.max_unchoked
483 }
484
485 pub fn recalculate_unchokes(
489 &mut self,
490 connections: &mut SlotMap<ConnectionId, PeerConnection>,
491 ) {
492 struct ComparisonData {
493 is_choking: bool,
494 uploaded_since_unchoked: u64,
495 downloaded_in_last_round: u64,
496 uploaded_in_last_round: u64,
497 last_unchoked: Option<Instant>,
498 }
499 log::info!("Recalculating unchokes");
500 let mut peers = Vec::with_capacity(connections.len());
501 for (id, peer) in connections.iter_mut() {
502 if !peer.peer_interested || peer.pending_disconnect.is_some() {
503 peer.network_stats.reset_round();
504 if !peer.is_choking {
505 if peer.optimistically_unchoked {
506 peer.optimistically_unchoked = false;
507 self.ticks_to_recalc_optimistic_unchoke = 0;
509 }
510 peer.choke(self);
511 }
512
513 continue;
514 }
515 peers.push((
516 id,
517 ComparisonData {
518 is_choking: peer.is_choking,
519 uploaded_since_unchoked: peer.network_stats.upload_since_unchoked,
520 downloaded_in_last_round: peer.network_stats.downloaded_in_last_round,
521 uploaded_in_last_round: peer.network_stats.uploaded_in_last_round,
522 last_unchoked: peer.last_unchoked,
523 },
524 ));
525 }
526 if !self.is_complete {
527 peers.sort_unstable_by(|(_, a), (_, b)| {
529 a.downloaded_in_last_round
530 .cmp(&b.downloaded_in_last_round)
531 .reverse()
532 });
533 } else {
534 let piece_length = self.piece_selector.avg_piece_length();
535 let quota_bytes = (piece_length * self.config.seeding_piece_quota) as u64;
536 peers.sort_unstable_by(|(_, a), (_, b)| {
538 let a_quota_complete = !a.is_choking
539 && a.uploaded_since_unchoked > quota_bytes
540 && a.last_unchoked
541 .is_some_and(|time| time.elapsed() > Duration::from_mins(1));
542 let b_quota_complete = !b.is_choking
543 && b.uploaded_since_unchoked > quota_bytes
544 && b.last_unchoked
545 .is_some_and(|time| time.elapsed() > Duration::from_mins(1));
546 if a_quota_complete != b_quota_complete {
547 a_quota_complete.cmp(&b_quota_complete)
548 } else if a.uploaded_in_last_round != b.uploaded_in_last_round {
549 a.uploaded_in_last_round
550 .cmp(&b.uploaded_in_last_round)
551 .reverse()
552 } else {
553 a.last_unchoked
554 .map_or(Duration::MAX, |time| time.elapsed())
555 .cmp(&b.last_unchoked.map_or(Duration::MAX, |time| time.elapsed()))
556 .reverse()
557 }
558 });
559 }
560 let optimistic_unchoke_slots = std::cmp::max(1, self.config.max_unchoked / 5);
561 let mut remaining_unchoke_slots = self.config.max_unchoked - optimistic_unchoke_slots;
562 for (id, _) in peers {
563 let peer = &mut connections[id];
564 peer.network_stats.reset_round();
565 if remaining_unchoke_slots > 0 {
566 if peer.is_choking {
567 log::debug!(
568 "Peer[{}] now unchoked after recalculating throughputs",
569 peer.peer_id
570 );
571 peer.unchoke(self);
572 }
573 remaining_unchoke_slots -= 1;
574 if peer.optimistically_unchoked {
575 log::trace!(
576 "Peer[{}] previously optimistically unchoked, promoted to normal unchoke",
577 peer.peer_id
578 );
579 peer.optimistically_unchoked = false;
582 self.ticks_to_recalc_optimistic_unchoke = 0;
584 }
585 } else if !peer.is_choking && !peer.optimistically_unchoked {
586 log::debug!(
587 "Peer[{}] no longer unchoked after recalculating throughputs",
588 peer.peer_id
589 );
590 peer.choke(self);
591 }
592 }
593 }
594
595 pub fn recalculate_optimistic_unchokes(
598 &mut self,
599 connections: &mut SlotMap<ConnectionId, PeerConnection>,
600 ) {
601 log::info!("Recalculating optimistic unchokes");
602 let num_opt_unchoked = std::cmp::max(1, self.config.max_unchoked / 5) as usize;
603 let mut previously_opt_unchoked = ahash::HashSet::with_capacity(num_opt_unchoked);
604 let mut candidates = Vec::with_capacity(self.config.max_unchoked as usize);
605 for (id, peer) in connections.iter_mut() {
606 if peer.optimistically_unchoked {
607 previously_opt_unchoked.insert(id);
608 }
609 if peer.pending_disconnect.is_none()
610 && peer.peer_interested
611 && (peer.is_choking || peer.optimistically_unchoked)
612 {
613 candidates.push((
614 id,
615 peer.last_optimistically_unchoked
616 .map_or(u64::MAX, |time| time.elapsed().as_secs()),
617 ));
618 }
619 }
620 candidates.sort_unstable_by(|(_, a), (_, b)| a.cmp(b).reverse());
622 for (id, _) in candidates.iter().take(num_opt_unchoked) {
623 let peer = &mut connections[*id];
624 if peer.optimistically_unchoked {
625 log::debug!("Peer[{}] optmistically unchoked again", peer.peer_id);
626 previously_opt_unchoked.remove(id);
627 } else {
628 peer.optimistically_unchoke(self);
629 }
630 }
631
632 for id in previously_opt_unchoked {
633 let peer = &mut connections[id];
634 peer.choke(self);
635 }
636 }
637}
638
639impl Drop for InitializedState {
640 fn drop(&mut self) {
641 self.piece_buffer_pool.stop_tracking();
642 }
643}
644
645pub struct State {
647 pub(crate) info_hash: [u8; 20],
648 pub(crate) listener_port: Option<u16>,
649 root: PathBuf,
651 torrent_state: Option<InitializedState>,
652 file: OnceCell<Box<TorrentMetadata>>,
653 pub(crate) config: Config,
654}
655
656impl State {
657 pub fn info_hash(&self) -> [u8; 20] {
659 self.info_hash
660 }
661
662 pub fn unstarted(info_hash: [u8; 20], root: PathBuf, config: Config) -> Self {
668 Self {
669 info_hash,
670 root,
671 listener_port: None,
672 torrent_state: None,
673 file: OnceCell::new(),
674 config,
675 }
676 }
677
678 pub fn is_complete(&self) -> bool {
680 self.torrent_state
681 .as_ref()
682 .is_some_and(|state| state.is_complete)
683 }
684
685 pub fn progress(&self) -> Option<TorrentProgress> {
697 self.torrent_state
698 .as_ref()
699 .map(|state| state.piece_selector.progress())
700 }
701
702 pub fn from_metadata_and_root(
714 metadata: TorrentMetadata,
715 root: PathBuf,
716 config: Config,
717 ) -> io::Result<Self> {
718 let mut initialized_state = InitializedState::new(&root, &metadata, config)?;
719 let file_store = &initialized_state.file_store;
720 let completed_pieces: Box<[bool]> = metadata
721 .pieces
722 .as_slice()
723 .par_iter()
724 .enumerate()
725 .map(
726 |(idx, hash)| match file_store.check_piece_hash_sync(idx as i32, hash) {
727 Ok(is_valid) => is_valid,
728 Err(err) => {
729 if err.kind() != io::ErrorKind::NotFound {
730 log::warn!("Error checking piece {idx}: {err}");
731 }
732 false
733 }
734 },
735 )
736 .collect();
737 let completed_pieces: BitVec<u8, Msb0> = completed_pieces.into_iter().collect();
738 let completed_pieces: BitBox<u8, Msb0> = completed_pieces.into_boxed_bitslice();
739 log::trace!("Completed pieces: {completed_pieces}");
740 initialized_state
741 .piece_selector
742 .set_completed_bitfield(completed_pieces);
743 initialized_state.is_complete = initialized_state.piece_selector.completed_all();
744
745 Ok(Self {
746 info_hash: metadata
747 .info_hash_bytes()
748 .try_into()
749 .expect("Invalid info hash"),
750 root,
751 listener_port: None,
752 torrent_state: Some(initialized_state),
753 file: OnceCell::from(Box::new(metadata)),
754 config,
755 })
756 }
757
758 #[cfg(test)]
759 pub fn inprogress(
760 info_hash: [u8; 20],
761 root: PathBuf,
762 metadata: lava_torrent::torrent::v1::Torrent,
763 state: InitializedState,
764 config: Config,
765 ) -> Self {
766 Self {
767 info_hash,
768 root,
769 listener_port: None,
770 torrent_state: Some(state),
771 file: OnceCell::from(Box::new(metadata)),
772 config,
773 }
774 }
775
776 pub fn as_ref(&mut self) -> StateRef<'_> {
777 StateRef {
778 info_hash: self.info_hash,
779 root: &self.root,
780 listener_port: &self.listener_port,
781 torrent: &mut self.torrent_state,
782 full: &self.file,
783 config: &self.config,
784 }
785 }
786}
787
788pub struct StateRef<'state> {
790 info_hash: [u8; 20],
791 root: &'state Path,
792 pub listener_port: &'state Option<u16>,
793 torrent: &'state mut Option<InitializedState>,
794 full: &'state OnceCell<Box<TorrentMetadata>>,
795 pub config: &'state Config,
796}
797
798impl<'e_iter, 'state: 'e_iter> StateRef<'state> {
799 pub fn info_hash(&self) -> &[u8; 20] {
800 &self.info_hash
801 }
802
803 pub fn state(&'e_iter mut self) -> Option<&'e_iter mut InitializedState> {
804 self.torrent.as_mut()
805 }
806
807 #[allow(clippy::borrowed_box)]
808 pub fn metadata(&'e_iter mut self) -> Option<&'state Box<TorrentMetadata>> {
809 self.full.get()
810 }
811
812 #[inline]
813 pub fn is_initialzied(&self) -> bool {
814 self.full.get().is_some()
815 }
816
817 pub fn init(&'e_iter mut self, metadata: TorrentMetadata) -> io::Result<()> {
818 if self.is_initialzied() {
819 return Err(io::Error::other("State initialized twice"));
820 }
821 *self.torrent = Some(InitializedState::new(self.root, &metadata, *self.config)?);
822 self.full
823 .set(Box::new(metadata))
824 .map_err(|_e| io::Error::other("State initialized twice"))?;
825 Ok(())
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::TorrentProgress;
832 use bitvec::prelude::{Msb0, bitbox};
833
834 #[test]
835 fn progress_get_is_msb0_and_bounds_checked() {
836 let progress = TorrentProgress::new(bitbox![u8, Msb0; 1, 0, 1, 0]);
838 assert!(progress.get(0));
839 assert!(!progress.get(1));
840 assert!(progress.get(2));
841 assert!(!progress.get(3));
842 assert!(!progress.get(4));
844 assert!(!progress.get(100));
845 }
846
847 #[test]
848 fn progress_total_completed_counts_set_bits() {
849 let progress = TorrentProgress::new(bitbox![u8, Msb0; 1, 0, 1, 0]);
850 assert_eq!(progress.num_pieces(), 4);
851 assert_eq!(progress.total_completed(), 2);
852 }
853
854 #[test]
855 fn progress_iter_yields_each_piece_in_order() {
856 let progress = TorrentProgress::new(bitbox![u8, Msb0; 1, 0, 1, 0]);
857 let states: Vec<bool> = progress.iter().collect();
858 assert_eq!(states, vec![true, false, true, false]);
859 assert_eq!((&progress).into_iter().count(), 4);
861 assert_eq!(progress.iter().len(), 4);
862 }
863
864 #[test]
865 fn empty_progress_has_no_pieces() {
866 let progress = TorrentProgress::new(bitbox![u8, Msb0;]);
867 assert_eq!(progress.num_pieces(), 0);
868 assert_eq!(progress.total_completed(), 0);
869 assert!(!progress.get(0));
870 assert_eq!(progress.iter().count(), 0);
871 }
872}