mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Adapterize ActionCable storage and extract behavior
This commit is contained in:
parent
75f1b229fd
commit
0016e0410b
17 changed files with 173 additions and 50 deletions
|
@ -47,4 +47,5 @@ module ActionCable
|
||||||
autoload :Connection
|
autoload :Connection
|
||||||
autoload :Channel
|
autoload :Channel
|
||||||
autoload :RemoteConnections
|
autoload :RemoteConnections
|
||||||
|
autoload :StorageAdapter
|
||||||
end
|
end
|
||||||
|
|
|
@ -133,8 +133,8 @@ module ActionCable
|
||||||
@identifier = identifier
|
@identifier = identifier
|
||||||
@params = params
|
@params = params
|
||||||
|
|
||||||
# When a channel is streaming via redis pubsub, we want to delay the confirmation
|
# When a channel is streaming via pubsub, we want to delay the confirmation
|
||||||
# transmission until redis pubsub subscription is confirmed.
|
# transmission until pubsub subscription is confirmed.
|
||||||
@defer_subscription_confirmation = false
|
@defer_subscription_confirmation = false
|
||||||
|
|
||||||
@reject_subscription = nil
|
@reject_subscription = nil
|
||||||
|
|
|
@ -60,7 +60,7 @@ module ActionCable
|
||||||
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
|
||||||
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
|
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
|
||||||
|
|
||||||
@_internal_redis_subscriptions = nil
|
@_internal_subscriptions = nil
|
||||||
@started_at = Time.now
|
@started_at = Time.now
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,24 +5,24 @@ module ActionCable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
private
|
private
|
||||||
def internal_redis_channel
|
def internal_channel
|
||||||
"action_cable/#{connection_identifier}"
|
"action_cable/#{connection_identifier}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def subscribe_to_internal_channel
|
def subscribe_to_internal_channel
|
||||||
if connection_identifier.present?
|
if connection_identifier.present?
|
||||||
callback = -> (message) { process_internal_message(message) }
|
callback = -> (message) { process_internal_message(message) }
|
||||||
@_internal_redis_subscriptions ||= []
|
@_internal_subscriptions ||= []
|
||||||
@_internal_redis_subscriptions << [ internal_redis_channel, callback ]
|
@_internal_subscriptions << [ internal_channel, callback ]
|
||||||
|
|
||||||
EM.next_tick { pubsub.subscribe(internal_redis_channel, &callback) }
|
EM.next_tick { pubsub.subscribe(internal_channel, &callback) }
|
||||||
logger.info "Registered connection (#{connection_identifier})"
|
logger.info "Registered connection (#{connection_identifier})"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsubscribe_from_internal_channel
|
def unsubscribe_from_internal_channel
|
||||||
if @_internal_redis_subscriptions.present?
|
if @_internal_subscriptions.present?
|
||||||
@_internal_redis_subscriptions.each { |channel, callback| EM.next_tick { pubsub.unsubscribe_proc(channel, callback) } }
|
@_internal_subscriptions.each { |channel, callback| EM.next_tick { pubsub.unsubscribe_proc(channel, callback) } }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -24,11 +24,11 @@ module ActionCable
|
||||||
options = app.config.action_cable
|
options = app.config.action_cable
|
||||||
options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?
|
options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?
|
||||||
|
|
||||||
app.paths.add "config/redis/cable", with: "config/redis/cable.yml"
|
app.paths.add "config/cable", with: "config/cable.yml"
|
||||||
|
|
||||||
ActiveSupport.on_load(:action_cable) do
|
ActiveSupport.on_load(:action_cable) do
|
||||||
if (redis_cable_path = Pathname.new(app.config.paths["config/redis/cable"].first)).exist?
|
if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
|
||||||
self.redis = Rails.application.config_for(redis_cable_path).with_indifferent_access
|
self.config_opts = Rails.application.config_for(config_path).with_indifferent_access
|
||||||
end
|
end
|
||||||
|
|
||||||
options.each { |k,v| send("#{k}=", v) }
|
options.each { |k,v| send("#{k}=", v) }
|
||||||
|
|
|
@ -39,7 +39,7 @@ module ActionCable
|
||||||
|
|
||||||
# Uses the internal channel to disconnect the connection.
|
# Uses the internal channel to disconnect the connection.
|
||||||
def disconnect
|
def disconnect
|
||||||
server.broadcast internal_redis_channel, type: 'disconnect'
|
server.broadcast internal_channel, type: 'disconnect'
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns all the identifiers that were applied to this connection.
|
# Returns all the identifiers that were applied to this connection.
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
require 'em-hiredis'
|
|
||||||
|
|
||||||
module ActionCable
|
module ActionCable
|
||||||
module Server
|
module Server
|
||||||
# A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the rack process that starts the cable server, but
|
# A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the rack process that starts the cable server, but
|
||||||
|
@ -47,20 +45,9 @@ module ActionCable
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# The redis pubsub adapter used for all streams/broadcasting.
|
# The pubsub adapter used for all streams/broadcasting.
|
||||||
def pubsub
|
def pubsub
|
||||||
@pubsub ||= redis.pubsub
|
@pubsub ||= config.storage_adapter.new(self).pubsub
|
||||||
end
|
|
||||||
|
|
||||||
# The EventMachine Redis instance used by the pubsub adapter.
|
|
||||||
def redis
|
|
||||||
@redis ||= EM::Hiredis.connect(config.redis[:url]).tap do |redis|
|
|
||||||
redis.on(:reconnect_failed) do
|
|
||||||
logger.info "[ActionCable] Redis reconnect failed."
|
|
||||||
# logger.info "[ActionCable] Redis reconnected. Closing all the open connections."
|
|
||||||
# @connections.map &:close
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# All the identifiers applied to the connection class associated with this server.
|
# All the identifiers applied to the connection class associated with this server.
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
require 'redis'
|
|
||||||
|
|
||||||
module ActionCable
|
module ActionCable
|
||||||
module Server
|
module Server
|
||||||
# Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these
|
# Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these
|
||||||
|
@ -31,11 +29,6 @@ module ActionCable
|
||||||
Broadcaster.new(self, broadcasting)
|
Broadcaster.new(self, broadcasting)
|
||||||
end
|
end
|
||||||
|
|
||||||
# The redis instance used for broadcasting. Not intended for direct user use.
|
|
||||||
def broadcasting_redis
|
|
||||||
@broadcasting_redis ||= Redis.new(config.redis)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
class Broadcaster
|
class Broadcaster
|
||||||
attr_reader :server, :broadcasting
|
attr_reader :server, :broadcasting
|
||||||
|
@ -46,7 +39,8 @@ module ActionCable
|
||||||
|
|
||||||
def broadcast(message)
|
def broadcast(message)
|
||||||
server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
|
server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
|
||||||
server.broadcasting_redis.publish broadcasting, ActiveSupport::JSON.encode(message)
|
broadcast_storage_adapter = server.config.storage_adapter.new(server).broadcast
|
||||||
|
broadcast_storage_adapter.publish broadcasting, ActiveSupport::JSON.encode(message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,9 +5,9 @@ module ActionCable
|
||||||
class Configuration
|
class Configuration
|
||||||
attr_accessor :logger, :log_tags
|
attr_accessor :logger, :log_tags
|
||||||
attr_accessor :connection_class, :worker_pool_size
|
attr_accessor :connection_class, :worker_pool_size
|
||||||
attr_accessor :redis, :channels_path
|
attr_accessor :channels_path
|
||||||
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
|
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
|
||||||
attr_accessor :url
|
attr_accessor :config_opts, :url
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@log_tags = []
|
@log_tags = []
|
||||||
|
@ -29,6 +29,22 @@ module ActionCable
|
||||||
Pathname.new(channel_path).basename.to_s.split('.').first.camelize
|
Pathname.new(channel_path).basename.to_s.split('.').first.camelize
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ADAPTER = ActionCable::StorageAdapter
|
||||||
|
|
||||||
|
# Returns constant of storage adapter specified in config/cable.yml
|
||||||
|
# If the adapter cannot be found, this will default to the Redis adapter
|
||||||
|
def storage_adapter
|
||||||
|
# "ActionCable::StorageAdapter::#{adapter.capitalize}"
|
||||||
|
adapter = config_opts['adapter']
|
||||||
|
adapter_const = "ActionCable::StorageAdapter::#{adapter.capitalize}"
|
||||||
|
|
||||||
|
if Object.const_defined?(adapter_const)
|
||||||
|
adapter_const.constantize
|
||||||
|
else
|
||||||
|
ADAPTER_BASE::Redis
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
6
actioncable/lib/action_cable/storage_adapter.rb
Normal file
6
actioncable/lib/action_cable/storage_adapter.rb
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
module ActionCable
|
||||||
|
module StorageAdapter
|
||||||
|
autoload :Base, 'action_cable/storage_adapter/base'
|
||||||
|
autoload :Redis, 'action_cable/storage_adapter/redis'
|
||||||
|
end
|
||||||
|
end
|
22
actioncable/lib/action_cable/storage_adapter/base.rb
Normal file
22
actioncable/lib/action_cable/storage_adapter/base.rb
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
module ActionCable
|
||||||
|
module StorageAdapter
|
||||||
|
class Base
|
||||||
|
attr_reader :logger, :server
|
||||||
|
|
||||||
|
def initialize(server)
|
||||||
|
@server = server
|
||||||
|
@logger = @server.logger
|
||||||
|
end
|
||||||
|
|
||||||
|
# Storage connection instance used for broadcasting. Not intended for direct user use.
|
||||||
|
def broadcast
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Storage connection instance used for pubsub.
|
||||||
|
def pubsub
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
30
actioncable/lib/action_cable/storage_adapter/redis.rb
Normal file
30
actioncable/lib/action_cable/storage_adapter/redis.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
require 'em-hiredis'
|
||||||
|
require 'redis'
|
||||||
|
|
||||||
|
module ActionCable
|
||||||
|
module StorageAdapter
|
||||||
|
class Redis < Base
|
||||||
|
# The redis instance used for broadcasting. Not intended for direct user use.
|
||||||
|
def broadcast
|
||||||
|
@broadcast ||= ::Redis.new(@server.config.config_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pubsub
|
||||||
|
redis.pubsub
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# The EventMachine Redis instance used by the pubsub adapter.
|
||||||
|
def redis
|
||||||
|
@redis ||= EM::Hiredis.connect(@server.config.config_opts[:url]).tap do |redis|
|
||||||
|
redis.on(:reconnect_failed) do
|
||||||
|
@logger.info "[ActionCable] Redis reconnect failed."
|
||||||
|
# logger.info "[ActionCable] Redis reconnected. Closing all the open connections."
|
||||||
|
# @connections.map &:close
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
64
actioncable/test/storage_adapter/base_test.rb
Normal file
64
actioncable/test/storage_adapter/base_test.rb
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
require 'test_helper'
|
||||||
|
require 'stubs/test_server'
|
||||||
|
|
||||||
|
class ActionCable::StorageAdapter::BaseTest < ActionCable::TestCase
|
||||||
|
## TEST THAT ERRORS ARE RETURNED FOR INHERITORS THAT DON'T OVERRIDE METHODS
|
||||||
|
|
||||||
|
class BrokenAdapter < ActionCable::StorageAdapter::Base
|
||||||
|
end
|
||||||
|
|
||||||
|
setup do
|
||||||
|
@server = TestServer.new
|
||||||
|
@server.config.allowed_request_origins = %w( http://rubyonrails.com )
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#broadcast returns NotImplementedError by default" do
|
||||||
|
assert_raises NotImplementedError do
|
||||||
|
BrokenAdapter.new(@server).broadcast
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#pubsub returns NotImplementedError by default" do
|
||||||
|
assert_raises NotImplementedError do
|
||||||
|
BrokenAdapter.new(@server).pubsub
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TEST METHODS THAT ARE REQUIRED OF THE ADAPTER'S BACKEND STORAGE OBJECT
|
||||||
|
|
||||||
|
class SuccessAdapterBackend
|
||||||
|
def publish(channel, message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribe(*channels, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribe(*channels, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class SuccessAdapter < ActionCable::StorageAdapter::Base
|
||||||
|
def broadcast
|
||||||
|
SuccessAdapterBackend.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def pubsub
|
||||||
|
SuccessAdapterBackend.new
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#broadcast responds to #publish" do
|
||||||
|
broadcast = SuccessAdapter.new(@server).broadcast
|
||||||
|
assert_respond_to(broadcast, :publish)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#pubsub responds to #subscribe" do
|
||||||
|
pubsub = SuccessAdapter.new(@server).pubsub
|
||||||
|
assert_respond_to(pubsub, :subscribe)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "#pubsub responds to #unsubscribe" do
|
||||||
|
pubsub = SuccessAdapter.new(@server).pubsub
|
||||||
|
assert_respond_to(pubsub, :unsubscribe)
|
||||||
|
end
|
||||||
|
end
|
|
@ -78,11 +78,11 @@ module Rails
|
||||||
template "application.rb"
|
template "application.rb"
|
||||||
template "environment.rb"
|
template "environment.rb"
|
||||||
template "secrets.yml"
|
template "secrets.yml"
|
||||||
|
template "cable.yml" unless options[:skip_action_cable]
|
||||||
|
|
||||||
directory "environments"
|
directory "environments"
|
||||||
directory "initializers"
|
directory "initializers"
|
||||||
directory "locales"
|
directory "locales"
|
||||||
directory "redis" unless options[:skip_action_cable]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ module Rails
|
||||||
|
|
||||||
def delete_action_cable_files_skipping_action_cable
|
def delete_action_cable_files_skipping_action_cable
|
||||||
if options[:skip_action_cable]
|
if options[:skip_action_cable]
|
||||||
remove_file 'config/redis/cable.yml'
|
remove_file 'config/cable.yml'
|
||||||
remove_file 'app/assets/javascripts/cable.coffee'
|
remove_file 'app/assets/javascripts/cable.coffee'
|
||||||
remove_dir 'app/channels'
|
remove_dir 'app/channels'
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket.
|
||||||
|
production:
|
||||||
|
adapter: redis
|
||||||
|
url: redis://localhost:6379/1
|
||||||
|
|
||||||
|
development:
|
||||||
|
adapter: redis
|
||||||
|
url: redis://localhost:6379/2
|
||||||
|
|
||||||
|
test:
|
||||||
|
adapter: redis
|
||||||
|
url: redis://localhost:6379/3
|
|
@ -1,9 +0,0 @@
|
||||||
# Action Cable uses Redis to administer connections, channels, and sending/receiving messages over the WebSocket.
|
|
||||||
production:
|
|
||||||
url: redis://localhost:6379/1
|
|
||||||
|
|
||||||
development:
|
|
||||||
url: redis://localhost:6379/2
|
|
||||||
|
|
||||||
test:
|
|
||||||
url: redis://localhost:6379/3
|
|
|
@ -392,7 +392,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
|
||||||
def test_generator_if_skip_action_cable_is_given
|
def test_generator_if_skip_action_cable_is_given
|
||||||
run_generator [destination_root, "--skip-action-cable"]
|
run_generator [destination_root, "--skip-action-cable"]
|
||||||
assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/
|
assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/
|
||||||
assert_no_file "config/redis/cable.yml"
|
assert_no_file "config/cable.yml"
|
||||||
assert_no_file "app/assets/javascripts/cable.coffee"
|
assert_no_file "app/assets/javascripts/cable.coffee"
|
||||||
assert_no_file "app/channels"
|
assert_no_file "app/channels"
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue