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