PageRenderTime 27ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/redis/connection/ruby.rb

http://github.com/redis/redis-rb
Ruby | 430 lines | 316 code | 77 blank | 37 comment | 33 complexity | 787f37f2b181580e5616d30cb212f267 MD5 | raw file
  1. # frozen_string_literal: true
  2. require_relative "registry"
  3. require_relative "command_helper"
  4. require_relative "../errors"
  5. require "socket"
  6. require "timeout"
  7. begin
  8. require "openssl"
  9. rescue LoadError
  10. # Not all systems have OpenSSL support
  11. end
  12. class Redis
  13. module Connection
  14. module SocketMixin
  15. CRLF = "\r\n"
  16. def initialize(*args)
  17. super(*args)
  18. @timeout = @write_timeout = nil
  19. @buffer = "".b
  20. end
  21. def timeout=(timeout)
  22. @timeout = (timeout if timeout && timeout > 0)
  23. end
  24. def write_timeout=(timeout)
  25. @write_timeout = (timeout if timeout && timeout > 0)
  26. end
  27. def read(nbytes)
  28. result = @buffer.slice!(0, nbytes)
  29. buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
  30. result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
  31. result
  32. end
  33. def gets
  34. while (crlf = @buffer.index(CRLF)).nil?
  35. @buffer << _read_from_socket(16_384)
  36. end
  37. @buffer.slice!(0, crlf + CRLF.bytesize)
  38. end
  39. def _read_from_socket(nbytes, buffer = nil)
  40. loop do
  41. case chunk = read_nonblock(nbytes, buffer, exception: false)
  42. when :wait_readable
  43. unless wait_readable(@timeout)
  44. raise Redis::TimeoutError
  45. end
  46. when :wait_writable
  47. unless wait_writable(@timeout)
  48. raise Redis::TimeoutError
  49. end
  50. when nil
  51. raise Errno::ECONNRESET
  52. when String
  53. return chunk
  54. end
  55. end
  56. end
  57. def write(buffer)
  58. return super(buffer) unless @write_timeout
  59. bytes_to_write = buffer.bytesize
  60. total_bytes_written = 0
  61. loop do
  62. case bytes_written = write_nonblock(buffer, exception: false)
  63. when :wait_readable
  64. unless wait_readable(@write_timeout)
  65. raise Redis::TimeoutError
  66. end
  67. when :wait_writable
  68. unless wait_writable(@write_timeout)
  69. raise Redis::TimeoutError
  70. end
  71. when nil
  72. raise Errno::ECONNRESET
  73. when Integer
  74. total_bytes_written += bytes_written
  75. if total_bytes_written >= bytes_to_write
  76. return total_bytes_written
  77. end
  78. buffer = buffer.byteslice(bytes_written..-1)
  79. end
  80. end
  81. end
  82. end
  83. if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
  84. require "timeout"
  85. class TCPSocket < ::TCPSocket
  86. include SocketMixin
  87. def self.connect(host, port, timeout)
  88. Timeout.timeout(timeout) do
  89. sock = new(host, port)
  90. sock
  91. end
  92. rescue Timeout::Error
  93. raise TimeoutError
  94. end
  95. end
  96. if defined?(::UNIXSocket)
  97. class UNIXSocket < ::UNIXSocket
  98. include SocketMixin
  99. def self.connect(path, timeout)
  100. Timeout.timeout(timeout) do
  101. sock = new(path)
  102. sock
  103. end
  104. rescue Timeout::Error
  105. raise TimeoutError
  106. end
  107. # JRuby raises Errno::EAGAIN on #read_nonblock even when it
  108. # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
  109. # Use the blocking #readpartial method instead.
  110. def _read_from_socket(nbytes, _buffer = nil)
  111. # JRuby: Throw away the buffer as we won't need it
  112. # but still need to support the max arity of 2
  113. readpartial(nbytes)
  114. rescue EOFError
  115. raise Errno::ECONNRESET
  116. end
  117. end
  118. end
  119. else
  120. class TCPSocket < ::Socket
  121. include SocketMixin
  122. def self.connect_addrinfo(addrinfo, port, timeout)
  123. sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0)
  124. sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3])
  125. begin
  126. sock.connect_nonblock(sockaddr)
  127. rescue Errno::EINPROGRESS
  128. raise TimeoutError unless sock.wait_writable(timeout)
  129. begin
  130. sock.connect_nonblock(sockaddr)
  131. rescue Errno::EISCONN
  132. end
  133. end
  134. sock
  135. end
  136. def self.connect(host, port, timeout)
  137. # Don't pass AI_ADDRCONFIG as flag to getaddrinfo(3)
  138. #
  139. # From the man page for getaddrinfo(3):
  140. #
  141. # If hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4
  142. # addresses are returned in the list pointed to by res only if the
  143. # local system has at least one IPv4 address configured, and IPv6
  144. # addresses are returned only if the local system has at least one
  145. # IPv6 address configured. The loopback address is not considered
  146. # for this case as valid as a configured address.
  147. #
  148. # We do want the IPv6 loopback address to be returned if applicable,
  149. # even if it is the only configured IPv6 address on the machine.
  150. # Also see: https://github.com/redis/redis-rb/pull/394.
  151. addrinfo = ::Socket.getaddrinfo(host, nil, Socket::AF_UNSPEC, Socket::SOCK_STREAM)
  152. # From the man page for getaddrinfo(3):
  153. #
  154. # Normally, the application should try using the addresses in the
  155. # order in which they are returned. The sorting function used
  156. # within getaddrinfo() is defined in RFC 3484 [...].
  157. #
  158. addrinfo.each_with_index do |ai, i|
  159. begin
  160. return connect_addrinfo(ai, port, timeout)
  161. rescue SystemCallError
  162. # Raise if this was our last attempt.
  163. raise if addrinfo.length == i + 1
  164. end
  165. end
  166. end
  167. end
  168. class UNIXSocket < ::Socket
  169. include SocketMixin
  170. def self.connect(path, timeout)
  171. sock = new(::Socket::AF_UNIX, Socket::SOCK_STREAM, 0)
  172. sockaddr = ::Socket.pack_sockaddr_un(path)
  173. begin
  174. sock.connect_nonblock(sockaddr)
  175. rescue Errno::EINPROGRESS
  176. raise TimeoutError unless sock.wait_writable(timeout)
  177. begin
  178. sock.connect_nonblock(sockaddr)
  179. rescue Errno::EISCONN
  180. end
  181. end
  182. sock
  183. end
  184. end
  185. end
  186. if defined?(OpenSSL)
  187. class SSLSocket < ::OpenSSL::SSL::SSLSocket
  188. include SocketMixin
  189. unless method_defined?(:wait_readable)
  190. def wait_readable(timeout = nil)
  191. to_io.wait_readable(timeout)
  192. end
  193. end
  194. unless method_defined?(:wait_writable)
  195. def wait_writable(timeout = nil)
  196. to_io.wait_writable(timeout)
  197. end
  198. end
  199. def self.connect(host, port, timeout, ssl_params)
  200. # Note: this is using Redis::Connection::TCPSocket
  201. tcp_sock = TCPSocket.connect(host, port, timeout)
  202. ctx = OpenSSL::SSL::SSLContext.new
  203. # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
  204. ctx.set_params(ssl_params || {})
  205. ssl_sock = new(tcp_sock, ctx)
  206. ssl_sock.hostname = host
  207. begin
  208. # Initiate the socket connection in the background. If it doesn't fail
  209. # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
  210. # indicating the connection is in progress.
  211. # Unlike waiting for a tcp socket to connect, you can't time out ssl socket
  212. # connections during the connect phase properly, because IO.select only partially works.
  213. # Instead, you have to retry.
  214. ssl_sock.connect_nonblock
  215. rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
  216. if ssl_sock.wait_readable(timeout)
  217. retry
  218. else
  219. raise TimeoutError
  220. end
  221. rescue IO::WaitWritable
  222. if ssl_sock.wait_writable(timeout)
  223. retry
  224. else
  225. raise TimeoutError
  226. end
  227. end
  228. unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (
  229. ctx.respond_to?(:verify_hostname) &&
  230. !ctx.verify_hostname
  231. )
  232. ssl_sock.post_connection_check(host)
  233. end
  234. ssl_sock
  235. end
  236. end
  237. end
  238. class Ruby
  239. include Redis::Connection::CommandHelper
  240. MINUS = "-"
  241. PLUS = "+"
  242. COLON = ":"
  243. DOLLAR = "$"
  244. ASTERISK = "*"
  245. def self.connect(config)
  246. if config[:scheme] == "unix"
  247. raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
  248. sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
  249. elsif config[:scheme] == "rediss" || config[:ssl]
  250. sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
  251. else
  252. sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
  253. end
  254. instance = new(sock)
  255. instance.timeout = config[:read_timeout]
  256. instance.write_timeout = config[:write_timeout]
  257. instance.set_tcp_keepalive config[:tcp_keepalive]
  258. instance.set_tcp_nodelay if sock.is_a? TCPSocket
  259. instance
  260. end
  261. if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c }
  262. def set_tcp_keepalive(keepalive)
  263. return unless keepalive.is_a?(Hash)
  264. @sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
  265. @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, keepalive[:time])
  266. @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, keepalive[:intvl])
  267. @sock.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, keepalive[:probes])
  268. end
  269. def get_tcp_keepalive
  270. {
  271. time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
  272. intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
  273. probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int
  274. }
  275. end
  276. else
  277. def set_tcp_keepalive(keepalive); end
  278. def get_tcp_keepalive
  279. {
  280. }
  281. end
  282. end
  283. # disables Nagle's Algorithm, prevents multiple round trips with MULTI
  284. if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c }
  285. def set_tcp_nodelay
  286. @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
  287. end
  288. else
  289. def set_tcp_nodelay; end
  290. end
  291. def initialize(sock)
  292. @sock = sock
  293. end
  294. def connected?
  295. !!@sock
  296. end
  297. def disconnect
  298. @sock.close
  299. rescue
  300. ensure
  301. @sock = nil
  302. end
  303. def timeout=(timeout)
  304. @sock.timeout = timeout if @sock.respond_to?(:timeout=)
  305. end
  306. def write_timeout=(timeout)
  307. @sock.write_timeout = timeout
  308. end
  309. def write(command)
  310. @sock.write(build_command(command))
  311. end
  312. def read
  313. line = @sock.gets
  314. reply_type = line.slice!(0, 1)
  315. format_reply(reply_type, line)
  316. rescue Errno::EAGAIN
  317. raise TimeoutError
  318. end
  319. def format_reply(reply_type, line)
  320. case reply_type
  321. when MINUS then format_error_reply(line)
  322. when PLUS then format_status_reply(line)
  323. when COLON then format_integer_reply(line)
  324. when DOLLAR then format_bulk_reply(line)
  325. when ASTERISK then format_multi_bulk_reply(line)
  326. else raise ProtocolError, reply_type
  327. end
  328. end
  329. def format_error_reply(line)
  330. CommandError.new(line.strip)
  331. end
  332. def format_status_reply(line)
  333. line.strip
  334. end
  335. def format_integer_reply(line)
  336. line.to_i
  337. end
  338. def format_bulk_reply(line)
  339. bulklen = line.to_i
  340. return if bulklen == -1
  341. reply = encode(@sock.read(bulklen))
  342. @sock.read(2) # Discard CRLF.
  343. reply
  344. end
  345. def format_multi_bulk_reply(line)
  346. n = line.to_i
  347. return if n == -1
  348. Array.new(n) { read }
  349. end
  350. end
  351. end
  352. end
  353. Redis::Connection.drivers << Redis::Connection::Ruby