Compare commits

..

2 Commits

Author SHA1 Message Date
spinline
d15392e148 fix: add timeout to SCGI requests to prevent background loop hang
All checks were successful
Build MIPS Binary / build (push) Successful in 4m22s
2026-02-09 00:28:07 +03:00
spinline
f3121898e2 fix: wake up background polling loop immediately when a client connects
All checks were successful
Build MIPS Binary / build (push) Successful in 4m22s
2026-02-09 00:22:28 +03:00
3 changed files with 35 additions and 10 deletions

View File

@@ -45,6 +45,7 @@ pub struct AppState {
pub db: db::Db, pub db: db::Db,
#[cfg(feature = "push-notifications")] #[cfg(feature = "push-notifications")]
pub push_store: push::PushSubscriptionStore, pub push_store: push::PushSubscriptionStore,
pub notify_poll: Arc<tokio::sync::Notify>,
} }
async fn auth_middleware( async fn auth_middleware(
@@ -336,6 +337,8 @@ async fn main() {
#[cfg(not(feature = "push-notifications"))] #[cfg(not(feature = "push-notifications"))]
let push_store = (); let push_store = ();
let notify_poll = Arc::new(tokio::sync::Notify::new());
let app_state = AppState { let app_state = AppState {
tx: tx.clone(), tx: tx.clone(),
event_bus: event_bus.clone(), event_bus: event_bus.clone(),
@@ -343,6 +346,7 @@ async fn main() {
db: db.clone(), db: db.clone(),
#[cfg(feature = "push-notifications")] #[cfg(feature = "push-notifications")]
push_store, push_store,
notify_poll: notify_poll.clone(),
}; };
// Spawn background task to poll rTorrent // Spawn background task to poll rTorrent
@@ -351,6 +355,7 @@ async fn main() {
let socket_path = args.socket.clone(); // Clone for background task let socket_path = args.socket.clone(); // Clone for background task
#[cfg(feature = "push-notifications")] #[cfg(feature = "push-notifications")]
let push_store_clone = app_state.push_store.clone(); let push_store_clone = app_state.push_store.clone();
let notify_poll_clone = notify_poll.clone();
tokio::spawn(async move { tokio::spawn(async move {
let client = xmlrpc::RtorrentClient::new(&socket_path); let client = xmlrpc::RtorrentClient::new(&socket_path);
@@ -438,8 +443,13 @@ async fn main() {
previous_torrents = new_torrents; previous_torrents = new_torrents;
// Success case: sleep for the determined interval // Success case: wait for the determined interval OR a wakeup notification
tokio::time::sleep(loop_interval).await; tokio::select! {
_ = tokio::time::sleep(loop_interval) => {},
_ = notify_poll_clone.notified() => {
tracing::debug!("Background loop awakened by new client connection");
}
}
} }
Err(e) => { Err(e) => {
tracing::error!("Error fetching torrents in background: {}", e); tracing::error!("Error fetching torrents in background: {}", e);

View File

@@ -11,6 +11,8 @@ pub enum ScgiError {
#[allow(dead_code)] #[allow(dead_code)]
#[error("Protocol Error: {0}")] #[error("Protocol Error: {0}")]
Protocol(String), Protocol(String),
#[error("Timeout: SCGI request took too long")]
Timeout,
} }
pub struct ScgiRequest { pub struct ScgiRequest {
@@ -78,20 +80,30 @@ impl ScgiRequest {
} }
pub async fn send_request(socket_path: &str, request: ScgiRequest) -> Result<Bytes, ScgiError> { pub async fn send_request(socket_path: &str, request: ScgiRequest) -> Result<Bytes, ScgiError> {
let mut stream = UnixStream::connect(socket_path).await?; let perform_request = async {
let data = request.encode(); let mut stream = UnixStream::connect(socket_path).await?;
stream.write_all(&data).await?; let data = request.encode();
stream.write_all(&data).await?;
let mut response = Vec::new(); let mut response = Vec::new();
stream.read_to_end(&mut response).await?; stream.read_to_end(&mut response).await?;
Ok::<Vec<u8>, std::io::Error>(response)
};
let response = tokio::time::timeout(std::time::Duration::from_secs(10), perform_request)
.await
.map_err(|_| ScgiError::Timeout)??;
let double_newline = b"\r\n\r\n"; let double_newline = b"\r\n\r\n";
if let Some(pos) = response let mut response_vec = response;
if let Some(pos) = response_vec
.windows(double_newline.len()) .windows(double_newline.len())
.position(|window| window == double_newline) .position(|window| window == double_newline)
{ {
Ok(Bytes::from(response.split_off(pos + double_newline.len()))) Ok(Bytes::from(
response_vec.split_off(pos + double_newline.len()),
))
} else { } else {
Ok(Bytes::from(response)) Ok(Bytes::from(response_vec))
} }
} }

View File

@@ -195,6 +195,9 @@ pub async fn fetch_global_stats(client: &RtorrentClient) -> Result<GlobalStats,
pub async fn sse_handler( pub async fn sse_handler(
State(state): State<AppState>, State(state): State<AppState>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> { ) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
// Notify background worker to wake up and poll immediately
state.notify_poll.notify_one();
// Get initial value synchronously (from the watch channel's current state) // Get initial value synchronously (from the watch channel's current state)
let initial_rx = state.tx.subscribe(); let initial_rx = state.tx.subscribe();
let initial_torrents = initial_rx.borrow().clone(); let initial_torrents = initial_rx.borrow().clone();