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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +2 -2
- data/README.md +89 -3
- data/Rakefile +15 -6
- data/ext/redis_client/hiredis/export.clang +2 -0
- data/ext/redis_client/hiredis/export.gcc +7 -0
- data/ext/redis_client/hiredis/extconf.rb +24 -9
- data/ext/redis_client/hiredis/hiredis_connection.c +13 -1
- data/lib/redis_client/command_builder.rb +83 -0
- data/lib/redis_client/config.rb +9 -48
- data/lib/redis_client/connection_mixin.rb +38 -0
- data/lib/redis_client/decorator.rb +84 -0
- data/lib/redis_client/hiredis_connection.rb +16 -1
- data/lib/redis_client/middlewares.rb +12 -0
- data/lib/redis_client/pooled.rb +55 -35
- data/lib/redis_client/ruby_connection/buffered_io.rb +153 -0
- data/lib/redis_client/{resp3.rb → ruby_connection/resp3.rb} +1 -27
- data/lib/redis_client/{connection.rb → ruby_connection.rb} +51 -5
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +239 -106
- metadata +11 -5
- data/lib/redis_client/buffered_io.rb +0 -149
data/lib/redis_client/pooled.rb
CHANGED
@@ -6,17 +6,29 @@ class RedisClient
|
|
6
6
|
class Pooled
|
7
7
|
EMPTY_HASH = {}.freeze
|
8
8
|
|
9
|
-
|
9
|
+
include Common
|
10
10
|
|
11
|
-
def initialize(
|
12
|
-
|
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
|
19
|
-
pool.with(options
|
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
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
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/
|
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
|
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 =
|
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.
|
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
|
data/lib/redis_client/version.rb
CHANGED