fix(client): bound the threads a peer flood can create

One popular search produced a ConnectToPeer for every responder, and
each one cost a 2 MiB wrapper thread, an unbounded 256 KiB actor, a
listener thread per inbound socket, and on failure a 2 MiB thread that
slept for 20 seconds. A container with a pids limit died on one album
search (issues #32, #33).

Four bounds, no behavior change for a quiet session:
- P-type registrations run inline on the ops loop; only F transfers
  keep a worker thread. The dial already ran on the actor thread.
- The broker timeout is a deadline swept by the ops loop instead of a
  sleeping thread per failed dial.
- The registry caps live actors (default 512, Client::set_max_peers).
  At capacity the oldest peer outside the caller's protected set
  (live downloads, uploads, pending serves, queued messages, browses
  in flight) is evicted; if none qualify the registration is refused
  before any thread is spawned.
- Concurrent listener handshakes are bounded by a 128-permit
  semaphore released once the peer-init arrives, so slow handshakes
  cannot multiply threads while transfers stay unthrottled.

A replayed TransferResponse for an InProgress or Completed token is
dropped instead of starting a second transfer thread over the same
.part file.

Fixes #32
Fixes #33
This commit is contained in:
Michel de Graaf
2026-08-13 13:46:18 +02:00
parent fd1d168ca6
commit 9fe1c9984c
9 changed files with 458 additions and 60 deletions
+2 -1
View File
@@ -177,7 +177,8 @@ impl Client {
let request =
crate::message::server::MessageFactory::build_get_share_file_list();
let (connected, registry) = {
let ctx = self.context.read_safe()?;
let mut ctx = self.context.write_safe()?;
ctx.mark_browse_pending(username);
(
ctx.peer_registry
.as_ref()