Skip to content

Commit 8e2df24

Browse files
authored
Merge pull request #262 from dblock/async-disconnect
Closes #257
2 parents a9dabc5 + cf6d601 commit 8e2df24

File tree

13 files changed

+147
-71
lines changed

13 files changed

+147
-71
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ Gemfile.lock
44
.DS_Store
55
.bundle
66
.idea
7+
.rspec_status

.rubocop_todo.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2019-01-19 17:37:31 -0500 using RuboCop version 0.61.1.
3+
# on 2019-04-07 11:03:29 -0400 using RuboCop version 0.61.1.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

9+
# Offense count: 2
10+
# Configuration parameters: AllowSafeAssignment.
11+
Lint/AssignmentInCondition:
12+
Exclude:
13+
- 'lib/slack/real_time/concurrency/async.rb'
14+
- 'lib/slack/real_time/socket.rb'
15+
916
# Offense count: 4
1017
Lint/HandleExceptions:
1118
Exclude:
@@ -45,10 +52,11 @@ Style/AccessModifierDeclarations:
4552
Style/GlobalVars:
4653
Enabled: false
4754

48-
# Offense count: 2
55+
# Offense count: 3
4956
# Configuration parameters: MinBodyLength.
5057
Style/GuardClause:
5158
Exclude:
59+
- 'lib/slack/real_time/socket.rb'
5260
- 'lib/slack/real_time/stores/store.rb'
5361
- 'lib/slack/web/faraday/response/raise_error.rb'
5462

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### 0.14.2 (Next)
22

33
* [#256](https://github.com/slack-ruby/slack-ruby-client/pull/256): Added support for specifying signing secrets on a per-request basis via optional parameters to the `Slack::Events::Request` constructor - [@gabrielmdeal](https://github.com/gabrielmdeal).
4+
* [#257](https://github.com/slack-ruby/slack-ruby-client/pull/257), [#262](https://github.com/slack-ruby/slack-ruby-client/pull/262): Fixed occasional failures to reconnect - [@ioquatix](https://github.com/ioquatix), [@dblock](https://github.com/dblock).
45
* Your contribution here.
56

67
### 0.14.1 (2019/2/26)

Rakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ end
1414
require 'rubocop/rake_task'
1515
RuboCop::RakeTask.new
1616

17-
task default: %i[rubocop spec]
17+
task default: %i[spec rubocop]
1818

1919
load 'tasks/git.rake'
2020
load 'tasks/web.rake'

lib/slack/real_time/client.rb

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ def config
8181

8282
def run_loop
8383
@socket.connect! do |driver|
84-
@callback.call(driver) if @callback
85-
8684
driver.on :open do |event|
8785
logger.debug("#{self.class}##{__method__}") { event.class.name }
8886
open(event)
@@ -100,16 +98,35 @@ def run_loop
10098
close(event)
10199
callback(event, :closed)
102100
end
101+
102+
# This must be called last to ensure any events are registered before invoking user code.
103+
@callback.call(driver) if @callback
103104
end
104105
end
105106

106-
def run_ping!
107+
# Ensure the server is running, and ping the remote server if no other messages were sent.
108+
def keep_alive?
109+
# We can't ping the remote server if we aren't connected.
110+
return false if @socket.nil? || !@socket.connected?
111+
107112
time_since_last_message = @socket.time_since_last_message
108-
return if time_since_last_message < websocket_ping
109-
raise Slack::RealTime::Client::ClientNotStartedError if !@socket.connected? || time_since_last_message > (websocket_ping * 2)
110113

114+
# If the server responded within the specified time, we are okay:
115+
return true if time_since_last_message < websocket_ping
116+
117+
# If the server has not responded for a while:
118+
return false if time_since_last_message > (websocket_ping * 2)
119+
120+
# Kick off the next ping message:
111121
ping
112-
rescue Slack::RealTime::Client::ClientNotStartedError
122+
123+
true
124+
end
125+
126+
# Check if the remote server is responsive, and if not, restart the connection.
127+
def run_ping!
128+
return if keep_alive?
129+
113130
restart_async
114131
end
115132

lib/slack/real_time/concurrency/async.rb

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
require 'async/websocket'
2+
require 'async/notification'
23
require 'async/clock'
34

45
module Slack
56
module RealTime
67
module Concurrency
78
module Async
8-
class Reactor < ::Async::Reactor
9-
def_delegators :@timers, :cancel
10-
end
11-
129
class Client < ::Async::WebSocket::Client
1310
extend ::Forwardable
1411
def_delegators :@driver, :on, :text, :binary, :emit
@@ -17,35 +14,57 @@ class Client < ::Async::WebSocket::Client
1714
class Socket < Slack::RealTime::Socket
1815
attr_reader :client
1916

17+
def start_sync(client)
18+
start_reactor(client).wait
19+
end
20+
2021
def start_async(client)
21-
@reactor = Reactor.new
2222
Thread.new do
23+
start_reactor(client)
24+
end
25+
end
26+
27+
def start_reactor(client)
28+
Async do |task|
29+
@restart = ::Async::Notification.new
30+
2331
if client.run_ping?
24-
@reactor.every(client.websocket_ping) do
25-
client.run_ping!
32+
@ping_task = task.async do |subtask|
33+
subtask.annotate 'client keep-alive'
34+
35+
# The timer task will naturally exit after the driver is set to nil.
36+
while @restart
37+
subtask.sleep client.websocket_ping
38+
client.run_ping! if @restart
39+
end
2640
end
2741
end
28-
@reactor.run do |task|
29-
task.async do
30-
client.run_loop
42+
43+
while @restart
44+
@client_task.stop if @client_task
45+
46+
@client_task = task.async do |subtask|
47+
begin
48+
subtask.annotate 'client run-loop'
49+
client.run_loop
50+
rescue ::Async::Wrapper::Cancelled => e
51+
# Will get restarted by ping worker.
52+
client.logger.warn(subtask.to_s) { e.message }
53+
end
3154
end
55+
56+
@restart.wait
3257
end
58+
59+
@ping_task.stop if @ping_task
3360
end
3461
end
3562

36-
def restart_async(client, new_url)
63+
def restart_async(_client, new_url)
3764
@url = new_url
3865
@last_message_at = current_time
39-
return unless @reactor
4066

41-
@reactor.async do
42-
client.run_loop
43-
end
44-
end
45-
46-
def disconnect!
47-
super
48-
@reactor.cancel
67+
@restart.signal if @restart
4968
end
5069

5170
def current_time
@@ -57,14 +76,27 @@ def connect!
5776
run_loop
5877
end
5978

79+
# Kill the restart/ping loop.
80+
def disconnect!
81+
super
82+
ensure
83+
if restart = @restart
84+
@restart = nil
85+
restart.signal
86+
end
87+
end
88+
89+
# Close the socket.
6090
def close
61-
@closing = true
62-
@driver.close if @driver
6391
super
92+
ensure
93+
if @socket
94+
@socket.close
95+
@socket = nil
96+
end
6497
end
6598

6699
def run_loop
67-
@closing = false
68100
while @driver && @driver.next_event
69101
# $stderr.puts event.inspect
70102
end

lib/slack/real_time/concurrency/celluloid.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ def disconnect!
4646

4747
def close
4848
@closing = true
49-
driver.close if driver
5049
super
5150
end
5251

lib/slack/real_time/concurrency/eventmachine.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,6 @@ def disconnect!
5656
@thread = nil
5757
end
5858

59-
def close
60-
driver.close if driver
61-
super
62-
end
63-
6459
def send_data(message)
6560
logger.debug("#{self.class}##{__method__}") { message }
6661
driver.send(message)

lib/slack/real_time/socket.rb

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ def initialize(url, options = {})
1818
def send_data(message)
1919
logger.debug("#{self.class}##{__method__}") { message }
2020
case message
21-
when Numeric then driver.text(message.to_s)
22-
when String then driver.text(message)
23-
when Array then driver.binary(message)
21+
when Numeric then @driver.text(message.to_s)
22+
when String then @driver.text(message)
23+
when Array then @driver.binary(message)
2424
else false
2525
end
2626
end
@@ -29,21 +29,24 @@ def connect!
2929
return if connected?
3030

3131
connect
32-
logger.debug("#{self.class}##{__method__}") { driver.class }
32+
logger.debug("#{self.class}##{__method__}") { @driver.class }
3333

34-
driver.on :message do
34+
@driver.on :message do
3535
@last_message_at = current_time
3636
end
3737

38-
yield driver if block_given?
38+
yield @driver if block_given?
3939
end
4040

41+
# Gracefully shut down the connection.
4142
def disconnect!
42-
driver.close
43+
@driver.close
44+
ensure
45+
close
4346
end
4447

4548
def connected?
46-
!driver.nil?
49+
!@driver.nil?
4750
end
4851

4952
def start_sync(client)
@@ -73,7 +76,11 @@ def current_time
7376
end
7477

7578
def close
76-
@driver = nil
79+
# When you call `driver.emit(:close)`, it will typically end up calling `client.close` which will call `@socket.close` and end up back here. In order to break this infinite recursion, we check and set `@driver = nil` before invoking `client.close`.
80+
if driver = @driver
81+
@driver = nil
82+
driver.emit(:close)
83+
end
7784
end
7885

7986
protected

0 commit comments

Comments
 (0)