redis-client 0.24.0 → 0.26.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c3cb528ecb15130b80ce97d00a5e0e45c31edfc6147068516e1a52295a94045
4
- data.tar.gz: b4517fbd99d0f3997e510b6d0d56c345c1fceb1ad44fcac59cceb28faf4326b5
3
+ metadata.gz: f75c9f8b8e4542079642b30d284bd0fbda23d27249e7846bc65c0b002ce61732
4
+ data.tar.gz: cdc1d818e27551467b14e53692b5e61da762d1f21de78be00d6650830fdb9d13
5
5
  SHA512:
6
- metadata.gz: dbc7fcacc084fcd9d765337ad48a2a8ee413eecfec48f1a166fbd58510299cf92b3e058ce08620055deadeca86464c2c25fe6cddcccab532ab3d2cd741f9cef0
7
- data.tar.gz: '09164c4ea9f7021d58df97690f78e7940821a44eb7e12cc4c25324bd17e0e784f447443accedbe2299e6d8c0a0a6f15391491465c4c32baaa56286501b77b693'
6
+ metadata.gz: 15d23f1645fc7bc86c3f58ff0bfebcce62a3aec4783fd5434123277c0445a8d7553fea6f7fc89a36c23c433d7fcd5716b88a79d5131d3a3452d58707e61b965d
7
+ data.tar.gz: 7447f4f33e056072445d5d0dad650f3e648af69f1a0690505cb6913d525c32b29f5954349e0ca4852b3bd8d47434ada0e8e1ffde09f8966a664cff08acc5a443
data/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.26.2
4
+
5
+ - Fix compatibility with `connection_pool` version 3+.
6
+
7
+ # 0.26.1
8
+
9
+ - Fix a few corner cases where `RedisClient::Error#final?` was innacurate.
10
+ - hiredis-client: Properly reconnect to the new leader after a sentinel failover.
11
+
12
+ # 0.26.0
13
+
14
+ - Add `RedisClient::Error#final?` and `#retriable?` to allow middleware to filter out non-final errors.
15
+ - Fix precedence of `db: nil` initialization parameter.
16
+
17
+ ```ruby
18
+ Redis.new(url: "redis://localhost:6379/3", db: nil).db
19
+ ```
20
+
21
+ Before: `0`
22
+ After: `3`
23
+
24
+ # 0.25.3
25
+
26
+ - Fix `hiredis-client` compilation with `clang 21`.
27
+
28
+ # 0.25.2
29
+
30
+ - Fix circuit breakers to respect the `error_threshold_timeout` config is provided.
31
+ - Fix circuit breakers to clear errors when closing back.
32
+
33
+ # 0.25.1
34
+
35
+ - Fix Ruby driver TCP keep alive TTL. It was intended to be 120 seconds but was mistakenly set to 15 seconds.
36
+
37
+ # 0.25.0
38
+
39
+ - Fix `hiredis-client` compilation with GCC 15.
40
+ - Fix `hiredis-client` from a work directory with spaces.
41
+ - Add `CommandError#code`.
42
+ - Add `RedisClient::NoScriptError` for `EVALSHA`.
43
+
3
44
  # 0.24.0
4
45
 
5
46
  - Allow `sentinel_password` to be provided as a `Proc`.
@@ -185,7 +226,7 @@
185
226
  - Added `_v` versions of `call` methods to make it easier to pass commands as arrays without splating.
186
227
  - Fix calling `blocking_call` with a block in a pipeline.
187
228
  - `blocking_call` now raise `ReadTimeoutError` if the command didn't complete in time.
188
- - Fix `blocking_call` to not respect `retry_attempts` on timeout.
229
+ - Fix `blocking_call` to not respect `reconnect_attempts` on timeout.
189
230
  - Stop parsing RESP3 sets as Ruby Set instances.
190
231
  - Fix `SystemStackError` when parsing very large hashes. Fix: #30
191
232
  - `hiredis` now more properly release the GVL when doing IOs.
data/README.md CHANGED
@@ -459,6 +459,29 @@ RedisClient.register(MyGlobalRedisInstrumentation)
459
459
 
460
460
  redis_config = RedisClient.config(custom: { tags: { "environment": Rails.env }})
461
461
  ```
462
+
463
+ ### Instrumenting Errors
464
+
465
+ It is important to note that when `reconnect_attempts` is enabled, all network errors are reported to the middlewares,
466
+ even the ones that will be retried.
467
+
468
+ In many cases you may want to ignore retriable errors, or report them differently:
469
+
470
+ ```ruby
471
+ module MyGlobalRedisInstrumentation
472
+ def call(command, redis_config)
473
+ super
474
+ rescue RedisClient::Error => error
475
+ if error.final?
476
+ # Error won't be retried.
477
+ else
478
+ # Error will be retried.
479
+ end
480
+ raise
481
+ end
482
+ end
483
+ ```
484
+
462
485
  ### Timeouts
463
486
 
464
487
  The client allows you to configure connect, read, and write timeouts.
@@ -79,7 +79,7 @@ class RedisClient
79
79
 
80
80
  def record_error
81
81
  now = RedisClient.now
82
- expiry = now - @error_timeout
82
+ expiry = now - @error_threshold_timeout
83
83
  @lock.synchronize do
84
84
  if @state == :closed
85
85
  @errors.reject! { |t| t < expiry }
@@ -100,6 +100,7 @@ class RedisClient
100
100
 
101
101
  @successes += 1
102
102
  if @successes >= @success_threshold
103
+ @errors.clear
103
104
  @state = :closed
104
105
  end
105
106
  end
@@ -140,6 +140,10 @@ class RedisClient
140
140
  @client_implementation.new(self, **kwargs)
141
141
  end
142
142
 
143
+ def retriable?(attempt)
144
+ @reconnect_attempts && @reconnect_attempts[attempt]
145
+ end
146
+
143
147
  def retry_connecting?(attempt, _error)
144
148
  if @reconnect_attempts
145
149
  if (pause = @reconnect_attempts[attempt])
@@ -182,7 +186,7 @@ class RedisClient
182
186
 
183
187
  include Common
184
188
 
185
- attr_reader :host, :port, :path
189
+ attr_reader :host, :port, :path, :server_key
186
190
 
187
191
  def initialize(
188
192
  url: nil,
@@ -191,14 +195,15 @@ class RedisClient
191
195
  path: nil,
192
196
  username: nil,
193
197
  password: nil,
198
+ db: nil,
194
199
  **kwargs
195
200
  )
196
201
  if url
197
202
  url_config = URLConfig.new(url)
198
203
  kwargs = {
199
204
  ssl: url_config.ssl?,
200
- db: url_config.db,
201
205
  }.compact.merge(kwargs)
206
+ db ||= url_config.db
202
207
  host ||= url_config.host
203
208
  port ||= url_config.port
204
209
  path ||= url_config.path
@@ -206,7 +211,7 @@ class RedisClient
206
211
  password ||= url_config.password
207
212
  end
208
213
 
209
- super(username: username, password: password, **kwargs)
214
+ super(username: username, password: password, db: db, **kwargs)
210
215
 
211
216
  if @path = path
212
217
  @host = nil
@@ -215,6 +220,8 @@ class RedisClient
215
220
  @host = host || DEFAULT_HOST
216
221
  @port = Integer(port || DEFAULT_PORT)
217
222
  end
223
+
224
+ @server_key = [@path, @host, @port].freeze
218
225
  end
219
226
  end
220
227
  end
@@ -2,8 +2,14 @@
2
2
 
3
3
  class RedisClient
4
4
  module ConnectionMixin
5
- def initialize
5
+ attr_accessor :retry_attempt
6
+ attr_reader :config
7
+
8
+ def initialize(config)
6
9
  @pending_reads = 0
10
+ @retry_attempt = nil
11
+ @config = config
12
+ @server_key = nil
7
13
  end
8
14
 
9
15
  def reconnect
@@ -17,7 +23,7 @@ class RedisClient
17
23
  end
18
24
 
19
25
  def revalidate
20
- if @pending_reads > 0
26
+ if @pending_reads > 0 || @server_key != @config.server_key
21
27
  close
22
28
  false
23
29
  else
@@ -33,6 +39,7 @@ class RedisClient
33
39
  if result.is_a?(Error)
34
40
  result._set_command(command)
35
41
  result._set_config(config)
42
+ result._set_retry_attempt(@retry_attempt)
36
43
  raise result
37
44
  else
38
45
  result
@@ -61,6 +68,7 @@ class RedisClient
61
68
  elsif result.is_a?(Error)
62
69
  result._set_command(commands[index])
63
70
  result._set_config(config)
71
+ result._set_retry_attempt(@retry_attempt)
64
72
  first_exception ||= result
65
73
  end
66
74
 
@@ -82,5 +90,17 @@ class RedisClient
82
90
  # to account for the network delay.
83
91
  timeout + config.read_timeout
84
92
  end
93
+
94
+ def protocol_error(message)
95
+ error = ProtocolError.with_config(message, config)
96
+ error._set_retry_attempt(@retry_attempt)
97
+ error
98
+ end
99
+
100
+ def connection_error(message)
101
+ error = ConnectionError.with_config(message, config)
102
+ error._set_retry_attempt(@retry_attempt)
103
+ error
104
+ end
85
105
  end
86
106
  end
@@ -23,7 +23,7 @@ class RedisClient
23
23
  end
24
24
 
25
25
  def with(options = EMPTY_HASH)
26
- pool.with(options) do |client|
26
+ pool.with(**options) do |client|
27
27
  client.connect_timeout = connect_timeout
28
28
  client.read_timeout = read_timeout
29
29
  client.write_timeout = write_timeout
@@ -40,11 +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
-
45
43
  def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
46
- super()
47
- @config = config
44
+ super(config)
48
45
  @connect_timeout = connect_timeout
49
46
  @read_timeout = read_timeout
50
47
  @write_timeout = write_timeout
@@ -75,7 +72,11 @@ class RedisClient
75
72
  begin
76
73
  @io.write(buffer)
77
74
  rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
78
- raise ConnectionError.with_config(error.message, config)
75
+ raise connection_error(error.message)
76
+ rescue Error => error
77
+ error._set_config(config)
78
+ error._set_retry_attempt(@retry_attempt)
79
+ raise error
79
80
  end
80
81
  end
81
82
 
@@ -87,7 +88,7 @@ class RedisClient
87
88
  begin
88
89
  @io.write(buffer)
89
90
  rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
90
- raise ConnectionError.with_config(error.message, config)
91
+ raise connection_error(error.message)
91
92
  end
92
93
  end
93
94
 
@@ -97,10 +98,10 @@ class RedisClient
97
98
  else
98
99
  @io.with_timeout(timeout) { RESP3.load(@io) }
99
100
  end
100
- rescue RedisClient::RESP3::UnknownType => error
101
- raise RedisClient::ProtocolError.with_config(error.message, config)
101
+ rescue RedisClient::RESP3::Error => error
102
+ raise protocol_error(error.message)
102
103
  rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
103
- raise ConnectionError.with_config(error.message, config)
104
+ raise connection_error(error.message)
104
105
  end
105
106
 
106
107
  def measure_round_trip_delay
@@ -112,6 +113,7 @@ class RedisClient
112
113
  private
113
114
 
114
115
  def connect
116
+ @server_key = @config.server_key
115
117
  socket = if @config.path
116
118
  UNIXSocket.new(@config.path)
117
119
  else
@@ -169,7 +171,7 @@ class RedisClient
169
171
  if %i[SOL_TCP SOL_SOCKET TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c } # Linux
170
172
  def enable_socket_keep_alive(socket)
171
173
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
172
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, KEEP_ALIVE_INTERVAL)
174
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, KEEP_ALIVE_TTL)
173
175
  socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, KEEP_ALIVE_INTERVAL)
174
176
  socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, KEEP_ALIVE_PROBES)
175
177
  end
@@ -82,6 +82,10 @@ class RedisClient
82
82
  end
83
83
  end
84
84
 
85
+ def server_key
86
+ config.server_key
87
+ end
88
+
85
89
  def host
86
90
  config.host
87
91
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.24.0"
4
+ VERSION = "0.26.2"
5
5
  end
data/lib/redis_client.rb CHANGED
@@ -99,13 +99,49 @@ class RedisClient
99
99
  end
100
100
  end
101
101
 
102
+ module Retriable
103
+ def _set_retry_attempt(retry_attempt)
104
+ @retry_attempt = retry_attempt
105
+ end
106
+
107
+ def retry_attempt
108
+ @retry_attempt || 0
109
+ end
110
+
111
+ def retriable?
112
+ !!@retry_attempt
113
+ end
114
+
115
+ def final?
116
+ !@retry_attempt
117
+ end
118
+ end
119
+
120
+ module Final
121
+ def _set_retry_attempt(_retry_attempt)
122
+ end
123
+
124
+ def retry_attempt
125
+ 0
126
+ end
127
+
128
+ def retriable?
129
+ false
130
+ end
131
+
132
+ def final?
133
+ true
134
+ end
135
+ end
136
+
102
137
  class Error < StandardError
103
138
  include HasConfig
139
+ include Retriable
104
140
 
105
141
  def self.with_config(message, config = nil)
106
- new(message).tap do |error|
107
- error._set_config(config)
108
- end
142
+ error = new(message)
143
+ error._set_config(config)
144
+ error
109
145
  end
110
146
  end
111
147
 
@@ -130,8 +166,19 @@ class RedisClient
130
166
  end
131
167
  end
132
168
 
169
+ module HasCode
170
+ attr_reader :code
171
+
172
+ def initialize(message = nil, code = nil)
173
+ super(message)
174
+ @code = code
175
+ end
176
+ end
177
+
133
178
  class CommandError < Error
134
179
  include HasCommand
180
+ include HasCode
181
+ include Final
135
182
 
136
183
  class << self
137
184
  def parse(error_message)
@@ -144,7 +191,7 @@ class RedisClient
144
191
  end
145
192
  code ||= error_message.split(' ', 2).first
146
193
  klass = ERRORS.fetch(code, self)
147
- klass.new(error_message.strip)
194
+ klass.new(error_message.strip, code.freeze)
148
195
  end
149
196
  end
150
197
  end
@@ -153,12 +200,15 @@ class RedisClient
153
200
  PermissionError = Class.new(CommandError)
154
201
  WrongTypeError = Class.new(CommandError)
155
202
  OutOfMemoryError = Class.new(CommandError)
203
+ NoScriptError = Class.new(CommandError)
156
204
 
157
205
  ReadOnlyError = Class.new(ConnectionError)
158
206
  ReadOnlyError.include(HasCommand)
207
+ ReadOnlyError.include(HasCode)
159
208
 
160
209
  MasterDownError = Class.new(ConnectionError)
161
210
  MasterDownError.include(HasCommand)
211
+ MasterDownError.include(HasCode)
162
212
 
163
213
  CommandError::ERRORS = {
164
214
  "WRONGPASS" => AuthenticationError,
@@ -167,6 +217,7 @@ class RedisClient
167
217
  "MASTERDOWN" => MasterDownError,
168
218
  "WRONGTYPE" => WrongTypeError,
169
219
  "OOM" => OutOfMemoryError,
220
+ "NOSCRIPT" => NoScriptError,
170
221
  }.freeze
171
222
 
172
223
  class << self
@@ -198,6 +249,7 @@ class RedisClient
198
249
  @middlewares = config.middlewares_stack.new(self)
199
250
  @raw_connection = nil
200
251
  @disable_reconnection = false
252
+ @retry_attempt = nil
201
253
  end
202
254
 
203
255
  def inspect
@@ -692,6 +744,7 @@ class RedisClient
692
744
  close if !config.inherit_socket && @pid != PIDCache.pid
693
745
 
694
746
  if @disable_reconnection
747
+ @raw_connection.retry_attempt = nil
695
748
  if block_given?
696
749
  yield @raw_connection
697
750
  else
@@ -702,6 +755,7 @@ class RedisClient
702
755
  connection = nil
703
756
  preferred_error = nil
704
757
  begin
758
+ @retry_attempt = config.retriable?(tries) ? tries : nil
705
759
  connection = raw_connection
706
760
  if block_given?
707
761
  yield connection
@@ -730,6 +784,7 @@ class RedisClient
730
784
  connection = ensure_connected
731
785
  begin
732
786
  @disable_reconnection = true
787
+ @raw_connection.retry_attempt = nil
733
788
  yield connection
734
789
  rescue ConnectionError, ProtocolError
735
790
  close
@@ -744,13 +799,14 @@ class RedisClient
744
799
  if @raw_connection.nil? || !@raw_connection.revalidate
745
800
  connect
746
801
  end
802
+ @raw_connection.retry_attempt = @retry_attempt
747
803
  @raw_connection
748
804
  end
749
805
 
750
806
  def connect
751
807
  @pid = PIDCache.pid
752
808
 
753
- if @raw_connection
809
+ if @raw_connection&.revalidate
754
810
  @middlewares.connect(config) do
755
811
  @raw_connection.reconnect
756
812
  end
@@ -764,6 +820,7 @@ class RedisClient
764
820
  )
765
821
  end
766
822
  end
823
+ @raw_connection.retry_attempt = @retry_attempt
767
824
 
768
825
  prelude = config.connection_prelude.dup
769
826
 
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.24.0
4
+ version: 0.26.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-05 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: connection_pool
@@ -70,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
70
  - !ruby/object:Gem::Version
71
71
  version: '0'
72
72
  requirements: []
73
- rubygems_version: 3.6.2
73
+ rubygems_version: 3.6.9
74
74
  specification_version: 4
75
75
  summary: Simple low-level client for Redis 6+
76
76
  test_files: []