From cb9cbacdd3bcfa98c2b9527be97a25a839d7a951 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 23:33:21 +0200 Subject: [PATCH] fix(server): Do not panic when a channel read fails A read timeout or an abrupt client hang-up unwound the channel thread, which flooded the logs with "closing channel" panics and skipped the connected clients counter decrement, leaking a client slot each time. Fixes #256 --- server/src/channel/handle.rs | 6 ++-- server/tests/channel_read_timeout.rs | 50 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 server/tests/channel_read_timeout.rs diff --git a/server/src/channel/handle.rs b/server/src/channel/handle.rs index 74a5f90..05e136c 100644 --- a/server/src/channel/handle.rs +++ b/server/src/channel/handle.rs @@ -216,9 +216,11 @@ impl ChannelHandle { } } Err(err) => { - tracing::error!("closing channel thread with traceback: {}", err); + // NOTE: Panicking here would unwind past the connected clients + // counter decrement in `client()`, leaking a client slot. + tracing::debug!("closing channel thread: {}", err); - panic!("closing channel"); + break 'handler; } } } diff --git a/server/tests/channel_read_timeout.rs b/server/tests/channel_read_timeout.rs new file mode 100644 index 0000000..8a28460 --- /dev/null +++ b/server/tests/channel_read_timeout.rs @@ -0,0 +1,50 @@ +// Sonic +// +// Fast, lightweight and schema-less search backend +// Copyright: 2026, DualFroz +// License: Mozilla Public License v2.0 (MPL v2.0) + +mod common; + +use std::io::{BufRead, BufReader, Write}; +use std::net::TcpStream; +use std::time::Duration; + +use crate::common::prelude::*; + +const TCP_TIMEOUT: u64 = 1; + +#[test] +fn read_timeout_releases_client_slot() { + let ctx = + start_empty(|command| command.env("SONIC_CHANNEL__TCP_TIMEOUT", TCP_TIMEOUT.to_string())); + + { + let mut stream = TcpStream::connect(ctx.addr).unwrap(); + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut line = String::new(); + + reader.read_line(&mut line).unwrap(); + + stream + .write_all(format!("START search {SONIC_PASSWORD}\n").as_bytes()) + .unwrap(); + + line.clear(); + reader.read_line(&mut line).unwrap(); + assert!(line.starts_with("STARTED"), "unexpected greeting: {line:?}"); + + // Stay idle until the server hits its read timeout, then hang up. + std::thread::sleep(Duration::from_secs(TCP_TIMEOUT + 1)); + } + + std::thread::sleep(Duration::from_millis(200)); + + let multiplexer = SonicMultiplexer::new().unwrap(); + let control = + SonicChannelControlBlocking::connect(ctx.addr, SONIC_PASSWORD, &multiplexer).unwrap(); + + let stats = control.info().unwrap(); + + assert_eq!(stats.clients_connected, 1); +}