redis-client 0.25.2 → 0.26.3

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: e14494ff0e910564d0afcc8dea365b35e428c73364d4da0c31c84f8a9ada8d8b
4
- data.tar.gz: dacf316c8c4893b1d0fd95e010a010837485825390fbd83b1620f8cc6dff6985
3
+ metadata.gz: f1158a824b42ce5297ff9eb38755a9af61ccc3635f6f201e17708e91ffc8b230
4
+ data.tar.gz: 8c8ffd7afa1672760e628ae6725f02b614588a963288dd55cb8fc2b5db43ed82
5
5
  SHA512:
6
- metadata.gz: 00ef6af52d83d2b35a5a8e638cb001f98b16d5f9752c35c107e66665415ac108d728992bf879b5577ac72859af67c70c3b0c4b40c62da73aee5cca17122de23f
7
- data.tar.gz: d9564edbac42517600787da3bcf3c929158e5a4d4e757f6b9ccff7cd6ee296637d89521adfbc902fc83ac880d53a155c289e3bbc09254bf32f8530cf3ab308dd
6
+ metadata.gz: 2ba91ae532685d186151cc2a2b3b9d2cc485968a4b4aafb765fae2bce0046d962776dd5dd9ba783c9baae7425d785af9600d28b22befaf982ea90689ec373242
7
+ data.tar.gz: 373343251df2027cb19ab4fab1cdc63de4b5d2f2ca5bcc2542e9486818bbc93b6b88da63d34a1db63044f4adba690b4d2f534390acb8c2739a633c9b6bfcda42
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Unreleased
2
2
 
3
+ # 0.26.3
4
+
5
+ - Fix `rediss://` (ssl) URLs used with Redis sentinel.
6
+ - Handle Ruby 4.0 connection timeout raising an `IO::Timeout` instead of `Errno::ETIMEDOUT`.
7
+ - Entirely close the connection on authentication failures.
8
+
9
+ # 0.26.2
10
+
11
+ - Fix compatibility with `connection_pool` version 3+.
12
+
13
+ # 0.26.1
14
+
15
+ - Fix a few corner cases where `RedisClient::Error#final?` was innacurate.
16
+ - hiredis-client: Properly reconnect to the new leader after a sentinel failover.
17
+
18
+ # 0.26.0
19
+
20
+ - Add `RedisClient::Error#final?` and `#retriable?` to allow middleware to filter out non-final errors.
21
+ - Fix precedence of `db: nil` initialization parameter.
22
+
23
+ ```ruby
24
+ Redis.new(url: "redis://localhost:6379/3", db: nil).db
25
+ ```
26
+
27
+ Before: `0`
28
+ After: `3`
29
+
30
+ # 0.25.3
31
+
32
+ - Fix `hiredis-client` compilation with `clang 21`.
33
+
3
34
  # 0.25.2
4
35
 
5
36
  - Fix circuit breakers to respect the `error_threshold_timeout` config is provided.
@@ -201,7 +232,7 @@
201
232
  - Added `_v` versions of `call` methods to make it easier to pass commands as arrays without splating.
202
233
  - Fix calling `blocking_call` with a block in a pipeline.
203
234
  - `blocking_call` now raise `ReadTimeoutError` if the command didn't complete in time.
204
- - Fix `blocking_call` to not respect `retry_attempts` on timeout.
235
+ - Fix `blocking_call` to not respect `reconnect_attempts` on timeout.
205
236
  - Stop parsing RESP3 sets as Ruby Set instances.
206
237
  - Fix `SystemStackError` when parsing very large hashes. Fix: #30
207
238
  - `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.
@@ -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
@@ -154,7 +156,7 @@ class RedisClient
154
156
  write_timeout: @write_timeout,
155
157
  )
156
158
  true
157
- rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
159
+ rescue SystemCallError, IOError, OpenSSL::SSL::SSLError, SocketError => error
158
160
  socket&.close
159
161
  raise CannotConnectError, error.message, error.backtrace
160
162
  end
@@ -28,6 +28,7 @@ class RedisClient
28
28
  username: url_config.username,
29
29
  password: url_config.password,
30
30
  db: url_config.db,
31
+ ssl: url_config.ssl?,
31
32
  }.compact.merge(client_config)
32
33
  name ||= url_config.host
33
34
  end
@@ -66,6 +67,8 @@ class RedisClient
66
67
 
67
68
  client_config[:reconnect_attempts] ||= DEFAULT_RECONNECT_ATTEMPTS
68
69
  @client_config = client_config || {}
70
+ @sentinel_client_config = @client_config.dup
71
+ @sentinel_client_config.delete(:ssl)
69
72
  super(**client_config)
70
73
  @sentinel_configs = sentinels_to_configs(sentinels)
71
74
  end
@@ -82,6 +85,10 @@ class RedisClient
82
85
  end
83
86
  end
84
87
 
88
+ def server_key
89
+ config.server_key
90
+ end
91
+
85
92
  def host
86
93
  config.host
87
94
  end
@@ -129,9 +136,9 @@ class RedisClient
129
136
  sentinels.map do |sentinel|
130
137
  case sentinel
131
138
  when String
132
- Config.new(**@client_config, **@extra_config, url: sentinel)
139
+ Config.new(**@sentinel_client_config, **@extra_config, url: sentinel)
133
140
  else
134
- Config.new(**@client_config, **@extra_config, **sentinel)
141
+ Config.new(**@sentinel_client_config, **@extra_config, **sentinel)
135
142
  end
136
143
  end
137
144
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RedisClient
4
- VERSION = "0.25.2"
4
+ VERSION = "0.26.3"
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
 
@@ -142,6 +178,7 @@ class RedisClient
142
178
  class CommandError < Error
143
179
  include HasCommand
144
180
  include HasCode
181
+ include Final
145
182
 
146
183
  class << self
147
184
  def parse(error_message)
@@ -212,6 +249,7 @@ class RedisClient
212
249
  @middlewares = config.middlewares_stack.new(self)
213
250
  @raw_connection = nil
214
251
  @disable_reconnection = false
252
+ @retry_attempt = nil
215
253
  end
216
254
 
217
255
  def inspect
@@ -706,6 +744,7 @@ class RedisClient
706
744
  close if !config.inherit_socket && @pid != PIDCache.pid
707
745
 
708
746
  if @disable_reconnection
747
+ @raw_connection.retry_attempt = nil
709
748
  if block_given?
710
749
  yield @raw_connection
711
750
  else
@@ -716,6 +755,7 @@ class RedisClient
716
755
  connection = nil
717
756
  preferred_error = nil
718
757
  begin
758
+ @retry_attempt = config.retriable?(tries) ? tries : nil
719
759
  connection = raw_connection
720
760
  if block_given?
721
761
  yield connection
@@ -744,6 +784,7 @@ class RedisClient
744
784
  connection = ensure_connected
745
785
  begin
746
786
  @disable_reconnection = true
787
+ @raw_connection.retry_attempt = nil
747
788
  yield connection
748
789
  rescue ConnectionError, ProtocolError
749
790
  close
@@ -758,13 +799,14 @@ class RedisClient
758
799
  if @raw_connection.nil? || !@raw_connection.revalidate
759
800
  connect
760
801
  end
802
+ @raw_connection.retry_attempt = @retry_attempt
761
803
  @raw_connection
762
804
  end
763
805
 
764
806
  def connect
765
807
  @pid = PIDCache.pid
766
808
 
767
- if @raw_connection
809
+ if @raw_connection&.revalidate
768
810
  @middlewares.connect(config) do
769
811
  @raw_connection.reconnect
770
812
  end
@@ -778,6 +820,7 @@ class RedisClient
778
820
  )
779
821
  end
780
822
  end
823
+ @raw_connection.retry_attempt = @retry_attempt
781
824
 
782
825
  prelude = config.connection_prelude.dup
783
826
 
@@ -800,13 +843,16 @@ class RedisClient
800
843
  end
801
844
  end
802
845
  rescue FailoverError, CannotConnectError => error
846
+ @raw_connection&.close
803
847
  error._set_config(config)
804
848
  raise error
805
849
  rescue ConnectionError => error
850
+ @raw_connection&.close
806
851
  connect_error = CannotConnectError.with_config(error.message, config)
807
852
  connect_error.set_backtrace(error.backtrace)
808
853
  raise connect_error
809
854
  rescue CommandError => error
855
+ @raw_connection&.close
810
856
  if error.message.match?(/ERR unknown command ['`]HELLO['`]/)
811
857
  raise UnsupportedServer,
812
858
  "redis-client requires Redis 6+ with HELLO command available (#{config.server_url})"
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.25.2
4
+ version: 0.26.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-10 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: 4.0.3
74
74
  specification_version: 4
75
75
  summary: Simple low-level client for Redis 6+
76
76
  test_files: []