Fix/issue 817 idle timeout log level (#824)

* fix(transport): downgrade idle timeout log from error to debug

Idle keep-alive timeout is normal zombie-session cleanup, not a transport failure.

Route it through a dedicated WorkerQuitReason::IdleTimeout variant.

Log it at debug level instead of treating it as a fatal error.

Remove the unused LocalSessionWorkerError::KeepAliveTimeout variant.

Closes #817

* fix(session): tolerate dead worker in close_session

Swallow SessionServiceTerminated in close_session when the worker has already exited.
This prevents a spurious ERROR log during the post-exit cleanup path in
spawn_session_worker.

* fix(transport): address PR review feedback

- deprecate KeepAliveTimeout
- harden tests
This commit is contained in:
lutz-grex 2026-05-05 02:14:06 +02:00 • committed by GitHub
commit 2f8d3b7355
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 269 additions and 6 deletions

View file

@ -66,9 +66,16 @@ impl SessionManager for LocalSessionManager {
Ok(response)
}
async fn close_session(&self, id: &SessionId) -> Result<(), Self::Error> {
let mut sessions = self.sessions.write().await;
if let Some(handle) = sessions.remove(id) {
handle.close().await?;
let handle = {
let mut sessions = self.sessions.write().await;
sessions.remove(id)
};
if let Some(handle) = handle {
match handle.close().await {
// Worker already exited — nothing left to clean up.
Ok(()) | Err(SessionError::SessionServiceTerminated) => {}
Err(e) => return Err(e.into()),
}
}
Ok(())
}
@ -928,6 +935,7 @@ pub enum LocalSessionWorkerError {
FailToSendInitializeRequest(SessionError),
#[error("fail to handle message: {0}")]
FailToHandleMessage(SessionError),
#[deprecated(note = "idle timeout now surfaces as WorkerQuitReason::IdleTimeout")]
#[error("keep alive timeout after {}ms", _0.as_millis())]
KeepAliveTimeout(Duration),
#[error("init timeout after {}ms", _0.as_millis())]
@ -1021,7 +1029,7 @@ impl Worker for LocalSessionWorker {
return Err(WorkerQuitReason::Cancelled)
}
_ = keep_alive_timeout => {
return Err(WorkerQuitReason::fatal(LocalSessionWorkerError::KeepAliveTimeout(keep_alive), "poll next session event"))
return Err(WorkerQuitReason::IdleTimeout(keep_alive))
}
};
match event {

View file

@ -1,4 +1,4 @@
use std::borrow::Cow;
use std::{borrow::Cow, time::Duration};
use tokio_util::sync::CancellationToken;
use tracing::{Instrument, Level};
@ -22,6 +22,8 @@ pub enum WorkerQuitReason<E> {
TransportClosed,
#[error("Handler terminated")]
HandlerTerminated,
#[error("Worker idle timeout after {}ms", _0.as_millis())]
IdleTimeout(Duration),
}
impl<E: std::error::Error + Send + 'static> WorkerQuitReason<E> {
@ -122,7 +124,8 @@ impl<W: Worker> WorkerTransport<W> {
.inspect_err(|e| match e {
WorkerQuitReason::Cancelled
| WorkerQuitReason::TransportClosed
| WorkerQuitReason::HandlerTerminated => {
| WorkerQuitReason::HandlerTerminated
| WorkerQuitReason::IdleTimeout(_) => {
tracing::debug!("worker quit with reason: {:?}", e);
}
WorkerQuitReason::Join(e) => {