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.rb45
-rw-r--r--actioncable/lib/action_cable/server/broadcasting.rb29
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb37
-rw-r--r--actioncable/lib/action_cable/server/connections.rb15
-rw-r--r--actioncable/lib/action_cable/server/worker.rb52
-rw-r--r--actioncable/lib/action_cable/server/worker/active_record_connection_management.rb5
6 files changed, 103 insertions, 80 deletions
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index fe48c112df..b1a0e11631 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -1,11 +1,11 @@
-require 'thread'
+require 'monitor'
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.
+ # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
+ # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
#
- # Also, this is the server instance used for broadcasting. See Broadcasting for details.
+ # Also, this is the server instance used for broadcasting. See Broadcasting for more information.
class Base
include ActionCable::Server::Broadcasting
include ActionCable::Server::Connections
@@ -18,12 +18,11 @@ module ActionCable
attr_reader :mutex
def initialize
- @mutex = Mutex.new
-
- @remote_connections = @stream_event_loop = @worker_pool = @channel_classes = @pubsub = nil
+ @mutex = Monitor.new
+ @remote_connections = @event_loop = @worker_pool = @channel_classes = @pubsub = nil
end
- # Called by rack to setup the server.
+ # Called by Rack to setup the server.
def call(env)
setup_heartbeat_timer
config.connection_class.new(self, env).process
@@ -34,21 +33,41 @@ module ActionCable
remote_connections.where(identifiers).disconnect
end
+ def restart
+ connections.each(&:close)
+
+ @mutex.synchronize do
+ worker_pool.halt if @worker_pool
+
+ @worker_pool = nil
+ end
+ end
+
# Gateway to RemoteConnections. See that class for details.
def remote_connections
@remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
end
- def stream_event_loop
- @stream_event_loop || @mutex.synchronize { @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new }
+ def event_loop
+ @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
end
- # The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
+ # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
+ # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
+ # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size.
+ #
+ # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
+ # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
+ # connections.
+ #
+ # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
+ # the db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
+ # db connection pool instead.
def worker_pool
@worker_pool || @mutex.synchronize { @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.
+ # Requires and returns a hash of all of the channel class constants, which are keyed by name.
def channel_classes
@channel_classes || @mutex.synchronize do
@channel_classes ||= begin
@@ -63,7 +82,7 @@ module ActionCable
@pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
end
- # All the identifiers applied to the connection class associated with this server.
+ # All of the identifiers applied to the connection class associated with this server.
def connection_identifiers
config.connection_class.identifiers
end
diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb
index 7e8aef45f4..8f93564113 100644
--- a/actioncable/lib/action_cable/server/broadcasting.rb
+++ b/actioncable/lib/action_cable/server/broadcasting.rb
@@ -1,6 +1,6 @@
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
+ # 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
# broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
#
# class WebNotificationsChannel < ApplicationCable::Channel
@@ -9,37 +9,38 @@ module ActionCable
# end
# end
#
- # # Somewhere in your app this is called, perhaps from a NewCommentJob
+ # # Somewhere in your app this is called, perhaps from a NewCommentJob:
# ActionCable.server.broadcast \
# "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
#
- # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications
+ # # Client-side CoffeeScript, 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)
+ # Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
+ def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
+ broadcaster_for(broadcasting, coder: coder).broadcast(message)
end
- # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have a object that
+ # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
# may need multiple spots to transmit to a specific broadcasting over and over.
- def broadcaster_for(broadcasting)
- Broadcaster.new(self, broadcasting)
+ def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
+ Broadcaster.new(self, String(broadcasting), coder: coder)
end
private
class Broadcaster
- attr_reader :server, :broadcasting
+ attr_reader :server, :broadcasting, :coder
- def initialize(server, broadcasting)
- @server, @broadcasting = server, broadcasting
+ def initialize(server, broadcasting, coder:)
+ @server, @broadcasting, @coder = server, broadcasting, coder
end
def broadcast(message)
- server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
- server.pubsub.broadcast broadcasting, ActiveSupport::JSON.encode(message)
+ server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
+ encoded = coder ? coder.encode(message) : message
+ server.pubsub.broadcast broadcasting, encoded
end
end
end
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 9a248933c4..0bb378cf03 100644
--- a/actioncable/lib/action_cable/server/configuration.rb
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -1,31 +1,24 @@
module ActionCable
module Server
- # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak the configuration points
+ # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
# in a Rails config initializer.
class Configuration
attr_accessor :logger, :log_tags
- attr_accessor :connection_class, :worker_pool_size
- attr_accessor :channel_load_paths
+ attr_accessor :use_faye, :connection_class, :worker_pool_size
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
- attr_accessor :cable, :url
+ attr_accessor :cable, :url, :mount_path
+
+ attr_accessor :channel_paths # :nodoc:
def initialize
@log_tags = []
- @connection_class = ApplicationCable::Connection
- @worker_pool_size = 100
-
- @channel_load_paths = [Rails.root.join('app/channels')]
+ @connection_class = ActionCable::Connection::Base
+ @worker_pool_size = 4
@disable_request_forgery_protection = false
end
- def channel_paths
- @channel_paths ||= channel_load_paths.flat_map do |path|
- Dir["#{path}/**/*_channel.rb"]
- end
- end
-
def channel_class_names
@channel_class_names ||= channel_paths.collect do |channel_path|
Pathname.new(channel_path).basename.to_s.split('.').first.camelize
@@ -50,6 +43,22 @@ module ActionCable
adapter = 'PostgreSQL' if adapter == 'Postgresql'
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
end
+
+ def event_loop_class
+ if use_faye
+ ActionCable::Connection::FayeEventLoop
+ else
+ ActionCable::Connection::StreamEventLoop
+ end
+ end
+
+ def client_socket_class
+ if use_faye
+ ActionCable::Connection::FayeClientSocket
+ else
+ ActionCable::Connection::ClientSocket
+ end
+ end
end
end
end
diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb
index 8671dd5ebd..5e61b4e335 100644
--- a/actioncable/lib/action_cable/server/connections.rb
+++ b/actioncable/lib/action_cable/server/connections.rb
@@ -1,9 +1,8 @@
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
+ # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
+ # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
+ module Connections # :nodoc:
BEAT_INTERVAL = 3
def connections
@@ -19,12 +18,12 @@ module ActionCable
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
+ # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
# disconnect.
def setup_heartbeat_timer
- @heartbeat_timer ||= Concurrent::TimerTask.new(execution_interval: BEAT_INTERVAL) do
- Concurrent.global_io_executor.post { connections.map(&:beat) }
- end.tap(&:execute)
+ @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
+ event_loop.post { connections.map(&:beat) }
+ end
end
def open_connections_statistics
diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb
index 3b6c6d44a1..46a8989f34 100644
--- a/actioncable/lib/action_cable/server/worker.rb
+++ b/actioncable/lib/action_cable/server/worker.rb
@@ -4,8 +4,8 @@ require 'concurrent'
module ActionCable
module Server
- # Worker used by Server.send_async to do connection work in threads. Only for internal use.
- class Worker
+ # Worker used by Server.send_async to do connection work in threads.
+ class Worker # :nodoc:
include ActiveSupport::Callbacks
thread_mattr_accessor :connection
@@ -20,44 +20,42 @@ module ActionCable
)
end
- def async_invoke(receiver, method, *args)
- @pool.post do
- invoke(receiver, method, *args)
- end
+ # Stop processing work: any work that has not already started
+ # running will be discarded from the queue
+ def halt
+ @pool.kill
end
- def invoke(receiver, method, *args)
- begin
- self.connection = receiver
+ def stopping?
+ @pool.shuttingdown?
+ end
- 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")
+ def work(connection)
+ self.connection = connection
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
- ensure
- self.connection = nil
+ run_callbacks :work do
+ yield
end
+ ensure
+ self.connection = nil
end
- def async_run_periodic_timer(channel, callback)
+ def async_invoke(receiver, method, *args)
@pool.post do
- run_periodic_timer(channel, callback)
+ invoke(receiver, method, *args)
end
end
- def run_periodic_timer(channel, callback)
- begin
- self.connection = channel.connection
+ def invoke(receiver, method, *args)
+ work(receiver) do
+ begin
+ receiver.send method, *args
+ rescue Exception => e
+ logger.error "There was an exception - #{e.class}(#{e.message})"
+ logger.error e.backtrace.join("\n")
- run_callbacks :work do
- callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
end
- ensure
- self.connection = nil
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
index ecece4e270..c1e4aa8103 100644
--- a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
+++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
@@ -1,7 +1,6 @@
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
@@ -13,10 +12,8 @@ module ActionCable
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
+end