Skip to content

Commit b10af45

Browse files
committed
test for current behaviour of retry_operation_impl
1 parent 05d17e3 commit b10af45

File tree

3 files changed

+180
-0
lines changed

3 files changed

+180
-0
lines changed

ydb/_errors.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
from ydb import issues
5+
6+
_errors_retriable_fast_backoff_types = [
7+
issues.Unavailable,
8+
]
9+
_errors_retriable_slow_backoff_types = [
10+
issues.Aborted,
11+
issues.BadSession,
12+
issues.Overloaded,
13+
issues.SessionPoolEmpty,
14+
issues.ConnectionError,
15+
]
16+
_errors_retriable_slow_backoff_idempotent_types = [
17+
issues.Undetermined,
18+
]
19+
20+
21+
def check_retriable_error(err, retry_settings, attempt) -> "ErrorRetryInfo":
22+
if isinstance(err, issues.NotFound):
23+
if retry_settings.retry_not_found:
24+
return ErrorRetryInfo(True, retry_settings.fast_backoff.calc_timeout(attempt))
25+
else:
26+
return ErrorRetryInfo(False, None)
27+
28+
if isinstance(err, issues.InternalError):
29+
if retry_settings.retry_internal_error:
30+
return ErrorRetryInfo(True, retry_settings.slow_backoff.calc_timeout(attempt))
31+
else:
32+
return ErrorRetryInfo(False, None)
33+
34+
for t in _errors_retriable_fast_backoff_types:
35+
if isinstance(err, t):
36+
return ErrorRetryInfo(True, retry_settings.fast_backoff.calc_timeout(attempt))
37+
38+
for t in _errors_retriable_slow_backoff_types:
39+
if isinstance(err, t):
40+
return ErrorRetryInfo(True, retry_settings.slow_backoff.calc_timeout(attempt))
41+
42+
if retry_settings.idempotent:
43+
for t in _errors_retriable_slow_backoff_idempotent_types:
44+
return ErrorRetryInfo(True, retry_settings.slow_backoff.calc_timeout(attempt))
45+
46+
return ErrorRetryInfo(False, None)
47+
48+
49+
@dataclass
50+
class ErrorRetryInfo:
51+
is_retriable: bool
52+
sleep_timeout_seconds: Optional[float]

ydb/table.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,12 +916,24 @@ class YdbRetryOperationSleepOpt(object):
916916
def __init__(self, timeout):
917917
self.timeout = timeout
918918

919+
def __eq__(self, other):
920+
return type(self) == type(other) and self.timeout == other.timeout
921+
922+
def __repr__(self):
923+
return "YdbRetryOperationSleepOpt(%s)" % self.timeout
924+
919925

920926
class YdbRetryOperationFinalResult(object):
921927
def __init__(self, result):
922928
self.result = result
923929
self.exc = None
924930

931+
def __eq__(self, other):
932+
return type(self) == type(other) and self.result == other.result and self.exc == other.exc
933+
934+
def __repr__(self):
935+
return "YdbRetryOperationFinalResult(%s, exc=%s)" % (self.result, self.exc)
936+
925937
def set_exception(self, exc):
926938
self.exc = exc
927939

ydb/table_test.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import random
2+
from unittest import mock
3+
from ydb import retry_operation_impl, YdbRetryOperationFinalResult, issues, YdbRetryOperationSleepOpt, RetrySettings
4+
5+
6+
def test_retry_operation_impl(monkeypatch):
7+
monkeypatch.setattr("random.random", lambda: 0.5)
8+
monkeypatch.setattr(issues.Error, "__eq__",
9+
lambda self, other: type(self) == type(other) and self.message == other.message
10+
)
11+
12+
retry_once_settings = RetrySettings(
13+
max_retries=1,
14+
on_ydb_error_callback=mock.Mock(),
15+
)
16+
retry_once_settings.unknown_error_handler = mock.Mock()
17+
18+
def get_results(callee):
19+
res_generator = retry_operation_impl(callee, retry_settings=retry_once_settings)
20+
results = []
21+
exc = None
22+
try:
23+
for res in res_generator:
24+
results.append(res)
25+
if isinstance(res, YdbRetryOperationFinalResult):
26+
break
27+
except Exception as e:
28+
exc = e
29+
30+
return results, exc
31+
32+
class TestException(Exception):
33+
def __init__(self, message):
34+
super(TestException, self).__init__(message)
35+
self.message = message
36+
37+
def __eq__(self, other):
38+
return type(self) == type(other) and self.message == other.message
39+
40+
def check_unretriable_error(err_type, call_ydb_handler):
41+
retry_once_settings.on_ydb_error_callback.reset_mock()
42+
retry_once_settings.unknown_error_handler.reset_mock()
43+
44+
results = get_results(mock.Mock(side_effect=[err_type("test1"), err_type("test2")]))
45+
yields = results[0]
46+
exc = results[1]
47+
48+
assert yields == []
49+
assert exc == err_type("test1")
50+
51+
if call_ydb_handler:
52+
assert retry_once_settings.on_ydb_error_callback.call_count == 1
53+
retry_once_settings.on_ydb_error_callback.assert_called_with(err_type("test1"))
54+
55+
assert retry_once_settings.unknown_error_handler.call_count == 0
56+
else:
57+
assert retry_once_settings.on_ydb_error_callback.call_count == 0
58+
59+
assert retry_once_settings.unknown_error_handler.call_count == 1
60+
retry_once_settings.unknown_error_handler.assert_called_with(err_type("test1"))
61+
62+
63+
def check_retriable_error(err_type, backoff):
64+
retry_once_settings.on_ydb_error_callback.reset_mock()
65+
66+
results = get_results(mock.Mock(side_effect=[err_type("test1"), err_type("test2")]))
67+
yields = results[0]
68+
exc = results[1]
69+
70+
if backoff:
71+
assert [YdbRetryOperationSleepOpt(backoff.calc_timeout(0)), YdbRetryOperationSleepOpt(backoff.calc_timeout(1))] == yields
72+
else:
73+
assert [] == yields
74+
75+
assert exc == err_type("test2")
76+
77+
assert retry_once_settings.on_ydb_error_callback.call_count == 2
78+
retry_once_settings.on_ydb_error_callback.assert_any_call(err_type("test1"))
79+
retry_once_settings.on_ydb_error_callback.assert_called_with(err_type("test2"))
80+
81+
assert retry_once_settings.unknown_error_handler.call_count == 0
82+
83+
# check ok
84+
assert get_results(lambda: True) == ([YdbRetryOperationFinalResult(True)], None)
85+
86+
# check retry error and return result
87+
assert get_results(mock.Mock(side_effect=[issues.Overloaded("test"), True])) == ([
88+
YdbRetryOperationSleepOpt(retry_once_settings.slow_backoff.calc_timeout(0)),
89+
YdbRetryOperationFinalResult(True),
90+
], None)
91+
92+
# check errors
93+
check_retriable_error(issues.Aborted, None)
94+
check_retriable_error(issues.BadSession, None)
95+
96+
check_retriable_error(issues.NotFound, None)
97+
with mock.patch.object(retry_once_settings, "retry_not_found", False):
98+
check_unretriable_error(issues.NotFound, True)
99+
100+
check_retriable_error(issues.InternalError, None)
101+
with mock.patch.object(retry_once_settings, "retry_internal_error", False):
102+
check_unretriable_error(issues.InternalError, True)
103+
104+
check_retriable_error(issues.Overloaded, retry_once_settings.slow_backoff)
105+
check_retriable_error(issues.SessionPoolEmpty, retry_once_settings.slow_backoff)
106+
check_retriable_error(issues.ConnectionError, retry_once_settings.slow_backoff)
107+
108+
check_retriable_error(issues.Unavailable, retry_once_settings.fast_backoff)
109+
110+
check_unretriable_error(issues.Undetermined, True)
111+
with mock.patch.object(retry_once_settings, "idempotent", True):
112+
check_retriable_error(issues.Unavailable, retry_once_settings.fast_backoff)
113+
114+
check_unretriable_error(issues.Error, True)
115+
check_unretriable_error(TestException, False)
116+

0 commit comments

Comments
 (0)