Skip to content

Commit 07cce9b

Browse files
authored
Auto reconnect RTM (slackapi#297)
* Added reconnect logic to RTM client * Added a lot more tests * updated RTM test fixture * Disable testing for 3.6 on Windows until 3.6.5 release due to a windows web socket bug: https://bugs.python.org/issue32394
1 parent 1e841a3 commit 07cce9b

File tree

9 files changed

+328
-53
lines changed

9 files changed

+328
-53
lines changed

.appveyor.yml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ environment:
1010
PYTHON_VERSION: "py34-x86"
1111
- PYTHON: "C:\\Python35"
1212
PYTHON_VERSION: "py35-x86"
13-
- PYTHON: "C:\\Python36"
14-
PYTHON_VERSION: "py36-x86"
1513
- PYTHON: "C:\\Python27-x64"
1614
PYTHON_VERSION: "py27-x64"
1715
- PYTHON: "C:\\Python33-x64"
@@ -20,8 +18,6 @@ environment:
2018
PYTHON_VERSION: "py34-x64"
2119
- PYTHON: "C:\\Python35-x64"
2220
PYTHON_VERSION: "py35-x64"
23-
- PYTHON: "C:\\Python36-x64"
24-
PYTHON_VERSION: "py36-x64"
2521

2622
install:
2723
- "%PYTHON%\\python.exe -m pip install wheel"

docs-src/real_time_messaging.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Connecting to the Real Time Messaging API
2323
sc = SlackClient(slack_token)
2424

2525
if sc.rtm_connect():
26-
while True:
26+
while sc.server.connected is True:
2727
print sc.rtm_read()
2828
time.sleep(1)
2929
else:
@@ -70,6 +70,9 @@ To do this, simply pass `with_team_state=False` into the `rtm_connect` call, lik
7070
print "Connection Failed"
7171

7272

73+
Passing `auto_reconnect=True` will tell the websocket client to automatically reconnect if the connection gets dropped.
74+
75+
7376
See the `rtm.start docs <https://api.slack.com/methods/rtm.start>`_ and the `rtm.connect docs <https://api.slack.com/methods/rtm.connect>`_
7477
for more details.
7578

docs/real_time_messaging.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ <h2>Connecting to the Real Time Messaging API<a class="headerlink" href="#connec
155155
<span class="n">sc</span> <span class="o">=</span> <span class="n">SlackClient</span><span class="p">(</span><span class="n">slack_token</span><span class="p">)</span>
156156

157157
<span class="k">if</span> <span class="n">sc</span><span class="o">.</span><span class="n">rtm_connect</span><span class="p">():</span>
158-
<span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
158+
<span class="k">while</span> <span class="n">sc</span><span class="o">.</span><span class="n">server</span><span class="o">.</span><span class="n">connected</span> <span class="ow">is</span> <span class="bp">True</span><span class="p">:</span>
159159
<span class="k">print</span> <span class="n">sc</span><span class="o">.</span><span class="n">rtm_read</span><span class="p">()</span>
160160
<span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
161161
<span class="k">else</span><span class="p">:</span>
@@ -198,6 +198,7 @@ <h2>rtm.start vs rtm.connect<a class="headerlink" href="#rtm-start-vs-rtm-connec
198198
<span class="k">print</span> <span class="s2">&quot;Connection Failed&quot;</span>
199199
</pre></div>
200200
</div>
201+
<p>Passing <cite>auto_reconnect=True</cite> will tell the websocket client to automatically reconnect if the connection gets dropped.</p>
201202
<p>See the <a class="reference external" href="https://api.slack.com/methods/rtm.start">rtm.start docs</a> and the <a class="reference external" href="https://api.slack.com/methods/rtm.connect">rtm.connect docs</a>
202203
for more details.</p>
203204
</div>

docs/searchindex.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

slackclient/client.py

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def rtm_connect(self, with_team_state=True, **kwargs):
5050

5151
try:
5252
self.server.rtm_connect(use_rtm_start=with_team_state, **kwargs)
53-
return True
53+
return self.server.connected
5454
except Exception:
5555
traceback.print_exc()
5656
return False
@@ -90,24 +90,14 @@ def api_call(self, method, timeout=None, **kwargs):
9090
result = json.loads(response_body)
9191
except ValueError as json_decode_error:
9292
raise ParseResponseError(response_body, json_decode_error)
93-
if self.server:
93+
94+
if "ok" in result and result["ok"]:
9495
if method == 'im.open':
95-
if "ok" in result and result["ok"]:
96-
self.server.attach_channel(kwargs["user"], result["channel"]["id"])
96+
self.server.attach_channel(kwargs["user"], result["channel"]["id"])
9797
elif method in ('mpim.open', 'groups.create', 'groups.createchild'):
98-
if "ok" in result and result["ok"]:
99-
self.server.attach_channel(
100-
result['group']['name'],
101-
result['group']['id'],
102-
result['group']['members']
103-
)
98+
self.server.parse_channel_data([result['group']])
10499
elif method in ('channels.create', 'channels.join'):
105-
if 'ok' in result and result['ok']:
106-
self.server.attach_channel(
107-
result['channel']['name'],
108-
result['channel']['id'],
109-
result['channel']['members']
110-
)
100+
self.server.parse_channel_data([result['channel']])
111101
return result
112102

113103
def rtm_read(self):

slackclient/server.py

Lines changed: 77 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
from .slackrequest import SlackRequest
2-
from requests.packages.urllib3.util.url import parse_url
31
from .channel import Channel
2+
from .exceptions import SlackClientError
3+
from .slackrequest import SlackRequest
44
from .user import User
55
from .util import SearchList, SearchDict
6-
from .exceptions import SlackClientError
7-
from ssl import SSLError
86

9-
from websocket import create_connection
107
import json
8+
import logging
9+
import time
10+
import random
11+
12+
from requests.packages.urllib3.util.url import parse_url
13+
from ssl import SSLError
14+
from websocket import create_connection
15+
from websocket._exceptions import WebSocketConnectionClosedException
1116

1217

1318
class Server(object):
@@ -17,18 +22,27 @@ class Server(object):
1722
1823
"""
1924
def __init__(self, token, connect=True, proxies=None):
25+
# Slack client configs
2026
self.token = token
27+
self.proxies = proxies
28+
self.api_requester = SlackRequest(proxies=proxies)
29+
30+
# Workspace metadata
2131
self.username = None
2232
self.domain = None
2333
self.login_data = None
24-
self.websocket = None
2534
self.users = SearchDict()
2635
self.channels = SearchList()
27-
self.connected = False
36+
37+
# RTM configs
38+
self.websocket = None
2839
self.ws_url = None
29-
self.proxies = proxies
30-
self.api_requester = SlackRequest(proxies=proxies)
40+
self.connected = False
41+
self.last_connected_at = 0
42+
self.auto_reconnect = False
43+
self.reconnect_attempt = 0
3144

45+
# Connect to RTM on load
3246
if connect:
3347
self.rtm_connect()
3448

@@ -68,8 +82,51 @@ def append_user_agent(self, name, version):
6882
self.api_requester.append_user_agent(name, version)
6983

7084
def rtm_connect(self, reconnect=False, timeout=None, use_rtm_start=True, **kwargs):
85+
"""
86+
Connects to the RTM API - https://api.slack.com/rtm
87+
88+
If `auto_reconnect` is set to `True` then the SlackClient is initialized, this method
89+
will be used to reconnect on websocket read failures, which indicate disconnection
90+
91+
:Args:
92+
reconnect (boolean) Whether this method is being called to reconnect to RTM
93+
timeout (int): Timeout for Web API calls
94+
use_rtm_start (boolean): `True` to connect using `rtm.start` or
95+
`False` to connect using`rtm.connect`
96+
https://api.slack.com/rtm#connecting_with_rtm.connect_vs._rtm.start
97+
98+
:Returns:
99+
None
100+
101+
"""
102+
71103
# rtm.start returns user and channel info, rtm.connect does not.
72104
connect_method = "rtm.start" if use_rtm_start else "rtm.connect"
105+
106+
# 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
109+
110+
# If this is an auto reconnect, rate limit reconnect attempts
111+
if self.auto_reconnect and reconnect:
112+
# Raise a SlackConnectionError after 5 retries within 3 minutes
113+
recon_attempt = self.reconnect_attempt
114+
if recon_attempt == 5:
115+
logging.error("RTM connection failed, reached max reconnects.")
116+
raise SlackConnectionError("RTM connection failed, reached max reconnects.")
117+
# Wait to reconnect if the last reconnect was more than 3 minutes ago
118+
if (time.time() - self.last_connected_at) < 180:
119+
if recon_attempt > 0:
120+
# Back off after the the first attempt
121+
backoff_offset_multiplier = random.randint(1, 4)
122+
retry_timeout = (backoff_offset_multiplier * recon_attempt * recon_attempt)
123+
logging.debug("Reconnecting in %d seconds", retry_timeout)
124+
125+
time.sleep(retry_timeout)
126+
self.reconnect_attempt += 1
127+
else:
128+
self.reconnect_attempt = 0
129+
73130
reply = self.api_requester.do(self.token, connect_method, timeout=timeout, post_data=kwargs)
74131

75132
if reply.status_code != 200:
@@ -111,8 +168,12 @@ def connect_slack_websocket(self, ws_url):
111168
http_proxy_host=proxy_host,
112169
http_proxy_port=proxy_port,
113170
http_proxy_auth=proxy_auth)
171+
self.connected = True
172+
self.last_connected_at = time.time()
173+
logging.debug("RTM connected")
114174
self.websocket.sock.setblocking(0)
115175
except Exception as e:
176+
self.connected = False
116177
raise SlackConnectionError(message=str(e))
117178

118179
def parse_channel_data(self, channel_data):
@@ -202,6 +263,13 @@ def websocket_safe_read(self):
202263
# SSLWantReadError
203264
return ''
204265
raise
266+
except WebSocketConnectionClosedException as e:
267+
logging.debug("RTM disconnected")
268+
self.connected = False
269+
if self.auto_reconnect:
270+
self.rtm_connect(reconnect=True)
271+
else:
272+
raise SlackConnectionError("Unable to send due to closed RTM websocket")
205273
return data.rstrip()
206274

207275
def attach_user(self, name, user_id, real_name, tz, email):

tests/data/rtm.start.json

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,7 @@
245245
"deleted": false,
246246
"status": null,
247247
"color": "9f69e7",
248-
"real_name": "",
249-
"tz": "America\/Los_Angeles",
250-
"tz_label": "Pacific Daylight Time",
251-
"tz_offset": -25200,
252248
"profile": {
253-
"real_name": "",
254-
"real_name_normalized": "",
255249
"email": "fakeuser@example.com",
256250
"image_24": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png",
257251
"image_32": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png",
@@ -268,6 +262,28 @@
268262
"has_files": false,
269263
"presence": "away"
270264
},
265+
{
266+
"id": "U10CX1235",
267+
"name": "userwithoutemail",
268+
"deleted": false,
269+
"status": null,
270+
"color": "9f69e7",
271+
"profile": {
272+
"image_24": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png",
273+
"image_32": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png",
274+
"image_48": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0002-48.png",
275+
"image_72": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-72.png",
276+
"image_192": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002.png"
277+
},
278+
"is_admin": true,
279+
"is_owner": true,
280+
"is_primary_owner": true,
281+
"is_restricted": false,
282+
"is_ultra_restricted": false,
283+
"is_bot": false,
284+
"has_files": false,
285+
"presence": "away"
286+
},
271287
{
272288
"id": "USLACKBOT",
273289
"name": "slackbot",
@@ -301,5 +317,5 @@
301317
],
302318
"bots": [],
303319
"cache_version": "v5-dog",
304-
"url": "wss:\/\/ms9999.slack-msgs.com\/websocket\/rvyiQ_oxNhQ2C6_613rtqs1PFfT0AmivZTokv\/VOVQCmq3bk\/KarC2Z2ZMFfdMMtxn4kx9ILl6sE7JgvKv6Bct5okT0Lgru416DXsKJolJQ="
320+
"url": "wss:\/\/cerberus-xxxx.lb.slack-msgs.com\/websocket\/ifkp3MKfNXd6ftbrEGllwcHn"
305321
}

0 commit comments

Comments
 (0)