Skip to content

Commit cdb5445

Browse files
committed
feat: add retry decorator configuration and update deployment templates
1 parent 815ca87 commit cdb5445

7 files changed

Lines changed: 89 additions & 89 deletions

File tree

infrastructure/rag/templates/_helpers.tpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
{{- printf "%s-usecase-configmap" .Release.Name | trunc 63 | trimSuffix "-" -}}
1111
{{- end -}}
1212

13+
{{- define "configmap.retryDecoratorName" -}}
14+
{{- printf "%s-retry-decorator-configmap" .Release.Name | trunc 63 | trimSuffix "-" -}}
15+
{{- end -}}
16+
1317
{{- define "secret.usecaseName" -}}
1418
{{- printf "%s-usecase-secret" .Release.Name | trunc 63 | trimSuffix "-" -}}
1519
{{- end -}}

infrastructure/rag/templates/admin-backend/deployment.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ spec:
106106
name: {{ template "configmap.keyValueStoreName" . }}
107107
- configMapRef:
108108
name: {{ template "configmap.sourceUploaderName" . }}
109+
- configMapRef:
110+
name: {{ template "configmap.retryDecoratorName" . }}
109111
- secretRef:
110112
name: {{ template "secret.langfuseName" . }}
111113
- secretRef:

infrastructure/rag/templates/backend/deployment.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ spec:
131131
name: {{ template "configmap.fakeEmbedderName" . }}
132132
- configMapRef:
133133
name: {{ template "configmap.chatHistoryName" . }}
134+
- configMapRef:
135+
name: {{ template "configmap.retryDecoratorName" . }}
134136
- secretRef:
135137
name: {{ template "secret.langfuseName" . }}
136138
- secretRef:

infrastructure/rag/templates/configmap.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ data:
2424
{{- range $key, $value := .Values.shared.envs.usecase }}
2525
{{ $key }}: {{ $value | quote }}
2626
{{- end }}
27+
---
28+
apiVersion: v1
29+
kind: ConfigMap
30+
metadata:
31+
name: {{ template "configmap.retryDecoratorName" . }}
32+
data:
33+
{{- range $key, $value := .Values.shared.envs.retryDecorator }}
34+
{{ $key }}: {{ $value | quote }}
35+
{{- end }}

infrastructure/rag/values.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,14 @@ shared:
441441
s3:
442442
S3_ENDPOINT: http://rag-minio:9000
443443
S3_BUCKET: documents
444+
retryDecorator:
445+
RETRY_DECORATOR_MAX_RETRIES: "5"
446+
RETRY_DECORATOR_RETRY_BASE_DELAY: "0.5"
447+
RETRY_DECORATOR_RETRY_MAX_DELAY: "600"
448+
RETRY_DECORATOR_BACKOFF_FACTOR: "2"
449+
RETRY_DECORATOR_ATTEMPT_CAP: "6"
450+
RETRY_DECORATOR_JITTER_MIN: "0.05"
451+
RETRY_DECORATOR_JITTER_MAX: "0.25"
444452
usecase:
445453

446454

libs/rag-core-lib/src/rag_core_lib/impl/settings/retry_decorator_settings.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,8 @@ class RetryDecoratorSettings(BaseSettings):
2626
Maximum jitter to add to wait times.
2727
"""
2828

29-
# Pydantic v2 settings configuration
3029
model_config = SettingsConfigDict(env_prefix="RETRY_DECORATOR_", case_sensitive=False)
3130

32-
# Constrained fields
3331
max_retries: PositiveInt = Field(
3432
default=5,
3533
title="Max Retries",
Lines changed: 64 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import asyncio
2-
import logging
32
import time
43
from typing import Optional
54

@@ -19,160 +18,138 @@ def __init__(self, headers: Optional[dict[str, str]] = None, status_code: Option
1918
super().__init__("rate limit")
2019

2120

22-
@pytest.mark.asyncio
23-
async def test_async_success_first_try():
24-
calls = {"n": 0}
21+
@pytest.fixture
22+
def counter():
23+
class C:
24+
def __init__(self) -> None:
25+
self.n = 0
2526

26-
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=2))
27-
async def fn():
28-
calls["n"] += 1
29-
return 42
27+
def inc(self) -> int:
28+
self.n += 1
29+
return self.n
3030

31-
assert await fn() == 42
32-
assert calls["n"] == 1
31+
return C()
3332

3433

35-
@pytest.mark.asyncio
36-
async def test_async_retries_then_success(monkeypatch):
37-
calls = {"n": 0}
38-
slept = []
34+
@pytest.fixture
35+
def async_sleeps(monkeypatch):
36+
sleeps: list[float] = []
3937

4038
async def fake_sleep(x):
41-
slept.append(x)
39+
sleeps.append(x)
4240

4341
monkeypatch.setattr(asyncio, "sleep", fake_sleep)
42+
return sleeps
43+
44+
45+
@pytest.fixture
46+
def sync_sleeps(monkeypatch):
47+
sleeps: list[float] = []
48+
49+
def fake_sleep(x):
50+
sleeps.append(x)
51+
52+
monkeypatch.setattr(time, "sleep", fake_sleep)
53+
return sleeps
54+
4455

56+
@pytest.mark.asyncio
57+
async def test_async_success_first_try(counter):
58+
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=2))
59+
async def fn():
60+
counter.inc()
61+
return 42
62+
63+
assert await fn() == 42
64+
assert counter.n == 1
65+
66+
67+
@pytest.mark.asyncio
68+
async def test_async_retries_then_success(counter, async_sleeps):
4569
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=3, retry_base_delay=0.01))
4670
async def fn():
47-
calls["n"] += 1
48-
if calls["n"] < 3:
71+
if counter.inc() < 3:
4972
raise DummyError("boom")
5073
return "ok"
5174

5275
assert await fn() == "ok"
53-
assert calls["n"] == 3
54-
# Expect at least two sleeps due to two failures
55-
assert len(slept) >= 2
76+
assert counter.n == 3
77+
assert len(async_sleeps) >= 2 # two failures -> two sleeps
5678

5779

58-
def test_sync_success_first_try():
59-
calls = {"n": 0}
60-
80+
def test_sync_success_first_try(counter):
6181
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=2))
6282
def fn():
63-
calls["n"] += 1
83+
counter.inc()
6484
return 7
6585

6686
assert fn() == 7
67-
assert calls["n"] == 1
68-
69-
70-
def test_sync_retries_then_fail(monkeypatch):
71-
calls = {"n": 0}
72-
slept = []
87+
assert counter.n == 1
7388

74-
def fake_sleep(x):
75-
slept.append(x)
76-
77-
monkeypatch.setattr(time, "sleep", fake_sleep)
7889

90+
def test_sync_retries_then_fail(counter, sync_sleeps):
7991
@retry_with_backoff(settings=RetryDecoratorSettings(max_retries=2, retry_base_delay=0.01))
8092
def fn():
81-
calls["n"] += 1
93+
counter.inc()
8294
raise DummyError("always")
8395

8496
with pytest.raises(DummyError):
8597
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
98+
assert counter.n == 3 # 1 initial + 2 retries
99+
assert len(sync_sleeps) == 2
90100

91101

92102
@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-
}
103+
async def test_async_rate_limit_uses_header_wait(counter, async_sleeps):
104+
headers = {"x-ratelimit-reset-requests": "1.5s", "x-ratelimit-remaining-requests": "0"}
106105

107106
@retry_with_backoff(
108107
settings=RetryDecoratorSettings(max_retries=1, retry_base_delay=0.01),
109108
rate_limit_exceptions=(RateLimitError,),
110109
)
111110
async def fn():
112-
calls["n"] += 1
113-
if calls["n"] == 1:
111+
if counter.inc() == 1:
114112
raise RateLimitError(headers=headers, status_code=429)
115113
return "ok"
116114

117115
out = await fn()
118116
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)
117+
assert counter.n == 2
118+
assert any(x >= 1.5 for x in async_sleeps)
122119

123120

124121
@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:
122+
async def test_is_rate_limited_callback(counter, async_sleeps):
123+
def mark_rate_limited(exc: BaseException) -> bool: # noqa: ANN001 - explicit for clarity
135124
return isinstance(exc, DummyError)
136125

137126
@retry_with_backoff(
138127
settings=RetryDecoratorSettings(max_retries=1, retry_base_delay=0.01),
139128
is_rate_limited=mark_rate_limited,
140129
)
141130
async def fn():
142-
calls["n"] += 1
143-
if calls["n"] == 1:
131+
if counter.inc() == 1:
144132
raise DummyError("treated as rate limited")
145133
return "ok"
146134

147135
out = await fn()
148136
assert out == "ok"
149-
assert calls["n"] == 2
150-
assert len(slept) == 1
137+
assert counter.n == 2
138+
assert len(async_sleeps) == 1
151139

152140

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-
}
141+
def test_sync_rate_limit_headers(counter, sync_sleeps):
142+
headers = {"x-ratelimit-reset-requests": "2s"}
165143

166144
@retry_with_backoff(
167145
settings=RetryDecoratorSettings(max_retries=1, retry_base_delay=0.01),
168146
rate_limit_exceptions=(RateLimitError,),
169147
)
170148
def fn():
171-
calls["n"] += 1
172-
if calls["n"] == 1:
149+
if counter.inc() == 1:
173150
raise RateLimitError(headers=headers, status_code=429)
174151
return "ok"
175152

176153
assert fn() == "ok"
177-
assert calls["n"] == 2
178-
assert any(x >= 2.0 for x in slept)
154+
assert counter.n == 2
155+
assert any(x >= 2.0 for x in sync_sleeps)

0 commit comments

Comments
 (0)