redis-client 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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