Skip to content

Commit 2b1809b

Browse files
authored
Retry KEEPER_EXCEPTION in ClickhouseClient (#476)
1 parent 3296132 commit 2b1809b

3 files changed

Lines changed: 36 additions & 23 deletions

File tree

ch_tools/common/clickhouse/client/clickhouse_client.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,7 @@ def query(
194194
``query`` may be ``None`` to perform a connectivity check (used by
195195
:meth:`ping`); in that case an HTTP GET to the server root is issued.
196196
197-
Retries transient errors (ConnectionError, Timeout, ReadTimeout,
198-
ChunkedEncodingError, and ClickhouseError with status codes
199-
408, 429, 500, 502, 503, 504) using exponential back-off.
197+
Retries transient errors using exponential back-off.
200198
"""
201199
retrying = tenacity.Retrying(
202200
retry=tenacity.retry_if_exception(is_transient_error),

ch_tools/common/clickhouse/client/retry.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Any, Tuple, Type, Union
1+
from http import HTTPStatus
2+
from typing import Any, Optional, Tuple, Type, Union
23

34
import requests
45
import tenacity
@@ -7,25 +8,37 @@
78

89
from .error import ClickhouseError
910

11+
RETRYABLE_CLICKHOUSE_ERROR_CODES = {
12+
999, # KEEPER_EXCEPTION
13+
}
14+
15+
RETRYABLE_HTTP_STATUS_CODES = {
16+
HTTPStatus.TOO_MANY_REQUESTS,
17+
HTTPStatus.BAD_GATEWAY,
18+
HTTPStatus.SERVICE_UNAVAILABLE,
19+
HTTPStatus.GATEWAY_TIMEOUT,
20+
}
21+
22+
23+
def _get_clickhouse_error_code(exc: ClickhouseError) -> Optional[int]:
24+
"""
25+
Extract ClickHouse exception code from the response.
26+
27+
ClickHouse sets the X-ClickHouse-Exception-Code header in HTTP 500
28+
responses when the error occurs before streaming starts.
29+
"""
30+
if exc.response is None:
31+
return None
32+
header_value = exc.response.headers.get("X-ClickHouse-Exception-Code", "")
33+
try:
34+
return int(header_value)
35+
except (TypeError, ValueError):
36+
return None
37+
1038

1139
def is_transient_error(exc: BaseException) -> bool:
1240
"""
1341
Determine if an error is transient and can be retried.
14-
15-
Retryable errors:
16-
- requests.exceptions.ConnectionError (network issues, DNS, connection reset)
17-
- requests.exceptions.Timeout, ReadTimeout (transient network issues)
18-
- requests.exceptions.ChunkedEncodingError (transient network)
19-
- ClickhouseError with HTTP status codes from proxy/load balancer:
20-
- 429: Too Many Requests
21-
- 502: Bad Gateway
22-
- 503: Service Unavailable
23-
- 504: Gateway Timeout
24-
25-
Non-retryable errors:
26-
- HTTP 500: real ClickHouse DB errors (not idempotent to retry)
27-
- HTTP 4xx: client errors (syntax, permissions, unknown tables)
28-
- All other exceptions
2942
"""
3043
# Network-related errors are retryable
3144
if isinstance(
@@ -42,9 +55,11 @@ def is_transient_error(exc: BaseException) -> bool:
4255
# ClickHouse HTTP errors - check status code. Do not rely on
4356
# requests.Response truthiness: 4xx/5xx responses are falsy.
4457
if isinstance(exc, ClickhouseError):
45-
retryable_status_codes = {429, 502, 503, 504}
46-
status_code = exc.response.status_code if exc.response is not None else None
47-
return status_code in retryable_status_codes
58+
if exc.response is None:
59+
return False
60+
if exc.response.status_code in RETRYABLE_HTTP_STATUS_CODES:
61+
return True
62+
return _get_clickhouse_error_code(exc) in RETRYABLE_CLICKHOUSE_ERROR_CODES
4863

4964
return False
5065

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ score = false
192192

193193

194194
[tool.mypy]
195-
python_version = 3.10
195+
python_version = "3.10"
196196
ignore_missing_imports = true
197197
disallow_incomplete_defs = true
198198
check_untyped_defs = true

0 commit comments

Comments
 (0)