actioncable 5.0.0.beta3 → 5.0.0.beta4

Sign up to get free protection for your applications and to get access to all the features.
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