Skip to content

Commit 3aa3493

Browse files
authored
ステータスコードが429のときは、Retry-Afterヘッダ値だけ待つようにする (#431)
* update retry * format * version up
1 parent ad6f376 commit 3aa3493

File tree

4 files changed

+187
-23
lines changed

4 files changed

+187
-23
lines changed

annofabapi/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.55.0"
1+
__version__ = "0.55.1"

annofabapi/api.py

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import copy
22
import json
33
import logging
4+
import time
45
from functools import wraps
56
from json import JSONDecodeError
67
from typing import Any, Dict, Optional, Tuple
@@ -18,6 +19,9 @@
1819
DEFAULT_ENDPOINT_URL = "https://annofab.com"
1920
"""AnnoFab WebAPIのデフォルトのエンドポイントURL"""
2021

22+
DEFAULT_WAITING_TIME_SECONDS_WITH_429_STATUS_CODE = 300
23+
"""HTTP Status Codeが429のときの、デフォルト(Retry-Afterヘッダがないとき)の待ち時間です。"""
24+
2125

2226
def _raise_for_status(response: requests.Response) -> None:
2327
"""
@@ -157,13 +161,13 @@ def mask_key(d, key: str):
157161

158162

159163
def _should_retry_with_status(status_code: int) -> bool:
160-
"""HTTP Status Codeからリトライすべきかどうかを返す。"""
161-
if status_code == 429:
162-
return True
163-
elif 500 <= status_code < 600:
164+
"""
165+
HTTP Status Codeからリトライすべきかどうかを返す。
166+
"""
167+
# 注意:429(Too many requests)の場合は、backoffモジュール外でリトライするため、このメソッドでは判定しない
168+
if 500 <= status_code < 600:
164169
return True
165-
else:
166-
return False
170+
return False
167171

168172

169173
def my_backoff(function):
@@ -363,8 +367,9 @@ def _execute_http_request(
363367
raise_for_status: bool = True,
364368
**kwargs,
365369
) -> requests.Response:
366-
"""Session情報を使って、HTTP Requestを投げる。
367-
引数は ``requests.Session.request`` にそのまま渡す。
370+
"""
371+
Session情報を使って、HTTP Requestを投げます。AnnoFab WebAPIで取得したAWS S3のURLなどに、アクセスすることを想定しています。
372+
引数は ``requests.Session.request`` にそのまま渡します。
368373
369374
Args:
370375
raise_for_status: Trueの場合HTTP Status Codeが4XX,5XXのときはHTTPErrorをスローします
@@ -398,7 +403,47 @@ def _execute_http_request(
398403
},
399404
},
400405
)
401-
# リトライすべき場合はExceptionを返す
406+
407+
# リクエスト過多の場合、待ってから再度アクセスする
408+
if response.status_code == requests.codes.too_many_requests:
409+
retry_after_value = response.headers.get("Retry-After")
410+
waiting_time_seconds = (
411+
float(retry_after_value)
412+
if retry_after_value is not None
413+
else DEFAULT_WAITING_TIME_SECONDS_WITH_429_STATUS_CODE
414+
)
415+
416+
logger.warning(
417+
"HTTPステータスコードが'%s'なので、%s秒待ってからリトライします。 :: %s",
418+
response.status_code,
419+
waiting_time_seconds,
420+
{
421+
"response": {
422+
"status_code": response.status_code,
423+
"text": response.text,
424+
"headers": {"Retry-After": retry_after_value},
425+
},
426+
"request": {
427+
"http_method": http_method,
428+
"url": url,
429+
"query_params": _create_query_params_for_logger(params) if params is not None else None,
430+
},
431+
},
432+
)
433+
434+
time.sleep(float(waiting_time_seconds))
435+
return self._execute_http_request(
436+
http_method=http_method,
437+
url=url,
438+
params=params,
439+
data=data,
440+
json=json,
441+
headers=headers,
442+
raise_for_status=raise_for_status,
443+
**kwargs,
444+
)
445+
446+
# リトライすべき場合はExceptionをスローする
402447
if raise_for_status or _should_retry_with_status(response.status_code):
403448
_log_error_response(logger, response)
404449
_raise_for_status(response)
@@ -417,14 +462,14 @@ def _request_wrapper(
417462
raise_for_status: bool = True,
418463
) -> Tuple[Any, requests.Response]:
419464
"""
420-
HTTP Requestを投げて、Responseを返す
465+
AnnoFab WebAPIにアクセスして、レスポンスの中身とレスポンスを取得します
421466
422467
Args:
423468
http_method:
424-
url_path:
425-
query_params:
426-
header_params:
427-
request_body:
469+
url_path: AnnoFab WebAPIのパス(例:``/my/account``)
470+
query_params: クエリパラメタ
471+
header_params: リクエストヘッダ
472+
request_body: リクエストボディ
428473
raise_for_status: Trueの場合HTTP Status Codeが4XX,5XXのときはHTTPErrorをスローします。Falseの場合はtuple[None, Response]を返します。
429474
430475
Returns:
@@ -435,6 +480,8 @@ def _request_wrapper(
435480
HTTPError: 引数 ``raise_for_status`` がTrueで、HTTP status codeが4xxx,5xxのときにスローします。
436481
437482
"""
483+
484+
# TODO 判定条件が不明
438485
if url_path.startswith("/internal/"):
439486
url = f"{self.endpoint_url}/api{url_path}"
440487
else:
@@ -471,6 +518,41 @@ def _request_wrapper(
471518
request_body=request_body,
472519
raise_for_status=raise_for_status,
473520
)
521+
elif response.status_code == requests.codes.too_many_requests:
522+
retry_after_value = response.headers.get("Retry-After")
523+
waiting_time_seconds = (
524+
float(retry_after_value)
525+
if retry_after_value is not None
526+
else DEFAULT_WAITING_TIME_SECONDS_WITH_429_STATUS_CODE
527+
)
528+
529+
logger.warning(
530+
"HTTPステータスコードが'%s'なので、%s秒待ってからリトライします。 :: %s",
531+
response.status_code,
532+
waiting_time_seconds,
533+
{
534+
"response": {
535+
"status_code": response.status_code,
536+
"text": response.text,
537+
"headers": {"Retry-After": retry_after_value},
538+
},
539+
"request": {
540+
"http_method": http_method.lower(),
541+
"url": url,
542+
"query_params": query_params,
543+
},
544+
},
545+
)
546+
547+
time.sleep(waiting_time_seconds)
548+
return self._request_wrapper(
549+
http_method,
550+
url_path,
551+
query_params=query_params,
552+
header_params=header_params,
553+
request_body=request_body,
554+
raise_for_status=raise_for_status,
555+
)
474556

475557
response.encoding = "utf-8"
476558
content = self._response_to_content(response)

annofabapi/api2.py

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import logging
2+
import time
23
from typing import Any, Dict, Optional, Tuple
34

45
import requests
56
from requests.cookies import RequestsCookieJar
67

78
import annofabapi.utils
8-
from annofabapi.api import AnnofabApi, _log_error_response, _raise_for_status
9+
from annofabapi.api import (
10+
DEFAULT_WAITING_TIME_SECONDS_WITH_429_STATUS_CODE,
11+
AnnofabApi,
12+
_create_request_body_for_logger,
13+
_log_error_response,
14+
_raise_for_status,
15+
_should_retry_with_status,
16+
)
917
from annofabapi.generated_api2 import AbstractAnnofabApi2
1018

1119
logger = logging.getLogger(__name__)
@@ -39,9 +47,11 @@ def _request_wrapper(
3947
self,
4048
http_method: str,
4149
url_path: str,
50+
*,
4251
query_params: Optional[Dict[str, Any]] = None,
4352
header_params: Optional[Dict[str, Any]] = None,
4453
request_body: Optional[Any] = None,
54+
raise_for_status: bool = True,
4555
) -> Tuple[Any, requests.Response]:
4656
"""
4757
HTTP Requestを投げて、Responseを返す。
@@ -51,6 +61,7 @@ def _request_wrapper(
5161
query_params:
5262
header_params:
5363
request_body:
64+
raise_for_status: Trueの場合HTTP Status Codeが4XX,5XXのときはHTTPErrorをスローします。Falseの場合はtuple[None, Response]を返します。
5465
5566
Returns:
5667
Tuple[content, Response]. contentはcontent_typeにより型が変わる。
@@ -68,26 +79,97 @@ def _request_wrapper(
6879
# Unauthorized Errorならば、ログイン後に再度実行する
6980
if response.status_code == requests.codes.unauthorized:
7081
self.api.login()
71-
return self._request_wrapper(http_method, url_path, query_params, header_params, request_body)
82+
return self._request_wrapper(
83+
http_method,
84+
url_path,
85+
query_params=query_params,
86+
header_params=header_params,
87+
request_body=request_body,
88+
raise_for_status=raise_for_status,
89+
)
7290

7391
else:
7492
kwargs.update({"cookies": self.cookies})
7593

7694
# HTTP Requestを投げる
7795
response = self.api.session.request(method=http_method.lower(), url=url, **kwargs)
7896

97+
logger.debug(
98+
"Sent a request :: %s",
99+
{
100+
"request": {
101+
"http_method": http_method.lower(),
102+
"url": url,
103+
"query_params": query_params,
104+
"header_params": header_params,
105+
"request_body": _create_request_body_for_logger(request_body)
106+
if request_body is not None
107+
else None,
108+
},
109+
"response": {
110+
"status_code": response.status_code,
111+
"content_length": len(response.content),
112+
},
113+
},
114+
)
115+
79116
# CloudFrontから403 Errorが発生したとき
80117
if response.status_code == requests.codes.forbidden and response.headers.get("server") == "CloudFront":
81118

82119
self._get_signed_access_v2(url_path)
83-
return self._request_wrapper(http_method, url_path, query_params, header_params, request_body)
84-
85-
_log_error_response(logger, response)
120+
return self._request_wrapper(
121+
http_method,
122+
url_path,
123+
query_params=query_params,
124+
header_params=header_params,
125+
request_body=request_body,
126+
raise_for_status=raise_for_status,
127+
)
128+
129+
elif response.status_code == requests.codes.too_many_requests:
130+
retry_after_value = response.headers.get("Retry-After")
131+
waiting_time_seconds = (
132+
float(retry_after_value)
133+
if retry_after_value is not None
134+
else DEFAULT_WAITING_TIME_SECONDS_WITH_429_STATUS_CODE
135+
)
136+
137+
logger.warning(
138+
"HTTPステータスコードが'%s'なので、%s秒待ってからリトライします。 :: %s",
139+
response.status_code,
140+
waiting_time_seconds,
141+
{
142+
"response": {
143+
"status_code": response.status_code,
144+
"text": response.text,
145+
"headers": {"Retry-After": retry_after_value},
146+
},
147+
"request": {
148+
"http_method": http_method.lower(),
149+
"url": url,
150+
"query_params": query_params,
151+
},
152+
},
153+
)
154+
155+
time.sleep(waiting_time_seconds)
156+
return self._request_wrapper(
157+
http_method,
158+
url_path,
159+
query_params=query_params,
160+
header_params=header_params,
161+
request_body=request_body,
162+
raise_for_status=raise_for_status,
163+
)
86164

87165
response.encoding = "utf-8"
88-
_raise_for_status(response)
89-
90166
content = self.api._response_to_content(response)
167+
168+
# リトライすべき場合はExceptionを返す
169+
if raise_for_status or _should_retry_with_status(response.status_code):
170+
_log_error_response(logger, response)
171+
_raise_for_status(response)
172+
91173
return content, response
92174

93175
def _get_signed_access_v2(self, url_path: str):

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "annofabapi"
3-
version = "0.55.0"
3+
version = "0.55.1"
44
description = "Python Clinet Library of AnnoFab WebAPI (https://annofab.com/docs/api/)"
55
authors = ["yuji38kwmt"]
66
license = "MIT"

0 commit comments

Comments
 (0)