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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -3
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +60 -44
  5. data/lib/action_cable.rb +2 -1
  6. data/lib/action_cable/channel/base.rb +2 -2
  7. data/lib/action_cable/channel/periodic_timers.rb +3 -3
  8. data/lib/action_cable/channel/streams.rb +4 -4
  9. data/lib/action_cable/connection.rb +4 -1
  10. data/lib/action_cable/connection/base.rb +22 -21
  11. data/lib/action_cable/connection/client_socket.rb +150 -0
  12. data/lib/action_cable/connection/identification.rb +1 -1
  13. data/lib/action_cable/connection/internal_channel.rb +6 -6
  14. data/lib/action_cable/connection/stream.rb +59 -0
  15. data/lib/action_cable/connection/stream_event_loop.rb +94 -0
  16. data/lib/action_cable/connection/subscriptions.rb +0 -1
  17. data/lib/action_cable/connection/web_socket.rb +14 -8
  18. data/lib/action_cable/engine.rb +3 -3
  19. data/lib/action_cable/gem_version.rb +1 -1
  20. data/lib/action_cable/remote_connections.rb +1 -1
  21. data/lib/action_cable/server.rb +0 -4
  22. data/lib/action_cable/server/base.rb +19 -22
  23. data/lib/action_cable/server/broadcasting.rb +1 -8
  24. data/lib/action_cable/server/configuration.rb +25 -5
  25. data/lib/action_cable/server/connections.rb +3 -5
  26. data/lib/action_cable/server/worker.rb +42 -13
  27. data/lib/action_cable/subscription_adapter.rb +8 -0
  28. data/lib/action_cable/subscription_adapter/async.rb +22 -0
  29. data/lib/action_cable/subscription_adapter/base.rb +28 -0
  30. data/lib/action_cable/subscription_adapter/evented_redis.rb +67 -0
  31. data/lib/action_cable/subscription_adapter/inline.rb +35 -0
  32. data/lib/action_cable/subscription_adapter/postgresql.rb +106 -0
  33. data/lib/action_cable/subscription_adapter/redis.rb +163 -0
  34. data/lib/action_cable/subscription_adapter/subscriber_map.rb +53 -0
  35. data/lib/assets/compiled/action_cable.js +473 -0
  36. data/lib/rails/generators/channel/channel_generator.rb +6 -1
  37. metadata +21 -99
  38. data/lib/action_cable/process/logging.rb +0 -10
  39. data/lib/assets/javascripts/action_cable.coffee.erb +0 -23
  40. data/lib/assets/javascripts/action_cable/connection.coffee +0 -84
  41. data/lib/assets/javascripts/action_cable/connection_monitor.coffee +0 -84
  42. data/lib/assets/javascripts/action_cable/consumer.coffee +0 -31
  43. data/lib/assets/javascripts/action_cable/subscription.coffee +0 -68
  44. 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.broadcasting_redis.publish broadcasting, ActiveSupport::JSON.encode(message)
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 :redis, :channels_path
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
- @channels_path = Rails.root.join('app/channels')
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
- @channels ||= Dir["#{channels_path}/**/*_channel.rb"]
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
- EM.next_tick do
26
- @heartbeat_timer ||= EventMachine.add_periodic_timer(BEAT_INTERVAL) do
27
- EM.next_tick { connections.map(&:beat) }
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
- attr_reader :connection
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
- @connection = receiver
30
+ begin
31
+ self.connection = receiver
17
32
 
18
- run_callbacks :work do
19
- receiver.send method, *args
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
- rescue Exception => e
22
- logger.error "There was an exception - #{e.class}(#{e.message})"
23
- logger.error e.backtrace.join("\n")
44
+ end
24
45
 
25
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
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
- @connection = channel.connection
53
+ begin
54
+ self.connection = channel.connection
30
55
 
31
- run_callbacks :work do
32
- callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
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,8 @@
1
+ module ActionCable
2
+ module SubscriptionAdapter
3
+ extend ActiveSupport::Autoload
4
+
5
+ autoload :Base
6
+ autoload :SubscriberMap
7
+ end
8
+ 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