actioncable 7.0.4 → 7.1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -51
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +5 -5
  5. data/app/assets/javascripts/action_cable.js +27 -6
  6. data/app/assets/javascripts/actioncable.esm.js +27 -6
  7. data/app/assets/javascripts/actioncable.js +27 -6
  8. data/lib/action_cable/channel/base.rb +17 -5
  9. data/lib/action_cable/channel/broadcasting.rb +3 -1
  10. data/lib/action_cable/channel/callbacks.rb +37 -0
  11. data/lib/action_cable/channel/naming.rb +4 -2
  12. data/lib/action_cable/channel/streams.rb +2 -0
  13. data/lib/action_cable/channel/test_case.rb +9 -4
  14. data/lib/action_cable/connection/authorization.rb +1 -1
  15. data/lib/action_cable/connection/base.rb +16 -3
  16. data/lib/action_cable/connection/callbacks.rb +55 -0
  17. data/lib/action_cable/connection/internal_channel.rb +3 -1
  18. data/lib/action_cable/connection/stream.rb +1 -3
  19. data/lib/action_cable/connection/stream_event_loop.rb +0 -1
  20. data/lib/action_cable/connection/subscriptions.rb +2 -0
  21. data/lib/action_cable/connection/tagged_logger_proxy.rb +6 -4
  22. data/lib/action_cable/connection/test_case.rb +3 -1
  23. data/lib/action_cable/connection/web_socket.rb +2 -0
  24. data/lib/action_cable/deprecator.rb +7 -0
  25. data/lib/action_cable/engine.rb +13 -5
  26. data/lib/action_cable/gem_version.rb +4 -4
  27. data/lib/action_cable/remote_connections.rb +11 -2
  28. data/lib/action_cable/server/base.rb +3 -0
  29. data/lib/action_cable/server/broadcasting.rb +2 -0
  30. data/lib/action_cable/server/configuration.rb +12 -2
  31. data/lib/action_cable/server/connections.rb +3 -1
  32. data/lib/action_cable/server/worker.rb +0 -1
  33. data/lib/action_cable/subscription_adapter/async.rb +0 -2
  34. data/lib/action_cable/subscription_adapter/postgresql.rb +0 -1
  35. data/lib/action_cable/subscription_adapter/redis.rb +56 -10
  36. data/lib/action_cable/subscription_adapter/test.rb +3 -5
  37. data/lib/action_cable/test_helper.rb +46 -19
  38. data/lib/action_cable/version.rb +1 -1
  39. data/lib/action_cable.rb +24 -12
  40. data/lib/rails/generators/channel/USAGE +14 -8
  41. data/lib/rails/generators/channel/channel_generator.rb +21 -7
  42. metadata +29 -17
  43. data/lib/action_cable/channel.rb +0 -17
  44. data/lib/action_cable/connection.rb +0 -22
  45. data/lib/action_cable/server.rb +0 -16
  46. data/lib/action_cable/subscription_adapter.rb +0 -12
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Channel
5
+ # = Action Cable \Channel \Streams
6
+ #
5
7
  # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data
6
8
  # placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
7
9
  # streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent.
@@ -15,6 +15,8 @@ module ActionCable
15
15
  end
16
16
  end
17
17
 
18
+ # = Action Cable \Channel Stub
19
+ #
18
20
  # Stub +stream_from+ to track streams for the channel.
19
21
  # Add public aliases for +subscription_confirmation_sent?+ and
20
22
  # +subscription_rejected?+.
@@ -45,9 +47,12 @@ module ActionCable
45
47
  end
46
48
 
47
49
  class ConnectionStub
48
- attr_reader :transmissions, :identifiers, :subscriptions, :logger
50
+ attr_reader :server, :transmissions, :identifiers, :subscriptions, :logger
51
+
52
+ delegate :pubsub, :config, to: :server
49
53
 
50
54
  def initialize(identifiers = {})
55
+ @server = ActionCable.server
51
56
  @transmissions = []
52
57
 
53
58
  identifiers.each do |identifier, val|
@@ -135,11 +140,11 @@ module ActionCable
135
140
  # ActionCable::Channel::TestCase will also automatically provide the following instance
136
141
  # methods for use in the tests:
137
142
  #
138
- # <b>connection</b>::
143
+ # connection::
139
144
  # An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
140
- # <b>subscription</b>::
145
+ # subscription::
141
146
  # An instance of the current channel, created when you call +subscribe+.
142
- # <b>transmissions</b>::
147
+ # transmissions::
143
148
  # A list of all messages that have been transmitted into the channel.
144
149
  #
145
150
  #
@@ -5,7 +5,7 @@ module ActionCable
5
5
  module Authorization
6
6
  class UnauthorizedError < StandardError; end
7
7
 
8
- # Closes the WebSocket connection if it is open and returns a 404 "File not Found" response.
8
+ # Closes the WebSocket connection if it is open and returns an "unauthorized" reason.
9
9
  def reject_unauthorized_connection
10
10
  logger.error "An unauthorized connection attempt was rejected"
11
11
  raise UnauthorizedError
@@ -5,6 +5,8 @@ require "active_support/rescuable"
5
5
 
6
6
  module ActionCable
7
7
  module Connection
8
+ # = Action Cable \Connection \Base
9
+ #
8
10
  # For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
9
11
  # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
10
12
  # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
@@ -47,10 +49,11 @@ module ActionCable
47
49
  include Identification
48
50
  include InternalChannel
49
51
  include Authorization
52
+ include Callbacks
50
53
  include ActiveSupport::Rescuable
51
54
 
52
55
  attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
53
- delegate :event_loop, :pubsub, to: :server
56
+ delegate :event_loop, :pubsub, :config, to: :server
54
57
 
55
58
  def initialize(server, env, coder: ActiveSupport::JSON)
56
59
  @server, @env, @coder = server, env, coder
@@ -86,12 +89,18 @@ module ActionCable
86
89
 
87
90
  def dispatch_websocket_message(websocket_message) # :nodoc:
88
91
  if websocket.alive?
89
- subscriptions.execute_command decode(websocket_message)
92
+ handle_channel_command decode(websocket_message)
90
93
  else
91
94
  logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
92
95
  end
93
96
  end
94
97
 
98
+ def handle_channel_command(payload)
99
+ run_callbacks :command do
100
+ subscriptions.execute_command payload
101
+ end
102
+ end
103
+
95
104
  def transmit(cable_message) # :nodoc:
96
105
  websocket.transmit encode(cable_message)
97
106
  end
@@ -143,6 +152,10 @@ module ActionCable
143
152
  send_async :handle_close
144
153
  end
145
154
 
155
+ def inspect # :nodoc:
156
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
157
+ end
158
+
146
159
  private
147
160
  attr_reader :websocket
148
161
  attr_reader :message_buffer
@@ -222,7 +235,7 @@ module ActionCable
222
235
 
223
236
  logger.error invalid_request_message
224
237
  logger.info finished_request_message
225
- [ 404, { "Content-Type" => "text/plain" }, [ "Page not found" ] ]
238
+ [ 404, { Rack::CONTENT_TYPE => "text/plain; charset=utf-8" }, [ "Page not found" ] ]
226
239
  end
227
240
 
228
241
  # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+
5
+ module ActionCable
6
+ module Connection
7
+ # = Action Cable \Connection \Callbacks
8
+ #
9
+ # The {before_command}[rdoc-ref:ClassMethods#before_command],
10
+ # {after_command}[rdoc-ref:ClassMethods#after_command], and
11
+ # {around_command}[rdoc-ref:ClassMethods#around_command] callbacks are
12
+ # invoked when sending commands to the client, such as when subscribing,
13
+ # unsubscribing, or performing an action.
14
+ #
15
+ # ==== Example
16
+ #
17
+ # module ApplicationCable
18
+ # class Connection < ActionCable::Connection::Base
19
+ # identified_by :user
20
+ #
21
+ # around_command :set_current_account
22
+ #
23
+ # private
24
+ #
25
+ # def set_current_account
26
+ # # Now all channels could use Current.account
27
+ # Current.set(account: user.account) { yield }
28
+ # end
29
+ # end
30
+ # end
31
+ #
32
+ module Callbacks
33
+ extend ActiveSupport::Concern
34
+ include ActiveSupport::Callbacks
35
+
36
+ included do
37
+ define_callbacks :command
38
+ end
39
+
40
+ module ClassMethods
41
+ def before_command(*methods, &block)
42
+ set_callback(:command, :before, *methods, &block)
43
+ end
44
+
45
+ def after_command(*methods, &block)
46
+ set_callback(:command, :after, *methods, &block)
47
+ end
48
+
49
+ def around_command(*methods, &block)
50
+ set_callback(:command, :around, *methods, &block)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Connection
5
+ # = Action Cable \InternalChannel
6
+ #
5
7
  # Makes it possible for the RemoteConnection to disconnect a specific connection.
6
8
  module InternalChannel
7
9
  extend ActiveSupport::Concern
@@ -32,7 +34,7 @@ module ActionCable
32
34
  case message["type"]
33
35
  when "disconnect"
34
36
  logger.info "Removing connection (#{connection_identifier})"
35
- websocket.close
37
+ close(reason: ActionCable::INTERNAL[:disconnect_reasons][:remote], reconnect: message.fetch("reconnect", true))
36
38
  end
37
39
  rescue Exception => e
38
40
  logger.error "There was an exception - #{e.class}(#{e.message})"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
-
5
3
  module ActionCable
6
4
  module Connection
7
5
  #--
@@ -100,7 +98,7 @@ module ActionCable
100
98
 
101
99
  # This should return the underlying io according to the SPEC:
102
100
  @rack_hijack_io = @socket_object.env["rack.hijack"].call
103
- # Retain existing behaviour if required:
101
+ # Retain existing behavior if required:
104
102
  @rack_hijack_io ||= @socket_object.env["rack.hijack_io"]
105
103
 
106
104
  @event_loop.attach(@rack_hijack_io, self)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "nio"
4
- require "thread"
5
4
 
6
5
  module ActionCable
7
6
  module Connection
@@ -4,6 +4,8 @@ require "active_support/core_ext/hash/indifferent_access"
4
4
 
5
5
  module ActionCable
6
6
  module Connection
7
+ # = Action Cable \Connection \Subscriptions
8
+ #
7
9
  # Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on
8
10
  # the connection to the proper channel.
9
11
  class Subscriptions # :nodoc:
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Connection
5
+ # = Action Cable \Connection \TaggedLoggerProxy
6
+ #
5
7
  # Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
6
8
  # ActiveSupport::TaggedLogging enhanced Rails.logger, as that logger will reset the tags between requests.
7
9
  # The connection is long-lived, so it needs its own set of tags for its independent duration.
@@ -28,14 +30,14 @@ module ActionCable
28
30
  end
29
31
 
30
32
  %i( debug info warn error fatal unknown ).each do |severity|
31
- define_method(severity) do |message|
32
- log severity, message
33
+ define_method(severity) do |message = nil, &block|
34
+ log severity, message, &block
33
35
  end
34
36
  end
35
37
 
36
38
  private
37
- def log(type, message) # :doc:
38
- tag(@logger) { @logger.send type, message }
39
+ def log(type, message, &block) # :doc:
40
+ tag(@logger) { @logger.send type, message, &block }
39
41
  end
40
42
  end
41
43
  end
@@ -56,6 +56,8 @@ module ActionCable
56
56
  end
57
57
  end
58
58
 
59
+ # = Action Cable \Connection \TestCase
60
+ #
59
61
  # Unit test Action Cable connections.
60
62
  #
61
63
  # Useful to check whether a connection's +identified_by+ gets assigned properly
@@ -116,7 +118,7 @@ module ActionCable
116
118
  # assert_equal "1", connection.user_id
117
119
  # end
118
120
  #
119
- # == Connection is automatically inferred
121
+ # == \Connection is automatically inferred
120
122
  #
121
123
  # ActionCable::Connection::TestCase will automatically infer the connection under test
122
124
  # from the test class name. If the channel cannot be inferred from the test
@@ -4,6 +4,8 @@ require "websocket/driver"
4
4
 
5
5
  module ActionCable
6
6
  module Connection
7
+ # = Action Cable \Connection \WebSocket
8
+ #
7
9
  # Wrap the real socket to minimize the externally-presented API
8
10
  class WebSocket # :nodoc:
9
11
  def initialize(env, event_target, event_loop, protocols: ActionCable::INTERNAL[:protocols])
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ def self.deprecator # :nodoc:
5
+ @deprecator ||= ActiveSupport::Deprecation.new
6
+ end
7
+ end
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "rails"
4
4
  require "action_cable"
5
- require "action_cable/helpers/action_cable_helper"
6
5
  require "active_support/core_ext/hash/indifferent_access"
7
6
 
8
7
  module ActionCable
@@ -11,7 +10,9 @@ module ActionCable
11
10
  config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
12
11
  config.action_cable.precompile_assets = true
13
12
 
14
- config.eager_load_namespaces << ActionCable
13
+ initializer "action_cable.deprecator", before: :load_environment_config do |app|
14
+ app.deprecators[:action_cable] = ActionCable.deprecator
15
+ end
15
16
 
16
17
  initializer "action_cable.helpers" do
17
18
  ActiveSupport.on_load(:action_view) do
@@ -23,6 +24,12 @@ module ActionCable
23
24
  ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
24
25
  end
25
26
 
27
+ initializer "action_cable.health_check_application" do
28
+ ActiveSupport.on_load(:action_cable) {
29
+ self.health_check_application = ->(env) { Rails::HealthController.action(:show).call(env) }
30
+ }
31
+ end
32
+
26
33
  initializer "action_cable.asset" do
27
34
  config.after_initialize do |app|
28
35
  if app.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
@@ -39,11 +46,12 @@ module ActionCable
39
46
 
40
47
  ActiveSupport.on_load(:action_cable) do
41
48
  if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
42
- self.cable = Rails.application.config_for(config_path).to_h.with_indifferent_access
49
+ self.cable = app.config_for(config_path).to_h.with_indifferent_access
43
50
  end
44
51
 
45
52
  previous_connection_class = connection_class
46
53
  self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
54
+ self.filter_parameters += app.config.filter_parameters
47
55
 
48
56
  options.each { |k, v| send("#{k}=", v) }
49
57
  end
@@ -63,7 +71,7 @@ module ActionCable
63
71
  initializer "action_cable.set_work_hooks" do |app|
64
72
  ActiveSupport.on_load(:action_cable) do
65
73
  ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
66
- app.executor.wrap do
74
+ app.executor.wrap(source: "application.action_cable") do
67
75
  # If we took a while to get the lock, we may have been halted
68
76
  # in the meantime. As we haven't started doing any real work
69
77
  # yet, we should pretend that we never made it off the queue.
@@ -74,7 +82,7 @@ module ActionCable
74
82
  end
75
83
 
76
84
  wrap = lambda do |_, inner|
77
- app.executor.wrap(&inner)
85
+ app.executor.wrap(source: "application.action_cable", &inner)
78
86
  end
79
87
  ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
80
88
  ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionCable
4
- # Returns the currently loaded version of Action Cable as a <tt>Gem::Version</tt>.
4
+ # Returns the currently loaded version of Action Cable as a +Gem::Version+.
5
5
  def self.gem_version
6
6
  Gem::Version.new VERSION::STRING
7
7
  end
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 7
11
- MINOR = 0
12
- TINY = 4
13
- PRE = nil
11
+ MINOR = 1
12
+ TINY = 5
13
+ PRE = "1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -3,6 +3,8 @@
3
3
  require "active_support/core_ext/module/redefine_method"
4
4
 
5
5
  module ActionCable
6
+ # = Action Cable Remote Connections
7
+ #
6
8
  # If you need to disconnect a given connection, you can go through the
7
9
  # RemoteConnections. You can find the connections you're looking for by
8
10
  # searching for the identifier declared on the connection. For example:
@@ -19,6 +21,11 @@ module ActionCable
19
21
  # This will disconnect all the connections established for
20
22
  # <tt>User.find(1)</tt>, across all servers running on all machines, because
21
23
  # it uses the internal channel that all of these servers are subscribed to.
24
+ #
25
+ # By default, server sends a "disconnect" message with "reconnect" flag set to true.
26
+ # You can override it by specifying the +reconnect+ option:
27
+ #
28
+ # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect(reconnect: false)
22
29
  class RemoteConnections
23
30
  attr_reader :server
24
31
 
@@ -31,6 +38,8 @@ module ActionCable
31
38
  end
32
39
 
33
40
  private
41
+ # = Action Cable Remote \Connection
42
+ #
34
43
  # Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
35
44
  # Exists solely for the purpose of calling #disconnect on that connection.
36
45
  class RemoteConnection
@@ -44,8 +53,8 @@ module ActionCable
44
53
  end
45
54
 
46
55
  # Uses the internal channel to disconnect the connection.
47
- def disconnect
48
- server.broadcast internal_channel, { type: "disconnect" }
56
+ def disconnect(reconnect: true)
57
+ server.broadcast internal_channel, { type: "disconnect", reconnect: reconnect }
49
58
  end
50
59
 
51
60
  # Returns all the identifiers that were applied to this connection.
@@ -4,6 +4,8 @@ require "monitor"
4
4
 
5
5
  module ActionCable
6
6
  module Server
7
+ # = Action Cable \Server \Base
8
+ #
7
9
  # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
8
10
  # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
9
11
  #
@@ -29,6 +31,7 @@ module ActionCable
29
31
 
30
32
  # Called by Rack to set up the server.
31
33
  def call(env)
34
+ return config.health_check_application.call(env) if env["PATH_INFO"] == config.health_check_path
32
35
  setup_heartbeat_timer
33
36
  config.connection_class.call.new(self, env).process
34
37
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Server
5
+ # = Action Cable \Server \Broadcasting
6
+ #
5
7
  # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
6
8
  # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
7
9
  #
@@ -1,15 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rack"
4
+
3
5
  module ActionCable
4
6
  module Server
7
+ # = Action Cable \Server \Configuration
8
+ #
5
9
  # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
6
- # in a Rails config initializer.
10
+ # in a \Rails config initializer.
7
11
  class Configuration
8
12
  attr_accessor :logger, :log_tags
9
13
  attr_accessor :connection_class, :worker_pool_size
10
- attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
14
+ attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host, :filter_parameters
11
15
  attr_accessor :cable, :url, :mount_path
12
16
  attr_accessor :precompile_assets
17
+ attr_accessor :health_check_path, :health_check_application
13
18
 
14
19
  def initialize
15
20
  @log_tags = []
@@ -19,6 +24,11 @@ module ActionCable
19
24
 
20
25
  @disable_request_forgery_protection = false
21
26
  @allow_same_origin_as_host = true
27
+ @filter_parameters = []
28
+
29
+ @health_check_application = ->(env) {
30
+ [200, { Rack::CONTENT_TYPE => "text/html", "date" => Time.now.httpdate }, []]
31
+ }
22
32
  end
23
33
 
24
34
  # Returns constant of subscription adapter specified in config/cable.yml.
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActionCable
4
4
  module Server
5
+ # = Action Cable \Server \Connections
6
+ #
5
7
  # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
6
8
  # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
7
9
  module Connections # :nodoc:
@@ -24,7 +26,7 @@ module ActionCable
24
26
  # disconnect.
25
27
  def setup_heartbeat_timer
26
28
  @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
27
- event_loop.post { connections.map(&:beat) }
29
+ event_loop.post { connections.each(&:beat) }
28
30
  end
29
31
  end
30
32
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "active_support/callbacks"
4
4
  require "active_support/core_ext/module/attribute_accessors_per_thread"
5
- require "action_cable/server/worker/active_record_connection_management"
6
5
  require "concurrent"
7
6
 
8
7
  module ActionCable
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_cable/subscription_adapter/inline"
4
-
5
3
  module ActionCable
6
4
  module SubscriptionAdapter
7
5
  class Async < Inline # :nodoc:
@@ -2,7 +2,6 @@
2
2
 
3
3
  gem "pg", "~> 1.1"
4
4
  require "pg"
5
- require "thread"
6
5
  require "openssl"
7
6
 
8
7
  module ActionCable
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thread"
4
-
5
- gem "redis", ">= 3", "< 6"
3
+ gem "redis", ">= 4", "< 6"
6
4
  require "redis"
7
5
 
8
6
  require "active_support/core_ext/hash/except"
@@ -46,7 +44,7 @@ module ActionCable
46
44
 
47
45
  private
48
46
  def listener
49
- @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
47
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, config_options, @server.event_loop) }
50
48
  end
51
49
 
52
50
  def redis_connection_for_broadcasts
@@ -56,11 +54,15 @@ module ActionCable
56
54
  end
57
55
 
58
56
  def redis_connection
59
- self.class.redis_connector.call(@server.config.cable.symbolize_keys.merge(id: identifier))
57
+ self.class.redis_connector.call(config_options)
58
+ end
59
+
60
+ def config_options
61
+ @config_options ||= @server.config.cable.deep_symbolize_keys.merge(id: identifier)
60
62
  end
61
63
 
62
64
  class Listener < SubscriberMap
63
- def initialize(adapter, event_loop)
65
+ def initialize(adapter, config_options, event_loop)
64
66
  super()
65
67
 
66
68
  @adapter = adapter
@@ -69,6 +71,11 @@ module ActionCable
69
71
  @subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
70
72
  @subscription_lock = Mutex.new
71
73
 
74
+ @reconnect_attempt = 0
75
+ # Use the same config as used by Redis conn
76
+ @reconnect_attempts = config_options.fetch(:reconnect_attempts, 1)
77
+ @reconnect_attempts = Array.new(@reconnect_attempts, 0) if @reconnect_attempts.is_a?(Integer)
78
+
72
79
  @subscribed_client = nil
73
80
 
74
81
  @when_connected = []
@@ -84,6 +91,7 @@ module ActionCable
84
91
  on.subscribe do |chan, count|
85
92
  @subscription_lock.synchronize do
86
93
  if count == 1
94
+ @reconnect_attempt = 0
87
95
  @subscribed_client = original_client
88
96
 
89
97
  until @when_connected.empty?
@@ -150,8 +158,16 @@ module ActionCable
150
158
  @thread ||= Thread.new do
151
159
  Thread.current.abort_on_exception = true
152
160
 
153
- conn = @adapter.redis_connection_for_subscriptions
154
- listen conn
161
+ begin
162
+ conn = @adapter.redis_connection_for_subscriptions
163
+ listen conn
164
+ rescue ConnectionError
165
+ reset
166
+ if retry_connecting?
167
+ when_connected { resubscribe }
168
+ retry
169
+ end
170
+ end
155
171
  end
156
172
  end
157
173
 
@@ -163,7 +179,36 @@ module ActionCable
163
179
  end
164
180
  end
165
181
 
182
+ def retry_connecting?
183
+ @reconnect_attempt += 1
184
+
185
+ return false if @reconnect_attempt > @reconnect_attempts.size
186
+
187
+ sleep_t = @reconnect_attempts[@reconnect_attempt - 1]
188
+
189
+ sleep(sleep_t) if sleep_t > 0
190
+
191
+ true
192
+ end
193
+
194
+ def resubscribe
195
+ channels = @sync.synchronize do
196
+ @subscribers.keys
197
+ end
198
+ @subscribed_client.subscribe(*channels) unless channels.empty?
199
+ end
200
+
201
+ def reset
202
+ @subscription_lock.synchronize do
203
+ @subscribed_client = nil
204
+ @subscribe_callbacks.clear
205
+ @when_connected.clear
206
+ end
207
+ end
208
+
166
209
  if ::Redis::VERSION < "5"
210
+ ConnectionError = ::Redis::BaseConnectionError
211
+
167
212
  class SubscribedClient
168
213
  def initialize(raw_client)
169
214
  @raw_client = raw_client
@@ -193,10 +238,11 @@ module ActionCable
193
238
  end
194
239
 
195
240
  def extract_subscribed_client(conn)
196
- raw_client = conn.respond_to?(:_client) ? conn._client : conn.client
197
- SubscribedClient.new(raw_client)
241
+ SubscribedClient.new(conn._client)
198
242
  end
199
243
  else
244
+ ConnectionError = RedisClient::ConnectionError
245
+
200
246
  def extract_subscribed_client(conn)
201
247
  conn
202
248
  end