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