Module: TcpUserTimeout
- Defined in:
- lib/tcp_user_timeout.rb,
lib/tcp_user_timeout/hook.rb,
lib/tcp_user_timeout/rack.rb,
lib/tcp_user_timeout/storage.rb,
lib/tcp_user_timeout/version.rb,
lib/tcp_user_timeout/active_job.rb
Overview
Kernel-enforced socket deadlines on Linux via TCP_USER_TIMEOUT.
TcpUserTimeout.with_timeout(30) do
Net::HTTP.get(URI("https://upstream.example/slow"))
end
Sockets opened inside the block get TCP_USER_TIMEOUT applied. The Linux kernel forcibly closes the connection when transmitted data has been unacknowledged for the configured time, raising Errno::ETIMEDOUT / IO::TimeoutError from the next blocking syscall. Catches write-side wedges and network partitions.
Pre-existing sockets — DB pools, persistent HTTP pools created at boot — are never re-bound. Only sockets created inside the block inherit the deadline.
Platform support:
- Linux: enforced by the kernel.
- Other platforms (macOS, BSD, Windows): silent no-op. setsockopt
raises Errno::ENOPROTOOPT (or similar) and is rescued. There is no
direct equivalent of TCP_USER_TIMEOUT on macOS.
What this does NOT cover:
- Read-side wedges where the peer's userspace is wedged but its
kernel is responsive. TCP_USER_TIMEOUT does not fire because the
peer kernel auto-ACKs packets even when its application is stuck.
For those wedges use application-level timeouts (Net::HTTP
read_timeout, IO#timeout=, SDK-specific request timeouts).
- FFI / libcurl-based HTTP clients (curb, etc.) — bypass Ruby's
socket layer entirely.
- DNS (getaddrinfo). Mitigate via resolv.conf.
- Connect phase. Use Net::HTTP#open_timeout / libpq connect_timeout.
Defined Under Namespace
Modules: ActiveJob, Hook, Rack, Storage Classes: Error, State
Constant Summary collapse
- TCP_USER_TIMEOUT_OPT =
TCP_USER_TIMEOUT optname from <linux/tcp.h>. Hardcoded because Socket::TCP_USER_TIMEOUT is not exposed on every Ruby version.
18- STORAGE_KEY =
:tcp_user_timeout_state- VERSION =
"0.1.0"
Class Attribute Summary collapse
-
.global_default_seconds ⇒ Object
Optional ceiling applied to every outbound TCP socket created when no with_timeout block is in effect.
Class Method Summary collapse
-
.current_state ⇒ Object
Public for testing and instrumentation.
-
.install! ⇒ Object
Idempotent installation of Socket / TCPSocket prepend hooks.
- .installed? ⇒ Boolean
-
.maybe_apply!(socket) ⇒ Object
Apply the currently-scoped state (or the global default) to a socket.
-
.with_timeout(seconds) ⇒ Object
Bound sockets opened inside the block to ‘seconds`.
Class Attribute Details
.global_default_seconds ⇒ Object
Optional ceiling applied to every outbound TCP socket created when no with_timeout block is in effect. nil disables the global default.
58 59 60 |
# File 'lib/tcp_user_timeout.rb', line 58 def global_default_seconds @global_default_seconds end |
Class Method Details
.current_state ⇒ Object
Public for testing and instrumentation.
100 101 102 |
# File 'lib/tcp_user_timeout.rb', line 100 def current_state Storage[STORAGE_KEY] || global_default_state end |
.install! ⇒ Object
Idempotent installation of Socket / TCPSocket prepend hooks.
87 88 89 90 91 92 93 |
# File 'lib/tcp_user_timeout.rb', line 87 def install! return if @installed ::Socket.singleton_class.prepend(Hook::SocketTcp) ::TCPSocket.singleton_class.prepend(Hook::TCPSocketSingleton) @installed = true end |
.installed? ⇒ Boolean
95 96 97 |
# File 'lib/tcp_user_timeout.rb', line 95 def installed? @installed == true end |
.maybe_apply!(socket) ⇒ Object
Apply the currently-scoped state (or the global default) to a socket. Silently no-ops on platforms / sockets that don’t support the option.
77 78 79 80 81 82 83 84 |
# File 'lib/tcp_user_timeout.rb', line 77 def maybe_apply!(socket) state = current_state return unless state&.timeout_ms&.positive? socket.setsockopt(::Socket::IPPROTO_TCP, TCP_USER_TIMEOUT_OPT, state.timeout_ms) rescue Errno::ENOPROTOOPT, Errno::EINVAL, Errno::ENOTSOCK, Errno::EBADF nil end |
.with_timeout(seconds) ⇒ Object
Bound sockets opened inside the block to ‘seconds`.
65 66 67 68 69 70 71 72 73 |
# File 'lib/tcp_user_timeout.rb', line 65 def with_timeout(seconds) install! state = State.new(seconds.to_f) prev = Storage[STORAGE_KEY] Storage[STORAGE_KEY] = state yield ensure Storage[STORAGE_KEY] = prev end |