redis-client 0.21.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 +6 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -3
- data/README.md +21 -0
- data/Rakefile +4 -1
- data/lib/redis_client/connection_mixin.rb +5 -5
- 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 +32 -9
- data/lib/redis_client/version.rb +1 -1
- data/lib/redis_client.rb +2 -2
- metadata +2 -2
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,11 @@
|
|
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
|
+
|
3
9
|
# 0.21.1
|
4
10
|
|
5
11
|
- Handle unresolved Sentinel master/replica error when displaying server URL in exceptions. See #182.
|
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
|
@@ -23,7 +23,7 @@ GEM
|
|
23
23
|
racc (1.7.3)
|
24
24
|
racc (1.7.3-java)
|
25
25
|
rainbow (3.1.1)
|
26
|
-
rake (13.1
|
26
|
+
rake (13.2.1)
|
27
27
|
rake-compiler (1.2.7)
|
28
28
|
rake
|
29
29
|
redis (4.6.0)
|
@@ -59,7 +59,7 @@ DEPENDENCIES
|
|
59
59
|
byebug
|
60
60
|
hiredis
|
61
61
|
minitest
|
62
|
-
rake (~> 13.
|
62
|
+
rake (~> 13.2)
|
63
63
|
rake-compiler
|
64
64
|
redis (~> 4.6)
|
65
65
|
redis-client!
|
data/README.md
CHANGED
@@ -332,6 +332,27 @@ end
|
|
332
332
|
# => ["OK", 1]
|
333
333
|
```
|
334
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
|
+
|
335
356
|
### Transactions
|
336
357
|
|
337
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
|
@@ -39,8 +39,8 @@ class RedisClient
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
-
def call_pipelined(commands, timeouts)
|
43
|
-
|
42
|
+
def call_pipelined(commands, timeouts, exception: true)
|
43
|
+
first_exception = nil
|
44
44
|
|
45
45
|
size = commands.size
|
46
46
|
results = Array.new(commands.size)
|
@@ -61,14 +61,14 @@ class RedisClient
|
|
61
61
|
elsif result.is_a?(Error)
|
62
62
|
result._set_command(commands[index])
|
63
63
|
result._set_config(config)
|
64
|
-
|
64
|
+
first_exception ||= result
|
65
65
|
end
|
66
66
|
|
67
67
|
results[index] = result
|
68
68
|
end
|
69
69
|
|
70
|
-
if exception
|
71
|
-
raise
|
70
|
+
if first_exception && exception
|
71
|
+
raise first_exception
|
72
72
|
else
|
73
73
|
results
|
74
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
|
@@ -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
|
data/lib/redis_client/version.rb
CHANGED
data/lib/redis_client.rb
CHANGED
@@ -421,7 +421,7 @@ class RedisClient
|
|
421
421
|
ensure_connected(retryable: false, &block)
|
422
422
|
end
|
423
423
|
|
424
|
-
def pipelined
|
424
|
+
def pipelined(exception: true)
|
425
425
|
pipeline = Pipeline.new(@command_builder)
|
426
426
|
yield pipeline
|
427
427
|
|
@@ -431,7 +431,7 @@ class RedisClient
|
|
431
431
|
results = ensure_connected(retryable: pipeline._retryable?) do |connection|
|
432
432
|
commands = pipeline._commands
|
433
433
|
@middlewares.call_pipelined(commands, config) do
|
434
|
-
connection.call_pipelined(commands, pipeline._timeouts)
|
434
|
+
connection.call_pipelined(commands, pipeline._timeouts, exception: exception)
|
435
435
|
end
|
436
436
|
end
|
437
437
|
|
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: 2024-
|
11
|
+
date: 2024-04-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: connection_pool
|