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:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user