From 491947ecab041956ec844bf86fbc01d5ebec08c5 Mon Sep 17 00:00:00 2001 From: Harvey Date: Fri, 3 Apr 2026 23:49:50 +0100 Subject: [PATCH] fix - count circuit breaker failures per logical call NOT per retry + actionable errors and placeholder in lidarr connection test --- backend/infrastructure/resilience/retry.py | 9 +++++---- backend/services/settings_service.py | 15 ++++++++++++--- .../infrastructure/test_retry_non_breaking.py | 7 ++++++- .../settings/SettingsLidarrConnection.svelte | 2 +- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/backend/infrastructure/resilience/retry.py b/backend/infrastructure/resilience/retry.py index a75b3d7..a244d42 100644 --- a/backend/infrastructure/resilience/retry.py +++ b/backend/infrastructure/resilience/retry.py @@ -244,10 +244,6 @@ def with_retry( last_exception = e elapsed_ms = int((time.time() - attempt_start) * 1000) - is_non_breaking = isinstance(e, non_breaking_exceptions) if non_breaking_exceptions else False - if circuit_breaker and (not is_non_breaking or circuit_breaker.state == CircuitState.HALF_OPEN): - await circuit_breaker.arecord_failure() - if attempt >= max_attempts: total_elapsed_ms = int((time.time() - start_time) * 1000) logger.error( @@ -295,6 +291,11 @@ def with_retry( await asyncio.sleep(delay) + if circuit_breaker and last_exception: + is_non_breaking = isinstance(last_exception, non_breaking_exceptions) if non_breaking_exceptions else False + if not is_non_breaking or circuit_breaker.state == CircuitState.HALF_OPEN: + await circuit_breaker.arecord_failure() + raise last_exception return wrapper diff --git a/backend/services/settings_service.py b/backend/services/settings_service.py index 3c4a1c7..f130fb3 100644 --- a/backend/services/settings_service.py +++ b/backend/services/settings_service.py @@ -132,10 +132,19 @@ class SettingsService: root_folders=root_folders ) except ExternalServiceError as e: - logger.warning(f"Lidarr connection test failed: {e}") + detail = str(e) + logger.warning(f"Lidarr connection test failed: {detail}") + if "No address associated with hostname" in detail or "Name or service not known" in detail: + hint = "DNS resolution failed — check the hostname is reachable from inside the container" + elif "Connection refused" in detail: + hint = "Connection refused — check the port and that Lidarr is running" + elif "timed out" in detail.lower() or "timeout" in detail.lower(): + hint = "Connection timed out — check network/firewall settings" + else: + hint = detail return LidarrVerifyResponse( success=False, - message="Couldn't reach Lidarr", + message=f"Couldn't reach Lidarr: {hint}", quality_profiles=[], metadata_profiles=[], root_folders=[] @@ -144,7 +153,7 @@ class SettingsService: logger.exception(f"Failed to verify Lidarr connection: {e}") return LidarrVerifyResponse( success=False, - message="Couldn't finish the connection test", + message=f"Couldn't finish the connection test: {e}", quality_profiles=[], metadata_profiles=[], root_folders=[] diff --git a/backend/tests/infrastructure/test_retry_non_breaking.py b/backend/tests/infrastructure/test_retry_non_breaking.py index 60e247a..c197e9f 100644 --- a/backend/tests/infrastructure/test_retry_non_breaking.py +++ b/backend/tests/infrastructure/test_retry_non_breaking.py @@ -68,10 +68,15 @@ async def test_breaking_exception_still_trips_circuit(): call_count += 1 raise _ServiceDown("down") + # Each call records one failure after all retries exhausted (not per retry) with pytest.raises(_ServiceDown): await fail() + assert cb.failure_count == 1 + assert cb.state == CircuitState.CLOSED - assert call_count == 3 + call_count = 0 + with pytest.raises(_ServiceDown): + await fail() assert cb.state == CircuitState.OPEN diff --git a/frontend/src/lib/components/settings/SettingsLidarrConnection.svelte b/frontend/src/lib/components/settings/SettingsLidarrConnection.svelte index 568e06a..21f1dcf 100644 --- a/frontend/src/lib/components/settings/SettingsLidarrConnection.svelte +++ b/frontend/src/lib/components/settings/SettingsLidarrConnection.svelte @@ -82,7 +82,7 @@ type="url" bind:value={form.data.lidarr_url} class="input input-bordered w-full" - placeholder="http://localhost:8686" + placeholder="http://lidarr:8686 or http://:8686" />