actioncable 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +169 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +517 -0
  6. data/lib/action_cable.rb +62 -0
  7. data/lib/action_cable/channel.rb +17 -0
  8. data/lib/action_cable/channel/base.rb +311 -0
  9. data/lib/action_cable/channel/broadcasting.rb +41 -0
  10. data/lib/action_cable/channel/callbacks.rb +37 -0
  11. data/lib/action_cable/channel/naming.rb +25 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +176 -0
  14. data/lib/action_cable/channel/test_case.rb +310 -0
  15. data/lib/action_cable/connection.rb +22 -0
  16. data/lib/action_cable/connection/authorization.rb +15 -0
  17. data/lib/action_cable/connection/base.rb +264 -0
  18. data/lib/action_cable/connection/client_socket.rb +157 -0
  19. data/lib/action_cable/connection/identification.rb +47 -0
  20. data/lib/action_cable/connection/internal_channel.rb +45 -0
  21. data/lib/action_cable/connection/message_buffer.rb +54 -0
  22. data/lib/action_cable/connection/stream.rb +117 -0
  23. data/lib/action_cable/connection/stream_event_loop.rb +136 -0
  24. data/lib/action_cable/connection/subscriptions.rb +79 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +42 -0
  26. data/lib/action_cable/connection/test_case.rb +234 -0
  27. data/lib/action_cable/connection/web_socket.rb +41 -0
  28. data/lib/action_cable/engine.rb +79 -0
  29. data/lib/action_cable/gem_version.rb +17 -0
  30. data/lib/action_cable/helpers/action_cable_helper.rb +42 -0
  31. data/lib/action_cable/remote_connections.rb +71 -0
  32. data/lib/action_cable/server.rb +17 -0
  33. data/lib/action_cable/server/base.rb +94 -0
  34. data/lib/action_cable/server/broadcasting.rb +54 -0
  35. data/lib/action_cable/server/configuration.rb +56 -0
  36. data/lib/action_cable/server/connections.rb +36 -0
  37. data/lib/action_cable/server/worker.rb +75 -0
  38. data/lib/action_cable/server/worker/active_record_connection_management.rb +21 -0
  39. data/lib/action_cable/subscription_adapter.rb +12 -0
  40. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  41. data/lib/action_cable/subscription_adapter/base.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/channel_prefix.rb +28 -0
  43. data/lib/action_cable/subscription_adapter/inline.rb +37 -0
  44. data/lib/action_cable/subscription_adapter/postgresql.rb +132 -0
  45. data/lib/action_cable/subscription_adapter/redis.rb +181 -0
  46. data/lib/action_cable/subscription_adapter/subscriber_map.rb +59 -0
  47. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  48. data/lib/action_cable/test_case.rb +11 -0
  49. data/lib/action_cable/test_helper.rb +133 -0
  50. data/lib/action_cable/version.rb +10 -0
  51. data/lib/rails/generators/channel/USAGE +13 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +52 -0
  53. data/lib/rails/generators/channel/templates/application_cable/channel.rb.tt +4 -0
  54. data/lib/rails/generators/channel/templates/application_cable/connection.rb.tt +4 -0
  55. data/lib/rails/generators/channel/templates/channel.rb.tt +16 -0
  56. data/lib/rails/generators/channel/templates/javascript/channel.js.tt +20 -0
  57. data/lib/rails/generators/channel/templates/javascript/consumer.js.tt +6 -0
  58. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +149 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module/redefine_method"
4
+
5
+ module ActionCable
6
+ # If you need to disconnect a given connection, you can go through the
7
+ # RemoteConnections. You can find the connections you're looking for by
8
+ # searching for the identifier declared on the connection. For example:
9
+ #
10
+ # module ApplicationCable
11
+ # class Connection < ActionCable::Connection::Base
12
+ # identified_by :current_user
13
+ # ....
14
+ # end
15
+ # end
16
+ #
17
+ # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
18
+ #
19
+ # This will disconnect all the connections established for
20
+ # <tt>User.find(1)</tt>, across all servers running on all machines, because
21
+ # it uses the internal channel that all of these servers are subscribed to.
22
+ class RemoteConnections
23
+ attr_reader :server
24
+
25
+ def initialize(server)
26
+ @server = server
27
+ end
28
+
29
+ def where(identifier)
30
+ RemoteConnection.new(server, identifier)
31
+ end
32
+
33
+ private
34
+ # Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
35
+ # Exists solely for the purpose of calling #disconnect on that connection.
36
+ class RemoteConnection
37
+ class InvalidIdentifiersError < StandardError; end
38
+
39
+ include Connection::Identification, Connection::InternalChannel
40
+
41
+ def initialize(server, ids)
42
+ @server = server
43
+ set_identifier_instance_vars(ids)
44
+ end
45
+
46
+ # Uses the internal channel to disconnect the connection.
47
+ def disconnect
48
+ server.broadcast internal_channel, type: "disconnect"
49
+ end
50
+
51
+ # Returns all the identifiers that were applied to this connection.
52
+ redefine_method :identifiers do
53
+ server.connection_identifiers
54
+ end
55
+
56
+ protected
57
+ attr_reader :server
58
+
59
+ private
60
+ def set_identifier_instance_vars(ids)
61
+ raise InvalidIdentifiersError unless valid_identifiers?(ids)
62
+ ids.each { |k, v| instance_variable_set("@#{k}", v) }
63
+ end
64
+
65
+ def valid_identifiers?(ids)
66
+ keys = ids.keys
67
+ identifiers.all? { |id| keys.include?(id) }
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Server
5
+ extend ActiveSupport::Autoload
6
+
7
+ eager_autoload do
8
+ autoload :Base
9
+ autoload :Broadcasting
10
+ autoload :Connections
11
+ autoload :Configuration
12
+
13
+ autoload :Worker
14
+ autoload :ActiveRecordConnectionManagement, "action_cable/server/worker/active_record_connection_management"
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module ActionCable
6
+ module Server
7
+ # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
8
+ # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
9
+ #
10
+ # Also, this is the server instance used for broadcasting. See Broadcasting for more information.
11
+ class Base
12
+ include ActionCable::Server::Broadcasting
13
+ include ActionCable::Server::Connections
14
+
15
+ cattr_accessor :config, instance_accessor: false, default: ActionCable::Server::Configuration.new
16
+
17
+ attr_reader :config
18
+
19
+ def self.logger; config.logger; end
20
+ delegate :logger, to: :config
21
+
22
+ attr_reader :mutex
23
+
24
+ def initialize(config: self.class.config)
25
+ @config = config
26
+ @mutex = Monitor.new
27
+ @remote_connections = @event_loop = @worker_pool = @pubsub = nil
28
+ end
29
+
30
+ # Called by Rack to setup the server.
31
+ def call(env)
32
+ setup_heartbeat_timer
33
+ config.connection_class.call.new(self, env).process
34
+ end
35
+
36
+ # Disconnect all the connections identified by +identifiers+ on this server or any others via RemoteConnections.
37
+ def disconnect(identifiers)
38
+ remote_connections.where(identifiers).disconnect
39
+ end
40
+
41
+ def restart
42
+ connections.each do |connection|
43
+ connection.close(reason: ActionCable::INTERNAL[:disconnect_reasons][:server_restart])
44
+ end
45
+
46
+ @mutex.synchronize do
47
+ # Shutdown the worker pool
48
+ @worker_pool.halt if @worker_pool
49
+ @worker_pool = nil
50
+
51
+ # Shutdown the pub/sub adapter
52
+ @pubsub.shutdown if @pubsub
53
+ @pubsub = nil
54
+ end
55
+ end
56
+
57
+ # Gateway to RemoteConnections. See that class for details.
58
+ def remote_connections
59
+ @remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
60
+ end
61
+
62
+ def event_loop
63
+ @event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new }
64
+ end
65
+
66
+ # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
67
+ # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
68
+ # at 4 worker threads by default. Tune the size yourself with <tt>config.action_cable.worker_pool_size</tt>.
69
+ #
70
+ # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
71
+ # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
72
+ # connections.
73
+ #
74
+ # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
75
+ # the database connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
76
+ # database connection pool instead.
77
+ def worker_pool
78
+ @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
79
+ end
80
+
81
+ # Adapter used for all streams/broadcasting.
82
+ def pubsub
83
+ @pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
84
+ end
85
+
86
+ # All of the identifiers applied to the connection class associated with this server.
87
+ def connection_identifiers
88
+ config.connection_class.call.identifiers
89
+ end
90
+ end
91
+
92
+ ActiveSupport.run_load_hooks(:action_cable, Base.config)
93
+ end
94
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Server
5
+ # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
6
+ # broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
7
+ #
8
+ # class WebNotificationsChannel < ApplicationCable::Channel
9
+ # def subscribed
10
+ # stream_from "web_notifications_#{current_user.id}"
11
+ # end
12
+ # end
13
+ #
14
+ # # Somewhere in your app this is called, perhaps from a NewCommentJob:
15
+ # ActionCable.server.broadcast \
16
+ # "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
17
+ #
18
+ # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
19
+ # App.cable.subscriptions.create "WebNotificationsChannel",
20
+ # received: (data) ->
21
+ # new Notification data['title'], body: data['body']
22
+ module Broadcasting
23
+ # Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
24
+ def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
25
+ broadcaster_for(broadcasting, coder: coder).broadcast(message)
26
+ end
27
+
28
+ # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
29
+ # may need multiple spots to transmit to a specific broadcasting over and over.
30
+ def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
31
+ Broadcaster.new(self, String(broadcasting), coder: coder)
32
+ end
33
+
34
+ private
35
+ class Broadcaster
36
+ attr_reader :server, :broadcasting, :coder
37
+
38
+ def initialize(server, broadcasting, coder:)
39
+ @server, @broadcasting, @coder = server, broadcasting, coder
40
+ end
41
+
42
+ def broadcast(message)
43
+ server.logger.debug "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
44
+
45
+ payload = { broadcasting: broadcasting, message: message, coder: coder }
46
+ ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
47
+ encoded = coder ? coder.encode(message) : message
48
+ server.pubsub.broadcast broadcasting, encoded
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Server
5
+ # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
6
+ # in a Rails config initializer.
7
+ class Configuration
8
+ attr_accessor :logger, :log_tags
9
+ attr_accessor :connection_class, :worker_pool_size
10
+ attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
11
+ attr_accessor :cable, :url, :mount_path
12
+
13
+ def initialize
14
+ @log_tags = []
15
+
16
+ @connection_class = -> { ActionCable::Connection::Base }
17
+ @worker_pool_size = 4
18
+
19
+ @disable_request_forgery_protection = false
20
+ @allow_same_origin_as_host = true
21
+ end
22
+
23
+ # Returns constant of subscription adapter specified in config/cable.yml.
24
+ # If the adapter cannot be found, this will default to the Redis adapter.
25
+ # Also makes sure proper dependencies are required.
26
+ def pubsub_adapter
27
+ adapter = (cable.fetch("adapter") { "redis" })
28
+
29
+ # Require the adapter itself and give useful feedback about
30
+ # 1. Missing adapter gems and
31
+ # 2. Adapter gems' missing dependencies.
32
+ path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
33
+ begin
34
+ require path_to_adapter
35
+ rescue LoadError => e
36
+ # We couldn't require the adapter itself. Raise an exception that
37
+ # points out config typos and missing gems.
38
+ if e.path == path_to_adapter
39
+ # We can assume that a non-builtin adapter was specified, so it's
40
+ # either misspelled or missing from Gemfile.
41
+ raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
42
+
43
+ # Bubbled up from the adapter require. Prefix the exception message
44
+ # with some guidance about how to address it and reraise.
45
+ else
46
+ raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
47
+ end
48
+ end
49
+
50
+ adapter = adapter.camelize
51
+ adapter = "PostgreSQL" if adapter == "Postgresql"
52
+ "ActionCable::SubscriptionAdapter::#{adapter}".constantize
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Server
5
+ # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
6
+ # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
7
+ module Connections # :nodoc:
8
+ BEAT_INTERVAL = 3
9
+
10
+ def connections
11
+ @connections ||= []
12
+ end
13
+
14
+ def add_connection(connection)
15
+ connections << connection
16
+ end
17
+
18
+ def remove_connection(connection)
19
+ connections.delete connection
20
+ end
21
+
22
+ # WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
23
+ # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
24
+ # disconnect.
25
+ def setup_heartbeat_timer
26
+ @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
27
+ event_loop.post { connections.map(&:beat) }
28
+ end
29
+ end
30
+
31
+ def open_connections_statistics
32
+ connections.map(&:statistics)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/callbacks"
4
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
5
+ require "concurrent"
6
+
7
+ module ActionCable
8
+ module Server
9
+ # Worker used by Server.send_async to do connection work in threads.
10
+ class Worker # :nodoc:
11
+ include ActiveSupport::Callbacks
12
+
13
+ thread_mattr_accessor :connection
14
+ define_callbacks :work
15
+ include ActiveRecordConnectionManagement
16
+
17
+ attr_reader :executor
18
+
19
+ def initialize(max_size: 5)
20
+ @executor = Concurrent::ThreadPoolExecutor.new(
21
+ min_threads: 1,
22
+ max_threads: max_size,
23
+ max_queue: 0,
24
+ )
25
+ end
26
+
27
+ # Stop processing work: any work that has not already started
28
+ # running will be discarded from the queue
29
+ def halt
30
+ @executor.shutdown
31
+ end
32
+
33
+ def stopping?
34
+ @executor.shuttingdown?
35
+ end
36
+
37
+ def work(connection)
38
+ self.connection = connection
39
+
40
+ run_callbacks :work do
41
+ yield
42
+ end
43
+ ensure
44
+ self.connection = nil
45
+ end
46
+
47
+ def async_exec(receiver, *args, connection:, &block)
48
+ async_invoke receiver, :instance_exec, *args, connection: connection, &block
49
+ end
50
+
51
+ def async_invoke(receiver, method, *args, connection: receiver, &block)
52
+ @executor.post do
53
+ invoke(receiver, method, *args, connection: connection, &block)
54
+ end
55
+ end
56
+
57
+ def invoke(receiver, method, *args, connection:, &block)
58
+ work(connection) do
59
+ receiver.send method, *args, &block
60
+ rescue Exception => e
61
+ logger.error "There was an exception - #{e.class}(#{e.message})"
62
+ logger.error e.backtrace.join("\n")
63
+
64
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def logger
71
+ ActionCable.server.logger
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module Server
5
+ class Worker
6
+ module ActiveRecordConnectionManagement
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ if defined?(ActiveRecord::Base)
11
+ set_callback :work, :around, :with_database_connections
12
+ end
13
+ end
14
+
15
+ def with_database_connections
16
+ connection.logger.tag(ActiveRecord::Base.logger) { yield }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionCable
4
+ module SubscriptionAdapter
5
+ extend ActiveSupport::Autoload
6
+
7
+ autoload :Base
8
+ autoload :Test
9
+ autoload :SubscriberMap
10
+ autoload :ChannelPrefix
11
+ end
12
+ end