actioncable 5.0.0.beta3 → 5.0.0.beta4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -6
  3. data/README.md +11 -15
  4. data/lib/action_cable.rb +5 -4
  5. data/lib/action_cable/channel/base.rb +19 -6
  6. data/lib/action_cable/channel/periodic_timers.rb +45 -7
  7. data/lib/action_cable/channel/streams.rb +70 -14
  8. data/lib/action_cable/connection.rb +2 -0
  9. data/lib/action_cable/connection/base.rb +33 -21
  10. data/lib/action_cable/connection/client_socket.rb +17 -9
  11. data/lib/action_cable/connection/faye_client_socket.rb +48 -0
  12. data/lib/action_cable/connection/faye_event_loop.rb +44 -0
  13. data/lib/action_cable/connection/internal_channel.rb +3 -5
  14. data/lib/action_cable/connection/message_buffer.rb +2 -2
  15. data/lib/action_cable/connection/stream.rb +9 -11
  16. data/lib/action_cable/connection/stream_event_loop.rb +10 -1
  17. data/lib/action_cable/connection/web_socket.rb +6 -2
  18. data/lib/action_cable/engine.rb +37 -1
  19. data/lib/action_cable/gem_version.rb +1 -1
  20. data/lib/action_cable/helpers/action_cable_helper.rb +19 -8
  21. data/lib/action_cable/remote_connections.rb +1 -1
  22. data/lib/action_cable/server/base.rb +26 -6
  23. data/lib/action_cable/server/broadcasting.rb +10 -9
  24. data/lib/action_cable/server/configuration.rb +19 -3
  25. data/lib/action_cable/server/connections.rb +3 -3
  26. data/lib/action_cable/server/worker.rb +27 -27
  27. data/lib/action_cable/server/worker/active_record_connection_management.rb +0 -3
  28. data/lib/action_cable/subscription_adapter/async.rb +8 -3
  29. data/lib/action_cable/subscription_adapter/evented_redis.rb +5 -1
  30. data/lib/action_cable/subscription_adapter/postgresql.rb +5 -4
  31. data/lib/action_cable/subscription_adapter/redis.rb +11 -6
  32. data/lib/assets/compiled/action_cable.js +248 -188
  33. data/lib/rails/generators/channel/USAGE +1 -1
  34. data/lib/rails/generators/channel/channel_generator.rb +4 -1
  35. data/lib/rails/generators/channel/templates/assets/cable.js +13 -0
  36. metadata +8 -5
@@ -29,10 +29,10 @@ module ActionCable
29
29
 
30
30
  attr_reader :env, :url
31
31
 
32
- def initialize(env, event_target, stream_event_loop)
33
- @env = env
34
- @event_target = event_target
35
- @stream_event_loop = stream_event_loop
32
+ def initialize(env, event_target, event_loop, protocols)
33
+ @env = env
34
+ @event_target = event_target
35
+ @event_loop = event_loop
36
36
 
37
37
  @url = ClientSocket.determine_url(@env)
38
38
 
@@ -42,22 +42,24 @@ module ActionCable
42
42
  @ready_state = CONNECTING
43
43
 
44
44
  # The driver calls +env+, +url+, and +write+
45
- @driver = ::WebSocket::Driver.rack(self)
45
+ @driver = ::WebSocket::Driver.rack(self, protocols: protocols)
46
46
 
47
47
  @driver.on(:open) { |e| open }
48
48
  @driver.on(:message) { |e| receive_message(e.data) }
49
49
  @driver.on(:close) { |e| begin_close(e.reason, e.code) }
50
50
  @driver.on(:error) { |e| emit_error(e.message) }
51
51
 
52
- @stream = ActionCable::Connection::Stream.new(@stream_event_loop, self)
52
+ @stream = ActionCable::Connection::Stream.new(@event_loop, self)
53
+ end
54
+
55
+ def start_driver
56
+ return if @driver.nil? || @driver_started
57
+ @stream.hijack_rack_socket
53
58
 
54
59
  if callback = @env['async.callback']
55
60
  callback.call([101, {}, @stream])
56
61
  end
57
- end
58
62
 
59
- def start_driver
60
- return if @driver.nil? || @driver_started
61
63
  @driver_started = true
62
64
  @driver.start
63
65
  end
@@ -69,6 +71,8 @@ module ActionCable
69
71
 
70
72
  def write(data)
71
73
  @stream.write(data)
74
+ rescue => e
75
+ emit_error e.message
72
76
  end
73
77
 
74
78
  def transmit(message)
@@ -107,6 +111,10 @@ module ActionCable
107
111
  @ready_state == OPEN
108
112
  end
109
113
 
114
+ def protocol
115
+ @driver.protocol
116
+ end
117
+
110
118
  private
111
119
  def open
112
120
  return unless @ready_state == CONNECTING
@@ -0,0 +1,48 @@
1
+ require 'faye/websocket'
2
+
3
+ module ActionCable
4
+ module Connection
5
+ class FayeClientSocket
6
+ def initialize(env, event_target, stream_event_loop, protocols)
7
+ @env = env
8
+ @event_target = event_target
9
+ @protocols = protocols
10
+
11
+ @faye = nil
12
+ end
13
+
14
+ def alive?
15
+ @faye && @faye.ready_state == Faye::WebSocket::API::OPEN
16
+ end
17
+
18
+ def transmit(data)
19
+ connect
20
+ @faye.send data
21
+ end
22
+
23
+ def close
24
+ @faye && @faye.close
25
+ end
26
+
27
+ def protocol
28
+ @faye && @faye.protocol
29
+ end
30
+
31
+ def rack_response
32
+ connect
33
+ @faye.rack_response
34
+ end
35
+
36
+ private
37
+ def connect
38
+ return if @faye
39
+ @faye = Faye::WebSocket.new(@env, @protocols)
40
+
41
+ @faye.on(:open) { |event| @event_target.on_open }
42
+ @faye.on(:message) { |event| @event_target.on_message(event.data) }
43
+ @faye.on(:close) { |event| @event_target.on_close(event.reason, event.code) }
44
+ @faye.on(:error) { |event| @event_target.on_error(event.message) }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ require 'thread'
2
+
3
+ require 'eventmachine'
4
+ EventMachine.epoll if EventMachine.epoll?
5
+ EventMachine.kqueue if EventMachine.kqueue?
6
+
7
+ module ActionCable
8
+ module Connection
9
+ class FayeEventLoop
10
+ @@mutex = Mutex.new
11
+
12
+ def timer(interval, &block)
13
+ ensure_reactor_running
14
+ EMTimer.new(::EM::PeriodicTimer.new(interval, &block))
15
+ end
16
+
17
+ def post(task = nil, &block)
18
+ task ||= block
19
+
20
+ ensure_reactor_running
21
+ ::EM.next_tick(&task)
22
+ end
23
+
24
+ private
25
+ def ensure_reactor_running
26
+ return if EventMachine.reactor_running?
27
+ @@mutex.synchronize do
28
+ Thread.new { EventMachine.run } unless EventMachine.reactor_running?
29
+ Thread.pass until EventMachine.reactor_running?
30
+ end
31
+ end
32
+
33
+ class EMTimer
34
+ def initialize(inner)
35
+ @inner = inner
36
+ end
37
+
38
+ def shutdown
39
+ @inner.cancel
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -11,24 +11,22 @@ module ActionCable
11
11
 
12
12
  def subscribe_to_internal_channel
13
13
  if connection_identifier.present?
14
- callback = -> (message) { process_internal_message(message) }
14
+ callback = -> (message) { process_internal_message decode(message) }
15
15
  @_internal_subscriptions ||= []
16
16
  @_internal_subscriptions << [ internal_channel, callback ]
17
17
 
18
- Concurrent.global_io_executor.post { pubsub.subscribe(internal_channel, callback) }
18
+ server.event_loop.post { pubsub.subscribe(internal_channel, callback) }
19
19
  logger.info "Registered connection (#{connection_identifier})"
20
20
  end
21
21
  end
22
22
 
23
23
  def unsubscribe_from_internal_channel
24
24
  if @_internal_subscriptions.present?
25
- @_internal_subscriptions.each { |channel, callback| Concurrent.global_io_executor.post { pubsub.unsubscribe(channel, callback) } }
25
+ @_internal_subscriptions.each { |channel, callback| server.event_loop.post { pubsub.unsubscribe(channel, callback) } }
26
26
  end
27
27
  end
28
28
 
29
29
  def process_internal_message(message)
30
- message = ActiveSupport::JSON.decode(message)
31
-
32
30
  case message['type']
33
31
  when 'disconnect'
34
32
  logger.info "Removing connection (#{connection_identifier})"
@@ -30,7 +30,7 @@ module ActionCable
30
30
 
31
31
  protected
32
32
  attr_reader :connection
33
- attr_accessor :buffered_messages
33
+ attr_reader :buffered_messages
34
34
 
35
35
  private
36
36
  def valid?(message)
@@ -38,7 +38,7 @@ module ActionCable
38
38
  end
39
39
 
40
40
  def receive(message)
41
- connection.send_async :receive, message
41
+ connection.receive message
42
42
  end
43
43
 
44
44
  def buffer(message)
@@ -4,15 +4,13 @@ module ActionCable
4
4
  # This class is heavily based on faye-websocket-ruby
5
5
  #
6
6
  # Copyright (c) 2010-2015 James Coglan
7
- class Stream
7
+ class Stream # :nodoc:
8
8
  def initialize(event_loop, socket)
9
9
  @event_loop = event_loop
10
10
  @socket_object = socket
11
11
  @stream_send = socket.env['stream.send']
12
12
 
13
13
  @rack_hijack_io = nil
14
-
15
- hijack_rack_socket
16
14
  end
17
15
 
18
16
  def each(&callback)
@@ -31,7 +29,7 @@ module ActionCable
31
29
  def write(data)
32
30
  return @rack_hijack_io.write(data) if @rack_hijack_io
33
31
  return @stream_send.call(data) if @stream_send
34
- rescue EOFError
32
+ rescue EOFError, Errno::ECONNRESET
35
33
  @socket_object.client_gone
36
34
  end
37
35
 
@@ -39,16 +37,16 @@ module ActionCable
39
37
  @socket_object.parse(data)
40
38
  end
41
39
 
42
- private
43
- def hijack_rack_socket
44
- return unless @socket_object.env['rack.hijack']
40
+ def hijack_rack_socket
41
+ return unless @socket_object.env['rack.hijack']
45
42
 
46
- @socket_object.env['rack.hijack'].call
47
- @rack_hijack_io = @socket_object.env['rack.hijack_io']
43
+ @socket_object.env['rack.hijack'].call
44
+ @rack_hijack_io = @socket_object.env['rack.hijack_io']
48
45
 
49
- @event_loop.attach(@rack_hijack_io, self)
50
- end
46
+ @event_loop.attach(@rack_hijack_io, self)
47
+ end
51
48
 
49
+ private
52
50
  def clean_rack_hijack
53
51
  return unless @rack_hijack_io
54
52
  @event_loop.detach(@rack_hijack_io, self)
@@ -11,7 +11,16 @@ module ActionCable
11
11
  @todo = Queue.new
12
12
 
13
13
  @spawn_mutex = Mutex.new
14
- spawn
14
+ end
15
+
16
+ def timer(interval, &block)
17
+ Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute)
18
+ end
19
+
20
+ def post(task = nil, &block)
21
+ task ||= block
22
+
23
+ Concurrent.global_io_executor << task
15
24
  end
16
25
 
17
26
  def attach(io, stream)
@@ -4,8 +4,8 @@ module ActionCable
4
4
  module Connection
5
5
  # Wrap the real socket to minimize the externally-presented API
6
6
  class WebSocket
7
- def initialize(env, event_target, stream_event_loop)
8
- @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, stream_event_loop) : nil
7
+ def initialize(env, event_target, event_loop, client_socket_class, protocols: ActionCable::INTERNAL[:protocols])
8
+ @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop, protocols) : nil
9
9
  end
10
10
 
11
11
  def possible?
@@ -24,6 +24,10 @@ module ActionCable
24
24
  websocket.close
25
25
  end
26
26
 
27
+ def protocol
28
+ websocket.protocol
29
+ end
30
+
27
31
  def rack_response
28
32
  websocket.rack_response
29
33
  end
@@ -6,7 +6,7 @@ require "active_support/core_ext/hash/indifferent_access"
6
6
  module ActionCable
7
7
  class Railtie < Rails::Engine # :nodoc:
8
8
  config.action_cable = ActiveSupport::OrderedOptions.new
9
- config.action_cable.url = '/cable'
9
+ config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
10
10
 
11
11
  config.eager_load_namespaces << ActionCable
12
12
 
@@ -40,5 +40,41 @@ module ActionCable
40
40
  options.each { |k,v| send("#{k}=", v) }
41
41
  end
42
42
  end
43
+
44
+ initializer "action_cable.routes" do
45
+ config.after_initialize do |app|
46
+ config = app.config
47
+ unless config.action_cable.mount_path.nil?
48
+ app.routes.prepend do
49
+ mount ActionCable.server => config.action_cable.mount_path, internal: true
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ initializer "action_cable.set_work_hooks" do |app|
56
+ ActiveSupport.on_load(:action_cable) do
57
+ ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
58
+ app.executor.wrap do
59
+ # If we took a while to get the lock, we may have been halted
60
+ # in the meantime. As we haven't started doing any real work
61
+ # yet, we should pretend that we never made it off the queue.
62
+ unless stopping?
63
+ inner.call
64
+ end
65
+ end
66
+ end
67
+
68
+ wrap = lambda do |_, inner|
69
+ app.executor.wrap(&inner)
70
+ end
71
+ ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
72
+ ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
73
+
74
+ app.reloader.before_class_unload do
75
+ ActionCable.server.restart
76
+ end
77
+ end
78
+ end
43
79
  end
44
80
  end
@@ -8,7 +8,7 @@ module ActionCable
8
8
  MAJOR = 5
9
9
  MINOR = 0
10
10
  TINY = 0
11
- PRE = "beta3"
11
+ PRE = "beta4"
12
12
 
13
13
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14
14
  end
@@ -1,28 +1,39 @@
1
1
  module ActionCable
2
2
  module Helpers
3
3
  module ActionCableHelper
4
- # Returns an "action-cable-url" meta tag with the value of the url specified in your
5
- # configuration. Ensure this is above your javascript tag:
4
+ # Returns an "action-cable-url" meta tag with the value of the URL specified in your
5
+ # configuration. Ensure this is above your JavaScript tag:
6
6
  #
7
7
  # <head>
8
8
  # <%= action_cable_meta_tag %>
9
9
  # <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
10
10
  # </head>
11
11
  #
12
- # This is then used by Action Cable to determine the url of your WebSocket server.
12
+ # This is then used by Action Cable to determine the URL of your WebSocket server.
13
13
  # Your CoffeeScript can then connect to the server without needing to specify the
14
- # url directly:
14
+ # URL directly:
15
15
  #
16
16
  # #= require cable
17
17
  # @App = {}
18
18
  # App.cable = Cable.createConsumer()
19
19
  #
20
- # Make sure to specify the correct server location in each of your environments
21
- # config file:
20
+ # Make sure to specify the correct server location in each of your environment
21
+ # config files:
22
+ #
23
+ # config.action_cable.mount_path = "/cable123"
24
+ # <%= action_cable_meta_tag %> would render:
25
+ # => <meta name="action-cable-url" content="/cable123" />
26
+ #
27
+ # config.action_cable.url = "ws://actioncable.com"
28
+ # <%= action_cable_meta_tag %> would render:
29
+ # => <meta name="action-cable-url" content="ws://actioncable.com" />
22
30
  #
23
- # config.action_cable.url = "ws://example.com:28080"
24
31
  def action_cable_meta_tag
25
- tag "meta", name: "action-cable-url", content: Rails.application.config.action_cable.url
32
+ tag "meta", name: "action-cable-url", content: (
33
+ ActionCable.server.config.url ||
34
+ ActionCable.server.config.mount_path ||
35
+ raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
36
+ )
26
37
  end
27
38
  end
28
39
  end
@@ -28,7 +28,7 @@ module ActionCable
28
28
 
29
29
  private
30
30
  # Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
31
- # Exists for the solely for the purpose of calling #disconnect on that connection.
31
+ # Exists solely for the purpose of calling #disconnect on that connection.
32
32
  class RemoteConnection
33
33
  class InvalidIdentifiersError < StandardError; end
34
34
 
@@ -1,4 +1,4 @@
1
- require 'thread'
1
+ require 'monitor'
2
2
 
3
3
  module ActionCable
4
4
  module Server
@@ -18,8 +18,8 @@ module ActionCable
18
18
  attr_reader :mutex
19
19
 
20
20
  def initialize
21
- @mutex = Mutex.new
22
- @remote_connections = @stream_event_loop = @worker_pool = @channel_classes = @pubsub = nil
21
+ @mutex = Monitor.new
22
+ @remote_connections = @event_loop = @worker_pool = @channel_classes = @pubsub = nil
23
23
  end
24
24
 
25
25
  # Called by Rack to setup the server.
@@ -33,16 +33,36 @@ module ActionCable
33
33
  remote_connections.where(identifiers).disconnect
34
34
  end
35
35
 
36
+ def restart
37
+ connections.each(&:close)
38
+
39
+ @mutex.synchronize do
40
+ worker_pool.halt if @worker_pool
41
+
42
+ @worker_pool = nil
43
+ end
44
+ end
45
+
36
46
  # Gateway to RemoteConnections. See that class for details.
37
47
  def remote_connections
38
48
  @remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
39
49
  end
40
50
 
41
- def stream_event_loop
42
- @stream_event_loop || @mutex.synchronize { @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new }
51
+ def event_loop
52
+ @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
43
53
  end
44
54
 
45
- # The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
55
+ # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
56
+ # 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
57
+ # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size.
58
+ #
59
+ # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
60
+ # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
61
+ # connections.
62
+ #
63
+ # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
64
+ # the db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
65
+ # db connection pool instead.
46
66
  def worker_pool
47
67
  @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
48
68
  end