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.
- 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
|