Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package rubygem-redis-client for 
openSUSE:Factory checked in at 2026-07-15 16:34:02
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/rubygem-redis-client (Old)
 and      /work/SRC/openSUSE:Factory/.rubygem-redis-client.new.1991 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "rubygem-redis-client"

Wed Jul 15 16:34:02 2026 rev:3 rq:1365330 version:0.30.0

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/rubygem-redis-client/rubygem-redis-client.changes    
    2025-02-06 22:06:07.791858786 +0100
+++ 
/work/SRC/openSUSE:Factory/.rubygem-redis-client.new.1991/rubygem-redis-client.changes
      2026-07-15 16:51:15.834163960 +0200
@@ -1,0 +2,6 @@
+Mon Jul 13 10:54:23 UTC 2026 - Aleksei Burlakov <[email protected]>
+
+- New upstream release version 0.30.0:
+  * hiredis-client: Now support configuring SSL `verify_mode`.
+
+-------------------------------------------------------------------

Old:
----
  redis-client-0.23.2.gem

New:
----
  redis-client-0.30.0.gem

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ rubygem-redis-client.spec ++++++
--- /var/tmp/diff_new_pack.fVEG4W/_old  2026-07-15 16:51:17.282213161 +0200
+++ /var/tmp/diff_new_pack.fVEG4W/_new  2026-07-15 16:51:17.282213161 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package rubygem-redis-client
 #
-# Copyright (c) 2025 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -24,7 +24,7 @@
 #
 
 Name:           rubygem-redis-client
-Version:        0.23.2
+Version:        0.30.0
 Release:        0
 %define mod_name redis-client
 %define mod_full_name %{mod_name}-%{version}

++++++ redis-client-0.23.2.gem -> redis-client-0.30.0.gem ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/CHANGELOG.md new/CHANGELOG.md
--- old/CHANGELOG.md    2025-01-16 21:20:09.000000000 +0100
+++ new/CHANGELOG.md    1980-01-02 01:00:00.000000000 +0100
@@ -1,5 +1,80 @@
 # Unreleased
 
+# 0.30.0
+
+- hiredis-client: Now support configuring SSL `verify_mode`.
+
+# 0.29.0
+
+- Fix connecting to Redis 7.1 and older with `driver_info:`set.
+- Added `RedisClient::Error#next_error` for an easier way to check all errors 
produced by a pipeline.
+
+# 0.28.0
+
+- Added `RedisClient::HashRing` for horizontal sharing (compatible with 
`Redis::Distributed` from `redis-rb`).
+
+# 0.27.0
+
+- Added `idle_timeout` to revalidate connections that haven't been successfuly 
used in a long time. Defaults to 30 seconds.
+- Added `driver_info` configuration, to issue `CLIENT SETINFO` during 
connection prelude.
+
+# 0.26.4
+
+- Further improve `rediss://` URLs used with Redis sentinel. Now avoid 
override explictly set `ssl:` parameter.
+- Fix compatibility with `redis-rb` in sentinel mode.
+
+# 0.26.3
+
+- Fix `rediss://` (ssl) URLs used with Redis sentinel.
+- Handle Ruby 4.0 connection timeout raising an `IO::Timeout` instead of 
`Errno::ETIMEDOUT`.
+- Entirely close the connection on authentication failures.
+
+# 0.26.2
+
+- Fix compatibility with `connection_pool` version 3+.
+
+# 0.26.1
+
+- Fix a few corner cases where `RedisClient::Error#final?` was innacurate.
+- hiredis-client: Properly reconnect to the new leader after a sentinel 
failover.
+
+# 0.26.0
+
+- Add `RedisClient::Error#final?` and `#retriable?` to allow middleware to 
filter out non-final errors.
+- Fix precedence of `db: nil` initialization parameter.
+
+  ```ruby
+  Redis.new(url: "redis://localhost:6379/3", db: nil).db
+  ```
+
+  Before: `0`
+  After: `3`
+
+# 0.25.3
+
+- Fix `hiredis-client` compilation with `clang 21`.
+
+# 0.25.2
+
+- Fix circuit breakers to respect the `error_threshold_timeout` config is 
provided.
+- Fix circuit breakers to clear errors when closing back.
+
+# 0.25.1
+
+- Fix Ruby driver TCP keep alive TTL. It was intended to be 120 seconds but 
was mistakenly set to 15 seconds.
+
+# 0.25.0
+
+- Fix `hiredis-client` compilation with GCC 15.
+- Fix `hiredis-client` from a work directory with spaces.
+- Add `CommandError#code`.
+- Add `RedisClient::NoScriptError` for `EVALSHA`.
+
+# 0.24.0
+
+- Allow `sentinel_password` to be provided as a `Proc`.
+- Ensure `Config#inspect` and `Config#to_s` do not display stored passwords.
+
 # 0.23.2
 
 - Fix retry logic not to attempt to retry on an open circuit breaker. Fix #227.
@@ -75,6 +150,7 @@
 - Discard sockets rather than explictly close them when a fork is detected. 
#126.
 - Allow to configure sentinel client via url. #117.
 - Fix sentinel to preverse the auth/password when refreshing the sentinel 
list. #107.
+- Added `RedisClient#measure_round_trip_delay` method. #113.
 
 # 0.14.1
 
@@ -179,7 +255,7 @@
 - Added `_v` versions of `call` methods to make it easier to pass commands as 
arrays without splating.
 - Fix calling `blocking_call` with a block in a pipeline.
 - `blocking_call` now raise `ReadTimeoutError` if the command didn't complete 
in time.
-- Fix `blocking_call` to not respect `retry_attempts` on timeout.
+- Fix `blocking_call` to not respect `reconnect_attempts` on timeout.
 - Stop parsing RESP3 sets as Ruby Set instances.
 - Fix `SystemStackError` when parsing very large hashes. Fix: #30
 - `hiredis` now more properly release the GVL when doing IOs.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/README.md new/README.md
--- old/README.md       2025-01-16 21:20:09.000000000 +0100
+++ new/README.md       1980-01-02 01:00:00.000000000 +0100
@@ -83,6 +83,7 @@
 - `connect_timeout`: The connection timeout, takes precedence over the general 
timeout when connecting to the server.
 - `read_timeout`: The read timeout, takes precedence over the general timeout 
when reading responses from the server.
 - `write_timeout`: The write timeout, takes precedence over the general 
timeout when sending commands to the server.
+- `idle_timeout`: Amount of time after which an idle connection has to be 
revalidated with a PING command. Defaults to `30` seconds.
 - `reconnect_attempts`: Specify how many times the client should retry to send 
queries. Defaults to `0`. Makes sure to read the [reconnection 
section](#reconnection) before enabling it.
 - `circuit_breaker`: A Hash with circuit breaker configuration. Defaults to 
`nil`. See the [circuit breaker section](#circuit-breaker) for details.
 - `protocol:` The version of the RESP protocol to use. Default to `3`.
@@ -143,6 +144,7 @@
 If you specify a username and/or password at the top level for your main Redis 
instance, Sentinel *will not* using thouse credentials
 
 ```ruby
+
 # Use 'mysecret' to authenticate against the mymaster instance, but skip 
authentication for the sentinels:
 SENTINELS = [{ host: '127.0.0.1', port: 26380 },
              { host: '127.0.0.1', port: 26381 }]
@@ -166,6 +168,30 @@
 redis_config = RedisClient.sentinel(url: 
"redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master)
 ```
 
+### Consistent Hashing
+
+To horizontally shard keys across multiple servers without relying on 
clustering, a `RedisClient::HashRing` class is provided:
+
+```ruby
+ring = RedisClient.ring(
+  RedisClient.config(host: "10.0.1.1", port: 6380).new_pool(timeout: 0.5, 
size: 3),
+  RedisClient.config(host: "10.0.1.2", port: 6380).new_pool(timeout: 0.5, 
size: 3),
+  RedisClient.config(host: "10.0.1.3", port: 6380).new_pool(timeout: 0.5, 
size: 3),
+)
+
+ring.node_for("cache_key").call("GET", "cache_key") # => "value"
+
+ring.nodes_for("key1", "key2", "key3").each do |node, keys|
+  node.call("DEL", *keys)
+end
+
+ring.nodes.each do |node|
+  node.close
+end
+```
+
+Note that regular clients do respond to `node_for`, `nodes_for` and `nodes`, 
so that code that support `RedisClient.ring` is also usable with a single 
server.
+
 ### Type support
 
 Only a select few Ruby types are supported as arguments beside strings.
@@ -335,7 +361,11 @@
 
 #### Exception management
 
-The `exception` flag in the `#pipelined` method of `RedisClient` is a feature 
that modifies the pipeline execution
+By default, when a pipeline produce multiple command errors, only the first 
encountered error is raised.
+
+However, the raised exception have a `.next_error` method you can use like a 
linked list to check all encountered errors.
+
+Alternatively, the `exception` flag in the `#pipelined` method of 
`RedisClient` is a feature that modifies the pipeline execution
 behavior. When set to `false`, it doesn't raise an exception when a command 
error occurs. Instead, it allows the
 pipeline to execute all commands, and any failed command will be available in 
the returned array. (Defaults to `true`)
 
@@ -459,6 +489,29 @@
 
 redis_config = RedisClient.config(custom: { tags: { "environment": Rails.env 
}})
 ```
+
+### Instrumenting Errors
+
+It is important to note that when `reconnect_attempts` is enabled, all network 
errors are reported to the middlewares,
+even the ones that will be retried.
+
+In many cases you may want to ignore retriable errors, or report them 
differently:
+
+```ruby
+module MyGlobalRedisInstrumentation
+  def call(command, redis_config)
+    super
+  rescue RedisClient::Error => error
+    if error.final?
+      # Error won't be retried.
+    else
+      # Error will be retried.
+    end
+    raise
+  end
+end
+```
+
 ### Timeouts
 
 The client allows you to configure connect, read, and write timeouts.
Binary files old/checksums.yaml.gz and new/checksums.yaml.gz differ
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/circuit_breaker.rb 
new/lib/redis_client/circuit_breaker.rb
--- old/lib/redis_client/circuit_breaker.rb     2025-01-16 21:20:09.000000000 
+0100
+++ new/lib/redis_client/circuit_breaker.rb     1980-01-02 01:00:00.000000000 
+0100
@@ -79,7 +79,7 @@
 
     def record_error
       now = RedisClient.now
-      expiry = now - @error_timeout
+      expiry = now - @error_threshold_timeout
       @lock.synchronize do
         if @state == :closed
           @errors.reject! { |t| t < expiry }
@@ -100,6 +100,7 @@
 
         @successes += 1
         if @successes >= @success_threshold
+          @errors.clear
           @state = :closed
         end
       end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/config.rb 
new/lib/redis_client/config.rb
--- old/lib/redis_client/config.rb      2025-01-16 21:20:09.000000000 +0100
+++ new/lib/redis_client/config.rb      1980-01-02 01:00:00.000000000 +0100
@@ -10,11 +10,12 @@
     DEFAULT_PORT = 6379
     DEFAULT_USERNAME = "default"
     DEFAULT_DB = 0
+    DEFAULT_IDLE_TIMEOUT = 30.0
 
     module Common
       attr_reader :db, :id, :ssl, :ssl_params, :command_builder, 
:inherit_socket,
         :connect_timeout, :read_timeout, :write_timeout, :driver, :protocol,
-        :middlewares_stack, :custom, :circuit_breaker
+        :middlewares_stack, :custom, :circuit_breaker, :driver_info, 
:idle_timeout
 
       alias_method :ssl?, :ssl
 
@@ -27,6 +28,7 @@
         read_timeout: timeout,
         write_timeout: timeout,
         connect_timeout: timeout,
+        idle_timeout: DEFAULT_IDLE_TIMEOUT,
         ssl: nil,
         custom: {},
         ssl_params: nil,
@@ -37,10 +39,11 @@
         inherit_socket: false,
         reconnect_attempts: false,
         middlewares: false,
-        circuit_breaker: nil
+        circuit_breaker: nil,
+        driver_info: nil
       )
         @username = username
-        @password = password
+        @password = password && !password.respond_to?(:call) ? ->(_) { 
password } : password
         @db = begin
           Integer(db || DEFAULT_DB)
         rescue ArgumentError
@@ -54,6 +57,7 @@
         @connect_timeout = connect_timeout
         @read_timeout = read_timeout
         @write_timeout = write_timeout
+        @idle_timeout = idle_timeout
 
         @driver = driver ? RedisClient.driver(driver) : 
RedisClient.default_driver
 
@@ -70,7 +74,6 @@
 
         reconnect_attempts = Array.new(reconnect_attempts, 0).freeze if 
reconnect_attempts.is_a?(Integer)
         @reconnect_attempts = reconnect_attempts
-        @connection_prelude = (build_connection_prelude unless 
@password.respond_to?(:call))
 
         circuit_breaker = CircuitBreaker.new(**circuit_breaker) if 
circuit_breaker.is_a?(Hash)
         if @circuit_breaker = circuit_breaker
@@ -85,28 +88,73 @@
           end
         end
         @middlewares_stack = middlewares_stack
+
+        @driver_info = driver_info
       end
 
       def connection_prelude
-        if @password.respond_to?(:call)
-          build_connection_prelude
-        else
-          @connection_prelude
+        prelude = []
+        pass = password
+        if protocol == 3
+          prelude << if pass
+            ["HELLO", "3", "AUTH", username, pass]
+          else
+            ["HELLO", "3"]
+          end
+        elsif pass
+          prelude << if @username && [email protected]?
+            ["AUTH", @username, pass]
+          else
+            ["AUTH", pass]
+          end
+        end
+
+        if @db && @db != 0
+          prelude << ["SELECT", @db.to_s]
+        end
+
+        # Add CLIENT SETINFO commands for lib-name and lib-ver
+        # These commands are supported in Redis 7.2+
+        if @driver_info
+          prelude << ["CLIENT", "SETINFO", "LIB-NAME", build_lib_name]
+          prelude << ["CLIENT", "SETINFO", "LIB-VER", RedisClient::VERSION]
         end
+
+        # Deep freeze all the strings and commands
+        prelude.map! do |commands|
+          commands = commands.map { |str| str.frozen? ? str : str.dup.freeze }
+          commands.freeze
+        end
+        prelude.freeze
       end
 
       def password
-        if @password.respond_to?(:call)
-          @password.call(username)
-        else
-          @password
-        end
+        @password&.call(username)
       end
 
       def username
         @username || DEFAULT_USERNAME
       end
 
+      # Build the library name for CLIENT SETINFO LIB-NAME.
+      #
+      # @param driver_info [String, Array<String>, nil] Upstream driver info
+      # @return [String] Library name with optional upstream driver info
+      # @raise [ArgumentError] if driver_info is not a String or Array
+      def build_lib_name
+        return "redis-client" if @driver_info.nil?
+
+        info = case @driver_info
+        when String then @driver_info
+        when Array then @driver_info.join(";")
+               else raise ArgumentError, "driver_info must be a String or 
Array of Strings"
+        end
+
+        return "redis-client" if info.empty?
+
+        "redis-client(#{info})"
+      end
+
       def resolved?
         true
       end
@@ -124,6 +172,10 @@
         @client_implementation.new(self, **kwargs)
       end
 
+      def retriable?(attempt)
+        @reconnect_attempts && @reconnect_attempts[attempt]
+      end
+
       def retry_connecting?(attempt, _error)
         if @reconnect_attempts
           if (pause = @reconnect_attempts[attempt])
@@ -162,42 +214,11 @@
         end
         url
       end
-
-      private
-
-      def build_connection_prelude
-        prelude = []
-        pass = password
-        if protocol == 3
-          prelude << if pass
-            ["HELLO", "3", "AUTH", username, pass]
-          else
-            ["HELLO", "3"]
-          end
-        elsif pass
-          prelude << if @username && [email protected]?
-            ["AUTH", @username, pass]
-          else
-            ["AUTH", pass]
-          end
-        end
-
-        if @db && @db != 0
-          prelude << ["SELECT", @db.to_s]
-        end
-
-        # Deep freeze all the strings and commands
-        prelude.map! do |commands|
-          commands = commands.map { |str| str.frozen? ? str : str.dup.freeze }
-          commands.freeze
-        end
-        prelude.freeze
-      end
     end
 
     include Common
 
-    attr_reader :host, :port, :path
+    attr_reader :host, :port, :path, :server_key
 
     def initialize(
       url: nil,
@@ -206,14 +227,15 @@
       path: nil,
       username: nil,
       password: nil,
+      db: nil,
       **kwargs
     )
       if url
         url_config = URLConfig.new(url)
         kwargs = {
           ssl: url_config.ssl?,
-          db: url_config.db,
         }.compact.merge(kwargs)
+        db ||= url_config.db
         host ||= url_config.host
         port ||= url_config.port
         path ||= url_config.path
@@ -221,7 +243,7 @@
         password ||= url_config.password
       end
 
-      super(username: username, password: password, **kwargs)
+      super(username: username, password: password, db: db, **kwargs)
 
       if @path = path
         @host = nil
@@ -230,6 +252,8 @@
         @host = host || DEFAULT_HOST
         @port = Integer(port || DEFAULT_PORT)
       end
+
+      @server_key = [@path, @host, @port].freeze
     end
   end
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/connection_mixin.rb 
new/lib/redis_client/connection_mixin.rb
--- old/lib/redis_client/connection_mixin.rb    2025-01-16 21:20:09.000000000 
+0100
+++ new/lib/redis_client/connection_mixin.rb    1980-01-02 01:00:00.000000000 
+0100
@@ -2,8 +2,14 @@
 
 class RedisClient
   module ConnectionMixin
-    def initialize
+    attr_accessor :retry_attempt
+    attr_reader :config
+
+    def initialize(config)
       @pending_reads = 0
+      @retry_attempt = nil
+      @config = config
+      @server_key = nil
     end
 
     def reconnect
@@ -17,7 +23,7 @@
     end
 
     def revalidate
-      if @pending_reads > 0
+      if @pending_reads > 0 || @server_key != @config.server_key
         close
         false
       else
@@ -33,6 +39,7 @@
       if result.is_a?(Error)
         result._set_command(command)
         result._set_config(config)
+        result._set_retry_attempt(@retry_attempt)
         raise result
       else
         result
@@ -40,7 +47,7 @@
     end
 
     def call_pipelined(commands, timeouts, exception: true)
-      first_exception = nil
+      first_error = last_error = nil
 
       size = commands.size
       results = Array.new(commands.size)
@@ -61,14 +68,17 @@
         elsif result.is_a?(Error)
           result._set_command(commands[index])
           result._set_config(config)
-          first_exception ||= result
+          result._set_retry_attempt(@retry_attempt)
+
+          last_error&._set_next_error(result)
+          first_error ||= last_error = result
         end
 
         results[index] = result
       end
 
-      if first_exception && exception
-        raise first_exception
+      if first_error && exception
+        raise first_error
       else
         results
       end
@@ -82,5 +92,17 @@
       # to account for the network delay.
       timeout + config.read_timeout
     end
+
+    def protocol_error(message)
+      error = ProtocolError.with_config(message, config)
+      error._set_retry_attempt(@retry_attempt)
+      error
+    end
+
+    def connection_error(message)
+      error = ConnectionError.with_config(message, config)
+      error._set_retry_attempt(@retry_attempt)
+      error
+    end
   end
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/hash_ring.rb 
new/lib/redis_client/hash_ring.rb
--- old/lib/redis_client/hash_ring.rb   1970-01-01 01:00:00.000000000 +0100
+++ new/lib/redis_client/hash_ring.rb   1980-01-02 01:00:00.000000000 +0100
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'zlib'
+
+class RedisClient
+  class HashRing
+    POINTS_PER_SERVER = 160
+
+    class << self
+      attr_writer :digest
+
+      def digest
+        @digest ||= begin
+          require 'digest/md5'
+          Digest::MD5
+        end
+      end
+    end
+
+    attr_reader :nodes
+
+    def initialize(nodes = [], replicas: POINTS_PER_SERVER, digest: 
self.class.digest)
+      @replicas = replicas
+      @ring = {}
+      @digest = digest
+      ids = {}
+      @nodes = nodes.dup.freeze
+      nodes.each do |node|
+        id = node.id || node.config.server_url
+        if ids[id]
+          raise ArgumentError, "duplicate node id: #{id.inspect}"
+        end
+
+        ids[id] = true
+
+        replicas.times do |i|
+          @ring[server_hash_for("#{id}:#{i}".freeze)] = node
+        end
+      end
+      @sorted_keys = @ring.keys
+      @sorted_keys.sort!
+    end
+
+    # get the node in the hash ring for this key
+    def node_for(key)
+      hash = hash_for(key)
+      idx = binary_search(@sorted_keys, hash)
+      @ring[@sorted_keys[idx]]
+    end
+
+    def nodes_for(*keys)
+      keys.flatten!
+      mapping = {}
+      keys.each do |key|
+        (mapping[node_for(key)] ||= []) << key
+      end
+      mapping
+    end
+
+    private
+
+    def hash_for(key)
+      Zlib.crc32(key)
+    end
+
+    def server_hash_for(key)
+      @digest.digest(key).unpack1("L>")
+    end
+
+    # Find the closest index in HashRing with value <= the given value
+    def binary_search(ary, value)
+      upper = ary.size
+      lower = 0
+
+      while lower < upper
+        mid = (lower + upper) / 2
+        if ary[mid] > value
+          upper = mid
+        else
+          lower = mid + 1
+        end
+      end
+
+      upper - 1
+    end
+  end
+end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/pooled.rb 
new/lib/redis_client/pooled.rb
--- old/lib/redis_client/pooled.rb      2025-01-16 21:20:09.000000000 +0100
+++ new/lib/redis_client/pooled.rb      1980-01-02 01:00:00.000000000 +0100
@@ -23,7 +23,7 @@
     end
 
     def with(options = EMPTY_HASH)
-      pool.with(options) do |client|
+      pool.with(**options) do |client|
         client.connect_timeout = connect_timeout
         client.read_timeout = read_timeout
         client.write_timeout = write_timeout
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/ruby_connection.rb 
new/lib/redis_client/ruby_connection.rb
--- old/lib/redis_client/ruby_connection.rb     2025-01-16 21:20:09.000000000 
+0100
+++ new/lib/redis_client/ruby_connection.rb     1980-01-02 01:00:00.000000000 
+0100
@@ -40,11 +40,8 @@
 
     SUPPORTS_RESOLV_TIMEOUT = Socket.method(:tcp).parameters.any? { |p| p.last 
== :resolv_timeout }
 
-    attr_reader :config
-
     def initialize(config, connect_timeout:, read_timeout:, write_timeout:)
-      super()
-      @config = config
+      super(config)
       @connect_timeout = connect_timeout
       @read_timeout = read_timeout
       @write_timeout = write_timeout
@@ -75,7 +72,11 @@
       begin
         @io.write(buffer)
       rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
-        raise ConnectionError.with_config(error.message, config)
+        raise connection_error(error.message)
+      rescue Error => error
+        error._set_config(config)
+        error._set_retry_attempt(@retry_attempt)
+        raise error
       end
     end
 
@@ -87,7 +88,7 @@
       begin
         @io.write(buffer)
       rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
-        raise ConnectionError.with_config(error.message, config)
+        raise connection_error(error.message)
       end
     end
 
@@ -97,10 +98,10 @@
       else
         @io.with_timeout(timeout) { RESP3.load(@io) }
       end
-    rescue RedisClient::RESP3::UnknownType => error
-      raise RedisClient::ProtocolError.with_config(error.message, config)
+    rescue RedisClient::RESP3::Error => error
+      raise protocol_error(error.message)
     rescue SystemCallError, IOError, OpenSSL::SSL::SSLError => error
-      raise ConnectionError.with_config(error.message, config)
+      raise connection_error(error.message)
     end
 
     def measure_round_trip_delay
@@ -112,6 +113,7 @@
     private
 
     def connect
+      @server_key = @config.server_key
       socket = if @config.path
         UNIXSocket.new(@config.path)
       else
@@ -154,7 +156,7 @@
         write_timeout: @write_timeout,
       )
       true
-    rescue SystemCallError, OpenSSL::SSL::SSLError, SocketError => error
+    rescue SystemCallError, IOError, OpenSSL::SSL::SSLError, SocketError => 
error
       socket&.close
       raise CannotConnectError, error.message, error.backtrace
     end
@@ -169,7 +171,7 @@
     if %i[SOL_TCP SOL_SOCKET TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { 
|c| Socket.const_defined? c } # Linux
       def enable_socket_keep_alive(socket)
         socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
-        socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 
KEEP_ALIVE_INTERVAL)
+        socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 
KEEP_ALIVE_TTL)
         socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 
KEEP_ALIVE_INTERVAL)
         socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 
KEEP_ALIVE_PROBES)
       end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/sentinel_config.rb 
new/lib/redis_client/sentinel_config.rb
--- old/lib/redis_client/sentinel_config.rb     2025-01-16 21:20:09.000000000 
+0100
+++ new/lib/redis_client/sentinel_config.rb     1980-01-02 01:00:00.000000000 
+0100
@@ -22,12 +22,16 @@
         raise ArgumentError, "Expected role to be either :master or :replica, 
got: #{role.inspect}"
       end
 
+      # Track whether SSL was explicitly provided by user
+      ssl_explicitly_set = client_config.key?(:ssl)
+
       if url
         url_config = URLConfig.new(url)
         client_config = {
           username: url_config.username,
           password: url_config.password,
           db: url_config.db,
+          ssl: url_config.ssl?,
         }.compact.merge(client_config)
         name ||= url_config.host
       end
@@ -38,9 +42,14 @@
       end
 
       @to_list_of_hash = @to_hash = nil
+      password = if sentinel_password && !sentinel_password.respond_to?(:call)
+        ->(_) { sentinel_password }
+      else
+        sentinel_password
+      end
       @extra_config = {
         username: sentinel_username,
-        password: sentinel_password,
+        password: password,
         db: nil,
       }
       if client_config[:protocol] == 2
@@ -61,6 +70,10 @@
 
       client_config[:reconnect_attempts] ||= DEFAULT_RECONNECT_ATTEMPTS
       @client_config = client_config || {}
+      @sentinel_client_config = @client_config.dup
+      # Only remove SSL from sentinel config if it was derived from URL,
+      # not if explicitly set by user.
+      @sentinel_client_config.delete(:ssl) unless ssl_explicitly_set
       super(**client_config)
       @sentinel_configs = sentinels_to_configs(sentinels)
     end
@@ -77,6 +90,10 @@
       end
     end
 
+    def server_key
+      config.server_key
+    end
+
     def host
       config.host
     end
@@ -99,6 +116,10 @@
     end
 
     def check_role!(role)
+      unless role.is_a?(String)
+        raise TypeError, "Expected role to be a string, got: #{role.inspect}"
+      end
+
       if @role == :master
         unless role == "master"
           sleep SENTINEL_DELAY
@@ -124,9 +145,9 @@
       sentinels.map do |sentinel|
         case sentinel
         when String
-          Config.new(**@client_config, **@extra_config, url: sentinel)
+          Config.new(**@sentinel_client_config, **@extra_config, url: sentinel)
         else
-          Config.new(**@client_config, **@extra_config, **sentinel)
+          Config.new(**@sentinel_client_config, **@extra_config, **sentinel)
         end
       end
     end
@@ -143,7 +164,7 @@
 
     def resolve_master
       each_sentinel do |sentinel_client|
-        host, port = sentinel_client.call("SENTINEL", 
"get-master-addr-by-name", @name)
+        host, port = sentinel_client.call_v(["SENTINEL", 
"get-master-addr-by-name", @name])
         next unless host && port
 
         refresh_sentinels(sentinel_client)
@@ -162,7 +183,7 @@
 
     def resolve_replica
       each_sentinel do |sentinel_client|
-        replicas = sentinel_client.call("SENTINEL", "replicas", @name, 
&@to_list_of_hash)
+        replicas = sentinel_client.call_v(["SENTINEL", "replicas", @name], 
&@to_list_of_hash)
         replicas.reject! do |r|
           flags = r["flags"].to_s.split(",")
           flags.include?("s_down") || flags.include?("o_down")
@@ -205,7 +226,7 @@
     end
 
     def refresh_sentinels(sentinel_client)
-      sentinel_response = sentinel_client.call("SENTINEL", "sentinels", @name, 
&@to_list_of_hash)
+      sentinel_response = sentinel_client.call_v(["SENTINEL", "sentinels", 
@name], &@to_list_of_hash)
       sentinels = sentinel_response.map do |sentinel|
         { host: sentinel.fetch("ip"), port: Integer(sentinel.fetch("port")) }
       end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client/version.rb 
new/lib/redis_client/version.rb
--- old/lib/redis_client/version.rb     2025-01-16 21:20:09.000000000 +0100
+++ new/lib/redis_client/version.rb     1980-01-02 01:00:00.000000000 +0100
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 class RedisClient
-  VERSION = "0.23.2"
+  VERSION = "0.30.0"
 end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/lib/redis_client.rb new/lib/redis_client.rb
--- old/lib/redis_client.rb     2025-01-16 21:20:09.000000000 +0100
+++ new/lib/redis_client.rb     1980-01-02 01:00:00.000000000 +0100
@@ -61,7 +61,7 @@
   end
 
   module Common
-    attr_reader :config, :id
+    attr_reader :config, :id, :nodes
     attr_accessor :connect_timeout, :read_timeout, :write_timeout
 
     def initialize(
@@ -78,11 +78,21 @@
       @write_timeout = write_timeout
       @command_builder = config.command_builder
       @pid = PIDCache.pid
+      @nodes = [self].freeze
     end
 
     def timeout=(timeout)
       @connect_timeout = @read_timeout = @write_timeout = timeout
     end
+
+    def node_for(_key)
+      self
+    end
+
+    def nodes_for(*keys)
+      keys.flatten!
+      { self => keys }
+    end
   end
 
   module HasConfig
@@ -99,13 +109,55 @@
     end
   end
 
+  module Retriable
+    def _set_retry_attempt(retry_attempt)
+      @retry_attempt = retry_attempt
+    end
+
+    def retry_attempt
+      @retry_attempt || 0
+    end
+
+    def retriable?
+      !!@retry_attempt
+    end
+
+    def final?
+      !@retry_attempt
+    end
+  end
+
+  module Final
+    def _set_retry_attempt(_retry_attempt)
+    end
+
+    def retry_attempt
+      0
+    end
+
+    def retriable?
+      false
+    end
+
+    def final?
+      true
+    end
+  end
+
   class Error < StandardError
     include HasConfig
+    include Retriable
+
+    attr_reader :next_error
 
     def self.with_config(message, config = nil)
-      new(message).tap do |error|
-        error._set_config(config)
-      end
+      error = new(message)
+      error._set_config(config)
+      error
+    end
+
+    def _set_next_error(error) # :nodoc:
+      @next_error = error
     end
   end
 
@@ -130,8 +182,19 @@
     end
   end
 
+  module HasCode
+    attr_reader :code
+
+    def initialize(message = nil, code = nil)
+      super(message)
+      @code = code
+    end
+  end
+
   class CommandError < Error
     include HasCommand
+    include HasCode
+    include Final
 
     class << self
       def parse(error_message)
@@ -144,7 +207,7 @@
         end
         code ||= error_message.split(' ', 2).first
         klass = ERRORS.fetch(code, self)
-        klass.new(error_message.strip)
+        klass.new(error_message.strip, code.freeze)
       end
     end
   end
@@ -153,12 +216,15 @@
   PermissionError = Class.new(CommandError)
   WrongTypeError = Class.new(CommandError)
   OutOfMemoryError = Class.new(CommandError)
+  NoScriptError = Class.new(CommandError)
 
   ReadOnlyError = Class.new(ConnectionError)
   ReadOnlyError.include(HasCommand)
+  ReadOnlyError.include(HasCode)
 
   MasterDownError = Class.new(ConnectionError)
   MasterDownError.include(HasCommand)
+  MasterDownError.include(HasCode)
 
   CommandError::ERRORS = {
     "WRONGPASS" => AuthenticationError,
@@ -167,6 +233,7 @@
     "MASTERDOWN" => MasterDownError,
     "WRONGTYPE" => WrongTypeError,
     "OOM" => OutOfMemoryError,
+    "NOSCRIPT" => NoScriptError,
   }.freeze
 
   class << self
@@ -178,6 +245,12 @@
       SentinelConfig.new(client_implementation: self, **kwargs)
     end
 
+    def ring(*clients, **options)
+      clients.flatten!
+      require "redis_client/hash_ring" unless defined?(HashRing)
+      HashRing.new(clients, **options)
+    end
+
     def new(arg = nil, **kwargs)
       if arg.is_a?(Config::Common)
         super
@@ -198,6 +271,7 @@
     @middlewares = config.middlewares_stack.new(self)
     @raw_connection = nil
     @disable_reconnection = false
+    @retry_attempt = nil
   end
 
   def inspect
@@ -217,6 +291,10 @@
     config.read_timeout
   end
 
+  def idle_timeout
+    config.idle_timeout
+  end
+
   def db
     config.db
   end
@@ -692,6 +770,7 @@
     close if !config.inherit_socket && @pid != PIDCache.pid
 
     if @disable_reconnection
+      @raw_connection.retry_attempt = nil
       if block_given?
         yield @raw_connection
       else
@@ -702,9 +781,12 @@
       connection = nil
       preferred_error = nil
       begin
+        @retry_attempt = config.retriable?(tries) ? tries : nil
         connection = raw_connection
         if block_given?
-          yield connection
+          result = yield connection
+          @last_used_at = RedisClient.now
+          result
         else
           connection
         end
@@ -730,7 +812,10 @@
       connection = ensure_connected
       begin
         @disable_reconnection = true
-        yield connection
+        @raw_connection.retry_attempt = nil
+        result = yield connection
+        @last_used_at = RedisClient.now
+        result
       rescue ConnectionError, ProtocolError
         close
         raise
@@ -744,13 +829,24 @@
     if @raw_connection.nil? || !@raw_connection.revalidate
       connect
     end
+
+    if config.idle_timeout && (@last_used_at + config.idle_timeout) < 
RedisClient.now
+      @middlewares.call(["PING"], config) do
+        @raw_connection.call(["PING"], nil)
+      rescue ConnectionError, ProtocolError
+        close
+        connect
+      end
+    end
+
+    @raw_connection.retry_attempt = @retry_attempt
     @raw_connection
   end
 
   def connect
     @pid = PIDCache.pid
 
-    if @raw_connection
+    if @raw_connection&.revalidate
       @middlewares.connect(config) do
         @raw_connection.reconnect
       end
@@ -764,6 +860,8 @@
         )
       end
     end
+    @last_used_at = RedisClient.now
+    @raw_connection.retry_attempt = @retry_attempt
 
     prelude = config.connection_prelude.dup
 
@@ -771,31 +869,42 @@
       prelude << ["CLIENT", "SETNAME", id]
     end
 
-    # The connection prelude is deliberately not sent to Middlewares
     if config.sentinel?
       prelude << ["ROLE"]
-      role, = @middlewares.call_pipelined(prelude, config) do
-        @raw_connection.call_pipelined(prelude, nil).last
+    end
+
+    unless prelude.empty?
+      results = @middlewares.call_pipelined(prelude, config) do
+        @raw_connection.call_pipelined(prelude, nil, exception: false)
       end
-      config.check_role!(role)
-    else
-      unless prelude.empty?
-        @middlewares.call_pipelined(prelude, config) do
-          @raw_connection.call_pipelined(prelude, nil)
+
+      results.each do |result|
+        # CLIENT SETINFO is unsupported on Redis < 7.2. The pipeline drained 
all responses before raising,
+        # so if that's the only error, the socket is healthy: keep it.
+        if result.is_a?(CommandError) && !(result.command[0] == "CLIENT" && 
result.command[1] == "SETINFO")
+          raise result
         end
       end
+
+      if config.sentinel?
+        config.check_role!(results.last.first)
+      end
     end
   rescue FailoverError, CannotConnectError => error
+    @raw_connection&.close
     error._set_config(config)
     raise error
   rescue ConnectionError => error
+    @raw_connection&.close
     connect_error = CannotConnectError.with_config(error.message, config)
     connect_error.set_backtrace(error.backtrace)
     raise connect_error
   rescue CommandError => error
-    if error.message.match?(/ERR unknown command ['`]HELLO['`]/)
+    @raw_connection&.close
+
+    if error.command&.first == "HELLO" && error.message.match?(/ERR unknown 
command/)
       raise UnsupportedServer,
-        "redis-client requires Redis 6+ with HELLO command available 
(#{config.server_url})"
+        "redis-client requires Redis 6+ with HELLO command available 
(#{config.server_url})", cause: error
     else
       raise
     end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/metadata new/metadata
--- old/metadata        2025-01-16 21:20:09.000000000 +0100
+++ new/metadata        1980-01-02 01:00:00.000000000 +0100
@@ -1,13 +1,13 @@
 --- !ruby/object:Gem::Specification
 name: redis-client
 version: !ruby/object:Gem::Version
-  version: 0.23.2
+  version: 0.30.0
 platform: ruby
 authors:
 - Jean Boussier
 bindir: bin
 cert_chain: []
-date: 2025-01-16 00:00:00.000000000 Z
+date: 1980-01-02 00:00:00.000000000 Z
 dependencies:
 - !ruby/object:Gem::Dependency
   name: connection_pool
@@ -39,6 +39,7 @@
 - lib/redis_client/config.rb
 - lib/redis_client/connection_mixin.rb
 - lib/redis_client/decorator.rb
+- lib/redis_client/hash_ring.rb
 - lib/redis_client/middlewares.rb
 - lib/redis_client/pid_cache.rb
 - lib/redis_client/pooled.rb
@@ -70,7 +71,7 @@
     - !ruby/object:Gem::Version
       version: '0'
 requirements: []
-rubygems_version: 3.6.2
+rubygems_version: 4.0.12
 specification_version: 4
 summary: Simple low-level client for Redis 6+
 test_files: []

Reply via email to