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: []