redis-client 0.19.1 → 0.22.2
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/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+
|