From 640a042f9b10f9e5b445447bef6d8f56e560474e Mon Sep 17 00:00:00 2001 From: Ioannis Karadimas Date: Fri, 14 Aug 2026 18:29:29 +0300 Subject: [PATCH] feat(session): add opt-in SessionOptions::trust_fastresume to skip the startup recheck With `fastresume: true`, librqbit still re-verifies every restored torrent on every session start: `validate_fastresume` loads the persisted piece bitfield and, even when it is present and the correct length, SHA-1-validates a sample of pieces (at least one per file, plus ~2% of the rest). For a large library of many-file torrents the ">=1 piece per file" minimum alone forces thousands of piece reads on every launch, and Session::new_with_opts does not return until they finish. Add an opt-in SessionOptions::trust_fastresume (default false, so existing behavior is unchanged). When set and validate_fastresume loads a bitfield whose length matches piece_bitfield_bytes(), it trusts the bitfield and returns immediately, skipping the sampling loop. Safety is preserved: a previously-errored torrent still has its bitfield cleared and gets a full recheck; a missing or wrong-length bitfield still falls through to the full initial_check; on-disk corruption is still caught per-piece while seeding/downloading. This matches clients like qBittorrent/Transmission, which trust fast-resume data on start. --- crates/librqbit/src/session.rs | 9 +++++++ .../src/torrent_state/initializing.rs | 24 ++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/crates/librqbit/src/session.rs b/crates/librqbit/src/session.rs index 75071b9c5..842570338 100644 --- a/crates/librqbit/src/session.rs +++ b/crates/librqbit/src/session.rs @@ -130,6 +130,7 @@ pub struct Session { peer_opts: PeerConnectionOptions, default_storage_factory: Option, persistence: Option>, + pub(crate) trust_fastresume: bool, trackers: HashSet, lsd: Option, @@ -430,6 +431,12 @@ pub struct SessionOptions { /// Enable fastresume, to restore state quickly after restart. pub fastresume: bool, + /// With `fastresume`, trust a valid persisted piece bitfield and skip the + /// on-startup sampling recheck (which otherwise reads and hashes at least + /// one piece per file for every torrent on every launch). A torrent that + /// previously errored is still fully rechecked. + pub trust_fastresume: bool, + /// Turn on to dump session contents into a file periodically, so that on next start /// all remembered torrents will continue where they left off. pub persistence: Option, @@ -488,6 +495,7 @@ impl Default for SessionOptions { bind_device_name: None, disable_trackers: false, fastresume: false, + trust_fastresume: false, persistence: None, peer_id: None, listen: None, @@ -779,6 +787,7 @@ impl Session { let session = Arc::new(Self { persistence, + trust_fastresume: opts.trust_fastresume, bitv_factory, peer_id, dht, diff --git a/crates/librqbit/src/torrent_state/initializing.rs b/crates/librqbit/src/torrent_state/initializing.rs index 099e8d2f8..79350d977 100644 --- a/crates/librqbit/src/torrent_state/initializing.rs +++ b/crates/librqbit/src/torrent_state/initializing.rs @@ -86,6 +86,7 @@ impl TorrentStateInitializing { &self, bitv_factory: &dyn BitVFactory, have_pieces: Option>, + trust: bool, ) -> Option> { let hp = have_pieces?; let actual = hp.as_bytes().len(); @@ -99,6 +100,15 @@ impl TorrentStateInitializing { return None; } + // With `trust_fastresume`, a length-valid persisted bitfield is taken at + // face value: skip the sampling recheck below (which reads and hashes at + // least one piece per file). On-disk corruption is still caught later at + // the per-piece level while seeding/downloading. + if trust { + trace!("trust_fastresume set, trusting the persisted bitfield without rechecking"); + return Some(hp); + } + let is_broken = self .shared .spawner @@ -187,13 +197,9 @@ impl TorrentStateInitializing { pub async fn check(&self) -> anyhow::Result { let id: TorrentIdOrHash = self.shared.info_hash.into(); - let bitv_factory = self - .shared - .session - .upgrade() - .context("session is dead")? - .bitv_factory - .clone(); + let session = self.shared.session.upgrade().context("session is dead")?; + let bitv_factory = session.bitv_factory.clone(); + let trust_fastresume = session.trust_fastresume; let have_pieces = if self.previously_errored { if let Err(e) = bitv_factory.clear(id).await { warn!(id=?self.shared.id, info_hash = ?self.shared.info_hash, error=?e, "error clearing bitfield"); @@ -206,7 +212,9 @@ impl TorrentStateInitializing { .context("error loading have_pieces")? }; - let have_pieces = self.validate_fastresume(&*bitv_factory, have_pieces).await; + let have_pieces = self + .validate_fastresume(&*bitv_factory, have_pieces, trust_fastresume) + .await; let have_pieces = match have_pieces { Some(h) => h,