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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4742ae57d2d4b4510921d37359540257a2e70ee0f9038276f40f95506ae2ffa0
4
- data.tar.gz: cb3f9702abcd04ae46cfb5d59350884fb9bcff6f6d4990f1e6a0ae66fb6a1f4a
3
+ metadata.gz: 6471d100e48137355f30f150526beba636372c0a8ab89b1d05c9870f156bd01c
4
+ data.tar.gz: 6c5a17e18eabfdb54e500a1f8deec09dbe1aba9234214e34f0ade2fea170b669
5
5
  SHA512:
6
- metadata.gz: 14212b26415dd02f6b23f2f729b9690bcf9bd06c156b60ffb5cb4b9061172a3c08f670242d2e6d391770c4abb9cfdd3bd79dfd8d039ae54a9e1537ea39dfd05f
7
- data.tar.gz: d5b1276dffe3c3fb6220fb06b9e22fd1440bd7d83e19e37031019f98b69ab3ae5c13cbef8bcd036e1929008f764d9b5117451c9fcb07964b28d11cad14ac449e
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
@@ -6,7 +6,7 @@ source "https://rubygems.org"
6
6
  gemspec name: "redis-client"
7
7
 
8
8
  gem "minitest"
9
- gem "rake", "~> 13.1"
9
+ gem "rake", "~> 13.2"
10
10
  gem "rake-compiler"
11
11
  gem "rubocop"
12
12
  gem "rubocop-minitest"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- redis-client (0.21.1)
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.0)
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.1)
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
- exception = nil
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
- exception ||= result
64
+ first_exception ||= result
65
65
  end
66
66
 
67
67
  results[index] = result
68
68
  end
69
69
 
70
- if exception
71
- raise exception
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
- def initialize(io, read_timeout:, write_timeout:, chunk_size: 4096)
14
- @io = io
15
- @buffer = "".b
16
- @offset = 0
17
- @chunk_size = chunk_size
18
- @read_timeout = read_timeout
19
- @write_timeout = write_timeout
20
- @blocking_reads = false
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
- ensure_remaining(1)
86
- byte = @buffer.getbyte(@offset)
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 gets_chomp
92
- fill_buffer(false) if @offset >= @buffer.bytesize
93
- until eol_index = @buffer.index(EOL, @offset)
94
- fill_buffer(false)
95
- end
159
+ def gets_integer
160
+ int = 0
161
+ offset = @offset
162
+ while true
163
+ chr = @buffer.getbyte(offset)
96
164
 
97
- line = @buffer.byteslice(@offset, eol_index - @offset)
98
- @offset = eol_index + EOL_SIZE
99
- line
100
- end
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
- def read_chomp(bytes)
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
- empty_buffer = @offset >= @buffer.bytesize
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
- raise "Unexpected `read_nonblock` return: #{bytes.inspect}"
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
- method = PARSER_TYPES.fetch(type) do
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, parse_integer(io))
167
+ parse_sequence(io, io.gets_integer)
144
168
  end
145
169
 
146
170
  def parse_set(io)
147
- parse_sequence(io, parse_integer(io))
171
+ parse_sequence(io, io.gets_integer)
148
172
  end
149
173
 
150
174
  def parse_map(io)
151
175
  hash = {}
152
- parse_integer(io).times do
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 = parse_integer(io)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.21.1"
4
+ VERSION = "0.22.0"
5
5
  end
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.21.1
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-03-22 00:00:00.000000000 Z
11
+ date: 2024-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: connection_pool