From 097b32e87f66befa3b51e7b85c2d9507e8bf271d Mon Sep 17 00:00:00 2001 From: bugrax Date: Tue, 9 Jun 2026 23:42:45 +0300 Subject: [PATCH 1/4] Throttle repeated *arr history-error logs (fixes #21) When a Sonarr/Radarr is misconfigured or unreachable, the history check fails for every transfer on every poll, and each failure logged an error. Over weeks this grew the log to many GB and filled users' disks. Throttle the error to at most once per 5 minutes per *arr, so the problem stays visible without runaway log growth. --- src/download_system/transfer.rs | 10 +++++++++- src/state.rs | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/download_system/transfer.rs b/src/download_system/transfer.rs index ccad29c..6bd0f2b 100644 --- a/src/download_system/transfer.rs +++ b/src/download_system/transfer.rs @@ -54,7 +54,15 @@ impl Transfer { let service_result = match app.check_imported(&target.to).await { Ok(r) => r, Err(e) => { - error!("Error retrieving history from {}: {}", app, e); + // A misconfigured/unreachable *arr fails for every + // transfer on every poll; throttle the log so it doesn't + // fill the disk over time (issue #21). + if self.app_data.state.should_log_arr_error(&app.to_string()).await { + error!( + "Error retrieving history from {} (suppressing repeats for 5m): {}", + app, e + ); + } false } }; diff --git a/src/state.rs b/src/state.rs index a162f2d..9f4b7df 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,6 +4,7 @@ use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; +use std::time::{Duration, Instant}; use tokio::sync::RwLock; /// Key under which putioarr stores its transfer state in put.io's per-user @@ -30,6 +31,10 @@ pub struct StateManager { /// disk. Used to avoid telling the *arr a download is complete before the /// files actually exist locally (see issue #16). local_complete: Arc>>, + /// Last time a connection error was logged for each *arr, used to throttle + /// the log. A misconfigured Sonarr/Radarr fails on every poll for every + /// transfer, and logging each one filled users' disks over time (issue #21). + arr_error_logged: Arc>>, } impl StateManager { @@ -38,6 +43,26 @@ impl StateManager { api_token, transfers: Arc::new(RwLock::new(HashMap::new())), local_complete: Arc::new(RwLock::new(HashSet::new())), + arr_error_logged: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Minimum time between logging the same *arr's connection error. + const ARR_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(300); + + /// Returns true if an error for `app` should be logged now, throttling + /// repeats to at most one per [`Self::ARR_ERROR_LOG_INTERVAL`]. Keeps a + /// persistently unreachable/misconfigured *arr from filling the disk with + /// identical error lines on every poll (issue #21). + pub async fn should_log_arr_error(&self, app: &str) -> bool { + let mut map = self.arr_error_logged.write().await; + let now = Instant::now(); + match map.get(app) { + Some(at) if now.duration_since(*at) < Self::ARR_ERROR_LOG_INTERVAL => false, + _ => { + map.insert(app.to_string(), now); + true + } } } From 1ff4941ff991b1d422ec078dbcec00c0f353f87d Mon Sep 17 00:00:00 2001 From: bugrax Date: Wed, 10 Jun 2026 00:25:32 +0300 Subject: [PATCH 2/4] Avoid allocating on the throttled error path Key the *arr error throttle on the app's existing name field instead of app.to_string(), so the suppressed (common) path does no allocation. The full Display form is still used in the log message itself. --- src/download_system/transfer.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/download_system/transfer.rs b/src/download_system/transfer.rs index 6bd0f2b..6c59ab3 100644 --- a/src/download_system/transfer.rs +++ b/src/download_system/transfer.rs @@ -56,8 +56,9 @@ impl Transfer { Err(e) => { // A misconfigured/unreachable *arr fails for every // transfer on every poll; throttle the log so it doesn't - // fill the disk over time (issue #21). - if self.app_data.state.should_log_arr_error(&app.to_string()).await { + // fill the disk over time (issue #21). Key on the app's + // existing name (no allocation on the suppressed path). + if self.app_data.state.should_log_arr_error(&app.name).await { error!( "Error retrieving history from {} (suppressing repeats for 5m): {}", app, e From 5f3cbc669ca66a4d90b3cc905f9232f0a4b260aa Mon Sep 17 00:00:00 2001 From: bugrax Date: Wed, 10 Jun 2026 00:27:04 +0300 Subject: [PATCH 3/4] Use saturating_duration_since in the error-log throttle Instant::duration_since can panic if the stored instant is somehow later than now; saturating_duration_since returns zero instead, making the throttle robust regardless. --- src/state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/state.rs b/src/state.rs index 9f4b7df..884111f 100644 --- a/src/state.rs +++ b/src/state.rs @@ -58,7 +58,7 @@ impl StateManager { let mut map = self.arr_error_logged.write().await; let now = Instant::now(); match map.get(app) { - Some(at) if now.duration_since(*at) < Self::ARR_ERROR_LOG_INTERVAL => false, + Some(at) if now.saturating_duration_since(*at) < Self::ARR_ERROR_LOG_INTERVAL => false, _ => { map.insert(app.to_string(), now); true From a5c64ceb6a65c3e49ac498d1a45d8b9d1c95bf8d Mon Sep 17 00:00:00 2001 From: bugrax Date: Wed, 10 Jun 2026 00:28:59 +0300 Subject: [PATCH 4/4] Derive the throttle interval in the log from the constant The log message hardcoded '5m' while the interval is defined by ARR_ERROR_LOG_INTERVAL; format the constant into the message so the two can't drift if the interval is changed. --- src/download_system/transfer.rs | 6 ++++-- src/state.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/download_system/transfer.rs b/src/download_system/transfer.rs index 6c59ab3..c466506 100644 --- a/src/download_system/transfer.rs +++ b/src/download_system/transfer.rs @@ -60,8 +60,10 @@ impl Transfer { // existing name (no allocation on the suppressed path). if self.app_data.state.should_log_arr_error(&app.name).await { error!( - "Error retrieving history from {} (suppressing repeats for 5m): {}", - app, e + "Error retrieving history from {} (suppressing repeats for {:?}): {}", + app, + crate::state::StateManager::ARR_ERROR_LOG_INTERVAL, + e ); } false diff --git a/src/state.rs b/src/state.rs index 884111f..caae304 100644 --- a/src/state.rs +++ b/src/state.rs @@ -48,7 +48,7 @@ impl StateManager { } /// Minimum time between logging the same *arr's connection error. - const ARR_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(300); + pub const ARR_ERROR_LOG_INTERVAL: Duration = Duration::from_secs(300); /// Returns true if an error for `app` should be logged now, throttling /// repeats to at most one per [`Self::ARR_ERROR_LOG_INTERVAL`]. Keeps a