omg-actioncable 8.0.0.alpha2

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +24 -0
  5. data/app/assets/javascripts/action_cable.js +511 -0
  6. data/app/assets/javascripts/actioncable.esm.js +512 -0
  7. data/app/assets/javascripts/actioncable.js +510 -0
  8. data/lib/action_cable/channel/base.rb +335 -0
  9. data/lib/action_cable/channel/broadcasting.rb +50 -0
  10. data/lib/action_cable/channel/callbacks.rb +76 -0
  11. data/lib/action_cable/channel/naming.rb +28 -0
  12. data/lib/action_cable/channel/periodic_timers.rb +78 -0
  13. data/lib/action_cable/channel/streams.rb +215 -0
  14. data/lib/action_cable/channel/test_case.rb +356 -0
  15. data/lib/action_cable/connection/authorization.rb +18 -0
  16. data/lib/action_cable/connection/base.rb +294 -0
  17. data/lib/action_cable/connection/callbacks.rb +57 -0
  18. data/lib/action_cable/connection/client_socket.rb +159 -0
  19. data/lib/action_cable/connection/identification.rb +51 -0
  20. data/lib/action_cable/connection/internal_channel.rb +50 -0
  21. data/lib/action_cable/connection/message_buffer.rb +57 -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 +85 -0
  25. data/lib/action_cable/connection/tagged_logger_proxy.rb +47 -0
  26. data/lib/action_cable/connection/test_case.rb +246 -0
  27. data/lib/action_cable/connection/web_socket.rb +45 -0
  28. data/lib/action_cable/deprecator.rb +9 -0
  29. data/lib/action_cable/engine.rb +98 -0
  30. data/lib/action_cable/gem_version.rb +19 -0
  31. data/lib/action_cable/helpers/action_cable_helper.rb +45 -0
  32. data/lib/action_cable/remote_connections.rb +82 -0
  33. data/lib/action_cable/server/base.rb +109 -0
  34. data/lib/action_cable/server/broadcasting.rb +62 -0
  35. data/lib/action_cable/server/configuration.rb +70 -0
  36. data/lib/action_cable/server/connections.rb +44 -0
  37. data/lib/action_cable/server/worker/active_record_connection_management.rb +23 -0
  38. data/lib/action_cable/server/worker.rb +75 -0
  39. data/lib/action_cable/subscription_adapter/async.rb +29 -0
  40. data/lib/action_cable/subscription_adapter/base.rb +36 -0
  41. data/lib/action_cable/subscription_adapter/channel_prefix.rb +30 -0
  42. data/lib/action_cable/subscription_adapter/inline.rb +39 -0
  43. data/lib/action_cable/subscription_adapter/postgresql.rb +134 -0
  44. data/lib/action_cable/subscription_adapter/redis.rb +256 -0
  45. data/lib/action_cable/subscription_adapter/subscriber_map.rb +61 -0
  46. data/lib/action_cable/subscription_adapter/test.rb +41 -0
  47. data/lib/action_cable/test_case.rb +13 -0
  48. data/lib/action_cable/test_helper.rb +163 -0
  49. data/lib/action_cable/version.rb +12 -0
  50. data/lib/action_cable.rb +80 -0
  51. data/lib/rails/generators/channel/USAGE +19 -0
  52. data/lib/rails/generators/channel/channel_generator.rb +127 -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 +1 -0
  59. data/lib/rails/generators/test_unit/channel_generator.rb +22 -0
  60. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  61. metadata +181 -0
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "rails"
6
+ require "action_cable"
7
+ require "active_support/core_ext/hash/indifferent_access"
8
+
9
+ module ActionCable
10
+ class Engine < Rails::Engine # :nodoc:
11
+ config.action_cable = ActiveSupport::OrderedOptions.new
12
+ config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
13
+ config.action_cable.precompile_assets = true
14
+
15
+ initializer "action_cable.deprecator", before: :load_environment_config do |app|
16
+ app.deprecators[:action_cable] = ActionCable.deprecator
17
+ end
18
+
19
+ initializer "action_cable.helpers" do
20
+ ActiveSupport.on_load(:action_view) do
21
+ include ActionCable::Helpers::ActionCableHelper
22
+ end
23
+ end
24
+
25
+ initializer "action_cable.logger" do
26
+ ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
27
+ end
28
+
29
+ initializer "action_cable.health_check_application" do
30
+ ActiveSupport.on_load(:action_cable) {
31
+ self.health_check_application = ->(env) { Rails::HealthController.action(:show).call(env) }
32
+ }
33
+ end
34
+
35
+ initializer "action_cable.asset" do
36
+ config.after_initialize do |app|
37
+ if app.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
38
+ app.config.assets.precompile += %w( actioncable.js actioncable.esm.js )
39
+ end
40
+ end
41
+ end
42
+
43
+ initializer "action_cable.set_configs" do |app|
44
+ options = app.config.action_cable
45
+ options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
46
+
47
+ app.paths.add "config/cable", with: "config/cable.yml"
48
+
49
+ ActiveSupport.on_load(:action_cable) do
50
+ if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
51
+ self.cable = app.config_for(config_path).to_h.with_indifferent_access
52
+ end
53
+
54
+ previous_connection_class = connection_class
55
+ self.connection_class = -> { "ApplicationCable::Connection".safe_constantize || previous_connection_class.call }
56
+ self.filter_parameters += app.config.filter_parameters
57
+
58
+ options.each { |k, v| send("#{k}=", v) }
59
+ end
60
+ end
61
+
62
+ initializer "action_cable.routes" do
63
+ config.after_initialize do |app|
64
+ config = app.config
65
+ unless config.action_cable.mount_path.nil?
66
+ app.routes.prepend do
67
+ mount ActionCable.server => config.action_cable.mount_path, internal: true, anchor: true
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ initializer "action_cable.set_work_hooks" do |app|
74
+ ActiveSupport.on_load(:action_cable) do
75
+ ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
76
+ app.executor.wrap(source: "application.action_cable") do
77
+ # If we took a while to get the lock, we may have been halted in the meantime.
78
+ # As we haven't started doing any real work yet, we should pretend that we never
79
+ # made it off the queue.
80
+ unless stopping?
81
+ inner.call
82
+ end
83
+ end
84
+ end
85
+
86
+ wrap = lambda do |_, inner|
87
+ app.executor.wrap(source: "application.action_cable", &inner)
88
+ end
89
+ ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
90
+ ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
91
+
92
+ app.reloader.before_class_unload do
93
+ ActionCable.server.restart
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ # Returns the currently loaded version of Action Cable as a `Gem::Version`.
7
+ def self.gem_version
8
+ Gem::Version.new VERSION::STRING
9
+ end
10
+
11
+ module VERSION
12
+ MAJOR = 8
13
+ MINOR = 0
14
+ TINY = 0
15
+ PRE = "alpha2"
16
+
17
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Helpers
7
+ module ActionCableHelper
8
+ # Returns an "action-cable-url" meta tag with the value of the URL specified in
9
+ # your configuration. Ensure this is above your JavaScript tag:
10
+ #
11
+ # <head>
12
+ # <%= action_cable_meta_tag %>
13
+ # <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %>
14
+ # </head>
15
+ #
16
+ # This is then used by Action Cable to determine the URL of your WebSocket
17
+ # server. Your JavaScript can then connect to the server without needing to
18
+ # specify the URL directly:
19
+ #
20
+ # import Cable from "@rails/actioncable"
21
+ # window.Cable = Cable
22
+ # window.App = {}
23
+ # App.cable = Cable.createConsumer()
24
+ #
25
+ # Make sure to specify the correct server location in each of your environment
26
+ # config files:
27
+ #
28
+ # config.action_cable.mount_path = "/cable123"
29
+ # <%= action_cable_meta_tag %> would render:
30
+ # => <meta name="action-cable-url" content="/cable123" />
31
+ #
32
+ # config.action_cable.url = "ws://actioncable.com"
33
+ # <%= action_cable_meta_tag %> would render:
34
+ # => <meta name="action-cable-url" content="ws://actioncable.com" />
35
+ #
36
+ def action_cable_meta_tag
37
+ tag "meta", name: "action-cable-url", content: (
38
+ ActionCable.server.config.url ||
39
+ ActionCable.server.config.mount_path ||
40
+ raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_support/core_ext/module/redefine_method"
6
+
7
+ module ActionCable
8
+ # # Action Cable Remote Connections
9
+ #
10
+ # If you need to disconnect a given connection, you can go through the
11
+ # RemoteConnections. You can find the connections you're looking for by
12
+ # searching for the identifier declared on the connection. For example:
13
+ #
14
+ # module ApplicationCable
15
+ # class Connection < ActionCable::Connection::Base
16
+ # identified_by :current_user
17
+ # ....
18
+ # end
19
+ # end
20
+ #
21
+ # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
22
+ #
23
+ # This will disconnect all the connections established for `User.find(1)`,
24
+ # across all servers running on all machines, because it uses the internal
25
+ # channel that all of these servers are subscribed to.
26
+ #
27
+ # By default, server sends a "disconnect" message with "reconnect" flag set to
28
+ # true. You can override it by specifying the `reconnect` option:
29
+ #
30
+ # ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect(reconnect: false)
31
+ class RemoteConnections
32
+ attr_reader :server
33
+
34
+ def initialize(server)
35
+ @server = server
36
+ end
37
+
38
+ def where(identifier)
39
+ RemoteConnection.new(server, identifier)
40
+ end
41
+
42
+ # # Action Cable Remote Connection
43
+ #
44
+ # Represents a single remote connection found via
45
+ # `ActionCable.server.remote_connections.where(*)`. Exists solely for the
46
+ # purpose of calling #disconnect on that connection.
47
+ class RemoteConnection
48
+ class InvalidIdentifiersError < StandardError; end
49
+
50
+ include Connection::Identification, Connection::InternalChannel
51
+
52
+ def initialize(server, ids)
53
+ @server = server
54
+ set_identifier_instance_vars(ids)
55
+ end
56
+
57
+ # Uses the internal channel to disconnect the connection.
58
+ def disconnect(reconnect: true)
59
+ server.broadcast internal_channel, { type: "disconnect", reconnect: reconnect }
60
+ end
61
+
62
+ # Returns all the identifiers that were applied to this connection.
63
+ redefine_method :identifiers do
64
+ server.connection_identifiers
65
+ end
66
+
67
+ protected
68
+ attr_reader :server
69
+
70
+ private
71
+ def set_identifier_instance_vars(ids)
72
+ raise InvalidIdentifiersError unless valid_identifiers?(ids)
73
+ ids.each { |k, v| instance_variable_set("@#{k}", v) }
74
+ end
75
+
76
+ def valid_identifiers?(ids)
77
+ keys = ids.keys
78
+ identifiers.all? { |id| keys.include?(id) }
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "monitor"
6
+
7
+ module ActionCable
8
+ module Server
9
+ # # Action Cable Server Base
10
+ #
11
+ # A singleton ActionCable::Server instance is available via ActionCable.server.
12
+ # It's used by the Rack process that starts the Action Cable server, but is also
13
+ # used by the user to reach the RemoteConnections object, which is used for
14
+ # finding and disconnecting connections across all servers.
15
+ #
16
+ # Also, this is the server instance used for broadcasting. See Broadcasting for
17
+ # more information.
18
+ class Base
19
+ include ActionCable::Server::Broadcasting
20
+ include ActionCable::Server::Connections
21
+
22
+ cattr_accessor :config, instance_accessor: false, default: ActionCable::Server::Configuration.new
23
+
24
+ attr_reader :config
25
+
26
+ def self.logger; config.logger; end
27
+ delegate :logger, to: :config
28
+
29
+ attr_reader :mutex
30
+
31
+ def initialize(config: self.class.config)
32
+ @config = config
33
+ @mutex = Monitor.new
34
+ @remote_connections = @event_loop = @worker_pool = @pubsub = nil
35
+ end
36
+
37
+ # Called by Rack to set up the server.
38
+ def call(env)
39
+ return config.health_check_application.call(env) if env["PATH_INFO"] == config.health_check_path
40
+ setup_heartbeat_timer
41
+ config.connection_class.call.new(self, env).process
42
+ end
43
+
44
+ # Disconnect all the connections identified by `identifiers` on this server or
45
+ # any others via RemoteConnections.
46
+ def disconnect(identifiers)
47
+ remote_connections.where(identifiers).disconnect
48
+ end
49
+
50
+ def restart
51
+ connections.each do |connection|
52
+ connection.close(reason: ActionCable::INTERNAL[:disconnect_reasons][:server_restart])
53
+ end
54
+
55
+ @mutex.synchronize do
56
+ # Shutdown the worker pool
57
+ @worker_pool.halt if @worker_pool
58
+ @worker_pool = nil
59
+
60
+ # Shutdown the pub/sub adapter
61
+ @pubsub.shutdown if @pubsub
62
+ @pubsub = nil
63
+ end
64
+ end
65
+
66
+ # Gateway to RemoteConnections. See that class for details.
67
+ def remote_connections
68
+ @remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
69
+ end
70
+
71
+ def event_loop
72
+ @event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new }
73
+ end
74
+
75
+ # The worker pool is where we run connection callbacks and channel actions. We
76
+ # do as little as possible on the server's main thread. The worker pool is an
77
+ # executor service that's backed by a pool of threads working from a task queue.
78
+ # The thread pool size maxes out at 4 worker threads by default. Tune the size
79
+ # yourself with `config.action_cable.worker_pool_size`.
80
+ #
81
+ # Using Active Record, Redis, etc within your channel actions means you'll get a
82
+ # separate connection from each thread in the worker pool. Plan your deployment
83
+ # accordingly: 5 servers each running 5 Puma workers each running an 8-thread
84
+ # worker pool means at least 200 database connections.
85
+ #
86
+ # Also, ensure that your database connection pool size is as least as large as
87
+ # your worker pool size. Otherwise, workers may oversubscribe the database
88
+ # connection pool and block while they wait for other workers to release their
89
+ # connections. Use a smaller worker pool or a larger database connection pool
90
+ # instead.
91
+ def worker_pool
92
+ @worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
93
+ end
94
+
95
+ # Adapter used for all streams/broadcasting.
96
+ def pubsub
97
+ @pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
98
+ end
99
+
100
+ # All of the identifiers applied to the connection class associated with this
101
+ # server.
102
+ def connection_identifiers
103
+ config.connection_class.call.identifiers
104
+ end
105
+ end
106
+
107
+ ActiveSupport.run_load_hooks(:action_cable, Base.config)
108
+ end
109
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Server
7
+ # # Action Cable Server Broadcasting
8
+ #
9
+ # Broadcasting is how other parts of your application can send messages to a
10
+ # channel's subscribers. As explained in Channel, most of the time, these
11
+ # broadcastings are streamed directly to the clients subscribed to the named
12
+ # broadcasting. Let's explain with a full-stack example:
13
+ #
14
+ # class WebNotificationsChannel < ApplicationCable::Channel
15
+ # def subscribed
16
+ # stream_from "web_notifications_#{current_user.id}"
17
+ # end
18
+ # end
19
+ #
20
+ # # Somewhere in your app this is called, perhaps from a NewCommentJob:
21
+ # ActionCable.server.broadcast \
22
+ # "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
23
+ #
24
+ # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
25
+ # App.cable.subscriptions.create "WebNotificationsChannel",
26
+ # received: (data) ->
27
+ # new Notification data['title'], body: data['body']
28
+ module Broadcasting
29
+ # Broadcast a hash directly to a named `broadcasting`. This will later be JSON
30
+ # encoded.
31
+ def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
32
+ broadcaster_for(broadcasting, coder: coder).broadcast(message)
33
+ end
34
+
35
+ # Returns a broadcaster for a named `broadcasting` that can be reused. Useful
36
+ # when you have an object that may need multiple spots to transmit to a specific
37
+ # broadcasting over and over.
38
+ def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
39
+ Broadcaster.new(self, String(broadcasting), coder: coder)
40
+ end
41
+
42
+ private
43
+ class Broadcaster
44
+ attr_reader :server, :broadcasting, :coder
45
+
46
+ def initialize(server, broadcasting, coder:)
47
+ @server, @broadcasting, @coder = server, broadcasting, coder
48
+ end
49
+
50
+ def broadcast(message)
51
+ server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect.truncate(300)}" }
52
+
53
+ payload = { broadcasting: broadcasting, message: message, coder: coder }
54
+ ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
55
+ encoded = coder ? coder.encode(message) : message
56
+ server.pubsub.broadcast broadcasting, encoded
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "rack"
6
+
7
+ module ActionCable
8
+ module Server
9
+ # # Action Cable Server Configuration
10
+ #
11
+ # An instance of this configuration object is available via
12
+ # ActionCable.server.config, which allows you to tweak Action Cable
13
+ # configuration in a Rails config initializer.
14
+ class Configuration
15
+ attr_accessor :logger, :log_tags
16
+ attr_accessor :connection_class, :worker_pool_size
17
+ attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host, :filter_parameters
18
+ attr_accessor :cable, :url, :mount_path
19
+ attr_accessor :precompile_assets
20
+ attr_accessor :health_check_path, :health_check_application
21
+
22
+ def initialize
23
+ @log_tags = []
24
+
25
+ @connection_class = -> { ActionCable::Connection::Base }
26
+ @worker_pool_size = 4
27
+
28
+ @disable_request_forgery_protection = false
29
+ @allow_same_origin_as_host = true
30
+ @filter_parameters = []
31
+
32
+ @health_check_application = ->(env) {
33
+ [200, { Rack::CONTENT_TYPE => "text/html", "date" => Time.now.httpdate }, []]
34
+ }
35
+ end
36
+
37
+ # Returns constant of subscription adapter specified in config/cable.yml. If the
38
+ # adapter cannot be found, this will default to the Redis adapter. Also makes
39
+ # sure proper dependencies are required.
40
+ def pubsub_adapter
41
+ adapter = (cable.fetch("adapter") { "redis" })
42
+
43
+ # Require the adapter itself and give useful feedback about
44
+ # 1. Missing adapter gems and
45
+ # 2. Adapter gems' missing dependencies.
46
+ path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
47
+ begin
48
+ require path_to_adapter
49
+ rescue LoadError => e
50
+ # We couldn't require the adapter itself. Raise an exception that points out
51
+ # config typos and missing gems.
52
+ if e.path == path_to_adapter
53
+ # We can assume that a non-builtin adapter was specified, so it's either
54
+ # misspelled or missing from Gemfile.
55
+ 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
56
+
57
+ # Bubbled up from the adapter require. Prefix the exception message with some
58
+ # guidance about how to address it and reraise.
59
+ else
60
+ raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
61
+ end
62
+ end
63
+
64
+ adapter = adapter.camelize
65
+ adapter = "PostgreSQL" if adapter == "Postgresql"
66
+ "ActionCable::SubscriptionAdapter::#{adapter}".constantize
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Server
7
+ # # Action Cable Server Connections
8
+ #
9
+ # Collection class for all the connections that have been established on this
10
+ # specific server. Remember, usually you'll run many Action Cable servers, so
11
+ # you can't use this collection as a full list of all of the connections
12
+ # established against your application. Instead, use RemoteConnections for that.
13
+ module Connections # :nodoc:
14
+ BEAT_INTERVAL = 3
15
+
16
+ def connections
17
+ @connections ||= []
18
+ end
19
+
20
+ def add_connection(connection)
21
+ connections << connection
22
+ end
23
+
24
+ def remove_connection(connection)
25
+ connections.delete connection
26
+ end
27
+
28
+ # WebSocket connection implementations differ on when they'll mark a connection
29
+ # as stale. We basically never want a connection to go stale, as you then can't
30
+ # rely on being able to communicate with the connection. To solve this, a 3
31
+ # second heartbeat runs on all connections. If the beat fails, we automatically
32
+ # disconnect.
33
+ def setup_heartbeat_timer
34
+ @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
35
+ event_loop.post { connections.each(&:beat) }
36
+ end
37
+ end
38
+
39
+ def open_connections_statistics
40
+ connections.map(&:statistics)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module Server
7
+ class Worker
8
+ module ActiveRecordConnectionManagement
9
+ extend ActiveSupport::Concern
10
+
11
+ included do
12
+ if defined?(ActiveRecord::Base)
13
+ set_callback :work, :around, :with_database_connections
14
+ end
15
+ end
16
+
17
+ def with_database_connections(&block)
18
+ connection.logger.tag(ActiveRecord::Base.logger, &block)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ require "active_support/callbacks"
6
+ require "active_support/core_ext/module/attribute_accessors_per_thread"
7
+ require "concurrent"
8
+
9
+ module ActionCable
10
+ module Server
11
+ # Worker used by Server.send_async to do connection work in threads.
12
+ class Worker # :nodoc:
13
+ include ActiveSupport::Callbacks
14
+
15
+ thread_mattr_accessor :connection
16
+ define_callbacks :work
17
+ include ActiveRecordConnectionManagement
18
+
19
+ attr_reader :executor
20
+
21
+ def initialize(max_size: 5)
22
+ @executor = Concurrent::ThreadPoolExecutor.new(
23
+ name: "ActionCable",
24
+ min_threads: 1,
25
+ max_threads: max_size,
26
+ max_queue: 0,
27
+ )
28
+ end
29
+
30
+ # Stop processing work: any work that has not already started running will be
31
+ # discarded from the queue
32
+ def halt
33
+ @executor.shutdown
34
+ end
35
+
36
+ def stopping?
37
+ @executor.shuttingdown?
38
+ end
39
+
40
+ def work(connection, &block)
41
+ self.connection = connection
42
+
43
+ run_callbacks :work, &block
44
+ ensure
45
+ self.connection = nil
46
+ end
47
+
48
+ def async_exec(receiver, *args, connection:, &block)
49
+ async_invoke receiver, :instance_exec, *args, connection: connection, &block
50
+ end
51
+
52
+ def async_invoke(receiver, method, *args, connection: receiver, &block)
53
+ @executor.post do
54
+ invoke(receiver, method, *args, connection: connection, &block)
55
+ end
56
+ end
57
+
58
+ def invoke(receiver, method, *args, connection:, &block)
59
+ work(connection) do
60
+ receiver.send method, *args, &block
61
+ rescue Exception => e
62
+ logger.error "There was an exception - #{e.class}(#{e.message})"
63
+ logger.error e.backtrace.join("\n")
64
+
65
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
66
+ end
67
+ end
68
+
69
+ private
70
+ def logger
71
+ ActionCable.server.logger
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionCable
6
+ module SubscriptionAdapter
7
+ class Async < Inline # :nodoc:
8
+ private
9
+ def new_subscriber_map
10
+ AsyncSubscriberMap.new(server.event_loop)
11
+ end
12
+
13
+ class AsyncSubscriberMap < SubscriberMap
14
+ def initialize(event_loop)
15
+ @event_loop = event_loop
16
+ super()
17
+ end
18
+
19
+ def add_subscriber(*)
20
+ @event_loop.post { super }
21
+ end
22
+
23
+ def invoke_callback(*)
24
+ @event_loop.post { super }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end