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
+33 -1
View File
@@ -2,7 +2,7 @@ use super::{
Arc, Client, ClientContext, ClientOperation, ConnectionType, DownloadPeer,
DownloadStatus, Listen, Peer, PeerRegistry, Receiver, Result, RwLock,
RwLockExt, Sender, ServerActor, ServerMessage, Shares, SoulseekRs,
TcpStream, error, info, mpsc, thread, trace,
TcpStream, error, info, mpsc, thread, trace, warn,
};
/// Ceiling on the wait for a login verdict. Generous enough for a slow server
@@ -10,8 +10,40 @@ use super::{
const LOGIN_RESPONSE_TIMEOUT: std::time::Duration =
std::time::Duration::from_secs(45);
/// Soft open-file limit below which a session should expect "too many open
/// files": every peer holds a socket, and one busy search talks to hundreds
/// of them.
const COMFORTABLE_OPEN_FILES: u64 = 1024;
/// Say up front when the process cannot hold a busy session's sockets, so an
/// operator reads a warning with a number in it instead of an EMFILE storm an
/// hour later. The embedding process owns its rlimits; the library only looks.
#[cfg(unix)]
fn warn_if_open_file_limit_is_low() {
// SAFETY: getrlimit only fills the struct it is handed.
let limit = unsafe {
let mut limit = std::mem::zeroed::<libc::rlimit>();
if libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut limit) != 0 {
return;
}
limit.rlim_cur
};
if limit < COMFORTABLE_OPEN_FILES {
warn!(
"only {limit} open files allowed; a busy session wants \
{COMFORTABLE_OPEN_FILES} or more — expect \"too many open \
files\" under load"
);
}
}
#[cfg(not(unix))]
fn warn_if_open_file_limit_is_low() {}
impl Client {
pub fn connect(&mut self) -> Result<()> {
warn_if_open_file_limit_is_low();
let (sender, message_reader): (
Sender<ClientOperation>,
Receiver<ClientOperation>,