fix: survive busy sessions without exhausting file descriptors

The peer listener died with "Too many open files (os error 24)" on any
long-lived non-daemon session: only the daemon raised RLIMIT_NOFILE, and
peer sockets were unbounded — no idle reaping, no deadlines on accepted
transfer streams, and one peer could hold two connections under split
registry identities.

- raise the open-file limit at the top of run(), for every entry point,
  and warn from Client::connect when the soft limit is below 1024
- reap peer control connections idle for five minutes; a clean
  disconnect no longer discards the peer's queued uploads
- give listener-supplied download streams and upload serving the same
  read/write deadlines dialled sockets already had
- register inbound peers under their plain username so the registry
  holds one connection per peer, and announce them like pierced ones
- hold the registry lock across actor spawn + insert and surface
  thread-spawn failure, so an instantly-dead peer cannot leave a
  zombie entry
This commit is contained in:
Michel de Graaf
2026-08-09 22:34:36 +02:00
parent d11326f149
commit 0bbb559f68
17 changed files with 442 additions and 132 deletions
+35 -3
View File
@@ -231,6 +231,19 @@ impl DownloadPeer {
)
.map_err(DownloadError::ConnectionFailed)?;
Self::apply_transfer_deadlines(&stream)?;
Ok(stream)
}
/// Bound every stall on a transfer socket. This must cover streams the
/// listener accepted as well as ones we dialled: an accepted F connection
/// used to arrive here with no read timeout at all, so a peer that sent a
/// token and went silent parked the transfer thread — holding the socket
/// and the open `.part` file — for the rest of the session.
fn apply_transfer_deadlines(
stream: &TcpStream,
) -> Result<(), DownloadError> {
stream
.set_read_timeout(Some(Duration::from_secs(30)))
.map_err(DownloadError::ConnectionFailed)?;
@@ -240,8 +253,7 @@ impl DownloadPeer {
stream
.set_nodelay(true)
.map_err(DownloadError::ConnectionFailed)?;
Ok(stream)
Ok(())
}
fn perform_handshake(
@@ -481,7 +493,10 @@ impl DownloadPeer {
}
let mut stream = match stream {
Some(s) => s,
Some(s) => {
Self::apply_transfer_deadlines(&s)?;
s
}
None => self.establish_connection()?,
};
@@ -622,6 +637,23 @@ mod tests {
let _ = std::fs::remove_dir_all(dir);
}
// Streams the listener accepted arrive with no deadlines; without these a
// peer that sends a token and goes silent parks the transfer thread —
// socket and open .part file included — for the rest of the session.
#[test]
fn supplied_streams_get_transfer_deadlines() {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let stream =
std::net::TcpStream::connect(listener.local_addr().unwrap())
.unwrap();
DownloadPeer::apply_transfer_deadlines(&stream).unwrap();
let secs = std::time::Duration::from_secs;
assert_eq!(stream.read_timeout().unwrap(), Some(secs(30)));
assert_eq!(stream.write_timeout().unwrap(), Some(secs(5)));
}
#[test]
fn test_establish_connection_invalid_address() {
let download_peer = DownloadPeer::new(
+13 -2
View File
@@ -178,7 +178,7 @@ fn handle_file_connection(
let failure_token = download.as_ref().map(|d| d.token);
let download_peer = DownloadPeer::new(
format!("{}:direct", peer.username),
peer.username.clone(),
peer.host.clone(),
peer.port,
token,
@@ -325,8 +325,13 @@ fn handle_incoming_connection(
init_data.username, init_data.connection_type, init_data.token
);
// Register under the plain username, the same key outbound connections
// use. A suffixed key ("user:direct") put the same peer in the registry
// twice — two sockets, two threads — and made every "already connected?"
// check miss, so an inbound searcher still got a second, outbound
// connection dialled at them.
let peer = Peer::new(
format!("{}:direct", init_data.username),
init_data.username.clone(),
init_data.connection_type.clone(),
peer_ip.clone(),
peer_port.into(),
@@ -339,6 +344,12 @@ fn handle_incoming_connection(
match init_data.connection_type {
ConnectionType::P => {
handle_peer_connection(peer, stream, reader, &context);
// Inbound peers don't self-announce, so nudge the client to flush
// anything queued for this now-connected peer — same as the
// PierceFirewall path.
let _ = context
.client_sender
.send(ClientOperation::PeerConnected(init_data.username));
}
ConnectionType::F => handle_file_connection(
+11
View File
@@ -42,6 +42,17 @@ pub fn serve_file(
})?;
let mut stream =
TcpStream::connect_timeout(&socket, Duration::from_secs(20))?;
// A downloader that connects and then goes silent must cost seconds, not
// the rest of the session: without deadlines a wedged `read_exact` (or a
// zero-window `write_all`) holds this thread, the socket, the open file,
// and — because the upload stays InProgress — an upload slot, forever.
// Thirty seconds of total silence is a dead transfer, not a slow one: even
// a slow reader drains some buffer within that. The write deadline is
// deliberately looser than the download side's 5s: bulk data flows this
// way, and a congested but living peer may hold the send buffer full far
// longer than a peer draining our tiny control frames ever would.
stream.set_read_timeout(Some(Duration::from_secs(30)))?;
stream.set_write_timeout(Some(Duration::from_secs(30)))?;
stream.set_nodelay(true).ok();
// PeerInit(F) + the 4-byte token in a single write so they coalesce.