actioncable 5.0.0.beta1.1 → 5.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -3
- data/MIT-LICENSE +1 -1
- data/README.md +60 -44
- data/lib/action_cable.rb +2 -1
- data/lib/action_cable/channel/base.rb +2 -2
- data/lib/action_cable/channel/periodic_timers.rb +3 -3
- data/lib/action_cable/channel/streams.rb +4 -4
- data/lib/action_cable/connection.rb +4 -1
- data/lib/action_cable/connection/base.rb +22 -21
- data/lib/action_cable/connection/client_socket.rb +150 -0
- data/lib/action_cable/connection/identification.rb +1 -1
- data/lib/action_cable/connection/internal_channel.rb +6 -6
- data/lib/action_cable/connection/stream.rb +59 -0
- data/lib/action_cable/connection/stream_event_loop.rb +94 -0
- data/lib/action_cable/connection/subscriptions.rb +0 -1
- data/lib/action_cable/connection/web_socket.rb +14 -8
- data/lib/action_cable/engine.rb +3 -3
- data/lib/action_cable/gem_version.rb +1 -1
- data/lib/action_cable/remote_connections.rb +1 -1
- data/lib/action_cable/server.rb +0 -4
- data/lib/action_cable/server/base.rb +19 -22
- data/lib/action_cable/server/broadcasting.rb +1 -8
- data/lib/action_cable/server/configuration.rb +25 -5
- data/lib/action_cable/server/connections.rb +3 -5
- data/lib/action_cable/server/worker.rb +42 -13
- data/lib/action_cable/subscription_adapter.rb +8 -0
- data/lib/action_cable/subscription_adapter/async.rb +22 -0
- data/lib/action_cable/subscription_adapter/base.rb +28 -0
- data/lib/action_cable/subscription_adapter/evented_redis.rb +67 -0
- data/lib/action_cable/subscription_adapter/inline.rb +35 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +106 -0
- data/lib/action_cable/subscription_adapter/redis.rb +163 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +53 -0
- data/lib/assets/compiled/action_cable.js +473 -0
- data/lib/rails/generators/channel/channel_generator.rb +6 -1
- metadata +21 -99
- data/lib/action_cable/process/logging.rb +0 -10
- data/lib/assets/javascripts/action_cable.coffee.erb +0 -23
- data/lib/assets/javascripts/action_cable/connection.coffee +0 -84
- data/lib/assets/javascripts/action_cable/connection_monitor.coffee +0 -84
- data/lib/assets/javascripts/action_cable/consumer.coffee +0 -31
- data/lib/assets/javascripts/action_cable/subscription.coffee +0 -68
- data/lib/assets/javascripts/action_cable/subscriptions.coffee +0 -78
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'websocket/driver'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
#--
|
6
|
+
# This class is heavily based on faye-websocket-ruby
|
7
|
+
#
|
8
|
+
# Copyright (c) 2010-2015 James Coglan
|
9
|
+
class ClientSocket # :nodoc:
|
10
|
+
def self.determine_url(env)
|
11
|
+
scheme = secure_request?(env) ? 'wss:' : 'ws:'
|
12
|
+
"#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.secure_request?(env)
|
16
|
+
return true if env['HTTPS'] == 'on'
|
17
|
+
return true if env['HTTP_X_FORWARDED_SSL'] == 'on'
|
18
|
+
return true if env['HTTP_X_FORWARDED_SCHEME'] == 'https'
|
19
|
+
return true if env['HTTP_X_FORWARDED_PROTO'] == 'https'
|
20
|
+
return true if env['rack.url_scheme'] == 'https'
|
21
|
+
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
CONNECTING = 0
|
26
|
+
OPEN = 1
|
27
|
+
CLOSING = 2
|
28
|
+
CLOSED = 3
|
29
|
+
|
30
|
+
attr_reader :env, :url
|
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
|
36
|
+
|
37
|
+
@url = ClientSocket.determine_url(@env)
|
38
|
+
|
39
|
+
@driver = @driver_started = nil
|
40
|
+
@close_params = ['', 1006]
|
41
|
+
|
42
|
+
@ready_state = CONNECTING
|
43
|
+
|
44
|
+
# The driver calls +env+, +url+, and +write+
|
45
|
+
@driver = ::WebSocket::Driver.rack(self)
|
46
|
+
|
47
|
+
@driver.on(:open) { |e| open }
|
48
|
+
@driver.on(:message) { |e| receive_message(e.data) }
|
49
|
+
@driver.on(:close) { |e| begin_close(e.reason, e.code) }
|
50
|
+
@driver.on(:error) { |e| emit_error(e.message) }
|
51
|
+
|
52
|
+
@stream = ActionCable::Connection::Stream.new(@stream_event_loop, self)
|
53
|
+
|
54
|
+
if callback = @env['async.callback']
|
55
|
+
callback.call([101, {}, @stream])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def start_driver
|
60
|
+
return if @driver.nil? || @driver_started
|
61
|
+
@driver_started = true
|
62
|
+
@driver.start
|
63
|
+
end
|
64
|
+
|
65
|
+
def rack_response
|
66
|
+
start_driver
|
67
|
+
[ -1, {}, [] ]
|
68
|
+
end
|
69
|
+
|
70
|
+
def write(data)
|
71
|
+
@stream.write(data)
|
72
|
+
end
|
73
|
+
|
74
|
+
def transmit(message)
|
75
|
+
return false if @ready_state > OPEN
|
76
|
+
case message
|
77
|
+
when Numeric then @driver.text(message.to_s)
|
78
|
+
when String then @driver.text(message)
|
79
|
+
when Array then @driver.binary(message)
|
80
|
+
else false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def close(code = nil, reason = nil)
|
85
|
+
code ||= 1000
|
86
|
+
reason ||= ''
|
87
|
+
|
88
|
+
unless code == 1000 or (code >= 3000 and code <= 4999)
|
89
|
+
raise ArgumentError, "Failed to execute 'close' on WebSocket: " +
|
90
|
+
"The code must be either 1000, or between 3000 and 4999. " +
|
91
|
+
"#{code} is neither."
|
92
|
+
end
|
93
|
+
|
94
|
+
@ready_state = CLOSING unless @ready_state == CLOSED
|
95
|
+
@driver.close(reason, code)
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse(data)
|
99
|
+
@driver.parse(data)
|
100
|
+
end
|
101
|
+
|
102
|
+
def client_gone
|
103
|
+
finalize_close
|
104
|
+
end
|
105
|
+
|
106
|
+
def alive?
|
107
|
+
@ready_state == OPEN
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
def open
|
112
|
+
return unless @ready_state == CONNECTING
|
113
|
+
@ready_state = OPEN
|
114
|
+
|
115
|
+
@event_target.on_open
|
116
|
+
end
|
117
|
+
|
118
|
+
def receive_message(data)
|
119
|
+
return unless @ready_state == OPEN
|
120
|
+
|
121
|
+
@event_target.on_message(data)
|
122
|
+
end
|
123
|
+
|
124
|
+
def emit_error(message)
|
125
|
+
return if @ready_state >= CLOSING
|
126
|
+
|
127
|
+
@event_target.on_error(message)
|
128
|
+
end
|
129
|
+
|
130
|
+
def begin_close(reason, code)
|
131
|
+
return if @ready_state == CLOSED
|
132
|
+
@ready_state = CLOSING
|
133
|
+
@close_params = [reason, code]
|
134
|
+
|
135
|
+
if @stream
|
136
|
+
@stream.shutdown
|
137
|
+
else
|
138
|
+
finalize_close
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def finalize_close
|
143
|
+
return if @ready_state == CLOSED
|
144
|
+
@ready_state = CLOSED
|
145
|
+
|
146
|
+
@event_target.on_close(*@close_params)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -11,7 +11,7 @@ module ActionCable
|
|
11
11
|
end
|
12
12
|
|
13
13
|
class_methods do
|
14
|
-
# Mark a key as being a connection identifier index that can then used to find the specific connection again later.
|
14
|
+
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
|
15
15
|
# Common identifiers are current_user and current_account, but could be anything really.
|
16
16
|
#
|
17
17
|
# Note that anything marked as an identifier will automatically create a delegate by the same name on any
|
@@ -5,24 +5,24 @@ module ActionCable
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
private
|
8
|
-
def
|
8
|
+
def internal_channel
|
9
9
|
"action_cable/#{connection_identifier}"
|
10
10
|
end
|
11
11
|
|
12
12
|
def subscribe_to_internal_channel
|
13
13
|
if connection_identifier.present?
|
14
14
|
callback = -> (message) { process_internal_message(message) }
|
15
|
-
@
|
16
|
-
@
|
15
|
+
@_internal_subscriptions ||= []
|
16
|
+
@_internal_subscriptions << [ internal_channel, callback ]
|
17
17
|
|
18
|
-
|
18
|
+
Concurrent.global_io_executor.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
|
-
if @
|
25
|
-
@
|
24
|
+
if @_internal_subscriptions.present?
|
25
|
+
@_internal_subscriptions.each { |channel, callback| Concurrent.global_io_executor.post { pubsub.unsubscribe(channel, callback) } }
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Connection
|
3
|
+
#--
|
4
|
+
# This class is heavily based on faye-websocket-ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2010-2015 James Coglan
|
7
|
+
class Stream
|
8
|
+
def initialize(event_loop, socket)
|
9
|
+
@event_loop = event_loop
|
10
|
+
@socket_object = socket
|
11
|
+
@stream_send = socket.env['stream.send']
|
12
|
+
|
13
|
+
@rack_hijack_io = nil
|
14
|
+
|
15
|
+
hijack_rack_socket
|
16
|
+
end
|
17
|
+
|
18
|
+
def each(&callback)
|
19
|
+
@stream_send ||= callback
|
20
|
+
end
|
21
|
+
|
22
|
+
def close
|
23
|
+
shutdown
|
24
|
+
@socket_object.client_gone
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
clean_rack_hijack
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(data)
|
32
|
+
return @rack_hijack_io.write(data) if @rack_hijack_io
|
33
|
+
return @stream_send.call(data) if @stream_send
|
34
|
+
rescue EOFError
|
35
|
+
@socket_object.client_gone
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive(data)
|
39
|
+
@socket_object.parse(data)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def hijack_rack_socket
|
44
|
+
return unless @socket_object.env['rack.hijack']
|
45
|
+
|
46
|
+
@socket_object.env['rack.hijack'].call
|
47
|
+
@rack_hijack_io = @socket_object.env['rack.hijack_io']
|
48
|
+
|
49
|
+
@event_loop.attach(@rack_hijack_io, self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def clean_rack_hijack
|
53
|
+
return unless @rack_hijack_io
|
54
|
+
@event_loop.detach(@rack_hijack_io, self)
|
55
|
+
@rack_hijack_io = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'nio'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module ActionCable
|
5
|
+
module Connection
|
6
|
+
class StreamEventLoop
|
7
|
+
def initialize
|
8
|
+
@nio = @thread = nil
|
9
|
+
@map = {}
|
10
|
+
@stopping = false
|
11
|
+
@todo = Queue.new
|
12
|
+
|
13
|
+
@spawn_mutex = Mutex.new
|
14
|
+
spawn
|
15
|
+
end
|
16
|
+
|
17
|
+
def attach(io, stream)
|
18
|
+
@todo << lambda do
|
19
|
+
@map[io] = stream
|
20
|
+
@nio.register(io, :r)
|
21
|
+
end
|
22
|
+
wakeup
|
23
|
+
end
|
24
|
+
|
25
|
+
def detach(io, stream)
|
26
|
+
@todo << lambda do
|
27
|
+
@nio.deregister io
|
28
|
+
@map.delete io
|
29
|
+
end
|
30
|
+
wakeup
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
@stopping = true
|
35
|
+
wakeup if @nio
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def spawn
|
40
|
+
return if @thread && @thread.status
|
41
|
+
|
42
|
+
@spawn_mutex.synchronize do
|
43
|
+
return if @thread && @thread.status
|
44
|
+
|
45
|
+
@nio ||= NIO::Selector.new
|
46
|
+
@thread = Thread.new { run }
|
47
|
+
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def wakeup
|
53
|
+
spawn || @nio.wakeup
|
54
|
+
end
|
55
|
+
|
56
|
+
def run
|
57
|
+
loop do
|
58
|
+
if @stopping
|
59
|
+
@nio.close
|
60
|
+
break
|
61
|
+
end
|
62
|
+
|
63
|
+
until @todo.empty?
|
64
|
+
@todo.pop(true).call
|
65
|
+
end
|
66
|
+
|
67
|
+
next unless monitors = @nio.select
|
68
|
+
|
69
|
+
monitors.each do |monitor|
|
70
|
+
io = monitor.io
|
71
|
+
stream = @map[io]
|
72
|
+
|
73
|
+
begin
|
74
|
+
stream.receive io.read_nonblock(4096)
|
75
|
+
rescue IO::WaitReadable
|
76
|
+
next
|
77
|
+
rescue
|
78
|
+
# We expect one of EOFError or Errno::ECONNRESET in
|
79
|
+
# normal operation (when the client goes away). But if
|
80
|
+
# anything else goes wrong, this is still the best way
|
81
|
+
# to handle it.
|
82
|
+
begin
|
83
|
+
stream.close
|
84
|
+
rescue
|
85
|
+
@nio.deregister io
|
86
|
+
@map.delete io
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,13 +1,11 @@
|
|
1
|
-
require '
|
1
|
+
require 'websocket/driver'
|
2
2
|
|
3
3
|
module ActionCable
|
4
4
|
module Connection
|
5
|
-
#
|
5
|
+
# Wrap the real socket to minimize the externally-presented API
|
6
6
|
class WebSocket
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(env)
|
10
|
-
@websocket = Faye::WebSocket.websocket?(env) ? Faye::WebSocket.new(env) : nil
|
7
|
+
def initialize(env, event_target, stream_event_loop)
|
8
|
+
@websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, stream_event_loop) : nil
|
11
9
|
end
|
12
10
|
|
13
11
|
def possible?
|
@@ -15,11 +13,19 @@ module ActionCable
|
|
15
13
|
end
|
16
14
|
|
17
15
|
def alive?
|
18
|
-
websocket && websocket.
|
16
|
+
websocket && websocket.alive?
|
19
17
|
end
|
20
18
|
|
21
19
|
def transmit(data)
|
22
|
-
websocket.
|
20
|
+
websocket.transmit data
|
21
|
+
end
|
22
|
+
|
23
|
+
def close
|
24
|
+
websocket.close
|
25
|
+
end
|
26
|
+
|
27
|
+
def rack_response
|
28
|
+
websocket.rack_response
|
23
29
|
end
|
24
30
|
|
25
31
|
protected
|
data/lib/action_cable/engine.rb
CHANGED
@@ -24,11 +24,11 @@ module ActionCable
|
|
24
24
|
options = app.config.action_cable
|
25
25
|
options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?
|
26
26
|
|
27
|
-
app.paths.add "config/
|
27
|
+
app.paths.add "config/cable", with: "config/cable.yml"
|
28
28
|
|
29
29
|
ActiveSupport.on_load(:action_cable) do
|
30
|
-
if (
|
31
|
-
self.
|
30
|
+
if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
|
31
|
+
self.cable = Rails.application.config_for(config_path).with_indifferent_access
|
32
32
|
end
|
33
33
|
|
34
34
|
options.each { |k,v| send("#{k}=", v) }
|
@@ -39,7 +39,7 @@ module ActionCable
|
|
39
39
|
|
40
40
|
# Uses the internal channel to disconnect the connection.
|
41
41
|
def disconnect
|
42
|
-
server.broadcast
|
42
|
+
server.broadcast internal_channel, type: 'disconnect'
|
43
43
|
end
|
44
44
|
|
45
45
|
# Returns all the identifiers that were applied to this connection.
|
data/lib/action_cable/server.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
require 'celluloid/current'
|
3
|
-
|
4
|
-
require 'em-hiredis'
|
1
|
+
require 'thread'
|
5
2
|
|
6
3
|
module ActionCable
|
7
4
|
module Server
|
@@ -18,7 +15,12 @@ module ActionCable
|
|
18
15
|
def self.logger; config.logger; end
|
19
16
|
delegate :logger, to: :config
|
20
17
|
|
18
|
+
attr_reader :mutex
|
19
|
+
|
21
20
|
def initialize
|
21
|
+
@mutex = Mutex.new
|
22
|
+
|
23
|
+
@remote_connections = @stream_event_loop = @worker_pool = @channel_classes = @pubsub = nil
|
22
24
|
end
|
23
25
|
|
24
26
|
# Called by rack to setup the server.
|
@@ -34,36 +36,31 @@ module ActionCable
|
|
34
36
|
|
35
37
|
# Gateway to RemoteConnections. See that class for details.
|
36
38
|
def remote_connections
|
37
|
-
@remote_connections ||= RemoteConnections.new(self)
|
39
|
+
@remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def stream_event_loop
|
43
|
+
@stream_event_loop || @mutex.synchronize { @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new }
|
38
44
|
end
|
39
45
|
|
40
46
|
# The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
|
41
47
|
def worker_pool
|
42
|
-
@worker_pool ||= ActionCable::Server::Worker.
|
48
|
+
@worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
|
43
49
|
end
|
44
50
|
|
45
51
|
# Requires and returns a hash of all the channel class constants keyed by name.
|
46
52
|
def channel_classes
|
47
|
-
@channel_classes
|
48
|
-
|
49
|
-
|
53
|
+
@channel_classes || @mutex.synchronize do
|
54
|
+
@channel_classes ||= begin
|
55
|
+
config.channel_paths.each { |channel_path| require channel_path }
|
56
|
+
config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize }
|
57
|
+
end
|
50
58
|
end
|
51
59
|
end
|
52
60
|
|
53
|
-
#
|
61
|
+
# Adapter used for all streams/broadcasting.
|
54
62
|
def pubsub
|
55
|
-
@pubsub ||=
|
56
|
-
end
|
57
|
-
|
58
|
-
# The EventMachine Redis instance used by the pubsub adapter.
|
59
|
-
def redis
|
60
|
-
@redis ||= EM::Hiredis.connect(config.redis[:url]).tap do |redis|
|
61
|
-
redis.on(:reconnect_failed) do
|
62
|
-
logger.info "[ActionCable] Redis reconnect failed."
|
63
|
-
# logger.info "[ActionCable] Redis reconnected. Closing all the open connections."
|
64
|
-
# @connections.map &:close
|
65
|
-
end
|
66
|
-
end
|
63
|
+
@pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
|
67
64
|
end
|
68
65
|
|
69
66
|
# All the identifiers applied to the connection class associated with this server.
|