aboutsummaryrefslogtreecommitdiffstats
path: root/actioncable/lib/action_cable/server
diff options
context:
space:
mode:
Diffstat (limited to 'actioncable/lib/action_cable/server')
-rw-r--r--actioncable/lib/action_cable/server/base.rb74
-rw-r--r--actioncable/lib/action_cable/server/broadcasting.rb54
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb43
-rw-r--r--actioncable/lib/action_cable/server/connections.rb37
-rw-r--r--actioncable/lib/action_cable/server/worker.rb42
-rw-r--r--actioncable/lib/action_cable/server/worker/active_record_connection_management.rb22
6 files changed, 272 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..f1585dc776
--- /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.pool(size: config.worker_pool_size)
+ end
+
+ # Requires and returns an 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..6e0fbae387
--- /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..5d65f5e0e9
--- /dev/null
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -0,0 +1,43 @@
+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 log_to_stdout
+ console = ActiveSupport::Logger.new($stdout)
+ console.formatter = @logger.formatter
+ console.level = @logger.level
+
+ @logger.extend(ActiveSupport::Logger.broadcast(console))
+ 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..e063b2a2e1
--- /dev/null
+++ b/actioncable/lib/action_cable/server/worker.rb
@@ -0,0 +1,42 @@
+require 'celluloid'
+require 'active_support/callbacks'
+
+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
+ include Celluloid
+
+ attr_reader :connection
+ define_callbacks :work
+ include ActiveRecordConnectionManagement
+
+ def invoke(receiver, method, *args)
+ @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)
+ end
+
+ def run_periodic_timer(channel, callback)
+ @connection = channel.connection
+
+ run_callbacks :work do
+ callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
+ 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