Skip to content

Commit c054d80

Browse files
authored
fix(http): bound timeout and add conservative transient retry to Open… (#99)
2 parents 0a0d90b + 1563c6b commit c054d80

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

agentkit/client/base_service_client.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818
"""
1919

2020
import json
21+
import os
2122
from typing import Any, Dict, Type, TypeVar, Union, Optional
2223
from dataclasses import dataclass
2324

25+
from requests.adapters import HTTPAdapter
26+
from urllib3.util.retry import Retry
2427
from volcengine.ApiInfo import ApiInfo
2528
from volcengine.base.Service import Service
2629
from volcengine.Credentials import Credentials as VolcCredentials
@@ -174,6 +177,41 @@ def __init__(
174177
if self.session_token:
175178
self.set_session_token(self.session_token)
176179

180+
self._install_retry_adapter()
181+
182+
def _install_retry_adapter(self) -> None:
183+
"""Retry transient overload responses (429/503) and connection failures
184+
with exponential backoff on the shared ``Service`` session.
185+
186+
Scoped to cases that mean the request was not processed, so
187+
non-idempotent ``Create*`` actions are never double-executed: read
188+
timeouts are not retried (``read=0``) and only 429/503 are status-
189+
retried. Honors ``Retry-After``. Disabled with AGENTKIT_HTTP_RETRIES=0.
190+
"""
191+
try:
192+
retries = max(0, int(os.getenv("AGENTKIT_HTTP_RETRIES", "2")))
193+
except ValueError:
194+
retries = 2
195+
if retries <= 0:
196+
return
197+
session = getattr(self, "session", None)
198+
if session is None:
199+
return
200+
retry = Retry(
201+
total=retries,
202+
connect=retries,
203+
read=0,
204+
status=retries,
205+
status_forcelist=(429, 503),
206+
allowed_methods=None, # OpenAPI calls are POST; gate by status only
207+
backoff_factor=0.5,
208+
respect_retry_after_header=True,
209+
raise_on_status=False,
210+
)
211+
adapter = HTTPAdapter(max_retries=retry)
212+
session.mount("https://", adapter)
213+
session.mount("http://", adapter)
214+
177215
def _should_auto_refresh_vefaas_credentials(self) -> bool:
178216
if self._explicit_credentials:
179217
return False

agentkit/utils/ve_sign.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import hmac
3232
import os
3333
import platform
34+
import time
3435
import requests
3536
from urllib.parse import quote
3637

@@ -46,6 +47,79 @@
4647
MAX_X_CUSTOM_SOURCE_LENGTH = 256
4748

4849

50+
# Transient-failure handling for signed OpenAPI calls. Historically this used a
51+
# single timeout-less ``requests.request`` — a stalled connection could hang
52+
# forever, and a transient overload (429/503) or connection error failed the
53+
# call outright with no retry. Both are now bounded and conservatively retried.
54+
# Tunable via env; AGENTKIT_HTTP_RETRIES=0 disables retries.
55+
_RETRYABLE_STATUS = frozenset({429, 503})
56+
57+
58+
def _http_timeout() -> float:
59+
try:
60+
return max(1.0, float(os.getenv("AGENTKIT_HTTP_TIMEOUT", "30")))
61+
except ValueError:
62+
return 30.0
63+
64+
65+
def _http_retries() -> int:
66+
try:
67+
return max(0, int(os.getenv("AGENTKIT_HTTP_RETRIES", "2")))
68+
except ValueError:
69+
return 2
70+
71+
72+
def _backoff_seconds(attempt: int) -> float:
73+
return min(8.0, 0.5 * (2**attempt))
74+
75+
76+
def _retry_after_seconds(resp: requests.Response) -> float | None:
77+
raw = resp.headers.get("Retry-After")
78+
if not raw:
79+
return None
80+
try:
81+
return max(0.0, float(raw))
82+
except ValueError:
83+
return None
84+
85+
86+
def _signed_request(method, url, headers, params, data) -> requests.Response:
87+
"""Issue a signed HTTP request with a bounded timeout and conservative
88+
transient-retry.
89+
90+
Only retries failures that almost certainly mean the request was not
91+
processed — connection errors (incl. connect timeouts) and HTTP 429/503 —
92+
so non-idempotent ``Create*`` actions are never double-executed. Read
93+
timeouts and other 5xx are surfaced, not retried. Honors ``Retry-After``.
94+
"""
95+
retries = _http_retries()
96+
timeout = _http_timeout()
97+
resp: requests.Response | None = None
98+
for attempt in range(retries + 1):
99+
try:
100+
resp = requests.request(
101+
method=method,
102+
url=url,
103+
headers=headers,
104+
params=params,
105+
data=data,
106+
timeout=timeout,
107+
)
108+
except requests.ConnectionError:
109+
# Not delivered (connection failed/reset before completion), so a
110+
# retry cannot double-execute the request.
111+
if attempt < retries:
112+
time.sleep(_backoff_seconds(attempt))
113+
continue
114+
raise
115+
if resp.status_code in _RETRYABLE_STATUS and attempt < retries:
116+
time.sleep(_retry_after_seconds(resp) or _backoff_seconds(attempt))
117+
continue
118+
return resp
119+
assert resp is not None # loop always returns or raises before here
120+
return resp
121+
122+
49123
def _get_os_tag() -> str:
50124
system = platform.system().lower()
51125
if "linux" in system:
@@ -221,7 +295,7 @@ def request(method, date, query, header, ak, sk, action, body):
221295
header = {**header, **sign_result}
222296
# header = {**header, **{"X-Security-Token": SessionToken}}
223297
# 第六步:将 Signature 签名写入 HTTP Header 中,并发送 HTTP 请求。
224-
r = requests.request(
298+
r = _signed_request(
225299
method=method,
226300
url=f"{Scheme}://{request_param['host']}{request_param['path']}",
227301
headers=header,

0 commit comments

Comments
 (0)