Skip to content

feat: add peer and tracker management - #258

Open
artrixdotdev wants to merge 2 commits into
mainfrom
feat/add-peer-tracker-management-api
Open

feat: add peer and tracker management#258
artrixdotdev wants to merge 2 commits into
mainfrom
feat/add-peer-tracker-management-api

Conversation

@artrixdotdev

@artrixdotdev artrixdotdev commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • expose typed Torrent APIs to add and disconnect peers, add and remove trackers, and queue per-tracker or torrent-wide reannounces
  • keep Torrent as the sole command surface while peer and tracker handles provide stable identities and live status/event access
  • return typed duplicate, missing, unsupported-protocol, and actor-delivery errors instead of silently dropping frontend operations
  • cover peer and tracker management through deterministic local peer and HTTP tracker fixtures
  • use a portable CI build target so shared Rust cache artifacts work across GitHub runner CPUs

API boundary

Peer connection succeeds after the handshake and swarm registration boundary. Tracker registration and announce commands follow the existing actor model: command delivery is guaranteed, while tracker initialization and network responses remain asynchronous and observable through TrackerHandle views and listeners. Tracker removal immediately updates the authoritative registry and live terminal state; its stopped announce is best-effort.

The implementation reuses the existing peer removal path, tracker actor construction, live scope registry, and tracker status/event projection. Inline comments document only the non-obvious handshake, shared tracker-registration, and shutdown flows.

Closes #226

Validation

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo clippy -p libtortillas --no-default-features --all-targets -- -D warnings
  • cargo nextest run --workspace --all-features — 194 passed, 9 skipped
  • cargo nextest run --workspace --all-features --run-ignored only — 9 passed
  • cargo test -p libtortillas --all-features --doc — 28 passed, 6 ignored
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps
  • GitHub Actions build_and_test and lint

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Torrent live APIs now support manual peer management and tracker lifecycle controls. Actor message flows return typed results, tracker registration is supervised and protocol-checked, live hub lookups resolve tracker relationships, and integration tests cover peer and tracker operations.

Changes

Peer and tracker lifecycle

Layer / File(s) Summary
Lifecycle contracts and lookup wiring
crates/libtortillas/src/errors.rs, crates/libtortillas/src/torrent/mod.rs, crates/libtortillas/src/live/hub.rs, crates/libtortillas/src/facade.rs
Typed peer/tracker errors, feature-gated result aliases, tracker lookup helpers, and the public Tracker facade export were added.
Peer connection and disconnection flow
crates/libtortillas/src/torrent/swarm.rs, crates/libtortillas/src/torrent/messages.rs, crates/libtortillas/src/torrent/handle.rs, crates/libtortillas/src/torrent/actor.rs
Peer handshakes run asynchronously, manual additions return delegated results, duplicate or invalid peers return errors, and peers can be disconnected explicitly.
Tracker registration and control flow
crates/libtortillas/src/torrent/swarm.rs, crates/libtortillas/src/torrent/actor.rs, crates/libtortillas/src/torrent/messages.rs, crates/libtortillas/src/torrent/handle.rs
Tracker registration rejects unsupported protocols, creates supervised tracker actors, and exposes add, remove, reannounce, and force-reannounce operations.
Runtime initialization and lifecycle validation
crates/libtortillas/src/torrent/actor.rs, crates/libtortillas/src/torrent/piece_flow.rs, crates/libtortillas/src/lib.rs, crates/libtortillas/src/torrent/handle.rs
Actor fixtures initialize primary_addr, startup skips tracker registration failures, local peer connections remain active, and live peer/tracker lifecycle tests were added.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels: enhancement, med prio, refactor

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR adds public peer/tracker APIs, typed errors, and deterministic tests that match issue #226.
Out of Scope Changes check ✅ Passed The changes stay focused on peer/tracker management, reannounce flows, and required supporting tests/helpers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: new peer and tracker management APIs.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/add-peer-tracker-management-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added enhancement New feature or request med prio Medium Priority refactor Neither fixes a bug nor adds a feature labels Jul 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/libtortillas/src/torrent/messages.rs (1)

249-271: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

tell(...).await per tracker can stall the torrent actor.

force_reannounce runs inside the torrent actor's handler and awaits each tracker's bounded mailbox sequentially, so one saturated tracker mailbox blocks the whole torrent loop (and every other command behind it). The sibling broadcast paths already use for_each_concurrent or try_send for this reason. Consider try_send (counting refusals as failures) or a concurrent send bounded by tracker_broadcast_concurrency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libtortillas/src/torrent/messages.rs` around lines 249 - 271, The
force_reannounce handler must not await each tracker mailbox sequentially.
Update force_reannounce to use the existing non-blocking or bounded-concurrency
tracker broadcast pattern, such as try_send or tracker_broadcast_concurrency,
while preserving queued-counting and first-error reporting for failed sends.
crates/libtortillas/src/torrent/handle.rs (1)

271-279: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unbounded listener.recv() in tests.

Both new tests await listener.recv() without a timeout(...) in a couple of places (here and in the tracker test's first recvs they do use timeouts). An unmet event hangs the test until the harness-wide limit instead of failing fast. Wrapping this one in timeout keeps the suite diagnosable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libtortillas/src/torrent/handle.rs` around lines 271 - 279, Wrap the
await on listener.recv() in the disconnect-peer test with the existing timeout
mechanism, preserving the current Disconnected event assertion while making a
missing event fail promptly; apply the same bounded receive to the other new
unbounded listener.recv() calls in the tracker test.
crates/libtortillas/src/lib.rs (1)

570-575: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why the fixture never returns.

std::future::pending() deliberately keeps the connection open so the peer under test doesn't observe an immediate EOF after the scripted messages; cleanup relies on LocalPeer::drop aborting the accept task and, with it, the JoinSet. That contract is non-obvious — worth a one-line comment so a future reader doesn't "fix" it back to Ok(()).

📝 Suggested comment
+      // Keep the connection open so the client does not see an EOF right after
+      // the scripted messages. `LocalPeer::drop` aborts this task.
       std::future::pending().await
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libtortillas/src/lib.rs` around lines 570 - 575, Add a concise comment
immediately before std::future::pending().await in the fixture’s message-stream
handling to document that it intentionally keeps the connection open, preventing
EOF until LocalPeer::drop aborts the accept task and its JoinSet. Do not change
the existing control flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/libtortillas/src/torrent/swarm.rs`:
- Around line 46-91: Bound the spawned connection future in the add-peer flow
around PeerStream::connect, send_handshake, and recv_handshake_message using the
configured peer-handshake timeout. Convert expiration into the established typed
TorrentError timeout variant so the awaited add_peer request and discovery task
terminate predictably, while preserving existing handshake and registration
errors.

---

Nitpick comments:
In `@crates/libtortillas/src/lib.rs`:
- Around line 570-575: Add a concise comment immediately before
std::future::pending().await in the fixture’s message-stream handling to
document that it intentionally keeps the connection open, preventing EOF until
LocalPeer::drop aborts the accept task and its JoinSet. Do not change the
existing control flow.

In `@crates/libtortillas/src/torrent/handle.rs`:
- Around line 271-279: Wrap the await on listener.recv() in the disconnect-peer
test with the existing timeout mechanism, preserving the current Disconnected
event assertion while making a missing event fail promptly; apply the same
bounded receive to the other new unbounded listener.recv() calls in the tracker
test.

In `@crates/libtortillas/src/torrent/messages.rs`:
- Around line 249-271: The force_reannounce handler must not await each tracker
mailbox sequentially. Update force_reannounce to use the existing non-blocking
or bounded-concurrency tracker broadcast pattern, such as try_send or
tracker_broadcast_concurrency, while preserving queued-counting and first-error
reporting for failed sends.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 688a3e94-4158-421e-972b-63eb0954e6fe

📥 Commits

Reviewing files that changed from the base of the PR and between ca568eb and d747efc.

📒 Files selected for processing (10)
  • crates/libtortillas/src/errors.rs
  • crates/libtortillas/src/facade.rs
  • crates/libtortillas/src/lib.rs
  • crates/libtortillas/src/live/hub.rs
  • crates/libtortillas/src/torrent/actor.rs
  • crates/libtortillas/src/torrent/handle.rs
  • crates/libtortillas/src/torrent/messages.rs
  • crates/libtortillas/src/torrent/mod.rs
  • crates/libtortillas/src/torrent/piece_flow.rs
  • crates/libtortillas/src/torrent/swarm.rs

Comment on lines 46 to +91
tokio::spawn(async move {
let mut id = peer.id;
let (stream, reserved) = match stream {
Some((mut stream, reserved)) => {
let handshake = Handshake::new(info_hash, our_id);
if let Err(err) = stream.send(PeerMessages::Handshake(handshake)).await {
debug!(error = %err, peer_addr = %peer.socket_addr(), "Failed to send handshake to peer");
return;
let connection = async {
let (stream, reserved) = match stream {
Some((mut stream, reserved)) => {
let handshake = Handshake::new(info_hash, our_id);
stream.send(PeerMessages::Handshake(handshake)).await?;
(stream, reserved)
}
(stream, reserved)
}
None => {
let stream = PeerStream::connect(peer.socket_addr(), Some(utp_server)).await;
match stream {
Ok(mut stream) => match stream.send_handshake(our_id, info_hash).await {
Ok(_) => match stream.recv_handshake_message().await {
Ok(handshake) => {
if let Err(err) =
validate_handshake(&handshake, peer.socket_addr(), info_hash)
{
trace!(error = %err, peer_addr = %peer.socket_addr(), "Failed to validate peer handshake; exiting");
return;
}
id = Some(handshake.peer_id);
(stream, handshake.reserved)
}
Err(err) => {
trace!(error = %err, peer_addr = %peer.socket_addr(), "Failed to receive handshake from peer; exiting");
return;
}
},
Err(err) => {
trace!(error = %err, peer_addr = %peer.socket_addr(), "Failed to send handshake to peer; exiting");
return;
}
},
Err(err) => {
trace!(error = %err, peer_addr = %peer.socket_addr(), "Failed to connect to peer; exiting");
return;
}
None => {
let mut stream =
PeerStream::connect(peer.socket_addr(), Some(utp_server)).await?;
stream.send_handshake(our_id, info_hash).await?;
let handshake = stream.recv_handshake_message().await?;
validate_handshake(&handshake, peer.socket_addr(), info_hash)?;
id = Some(handshake.peer_id);
(stream, handshake.reserved)
}
};

let id = id.ok_or_else(|| TorrentError::InvalidOperation {
operation: "add peer",
reason: "peer connection completed without a peer id".to_string(),
})?;

if id == our_id {
return Err(TorrentError::InvalidOperation {
operation: "add peer",
reason: "a torrent cannot connect to its own peer id".to_string(),
});
}
};

let Some(id) = id else {
trace!(peer_addr = %peer.socket_addr(), "Peer connection completed without a peer id; exiting");
return;
};
peer.id = Some(id);

if id == our_id {
return;
// Registration is the boundary where the connection becomes part
// of the swarm, so a manual add does not succeed before this ask.
actor_ref
.ask(PeerConnected {
peer,
reserved,
stream,
})
.await
.map_err(|error| map_torrent_send_error("register connected peer", error))
}
.await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unbounded connect/handshake makes Torrent::add_peer hang indefinitely.

The spawned connection future has no timeout around PeerStream::connect, send_handshake, or recv_handshake_message. For discovery this only leaks a task, but the new public add_peer awaits this oneshot, so a black-holed address leaves the caller (and the TUI) blocked until the OS TCP timeout — or forever for a peer that accepts and never sends a handshake (which is exactly what the LocalPeer fixture now does after std::future::pending()). Consider bounding the connection future with a configured peer-handshake timeout and surfacing a typed timeout error.

🛡️ Sketch
-         let connection = async {
+         let connection = match tokio::time::timeout(handshake_timeout, async {
             ...
-         }
-         .await;
+         })
+         .await
+         {
+            Ok(result) => result,
+            Err(_) => Err(TorrentError::InvalidOperation {
+               operation: "add peer",
+               reason: "peer handshake timed out".to_string(),
+            }),
+         };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/libtortillas/src/torrent/swarm.rs` around lines 46 - 91, Bound the
spawned connection future in the add-peer flow around PeerStream::connect,
send_handshake, and recv_handshake_message using the configured peer-handshake
timeout. Convert expiration into the established typed TorrentError timeout
variant so the awaited add_peer request and discovery task terminate
predictably, while preserving existing handshake and registration errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request med prio Medium Priority refactor Neither fixes a bug nor adds a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add public peer and tracker management APIs

1 participant