Skip to content

Commit c6efe45

Browse files
authored
Fix #887 Enable automatic retry by a handy way (#1084)
1 parent 8a0c802 commit c6efe45

48 files changed

Lines changed: 2431 additions & 357 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

integration_tests/webhook/test_webhook.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import unittest
33
import time
44

5+
import pytest
6+
57
from integration_tests.env_variable_names import (
68
SLACK_SDK_TEST_INCOMING_WEBHOOK_URL,
79
SLACK_SDK_TEST_INCOMING_WEBHOOK_CHANNEL_NAME,
@@ -68,6 +70,9 @@ def test_with_unfurls_off(self):
6870
self.assertIsNotNone(history)
6971
self.assertTrue("attachments" not in history["messages"][0])
7072

73+
74+
# FIXME: This test started failing as of August 5, 2021
75+
@pytest.mark.skip()
7176
def test_with_unfurls_on(self):
7277
# Slack API rate limits unfurls of unique links so test will
7378
# fail when repeated. For testing, either use a different URL

slack_sdk/audit_logs/v1/async_client.py

Lines changed: 116 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import json
66
import logging
77
from ssl import SSLContext
8-
from typing import Any
8+
from typing import Any, List
99
from typing import Dict, Optional
1010

1111
import aiohttp
@@ -18,6 +18,11 @@
1818
get_user_agent,
1919
)
2020
from .response import AuditLogsResponse
21+
from slack_sdk.http_retry.async_handler import AsyncRetryHandler
22+
from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers
23+
from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest
24+
from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse
25+
from slack_sdk.http_retry.state import RetryState
2126
from ...proxy_env_variable_loader import load_http_proxy_from_env
2227

2328

@@ -34,6 +39,7 @@ class AsyncAuditLogsClient:
3439
auth: Optional[BasicAuth]
3540
default_headers: Dict[str, str]
3641
logger: logging.Logger
42+
retry_handlers: List[AsyncRetryHandler]
3743

3844
def __init__(
3945
self,
@@ -49,6 +55,7 @@ def __init__(
4955
user_agent_prefix: Optional[str] = None,
5056
user_agent_suffix: Optional[str] = None,
5157
logger: Optional[logging.Logger] = None,
58+
retry_handlers: List[AsyncRetryHandler] = async_default_handlers,
5259
):
5360
"""API client for Audit Logs API
5461
See https://api.slack.com/admins/audit-logs for more details
@@ -66,6 +73,7 @@ def __init__(
6673
user_agent_prefix: Prefix for User-Agent header value
6774
user_agent_suffix: Suffix for User-Agent header value
6875
logger: Custom logger
76+
retry_handlers: Retry handlers
6977
"""
7078
self.token = token
7179
self.timeout = timeout
@@ -80,6 +88,7 @@ def __init__(
8088
user_agent_prefix, user_agent_suffix
8189
)
8290
self.logger = logger if logger is not None else logging.getLogger(__name__)
91+
self.retry_handlers = retry_handlers
8392

8493
if self.proxy is None or len(self.proxy.strip()) == 0:
8594
env_variable = load_http_proxy_from_env(self.logger)
@@ -218,18 +227,6 @@ async def _perform_http_request(
218227
body_params = json.dumps(body_params)
219228
headers["Content-Type"] = "application/json;charset=utf-8"
220229

221-
if self.logger.level <= logging.DEBUG:
222-
headers_for_logging = {
223-
k: "(redacted)" if k.lower() == "authorization" else v
224-
for k, v in headers.items()
225-
}
226-
self.logger.debug(
227-
f"Sending a request - "
228-
f"url: {url}, "
229-
f"params: {query_params}, "
230-
f"body: {body_params}, "
231-
f"headers: {headers_for_logging}"
232-
)
233230
session: Optional[ClientSession] = None
234231
use_running_session = self.session and not self.session.closed
235232
if use_running_session:
@@ -241,7 +238,8 @@ async def _perform_http_request(
241238
trust_env=self.trust_env_in_session,
242239
)
243240

244-
resp: AuditLogsResponse
241+
last_error = None
242+
resp: Optional[AuditLogsResponse] = None
245243
try:
246244
request_kwargs = {
247245
"headers": headers,
@@ -250,25 +248,112 @@ async def _perform_http_request(
250248
"ssl": self.ssl,
251249
"proxy": self.proxy,
252250
}
253-
async with session.request(http_verb, url, **request_kwargs) as res:
254-
response_body = {}
255-
try:
256-
response_body = await res.text()
257-
except aiohttp.ContentTypeError:
251+
retry_request = RetryHttpRequest(
252+
method=http_verb,
253+
url=url,
254+
headers=headers,
255+
body_params=body_params,
256+
)
257+
258+
retry_state = RetryState()
259+
counter_for_safety = 0
260+
while counter_for_safety < 100:
261+
counter_for_safety += 1
262+
# If this is a retry, the next try started here. We can reset the flag.
263+
retry_state.next_attempt_requested = False
264+
retry_response: Optional[RetryHttpResponse] = None
265+
response_body = ""
266+
267+
if self.logger.level <= logging.DEBUG:
268+
headers_for_logging = {
269+
k: "(redacted)" if k.lower() == "authorization" else v
270+
for k, v in headers.items()
271+
}
258272
self.logger.debug(
259-
f"No response data returned from the following API call: {url}."
273+
f"Sending a request - "
274+
f"url: {url}, "
275+
f"params: {query_params}, "
276+
f"body: {body_params}, "
277+
f"headers: {headers_for_logging}"
260278
)
261-
except json.decoder.JSONDecodeError as e:
262-
message = f"Failed to parse the response body: {str(e)}"
263-
raise SlackApiError(message, res)
264-
265-
resp = AuditLogsResponse(
266-
url=url,
267-
status_code=res.status,
268-
raw_body=response_body,
269-
headers=res.headers,
270-
)
271-
_debug_log_response(self.logger, resp)
279+
280+
try:
281+
async with session.request(http_verb, url, **request_kwargs) as res:
282+
try:
283+
response_body = await res.text()
284+
retry_response = RetryHttpResponse(
285+
status_code=res.status,
286+
headers=res.headers,
287+
data=response_body.encode("utf-8")
288+
if response_body is not None
289+
else None,
290+
)
291+
except aiohttp.ContentTypeError:
292+
self.logger.debug(
293+
f"No response data returned from the following API call: {url}."
294+
)
295+
except json.decoder.JSONDecodeError as e:
296+
message = f"Failed to parse the response body: {str(e)}"
297+
raise SlackApiError(message, res)
298+
299+
if res.status == 429:
300+
for handler in self.retry_handlers:
301+
if await handler.can_retry_async(
302+
state=retry_state,
303+
request=retry_request,
304+
response=retry_response,
305+
):
306+
if self.logger.level <= logging.DEBUG:
307+
self.logger.info(
308+
f"A retry handler found: {type(handler).__name__} "
309+
f"for {http_verb} {url} - rate_limited"
310+
)
311+
await handler.prepare_for_next_attempt_async(
312+
state=retry_state,
313+
request=retry_request,
314+
response=retry_response,
315+
)
316+
break
317+
318+
if retry_state.next_attempt_requested is False:
319+
resp = AuditLogsResponse(
320+
url=url,
321+
status_code=res.status,
322+
raw_body=response_body,
323+
headers=res.headers,
324+
)
325+
_debug_log_response(self.logger, resp)
326+
return resp
327+
328+
except Exception as e:
329+
last_error = e
330+
for handler in self.retry_handlers:
331+
if await handler.can_retry_async(
332+
state=retry_state,
333+
request=retry_request,
334+
response=retry_response,
335+
error=e,
336+
):
337+
if self.logger.level <= logging.DEBUG:
338+
self.logger.info(
339+
f"A retry handler found: {type(handler).__name__} "
340+
f"for {http_verb} {url} - {e}"
341+
)
342+
await handler.prepare_for_next_attempt_async(
343+
state=retry_state,
344+
request=retry_request,
345+
response=retry_response,
346+
error=e,
347+
)
348+
break
349+
350+
if retry_state.next_attempt_requested is False:
351+
raise last_error
352+
353+
if resp is not None:
354+
return resp
355+
raise last_error
356+
272357
finally:
273358
if not use_running_session:
274359
await session.close()

0 commit comments

Comments
 (0)