Skip to main content

vortex_bittorrent/
torrent.rs

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/// Configuration settings for a given torrent
45#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
46#[serde(default)]
47pub struct Config {
48    /// The max number of total connections that can be open at a time for the torrent
49    pub max_connections: usize,
50    /// The max number of outstanding requests this vortex should report to other peers via the
51    /// `reqq` extension. This currently does not impact the behavoir of vortex but it should impact
52    /// the amount of load connected peers send to us. Vortex does respect connected peers reported
53    /// `reqq` value.
54    pub max_reported_outstanding_requests: u64,
55    /// The maximal amount of allowed unchoked peers at any given time
56    pub max_unchoked: u32,
57    /// Controls how frequently "which peers should be unchoked" is calculated.
58    /// After this number of ticks, vortex will look over all peers and redistribute
59    /// which ones are unchoked and not.
60    pub num_ticks_before_unchoke_recalc: u32,
61    /// Controls the longest possible interval "which peers should be optimistically unchoked" is calculated.
62    /// After this number of ticks, vortex will look over all peers and redistribute
63    /// which ones are optimistically unchoked and not. Note that this may happen more frequently
64    /// than this number suggests due to the normal unchoke distribution "promoting" optimistically
65    /// unchoked peers to normally unchoked peers. In that case this recaluclation will happen
66    /// immedieately afterwards.
67    pub num_ticks_before_optimistic_unchoke_recalc: u32,
68    /// This determines the minimal target of pieces we want to upload to a peer before
69    /// moving on to another peer when the "round-robin" unchoking strategy is used. The
70    /// "round-robin" strategy is currently only used when seeding.
71    pub seeding_piece_quota: u32,
72    /// Controls the size of the io_uring completion queue
73    pub cq_size: u32,
74    /// Controls the size of the io_uring submission queue
75    pub sq_size: u32,
76    /// The event loop will wait for at least these amont of completion events
77    /// before it starts processing them. If the target isn't reached it will wait for
78    /// at most [ `CQE_WAIT_TIME_NS` ] nanoseconds before processing the ones currently in the completion queue.
79    pub completion_event_want: usize,
80    /// The size of the Read buffers used for network operations. Defaults to SUBPIECE_SIZE * 2
81    pub network_read_buffer_size: usize,
82    /// The size of the Write buffers used for network operations. Defaults to SUBPIECE_SIZE + 4Kb
83    pub network_write_buffer_size: usize,
84    /// The size of the Write network buffer pools
85    pub write_buffer_pool_size: usize,
86    /// The size of the Reaad network buffer pools
87    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
110/// A single torrent containing the current torrent state
111/// and our peer id. This object is responsible for starting
112/// the I/O event loop.
113pub struct Torrent {
114    our_id: PeerId,
115    state: State,
116}
117
118impl Torrent {
119    /// Create a new torrent from the given state. Use the given peer id
120    /// when communicating with other peers.
121    pub fn new(our_id: PeerId, state: State) -> Self {
122        Self { our_id, state }
123    }
124
125    /// Returns if the torrent is completed or not.
126    /// This is mostly useful to check status BEFORE
127    /// starting the torrent. Use [ `TorrentEvent::TorrentComplete` ]
128    /// to detect if a torrent have been completed download if you start
129    /// an incomplete torrent.
130    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    /// Start the I/O event loop and the torrent itself. This will start
138    /// a new io_uring instance and new buffer pools
139    /// will be allocated and registered to the io uring instance.
140    /// This is expected to run in a separate thread and interaction with
141    /// the event loop happens via the mpsc command queue. Torrent events will be sent to the
142    /// consumer of the `event_tx` spsc channel.
143    pub fn start(
144        &mut self,
145        event_tx: Producer<TorrentEvent>,
146        command_rc: Receiver<Command>,
147        listener: TcpListener,
148    ) -> Result<(), Error> {
149        // check ulimit
150        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/// Commands that can be sent to the torrent event loop through the command channel
165#[derive(Debug)]
166pub enum Command {
167    /// Connect to peers at the given address.
168    /// Already connected peers will be filtered out
169    ConnectToPeers(Vec<SocketAddrV4>),
170    /// Stop the event loop gracefully, disconnecting peers and freeing resources
171    Stop,
172    /// Pause the torrent gracefully, disconnecting any peers but keeping resources in memory.
173    Pause,
174    /// Resumes a paused torrent, only relevant when the torrent is in the "Paused" state.
175    /// Use the `TorrentEvent::Paused` event to determine when the torrent successfully entered
176    /// the paused state.
177    Resume,
178}
179
180/// Metrics for a given peer
181#[derive(Debug)]
182pub struct PeerMetrics {
183    /// The numer of bytes per second we are currently downloading from the peer
184    pub download_throughput: u64,
185    /// The number of bytes per second we are currently uploading to the peer
186    pub upload_throughput: u64,
187    /// Progress for downloading metadata if supported
188    pub metadata_progress: Option<MetadataProgress>,
189}
190
191/// Per-piece download progress for a torrent: which pieces are complete
192/// (downloaded *and* hash-verified) and how many.
193///
194/// This hides the underlying MSB0-packed bitfield: query individual pieces
195/// with [`TorrentProgress::get`], the completed count with
196/// [`TorrentProgress::total_completed`], and iterate over every piece's
197/// completion state via [`TorrentProgress::iter`] (or `&progress` directly).
198#[derive(Debug, Clone)]
199pub struct TorrentProgress {
200    /// Completed-piece bitfield, one bit per piece: bit `i` is set when piece
201    /// `i` is complete. Its length equals the torrent's piece count.
202    completed: BitBox<u8, Msb0>,
203    /// Cached count of completed pieces.
204    completed_count: usize,
205}
206
207impl TorrentProgress {
208    /// Builds progress from the completed-piece bitfield (one bit per piece).
209    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    /// Total number of pieces in the torrent.
218    #[inline]
219    pub fn num_pieces(&self) -> usize {
220        self.completed.len()
221    }
222
223    /// Number of completed (downloaded and hash-verified) pieces.
224    #[inline]
225    pub fn total_completed(&self) -> usize {
226        self.completed_count
227    }
228
229    /// Whether piece `index` is complete. Returns `false` for out-of-range
230    /// indices.
231    #[inline]
232    pub fn get(&self, index: usize) -> bool {
233        self.completed.get(index).is_some_and(|bit| *bit)
234    }
235
236    /// Iterates over the completion state of every piece, in order.
237    #[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
254/// Iterator over the per-piece completion state of a [`TorrentProgress`],
255/// yielding `true` for each complete piece and `false` otherwise.
256pub 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/// Events from the torrent
277#[derive(Debug)]
278pub enum TorrentEvent {
279    /// The torrent finished downloading. Note you will NOT receive
280    /// this event if the torrent already is completed when you are
281    /// starting the torrent. Use [ `Torrent::is_complete` ] to check if it's already completed
282    /// before starting the torrent.
283    TorrentComplete,
284    /// The metadata has finished downloading from the connected peers.
285    /// Note you will NOT receive this event if the metadata already is
286    /// completed when starting the torrent.
287    MetadataComplete(Box<TorrentMetadata>),
288    /// The listener for incoming connection has finished set up
289    /// on the provided port and the event loop has started.
290    /// This will be emitted on first startup and every time
291    /// the torrent is resumed after a previous pause.
292    Running { port: u16 },
293    /// The torrent was successfully paused.
294    Paused,
295    /// Over all metrics for the torrent, sent every tick.
296    TorrentMetrics {
297        /// How many pieces have been allocated by peers for
298        /// download
299        pieces_allocated: usize,
300        /// Peer metrics for all currently connected peers
301        peer_metrics: Vec<PeerMetrics>,
302        /// Per-piece completion progress (which pieces are complete and how
303        /// many), or `None` if the torrent has not been initialized yet
304        /// (e.g. a magnet still fetching metadata). Populated both while
305        /// downloading and by the startup hashing scan in
306        /// [`State::from_metadata_and_root`], so it is meaningful immediately
307        /// after a resume. Read the completed count via
308        /// [`TorrentProgress::total_completed`].
309        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            // We are no longer interestead in any of the
378            // peers
379            for (_, peer) in connections.iter_mut() {
380                peer.not_interested();
381                // If the peer is upload only and
382                // we are upload only there is no reason
383                // to stay connected
384                if peer.is_upload_only {
385                    peer.pending_disconnect = Some(DisconnectReason::RedundantConnection);
386                }
387                // Notify all extensions that the torrent completed
388                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                // We are no longer interestead in this peer
401                peer.not_interested();
402                // if it's upload only we can close the
403                // connection since it will never download from
404                // us
405                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    // TODO: Put this in the event loop directly instead when that is easier to test
415    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                // Only need to mark this as not downloaded when it fails
431                // since otherwise it will be marked as completed and this is moot
432                self.piece_selector
433                    .mark_not_downloaded(completed_piece.index);
434                self.piece_buffer_pool.return_buffer(completed_piece.buffer);
435                // deallocate piece peer peer
436                // TODO: disconnect
437                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    // Allocates a piece and increments the piece ref count
445    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    // Deallocate a piece
462    pub fn deallocate_piece(&mut self, index: i32, conn_id: ConnectionId) {
463        log::debug!("Deallocating piece: conn_id: {conn_id:?}, index: {index}");
464        // Mark the piece as interesting again so it can be picked again
465        // if necessary
466        self.piece_selector
467            .update_peer_piece_intrest(conn_id, index as usize);
468        // The piece might have been mid hashing when a timeout is received
469        // (two separate peer) which causes to be completed whilst another peer
470        // tried to download it. It's fine
471        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    /// Determine the most suited peers to be unchoked based on the selected unchoking strategy.
486    /// Some unchoke slots are left for optimistic unchokes. The strategy currently is only
487    /// affected if the torrent is completed or not.
488    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                        // reset so another peer can be optimistically unchoked
508                        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            // Sort based of downloaded_in_last_round
528            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            // The torrent is completed. Do round robin sorting like in libtorrent
537            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                    // no longer optimistic, the peer is "promoted"
580                    // to a normal unchoke slot
581                    peer.optimistically_unchoked = false;
582                    // reset so another peer can be optimistically unchoked
583                    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    // Give some lucky winners unchokes to test if they will have better throughput than the
596    // currently unchoked peers
597    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        // Sort in the order of peers that have waited the longest
621        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
645/// Current state of the torrent
646pub struct State {
647    pub(crate) info_hash: [u8; 20],
648    pub(crate) listener_port: Option<u16>,
649    // TODO: Consider checking this is accessible at construction
650    root: PathBuf,
651    torrent_state: Option<InitializedState>,
652    file: OnceCell<Box<TorrentMetadata>>,
653    pub(crate) config: Config,
654}
655
656impl State {
657    /// The torrent info hash
658    pub fn info_hash(&self) -> [u8; 20] {
659        self.info_hash
660    }
661
662    /// Use this constructor if the torrent is unstarted.
663    /// `info_hash` is the info hash of the torrent that should be downloaded.
664    /// `root` is the directory where the torrent will be downloaded into.
665    /// Vortex will create this directory if it doesn't already exist.
666    /// `config` is the vortex config that should be used
667    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    /// Returns if the torrent has been completed
679    pub fn is_complete(&self) -> bool {
680        self.torrent_state
681            .as_ref()
682            .is_some_and(|state| state.is_complete)
683    }
684
685    /// Returns the per-piece completion progress (which pieces are complete
686    /// and how many), or `None` if the torrent has not been initialized yet
687    /// (e.g. a magnet still fetching metadata).
688    ///
689    /// This is an at-rest accessor reflecting the startup hashing scan
690    /// performed by [`State::from_metadata_and_root`], so it is meaningful
691    /// *before* the event loop is started — useful for rendering accurate
692    /// progress for a resumed torrent without waiting for the first metrics
693    /// tick, and for inspecting on-disk progress or for tests. During an
694    /// active download the same information arrives via the `progress` field
695    /// of [`TorrentEvent::TorrentMetrics`].
696    pub fn progress(&self) -> Option<TorrentProgress> {
697        self.torrent_state
698            .as_ref()
699            .map(|state| state.piece_selector.progress())
700    }
701
702    /// Use this constructor if you have access to the torrent metadata
703    /// and/or if the torrent has already started.
704    ///
705    /// `metadata` is the metadata associated with the torrent.
706    /// `root` is the directory where potentially already started torrent files
707    /// are expected to be found. If the folder doesn't exist vortex will create it.
708    ///
709    /// `config` is the vortex config that should be used.
710    ///
711    /// NOTE: This will go through all files in `root` and hash their pieces (in parallel) to determine torrent progress
712    /// which may be slow on large torrents.
713    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
788// is this even needed?
789pub 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        // Pieces 0 and 2 complete, others clear.
837        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        // Out-of-range indices read as incomplete.
843        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        // `&progress` iterates too, and reports an exact length.
860        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}