actioncable 5.0.0.beta1.1 → 5.0.0.beta2
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 +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
|