redis-client 0.19.1 → 0.22.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +27 -21
- data/README.md +26 -3
- data/Rakefile +4 -1
- data/lib/redis_client/config.rb +33 -5
- data/lib/redis_client/connection_mixin.rb +27 -8
- data/lib/redis_client/decorator.rb +2 -2
- data/lib/redis_client/ruby_connection/buffered_io.rb +124 -35
- data/lib/redis_client/ruby_connection/resp3.rb +35 -12
- data/lib/redis_client/ruby_connection.rb +10 -8
- data/lib/redis_client/sentinel_config.rb +10 -0
- data/lib/redis_client/url_config.rb +29 -8
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +32 -7
- data/redis-client.gemspec +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 796fe04bd550f92d5bf2656a62de54caf1f640b45ab5d5f51fed61d0b61ee883
|
4
|
+
data.tar.gz: 4b34ce4e0ed5a2d4816863970789e7c23ce53fe8a561718b78b79432d09e3fe8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9dfb29603171606e71daaa43474812790988876720dd254a0069fc429ba035ede74b1b19d243967b9adb61c0d48ab4aa2931ef188cd2bb17d820b480967c897
|
7
|
+
data.tar.gz: e6ff3a4543991e139a26f8fa4bdb714aafe83678490f9890ca5fd0ec1a95bdddd0c8fec57dcb4ff88fb28a19d577a556172cb39e4bd4f290d41c6871de43345b
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,34 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# 0.22.2
|
4
|
+
|
5
|
+
- Fix the sentinel client to properly extend timeout for blocking commands.
|
6
|
+
- Fix IPv6 support in `RedisClient::Config#server_url`.
|
7
|
+
|
8
|
+
# 0.22.1
|
9
|
+
|
10
|
+
- Fix `ProtocolError: Unknown sigil type` errors when using SSL connection. See #190.
|
11
|
+
|
12
|
+
# 0.22.0
|
13
|
+
|
14
|
+
- Made various performance optimizations to the Ruby driver. See #184.
|
15
|
+
- Always assume UTF-8 encoding instead of relying on `Encoding.default_external`.
|
16
|
+
- Add `exception` flag in `pipelined` allowing failed commands to be returned in the result array when set to `false`. See #187.
|
17
|
+
|
18
|
+
# 0.21.1
|
19
|
+
|
20
|
+
- Handle unresolved Sentinel master/replica error when displaying server URL in exceptions. See #182.
|
21
|
+
|
22
|
+
# 0.21.0
|
23
|
+
|
24
|
+
- Include redis server URL in most error messages. See #178.
|
25
|
+
- Close Redis Sentinel connection after resolving role. See #176.
|
26
|
+
|
27
|
+
# 0.20.0
|
28
|
+
|
29
|
+
- Accept `unix://` schemes as well as simple paths in the `url:` config parameter. #170.
|
30
|
+
- Make basic usage Ractor compatible.
|
31
|
+
|
3
32
|
# 0.19.1
|
4
33
|
|
5
34
|
- Fixed a bug in `hiredis-client` that could cause a crash if interrupted by `Timeout.timeout` or other `Thread#raise` based mecanism.
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
redis-client (0.
|
4
|
+
redis-client (0.22.2)
|
5
5
|
connection_pool
|
6
6
|
|
7
7
|
GEM
|
@@ -13,34 +13,40 @@ GEM
|
|
13
13
|
connection_pool (2.4.1)
|
14
14
|
hiredis (0.6.3)
|
15
15
|
hiredis (0.6.3-java)
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
json (2.7.1)
|
17
|
+
json (2.7.1-java)
|
18
|
+
minitest (5.23.0)
|
19
|
+
parallel (1.24.0)
|
20
|
+
parser (3.3.0.5)
|
19
21
|
ast (~> 2.4.1)
|
22
|
+
racc
|
23
|
+
racc (1.7.3)
|
24
|
+
racc (1.7.3-java)
|
20
25
|
rainbow (3.1.1)
|
21
|
-
rake (13.1
|
22
|
-
rake-compiler (1.2.
|
26
|
+
rake (13.2.1)
|
27
|
+
rake-compiler (1.2.7)
|
23
28
|
rake
|
24
29
|
redis (4.6.0)
|
25
|
-
regexp_parser (2.
|
26
|
-
rexml (3.2.
|
27
|
-
rubocop (1.
|
30
|
+
regexp_parser (2.9.0)
|
31
|
+
rexml (3.2.6)
|
32
|
+
rubocop (1.50.2)
|
33
|
+
json (~> 2.3)
|
28
34
|
parallel (~> 1.10)
|
29
|
-
parser (>= 3.
|
35
|
+
parser (>= 3.2.0.0)
|
30
36
|
rainbow (>= 2.2.2, < 4.0)
|
31
37
|
regexp_parser (>= 1.8, < 3.0)
|
32
|
-
rexml
|
33
|
-
rubocop-ast (>= 1.
|
38
|
+
rexml (>= 3.2.5, < 4.0)
|
39
|
+
rubocop-ast (>= 1.28.0, < 2.0)
|
34
40
|
ruby-progressbar (~> 1.7)
|
35
|
-
unicode-display_width (>=
|
36
|
-
rubocop-ast (1.
|
37
|
-
parser (>= 3.
|
38
|
-
rubocop-minitest (0.
|
39
|
-
rubocop (>=
|
40
|
-
ruby-progressbar (1.
|
41
|
-
stackprof (0.2.
|
41
|
+
unicode-display_width (>= 2.4.0, < 3.0)
|
42
|
+
rubocop-ast (1.30.0)
|
43
|
+
parser (>= 3.2.1.0)
|
44
|
+
rubocop-minitest (0.30.0)
|
45
|
+
rubocop (>= 1.39, < 2.0)
|
46
|
+
ruby-progressbar (1.13.0)
|
47
|
+
stackprof (0.2.26)
|
42
48
|
toxiproxy (2.0.2)
|
43
|
-
unicode-display_width (2.
|
49
|
+
unicode-display_width (2.5.0)
|
44
50
|
|
45
51
|
PLATFORMS
|
46
52
|
ruby
|
@@ -53,7 +59,7 @@ DEPENDENCIES
|
|
53
59
|
byebug
|
54
60
|
hiredis
|
55
61
|
minitest
|
56
|
-
rake (~> 13.
|
62
|
+
rake (~> 13.2)
|
57
63
|
rake-compiler
|
58
64
|
redis (~> 4.6)
|
59
65
|
redis-client!
|
data/README.md
CHANGED
@@ -62,7 +62,9 @@ redis.call("GET", "mykey")
|
|
62
62
|
|
63
63
|
### Configuration
|
64
64
|
|
65
|
-
- `url`: A Redis connection URL, e.g. `redis://example.com:6379/5
|
65
|
+
- `url`: A Redis connection URL, e.g. `redis://example.com:6379/5` - a `rediss://` scheme enables SSL, and the path is interpreted as a database number.
|
66
|
+
To connect to UNIX domain sockets, the `url` can also just be a path, and the database specified as query parameter: `/run/redis/foo.sock?db=5`, or optionally
|
67
|
+
have a `unix://` scheme: `unix:///run/redis/foo.sock?db=5`
|
66
68
|
Note that all other configurations take precedence, e.g. `RedisClient.config(url: "redis://localhost:3000", port: 6380)` will connect on port `6380`.
|
67
69
|
- `host`: The server hostname or IP address. Defaults to `"localhost"`.
|
68
70
|
- `port`: The server port. Defaults to `6379`.
|
@@ -147,7 +149,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380 },
|
|
147
149
|
redis_config = RedisClient.sentinel(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret')
|
148
150
|
```
|
149
151
|
|
150
|
-
So you have to provide Sentinel credential and Redis
|
152
|
+
So you have to provide Sentinel credential and Redis explicitly even they are the same
|
151
153
|
|
152
154
|
```ruby
|
153
155
|
# Use 'mysecret' to authenticate against the mymaster instance and sentinel
|
@@ -330,6 +332,27 @@ end
|
|
330
332
|
# => ["OK", 1]
|
331
333
|
```
|
332
334
|
|
335
|
+
#### Exception management
|
336
|
+
|
337
|
+
The `exception` flag in the `#pipelined` method of `RedisClient` is a feature that modifies the pipeline execution
|
338
|
+
behavior. When set to `false`, it doesn't raise an exception when a command error occurs. Instead, it allows the
|
339
|
+
pipeline to execute all commands, and any failed command will be available in the returned array. (Defaults to `true`)
|
340
|
+
|
341
|
+
```ruby
|
342
|
+
results = redis.pipelined(exception: false) do |pipeline|
|
343
|
+
pipeline.call("SET", "foo", "bar") # => nil
|
344
|
+
pipeline.call("DOESNOTEXIST", 12) # => nil
|
345
|
+
pipeline.call("INCR", "baz") # => nil
|
346
|
+
end
|
347
|
+
# results => ["OK", #<RedisClient::CommandError: ERR unknown command 'DOESNOTEXIST', with args beginning with: '12'>, 2]
|
348
|
+
|
349
|
+
results.each do |result|
|
350
|
+
if result.is_a?(RedisClient::CommandError)
|
351
|
+
# Do something with the failed result
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
333
356
|
### Transactions
|
334
357
|
|
335
358
|
You can use [`MULTI/EXEC` to run a number of commands in an atomic fashion](https://redis.io/topics/transactions).
|
@@ -501,7 +524,7 @@ recover for a while.
|
|
501
524
|
|
502
525
|
[Circuit breakers are a pattern that does exactly that](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern).
|
503
526
|
|
504
|
-
|
527
|
+
Configuration options:
|
505
528
|
|
506
529
|
- `error_threshold`. The amount of errors to encounter within `error_threshold_timeout` amount of time before opening the circuit, that is to start rejecting requests instantly.
|
507
530
|
- `error_threshold_timeout`. The amount of time in seconds that `error_threshold` errors must occur to open the circuit. Defaults to `error_timeout` seconds if not set.
|
data/Rakefile
CHANGED
@@ -71,12 +71,15 @@ namespace :hiredis do
|
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
74
|
-
benchmark_suites = %w(single pipelined)
|
74
|
+
benchmark_suites = %w(single pipelined drivers)
|
75
75
|
benchmark_modes = %i[ruby yjit hiredis]
|
76
76
|
namespace :benchmark do
|
77
77
|
benchmark_suites.each do |suite|
|
78
78
|
benchmark_modes.each do |mode|
|
79
|
+
next if suite == "drivers" && mode == :hiredis
|
80
|
+
|
79
81
|
name = "#{suite}_#{mode}"
|
82
|
+
desc name
|
80
83
|
task name do
|
81
84
|
output_path = "benchmark/#{name}.md"
|
82
85
|
sh "rm", "-f", output_path
|
data/lib/redis_client/config.rb
CHANGED
@@ -91,6 +91,10 @@ class RedisClient
|
|
91
91
|
@username || DEFAULT_USERNAME
|
92
92
|
end
|
93
93
|
|
94
|
+
def resolved?
|
95
|
+
true
|
96
|
+
end
|
97
|
+
|
94
98
|
def sentinel?
|
95
99
|
false
|
96
100
|
end
|
@@ -124,10 +128,23 @@ class RedisClient
|
|
124
128
|
|
125
129
|
def server_url
|
126
130
|
if path
|
127
|
-
"
|
131
|
+
url = "unix://#{path}"
|
132
|
+
if db != 0
|
133
|
+
url = "#{url}?db=#{db}"
|
134
|
+
end
|
128
135
|
else
|
129
|
-
|
136
|
+
# add brackets to IPv6 address
|
137
|
+
redis_host = if host.count(":") >= 2
|
138
|
+
"[#{host}]"
|
139
|
+
else
|
140
|
+
host
|
141
|
+
end
|
142
|
+
url = "redis#{'s' if ssl?}://#{redis_host}:#{port}"
|
143
|
+
if db != 0
|
144
|
+
url = "#{url}/#{db}"
|
145
|
+
end
|
130
146
|
end
|
147
|
+
url
|
131
148
|
end
|
132
149
|
|
133
150
|
private
|
@@ -151,6 +168,12 @@ class RedisClient
|
|
151
168
|
if @db && @db != 0
|
152
169
|
prelude << ["SELECT", @db.to_s]
|
153
170
|
end
|
171
|
+
|
172
|
+
# Deep freeze all the strings and commands
|
173
|
+
prelude.map! do |commands|
|
174
|
+
commands = commands.map { |str| str.frozen? ? str : str.dup.freeze }
|
175
|
+
commands.freeze
|
176
|
+
end
|
154
177
|
prelude.freeze
|
155
178
|
end
|
156
179
|
end
|
@@ -176,15 +199,20 @@ class RedisClient
|
|
176
199
|
}.compact.merge(kwargs)
|
177
200
|
host ||= url_config.host
|
178
201
|
port ||= url_config.port
|
202
|
+
path ||= url_config.path
|
179
203
|
username ||= url_config.username
|
180
204
|
password ||= url_config.password
|
181
205
|
end
|
182
206
|
|
183
207
|
super(username: username, password: password, **kwargs)
|
184
208
|
|
185
|
-
@
|
186
|
-
|
187
|
-
|
209
|
+
if @path = path
|
210
|
+
@host = nil
|
211
|
+
@port = nil
|
212
|
+
else
|
213
|
+
@host = host || DEFAULT_HOST
|
214
|
+
@port = Integer(port || DEFAULT_PORT)
|
215
|
+
end
|
188
216
|
end
|
189
217
|
end
|
190
218
|
end
|
@@ -28,18 +28,19 @@ class RedisClient
|
|
28
28
|
def call(command, timeout)
|
29
29
|
@pending_reads += 1
|
30
30
|
write(command)
|
31
|
-
result = read(timeout)
|
31
|
+
result = read(connection_timeout(timeout))
|
32
32
|
@pending_reads -= 1
|
33
33
|
if result.is_a?(Error)
|
34
34
|
result._set_command(command)
|
35
|
+
result._set_config(config)
|
35
36
|
raise result
|
36
37
|
else
|
37
38
|
result
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
|
-
def call_pipelined(commands, timeouts)
|
42
|
-
|
42
|
+
def call_pipelined(commands, timeouts, exception: true)
|
43
|
+
first_exception = nil
|
43
44
|
|
44
45
|
size = commands.size
|
45
46
|
results = Array.new(commands.size)
|
@@ -48,20 +49,38 @@ class RedisClient
|
|
48
49
|
|
49
50
|
size.times do |index|
|
50
51
|
timeout = timeouts && timeouts[index]
|
51
|
-
result = read(timeout)
|
52
|
+
result = read(connection_timeout(timeout))
|
52
53
|
@pending_reads -= 1
|
53
|
-
|
54
|
+
|
55
|
+
# A multi/exec command can return an array of results.
|
56
|
+
# An error from a multi/exec command is handled in Multi#_coerce!.
|
57
|
+
if result.is_a?(Array)
|
58
|
+
result.each do |res|
|
59
|
+
res._set_config(config) if res.is_a?(Error)
|
60
|
+
end
|
61
|
+
elsif result.is_a?(Error)
|
54
62
|
result._set_command(commands[index])
|
55
|
-
|
63
|
+
result._set_config(config)
|
64
|
+
first_exception ||= result
|
56
65
|
end
|
66
|
+
|
57
67
|
results[index] = result
|
58
68
|
end
|
59
69
|
|
60
|
-
if exception
|
61
|
-
raise
|
70
|
+
if first_exception && exception
|
71
|
+
raise first_exception
|
62
72
|
else
|
63
73
|
results
|
64
74
|
end
|
65
75
|
end
|
76
|
+
|
77
|
+
def connection_timeout(timeout)
|
78
|
+
return timeout unless timeout && timeout > 0
|
79
|
+
|
80
|
+
# Can't use the command timeout argument as the connection timeout
|
81
|
+
# otherwise it would be very racy. So we add the regular read_timeout on top
|
82
|
+
# to account for the network delay.
|
83
|
+
timeout + config.read_timeout
|
84
|
+
end
|
66
85
|
end
|
67
86
|
end
|
@@ -47,8 +47,8 @@ class RedisClient
|
|
47
47
|
end
|
48
48
|
ruby2_keywords :with if respond_to?(:ruby2_keywords, true)
|
49
49
|
|
50
|
-
def pipelined
|
51
|
-
@client.pipelined { |p| yield @_pipeline_class.new(p) }
|
50
|
+
def pipelined(exception: true)
|
51
|
+
@client.pipelined(exception: exception) { |p| yield @_pipeline_class.new(p) }
|
52
52
|
end
|
53
53
|
|
54
54
|
def multi(**kwargs)
|
@@ -10,14 +10,80 @@ class RedisClient
|
|
10
10
|
|
11
11
|
attr_accessor :read_timeout, :write_timeout
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
13
|
+
if String.method_defined?(:byteindex) # Ruby 3.2+
|
14
|
+
ENCODING = Encoding::UTF_8
|
15
|
+
|
16
|
+
def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
|
17
|
+
@io = io
|
18
|
+
@buffer = +""
|
19
|
+
@offset = 0
|
20
|
+
@chunk_size = chunk_size
|
21
|
+
@read_timeout = read_timeout
|
22
|
+
@write_timeout = write_timeout
|
23
|
+
@blocking_reads = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def gets_chomp
|
27
|
+
fill_buffer(false) if @offset >= @buffer.bytesize
|
28
|
+
until eol_index = @buffer.byteindex(EOL, @offset)
|
29
|
+
fill_buffer(false)
|
30
|
+
end
|
31
|
+
|
32
|
+
line = @buffer.byteslice(@offset, eol_index - @offset)
|
33
|
+
@offset = eol_index + EOL_SIZE
|
34
|
+
line
|
35
|
+
end
|
36
|
+
|
37
|
+
def read_chomp(bytes)
|
38
|
+
ensure_remaining(bytes + EOL_SIZE)
|
39
|
+
str = @buffer.byteslice(@offset, bytes)
|
40
|
+
@offset += bytes + EOL_SIZE
|
41
|
+
str
|
42
|
+
end
|
43
|
+
|
44
|
+
private def ensure_line
|
45
|
+
fill_buffer(false) if @offset >= @buffer.bytesize
|
46
|
+
until @buffer.byteindex(EOL, @offset)
|
47
|
+
fill_buffer(false)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
else
|
51
|
+
ENCODING = Encoding::BINARY
|
52
|
+
|
53
|
+
def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
|
54
|
+
@io = io
|
55
|
+
@buffer = "".b
|
56
|
+
@offset = 0
|
57
|
+
@chunk_size = chunk_size
|
58
|
+
@read_timeout = read_timeout
|
59
|
+
@write_timeout = write_timeout
|
60
|
+
@blocking_reads = false
|
61
|
+
end
|
62
|
+
|
63
|
+
def gets_chomp
|
64
|
+
fill_buffer(false) if @offset >= @buffer.bytesize
|
65
|
+
until eol_index = @buffer.index(EOL, @offset)
|
66
|
+
fill_buffer(false)
|
67
|
+
end
|
68
|
+
|
69
|
+
line = @buffer.byteslice(@offset, eol_index - @offset)
|
70
|
+
@offset = eol_index + EOL_SIZE
|
71
|
+
line
|
72
|
+
end
|
73
|
+
|
74
|
+
def read_chomp(bytes)
|
75
|
+
ensure_remaining(bytes + EOL_SIZE)
|
76
|
+
str = @buffer.byteslice(@offset, bytes)
|
77
|
+
@offset += bytes + EOL_SIZE
|
78
|
+
str.force_encoding(Encoding::UTF_8)
|
79
|
+
end
|
80
|
+
|
81
|
+
private def ensure_line
|
82
|
+
fill_buffer(false) if @offset >= @buffer.bytesize
|
83
|
+
until @buffer.index(EOL, @offset)
|
84
|
+
fill_buffer(false)
|
85
|
+
end
|
86
|
+
end
|
21
87
|
end
|
22
88
|
|
23
89
|
def close
|
@@ -82,28 +148,35 @@ class RedisClient
|
|
82
148
|
end
|
83
149
|
|
84
150
|
def getbyte
|
85
|
-
|
86
|
-
|
151
|
+
unless byte = @buffer.getbyte(@offset)
|
152
|
+
ensure_remaining(1)
|
153
|
+
byte = @buffer.getbyte(@offset)
|
154
|
+
end
|
87
155
|
@offset += 1
|
88
156
|
byte
|
89
157
|
end
|
90
158
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
159
|
+
def gets_integer
|
160
|
+
int = 0
|
161
|
+
offset = @offset
|
162
|
+
while true
|
163
|
+
chr = @buffer.getbyte(offset)
|
96
164
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
165
|
+
if chr
|
166
|
+
if chr == 13 # "\r".ord
|
167
|
+
@offset = offset + 2
|
168
|
+
break
|
169
|
+
else
|
170
|
+
int = (int * 10) + chr - 48
|
171
|
+
end
|
172
|
+
offset += 1
|
173
|
+
else
|
174
|
+
ensure_line
|
175
|
+
return gets_integer
|
176
|
+
end
|
177
|
+
end
|
101
178
|
|
102
|
-
|
103
|
-
ensure_remaining(bytes + EOL_SIZE)
|
104
|
-
str = @buffer.byteslice(@offset, bytes)
|
105
|
-
@offset += bytes + EOL_SIZE
|
106
|
-
str
|
179
|
+
int
|
107
180
|
end
|
108
181
|
|
109
182
|
private
|
@@ -117,7 +190,9 @@ class RedisClient
|
|
117
190
|
|
118
191
|
def fill_buffer(strict, size = @chunk_size)
|
119
192
|
remaining = size
|
120
|
-
|
193
|
+
buffer_size = @buffer.bytesize
|
194
|
+
start = @offset - buffer_size
|
195
|
+
empty_buffer = start >= 0
|
121
196
|
|
122
197
|
loop do
|
123
198
|
bytes = if empty_buffer
|
@@ -125,26 +200,40 @@ class RedisClient
|
|
125
200
|
else
|
126
201
|
@io.read_nonblock([remaining, @chunk_size].max, exception: false)
|
127
202
|
end
|
203
|
+
|
128
204
|
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
205
|
when :wait_readable
|
206
|
+
# Ref: https://github.com/redis-rb/redis-client/issues/190
|
207
|
+
# SSLSocket always clear the provided buffer, even when it didn't
|
208
|
+
# read anything. So we need to reset the offset accordingly.
|
209
|
+
if empty_buffer && @buffer.empty?
|
210
|
+
@offset -= buffer_size
|
211
|
+
end
|
212
|
+
|
139
213
|
unless @io.to_io.wait_readable(@read_timeout)
|
140
214
|
raise ReadTimeoutError, "Waited #{@read_timeout} seconds" unless @blocking_reads
|
141
215
|
end
|
142
216
|
when :wait_writable
|
217
|
+
# Ref: https://github.com/redis-rb/redis-client/issues/190
|
218
|
+
# SSLSocket always clear the provided buffer, even when it didn't
|
219
|
+
# read anything. So we need to reset the offset accordingly.
|
220
|
+
if empty_buffer && @buffer.empty?
|
221
|
+
@offset -= buffer_size
|
222
|
+
end
|
223
|
+
|
143
224
|
@io.to_io.wait_writable(@write_timeout) or raise(WriteTimeoutError, "Waited #{@write_timeout} seconds")
|
144
225
|
when nil
|
145
226
|
raise EOFError
|
146
227
|
else
|
147
|
-
|
228
|
+
if empty_buffer
|
229
|
+
@offset = start
|
230
|
+
empty_buffer = false
|
231
|
+
@buffer.force_encoding(ENCODING) unless @buffer.encoding == ENCODING
|
232
|
+
else
|
233
|
+
@buffer << bytes.force_encoding(ENCODING)
|
234
|
+
end
|
235
|
+
remaining -= bytes.bytesize
|
236
|
+
return if !strict || remaining <= 0
|
148
237
|
end
|
149
238
|
end
|
150
239
|
end
|
@@ -10,12 +10,12 @@ class RedisClient
|
|
10
10
|
|
11
11
|
EOL = "\r\n".b.freeze
|
12
12
|
EOL_SIZE = EOL.bytesize
|
13
|
-
DUMP_TYPES = {
|
13
|
+
DUMP_TYPES = {
|
14
14
|
String => :dump_string,
|
15
15
|
Symbol => :dump_symbol,
|
16
16
|
Integer => :dump_numeric,
|
17
17
|
Float => :dump_numeric,
|
18
|
-
}
|
18
|
+
}.freeze
|
19
19
|
PARSER_TYPES = {
|
20
20
|
'#' => :parse_boolean,
|
21
21
|
'$' => :parse_blob,
|
@@ -57,7 +57,7 @@ class RedisClient
|
|
57
57
|
def dump_any(object, buffer)
|
58
58
|
method = DUMP_TYPES.fetch(object.class) do |unexpected_class|
|
59
59
|
if superclass = DUMP_TYPES.keys.find { |t| t > unexpected_class }
|
60
|
-
DUMP_TYPES[
|
60
|
+
DUMP_TYPES[superclass]
|
61
61
|
else
|
62
62
|
raise TypeError, "Unsupported command argument type: #{unexpected_class}"
|
63
63
|
end
|
@@ -111,15 +111,39 @@ class RedisClient
|
|
111
111
|
|
112
112
|
def parse(io)
|
113
113
|
type = io.getbyte
|
114
|
-
|
114
|
+
if type == 35 # '#'.ord
|
115
|
+
parse_boolean(io)
|
116
|
+
elsif type == 36 # '$'.ord
|
117
|
+
parse_blob(io)
|
118
|
+
elsif type == 43 # '+'.ord
|
119
|
+
parse_string(io)
|
120
|
+
elsif type == 61 # '='.ord
|
121
|
+
parse_verbatim_string(io)
|
122
|
+
elsif type == 45 # '-'.ord
|
123
|
+
parse_error(io)
|
124
|
+
elsif type == 58 # ':'.ord
|
125
|
+
parse_integer(io)
|
126
|
+
elsif type == 40 # '('.ord
|
127
|
+
parse_integer(io)
|
128
|
+
elsif type == 44 # ','.ord
|
129
|
+
parse_double(io)
|
130
|
+
elsif type == 95 # '_'.ord
|
131
|
+
parse_null(io)
|
132
|
+
elsif type == 42 # '*'.ord
|
133
|
+
parse_array(io)
|
134
|
+
elsif type == 37 # '%'.ord
|
135
|
+
parse_map(io)
|
136
|
+
elsif type == 126 # '~'.ord
|
137
|
+
parse_set(io)
|
138
|
+
elsif type == 62 # '>'.ord
|
139
|
+
parse_array(io)
|
140
|
+
else
|
115
141
|
raise UnknownType, "Unknown sigil type: #{type.chr.inspect}"
|
116
142
|
end
|
117
|
-
send(method, io)
|
118
143
|
end
|
119
144
|
|
120
145
|
def parse_string(io)
|
121
146
|
str = io.gets_chomp
|
122
|
-
str.force_encoding(Encoding.default_external)
|
123
147
|
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
|
124
148
|
str.freeze
|
125
149
|
end
|
@@ -140,17 +164,17 @@ class RedisClient
|
|
140
164
|
end
|
141
165
|
|
142
166
|
def parse_array(io)
|
143
|
-
parse_sequence(io,
|
167
|
+
parse_sequence(io, io.gets_integer)
|
144
168
|
end
|
145
169
|
|
146
170
|
def parse_set(io)
|
147
|
-
parse_sequence(io,
|
171
|
+
parse_sequence(io, io.gets_integer)
|
148
172
|
end
|
149
173
|
|
150
174
|
def parse_map(io)
|
151
175
|
hash = {}
|
152
|
-
|
153
|
-
hash[parse(io)] = parse(io)
|
176
|
+
io.gets_integer.times do
|
177
|
+
hash[parse(io).freeze] = parse(io)
|
154
178
|
end
|
155
179
|
hash
|
156
180
|
end
|
@@ -192,11 +216,10 @@ class RedisClient
|
|
192
216
|
end
|
193
217
|
|
194
218
|
def parse_blob(io)
|
195
|
-
bytesize =
|
219
|
+
bytesize = io.gets_integer
|
196
220
|
return if bytesize < 0 # RESP2 nil type
|
197
221
|
|
198
222
|
str = io.read_chomp(bytesize)
|
199
|
-
str.force_encoding(Encoding.default_external)
|
200
223
|
str.force_encoding(Encoding::BINARY) unless str.valid_encoding?
|
201
224
|
str
|
202
225
|
end
|
@@ -40,6 +40,8 @@ class RedisClient
|
|
40
40
|
|
41
41
|
SUPPORTS_RESOLV_TIMEOUT = Socket.method(:tcp).parameters.any? { |p| p.last == :resolv_timeout }
|
42
42
|
|
43
|
+
attr_reader :config
|
44
|
+
|
43
45
|
def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
|
44
46
|
super()
|
45
47
|
@config = config
|
@@ -72,8 +74,8 @@ class RedisClient
|
|
72
74
|
buffer = RESP3.dump(command)
|
73
75
|
begin
|
74
76
|
@io.write(buffer)
|
75
|
-
rescue SystemCallError, IOError => error
|
76
|
-
raise ConnectionError
|
77
|
+
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
|
78
|
+
raise ConnectionError.with_config(error.message, config)
|
77
79
|
end
|
78
80
|
end
|
79
81
|
|
@@ -84,8 +86,8 @@ class RedisClient
|
|
84
86
|
end
|
85
87
|
begin
|
86
88
|
@io.write(buffer)
|
87
|
-
rescue SystemCallError, IOError => error
|
88
|
-
raise ConnectionError
|
89
|
+
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
|
90
|
+
raise ConnectionError.with_config(error.message, config)
|
89
91
|
end
|
90
92
|
end
|
91
93
|
|
@@ -96,9 +98,9 @@ class RedisClient
|
|
96
98
|
@io.with_timeout(timeout) { RESP3.load(@io) }
|
97
99
|
end
|
98
100
|
rescue RedisClient::RESP3::UnknownType => error
|
99
|
-
raise RedisClient::ProtocolError
|
101
|
+
raise RedisClient::ProtocolError.with_config(error.message, config)
|
100
102
|
rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
|
101
|
-
raise ConnectionError
|
103
|
+
raise ConnectionError.with_config(error.message, config)
|
102
104
|
end
|
103
105
|
|
104
106
|
def measure_round_trip_delay
|
@@ -130,9 +132,9 @@ class RedisClient
|
|
130
132
|
loop do
|
131
133
|
case status = socket.connect_nonblock(exception: false)
|
132
134
|
when :wait_readable
|
133
|
-
socket.to_io.wait_readable(@connect_timeout) or raise CannotConnectError
|
135
|
+
socket.to_io.wait_readable(@connect_timeout) or raise CannotConnectError.with_config("", config)
|
134
136
|
when :wait_writable
|
135
|
-
socket.to_io.wait_writable(@connect_timeout) or raise CannotConnectError
|
137
|
+
socket.to_io.wait_writable(@connect_timeout) or raise CannotConnectError.with_config("", config)
|
136
138
|
when socket
|
137
139
|
break
|
138
140
|
else
|
@@ -112,6 +112,12 @@ class RedisClient
|
|
112
112
|
end
|
113
113
|
end
|
114
114
|
|
115
|
+
def resolved?
|
116
|
+
@mutex.synchronize do
|
117
|
+
!!@config
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
115
121
|
private
|
116
122
|
|
117
123
|
def sentinels_to_configs(sentinels)
|
@@ -188,6 +194,10 @@ class RedisClient
|
|
188
194
|
if success
|
189
195
|
@sentinel_configs.unshift(@sentinel_configs.delete(sentinel_config))
|
190
196
|
end
|
197
|
+
# Redis Sentinels may be configured to have a lower maxclients setting than
|
198
|
+
# the Redis nodes. Close the connection to the Sentinel node to avoid using
|
199
|
+
# a connection.
|
200
|
+
sentinel_client.close
|
191
201
|
end
|
192
202
|
end
|
193
203
|
|
@@ -4,26 +4,41 @@ require "uri"
|
|
4
4
|
|
5
5
|
class RedisClient
|
6
6
|
class URLConfig
|
7
|
-
DEFAULT_SCHEMA = "redis"
|
8
|
-
SSL_SCHEMA = "rediss"
|
9
|
-
|
10
7
|
attr_reader :url, :uri
|
11
8
|
|
12
9
|
def initialize(url)
|
13
10
|
@url = url
|
14
11
|
@uri = URI(url)
|
15
|
-
|
16
|
-
|
12
|
+
@unix = false
|
13
|
+
@ssl = false
|
14
|
+
case uri.scheme
|
15
|
+
when "redis"
|
16
|
+
# expected
|
17
|
+
when "rediss"
|
18
|
+
@ssl = true
|
19
|
+
when "unix", nil
|
20
|
+
@unix = true
|
21
|
+
else
|
22
|
+
raise ArgumentError, "Unknown URL scheme: #{url.inspect}"
|
17
23
|
end
|
18
24
|
end
|
19
25
|
|
20
26
|
def ssl?
|
21
|
-
@
|
27
|
+
@ssl
|
22
28
|
end
|
23
29
|
|
24
30
|
def db
|
25
|
-
|
26
|
-
|
31
|
+
unless @unix
|
32
|
+
db_path = uri.path&.delete_prefix("/")
|
33
|
+
return Integer(db_path) if db_path && !db_path.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
unless uri.query.nil? || uri.query.empty?
|
37
|
+
_, db_query = URI.decode_www_form(uri.query).find do |key, _|
|
38
|
+
key == "db"
|
39
|
+
end
|
40
|
+
return Integer(db_query) if db_query && !db_query.empty?
|
41
|
+
end
|
27
42
|
end
|
28
43
|
|
29
44
|
def username
|
@@ -44,6 +59,12 @@ class RedisClient
|
|
44
59
|
uri.host.sub(/\A\[(.*)\]\z/, '\1')
|
45
60
|
end
|
46
61
|
|
62
|
+
def path
|
63
|
+
if @unix
|
64
|
+
File.join(*[uri.host, uri.path].compact)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
47
68
|
def port
|
48
69
|
return unless uri.port
|
49
70
|
|
data/lib/redis_client/version.rb
CHANGED
data/lib/redis_client.rb
CHANGED
@@ -77,7 +77,29 @@ class RedisClient
|
|
77
77
|
end
|
78
78
|
end
|
79
79
|
|
80
|
-
|
80
|
+
module HasConfig
|
81
|
+
attr_reader :config
|
82
|
+
|
83
|
+
def _set_config(config)
|
84
|
+
@config = config
|
85
|
+
end
|
86
|
+
|
87
|
+
def message
|
88
|
+
return super unless config&.resolved?
|
89
|
+
|
90
|
+
"#{super} (#{config.server_url})"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Error < StandardError
|
95
|
+
include HasConfig
|
96
|
+
|
97
|
+
def self.with_config(message, config = nil)
|
98
|
+
new(message).tap do |error|
|
99
|
+
error._set_config(config)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
81
103
|
|
82
104
|
ProtocolError = Class.new(Error)
|
83
105
|
UnsupportedServer = Class.new(Error)
|
@@ -114,7 +136,7 @@ class RedisClient
|
|
114
136
|
end
|
115
137
|
code ||= error_message.split(' ', 2).first
|
116
138
|
klass = ERRORS.fetch(code, self)
|
117
|
-
klass.new(error_message)
|
139
|
+
klass.new(error_message.strip)
|
118
140
|
end
|
119
141
|
end
|
120
142
|
end
|
@@ -399,7 +421,7 @@ class RedisClient
|
|
399
421
|
ensure_connected(retryable: false, &block)
|
400
422
|
end
|
401
423
|
|
402
|
-
def pipelined
|
424
|
+
def pipelined(exception: true)
|
403
425
|
pipeline = Pipeline.new(@command_builder)
|
404
426
|
yield pipeline
|
405
427
|
|
@@ -409,7 +431,7 @@ class RedisClient
|
|
409
431
|
results = ensure_connected(retryable: pipeline._retryable?) do |connection|
|
410
432
|
commands = pipeline._commands
|
411
433
|
@middlewares.call_pipelined(commands, config) do
|
412
|
-
connection.call_pipelined(commands, pipeline._timeouts)
|
434
|
+
connection.call_pipelined(commands, pipeline._timeouts, exception: exception)
|
413
435
|
end
|
414
436
|
end
|
415
437
|
|
@@ -750,10 +772,13 @@ class RedisClient
|
|
750
772
|
end
|
751
773
|
end
|
752
774
|
end
|
753
|
-
rescue FailoverError, CannotConnectError
|
754
|
-
|
775
|
+
rescue FailoverError, CannotConnectError => error
|
776
|
+
error._set_config(config)
|
777
|
+
raise error
|
755
778
|
rescue ConnectionError => error
|
756
|
-
|
779
|
+
connect_error = CannotConnectError.with_config(error.message, config)
|
780
|
+
connect_error.set_backtrace(error.backtrace)
|
781
|
+
raise connect_error
|
757
782
|
rescue CommandError => error
|
758
783
|
if error.message.match?(/ERR unknown command ['`]HELLO['`]/)
|
759
784
|
raise UnsupportedServer,
|
data/redis-client.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.summary = "Simple low-level client for Redis 6+"
|
12
12
|
spec.homepage = "https://github.com/redis-rb/redis-client"
|
13
13
|
spec.license = "MIT"
|
14
|
-
spec.required_ruby_version = ">= 2.
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
15
15
|
|
16
16
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
17
17
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.22.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-05-22 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: connection_pool
|
@@ -70,14 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
70
70
|
requirements:
|
71
71
|
- - ">="
|
72
72
|
- !ruby/object:Gem::Version
|
73
|
-
version: 2.
|
73
|
+
version: 2.6.0
|
74
74
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
75
|
requirements:
|
76
76
|
- - ">="
|
77
77
|
- !ruby/object:Gem::Version
|
78
78
|
version: '0'
|
79
79
|
requirements: []
|
80
|
-
rubygems_version: 3.
|
80
|
+
rubygems_version: 3.5.9
|
81
81
|
signing_key:
|
82
82
|
specification_version: 4
|
83
83
|
summary: Simple low-level client for Redis 6+
|