Skip to content

Commit 815ca87

Browse files
committed
test: add comprehensive tests for retry decorator with async and sync handling
1 parent cbdd287 commit 815ca87

1 file changed

Lines changed: 178 additions & 0 deletions

File tree

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import asyncio
2+
import logging
3+
import time
4+
from typing import Optional
5+
6+
import pytest
7+
8+
from rag_core_lib.impl.settings.retry_decorator_settings import RetryDecoratorSettings
9+
from rag_core_lib.impl.utils.retry_decorator import retry_with_backoff
10+
11+
12+
class DummyError(Exception):
13+
pass
14+
15+
16+
class RateLimitError(Exception):
17+
def __init__(self, headers: Optional[dict[str, str]] = None, status_code: Optional[int] = None):
18+
self.response = type("Resp", (), {"headers": headers or {}, "status_code": status_code})()
19+
super().__init__("rate limit")
20+
21+
22+
@pytest.mark.asyncio
23+
async def test_async_success_first_try():
24+
calls = {"n": 0}
25+
26+
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=2))
27+
async def fn():
28+
calls["n"] += 1
29+
return 42
30+
31+
assert await fn() == 42
32+
assert calls["n"] == 1
33+
34+
35+
@pytest.mark.asyncio
36+
async def test_async_retries_then_success(monkeypatch):
37+
calls = {"n": 0}
38+
slept = []
39+
40+
async def fake_sleep(x):
41+
slept.append(x)
42+
43+
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
44+
45+
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=3, retry_base_delay=0.01))
46+
async def fn():
47+
calls["n"] += 1
48+
if calls["n"] < 3:
49+
raise DummyError("boom")
50+
return "ok"
51+
52+
assert await fn() == "ok"
53+
assert calls["n"] == 3
54+
# Expect at least two sleeps due to two failures
55+
assert len(slept) >= 2
56+
57+
58+
def test_sync_success_first_try():
59+
calls = {"n": 0}
60+
61+
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=2))
62+
def fn():
63+
calls["n"] += 1
64+
return 7
65+
66+
assert fn() == 7
67+
assert calls["n"] == 1
68+
69+
70+
def test_sync_retries_then_fail(monkeypatch):
71+
calls = {"n": 0}
72+
slept = []
73+
74+
def fake_sleep(x):
75+
slept.append(x)
76+
77+
monkeypatch.setattr(time, "sleep", fake_sleep)
78+
79+
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=2, retry_base_delay=0.01))
80+
def fn():
81+
calls["n"] += 1
82+
raise DummyError("always")
83+
84+
with pytest.raises(DummyError):
85+
fn()
86+
# 1 initial + 2 retries = 3 calls total
87+
assert calls["n"] == 3
88+
# Two sleeps (after two failures)
89+
assert len(slept) == 2
90+
91+
92+
@pytest.mark.asyncio
93+
async def test_async_rate_limit_uses_header_wait(monkeypatch):
94+
calls = {"n": 0}
95+
slept = []
96+
97+
async def fake_sleep(x):
98+
slept.append(x)
99+
100+
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
101+
102+
headers = {
103+
"x-ratelimit-reset-requests": "1.5s",
104+
"x-ratelimit-remaining-requests": "0",
105+
}
106+
107+
@retry_with_backoff(
108+
settings=RetryDecoratorSettings(max_retries=1, retry_base_delay=0.01),
109+
rate_limit_exceptions=(RateLimitError,),
110+
)
111+
async def fn():
112+
calls["n"] += 1
113+
if calls["n"] == 1:
114+
raise RateLimitError(headers=headers, status_code=429)
115+
return "ok"
116+
117+
out = await fn()
118+
assert out == "ok"
119+
assert calls["n"] == 2
120+
# Should sleep roughly ~1.5s (+jitter). Verify >= 1.5
121+
assert any(x >= 1.5 for x in slept)
122+
123+
124+
@pytest.mark.asyncio
125+
async def test_is_rate_limited_callback(monkeypatch):
126+
calls = {"n": 0}
127+
slept = []
128+
129+
async def fake_sleep(x):
130+
slept.append(x)
131+
132+
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
133+
134+
def mark_rate_limited(exc: BaseException) -> bool:
135+
return isinstance(exc, DummyError)
136+
137+
@retry_with_backoff(
138+
settings=RetryDecoratorSettings(max_retries=1, retry_base_delay=0.01),
139+
is_rate_limited=mark_rate_limited,
140+
)
141+
async def fn():
142+
calls["n"] += 1
143+
if calls["n"] == 1:
144+
raise DummyError("treated as rate limited")
145+
return "ok"
146+
147+
out = await fn()
148+
assert out == "ok"
149+
assert calls["n"] == 2
150+
assert len(slept) == 1
151+
152+
153+
def test_sync_rate_limit_headers(monkeypatch):
154+
calls = {"n": 0}
155+
slept = []
156+
157+
def fake_sleep(x):
158+
slept.append(x)
159+
160+
monkeypatch.setattr(time, "sleep", fake_sleep)
161+
162+
headers = {
163+
"x-ratelimit-reset-requests": "2s",
164+
}
165+
166+
@retry_with_backoff(
167+
settings=RetryDecoratorSettings(max_retries=1, retry_base_delay=0.01),
168+
rate_limit_exceptions=(RateLimitError,),
169+
)
170+
def fn():
171+
calls["n"] += 1
172+
if calls["n"] == 1:
173+
raise RateLimitError(headers=headers, status_code=429)
174+
return "ok"
175+
176+
assert fn() == "ok"
177+
assert calls["n"] == 2
178+
assert any(x >= 2.0 for x in slept)

0 commit comments

Comments
 (0)