Skip to content

Commit bf57b88

Browse files
Copilotmpieniak01
andcommitted
Add tests for anti-loop protection methods and document async client cleanup limitation
Co-authored-by: mpieniak01 <8170413+mpieniak01@users.noreply.github.com>
1 parent 85b7e43 commit bf57b88

File tree

2 files changed

+126
-0
lines changed

2 files changed

+126
-0
lines changed

tests/test_traffic_control.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,3 +432,111 @@ def test_traffic_controller_provider_configs(provider, expected_capacity):
432432
controller.check_outbound_request(provider) # Create policy
433433
metrics = controller.get_metrics(provider)
434434
assert metrics["rate_limit"]["capacity"] == expected_capacity
435+
436+
437+
class TestAntiLoopProtection:
438+
"""Testy dla anti-loop protection helper methods."""
439+
440+
def test_is_under_global_request_cap_below_threshold(self):
441+
"""Test że requests poniżej globalnego limitu zwracają True."""
442+
config = TrafficControlConfig()
443+
config.max_requests_per_minute_global = 1000
444+
445+
assert config.is_under_global_request_cap(500) is True
446+
assert config.is_under_global_request_cap(999) is True
447+
448+
def test_is_under_global_request_cap_at_threshold(self):
449+
"""Test że requests równe limitowi zwracają False."""
450+
config = TrafficControlConfig()
451+
config.max_requests_per_minute_global = 1000
452+
453+
assert config.is_under_global_request_cap(1000) is False
454+
455+
def test_is_under_global_request_cap_above_threshold(self):
456+
"""Test że requests powyżej limitu zwracają False."""
457+
config = TrafficControlConfig()
458+
config.max_requests_per_minute_global = 1000
459+
460+
assert config.is_under_global_request_cap(1001) is False
461+
assert config.is_under_global_request_cap(2000) is False
462+
463+
def test_can_retry_operation_below_max(self):
464+
"""Test że retry_count poniżej max_retries zwraca True."""
465+
config = TrafficControlConfig()
466+
config.max_retries_per_operation = 5
467+
468+
assert config.can_retry_operation(0) is True
469+
assert config.can_retry_operation(4) is True
470+
471+
def test_can_retry_operation_at_max(self):
472+
"""Test że retry_count równy max_retries zwraca False."""
473+
config = TrafficControlConfig()
474+
config.max_retries_per_operation = 5
475+
476+
assert config.can_retry_operation(5) is False
477+
478+
def test_can_retry_operation_above_max(self):
479+
"""Test że retry_count powyżej max_retries zwraca False."""
480+
config = TrafficControlConfig()
481+
config.max_retries_per_operation = 5
482+
483+
assert config.can_retry_operation(6) is False
484+
assert config.can_retry_operation(10) is False
485+
486+
def test_should_enter_degraded_state_disabled(self):
487+
"""Test że degraded mode wyłączony zawsze zwraca False."""
488+
config = TrafficControlConfig()
489+
config.degraded_mode_enabled = False
490+
config.max_requests_per_minute_global = 1000
491+
config.degraded_mode_failure_threshold = 10
492+
493+
# Nawet przy przekroczeniu limitów
494+
assert config.should_enter_degraded_state(2000, 20) is False
495+
496+
def test_should_enter_degraded_state_request_cap_exceeded(self):
497+
"""Test przejścia w degraded gdy przekroczony globalny limit requestów."""
498+
config = TrafficControlConfig()
499+
config.degraded_mode_enabled = True
500+
config.max_requests_per_minute_global = 1000
501+
config.degraded_mode_failure_threshold = 10
502+
503+
# Przekroczenie requestów
504+
assert config.should_enter_degraded_state(1000, 0) is True
505+
assert config.should_enter_degraded_state(1500, 5) is True
506+
507+
def test_should_enter_degraded_state_failure_threshold_exceeded(self):
508+
"""Test przejścia w degraded gdy przekroczony próg błędów."""
509+
config = TrafficControlConfig()
510+
config.degraded_mode_enabled = True
511+
config.max_requests_per_minute_global = 1000
512+
config.degraded_mode_failure_threshold = 10
513+
514+
# Przekroczenie błędów
515+
assert config.should_enter_degraded_state(500, 10) is True
516+
assert config.should_enter_degraded_state(100, 15) is True
517+
518+
def test_should_enter_degraded_state_below_all_thresholds(self):
519+
"""Test że degraded mode nie włącza się gdy wszystko w normie."""
520+
config = TrafficControlConfig()
521+
config.degraded_mode_enabled = True
522+
config.max_requests_per_minute_global = 1000
523+
config.degraded_mode_failure_threshold = 10
524+
525+
assert config.should_enter_degraded_state(500, 5) is False
526+
assert config.should_enter_degraded_state(999, 9) is False
527+
528+
def test_should_enter_degraded_state_boundary_conditions(self):
529+
"""Test warunków brzegowych dla degraded mode."""
530+
config = TrafficControlConfig()
531+
config.degraded_mode_enabled = True
532+
config.max_requests_per_minute_global = 1000
533+
config.degraded_mode_failure_threshold = 10
534+
535+
# Dokładnie na granicy requestów (should trigger)
536+
assert config.should_enter_degraded_state(1000, 0) is True
537+
538+
# Dokładnie na granicy błędów (should trigger)
539+
assert config.should_enter_degraded_state(0, 10) is True
540+
541+
# Jeden poniżej granic (should not trigger)
542+
assert config.should_enter_degraded_state(999, 9) is False

venom_core/infrastructure/traffic_control/http_client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ class TrafficControlledHttpClient:
2424
2. Circuit breaker dla ochrony przed degradacją
2525
3. Retry policy z exponential backoff
2626
4. Telemetria requestów
27+
28+
**Ważne zasady użycia:**
29+
- Metody synchroniczne (get, post, etc.) mogą być używane bez context managera,
30+
ale zalecane jest użycie: `with TrafficControlledHttpClient(...) as client:`
31+
- Metody async (aget, apost, etc.) MUSZĄ być używane z async context manager:
32+
`async with TrafficControlledHttpClient(...) as client:`
33+
- Resource cleanup dla async clienta działa TYLKO przez context manager
34+
(__del__ nie może bezpiecznie zamknąć async zasobów)
2735
"""
2836

2937
def __init__(
@@ -276,6 +284,16 @@ def __del__(self) -> None:
276284
"""
277285
Best-effort cleanup w przypadku gdy klient nie jest użyty jako context manager.
278286
287+
WAŻNE: Ta metoda zamyka TYLKO synchroniczny _client. Async _async_client
288+
NIE jest zamykany, ponieważ async cleanup w __del__ jest niebezpieczny
289+
(brak running event loop podczas garbage collection).
290+
291+
**Zalecenie**: Używaj async metod WYŁĄCZNIE z async context manager:
292+
```python
293+
async with TrafficControlledHttpClient(provider="github") as client:
294+
response = await client.aget("https://api.github.com/users/mpieniak01")
295+
```
296+
279297
Uwaga: Wyjątki podczas cleanup są pomijane aby uniknąć problemów
280298
podczas garbage collection.
281299
"""

0 commit comments

Comments
 (0)