Skip to content

Commit 4864666

Browse files
authored
fix: Improve remote evaluation fetch retry logic (#42)
1 parent 412addb commit 4864666

File tree

8 files changed

+48
-5
lines changed

8 files changed

+48
-5
lines changed

.github/workflows/publish-to-pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
python-version: 3.7
3737

3838
- name: Install dependencies
39-
run: python -m pip install build setuptools wheel twine amplitude_analytics python-semantic-release==7.34.6
39+
run: python -m pip install build setuptools wheel twine amplitude_analytics parameterized python-semantic-release==7.34.6
4040

4141
- name: Run Test
4242
run: python -m unittest discover -s ./tests -p '*_test.py'

.github/workflows/publish-to-test-pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
python-version: 3.7
2828

2929
- name: Install dependencies
30-
run: python -m pip install build setuptools wheel twine amplitude_analytics
30+
run: python -m pip install build setuptools wheel twine amplitude_analytics parameterized
3131

3232
- name: Build a binary wheel and a source tarball
3333
run: python -m build --sdist --wheel --outdir dist/ .

.github/workflows/test-arm.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,5 @@ jobs:
2121
apt -y install ca-certificates
2222
run: |
2323
pip install -r requirements.txt
24+
pip install -r requirements-dev.txt
2425
python3 -m unittest discover -s ./tests -p '*_test.py'

.github/workflows/test.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020

2121
- name: Install requirements
2222
run: pip install -r requirements.txt
23+
pip install -r requirements-dev.txt
2324

2425
- name: Unit Test
2526
run: python -m unittest discover -s ./tests -p '*_test.py'

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
parameterized~=0.9.0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
class FetchException(Exception):
2+
def __init__(self, status_code, message):
3+
super().__init__(message)
4+
self.status_code = status_code

src/amplitude_experiment/remote/client.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from .config import RemoteEvaluationConfig
99
from ..connection_pool import HTTPConnectionPool
10+
from ..exception import FetchException
1011
from ..user import User
1112
from ..util.deprecated import deprecated
1213
from ..util.variant import evaluation_variants_json_to_variants
@@ -112,7 +113,8 @@ def __fetch_internal(self, user):
112113
return self.__do_fetch(user)
113114
except Exception as e:
114115
self.logger.error(f"[Experiment] Fetch failed: {e}")
115-
return self.__retry_fetch(user)
116+
if self.__should_retry_fetch(e):
117+
return self.__retry_fetch(user)
116118

117119
def __retry_fetch(self, user):
118120
if self.config.fetch_retries == 0:
@@ -148,6 +150,9 @@ def __do_fetch(self, user):
148150
response = conn.request('POST', '/sdk/v2/vardata?v=0', body, headers)
149151
elapsed = '%.3f' % ((time.time() - start) * 1000)
150152
self.logger.debug(f"[Experiment] Fetch complete in {elapsed} ms")
153+
if response.status != 200:
154+
raise FetchException(response.status,
155+
f"Fetch error response: status={response.status} {response.reason}")
151156
json_response = json.loads(response.read().decode("utf8"))
152157
variants = evaluation_variants_json_to_variants(json_response)
153158
self.logger.debug(f"[Experiment] Fetched variants: {json.dumps(variants, default=str)}")
@@ -189,6 +194,11 @@ def is_default_variant(variant: Variant) -> bool:
189194
if variant.metadata is not None and variant.metadata.get('deployed') is not None:
190195
deployed = variant.metadata.get('deployed')
191196
return default and not deployed
192-
return {key: variant for key, variant in variants.items() if not is_default_variant(variant)}
193197

198+
return {key: variant for key, variant in variants.items() if not is_default_variant(variant)}
194199

200+
@staticmethod
201+
def __should_retry_fetch(err: Exception):
202+
if isinstance(err, FetchException):
203+
return err.status_code < 400 or err.status_code >= 500 or err.status_code == 429
204+
return True

tests/remote/client_test.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import unittest
2+
from unittest import mock
3+
4+
from parameterized import parameterized
25

36
from src.amplitude_experiment import RemoteEvaluationClient, Variant, User, RemoteEvaluationConfig
7+
from src.amplitude_experiment.exception import FetchException
48

59
API_KEY = 'client-DvWljIjiiuqLbyjqdvBaLFfEBrAvGuA3'
610
SERVER_URL = 'https://api.lab.amplitude.com/sdk/vardata'
@@ -36,11 +40,33 @@ def test_fetch_async(self):
3640
self.client.fetch_async(user, self.callback_for_async)
3741

3842
def test_fetch_failed_with_retry(self):
39-
with RemoteEvaluationClient(API_KEY, RemoteEvaluationConfig(debug=False, fetch_retries=1, fetch_timeout_millis=1)) as client:
43+
with RemoteEvaluationClient(API_KEY, RemoteEvaluationConfig(debug=False, fetch_retries=1,
44+
fetch_timeout_millis=1)) as client:
4045
user = User(user_id='test_user')
4146
variants = client.fetch(user)
4247
self.assertEqual({}, variants)
4348

49+
@parameterized.expand([
50+
(300, "Fetch Exception 300", True),
51+
(400, "Fetch Exception 400", False),
52+
(429, "Fetch Exception 429", True),
53+
(500, "Fetch Exception 500", True),
54+
(000, "Other Exception", True),
55+
])
56+
@mock.patch("src.amplitude_experiment.remote.client.RemoteEvaluationClient._RemoteEvaluationClient__retry_fetch")
57+
@mock.patch("src.amplitude_experiment.remote.client.RemoteEvaluationClient._RemoteEvaluationClient__do_fetch")
58+
def test_fetch_retry_with_response(self, response_code, error_message, should_call_retry, mock_do_fetch,
59+
mock_retry_fetch):
60+
if response_code == 000:
61+
mock_do_fetch.side_effect = Exception(error_message)
62+
else:
63+
mock_do_fetch.side_effect = FetchException(response_code, error_message)
64+
instance = RemoteEvaluationClient(API_KEY, RemoteEvaluationConfig(fetch_retries=1))
65+
user = User(user_id='test_user')
66+
instance.fetch(user)
67+
mock_do_fetch.assert_called_once_with(user)
68+
self.assertEqual(should_call_retry, mock_retry_fetch.called)
69+
4470

4571
if __name__ == '__main__':
4672
unittest.main()

0 commit comments

Comments
 (0)