diff options
Diffstat (limited to 'actioncable')
-rw-r--r-- | actioncable/CHANGELOG.md | 5 | ||||
-rw-r--r-- | actioncable/README.md | 8 | ||||
-rw-r--r-- | actioncable/lib/action_cable/connection/base.rb | 5 | ||||
-rw-r--r-- | actioncable/lib/action_cable/engine.rb | 25 | ||||
-rw-r--r-- | actioncable/lib/action_cable/server/base.rb | 10 | ||||
-rw-r--r-- | actioncable/lib/action_cable/server/worker.rb | 48 | ||||
-rw-r--r-- | actioncable/lib/action_cable/server/worker/active_record_connection_management.rb | 3 | ||||
-rw-r--r-- | actioncable/lib/rails/generators/channel/USAGE | 2 | ||||
-rw-r--r-- | actioncable/test/client_test.rb | 21 | ||||
-rw-r--r-- | actioncable/test/stubs/test_server.rb | 4 | ||||
-rw-r--r-- | actioncable/test/test_helper.rb | 3 |
11 files changed, 100 insertions, 34 deletions
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index a6842d77ef..946fdfb3fc 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,8 @@ +* Safely support autoloading and class unloading, by preventing concurrent + loads, and disconnecting all cables during reload. + + *Matthew Draper* + * Ensure ActionCable behaves correctly for non-string queue names. *Jay Hayes* diff --git a/actioncable/README.md b/actioncable/README.md index bb15ad3c70..595830feb0 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -412,7 +412,7 @@ The above will start a cable server on port 28080. ### In app -If you are using a threaded server like Puma or Thin, the current implementation of Action Cable can run side-along with your Rails application. For example, to listen for WebSocket requests on `/cable`, mount the server at that path: +If you are using a server that supports the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/cable`, mount the server at that path: ```ruby # config/routes.rb @@ -445,12 +445,8 @@ connection management is handled internally by utilizing Ruby’s native thread support, which means you can use all your regular Rails models with no problems as long as you haven’t committed any thread-safety sins. -But this also means that Action Cable needs to run in its own server process. -So you'll have one set of server processes for your normal web work, and another -set of server processes for the Action Cable. - The Action Cable server does _not_ need to be a multi-threaded application server. -This is because Action Cable uses the [Rack socket hijacking API](http://old.blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/) +This is because Action Cable uses the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking) to take over control of connections from the application server. Action Cable then manages connections internally, in a multithreaded manner, regardless of whether the application server is multi-threaded or not. So Action Cable works diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index 79253ef4ef..f34f5eb109 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -48,12 +48,13 @@ module ActionCable include InternalChannel include Authorization - attr_reader :server, :env, :subscriptions, :logger - delegate :stream_event_loop, :worker_pool, :pubsub, to: :server + attr_reader :server, :env, :subscriptions, :logger, :worker_pool + delegate :stream_event_loop, :pubsub, to: :server def initialize(server, env) @server, @env = server, env + @worker_pool = server.worker_pool @logger = new_tagged_logger @websocket = ActionCable::Connection::WebSocket.new(env, self, stream_event_loop) diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index ae0c59dccd..c90aadaf2c 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -51,5 +51,30 @@ module ActionCable end end end + + initializer "action_cable.set_work_hooks" do |app| + ActiveSupport.on_load(:action_cable) do + ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner| + app.executor.wrap do + # If we took a while to get the lock, we may have been halted + # in the meantime. As we haven't started doing any real work + # yet, we should pretend that we never made it off the queue. + unless stopping? + inner.call + end + end + end + + wrap = lambda do |_, inner| + app.executor.wrap(&inner) + end + ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap + ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap + + app.reloader.before_class_unload do + ActionCable.server.restart + end + end + end end end diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index c3b64299e3..d9a2653cc2 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -33,6 +33,16 @@ 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) } diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb index b920b880db..49cbaec0c0 100644 --- a/actioncable/lib/action_cable/server/worker.rb +++ b/actioncable/lib/action_cable/server/worker.rb @@ -20,6 +20,26 @@ module ActionCable ) end + # Stop processing work: any work that has not already started + # running will be discarded from the queue + def halt + @pool.kill + end + + def stopping? + @pool.shuttingdown? + end + + def work(connection) + self.connection = connection + + run_callbacks :work do + yield + end + ensure + self.connection = nil + end + def async_invoke(receiver, method, *args) @pool.post do invoke(receiver, method, *args) @@ -27,19 +47,15 @@ module ActionCable end def invoke(receiver, method, *args) - begin - self.connection = receiver - - run_callbacks :work do + work(receiver) do + begin receiver.send method, *args - end - rescue Exception => e - logger.error "There was an exception - #{e.class}(#{e.message})" - logger.error e.backtrace.join("\n") + 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 + receiver.handle_exception if receiver.respond_to?(:handle_exception) + end end end @@ -50,14 +66,8 @@ module ActionCable 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 + work(channel.connection) do + callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback) 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 1ac8934410..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 that way long-running channels or connection processes do not hoard connections. module ActiveRecordConnectionManagement extend ActiveSupport::Concern @@ -13,8 +12,6 @@ module ActionCable def with_database_connections connection.logger.tag(ActiveRecord::Base.logger) { yield } - ensure - ActiveRecord::Base.clear_active_connections! end end end diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE index 27a934c689..6249553c22 100644 --- a/actioncable/lib/rails/generators/channel/USAGE +++ b/actioncable/lib/rails/generators/channel/USAGE @@ -3,7 +3,7 @@ Description: Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript). Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments. - Note: Turn on the cable connection in app/assets/javascript/cable.coffee after generating any channels. + Note: Turn on the cable connection in app/assets/javascript/cable.js after generating any channels. Example: ======== diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index be0529bb8b..75545993da 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -127,8 +127,16 @@ class ClientTest < ActionCable::TestCase end @ws.close + wait_for_close + end + + def wait_for_close @closed.wait(WAIT_WHEN_EXPECTING_EVENT) end + + def closed? + @closed.set? + end end def faye_client(port) @@ -226,4 +234,17 @@ class ClientTest < ActionCable::TestCase assert_equal(0, app.connections.count) end end + + def test_server_restart + with_puma_server do |port| + c = faye_client(port) + assert_equal({"type" => "welcome"}, c.read_message) + c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel') + assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message) + + ActionCable.server.restart + c.wait_for_close + assert c.closed? + end + end end diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb index 56d132b30a..5916cf1e83 100644 --- a/actioncable/test/stubs/test_server.rb +++ b/actioncable/test/stubs/test_server.rb @@ -17,4 +17,8 @@ class TestServer def stream_event_loop @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new end + + def worker_pool + @worker_pool ||= ActionCable::Server::Worker.new(max_size: 5) + end end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index 8ddbd4e764..8b157c9b46 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -1,9 +1,6 @@ -require File.expand_path('../../../load_paths', __FILE__) - require 'action_cable' require 'active_support/testing/autorun' - require 'puma' require 'mocha/setup' |