redis-client 0.19.1 → 0.22.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/CHANGELOG.md +20 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +27 -21
- data/README.md +24 -1
- data/Rakefile +4 -1
- data/lib/redis_client/config.rb +27 -5
- data/lib/redis_client/connection_mixin.rb +16 -6
- data/lib/redis_client/decorator.rb +2 -2
- data/lib/redis_client/ruby_connection/buffered_io.rb +108 -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: 6471d100e48137355f30f150526beba636372c0a8ab89b1d05c9870f156bd01c
|
|
4
|
+
data.tar.gz: 6c5a17e18eabfdb54e500a1f8deec09dbe1aba9234214e34f0ade2fea170b669
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b2253e3c62b2cdce341b210ceeebb25f37484161a356c3982d747884870acb040d071471ca50d0abac605a9e759a89d678fbaf19fd1a21cf0b2bfe3c5fd2472
|
|
7
|
+
data.tar.gz: 8b44296f79fa5591b5776ca6a236b1d2cbce76f9f56a008ab564d2ae8381b1da66385c74f0fe7c00d244bd418f1f7e564b8c198ade7c43588f45e25caa94581e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Unreleased
|
|
2
2
|
|
|
3
|
+
# 0.22.0
|
|
4
|
+
|
|
5
|
+
- Made various performance optimizations to the Ruby driver. See #184.
|
|
6
|
+
- Always assume UTF-8 encoding instead of relying on `Encoding.default_external`.
|
|
7
|
+
- Add `exception` flag in `pipelined` allowing failed commands to be returned in the result array when set to `false`. See #187.
|
|
8
|
+
|
|
9
|
+
# 0.21.1
|
|
10
|
+
|
|
11
|
+
- Handle unresolved Sentinel master/replica error when displaying server URL in exceptions. See #182.
|
|
12
|
+
|
|
13
|
+
# 0.21.0
|
|
14
|
+
|
|
15
|
+
- Include redis server URL in most error messages. See #178.
|
|
16
|
+
- Close Redis Sentinel connection after resolving role. See #176.
|
|
17
|
+
|
|
18
|
+
# 0.20.0
|
|
19
|
+
|
|
20
|
+
- Accept `unix://` schemes as well as simple paths in the `url:` config parameter. #170.
|
|
21
|
+
- Make basic usage Ractor compatible.
|
|
22
|
+
|
|
3
23
|
# 0.19.1
|
|
4
24
|
|
|
5
25
|
- 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.0)
|
|
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.22.3)
|
|
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`.
|
|
@@ -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).
|
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,17 @@ 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
|
-
"redis#{'s' if ssl?}://#{host}:#{port}
|
|
136
|
+
url = "redis#{'s' if ssl?}://#{host}:#{port}"
|
|
137
|
+
if db != 0
|
|
138
|
+
url = "#{url}/#{db}"
|
|
139
|
+
end
|
|
130
140
|
end
|
|
141
|
+
url
|
|
131
142
|
end
|
|
132
143
|
|
|
133
144
|
private
|
|
@@ -151,6 +162,12 @@ class RedisClient
|
|
|
151
162
|
if @db && @db != 0
|
|
152
163
|
prelude << ["SELECT", @db.to_s]
|
|
153
164
|
end
|
|
165
|
+
|
|
166
|
+
# Deep freeze all the strings and commands
|
|
167
|
+
prelude.map! do |commands|
|
|
168
|
+
commands = commands.map { |str| str.frozen? ? str : str.dup.freeze }
|
|
169
|
+
commands.freeze
|
|
170
|
+
end
|
|
154
171
|
prelude.freeze
|
|
155
172
|
end
|
|
156
173
|
end
|
|
@@ -176,15 +193,20 @@ class RedisClient
|
|
|
176
193
|
}.compact.merge(kwargs)
|
|
177
194
|
host ||= url_config.host
|
|
178
195
|
port ||= url_config.port
|
|
196
|
+
path ||= url_config.path
|
|
179
197
|
username ||= url_config.username
|
|
180
198
|
password ||= url_config.password
|
|
181
199
|
end
|
|
182
200
|
|
|
183
201
|
super(username: username, password: password, **kwargs)
|
|
184
202
|
|
|
185
|
-
@
|
|
186
|
-
|
|
187
|
-
|
|
203
|
+
if @path = path
|
|
204
|
+
@host = nil
|
|
205
|
+
@port = nil
|
|
206
|
+
else
|
|
207
|
+
@host = host || DEFAULT_HOST
|
|
208
|
+
@port = Integer(port || DEFAULT_PORT)
|
|
209
|
+
end
|
|
188
210
|
end
|
|
189
211
|
end
|
|
190
212
|
end
|
|
@@ -32,14 +32,15 @@ class RedisClient
|
|
|
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)
|
|
@@ -50,15 +51,24 @@ class RedisClient
|
|
|
50
51
|
timeout = timeouts && timeouts[index]
|
|
51
52
|
result = read(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
|
|
@@ -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,8 @@ class RedisClient
|
|
|
117
190
|
|
|
118
191
|
def fill_buffer(strict, size = @chunk_size)
|
|
119
192
|
remaining = size
|
|
120
|
-
|
|
193
|
+
start = @offset - @buffer.bytesize
|
|
194
|
+
empty_buffer = start >= 0
|
|
121
195
|
|
|
122
196
|
loop do
|
|
123
197
|
bytes = if empty_buffer
|
|
@@ -126,15 +200,6 @@ class RedisClient
|
|
|
126
200
|
@io.read_nonblock([remaining, @chunk_size].max, exception: false)
|
|
127
201
|
end
|
|
128
202
|
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
203
|
when :wait_readable
|
|
139
204
|
unless @io.to_io.wait_readable(@read_timeout)
|
|
140
205
|
raise ReadTimeoutError, "Waited #{@read_timeout} seconds" unless @blocking_reads
|
|
@@ -144,7 +209,15 @@ class RedisClient
|
|
|
144
209
|
when nil
|
|
145
210
|
raise EOFError
|
|
146
211
|
else
|
|
147
|
-
|
|
212
|
+
if empty_buffer
|
|
213
|
+
@offset = start
|
|
214
|
+
empty_buffer = false
|
|
215
|
+
@buffer.force_encoding(ENCODING) unless @buffer.encoding == ENCODING
|
|
216
|
+
else
|
|
217
|
+
@buffer << bytes.force_encoding(ENCODING)
|
|
218
|
+
end
|
|
219
|
+
remaining -= bytes.bytesize
|
|
220
|
+
return if !strict || remaining <= 0
|
|
148
221
|
end
|
|
149
222
|
end
|
|
150
223
|
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.0
|
|
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-04-12 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.5
|
|
81
81
|
signing_key:
|
|
82
82
|
specification_version: 4
|
|
83
83
|
summary: Simple low-level client for Redis 6+
|