Class: Clacky::Server::HttpServer::WebSocketConnection
- Inherits:
-
Object
- Object
- Clacky::Server::HttpServer::WebSocketConnection
- Defined in:
- lib/clacky/server/http_server.rb
Overview
Wraps a raw TCP socket, providing thread-safe WebSocket frame sending.
IMPORTANT: send_raw is called from the Agent thread via broadcast() →send_json(). A blocking socket write with no deadline can pin the Agent thread indefinitely when the client’s receive buffer fills up (silent disconnects such as Wi-Fi handoff or NAT timeout, where TCP keepalive defaults are measured in hours). Thread#raise on blocking native socket writes is best-effort and unreliable, so instead we bound every write with an explicit deadline using IO.select + write_nonblock and declare the connection dead on timeout.
Constant Summary collapse
- SEND_DEADLINE =
Maximum time a single send_raw call is allowed to spend writing. 5 seconds is generous for healthy LAN/Internet clients and short enough that a stuck Agent becomes responsive again quickly.
5.0- SEND_SLOW_WARN =
Warn threshold — any individual send_raw that exceeds this is logged so we can spot sluggish clients before they fully hang.
1.0
Instance Attribute Summary collapse
-
#session_id ⇒ Object
Returns the value of attribute session_id.
Class Method Summary collapse
-
.apply_keepalive(socket) ⇒ Object
Enable TCP keepalive on the underlying socket so silently dead peers are detected in minutes instead of the OS default of hours.
Instance Method Summary collapse
-
#closed? ⇒ Boolean
Returns true if the underlying socket has been detected as dead.
-
#force_close! ⇒ Object
Force-close the connection (used by the interrupt watchdog when an Agent thread is stuck on an unresponsive socket write).
-
#initialize(socket, version) ⇒ WebSocketConnection
constructor
A new instance of WebSocketConnection.
-
#send_json(data) ⇒ Object
Send a JSON-serializable object over the WebSocket.
-
#send_raw(type, data) ⇒ Object
Send a raw WebSocket frame.
Constructor Details
#initialize(socket, version) ⇒ WebSocketConnection
Returns a new instance of WebSocketConnection.
4804 4805 4806 4807 4808 4809 4810 |
# File 'lib/clacky/server/http_server.rb', line 4804 def initialize(socket, version) @socket = socket @version = version @send_mutex = Mutex.new @closed = false WebSocketConnection.apply_keepalive(socket) end |
Instance Attribute Details
#session_id ⇒ Object
Returns the value of attribute session_id.
4793 4794 4795 |
# File 'lib/clacky/server/http_server.rb', line 4793 def session_id @session_id end |
Class Method Details
.apply_keepalive(socket) ⇒ Object
Enable TCP keepalive on the underlying socket so silently dead peers are detected in minutes instead of the OS default of hours. Best-effort: any failure is logged at debug level and ignored.
4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 4942 4943 4944 4945 4946 4947 4948 4949 4950 |
# File 'lib/clacky/server/http_server.rb', line 4924 def self.apply_keepalive(socket) return unless socket.respond_to?(:setsockopt) socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true) # TCP-level keepalive tuning — constants vary by platform and are # only set when available. Values chosen to detect dead peers in # roughly 60-90 seconds total. if defined?(Socket::IPPROTO_TCP) # Idle time before first probe (Linux: TCP_KEEPIDLE, macOS: TCP_KEEPALIVE) idle_const = if Socket.const_defined?(:TCP_KEEPIDLE) Socket::TCP_KEEPIDLE elsif Socket.const_defined?(:TCP_KEEPALIVE) Socket::TCP_KEEPALIVE end socket.setsockopt(Socket::IPPROTO_TCP, idle_const, 60) if idle_const if Socket.const_defined?(:TCP_KEEPINTVL) socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, 10) end if Socket.const_defined?(:TCP_KEEPCNT) socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPCNT, 3) end end rescue StandardError => e Clacky::Logger.debug("[WS] failed to set keepalive: #{e.class}: #{e.}") end |
Instance Method Details
#closed? ⇒ Boolean
Returns true if the underlying socket has been detected as dead.
4813 4814 4815 |
# File 'lib/clacky/server/http_server.rb', line 4813 def closed? @closed end |
#force_close! ⇒ Object
Force-close the connection (used by the interrupt watchdog when an Agent thread is stuck on an unresponsive socket write).
4819 4820 4821 4822 4823 4824 |
# File 'lib/clacky/server/http_server.rb', line 4819 def force_close! @closed = true @socket.close rescue StandardError # best effort end |
#send_json(data) ⇒ Object
Send a JSON-serializable object over the WebSocket. Returns true on success, false if the connection is dead.
4828 4829 4830 4831 4832 4833 |
# File 'lib/clacky/server/http_server.rb', line 4828 def send_json(data) send_raw(:text, JSON.generate(data)) rescue => e Clacky::Logger.debug("WS send error (connection dead): #{e.}") false end |
#send_raw(type, data) ⇒ Object
Send a raw WebSocket frame. Returns true on success, false on broken/closed/sluggish socket.
Uses write_nonblock with an overall deadline so the caller (typically the Agent thread) never blocks longer than SEND_DEADLINE, even if the client silently stopped reading.
4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 |
# File 'lib/clacky/server/http_server.rb', line 4841 def send_raw(type, data) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) @send_mutex.synchronize do return false if @closed outgoing = WebSocket::Frame::Outgoing::Server.new( version: @version, data: data, type: type ) bytes = outgoing.to_s unless write_with_deadline(bytes, SEND_DEADLINE) # Deadline exceeded — treat as a dead connection so broadcast # purges it and the Agent thread is freed immediately. @closed = true begin @socket.close rescue StandardError # ignore end Clacky::Logger.warn( "[WS] send_raw deadline exceeded — closing sluggish connection " \ "(bytes=#{bytes.bytesize}, deadline=#{SEND_DEADLINE}s)" ) return false end end elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at if elapsed > SEND_SLOW_WARN Clacky::Logger.warn( "[WS] send_raw slow: #{elapsed.round(2)}s (type=#{type})" ) end true rescue Errno::EPIPE, Errno::ECONNRESET, IOError, Errno::EBADF => e @closed = true Clacky::Logger.debug("WS send_raw error (client disconnected): #{e.}") false rescue => e @closed = true Clacky::Logger.debug("WS send_raw unexpected error: #{e.}") false end |