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
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'redis'
|
2
|
-
|
3
1
|
module ActionCable
|
4
2
|
module Server
|
5
3
|
# Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these
|
@@ -31,11 +29,6 @@ module ActionCable
|
|
31
29
|
Broadcaster.new(self, broadcasting)
|
32
30
|
end
|
33
31
|
|
34
|
-
# The redis instance used for broadcasting. Not intended for direct user use.
|
35
|
-
def broadcasting_redis
|
36
|
-
@broadcasting_redis ||= Redis.new(config.redis)
|
37
|
-
end
|
38
|
-
|
39
32
|
private
|
40
33
|
class Broadcaster
|
41
34
|
attr_reader :server, :broadcasting
|
@@ -46,7 +39,7 @@ module ActionCable
|
|
46
39
|
|
47
40
|
def broadcast(message)
|
48
41
|
server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
|
49
|
-
server.
|
42
|
+
server.pubsub.broadcast broadcasting, ActiveSupport::JSON.encode(message)
|
50
43
|
end
|
51
44
|
end
|
52
45
|
end
|
@@ -5,9 +5,9 @@ module ActionCable
|
|
5
5
|
class Configuration
|
6
6
|
attr_accessor :logger, :log_tags
|
7
7
|
attr_accessor :connection_class, :worker_pool_size
|
8
|
-
attr_accessor :
|
8
|
+
attr_accessor :channel_load_paths
|
9
9
|
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
|
10
|
-
attr_accessor :url
|
10
|
+
attr_accessor :cable, :url
|
11
11
|
|
12
12
|
def initialize
|
13
13
|
@log_tags = []
|
@@ -15,13 +15,15 @@ module ActionCable
|
|
15
15
|
@connection_class = ApplicationCable::Connection
|
16
16
|
@worker_pool_size = 100
|
17
17
|
|
18
|
-
@
|
18
|
+
@channel_load_paths = [Rails.root.join('app/channels')]
|
19
19
|
|
20
20
|
@disable_request_forgery_protection = false
|
21
21
|
end
|
22
22
|
|
23
23
|
def channel_paths
|
24
|
-
@
|
24
|
+
@channel_paths ||= channel_load_paths.flat_map do |path|
|
25
|
+
Dir["#{path}/**/*_channel.rb"]
|
26
|
+
end
|
25
27
|
end
|
26
28
|
|
27
29
|
def channel_class_names
|
@@ -29,7 +31,25 @@ module ActionCable
|
|
29
31
|
Pathname.new(channel_path).basename.to_s.split('.').first.camelize
|
30
32
|
end
|
31
33
|
end
|
34
|
+
|
35
|
+
# Returns constant of subscription adapter specified in config/cable.yml.
|
36
|
+
# If the adapter cannot be found, this will default to the Redis adapter.
|
37
|
+
# Also makes sure proper dependencies are required.
|
38
|
+
def pubsub_adapter
|
39
|
+
adapter = (cable.fetch('adapter') { 'redis' })
|
40
|
+
path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
|
41
|
+
begin
|
42
|
+
require path_to_adapter
|
43
|
+
rescue Gem::LoadError => e
|
44
|
+
raise Gem::LoadError, "Specified '#{adapter}' for Action Cable pubsub adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by Action Cable)."
|
45
|
+
rescue LoadError => e
|
46
|
+
raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/cable.yml is valid. If you use an adapter other than 'postgresql' or 'redis' add the necessary adapter gem to the Gemfile.", e.backtrace
|
47
|
+
end
|
48
|
+
|
49
|
+
adapter = adapter.camelize
|
50
|
+
adapter = 'PostgreSQL' if adapter == 'Postgresql'
|
51
|
+
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
|
52
|
+
end
|
32
53
|
end
|
33
54
|
end
|
34
55
|
end
|
35
|
-
|
@@ -22,11 +22,9 @@ module ActionCable
|
|
22
22
|
# then can't rely on being able to receive and send to it. So there's a 3 second heartbeat running on all connections. If the beat fails, we automatically
|
23
23
|
# disconnect.
|
24
24
|
def setup_heartbeat_timer
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
29
|
-
end
|
25
|
+
@heartbeat_timer ||= Concurrent::TimerTask.new(execution_interval: BEAT_INTERVAL) do
|
26
|
+
Concurrent.global_io_executor.post { connections.map(&:beat) }
|
27
|
+
end.tap(&:execute)
|
30
28
|
end
|
31
29
|
|
32
30
|
def open_connections_statistics
|
@@ -1,39 +1,68 @@
|
|
1
|
-
require 'celluloid'
|
2
1
|
require 'active_support/callbacks'
|
2
|
+
require 'active_support/core_ext/module/attribute_accessors_per_thread'
|
3
|
+
require 'concurrent'
|
3
4
|
|
4
5
|
module ActionCable
|
5
6
|
module Server
|
6
7
|
# Worker used by Server.send_async to do connection work in threads. Only for internal use.
|
7
8
|
class Worker
|
8
9
|
include ActiveSupport::Callbacks
|
9
|
-
include Celluloid
|
10
10
|
|
11
|
-
|
11
|
+
thread_mattr_accessor :connection
|
12
12
|
define_callbacks :work
|
13
13
|
include ActiveRecordConnectionManagement
|
14
14
|
|
15
|
+
def initialize(max_size: 5)
|
16
|
+
@pool = Concurrent::ThreadPoolExecutor.new(
|
17
|
+
min_threads: 1,
|
18
|
+
max_threads: max_size,
|
19
|
+
max_queue: 0,
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def async_invoke(receiver, method, *args)
|
24
|
+
@pool.post do
|
25
|
+
invoke(receiver, method, *args)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
15
29
|
def invoke(receiver, method, *args)
|
16
|
-
|
30
|
+
begin
|
31
|
+
self.connection = receiver
|
17
32
|
|
18
|
-
|
19
|
-
|
33
|
+
run_callbacks :work do
|
34
|
+
receiver.send method, *args
|
35
|
+
end
|
36
|
+
rescue Exception => e
|
37
|
+
logger.error "There was an exception - #{e.class}(#{e.message})"
|
38
|
+
logger.error e.backtrace.join("\n")
|
39
|
+
|
40
|
+
receiver.handle_exception if receiver.respond_to?(:handle_exception)
|
41
|
+
ensure
|
42
|
+
self.connection = nil
|
20
43
|
end
|
21
|
-
|
22
|
-
logger.error "There was an exception - #{e.class}(#{e.message})"
|
23
|
-
logger.error e.backtrace.join("\n")
|
44
|
+
end
|
24
45
|
|
25
|
-
|
46
|
+
def async_run_periodic_timer(channel, callback)
|
47
|
+
@pool.post do
|
48
|
+
run_periodic_timer(channel, callback)
|
49
|
+
end
|
26
50
|
end
|
27
51
|
|
28
52
|
def run_periodic_timer(channel, callback)
|
29
|
-
|
53
|
+
begin
|
54
|
+
self.connection = channel.connection
|
30
55
|
|
31
|
-
|
32
|
-
|
56
|
+
run_callbacks :work do
|
57
|
+
callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
|
58
|
+
end
|
59
|
+
ensure
|
60
|
+
self.connection = nil
|
33
61
|
end
|
34
62
|
end
|
35
63
|
|
36
64
|
private
|
65
|
+
|
37
66
|
def logger
|
38
67
|
ActionCable.server.logger
|
39
68
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'action_cable/subscription_adapter/inline'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module SubscriptionAdapter
|
5
|
+
class Async < Inline # :nodoc:
|
6
|
+
private
|
7
|
+
def new_subscriber_map
|
8
|
+
AsyncSubscriberMap.new
|
9
|
+
end
|
10
|
+
|
11
|
+
class AsyncSubscriberMap < SubscriberMap
|
12
|
+
def add_subscriber(*)
|
13
|
+
Concurrent.global_io_executor.post { super }
|
14
|
+
end
|
15
|
+
|
16
|
+
def invoke_callback(*)
|
17
|
+
Concurrent.global_io_executor.post { super }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module SubscriptionAdapter
|
3
|
+
class Base
|
4
|
+
attr_reader :logger, :server
|
5
|
+
|
6
|
+
def initialize(server)
|
7
|
+
@server = server
|
8
|
+
@logger = @server.logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def broadcast(channel, payload)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
def subscribe(channel, message_callback, success_callback = nil)
|
16
|
+
raise NotImplementedError
|
17
|
+
end
|
18
|
+
|
19
|
+
def unsubscribe(channel, message_callback)
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def shutdown
|
24
|
+
raise NotImplementedError
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
gem 'em-hiredis', '~> 0.3.0'
|
4
|
+
gem 'redis', '~> 3.0'
|
5
|
+
require 'em-hiredis'
|
6
|
+
require 'redis'
|
7
|
+
|
8
|
+
EventMachine.epoll if EventMachine.epoll?
|
9
|
+
EventMachine.kqueue if EventMachine.kqueue?
|
10
|
+
|
11
|
+
module ActionCable
|
12
|
+
module SubscriptionAdapter
|
13
|
+
class EventedRedis < Base # :nodoc:
|
14
|
+
@@mutex = Mutex.new
|
15
|
+
|
16
|
+
def initialize(*)
|
17
|
+
super
|
18
|
+
@redis_connection_for_broadcasts = @redis_connection_for_subscriptions = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def broadcast(channel, payload)
|
22
|
+
redis_connection_for_broadcasts.publish(channel, payload)
|
23
|
+
end
|
24
|
+
|
25
|
+
def subscribe(channel, message_callback, success_callback = nil)
|
26
|
+
redis_connection_for_subscriptions.pubsub.subscribe(channel, &message_callback).tap do |result|
|
27
|
+
result.callback { |reply| success_callback.call } if success_callback
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def unsubscribe(channel, message_callback)
|
32
|
+
redis_connection_for_subscriptions.pubsub.unsubscribe_proc(channel, message_callback)
|
33
|
+
end
|
34
|
+
|
35
|
+
def shutdown
|
36
|
+
redis_connection_for_subscriptions.pubsub.close_connection
|
37
|
+
@redis_connection_for_subscriptions = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def redis_connection_for_subscriptions
|
42
|
+
ensure_reactor_running
|
43
|
+
@redis_connection_for_subscriptions || @server.mutex.synchronize do
|
44
|
+
@redis_connection_for_subscriptions ||= EM::Hiredis.connect(@server.config.cable[:url]).tap do |redis|
|
45
|
+
redis.on(:reconnect_failed) do
|
46
|
+
@logger.info "[ActionCable] Redis reconnect failed."
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def redis_connection_for_broadcasts
|
53
|
+
@redis_connection_for_broadcasts || @server.mutex.synchronize do
|
54
|
+
@redis_connection_for_broadcasts ||= ::Redis.new(@server.config.cable)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def ensure_reactor_running
|
59
|
+
return if EventMachine.reactor_running?
|
60
|
+
@@mutex.synchronize do
|
61
|
+
Thread.new { EventMachine.run } unless EventMachine.reactor_running?
|
62
|
+
Thread.pass until EventMachine.reactor_running?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module SubscriptionAdapter
|
3
|
+
class Inline < Base # :nodoc:
|
4
|
+
def initialize(*)
|
5
|
+
super
|
6
|
+
@subscriber_map = nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def broadcast(channel, payload)
|
10
|
+
subscriber_map.broadcast(channel, payload)
|
11
|
+
end
|
12
|
+
|
13
|
+
def subscribe(channel, callback, success_callback = nil)
|
14
|
+
subscriber_map.add_subscriber(channel, callback, success_callback)
|
15
|
+
end
|
16
|
+
|
17
|
+
def unsubscribe(channel, callback)
|
18
|
+
subscriber_map.remove_subscriber(channel, callback)
|
19
|
+
end
|
20
|
+
|
21
|
+
def shutdown
|
22
|
+
# nothing to do
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
def subscriber_map
|
27
|
+
@subscriber_map || @server.mutex.synchronize { @subscriber_map ||= new_subscriber_map }
|
28
|
+
end
|
29
|
+
|
30
|
+
def new_subscriber_map
|
31
|
+
SubscriberMap.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
gem 'pg', '~> 0.18'
|
2
|
+
require 'pg'
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module SubscriptionAdapter
|
7
|
+
class PostgreSQL < Base # :nodoc:
|
8
|
+
def initialize(*)
|
9
|
+
super
|
10
|
+
@listener = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def broadcast(channel, payload)
|
14
|
+
with_connection do |pg_conn|
|
15
|
+
pg_conn.exec("NOTIFY #{pg_conn.escape_identifier(channel)}, '#{pg_conn.escape_string(payload)}'")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def subscribe(channel, callback, success_callback = nil)
|
20
|
+
listener.add_subscriber(channel, callback, success_callback)
|
21
|
+
end
|
22
|
+
|
23
|
+
def unsubscribe(channel, callback)
|
24
|
+
listener.remove_subscriber(channel, callback)
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
listener.shutdown
|
29
|
+
end
|
30
|
+
|
31
|
+
def with_connection(&block) # :nodoc:
|
32
|
+
ActiveRecord::Base.connection_pool.with_connection do |ar_conn|
|
33
|
+
pg_conn = ar_conn.raw_connection
|
34
|
+
|
35
|
+
unless pg_conn.is_a?(PG::Connection)
|
36
|
+
raise 'ActiveRecord database must be Postgres in order to use the Postgres ActionCable storage adapter'
|
37
|
+
end
|
38
|
+
|
39
|
+
yield pg_conn
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def listener
|
45
|
+
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self) }
|
46
|
+
end
|
47
|
+
|
48
|
+
class Listener < SubscriberMap
|
49
|
+
def initialize(adapter)
|
50
|
+
super()
|
51
|
+
|
52
|
+
@adapter = adapter
|
53
|
+
@queue = Queue.new
|
54
|
+
|
55
|
+
@thread = Thread.new do
|
56
|
+
Thread.current.abort_on_exception = true
|
57
|
+
listen
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def listen
|
62
|
+
@adapter.with_connection do |pg_conn|
|
63
|
+
catch :shutdown do
|
64
|
+
loop do
|
65
|
+
until @queue.empty?
|
66
|
+
action, channel, callback = @queue.pop(true)
|
67
|
+
|
68
|
+
case action
|
69
|
+
when :listen
|
70
|
+
pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}")
|
71
|
+
Concurrent.global_io_executor << callback if callback
|
72
|
+
when :unlisten
|
73
|
+
pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}")
|
74
|
+
when :shutdown
|
75
|
+
throw :shutdown
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
pg_conn.wait_for_notify(1) do |chan, pid, message|
|
80
|
+
broadcast(chan, message)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def shutdown
|
88
|
+
@queue.push([:shutdown])
|
89
|
+
Thread.pass while @thread.alive?
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_channel(channel, on_success)
|
93
|
+
@queue.push([:listen, channel, on_success])
|
94
|
+
end
|
95
|
+
|
96
|
+
def remove_channel(channel)
|
97
|
+
@queue.push([:unlisten, channel])
|
98
|
+
end
|
99
|
+
|
100
|
+
def invoke_callback(*)
|
101
|
+
Concurrent.global_io_executor.post { super }
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|