Skip to content

Commit 51117e3

Browse files
authored
Handle rate limiting on RTM connections (#306)
* Added automatic retry for rtm.connect/rtm.start calls * Improved error handling and added debug output
1 parent ca3c493 commit 51117e3

File tree

3 files changed

+55
-28
lines changed

3 files changed

+55
-28
lines changed

slackclient/server.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ def __init__(self, token, connect=True, proxies=None):
3838
self.websocket = None
3939
self.ws_url = None
4040
self.connected = False
41-
self.last_connected_at = 0
4241
self.auto_reconnect = False
43-
self.reconnect_attempt = 0
42+
self.last_connected_at = 0
43+
self.reconnect_count = 0
44+
self.rtm_connect_retries = 0
4445

4546
# Connect to RTM on load
4647
if connect:
@@ -90,7 +91,7 @@ def rtm_connect(self, reconnect=False, timeout=None, use_rtm_start=True, **kwarg
9091
9192
:Args:
9293
reconnect (boolean) Whether this method is being called to reconnect to RTM
93-
timeout (int): Timeout for Web API calls
94+
timeout (int): Stop waiting for Web API response after this many seconds
9495
use_rtm_start (boolean): `True` to connect using `rtm.start` or
9596
`False` to connect using`rtm.connect`
9697
https://api.slack.com/rtm#connecting_with_rtm.connect_vs._rtm.start
@@ -104,34 +105,42 @@ def rtm_connect(self, reconnect=False, timeout=None, use_rtm_start=True, **kwarg
104105
connect_method = "rtm.start" if use_rtm_start else "rtm.connect"
105106

106107
# If the `auto_reconnect` param was passed, set the server's `auto_reconnect` attr
107-
if kwargs and kwargs["auto_reconnect"] is True:
108-
self.auto_reconnect = True
108+
if 'auto_reconnect' in kwargs:
109+
self.auto_reconnect = kwargs["auto_reconnect"]
109110

110111
# If this is an auto reconnect, rate limit reconnect attempts
111112
if self.auto_reconnect and reconnect:
112113
# Raise a SlackConnectionError after 5 retries within 3 minutes
113-
recon_attempt = self.reconnect_attempt
114-
if recon_attempt == 5:
114+
recon_count = self.reconnect_count
115+
if recon_count == 5:
115116
logging.error("RTM connection failed, reached max reconnects.")
116117
raise SlackConnectionError("RTM connection failed, reached max reconnects.")
117118
# Wait to reconnect if the last reconnect was more than 3 minutes ago
118119
if (time.time() - self.last_connected_at) < 180:
119-
if recon_attempt > 0:
120+
if recon_count > 0:
120121
# Back off after the the first attempt
121122
backoff_offset_multiplier = random.randint(1, 4)
122-
retry_timeout = (backoff_offset_multiplier * recon_attempt * recon_attempt)
123+
retry_timeout = (backoff_offset_multiplier * recon_count * recon_count)
123124
logging.debug("Reconnecting in %d seconds", retry_timeout)
124125

125126
time.sleep(retry_timeout)
126-
self.reconnect_attempt += 1
127+
self.reconnect_count += 1
127128
else:
128-
self.reconnect_attempt = 0
129+
self.reconnect_count = 0
129130

130131
reply = self.api_requester.do(self.token, connect_method, timeout=timeout, post_data=kwargs)
131132

132133
if reply.status_code != 200:
133-
raise SlackConnectionError(reply=reply)
134+
if self.rtm_connect_retries < 5 and reply.status_code == 429:
135+
self.rtm_connect_retries += 1
136+
retry_after = int(reply.headers.get('retry-after', 120))
137+
logging.debug("HTTP 429: Rate limited. Retrying in %d seconds", retry_after)
138+
time.sleep(retry_after)
139+
self.rtm_connect(reconnect=reconnect, timeout=timeout)
140+
else:
141+
raise SlackConnectionError("RTM connection attempt was rate limited 10 times.")
134142
else:
143+
self.rtm_connect_retries = 0
135144
login_data = reply.json()
136145
if login_data["ok"]:
137146
self.ws_url = login_data['url']

test_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
pytest<3.3
22
codecov
33
coverage
4+
mock
45
pytest-cov
56
pytest-mock
67
responses

tests/test_server.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
import requests
55
import responses
6+
from mock import patch
67
from slackclient.user import User
78
from slackclient.server import Server, SlackLoginError, SlackConnectionError
89
from slackclient.channel import Channel
@@ -51,25 +52,23 @@ def test_server_is_hashable(server):
5152
assert (server_map[server] == 'foo') is False
5253

5354

54-
def test_response_headers(server):
55+
@patch('time.sleep', return_value=None)
56+
def test_rate_limiting(patched_time_sleep, server):
5557
# Testing for rate limit retry headers
5658
with responses.RequestsMock() as rsps:
5759
rsps.add(
5860
responses.POST,
59-
"https://slack.com/api/auth.test",
61+
"https://slack.com/api/rtm.start",
6062
status=429,
6163
json={"ok": False},
6264
headers={'Retry-After': "1"}
6365
)
6466

65-
res = json.loads(server.api_call("auth.test"))
66-
67-
for call in rsps.calls:
68-
assert call.request.url in [
69-
"https://slack.com/api/auth.test"
70-
]
71-
assert call.response.status_code == 429
72-
assert res["headers"]['Retry-After'] == "1"
67+
with pytest.raises(SlackConnectionError) as e:
68+
server.rtm_connect()
69+
for call in rsps.calls:
70+
assert call.response.status_code == 429
71+
assert e.message == "RTM connection attempt was rate limited 10 times."
7372

7473

7574
def test_custom_agent(server):
@@ -147,6 +146,23 @@ def test_rtm_reconnect(server, rtm_start_fixture):
147146
]
148147

149148

149+
@patch('time.sleep', return_value=None)
150+
def test_rtm_max_reconnect_timeout(patched_time_sleep, server, rtm_start_fixture):
151+
with responses.RequestsMock() as rsps:
152+
rsps.add(
153+
responses.POST,
154+
"https://slack.com/api/rtm.connect",
155+
status=200,
156+
json=rtm_start_fixture
157+
)
158+
159+
server.reconnect_count = 4
160+
server.last_connected_at = time.time()
161+
server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)
162+
163+
assert server.reconnect_count == 5
164+
165+
150166
def test_rtm_reconnect_timeout_recently_connected(server, rtm_start_fixture):
151167
# If reconnected recently, server must wait to reconnect and increment the counter
152168
with responses.RequestsMock() as rsps:
@@ -157,11 +173,11 @@ def test_rtm_reconnect_timeout_recently_connected(server, rtm_start_fixture):
157173
json=rtm_start_fixture
158174
)
159175

160-
server.reconnect_attempt = 0
176+
server.reconnect_count = 0
161177
server.last_connected_at = time.time()
162178
server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)
163179

164-
assert server.reconnect_attempt == 1
180+
assert server.reconnect_count == 1
165181
for call in rsps.calls:
166182
assert call.request.url in [
167183
"https://slack.com/api/rtm.connect"
@@ -178,20 +194,21 @@ def test_rtm_reconnect_timeout_not_recently_connected(server, rtm_start_fixture)
178194
json=rtm_start_fixture
179195
)
180196

181-
server.reconnect_attempt = 1
197+
server.reconnect_count = 1
182198
server.last_connected_at = time.time() - 180
183199
server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)
184200

185-
assert server.reconnect_attempt == 0
201+
assert server.reconnect_count == 0
186202
for call in rsps.calls:
187203
assert call.request.url in [
188204
"https://slack.com/api/rtm.connect"
189205
]
190206

191207

192-
def test_max_rtm_reconnect_attempts(server):
208+
def test_max_rtm_reconnects(server, monkeypatch):
209+
monkeypatch.setattr("time.sleep", None)
193210
with pytest.raises(SlackConnectionError) as e:
194-
server.reconnect_attempt = 5
211+
server.reconnect_count = 5
195212
server.rtm_connect(auto_reconnect=True, reconnect=True, use_rtm_start=False)
196213
assert e.message == "RTM connection failed, reached max reconnects."
197214

0 commit comments

Comments
 (0)