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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -6
- data/README.md +11 -15
- data/lib/action_cable.rb +5 -4
- data/lib/action_cable/channel/base.rb +19 -6
- data/lib/action_cable/channel/periodic_timers.rb +45 -7
- data/lib/action_cable/channel/streams.rb +70 -14
- data/lib/action_cable/connection.rb +2 -0
- data/lib/action_cable/connection/base.rb +33 -21
- data/lib/action_cable/connection/client_socket.rb +17 -9
- data/lib/action_cable/connection/faye_client_socket.rb +48 -0
- data/lib/action_cable/connection/faye_event_loop.rb +44 -0
- data/lib/action_cable/connection/internal_channel.rb +3 -5
- data/lib/action_cable/connection/message_buffer.rb +2 -2
- data/lib/action_cable/connection/stream.rb +9 -11
- data/lib/action_cable/connection/stream_event_loop.rb +10 -1
- data/lib/action_cable/connection/web_socket.rb +6 -2
- data/lib/action_cable/engine.rb +37 -1
- data/lib/action_cable/gem_version.rb +1 -1
- data/lib/action_cable/helpers/action_cable_helper.rb +19 -8
- data/lib/action_cable/remote_connections.rb +1 -1
- data/lib/action_cable/server/base.rb +26 -6
- data/lib/action_cable/server/broadcasting.rb +10 -9
- data/lib/action_cable/server/configuration.rb +19 -3
- data/lib/action_cable/server/connections.rb +3 -3
- data/lib/action_cable/server/worker.rb +27 -27
- data/lib/action_cable/server/worker/active_record_connection_management.rb +0 -3
- data/lib/action_cable/subscription_adapter/async.rb +8 -3
- data/lib/action_cable/subscription_adapter/evented_redis.rb +5 -1
- data/lib/action_cable/subscription_adapter/postgresql.rb +5 -4
- data/lib/action_cable/subscription_adapter/redis.rb +11 -6
- data/lib/assets/compiled/action_cable.js +248 -188
- data/lib/rails/generators/channel/USAGE +1 -1
- data/lib/rails/generators/channel/channel_generator.rb +4 -1
- data/lib/rails/generators/channel/templates/assets/cable.js +13 -0
- 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,
|
33
|
-
@env
|
34
|
-
@event_target
|
35
|
-
@
|
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(@
|
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
|
-
|
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|
|
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
|
-
|
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.
|
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
|
-
|
43
|
-
|
44
|
-
return unless @socket_object.env['rack.hijack']
|
40
|
+
def hijack_rack_socket
|
41
|
+
return unless @socket_object.env['rack.hijack']
|
45
42
|
|
46
|
-
|
47
|
-
|
43
|
+
@socket_object.env['rack.hijack'].call
|
44
|
+
@rack_hijack_io = @socket_object.env['rack.hijack_io']
|
48
45
|
|
49
|
-
|
50
|
-
|
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
|
-
|
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,
|
8
|
-
@websocket = ::WebSocket::Driver.websocket?(env) ?
|
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
|
data/lib/action_cable/engine.rb
CHANGED
@@ -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.
|
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
|
@@ -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
|
5
|
-
# configuration. Ensure this is above your
|
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
|
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
|
-
#
|
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
|
21
|
-
# config
|
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:
|
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
|
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 '
|
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 =
|
22
|
-
@remote_connections = @
|
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
|
42
|
-
@
|
51
|
+
def event_loop
|
52
|
+
@event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
|
43
53
|
end
|
44
54
|
|
45
|
-
# The
|
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
|