diff options
Diffstat (limited to 'actioncable/lib/action_cable/server')
6 files changed, 293 insertions, 0 deletions
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb new file mode 100644 index 0000000000..3785bbd154 --- /dev/null +++ b/actioncable/lib/action_cable/server/base.rb @@ -0,0 +1,74 @@ +require 'em-hiredis' + +module ActionCable + 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 + # also by the user to reach the RemoteConnections instead for finding and disconnecting connections across all servers. + # + # Also, this is the server instance used for broadcasting. See Broadcasting for details. + class Base + include ActionCable::Server::Broadcasting + include ActionCable::Server::Connections + + cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new } + + def self.logger; config.logger; end + delegate :logger, to: :config + + def initialize + end + + # Called by rack to setup the server. + def call(env) + setup_heartbeat_timer + config.connection_class.new(self, env).process + end + + # Disconnect all the connections identified by `identifiers` on this server or any others via RemoteConnections. + def disconnect(identifiers) + remote_connections.where(identifiers).disconnect + end + + # Gateway to RemoteConnections. See that class for details. + def remote_connections + @remote_connections ||= RemoteConnections.new(self) + end + + # The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size. + def worker_pool + @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) + end + + # Requires and returns a hash of all the channel class constants keyed by name. + def channel_classes + @channel_classes ||= begin + config.channel_paths.each { |channel_path| require channel_path } + config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize } + end + end + + # The redis pubsub adapter used for all streams/broadcasting. + def pubsub + @pubsub ||= redis.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 + + # All the identifiers applied to the connection class associated with this server. + def connection_identifiers + config.connection_class.identifiers + end + end + + ActiveSupport.run_load_hooks(:action_cable, Base.config) + end +end diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb new file mode 100644 index 0000000000..c759239a0e --- /dev/null +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -0,0 +1,54 @@ +require 'redis' + +module ActionCable + 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 + # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example: + # + # class WebNotificationsChannel < ApplicationCable::Channel + # def subscribed + # stream_from "web_notifications_#{current_user.id}" + # end + # end + # + # # Somewhere in your app this is called, perhaps from a NewCommentJob + # ActionCable.server.broadcast \ + # "web_notifications_1", { title: 'New things!', body: 'All shit fit for print' } + # + # # Client-side coffescript, which assumes you've already requested the right to send web notifications + # App.cable.subscriptions.create "WebNotificationsChannel", + # received: (data) -> + # new Notification data['title'], body: data['body'] + module Broadcasting + # Broadcast a hash directly to a named <tt>broadcasting</tt>. It'll automatically be JSON encoded. + def broadcast(broadcasting, message) + broadcaster_for(broadcasting).broadcast(message) + end + + # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have a object that + # may need multiple spots to transmit to a specific broadcasting over and over. + def broadcaster_for(broadcasting) + Broadcaster.new(self, broadcasting) + end + + # The redis instance used for broadcasting. Not intended for direct user use. + def broadcasting_redis + @broadcasting_redis ||= Redis.new(config.redis) + end + + private + class Broadcaster + attr_reader :server, :broadcasting + + def initialize(server, broadcasting) + @server, @broadcasting = server, broadcasting + end + + def broadcast(message) + server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}" + server.broadcasting_redis.publish broadcasting, ActiveSupport::JSON.encode(message) + end + end + end + end +end diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb new file mode 100644 index 0000000000..935133cbba --- /dev/null +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -0,0 +1,35 @@ +module ActionCable + module Server + # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak the configuration points + # in a Rails config initializer. + class Configuration + attr_accessor :logger, :log_tags + attr_accessor :connection_class, :worker_pool_size + attr_accessor :redis, :channels_path + attr_accessor :disable_request_forgery_protection, :allowed_request_origins + attr_accessor :url + + def initialize + @log_tags = [] + + @connection_class = ApplicationCable::Connection + @worker_pool_size = 100 + + @channels_path = Rails.root.join('app/channels') + + @disable_request_forgery_protection = false + end + + def channel_paths + @channels ||= Dir["#{channels_path}/**/*_channel.rb"] + end + + def channel_class_names + @channel_class_names ||= channel_paths.collect do |channel_path| + Pathname.new(channel_path).basename.to_s.split('.').first.camelize + end + end + end + end +end + diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb new file mode 100644 index 0000000000..47dcea8c20 --- /dev/null +++ b/actioncable/lib/action_cable/server/connections.rb @@ -0,0 +1,37 @@ +module ActionCable + module Server + # Collection class for all the connections that's been established on this specific server. Remember, usually you'll run many cable servers, so + # you can't use this collection as an full list of all the connections established against your application. Use RemoteConnections for that. + # As such, this is primarily for internal use. + module Connections + BEAT_INTERVAL = 3 + + def connections + @connections ||= [] + end + + def add_connection(connection) + connections << connection + end + + def remove_connection(connection) + connections.delete connection + end + + # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you + # then can't rely on being able to receive and send to it. So there's a 3 second heartbeat running on all connections. If the beat fails, we automatically + # disconnect. + def setup_heartbeat_timer + EM.next_tick do + @heartbeat_timer ||= EventMachine.add_periodic_timer(BEAT_INTERVAL) do + EM.next_tick { connections.map(&:beat) } + end + end + end + + def open_connections_statistics + connections.map(&:statistics) + end + end + end +end diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb new file mode 100644 index 0000000000..3b6c6d44a1 --- /dev/null +++ b/actioncable/lib/action_cable/server/worker.rb @@ -0,0 +1,71 @@ +require 'active_support/callbacks' +require 'active_support/core_ext/module/attribute_accessors_per_thread' +require 'concurrent' + +module ActionCable + module Server + # Worker used by Server.send_async to do connection work in threads. Only for internal use. + class Worker + include ActiveSupport::Callbacks + + thread_mattr_accessor :connection + define_callbacks :work + include ActiveRecordConnectionManagement + + def initialize(max_size: 5) + @pool = Concurrent::ThreadPoolExecutor.new( + min_threads: 1, + max_threads: max_size, + max_queue: 0, + ) + end + + def async_invoke(receiver, method, *args) + @pool.post do + invoke(receiver, method, *args) + end + end + + def invoke(receiver, method, *args) + begin + self.connection = receiver + + run_callbacks :work do + receiver.send method, *args + end + rescue Exception => e + logger.error "There was an exception - #{e.class}(#{e.message})" + logger.error e.backtrace.join("\n") + + receiver.handle_exception if receiver.respond_to?(:handle_exception) + ensure + self.connection = nil + end + end + + def async_run_periodic_timer(channel, callback) + @pool.post do + run_periodic_timer(channel, callback) + end + end + + def run_periodic_timer(channel, callback) + begin + self.connection = channel.connection + + run_callbacks :work do + callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback) + end + ensure + self.connection = nil + end + end + + private + + def logger + ActionCable.server.logger + end + end + end +end diff --git a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb new file mode 100644 index 0000000000..ecece4e270 --- /dev/null +++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb @@ -0,0 +1,22 @@ +module ActionCable + module Server + class Worker + # Clear active connections between units of work so the long-running channel or connection processes do not hoard connections. + module ActiveRecordConnectionManagement + extend ActiveSupport::Concern + + included do + if defined?(ActiveRecord::Base) + set_callback :work, :around, :with_database_connections + end + end + + def with_database_connections + connection.logger.tag(ActiveRecord::Base.logger) { yield } + ensure + ActiveRecord::Base.clear_active_connections! + end + end + end + end +end
\ No newline at end of file |