redis-client 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,17 +6,29 @@ class RedisClient
6
6
  class Pooled
7
7
  EMPTY_HASH = {}.freeze
8
8
 
9
- attr_reader :config
9
+ include Common
10
10
 
11
- def initialize(config, **kwargs)
12
- @config = config
11
+ def initialize(
12
+ config,
13
+ id: config.id,
14
+ connect_timeout: config.connect_timeout,
15
+ read_timeout: config.read_timeout,
16
+ write_timeout: config.write_timeout,
17
+ **kwargs
18
+ )
19
+ super(config, id: id, connect_timeout: connect_timeout, read_timeout: read_timeout, write_timeout: write_timeout)
13
20
  @pool_kwargs = kwargs
14
21
  @pool = new_pool
15
22
  @mutex = Mutex.new
16
23
  end
17
24
 
18
- def with(options = EMPTY_HASH, &block)
19
- pool.with(options, &block)
25
+ def with(options = EMPTY_HASH)
26
+ pool.with(options) do |client|
27
+ client.connect_timeout = connect_timeout
28
+ client.read_timeout = read_timeout
29
+ client.write_timeout = write_timeout
30
+ yield client
31
+ end
20
32
  rescue ConnectionPool::TimeoutError => error
21
33
  raise CheckoutTimeoutError, "Couldn't checkout a connection in time: #{error.message}"
22
34
  end
@@ -37,40 +49,48 @@ class RedisClient
37
49
  pool.size
38
50
  end
39
51
 
40
- %w(pipelined).each do |method|
41
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
42
- def #{method}(&block)
43
- with { |r| r.#{method}(&block) }
44
- end
45
- RUBY
46
- end
52
+ methods = %w(pipelined multi pubsub call call_once blocking_call)
53
+ iterable_methods = %w(scan sscan hscan zscan)
54
+ begin
55
+ methods.each do |method|
56
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
57
+ def #{method}(...)
58
+ with { |r| r.#{method}(...) }
59
+ end
60
+ RUBY
61
+ end
47
62
 
48
- %w(multi).each do |method|
49
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
50
- def #{method}(**kwargs, &block)
51
- with { |r| r.#{method}(**kwargs, &block) }
52
- end
53
- RUBY
54
- end
63
+ iterable_methods.each do |method|
64
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
65
+ def #{method}(...)
66
+ unless block_given?
67
+ return to_enum(__callee__, ...)
68
+ end
55
69
 
56
- %w(call call_once blocking_call pubsub).each do |method|
57
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
58
- def #{method}(*args)
59
- with { |r| r.#{method}(*args) }
60
- end
61
- RUBY
62
- end
63
-
64
- %w(scan sscan hscan zscan).each do |method|
65
- class_eval <<~RUBY, __FILE__, __LINE__ + 1
66
- def #{method}(*args, &block)
67
- unless block_given?
68
- return to_enum(__callee__, *args)
70
+ with { |r| r.#{method}(...) }
71
+ end
72
+ RUBY
73
+ end
74
+ rescue SyntaxError
75
+ methods.each do |method|
76
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
77
+ def #{method}(*args, &block)
78
+ with { |r| r.#{method}(*args, &block) }
69
79
  end
80
+ RUBY
81
+ end
70
82
 
71
- with { |r| r.#{method}(*args, &block) }
72
- end
73
- RUBY
83
+ iterable_methods.each do |method|
84
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
85
+ def #{method}(*args, &block)
86
+ unless block_given?
87
+ return to_enum(__callee__, *args)
88
+ end
89
+
90
+ with { |r| r.#{method}(*args, &block) }
91
+ end
92
+ RUBY
93
+ end
74
94
  end
75
95
 
76
96
  private
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/wait" unless IO.method_defined?(:wait_readable) && IO.method_defined?(:wait_writable)
4
+
5
+ class RedisClient
6
+ class RubyConnection
7
+ class BufferedIO
8
+ EOL = "\r\n".b.freeze
9
+ EOL_SIZE = EOL.bytesize
10
+
11
+ attr_accessor :read_timeout, :write_timeout
12
+
13
+ def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
14
+ @io = io
15
+ @buffer = "".b
16
+ @offset = 0
17
+ @chunk_size = chunk_size
18
+ @read_timeout = read_timeout
19
+ @write_timeout = write_timeout
20
+ @blocking_reads = false
21
+ end
22
+
23
+ def close
24
+ @io.to_io.close
25
+ end
26
+
27
+ def closed?
28
+ @io.to_io.closed?
29
+ end
30
+
31
+ def eof?
32
+ @offset >= @buffer.bytesize && @io.eof?
33
+ end
34
+
35
+ def with_timeout(new_timeout)
36
+ new_timeout = false if new_timeout == 0
37
+
38
+ previous_read_timeout = @read_timeout
39
+ previous_blocking_reads = @blocking_reads
40
+
41
+ if new_timeout
42
+ @read_timeout = new_timeout
43
+ else
44
+ @blocking_reads = true
45
+ end
46
+
47
+ begin
48
+ yield
49
+ ensure
50
+ @read_timeout = previous_read_timeout
51
+ @blocking_reads = previous_blocking_reads
52
+ end
53
+ end
54
+
55
+ def skip(offset)
56
+ ensure_remaining(offset)
57
+ @offset += offset
58
+ nil
59
+ end
60
+
61
+ def write(string)
62
+ total = remaining = string.bytesize
63
+ loop do
64
+ case bytes_written = @io.write_nonblock(string, exception: false)
65
+ when Integer
66
+ remaining -= bytes_written
67
+ if remaining > 0
68
+ string = string.byteslice(bytes_written..-1)
69
+ else
70
+ return total
71
+ end
72
+ when :wait_readable
73
+ @io.to_io.wait_readable(@read_timeout) or raise ReadTimeoutError
74
+ when :wait_writable
75
+ @io.to_io.wait_writable(@write_timeout) or raise WriteTimeoutError
76
+ when nil
77
+ raise Errno::ECONNRESET
78
+ else
79
+ raise "Unexpected `write_nonblock` return: #{bytes.inspect}"
80
+ end
81
+ end
82
+ end
83
+
84
+ def getbyte
85
+ ensure_remaining(1)
86
+ byte = @buffer.getbyte(@offset)
87
+ @offset += 1
88
+ byte
89
+ end
90
+
91
+ def gets_chomp
92
+ fill_buffer(false) if @offset >= @buffer.bytesize
93
+ until eol_index = @buffer.index(EOL, @offset)
94
+ fill_buffer(false)
95
+ end
96
+
97
+ line = @buffer.byteslice(@offset, eol_index - @offset)
98
+ @offset = eol_index + EOL_SIZE
99
+ line
100
+ end
101
+
102
+ def read_chomp(bytes)
103
+ ensure_remaining(bytes + EOL_SIZE)
104
+ str = @buffer.byteslice(@offset, bytes)
105
+ @offset += bytes + EOL_SIZE
106
+ str
107
+ end
108
+
109
+ private
110
+
111
+ def ensure_remaining(bytes)
112
+ needed = bytes - (@buffer.bytesize - @offset)
113
+ if needed > 0
114
+ fill_buffer(true, needed)
115
+ end
116
+ end
117
+
118
+ def fill_buffer(strict, size = @chunk_size)
119
+ remaining = size
120
+ empty_buffer = @offset >= @buffer.bytesize
121
+
122
+ loop do
123
+ bytes = if empty_buffer
124
+ @io.read_nonblock([remaining, @chunk_size].max, @buffer, exception: false)
125
+ else
126
+ @io.read_nonblock([remaining, @chunk_size].max, exception: false)
127
+ end
128
+ case bytes
129
+ when String
130
+ if empty_buffer
131
+ @offset = 0
132
+ empty_buffer = false
133
+ else
134
+ @buffer << bytes
135
+ end
136
+ remaining -= bytes.bytesize
137
+ return if !strict || remaining <= 0
138
+ when :wait_readable
139
+ unless @io.to_io.wait_readable(@read_timeout)
140
+ raise ReadTimeoutError unless @blocking_reads
141
+ end
142
+ when :wait_writable
143
+ @io.to_io.wait_writable(@write_timeout) or raise WriteTimeoutError
144
+ when nil
145
+ raise Errno::ECONNRESET
146
+ else
147
+ raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -58,32 +58,6 @@ class RedisClient
58
58
  String.new(encoding: Encoding::BINARY, capacity: 128)
59
59
  end
60
60
 
61
- def coerce_command!(command)
62
- command = command.flat_map do |element|
63
- case element
64
- when Hash
65
- element.flatten
66
- when Set
67
- element.to_a
68
- else
69
- element
70
- end
71
- end
72
-
73
- command.map! do |element|
74
- case element
75
- when String
76
- element
77
- when Integer, Float, Symbol
78
- element.to_s
79
- else
80
- raise TypeError, "Unsupported command argument type: #{element.class}"
81
- end
82
- end
83
-
84
- command
85
- end
86
-
87
61
  def dump_any(object, buffer)
88
62
  method = DUMP_TYPES.fetch(object.class) do
89
63
  raise TypeError, "Unsupported command argument type: #{object.class}"
@@ -147,7 +121,7 @@ class RedisClient
147
121
  str = io.gets_chomp
148
122
  str.force_encoding(Encoding.default_external)
149
123
  str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
150
- str
124
+ str.freeze
151
125
  end
152
126
 
153
127
  def parse_error(io)
@@ -2,22 +2,60 @@
2
2
 
3
3
  require "socket"
4
4
  require "openssl"
5
- require "redis_client/buffered_io"
5
+ require "redis_client/connection_mixin"
6
+ require "redis_client/ruby_connection/buffered_io"
7
+ require "redis_client/ruby_connection/resp3"
6
8
 
7
9
  class RedisClient
8
- class Connection
10
+ class RubyConnection
11
+ include ConnectionMixin
12
+
13
+ class << self
14
+ def ssl_context(ssl_params)
15
+ params = ssl_params.dup || {}
16
+
17
+ cert = params[:cert]
18
+ if cert.is_a?(String)
19
+ cert = File.read(cert) if File.exist?(cert)
20
+ params[:cert] = OpenSSL::X509::Certificate.new(cert)
21
+ end
22
+
23
+ key = params[:key]
24
+ if key.is_a?(String)
25
+ key = File.read(key) if File.exist?(key)
26
+ params[:key] = OpenSSL::PKey.read(key)
27
+ end
28
+
29
+ context = OpenSSL::SSL::SSLContext.new
30
+ context.set_params(params)
31
+ if context.verify_mode != OpenSSL::SSL::VERIFY_NONE
32
+ if context.respond_to?(:verify_hostname) # Missing on JRuby
33
+ context.verify_hostname
34
+ end
35
+ end
36
+
37
+ context
38
+ end
39
+ end
40
+
41
+ SUPPORTS_RESOLV_TIMEOUT = Socket.method(:tcp).parameters.any? { |p| p.last == :resolv_timeout }
42
+
9
43
  def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
10
44
  socket = if config.path
11
45
  UNIXSocket.new(config.path)
12
46
  else
13
- sock = Socket.tcp(config.host, config.port, connect_timeout: connect_timeout)
47
+ sock = if SUPPORTS_RESOLV_TIMEOUT
48
+ Socket.tcp(config.host, config.port, connect_timeout: connect_timeout, resolv_timeout: connect_timeout)
49
+ else
50
+ Socket.tcp(config.host, config.port, connect_timeout: connect_timeout)
51
+ end
14
52
  # disables Nagle's Algorithm, prevents multiple round trips with MULTI
15
53
  sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
16
54
  sock
17
55
  end
18
56
 
19
57
  if config.ssl
20
- socket = OpenSSL::SSL::SSLSocket.new(socket, config.openssl_context)
58
+ socket = OpenSSL::SSL::SSLSocket.new(socket, config.ssl_context)
21
59
  socket.hostname = config.host
22
60
  loop do
23
61
  case status = socket.connect_nonblock(exception: false)
@@ -40,7 +78,7 @@ class RedisClient
40
78
  )
41
79
  rescue Errno::ETIMEDOUT => error
42
80
  raise ConnectTimeoutError, error.message
43
- rescue SystemCallError, OpenSSL::SSL::SSLError => error
81
+ rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
44
82
  raise ConnectionError, error.message
45
83
  end
46
84
 
@@ -52,6 +90,14 @@ class RedisClient
52
90
  @io.close
53
91
  end
54
92
 
93
+ def read_timeout=(timeout)
94
+ @io.read_timeout = timeout if @io
95
+ end
96
+
97
+ def write_timeout=(timeout)
98
+ @io.write_timeout = timeout if @io
99
+ end
100
+
55
101
  def write(command)
56
102
  buffer = RESP3.dump(command)
57
103
  begin
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.1.0"
4
+ VERSION = "0.3.0"
5
5
  end