Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit8898c38

Browse files
nejchmax-wittig
authored andcommitted
feat(client): add retry handling to GraphQL client
1 parent3235c48 commit8898c38

File tree

6 files changed

+113
-38
lines changed

6 files changed

+113
-38
lines changed

‎gitlab/client.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
try:
2828
importgql
29+
importgql.transport.exceptions
2930
importgraphql
3031
importhttpx
3132

@@ -746,7 +747,9 @@ def http_request(
746747
if200<=result.status_code<300:
747748
returnresult.response
748749

749-
ifretry.handle_retry_on_status(result):
750+
ifretry.handle_retry_on_status(
751+
result.status_code,result.headers,result.reason
752+
):
750753
continue
751754

752755
error_message=result.content
@@ -1329,4 +1332,28 @@ def execute(
13291332
self,request:Union[str,graphql.Source],*args:Any,**kwargs:Any
13301333
)->Any:
13311334
parsed_document=self._gql(request)
1332-
returnself._client.execute(parsed_document,*args,**kwargs)
1335+
retry=utils.Retry(
1336+
max_retries=3,obey_rate_limit=True,retry_transient_errors=False
1337+
)
1338+
1339+
whileTrue:
1340+
try:
1341+
result=self._client.execute(parsed_document,*args,**kwargs)
1342+
exceptgql.transport.exceptions.TransportServerErrorase:
1343+
ifretry.handle_retry_on_status(
1344+
status_code=e.code,headers=self._transport.response_headers
1345+
):
1346+
continue
1347+
1348+
ife.code==401:
1349+
raisegitlab.exceptions.GitlabAuthenticationError(
1350+
response_code=e.code,
1351+
error_message=str(e),
1352+
)
1353+
1354+
raisegitlab.exceptions.GitlabHttpError(
1355+
response_code=e.code,
1356+
error_message=str(e),
1357+
)
1358+
1359+
returnresult

‎gitlab/utils.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,22 @@
66
importtraceback
77
importurllib.parse
88
importwarnings
9-
fromtypingimportAny,Callable,Dict,Iterator,Literal,Optional,Tuple,Type,Union
9+
fromtypingimport (
10+
Any,
11+
Callable,
12+
Dict,
13+
Iterator,
14+
Literal,
15+
MutableMapping,
16+
Optional,
17+
Tuple,
18+
Type,
19+
Union,
20+
)
1021

1122
importrequests
1223

1324
fromgitlabimportconst,types
14-
fromgitlab._backendsimportrequests_backend
1525

1626

1727
class_StdoutStream:
@@ -100,33 +110,40 @@ def __init__(
100110
self.retry_transient_errors=retry_transient_errors
101111

102112
def_retryable_status_code(
103-
self,
104-
result:requests_backend.RequestsResponse,
113+
self,status_code:Optional[int],reason:str=""
105114
)->bool:
106-
ifresult.status_code==429andself.obey_rate_limit:
115+
ifstatus_code==429andself.obey_rate_limit:
107116
returnTrue
108117

109118
ifnotself.retry_transient_errors:
110119
returnFalse
111-
ifresult.status_codeinconst.RETRYABLE_TRANSIENT_ERROR_CODES:
120+
ifstatus_codeinconst.RETRYABLE_TRANSIENT_ERROR_CODES:
112121
returnTrue
113-
ifresult.status_code==409and"Resource lock"inresult.reason:
122+
ifstatus_code==409and"Resource lock"inreason:
114123
returnTrue
115124

116125
returnFalse
117126

118-
defhandle_retry_on_status(self,result:requests_backend.RequestsResponse)->bool:
119-
ifnotself._retryable_status_code(result):
127+
defhandle_retry_on_status(
128+
self,
129+
status_code:Optional[int],
130+
headers:Optional[MutableMapping[str,str]]=None,
131+
reason:str="",
132+
)->bool:
133+
ifnotself._retryable_status_code(status_code,reason):
120134
returnFalse
121135

136+
ifheadersisNone:
137+
headers= {}
138+
122139
# Response headers documentation:
123140
# https://docs.gitlab.com/ee/user/admin_area/settings/user_and_ip_rate_limits.html#response-headers
124141
ifself.max_retries==-1orself.cur_retries<self.max_retries:
125142
wait_time=2**self.cur_retries*0.1
126-
if"Retry-After"inresult.headers:
127-
wait_time=int(result.headers["Retry-After"])
128-
elif"RateLimit-Reset"inresult.headers:
129-
wait_time=int(result.headers["RateLimit-Reset"])-time.time()
143+
if"Retry-After"inheaders:
144+
wait_time=int(headers["Retry-After"])
145+
elif"RateLimit-Reset"inheaders:
146+
wait_time=int(headers["RateLimit-Reset"])-time.time()
130147
self.cur_retries+=1
131148
time.sleep(wait_time)
132149
returnTrue

‎requirements-lint.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mypy==1.11.2
88
pylint==3.2.7
99
pytest==8.3.2
1010
responses==0.25.3
11+
respx==0.21.1
1112
types-PyYAML==6.0.12.20240808
1213
types-requests==2.32.0.20240907
1314
types-setuptools==74.1.0.20240907

‎requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ pytest-github-actions-annotate-failures==0.2.0
77
pytest==8.3.2
88
PyYaml==6.0.2
99
responses==0.25.3
10+
respx==0.21.1
1011
wheel==0.44.0

‎tests/unit/test_graphql.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1+
importhttpx
12
importpytest
3+
importrespx
24

35
importgitlab
46

57

8+
@pytest.fixture
9+
defgl_gql()->gitlab.GraphQL:
10+
returngitlab.GraphQL("https://gitlab.example.com")
11+
12+
613
deftest_import_error_includes_message(monkeypatch:pytest.MonkeyPatch):
714
monkeypatch.setattr(gitlab.client,"_GQL_INSTALLED",False)
815
withpytest.raises(ImportError,match="GraphQL client could not be initialized"):
@@ -12,3 +19,42 @@ def test_import_error_includes_message(monkeypatch: pytest.MonkeyPatch):
1219
deftest_graphql_as_context_manager_exits():
1320
withgitlab.GraphQL()asgl:
1421
assertisinstance(gl,gitlab.GraphQL)
22+
23+
24+
deftest_graphql_retries_on_429_response(
25+
gl_gql:gitlab.GraphQL,respx_mock:respx.MockRouter
26+
):
27+
url="https://gitlab.example.com/api/graphql"
28+
responses= [
29+
httpx.Response(429,headers={"retry-after":"1"}),
30+
httpx.Response(
31+
200,json={"data": {"currentUser": {"id":"gid://gitlab/User/1"}}}
32+
),
33+
]
34+
respx_mock.post(url).mock(side_effect=responses)
35+
gl_gql.execute("query {currentUser {id}}")
36+
37+
38+
deftest_graphql_raises_when_max_retries_exceeded(
39+
gl_gql:gitlab.GraphQL,respx_mock:respx.MockRouter
40+
):
41+
url="https://gitlab.example.com/api/graphql"
42+
responses= [
43+
httpx.Response(502),
44+
httpx.Response(502),
45+
httpx.Response(502),
46+
httpx.Response(502),
47+
httpx.Response(502),
48+
]
49+
respx_mock.post(url).mock(side_effect=responses)
50+
withpytest.raises(gitlab.GitlabHttpError):
51+
gl_gql.execute("query {currentUser {id}}")
52+
53+
54+
deftest_graphql_raises_on_401_response(
55+
gl_gql:gitlab.GraphQL,respx_mock:respx.MockRouter
56+
):
57+
url="https://gitlab.example.com/api/graphql"
58+
respx_mock.post(url).mock(return_value=httpx.Response(401))
59+
withpytest.raises(gitlab.GitlabAuthenticationError):
60+
gl_gql.execute("query {currentUser {id}}")

‎tests/unit/test_retry.py

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,24 @@
22
fromunittestimportmock
33

44
importpytest
5-
importrequests
65

76
fromgitlabimportutils
8-
fromgitlab._backendsimportrequests_backend
97

108

119
deftest_handle_retry_on_status_ignores_unknown_status_code():
1210
retry=utils.Retry(max_retries=1,retry_transient_errors=True)
13-
response=requests.Response()
14-
response.status_code=418
15-
backend_response=requests_backend.RequestsResponse(response)
16-
17-
assertretry.handle_retry_on_status(backend_response)isFalse
11+
assertretry.handle_retry_on_status(418)isFalse
1812

1913

2014
deftest_handle_retry_on_status_accepts_retry_after_header(
2115
monkeypatch:pytest.MonkeyPatch,
2216
):
2317
mock_sleep=mock.Mock()
2418
monkeypatch.setattr(time,"sleep",mock_sleep)
25-
2619
retry=utils.Retry(max_retries=1)
27-
response=requests.Response()
28-
response.status_code=429
29-
response.headers["Retry-After"]="1"
30-
backend_response=requests_backend.RequestsResponse(response)
20+
headers= {"Retry-After":"1"}
3121

32-
assertretry.handle_retry_on_status(backend_response)isTrue
22+
assertretry.handle_retry_on_status(429,headers=headers)isTrue
3323
assertisinstance(mock_sleep.call_args[0][0],int)
3424

3525

@@ -40,19 +30,12 @@ def test_handle_retry_on_status_accepts_ratelimit_reset_header(
4030
monkeypatch.setattr(time,"sleep",mock_sleep)
4131

4232
retry=utils.Retry(max_retries=1)
43-
response=requests.Response()
44-
response.status_code=429
45-
response.headers["RateLimit-Reset"]=str(int(time.time()+1))
46-
backend_response=requests_backend.RequestsResponse(response)
33+
headers= {"RateLimit-Reset":str(int(time.time()+1))}
4734

48-
assertretry.handle_retry_on_status(backend_response)isTrue
35+
assertretry.handle_retry_on_status(429,headers=headers)isTrue
4936
assertisinstance(mock_sleep.call_args[0][0],float)
5037

5138

5239
deftest_handle_retry_on_status_returns_false_when_max_retries_reached():
5340
retry=utils.Retry(max_retries=0)
54-
response=requests.Response()
55-
response.status_code=429
56-
backend_response=requests_backend.RequestsResponse(response)
57-
58-
assertretry.handle_retry_on_status(backend_response)isFalse
41+
assertretry.handle_retry_on_status(429)isFalse

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp