diff options
Diffstat (limited to 'actioncable')
24 files changed, 102 insertions, 107 deletions
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 64aea88588..22126d82b7 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,8 +1,5 @@ ## Rails 5.0.0.beta1 (December 18, 2015) ## -* No changes. - - * Added to Rails! - *DHH*
\ No newline at end of file + *DHH* diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE index a4910677eb..27a17cf41b 100644 --- a/actioncable/MIT-LICENSE +++ b/actioncable/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 Basecamp, LLC +Copyright (c) 2015-2016 Basecamp, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actioncable/README.md b/actioncable/README.md index 32f49c215f..636f9935cf 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -1,4 +1,4 @@ -# Action Cable –- Integrated WebSockets for Rails +# Action Cable – Integrated WebSockets for Rails Action Cable seamlessly integrates WebSockets with the rest of your Rails application. It allows for real-time features to be written in Ruby in the same style @@ -298,11 +298,12 @@ See the [rails/actioncable-examples](http://github.com/rails/actioncable-example ## Configuration -Action Cable has two required configurations: the Redis connection and specifying allowed request origins. +Action Cable has three required configurations: the Redis connection, allowed request origins, and the cable server url (which can optionally be set on the client side). ### Redis -By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/redis/cable.yml')`. The file must follow the following format: +By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/redis/cable.yml')`. +This file must specify a Redis url for each Rails environment. It may use the following format: ```yaml production: &production @@ -312,8 +313,7 @@ development: &development test: *development ``` -This format allows you to specify one configuration per Rails environment. You can also change the location of the Redis config file in -a Rails initializer with something like: +You can also change the location of the Redis config file in a Rails initializer with something like: ```ruby Rails.application.paths.add "config/redis/cable", with: "somewhere/else/cable.yml" @@ -327,29 +327,31 @@ Action Cable will only accept requests from specified origins, which are passed ActionCable.server.config.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/] ``` +When running in the development environment, this defaults to "http://localhost:3000". + To disable and allow requests from any origin: ```ruby ActionCable.server.config.disable_request_forgery_protection = true ``` -By default, Action Cable allows all requests from localhost:3000 when running in the development environment. +### Consumer Configuration -### Other Configurations +Once you have decided how to run your cable server (see below), you must provide the server url (or path) to your client-side setup. +There are two ways you can do this. -The other common option to configure is the log tags applied to the per-connection logger. Here's close to what we're using in Basecamp: +The first is to simply pass it in when creating your consumer. For a standalone server, +this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server, +something like: `App.cable = ActionCable.createConsumer("/cable")`. -```ruby -ActionCable.server.config.log_tags = [ - -> request { request.env['bc.account_id'] || "no-account" }, - :action_cable, - -> request { request.uuid } -] -``` +The second option is to pass the server url through the `action_cable_meta_tag` in your layout. +This uses a url or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable". -Your websocket url might change between environments. If you host your production server via https, you will need to use the wss scheme +This method is especially useful if your websocket url might change between environments. If you host your production server via https, you will need to use the wss scheme for your ActionCable server, but development might remain http and use the ws scheme. You might use localhost in development and your -domain in production. In any case, to vary the websocket url between environments, add the following configuration to each environment: +domain in production. + +In any case, to vary the websocket url between environments, add the following configuration to each environment: ```ruby config.action_cable.url = "ws://example.com:28080" @@ -364,7 +366,19 @@ Then add the following line to your layout before your JavaScript tag: And finally, create your consumer like so: ```coffeescript -App.cable = Cable.createConsumer() +App.cable = ActionCable.createConsumer() +``` + +### Other Configurations + +The other common option to configure is the log tags applied to the per-connection logger. Here's close to what we're using in Basecamp: + +```ruby +ActionCable.server.config.log_tags = [ + -> request { request.env['bc.account_id'] || "no-account" }, + :action_cable, + -> request { request.uuid } +] ``` For a full list of all configuration options, see the `ActionCable::Server::Configuration` class. @@ -394,22 +408,19 @@ Then you start the server using a binstub in bin/cable ala: bundle exec puma -p 28080 cable/config.ru ``` -The above will start a cable server on port 28080. Remember to point your client-side setup against that using something like: -`App.cable = ActionCable.createConsumer("ws://basecamp.dev:28080")`. +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 ActionCable can run side-along with your Rails application. For example, to listen for WebSocket requests on `/websocket`, match requests on that path: +If you are using a threaded server like Puma or Thin, the current implementation of ActionCable can run side-along with your Rails application. For example, to listen for WebSocket requests on `/cable`, mount the server at that path: ```ruby # config/routes.rb Example::Application.routes.draw do - match "/websocket", :to => ActionCable.server, via: [:get, :post] + mount ActionCable.server => '/cable' end ``` -You can use `App.cable = Cable.createConsumer("/websocket")` to connect to the cable server. - For every instance of your server you create and for every worker your server spawns, you will also have a new instance of ActionCable, but the use of Redis keeps messages synced across connections. ### Notes @@ -427,7 +438,7 @@ messages back and forth over the WebSocket cable connection. This dependency may be alleviated in the future, but for the moment that's what it is. So be sure to have Redis installed and running. -The Ruby side of things is built on top of [faye-websocket](https://github.com/faye/faye-websocket-ruby) and [celluloid](https://github.com/celluloid/celluloid). +The Ruby side of things is built on top of [faye-websocket](https://github.com/faye/faye-websocket-ruby) and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). ## Deployment @@ -452,6 +463,14 @@ Action Cable is released under the MIT license: ## Support -Bug reports can be filed for the alpha development project here: +API documentation is at: + +* http://api.rubyonrails.org + +Bug reports can be filed for the Ruby on Rails project here: + +* https://github.com/rails/rails/issues + +Feature requests should be discussed on the rails-core mailing list here: -* https://github.com/rails/actioncable/issues +* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec index 74c21bd24d..a04fc932aa 100644 --- a/actioncable/actioncable.gemspec +++ b/actioncable/actioncable.gemspec @@ -23,7 +23,6 @@ Gem::Specification.new do |s| s.add_dependency 'coffee-rails', '~> 4.1.0' s.add_dependency 'faye-websocket', '~> 0.10.0' s.add_dependency 'websocket-driver', '~> 0.6.1' - s.add_dependency 'celluloid', '~> 0.17.2' s.add_dependency 'em-hiredis', '~> 0.3.0' s.add_dependency 'redis', '~> 3.0' diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index f27698765c..97f485b32e 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2015 Basecamp, LLC +# Copyright (c) 2015-2016 Basecamp, LLC # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index 25fe8e5e54..7f0fb37afc 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -28,7 +28,7 @@ module ActionCable def start_periodic_timers self.class.periodic_timers.each do |callback, options| active_periodic_timers << EventMachine::PeriodicTimer.new(options[:every]) do - connection.worker_pool.async.run_periodic_timer(self, callback) + connection.worker_pool.async_run_periodic_timer(self, callback) end end end diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index f7b18a85ae..a8cfdf90f3 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -48,11 +48,9 @@ module ActionCable include InternalChannel include Authorization - attr_reader :server, :env, :subscriptions + attr_reader :server, :env, :subscriptions, :logger delegate :worker_pool, :pubsub, to: :server - attr_reader :logger - def initialize(server, env) @server, @env = server, env @@ -105,7 +103,7 @@ module ActionCable # Invoke a method on the connection asynchronously through the pool of thread workers. def send_async(method, *arguments) - worker_pool.async.invoke(self, method, *arguments) + worker_pool.async_invoke(self, method, *arguments) end # Return a basic hash of statistics for the connection keyed with `identifier`, `started_at`, and `subscriptions`. @@ -123,7 +121,6 @@ module ActionCable transmit ActiveSupport::JSON.encode(identifier: ActionCable::INTERNAL[:identifiers][:ping], message: Time.now.to_i) end - protected # The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc. def request @@ -138,8 +135,6 @@ module ActionCable request.cookie_jar end - - protected attr_reader :websocket attr_reader :message_buffer @@ -170,7 +165,6 @@ module ActionCable disconnect if respond_to?(:disconnect) end - def allow_request_origin? return true if server.config.disable_request_forgery_protection @@ -193,7 +187,6 @@ module ActionCable [ 404, { 'Content-Type' => 'text/plain' }, [ 'Page not found' ] ] end - # Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags. def new_tagged_logger TaggedLoggerProxy.new server.logger, diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index 2d75ff8d6d..885ff3f102 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -11,7 +11,7 @@ module ActionCable end class_methods do - # Mark a key as being a connection identifier index that can then used to find the specific connection again later. + # Mark a key as being a connection identifier index that can then be used to find the specific connection again later. # Common identifiers are current_user and current_account, but could be anything really. # # Note that anything marked as an identifier will automatically create a delegate by the same name on any diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb index 65d6634bb0..d7f95e6a62 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -49,7 +49,6 @@ module ActionCable find(data).perform_action ActiveSupport::JSON.decode(data['data']) end - def identifiers subscriptions.keys end diff --git a/actioncable/lib/action_cable/process/logging.rb b/actioncable/lib/action_cable/process/logging.rb index 72b1a080d1..dce637b3ca 100644 --- a/actioncable/lib/action_cable/process/logging.rb +++ b/actioncable/lib/action_cable/process/logging.rb @@ -1,10 +1,7 @@ require 'action_cable/server' require 'eventmachine' -require 'celluloid' EM.error_handler do |e| puts "Error raised inside the event loop: #{e.message}" puts e.backtrace.join("\n") end - -Celluloid.logger = ActionCable.server.logger diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index 740e4b301e..3785bbd154 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -1,6 +1,3 @@ -# FIXME: Cargo culted fix from https://github.com/celluloid/celluloid-pool/issues/10 -require 'celluloid/current' - require 'em-hiredis' module ActionCable @@ -39,7 +36,7 @@ module ActionCable # 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) + @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. diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb index e063b2a2e1..3b6c6d44a1 100644 --- a/actioncable/lib/action_cable/server/worker.rb +++ b/actioncable/lib/action_cable/server/worker.rb @@ -1,39 +1,68 @@ -require 'celluloid' 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 - include Celluloid - attr_reader :connection + 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) - @connection = receiver + begin + self.connection = receiver - run_callbacks :work do - receiver.send method, *args + 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 - rescue Exception => e - logger.error "There was an exception - #{e.class}(#{e.message})" - logger.error e.backtrace.join("\n") + end - receiver.handle_exception if receiver.respond_to?(:handle_exception) + def async_run_periodic_timer(channel, callback) + @pool.post do + run_periodic_timer(channel, callback) + end end def run_periodic_timer(channel, callback) - @connection = channel.connection + begin + self.connection = channel.connection - run_callbacks :work do - callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback) + 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 diff --git a/actioncable/lib/assets/javascripts/action_cable/connection.coffee b/actioncable/lib/assets/javascripts/action_cable/connection.coffee index 2f69a9b26c..fbd7dbd35b 100644 --- a/actioncable/lib/assets/javascripts/action_cable/connection.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/connection.coffee @@ -79,6 +79,3 @@ class ActionCable.Connection return if @disconnected @disconnected = true @consumer.subscriptions.notifyAll("disconnected") - - toJSON: -> - state: @getState() diff --git a/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee index b594802be1..99b9a1c6d5 100644 --- a/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee @@ -69,11 +69,6 @@ class ActionCable.ConnectionMonitor @consumer.connection.reopen() , 200 - toJSON: -> - interval = @getInterval() - connectionIsStale = @connectionIsStale() - {@startedAt, @stoppedAt, @pingedAt, @reconnectAttempts, connectionIsStale, interval} - now = -> new Date().getTime() diff --git a/actioncable/lib/assets/javascripts/action_cable/consumer.coffee b/actioncable/lib/assets/javascripts/action_cable/consumer.coffee index 5cf8978d77..fcd8d0fb6c 100644 --- a/actioncable/lib/assets/javascripts/action_cable/consumer.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/consumer.coffee @@ -23,9 +23,3 @@ class ActionCable.Consumer send: (data) -> @connection.send(data) - - inspect: -> - JSON.stringify(this, null, 2) - - toJSON: -> - {@url, @subscriptions, @connection, @connectionMonitor} diff --git a/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee index 0316f76a24..ae041ffa2b 100644 --- a/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee @@ -9,7 +9,6 @@ class ActionCable.Subscriptions constructor: (@consumer) -> @subscriptions = [] - @history = [] create: (channelName, mixin) -> channel = channelName @@ -57,22 +56,9 @@ class ActionCable.Subscriptions for subscription in subscriptions subscription[callbackName]?(args...) - if callbackName in ["initialized", "connected", "disconnected", "rejected"] - {identifier} = subscription - @record(notification: {identifier, callbackName, args}) - sendCommand: (subscription, command) -> {identifier} = subscription if identifier is ActionCable.INTERNAL.identifiers.ping @consumer.connection.isOpen() else @consumer.send({command, identifier}) - - record: (data) -> - data.time = new Date() - @history = @history.slice(-19) - @history.push(data) - - toJSON: -> - history: @history - identifiers: (subscription.identifier for subscription in @subscriptions) diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index 2f37d8055b..c5d398810a 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -5,11 +5,16 @@ module Rails argument :actions, type: :array, default: [], banner: "method method" + class_option :assets, type: :boolean + check_class_collision suffix: "Channel" def create_channel_file template "channel.rb", File.join('app/channels', class_path, "#{file_name}_channel.rb") - template "assets/channel.coffee", File.join('app/assets/javascripts/channels', class_path, "#{file_name}.coffee") + + if options[:assets] + template "assets/channel.coffee", File.join('app/assets/javascripts/channels', class_path, "#{file_name}.coffee") + end end protected diff --git a/actioncable/test/connection/authorization_test.rb b/actioncable/test/connection/authorization_test.rb index 68668b2835..87d0e79ef3 100644 --- a/actioncable/test/connection/authorization_test.rb +++ b/actioncable/test/connection/authorization_test.rb @@ -10,7 +10,6 @@ class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase end def send_async(method, *args) - # Bypass Celluloid send method, *args end end diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb index da6041db4a..182562db82 100644 --- a/actioncable/test/connection/base_test.rb +++ b/actioncable/test/connection/base_test.rb @@ -14,7 +14,6 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase end def send_async(method, *args) - # Bypass Celluloid send method, *args end end diff --git a/actioncable/test/connection/cross_site_forgery_test.rb b/actioncable/test/connection/cross_site_forgery_test.rb index d445e08f2a..a29f65fb97 100644 --- a/actioncable/test/connection/cross_site_forgery_test.rb +++ b/actioncable/test/connection/cross_site_forgery_test.rb @@ -6,7 +6,6 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase class Connection < ActionCable::Connection::Base def send_async(method, *args) - # Bypass Celluloid send method, *args end end diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb index ab69df57b3..9d0bda83ef 100644 --- a/actioncable/test/connection/string_identifier_test.rb +++ b/actioncable/test/connection/string_identifier_test.rb @@ -10,7 +10,6 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase end def send_async(method, *args) - # Bypass Celluloid send method, *args end end diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb index 4f6760827e..62e41484fe 100644 --- a/actioncable/test/connection/subscriptions_test.rb +++ b/actioncable/test/connection/subscriptions_test.rb @@ -5,7 +5,6 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase attr_reader :websocket def send_async(method, *args) - # Bypass Celluloid send method, *args end end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index 12dcd98402..325305939f 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -14,11 +14,6 @@ require 'rack/mock' # Require all the stubs and models Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file } -$CELLULOID_DEBUG = false -$CELLULOID_TEST = false -require 'celluloid' -Celluloid.logger = Logger.new(StringIO.new) - require 'faye/websocket' class << Faye::WebSocket remove_method :ensure_reactor_running diff --git a/actioncable/test/worker_test.rb b/actioncable/test/worker_test.rb index 69c4b6529d..9911a3b98b 100644 --- a/actioncable/test/worker_test.rb +++ b/actioncable/test/worker_test.rb @@ -17,8 +17,6 @@ class WorkerTest < ActiveSupport::TestCase end setup do - Celluloid.boot - @worker = ActionCable::Server::Worker.new @receiver = Receiver.new end |