fix: Correct retry logic for 4xx errors and update deprecated datetime calls

- Fixed resilient_http_call decorator to NOT retry on 4xx client errors (only 5xx)
- Changed retry condition from retry_if_exception_type to retry_if_exception with custom logic
- Updated datetime.utcnow() to datetime.now(UTC) to fix deprecation warnings
- Fixed test imports to add lib/ to sys.path

All 12 unit tests now pass with no warnings.
This commit is contained in:
nathan 2026-04-13 11:00:47 -04:00
parent 6337182226
commit eb8b14b86f
2 changed files with 11 additions and 11 deletions

View File

@ -15,7 +15,7 @@ Usage:
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta, UTC
from enum import Enum from enum import Enum
from typing import Any, Callable, TypeVar from typing import Any, Callable, TypeVar
from functools import wraps from functools import wraps
@ -25,7 +25,7 @@ from tenacity import (
retry, retry,
stop_after_attempt, stop_after_attempt,
wait_exponential, wait_exponential,
retry_if_exception_type, retry_if_exception,
before_sleep_log, before_sleep_log,
RetryError, RetryError,
) )
@ -81,7 +81,7 @@ class CircuitBreaker:
async with self._lock: async with self._lock:
# Check if we should transition from OPEN → HALF_OPEN # Check if we should transition from OPEN → HALF_OPEN
if self.state == CircuitState.OPEN: if self.state == CircuitState.OPEN:
if self.last_failure_time and datetime.utcnow() - self.last_failure_time > timedelta(seconds=self.timeout_seconds): if self.last_failure_time and datetime.now(UTC) - self.last_failure_time > timedelta(seconds=self.timeout_seconds):
logger.info(f"[{self.service_name}] Circuit transitioning OPEN → HALF_OPEN (testing recovery)") logger.info(f"[{self.service_name}] Circuit transitioning OPEN → HALF_OPEN (testing recovery)")
self.state = CircuitState.HALF_OPEN self.state = CircuitState.HALF_OPEN
else: else:
@ -112,7 +112,7 @@ class CircuitBreaker:
"""Handle failed call — increment failures and potentially open circuit.""" """Handle failed call — increment failures and potentially open circuit."""
async with self._lock: async with self._lock:
self.consecutive_failures += 1 self.consecutive_failures += 1
self.last_failure_time = datetime.utcnow() self.last_failure_time = datetime.now(UTC)
if self.state == CircuitState.HALF_OPEN: if self.state == CircuitState.HALF_OPEN:
# Half-open test failed → back to OPEN # Half-open test failed → back to OPEN
@ -185,16 +185,11 @@ def resilient_http_call(
return True return True
return False return False
# Apply tenacity retry decorator # Apply tenacity retry decorator with custom retry condition
retrying_func = retry( retrying_func = retry(
stop=stop_after_attempt(max_attempts), stop=stop_after_attempt(max_attempts),
wait=wait_exponential(multiplier=1, min=2, max=10), wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type(( retry=retry_if_exception(should_retry_exception),
httpx.TimeoutException,
httpx.HTTPStatusError,
httpx.ConnectError,
httpx.RemoteProtocolError,
)),
before_sleep=before_sleep_log(logger, logging.INFO), before_sleep=before_sleep_log(logger, logging.INFO),
reraise=True, reraise=True,
)(func) )(func)

View File

@ -2,9 +2,14 @@
import pytest import pytest
import asyncio import asyncio
import sys
import os
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import httpx import httpx
# Add lib/ to path so we can import resilience module
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from resilience import ( from resilience import (
resilient_http_call, resilient_http_call,
handle_404_gracefully, handle_404_gracefully,