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
@@ -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 internal_redis_channel
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
- @_internal_redis_subscriptions ||= []
16
- @_internal_redis_subscriptions << [ internal_redis_channel, callback ]
15
+ @_internal_subscriptions ||= []
16
+ @_internal_subscriptions << [ internal_channel, callback ]
17
17
 
18
- EM.next_tick { pubsub.subscribe(internal_redis_channel, &callback) }
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 @_internal_redis_subscriptions.present?
25
- @_internal_redis_subscriptions.each { |channel, callback| EM.next_tick { pubsub.unsubscribe_proc(channel, callback) } }
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
@@ -49,7 +49,6 @@ module ActionCable
49
49
  find(data).perform_action ActiveSupport::JSON.decode(data['data'])
50
50
  end
51
51
 
52
-
53
52
  def identifiers
54
53
  subscriptions.keys
55
54
  end
@@ -1,13 +1,11 @@
1
- require 'faye/websocket'
1
+ require 'websocket/driver'
2
2
 
3
3
  module ActionCable
4
4
  module Connection
5
- # Decorate the Faye::WebSocket with helpers we need.
5
+ # Wrap the real socket to minimize the externally-presented API
6
6
  class WebSocket
7
- delegate :rack_response, :close, :on, to: :websocket
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.ready_state == Faye::WebSocket::API::OPEN
16
+ websocket && websocket.alive?
19
17
  end
20
18
 
21
19
  def transmit(data)
22
- websocket.send data
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
@@ -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/redis/cable", with: "config/redis/cable.yml"
27
+ app.paths.add "config/cable", with: "config/cable.yml"
28
28
 
29
29
  ActiveSupport.on_load(:action_cable) do
30
- if (redis_cable_path = Pathname.new(app.config.paths["config/redis/cable"].first)).exist?
31
- self.redis = Rails.application.config_for(redis_cable_path).with_indifferent_access
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) }
@@ -8,7 +8,7 @@ module ActionCable
8
8
  MAJOR = 5
9
9
  MINOR = 0
10
10
  TINY = 0
11
- PRE = "beta1.1"
11
+ PRE = "beta2"
12
12
 
13
13
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
14
14
  end
@@ -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 internal_redis_channel, type: 'disconnect'
42
+ server.broadcast internal_channel, type: 'disconnect'
43
43
  end
44
44
 
45
45
  # Returns all the identifiers that were applied to this connection.
@@ -1,7 +1,3 @@
1
- require 'eventmachine'
2
- EventMachine.epoll if EventMachine.epoll?
3
- EventMachine.kqueue if EventMachine.kqueue?
4
-
5
1
  module ActionCable
6
2
  module Server
7
3
  extend ActiveSupport::Autoload
@@ -1,7 +1,4 @@
1
- # FIXME: Cargo culted fix from https://github.com/celluloid/celluloid-pool/issues/10
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.pool(size: config.worker_pool_size)
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 ||= begin
48
- config.channel_paths.each { |channel_path| require channel_path }
49
- config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize }
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
- # The redis pubsub adapter used for all streams/broadcasting.
61
+ # Adapter used for all streams/broadcasting.
54
62
  def pubsub
55
- @pubsub ||= redis.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.