Skip to content

Commit 8ca62ff

Browse files
authored
Merge pull request #99 Extract check retriable error
2 parents 05d17e3 + 164597b commit 8ca62ff

File tree

3 files changed

+230
-44
lines changed

3 files changed

+230
-44
lines changed

ydb/_errors.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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):
22+
if isinstance(err, issues.NotFound):
23+
if retry_settings.retry_not_found:
24+
return ErrorRetryInfo(
25+
True, retry_settings.fast_backoff.calc_timeout(attempt)
26+
)
27+
else:
28+
return ErrorRetryInfo(False, None)
29+
30+
if isinstance(err, issues.InternalError):
31+
if retry_settings.retry_internal_error:
32+
return ErrorRetryInfo(
33+
True, retry_settings.slow_backoff.calc_timeout(attempt)
34+
)
35+
else:
36+
return ErrorRetryInfo(False, None)
37+
38+
for t in _errors_retriable_fast_backoff_types:
39+
if isinstance(err, t):
40+
return ErrorRetryInfo(
41+
True, retry_settings.fast_backoff.calc_timeout(attempt)
42+
)
43+
44+
for t in _errors_retriable_slow_backoff_types:
45+
if isinstance(err, t):
46+
return ErrorRetryInfo(
47+
True, retry_settings.slow_backoff.calc_timeout(attempt)
48+
)
49+
50+
if retry_settings.idempotent:
51+
for t in _errors_retriable_slow_backoff_idempotent_types:
52+
return ErrorRetryInfo(
53+
True, retry_settings.slow_backoff.calc_timeout(attempt)
54+
)
55+
56+
return ErrorRetryInfo(False, None)
57+
58+
59+
@dataclass
60+
class ErrorRetryInfo:
61+
is_retriable: bool
62+
sleep_timeout_seconds: Optional[float]

ydb/table.py

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
_tx_ctx_impl,
2222
tracing,
2323
)
24+
from ._errors import check_retriable_error
2425

2526
try:
2627
from . import interceptor
@@ -916,12 +917,28 @@ class YdbRetryOperationSleepOpt(object):
916917
def __init__(self, timeout):
917918
self.timeout = timeout
918919

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

920927
class YdbRetryOperationFinalResult(object):
921928
def __init__(self, result):
922929
self.result = result
923930
self.exc = None
924931

932+
def __eq__(self, other):
933+
return (
934+
type(self) == type(other)
935+
and self.result == other.result
936+
and self.exc == other.exc
937+
)
938+
939+
def __repr__(self):
940+
return "YdbRetryOperationFinalResult(%s, exc=%s)" % (self.result, self.exc)
941+
925942
def set_exception(self, exc):
926943
self.exc = exc
927944

@@ -938,56 +955,28 @@ def retry_operation_impl(callee, retry_settings=None, *args, **kwargs):
938955
if result.exc is not None:
939956
raise result.exc
940957

941-
except (
942-
issues.Aborted,
943-
issues.BadSession,
944-
issues.NotFound,
945-
issues.InternalError,
946-
) as e:
947-
status = e
948-
retry_settings.on_ydb_error_callback(e)
949-
950-
if isinstance(e, issues.NotFound) and not retry_settings.retry_not_found:
951-
raise e
952-
953-
if (
954-
isinstance(e, issues.InternalError)
955-
and not retry_settings.retry_internal_error
956-
):
957-
raise e
958-
959-
except (
960-
issues.Overloaded,
961-
issues.SessionPoolEmpty,
962-
issues.ConnectionError,
963-
) as e:
964-
status = e
965-
retry_settings.on_ydb_error_callback(e)
966-
yield YdbRetryOperationSleepOpt(
967-
retry_settings.slow_backoff.calc_timeout(attempt)
968-
)
969-
970-
except issues.Unavailable as e:
958+
except issues.Error as e:
971959
status = e
972960
retry_settings.on_ydb_error_callback(e)
973-
yield YdbRetryOperationSleepOpt(
974-
retry_settings.fast_backoff.calc_timeout(attempt)
975-
)
976961

977-
except issues.Undetermined as e:
978-
status = e
979-
retry_settings.on_ydb_error_callback(e)
980-
if not retry_settings.idempotent:
981-
# operation is not idempotent, so we cannot retry.
962+
retriable_info = check_retriable_error(e, retry_settings, attempt)
963+
if not retriable_info.is_retriable:
982964
raise
983965

984-
yield YdbRetryOperationSleepOpt(
985-
retry_settings.fast_backoff.calc_timeout(attempt)
986-
)
966+
skip_yield_error_types = [
967+
issues.Aborted,
968+
issues.BadSession,
969+
issues.NotFound,
970+
issues.InternalError,
971+
]
987972

988-
except issues.Error as e:
989-
retry_settings.on_ydb_error_callback(e)
990-
raise
973+
yield_sleep = True
974+
for t in skip_yield_error_types:
975+
if isinstance(e, t):
976+
yield_sleep = False
977+
978+
if yield_sleep:
979+
yield YdbRetryOperationSleepOpt(retriable_info.sleep_timeout_seconds)
991980

992981
except Exception as e:
993982
# you should provide your own handler you want

ydb/table_test.py

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

0 commit comments

Comments
 (0)