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
+3 -8
View File
@@ -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.