Skip to content

Commit

Permalink
+ connection: add sid
Browse files Browse the repository at this point in the history
Add unique connection identifier (inferred from request ID f any).

The primary reason is to migrate the connections list to Hash for faster deletion.

Closes #4
  • Loading branch information
palkan committed Feb 3, 2025
1 parent 21e5ec7 commit 73e2e63
Show file tree
Hide file tree
Showing 8 changed files with 33 additions and 11 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## main

- Add `Connection#sid`, a unique connection identifier.

- Add `Connection#broadcast` as an interface for broadcasting from channels.

- Add `config.fastlane_broadcasts_enabled` to opt-in for optimized broadcasts (no double JSON encoding).
Expand Down
4 changes: 3 additions & 1 deletion lib/action_cable/connection/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Base
include Callbacks
include ActiveSupport::Rescuable

attr_reader :subscriptions, :logger
attr_reader :subscriptions, :logger, :sid
private attr_reader :server, :socket

delegate :pubsub, :executor, :config, :broadcast, to: :server
Expand All @@ -68,6 +68,8 @@ class Base
def initialize(server, socket)
@server = server
@socket = socket
# unique session identifier (obtained from the request_id by default)
@sid = socket.try(:request_id) || SecureRandom.uuid

@logger = socket.logger
@subscriptions = Subscriptions.new(self)
Expand Down
14 changes: 8 additions & 6 deletions lib/action_cable/server/connections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@ module Server
module Connections # :nodoc:
BEAT_INTERVAL = 3

def connections
@connections ||= []
def connections_map
@connections_map ||= {}
end

def connections = connections_map.values

def add_connection(connection)
connections << connection
connections_map[connection.sid] = connection
end

def remove_connection(connection)
connections.delete connection
connections_map.delete connection.sid
end

# WebSocket connection implementations differ on when they'll mark a connection
Expand All @@ -32,12 +34,12 @@ def remove_connection(connection)
# disconnect.
def setup_heartbeat_timer
@heartbeat_timer ||= executor.timer(BEAT_INTERVAL) do
executor.post { connections.each(&:beat) }
executor.post { connections_map.each_value(&:beat) }
end
end

def open_connections_statistics
connections.map(&:statistics)
connections_map.each_value.map(&:statistics)
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/action_cable/server/socket.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Server
# This connection object is also responsible for handling encoding and decoding of messages, so the user-level
# connection object shouldn't know about such details.
class Socket
attr_reader :server, :env, :protocol, :logger, :connection
attr_reader :server, :env, :protocol, :logger, :connection, :request_id
private attr_reader :worker_pool

delegate :event_loop, :pubsub, :config, to: :server
Expand All @@ -24,6 +24,7 @@ def initialize(server, env, coder: ActiveSupport::JSON)
@message_buffer = MessageBuffer.new(self)

@protocol = nil
@request_id = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
@connection = config.connection_class.call.new(server, self)
end

Expand Down
6 changes: 6 additions & 0 deletions test/server/base_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ def setup
end

class FakeConnection
attr_reader :sid

def initialize
@sid = SecureRandom.hex(3)
end

def close
end
end
Expand Down
6 changes: 5 additions & 1 deletion test/server/socket/client_socket_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
class ActionCable::Server::Socket::ClientSocketTest < ActionCable::TestCase
class TestSocket < ActionCable::Server::Socket
class TestConnection
attr_reader :sid

def initialize(socket)
@socket = socket
@sid = socket.request_id
end

def handle_open = @socket.connect
Expand Down Expand Up @@ -74,7 +77,8 @@ def on_error(message)
def open_connection
env = Rack::MockRequest.env_for "/test",
"HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
"HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
"HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com",
"HTTP_X_REQUEST_ID" => SecureRandom.uuid
io, client_io = \
begin
Socket.pair(Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
Expand Down
6 changes: 5 additions & 1 deletion test/server/socket/stream_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
class ActionCable::Server::Socket::StreamTest < ActionCable::TestCase
class TestSocket < ActionCable::Server::Socket
class TestConnection
attr_reader :sid

def initialize(socket)
@socket = socket
@sid = socket.request_id
end

def handle_open = @socket.connect
Expand Down Expand Up @@ -64,7 +67,8 @@ def on_error(message)
def open_connection(io)
env = Rack::MockRequest.env_for "/test",
"HTTP_CONNECTION" => "upgrade", "HTTP_UPGRADE" => "websocket",
"HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com"
"HTTP_HOST" => "localhost", "HTTP_ORIGIN" => "http://rubyonrails.com",
"action_dispatch.request_id" => SecureRandom.uuid
env["rack.hijack"] = -> { env["rack.hijack_io"] = io }

TestSocket.new(@server, env).tap do |socket|
Expand Down
3 changes: 2 additions & 1 deletion test/server/socket_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@

class ActionCable::Server::SocketTest < ActionCable::TestCase
class Connection
attr_reader :last_message, :socket, :connected
attr_reader :last_message, :socket, :connected, :sid

def initialize(_server, socket)
@socket = socket
@sid = SecureRandom.hex(3)
end

def handle_open
Expand Down

0 comments on commit 73e2e63

Please sign in to comment.