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:
@@ -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>,
|
||||
|
||||
@@ -248,7 +248,7 @@ pub enum ClientOperation {
|
||||
query: String,
|
||||
},
|
||||
/// A peer queued one of our shared files; `requester_key` is the registry
|
||||
/// key of the peer actor (may carry a `:direct` suffix).
|
||||
/// key of the peer actor — the peer's username.
|
||||
QueueUpload {
|
||||
requester_key: String,
|
||||
filename: String,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use super::uploads::downloader_of;
|
||||
use super::{
|
||||
Arc, BROKER_CONNECT_TIMEOUT, Client, ClientContext, ClientOperation,
|
||||
ConnectionType, Download, DownloadPeer, DownloadStatus, Peer, PeerMessage,
|
||||
@@ -75,6 +74,14 @@ impl Client {
|
||||
let _ = handle.stop();
|
||||
}
|
||||
}
|
||||
// Only an error is evidence the peer is gone. A clean
|
||||
// close — our idle reaper, or a remote client tidying
|
||||
// an idle socket while it waits in our queue — must
|
||||
// not throw away everything that peer has queued: the
|
||||
// connection comes back, the queue cannot. A departed
|
||||
// peer wedging uploads shut is still covered: its
|
||||
// erroring transfer fails within the socket deadlines
|
||||
// and the error branch here frees its slots.
|
||||
if let Some(error) = error {
|
||||
warn!(
|
||||
"[client] Peer {} disconnected with error: {:?}",
|
||||
@@ -85,12 +92,11 @@ impl Client {
|
||||
&username,
|
||||
None,
|
||||
);
|
||||
Self::release_upload_slots(
|
||||
&client_context,
|
||||
&username,
|
||||
);
|
||||
}
|
||||
// A peer that leaves must not keep occupying an
|
||||
// upload slot: with a slot cap, a couple of
|
||||
// departed peers would otherwise wedge uploads
|
||||
// shut for the rest of the session.
|
||||
Self::release_upload_slots(&client_context, &username);
|
||||
}
|
||||
ClientOperation::DownloadFromPeer(token, peer, allowed) => {
|
||||
let maybe_download = match client_context.read_safe() {
|
||||
@@ -440,10 +446,11 @@ impl Client {
|
||||
}
|
||||
}
|
||||
ClientOperation::PeerConnected(username) => {
|
||||
// An outbound control connection just handshook.
|
||||
// Flush any downloads that were queued for this
|
||||
// peer while we were still connecting. Collect
|
||||
// under a read guard, then act without it held.
|
||||
// A control connection just came up — one we dialled,
|
||||
// or an inbound one the listener registered. Flush any
|
||||
// downloads that were queued for this peer while it
|
||||
// was unreachable. Collect under a read guard, then
|
||||
// act without it held.
|
||||
let (registry, files): (
|
||||
Option<PeerRegistry>,
|
||||
Vec<String>,
|
||||
@@ -547,8 +554,6 @@ impl Client {
|
||||
filename,
|
||||
} => {
|
||||
// The peer served next may not be this one.
|
||||
let downloader =
|
||||
downloader_of(&requester_key).to_string();
|
||||
match client_context.write_safe() {
|
||||
Ok(mut ctx) => {
|
||||
let Some(file) = ctx.shares.get(&filename)
|
||||
@@ -563,7 +568,6 @@ impl Client {
|
||||
let real_path = file.real_path.clone();
|
||||
ctx.enqueue_upload(
|
||||
&requester_key,
|
||||
&downloader,
|
||||
&filename,
|
||||
real_path,
|
||||
size,
|
||||
@@ -580,12 +584,11 @@ impl Client {
|
||||
requester_key,
|
||||
filename,
|
||||
} => {
|
||||
let downloader = downloader_of(&requester_key);
|
||||
let (registry, place) = match client_context.read_safe()
|
||||
{
|
||||
Ok(ctx) => (
|
||||
ctx.peer_registry.clone(),
|
||||
ctx.place_in_queue(downloader, &filename),
|
||||
ctx.place_in_queue(&requester_key, &filename),
|
||||
),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
@@ -327,3 +327,71 @@ fn upload_speed_is_reported_only_while_running() {
|
||||
"unexpected rate {rate}"
|
||||
);
|
||||
}
|
||||
|
||||
// A clean disconnect — the idle reaper, or a remote client tidying an idle
|
||||
// socket while it waits in our queue — must keep the peer's queued uploads;
|
||||
// only an error disconnect is evidence the peer is gone.
|
||||
#[test]
|
||||
fn a_clean_disconnect_keeps_queued_uploads_an_error_drops_them() {
|
||||
let client = Client::new("test-user", "test-password");
|
||||
let (ops, ops_rx) = mpsc::channel();
|
||||
Client::listen_to_client_operations(
|
||||
ops_rx,
|
||||
client.context.clone(),
|
||||
"me".to_string(),
|
||||
);
|
||||
|
||||
client.context.write().unwrap().enqueue_upload(
|
||||
"amy",
|
||||
"@@share\\f.mp3",
|
||||
std::path::PathBuf::from("/tmp/f.mp3"),
|
||||
4096,
|
||||
);
|
||||
|
||||
ops.send(ClientOperation::PeerDisconnected(
|
||||
1,
|
||||
"amy".to_string(),
|
||||
None,
|
||||
))
|
||||
.unwrap();
|
||||
// The loop is serial, so once this fence op is visible the disconnect
|
||||
// before it has been handled.
|
||||
ops.send(ClientOperation::OwnPrivileges(7)).unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while client.own_privilege_seconds() != Some(7) {
|
||||
assert!(Instant::now() < deadline, "ops loop never caught up");
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
assert_eq!(
|
||||
client
|
||||
.context
|
||||
.read()
|
||||
.unwrap()
|
||||
.place_in_queue("amy", "@@share\\f.mp3"),
|
||||
Some(1),
|
||||
"a clean disconnect must keep the queue"
|
||||
);
|
||||
|
||||
ops.send(ClientOperation::PeerDisconnected(
|
||||
1,
|
||||
"amy".to_string(),
|
||||
Some(crate::error::SoulseekRs::NotConnected),
|
||||
))
|
||||
.unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
loop {
|
||||
let place = client
|
||||
.context
|
||||
.read()
|
||||
.unwrap()
|
||||
.place_in_queue("amy", "@@share\\f.mp3");
|
||||
if place.is_none() {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"an error disconnect must drop the queue"
|
||||
);
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,9 +31,7 @@ pub struct UploadOffer {
|
||||
/// A file a peer has asked us for and is waiting on.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct QueuedUpload {
|
||||
/// Registry key of the asking peer's actor, which may carry a `:direct`
|
||||
/// suffix — not the same string as `downloader`.
|
||||
pub requester_key: String,
|
||||
/// The asking peer's username — also the registry key of its actor.
|
||||
pub downloader: String,
|
||||
pub virtual_path: String,
|
||||
pub real_path: PathBuf,
|
||||
@@ -98,7 +96,6 @@ impl ClientContext {
|
||||
/// for a file we do not have never reaches the queue.
|
||||
pub fn enqueue_upload(
|
||||
&mut self,
|
||||
requester_key: &str,
|
||||
downloader: &str,
|
||||
filename: &str,
|
||||
real_path: std::path::PathBuf,
|
||||
@@ -106,7 +103,6 @@ impl ClientContext {
|
||||
) {
|
||||
self.upload_seq += 1;
|
||||
self.upload_queue.push(QueuedUpload {
|
||||
requester_key: requester_key.to_string(),
|
||||
downloader: downloader.to_string(),
|
||||
virtual_path: filename.to_string(),
|
||||
real_path,
|
||||
@@ -134,7 +130,7 @@ impl ClientContext {
|
||||
let job = self.upload_queue.remove(index);
|
||||
let token = next_token();
|
||||
offers.push(UploadOffer {
|
||||
requester_key: job.requester_key,
|
||||
requester_key: job.downloader.clone(),
|
||||
token,
|
||||
virtual_path: job.virtual_path.clone(),
|
||||
size: job.size,
|
||||
@@ -259,7 +255,6 @@ mod tests {
|
||||
|
||||
fn queued(user: &str, privileged: bool, seq: u64) -> QueuedUpload {
|
||||
QueuedUpload {
|
||||
requester_key: user.to_string(),
|
||||
downloader: user.to_string(),
|
||||
virtual_path: format!("@@share\\{user}.mp3"),
|
||||
real_path: PathBuf::from("/tmp/x.mp3"),
|
||||
@@ -365,7 +360,7 @@ mod context_tests {
|
||||
}
|
||||
|
||||
fn ask(ctx: &mut ClientContext, user: &str, file: &str) {
|
||||
ctx.enqueue_upload(user, user, file, PathBuf::from("/tmp/x"), 4096);
|
||||
ctx.enqueue_upload(user, file, PathBuf::from("/tmp/x"), 4096);
|
||||
}
|
||||
|
||||
/// Who the pump offered to, by the order it offered them.
|
||||
|
||||
@@ -5,14 +5,6 @@ use super::{
|
||||
use crate::types::UploadStatus;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64};
|
||||
|
||||
/// The peer behind a registry key, which may carry a `:direct` suffix the
|
||||
/// server never sees.
|
||||
pub fn downloader_of(requester_key: &str) -> &str {
|
||||
requester_key
|
||||
.strip_suffix(":direct")
|
||||
.unwrap_or(requester_key)
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Whether the server listed `username` as privileged. Privileged peers are
|
||||
/// served before everyone else.
|
||||
|
||||
Reference in New Issue
Block a user