redis 4.1.4 → 4.7.1
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 +4 -4
- data/CHANGELOG.md +141 -0
- data/README.md +52 -27
- data/lib/redis/client.rb +122 -87
- data/lib/redis/cluster/command.rb +4 -6
- data/lib/redis/cluster/command_loader.rb +8 -9
- data/lib/redis/cluster/node.rb +17 -1
- data/lib/redis/cluster/node_loader.rb +8 -11
- data/lib/redis/cluster/option.rb +18 -5
- data/lib/redis/cluster/slot.rb +28 -14
- data/lib/redis/cluster/slot_loader.rb +11 -15
- data/lib/redis/cluster.rb +37 -13
- data/lib/redis/commands/bitmaps.rb +63 -0
- data/lib/redis/commands/cluster.rb +45 -0
- data/lib/redis/commands/connection.rb +58 -0
- data/lib/redis/commands/geo.rb +84 -0
- data/lib/redis/commands/hashes.rb +251 -0
- data/lib/redis/commands/hyper_log_log.rb +37 -0
- data/lib/redis/commands/keys.rb +411 -0
- data/lib/redis/commands/lists.rb +289 -0
- data/lib/redis/commands/pubsub.rb +72 -0
- data/lib/redis/commands/scripting.rb +114 -0
- data/lib/redis/commands/server.rb +188 -0
- data/lib/redis/commands/sets.rb +207 -0
- data/lib/redis/commands/sorted_sets.rb +812 -0
- data/lib/redis/commands/streams.rb +382 -0
- data/lib/redis/commands/strings.rb +313 -0
- data/lib/redis/commands/transactions.rb +139 -0
- data/lib/redis/commands.rb +242 -0
- data/lib/redis/connection/command_helper.rb +4 -2
- data/lib/redis/connection/hiredis.rb +6 -7
- data/lib/redis/connection/registry.rb +1 -1
- data/lib/redis/connection/ruby.rb +106 -114
- data/lib/redis/connection/synchrony.rb +16 -10
- data/lib/redis/connection.rb +2 -1
- data/lib/redis/distributed.rb +200 -65
- data/lib/redis/errors.rb +10 -0
- data/lib/redis/hash_ring.rb +14 -14
- data/lib/redis/pipeline.rb +133 -10
- data/lib/redis/subscribe.rb +10 -12
- data/lib/redis/version.rb +2 -1
- data/lib/redis.rb +158 -3358
- metadata +32 -10
    
        data/lib/redis/client.rb
    CHANGED
    
    | @@ -1,31 +1,38 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
            require_relative "errors"
         | 
| 4 3 | 
             
            require "socket"
         | 
| 5 4 | 
             
            require "cgi"
         | 
| 5 | 
            +
            require "redis/errors"
         | 
| 6 6 |  | 
| 7 7 | 
             
            class Redis
         | 
| 8 8 | 
             
              class Client
         | 
| 9 | 
            -
             | 
| 9 | 
            +
                # Defaults are also used for converting string keys to symbols.
         | 
| 10 10 | 
             
                DEFAULTS = {
         | 
| 11 | 
            -
                  : | 
| 12 | 
            -
                  : | 
| 13 | 
            -
                  : | 
| 14 | 
            -
                  : | 
| 15 | 
            -
                  : | 
| 16 | 
            -
                  : | 
| 17 | 
            -
                  : | 
| 18 | 
            -
                  : | 
| 19 | 
            -
                  : | 
| 20 | 
            -
                  : | 
| 21 | 
            -
                  : | 
| 22 | 
            -
                  : | 
| 23 | 
            -
                  : | 
| 24 | 
            -
                  : | 
| 25 | 
            -
                  : | 
| 26 | 
            -
             | 
| 27 | 
            -
             | 
| 28 | 
            -
             | 
| 11 | 
            +
                  url: -> { ENV["REDIS_URL"] },
         | 
| 12 | 
            +
                  scheme: "redis",
         | 
| 13 | 
            +
                  host: "127.0.0.1",
         | 
| 14 | 
            +
                  port: 6379,
         | 
| 15 | 
            +
                  path: nil,
         | 
| 16 | 
            +
                  read_timeout: nil,
         | 
| 17 | 
            +
                  write_timeout: nil,
         | 
| 18 | 
            +
                  connect_timeout: nil,
         | 
| 19 | 
            +
                  timeout: 5.0,
         | 
| 20 | 
            +
                  username: nil,
         | 
| 21 | 
            +
                  password: nil,
         | 
| 22 | 
            +
                  db: 0,
         | 
| 23 | 
            +
                  driver: nil,
         | 
| 24 | 
            +
                  id: nil,
         | 
| 25 | 
            +
                  tcp_keepalive: 0,
         | 
| 26 | 
            +
                  reconnect_attempts: 1,
         | 
| 27 | 
            +
                  reconnect_delay: 0,
         | 
| 28 | 
            +
                  reconnect_delay_max: 0.5,
         | 
| 29 | 
            +
                  inherit_socket: false,
         | 
| 30 | 
            +
                  logger: nil,
         | 
| 31 | 
            +
                  sentinels: nil,
         | 
| 32 | 
            +
                  role: nil
         | 
| 33 | 
            +
                }.freeze
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                attr_reader :options, :connection, :command_map
         | 
| 29 36 |  | 
| 30 37 | 
             
                def scheme
         | 
| 31 38 | 
             
                  @options[:scheme]
         | 
| @@ -55,6 +62,10 @@ class Redis | |
| 55 62 | 
             
                  @options[:read_timeout]
         | 
| 56 63 | 
             
                end
         | 
| 57 64 |  | 
| 65 | 
            +
                def username
         | 
| 66 | 
            +
                  @options[:username]
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 58 69 | 
             
                def password
         | 
| 59 70 | 
             
                  @options[:password]
         | 
| 60 71 | 
             
                end
         | 
| @@ -76,8 +87,6 @@ class Redis | |
| 76 87 | 
             
                end
         | 
| 77 88 |  | 
| 78 89 | 
             
                attr_accessor :logger
         | 
| 79 | 
            -
                attr_reader :connection
         | 
| 80 | 
            -
                attr_reader :command_map
         | 
| 81 90 |  | 
| 82 91 | 
             
                def initialize(options = {})
         | 
| 83 92 | 
             
                  @options = _parse_options(options)
         | 
| @@ -89,7 +98,7 @@ class Redis | |
| 89 98 | 
             
                  @pending_reads = 0
         | 
| 90 99 |  | 
| 91 100 | 
             
                  @connector =
         | 
| 92 | 
            -
                    if options. | 
| 101 | 
            +
                    if !@options[:sentinels].nil?
         | 
| 93 102 | 
             
                      Connector::Sentinel.new(@options)
         | 
| 94 103 | 
             
                    elsif options.include?(:connector) && options[:connector].respond_to?(:new)
         | 
| 95 104 | 
             
                      options.delete(:connector).new(@options)
         | 
| @@ -104,7 +113,34 @@ class Redis | |
| 104 113 | 
             
                  # Don't try to reconnect when the connection is fresh
         | 
| 105 114 | 
             
                  with_reconnect(false) do
         | 
| 106 115 | 
             
                    establish_connection
         | 
| 107 | 
            -
                     | 
| 116 | 
            +
                    if password
         | 
| 117 | 
            +
                      if username
         | 
| 118 | 
            +
                        begin
         | 
| 119 | 
            +
                          call [:auth, username, password]
         | 
| 120 | 
            +
                        rescue CommandError => err # Likely on Redis < 6
         | 
| 121 | 
            +
                          case err.message
         | 
| 122 | 
            +
                          when /ERR wrong number of arguments for 'auth' command/
         | 
| 123 | 
            +
                            call [:auth, password]
         | 
| 124 | 
            +
                          when /WRONGPASS invalid username-password pair/
         | 
| 125 | 
            +
                            begin
         | 
| 126 | 
            +
                              call [:auth, password]
         | 
| 127 | 
            +
                            rescue CommandError
         | 
| 128 | 
            +
                              raise err
         | 
| 129 | 
            +
                            end
         | 
| 130 | 
            +
                            ::Redis.deprecate!(
         | 
| 131 | 
            +
                              "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
         | 
| 132 | 
            +
                              " the provided password was for the default user. This will start failing in redis-rb 5.0.0."
         | 
| 133 | 
            +
                            )
         | 
| 134 | 
            +
                          else
         | 
| 135 | 
            +
                            raise
         | 
| 136 | 
            +
                          end
         | 
| 137 | 
            +
                        end
         | 
| 138 | 
            +
                      else
         | 
| 139 | 
            +
                        call [:auth, password]
         | 
| 140 | 
            +
                      end
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                    call [:readonly] if @options[:readonly]
         | 
| 108 144 | 
             
                    call [:select, db] if db != 0
         | 
| 109 145 | 
             
                    call [:client, :setname, @options[:id]] if @options[:id]
         | 
| 110 146 | 
             
                    @connector.check(self)
         | 
| @@ -114,7 +150,7 @@ class Redis | |
| 114 150 | 
             
                end
         | 
| 115 151 |  | 
| 116 152 | 
             
                def id
         | 
| 117 | 
            -
                  @options[:id] || " | 
| 153 | 
            +
                  @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
         | 
| 118 154 | 
             
                end
         | 
| 119 155 |  | 
| 120 156 | 
             
                def location
         | 
| @@ -125,7 +161,7 @@ class Redis | |
| 125 161 | 
             
                  reply = process([command]) { read }
         | 
| 126 162 | 
             
                  raise reply if reply.is_a?(CommandError)
         | 
| 127 163 |  | 
| 128 | 
            -
                  if block_given?
         | 
| 164 | 
            +
                  if block_given? && reply != 'QUEUED'
         | 
| 129 165 | 
             
                    yield reply
         | 
| 130 166 | 
             
                  else
         | 
| 131 167 | 
             
                    reply
         | 
| @@ -166,6 +202,7 @@ class Redis | |
| 166 202 | 
             
                      end
         | 
| 167 203 | 
             
                    rescue ConnectionError => e
         | 
| 168 204 | 
             
                      return nil if pipeline.shutdown?
         | 
| 205 | 
            +
             | 
| 169 206 | 
             
                      # Assume the pipeline was sent in one piece, but execution of
         | 
| 170 207 | 
             
                      # SHUTDOWN caused none of the replies for commands that were executed
         | 
| 171 208 | 
             
                      # prior to it from coming back around.
         | 
| @@ -214,7 +251,8 @@ class Redis | |
| 214 251 | 
             
                  result
         | 
| 215 252 | 
             
                end
         | 
| 216 253 |  | 
| 217 | 
            -
                def call_with_timeout(command,  | 
| 254 | 
            +
                def call_with_timeout(command, extra_timeout, &blk)
         | 
| 255 | 
            +
                  timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
         | 
| 218 256 | 
             
                  with_socket_timeout(timeout) do
         | 
| 219 257 | 
             
                    call(command, &blk)
         | 
| 220 258 | 
             
                  end
         | 
| @@ -244,13 +282,13 @@ class Redis | |
| 244 282 | 
             
                end
         | 
| 245 283 |  | 
| 246 284 | 
             
                def connected?
         | 
| 247 | 
            -
                  !! | 
| 285 | 
            +
                  !!(connection && connection.connected?)
         | 
| 248 286 | 
             
                end
         | 
| 249 287 |  | 
| 250 288 | 
             
                def disconnect
         | 
| 251 289 | 
             
                  connection.disconnect if connected?
         | 
| 252 290 | 
             
                end
         | 
| 253 | 
            -
                 | 
| 291 | 
            +
                alias close disconnect
         | 
| 254 292 |  | 
| 255 293 | 
             
                def reconnect
         | 
| 256 294 | 
             
                  disconnect
         | 
| @@ -264,7 +302,7 @@ class Redis | |
| 264 302 | 
             
                  e2 = TimeoutError.new("Connection timed out")
         | 
| 265 303 | 
             
                  e2.set_backtrace(e1.backtrace)
         | 
| 266 304 | 
             
                  raise e2
         | 
| 267 | 
            -
                rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
         | 
| 305 | 
            +
                rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
         | 
| 268 306 | 
             
                  raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
         | 
| 269 307 | 
             
                end
         | 
| 270 308 |  | 
| @@ -301,30 +339,27 @@ class Redis | |
| 301 339 | 
             
                  with_socket_timeout(0, &blk)
         | 
| 302 340 | 
             
                end
         | 
| 303 341 |  | 
| 304 | 
            -
                def with_reconnect(val=true)
         | 
| 305 | 
            -
                   | 
| 306 | 
            -
             | 
| 307 | 
            -
             | 
| 308 | 
            -
                   | 
| 309 | 
            -
                    @reconnect = original
         | 
| 310 | 
            -
                  end
         | 
| 342 | 
            +
                def with_reconnect(val = true)
         | 
| 343 | 
            +
                  original, @reconnect = @reconnect, val
         | 
| 344 | 
            +
                  yield
         | 
| 345 | 
            +
                ensure
         | 
| 346 | 
            +
                  @reconnect = original
         | 
| 311 347 | 
             
                end
         | 
| 312 348 |  | 
| 313 349 | 
             
                def without_reconnect(&blk)
         | 
| 314 350 | 
             
                  with_reconnect(false, &blk)
         | 
| 315 351 | 
             
                end
         | 
| 316 352 |  | 
| 317 | 
            -
             | 
| 353 | 
            +
                protected
         | 
| 318 354 |  | 
| 319 355 | 
             
                def logging(commands)
         | 
| 320 | 
            -
                  return yield unless @logger | 
| 356 | 
            +
                  return yield unless @logger&.debug?
         | 
| 321 357 |  | 
| 322 358 | 
             
                  begin
         | 
| 323 359 | 
             
                    commands.each do |name, *args|
         | 
| 324 360 | 
             
                      logged_args = args.map do |a|
         | 
| 325 | 
            -
                         | 
| 326 | 
            -
                         | 
| 327 | 
            -
                        when a.respond_to?(:to_s)    then a.to_s
         | 
| 361 | 
            +
                        if a.respond_to?(:inspect) then a.inspect
         | 
| 362 | 
            +
                        elsif a.respond_to?(:to_s) then a.to_s
         | 
| 328 363 | 
             
                        else
         | 
| 329 364 | 
             
                          # handle poorly-behaved descendants of BasicObject
         | 
| 330 365 | 
             
                          klass = a.instance_exec { (class << self; self end).superclass }
         | 
| @@ -358,9 +393,9 @@ class Redis | |
| 358 393 | 
             
                       Errno::ENETUNREACH,
         | 
| 359 394 | 
             
                       Errno::ENOENT,
         | 
| 360 395 | 
             
                       Errno::ETIMEDOUT,
         | 
| 361 | 
            -
                       Errno::EINVAL
         | 
| 396 | 
            +
                       Errno::EINVAL => error
         | 
| 362 397 |  | 
| 363 | 
            -
                  raise CannotConnectError, "Error connecting to Redis on #{location} (#{ | 
| 398 | 
            +
                  raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
         | 
| 364 399 | 
             
                end
         | 
| 365 400 |  | 
| 366 401 | 
             
                def ensure_connected
         | 
| @@ -374,9 +409,9 @@ class Redis | |
| 374 409 | 
             
                    if connected?
         | 
| 375 410 | 
             
                      unless inherit_socket? || Process.pid == @pid
         | 
| 376 411 | 
             
                        raise InheritedError,
         | 
| 377 | 
            -
             | 
| 378 | 
            -
             | 
| 379 | 
            -
             | 
| 412 | 
            +
                              "Tried to use a connection from a child process without reconnecting. " \
         | 
| 413 | 
            +
                              "You need to reconnect to Redis after forking " \
         | 
| 414 | 
            +
                              "or set :inherit_socket to true."
         | 
| 380 415 | 
             
                      end
         | 
| 381 416 | 
             
                    else
         | 
| 382 417 | 
             
                      connect
         | 
| @@ -387,7 +422,7 @@ class Redis | |
| 387 422 | 
             
                    disconnect
         | 
| 388 423 |  | 
| 389 424 | 
             
                    if attempts <= @options[:reconnect_attempts] && @reconnect
         | 
| 390 | 
            -
                      sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
         | 
| 425 | 
            +
                      sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
         | 
| 391 426 | 
             
                                 @options[:reconnect_delay_max]].min
         | 
| 392 427 |  | 
| 393 428 | 
             
                      Kernel.sleep(sleep_t)
         | 
| @@ -407,18 +442,16 @@ class Redis | |
| 407 442 | 
             
                  defaults = DEFAULTS.dup
         | 
| 408 443 | 
             
                  options = options.dup
         | 
| 409 444 |  | 
| 410 | 
            -
                  defaults. | 
| 445 | 
            +
                  defaults.each_key do |key|
         | 
| 411 446 | 
             
                    # Fill in defaults if needed
         | 
| 412 | 
            -
                    if defaults[key].respond_to?(:call)
         | 
| 413 | 
            -
                      defaults[key] = defaults[key].call
         | 
| 414 | 
            -
                    end
         | 
| 447 | 
            +
                    defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
         | 
| 415 448 |  | 
| 416 449 | 
             
                    # Symbolize only keys that are needed
         | 
| 417 | 
            -
                    options[key] = options[key.to_s] if options. | 
| 450 | 
            +
                    options[key] = options[key.to_s] if options.key?(key.to_s)
         | 
| 418 451 | 
             
                  end
         | 
| 419 452 |  | 
| 420 453 | 
             
                  url = options[:url]
         | 
| 421 | 
            -
                  url = defaults[:url] if url | 
| 454 | 
            +
                  url = defaults[:url] if url.nil?
         | 
| 422 455 |  | 
| 423 456 | 
             
                  # Override defaults from URL if given
         | 
| 424 457 | 
             
                  if url
         | 
| @@ -426,13 +459,15 @@ class Redis | |
| 426 459 |  | 
| 427 460 | 
             
                    uri = URI(url)
         | 
| 428 461 |  | 
| 429 | 
            -
                     | 
| 430 | 
            -
             | 
| 431 | 
            -
             | 
| 462 | 
            +
                    case uri.scheme
         | 
| 463 | 
            +
                    when "unix"
         | 
| 464 | 
            +
                      defaults[:path] = uri.path
         | 
| 465 | 
            +
                    when "redis", "rediss"
         | 
| 432 466 | 
             
                      defaults[:scheme]   = uri.scheme
         | 
| 433 | 
            -
                      defaults[:host]     = uri.host if uri.host
         | 
| 467 | 
            +
                      defaults[:host]     = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
         | 
| 434 468 | 
             
                      defaults[:port]     = uri.port if uri.port
         | 
| 435 | 
            -
                      defaults[: | 
| 469 | 
            +
                      defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
         | 
| 470 | 
            +
                      defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
         | 
| 436 471 | 
             
                      defaults[:db]       = uri.path[1..-1].to_i if uri.path
         | 
| 437 472 | 
             
                      defaults[:role] = :master
         | 
| 438 473 | 
             
                    else
         | 
| @@ -443,7 +478,7 @@ class Redis | |
| 443 478 | 
             
                  end
         | 
| 444 479 |  | 
| 445 480 | 
             
                  # Use default when option is not specified or nil
         | 
| 446 | 
            -
                  defaults. | 
| 481 | 
            +
                  defaults.each_key do |key|
         | 
| 447 482 | 
             
                    options[key] = defaults[key] if options[key].nil?
         | 
| 448 483 | 
             
                  end
         | 
| 449 484 |  | 
| @@ -458,7 +493,7 @@ class Redis | |
| 458 493 | 
             
                    options[:port] = options[:port].to_i
         | 
| 459 494 | 
             
                  end
         | 
| 460 495 |  | 
| 461 | 
            -
                  if options. | 
| 496 | 
            +
                  if options.key?(:timeout)
         | 
| 462 497 | 
             
                    options[:connect_timeout] ||= options[:timeout]
         | 
| 463 498 | 
             
                    options[:read_timeout]    ||= options[:timeout]
         | 
| 464 499 | 
             
                    options[:write_timeout]   ||= options[:timeout]
         | 
| @@ -477,7 +512,7 @@ class Redis | |
| 477 512 |  | 
| 478 513 | 
             
                  case options[:tcp_keepalive]
         | 
| 479 514 | 
             
                  when Hash
         | 
| 480 | 
            -
                    [ | 
| 515 | 
            +
                    %i[time intvl probes].each do |key|
         | 
| 481 516 | 
             
                      unless options[:tcp_keepalive][key].is_a?(Integer)
         | 
| 482 517 | 
             
                        raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
         | 
| 483 518 | 
             
                      end
         | 
| @@ -485,13 +520,13 @@ class Redis | |
| 485 520 |  | 
| 486 521 | 
             
                  when Integer
         | 
| 487 522 | 
             
                    if options[:tcp_keepalive] >= 60
         | 
| 488 | 
            -
                      options[:tcp_keepalive] = {: | 
| 523 | 
            +
                      options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
         | 
| 489 524 |  | 
| 490 525 | 
             
                    elsif options[:tcp_keepalive] >= 30
         | 
| 491 | 
            -
                      options[:tcp_keepalive] = {: | 
| 526 | 
            +
                      options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
         | 
| 492 527 |  | 
| 493 528 | 
             
                    elsif options[:tcp_keepalive] >= 5
         | 
| 494 | 
            -
                      options[:tcp_keepalive] = {: | 
| 529 | 
            +
                      options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
         | 
| 495 530 | 
             
                    end
         | 
| 496 531 | 
             
                  end
         | 
| 497 532 |  | 
| @@ -503,14 +538,14 @@ class Redis | |
| 503 538 | 
             
                def _parse_driver(driver)
         | 
| 504 539 | 
             
                  driver = driver.to_s if driver.is_a?(Symbol)
         | 
| 505 540 |  | 
| 506 | 
            -
                  if driver. | 
| 541 | 
            +
                  if driver.is_a?(String)
         | 
| 507 542 | 
             
                    begin
         | 
| 508 543 | 
             
                      require_relative "connection/#{driver}"
         | 
| 509 | 
            -
                    rescue LoadError, NameError | 
| 544 | 
            +
                    rescue LoadError, NameError
         | 
| 510 545 | 
             
                      begin
         | 
| 511 | 
            -
                        require "connection/#{driver}"
         | 
| 512 | 
            -
                      rescue LoadError, NameError =>  | 
| 513 | 
            -
                        raise  | 
| 546 | 
            +
                        require "redis/connection/#{driver}"
         | 
| 547 | 
            +
                      rescue LoadError, NameError => error
         | 
| 548 | 
            +
                        raise "Cannot load driver #{driver.inspect}: #{error.message}"
         | 
| 514 549 | 
             
                      end
         | 
| 515 550 | 
             
                    end
         | 
| 516 551 |  | 
| @@ -529,8 +564,7 @@ class Redis | |
| 529 564 | 
             
                    @options
         | 
| 530 565 | 
             
                  end
         | 
| 531 566 |  | 
| 532 | 
            -
                  def check(client)
         | 
| 533 | 
            -
                  end
         | 
| 567 | 
            +
                  def check(client); end
         | 
| 534 568 |  | 
| 535 569 | 
             
                  class Sentinel < Connector
         | 
| 536 570 | 
             
                    def initialize(options)
         | 
| @@ -539,7 +573,7 @@ class Redis | |
| 539 573 | 
             
                      @options[:db] = DEFAULTS.fetch(:db)
         | 
| 540 574 |  | 
| 541 575 | 
             
                      @sentinels = @options.delete(:sentinels).dup
         | 
| 542 | 
            -
                      @role = @options | 
| 576 | 
            +
                      @role = (@options[:role] || "master").to_s
         | 
| 543 577 | 
             
                      @master = @options[:host]
         | 
| 544 578 | 
             
                    end
         | 
| 545 579 |  | 
| @@ -562,13 +596,13 @@ class Redis | |
| 562 596 |  | 
| 563 597 | 
             
                    def resolve
         | 
| 564 598 | 
             
                      result = case @role
         | 
| 565 | 
            -
             | 
| 566 | 
            -
             | 
| 567 | 
            -
             | 
| 568 | 
            -
             | 
| 569 | 
            -
             | 
| 570 | 
            -
             | 
| 571 | 
            -
             | 
| 599 | 
            +
                      when "master"
         | 
| 600 | 
            +
                        resolve_master
         | 
| 601 | 
            +
                      when "slave"
         | 
| 602 | 
            +
                        resolve_slave
         | 
| 603 | 
            +
                      else
         | 
| 604 | 
            +
                        raise ArgumentError, "Unknown instance role #{@role}"
         | 
| 605 | 
            +
                      end
         | 
| 572 606 |  | 
| 573 607 | 
             
                      result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
         | 
| 574 608 | 
             
                    end
         | 
| @@ -576,11 +610,12 @@ class Redis | |
| 576 610 | 
             
                    def sentinel_detect
         | 
| 577 611 | 
             
                      @sentinels.each do |sentinel|
         | 
| 578 612 | 
             
                        client = Client.new(@options.merge({
         | 
| 579 | 
            -
             | 
| 580 | 
            -
             | 
| 581 | 
            -
             | 
| 582 | 
            -
             | 
| 583 | 
            -
             | 
| 613 | 
            +
                                                             host: sentinel[:host] || sentinel["host"],
         | 
| 614 | 
            +
                                                             port: sentinel[:port] || sentinel["port"],
         | 
| 615 | 
            +
                                                             username: sentinel[:username] || sentinel["username"],
         | 
| 616 | 
            +
                                                             password: sentinel[:password] || sentinel["password"],
         | 
| 617 | 
            +
                                                             reconnect_attempts: 0
         | 
| 618 | 
            +
                                                           }))
         | 
| 584 619 |  | 
| 585 620 | 
             
                        begin
         | 
| 586 621 | 
             
                          if result = yield(client)
         | 
| @@ -602,7 +637,7 @@ class Redis | |
| 602 637 | 
             
                    def resolve_master
         | 
| 603 638 | 
             
                      sentinel_detect do |client|
         | 
| 604 639 | 
             
                        if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
         | 
| 605 | 
            -
                          {: | 
| 640 | 
            +
                          { host: reply[0], port: reply[1] }
         | 
| 606 641 | 
             
                        end
         | 
| 607 642 | 
             
                      end
         | 
| 608 643 | 
             
                    end
         | 
| @@ -620,7 +655,7 @@ class Redis | |
| 620 655 | 
             
                            slave = slaves.sample
         | 
| 621 656 | 
             
                            {
         | 
| 622 657 | 
             
                              host: slave.fetch('ip'),
         | 
| 623 | 
            -
                              port: slave.fetch('port') | 
| 658 | 
            +
                              port: slave.fetch('port')
         | 
| 624 659 | 
             
                            }
         | 
| 625 660 | 
             
                          end
         | 
| 626 661 | 
             
                        end
         | 
| @@ -31,13 +31,13 @@ class Redis | |
| 31 31 | 
             
                  private
         | 
| 32 32 |  | 
| 33 33 | 
             
                  def pick_details(details)
         | 
| 34 | 
            -
                    details. | 
| 35 | 
            -
                       | 
| 34 | 
            +
                    details.transform_values do |detail|
         | 
| 35 | 
            +
                      {
         | 
| 36 36 | 
             
                        first_key_position: detail[:first],
         | 
| 37 37 | 
             
                        write: detail[:flags].include?('write'),
         | 
| 38 38 | 
             
                        readonly: detail[:flags].include?('readonly')
         | 
| 39 | 
            -
                      } | 
| 40 | 
            -
                    end | 
| 39 | 
            +
                      }
         | 
| 40 | 
            +
                    end
         | 
| 41 41 | 
             
                  end
         | 
| 42 42 |  | 
| 43 43 | 
             
                  def dig_details(command, key)
         | 
| @@ -53,8 +53,6 @@ class Redis | |
| 53 53 | 
             
                    when 'object' then 2
         | 
| 54 54 | 
             
                    when 'memory'
         | 
| 55 55 | 
             
                      command[1].to_s.casecmp('usage').zero? ? 2 : 0
         | 
| 56 | 
            -
                    when 'scan', 'sscan', 'hscan', 'zscan'
         | 
| 57 | 
            -
                      determine_optional_key_position(command, 'match')
         | 
| 58 56 | 
             
                    when 'xread', 'xreadgroup'
         | 
| 59 57 | 
             
                      determine_optional_key_position(command, 'streams')
         | 
| 60 58 | 
             
                    else
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
             | 
| 3 | 
            +
            require 'redis/errors'
         | 
| 4 4 |  | 
| 5 5 | 
             
            class Redis
         | 
| 6 6 | 
             
              class Cluster
         | 
| @@ -10,22 +10,21 @@ class Redis | |
| 10 10 | 
             
                  module_function
         | 
| 11 11 |  | 
| 12 12 | 
             
                  def load(nodes)
         | 
| 13 | 
            -
                     | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 16 | 
            -
                       | 
| 17 | 
            -
             | 
| 13 | 
            +
                    errors = nodes.map do |node|
         | 
| 14 | 
            +
                      begin
         | 
| 15 | 
            +
                        return fetch_command_details(node)
         | 
| 16 | 
            +
                      rescue CannotConnectError, ConnectionError, CommandError => error
         | 
| 17 | 
            +
                        error
         | 
| 18 | 
            +
                      end
         | 
| 18 19 | 
             
                    end
         | 
| 19 20 |  | 
| 20 | 
            -
                     | 
| 21 | 
            +
                    raise InitialSetupError, errors
         | 
| 21 22 | 
             
                  end
         | 
| 22 23 |  | 
| 23 24 | 
             
                  def fetch_command_details(node)
         | 
| 24 25 | 
             
                    node.call(%i[command]).map do |reply|
         | 
| 25 26 | 
             
                      [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
         | 
| 26 27 | 
             
                    end.to_h
         | 
| 27 | 
            -
                  rescue CannotConnectError, ConnectionError, CommandError
         | 
| 28 | 
            -
                    {} # can retry on another node
         | 
| 29 28 | 
             
                  end
         | 
| 30 29 |  | 
| 31 30 | 
             
                  private_class_method :fetch_command_details
         | 
    
        data/lib/redis/cluster/node.rb
    CHANGED
    
    | @@ -39,6 +39,7 @@ class Redis | |
| 39 39 | 
             
                  def call_master(command, &block)
         | 
| 40 40 | 
             
                    try_map do |node_key, client|
         | 
| 41 41 | 
             
                      next if slave?(node_key)
         | 
| 42 | 
            +
             | 
| 42 43 | 
             
                      client.call(command, &block)
         | 
| 43 44 | 
             
                    end.values
         | 
| 44 45 | 
             
                  end
         | 
| @@ -48,6 +49,7 @@ class Redis | |
| 48 49 |  | 
| 49 50 | 
             
                    try_map do |node_key, client|
         | 
| 50 51 | 
             
                      next if master?(node_key)
         | 
| 52 | 
            +
             | 
| 51 53 | 
             
                      client.call(command, &block)
         | 
| 52 54 | 
             
                    end.values
         | 
| 53 55 | 
             
                  end
         | 
| @@ -56,6 +58,18 @@ class Redis | |
| 56 58 | 
             
                    try_map { |_, client| client.process(commands, &block) }.values
         | 
| 57 59 | 
             
                  end
         | 
| 58 60 |  | 
| 61 | 
            +
                  def scale_reading_clients
         | 
| 62 | 
            +
                    reading_clients = []
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                    @clients.each do |node_key, client|
         | 
| 65 | 
            +
                      next unless replica_disabled? ? master?(node_key) : slave?(node_key)
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                      reading_clients << client
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    reading_clients
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
             | 
| 59 73 | 
             
                  private
         | 
| 60 74 |  | 
| 61 75 | 
             
                  def replica_disabled?
         | 
| @@ -74,8 +88,9 @@ class Redis | |
| 74 88 | 
             
                    clients = options.map do |node_key, option|
         | 
| 75 89 | 
             
                      next if replica_disabled? && slave?(node_key)
         | 
| 76 90 |  | 
| 91 | 
            +
                      option = option.merge(readonly: true) if slave?(node_key)
         | 
| 92 | 
            +
             | 
| 77 93 | 
             
                      client = Client.new(option)
         | 
| 78 | 
            -
                      client.call(%i[readonly]) if slave?(node_key)
         | 
| 79 94 | 
             
                      [node_key, client]
         | 
| 80 95 | 
             
                    end
         | 
| 81 96 |  | 
| @@ -97,6 +112,7 @@ class Redis | |
| 97 112 | 
             
                    end
         | 
| 98 113 |  | 
| 99 114 | 
             
                    return results if errors.empty?
         | 
| 115 | 
            +
             | 
| 100 116 | 
             
                    raise CommandErrorCollection, errors
         | 
| 101 117 | 
             
                  end
         | 
| 102 118 | 
             
                end
         | 
| @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            -
             | 
| 3 | 
            +
            require 'redis/errors'
         | 
| 4 4 |  | 
| 5 5 | 
             
            class Redis
         | 
| 6 6 | 
             
              class Cluster
         | 
| @@ -9,16 +9,15 @@ class Redis | |
| 9 9 | 
             
                  module_function
         | 
| 10 10 |  | 
| 11 11 | 
             
                  def load_flags(nodes)
         | 
| 12 | 
            -
                     | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 15 | 
            -
                       | 
| 16 | 
            -
             | 
| 12 | 
            +
                    errors = nodes.map do |node|
         | 
| 13 | 
            +
                      begin
         | 
| 14 | 
            +
                        return fetch_node_info(node)
         | 
| 15 | 
            +
                      rescue CannotConnectError, ConnectionError, CommandError => error
         | 
| 16 | 
            +
                        error
         | 
| 17 | 
            +
                      end
         | 
| 17 18 | 
             
                    end
         | 
| 18 19 |  | 
| 19 | 
            -
                     | 
| 20 | 
            -
             | 
| 21 | 
            -
                    raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
         | 
| 20 | 
            +
                    raise InitialSetupError, errors
         | 
| 22 21 | 
             
                  end
         | 
| 23 22 |  | 
| 24 23 | 
             
                  def fetch_node_info(node)
         | 
| @@ -27,8 +26,6 @@ class Redis | |
| 27 26 | 
             
                        .map { |str| str.split(' ') }
         | 
| 28 27 | 
             
                        .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
         | 
| 29 28 | 
             
                        .to_h
         | 
| 30 | 
            -
                  rescue CannotConnectError, ConnectionError, CommandError
         | 
| 31 | 
            -
                    {} # can retry on another node
         | 
| 32 29 | 
             
                  end
         | 
| 33 30 |  | 
| 34 31 | 
             
                  private_class_method :fetch_node_info
         | 
    
        data/lib/redis/cluster/option.rb
    CHANGED
    
    | @@ -17,14 +17,20 @@ class Redis | |
| 17 17 | 
             
                    node_addrs = options.delete(:cluster)
         | 
| 18 18 | 
             
                    @node_opts = build_node_options(node_addrs)
         | 
| 19 19 | 
             
                    @replica = options.delete(:replica) == true
         | 
| 20 | 
            +
                    @fixed_hostname = options.delete(:fixed_hostname)
         | 
| 20 21 | 
             
                    add_common_node_option_if_needed(options, @node_opts, :scheme)
         | 
| 22 | 
            +
                    add_common_node_option_if_needed(options, @node_opts, :username)
         | 
| 21 23 | 
             
                    add_common_node_option_if_needed(options, @node_opts, :password)
         | 
| 22 24 | 
             
                    @options = options
         | 
| 23 25 | 
             
                  end
         | 
| 24 26 |  | 
| 25 27 | 
             
                  def per_node_key
         | 
| 26 | 
            -
                    @node_opts.map  | 
| 27 | 
            -
             | 
| 28 | 
            +
                    @node_opts.map do |opt|
         | 
| 29 | 
            +
                      node_key = NodeKey.build_from_host_port(opt[:host], opt[:port])
         | 
| 30 | 
            +
                      options = @options.merge(opt)
         | 
| 31 | 
            +
                      options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
         | 
| 32 | 
            +
                      [node_key, options]
         | 
| 33 | 
            +
                    end.to_h
         | 
| 28 34 | 
             
                  end
         | 
| 29 35 |  | 
| 30 36 | 
             
                  def use_replica?
         | 
| @@ -43,6 +49,7 @@ class Redis | |
| 43 49 |  | 
| 44 50 | 
             
                  def build_node_options(addrs)
         | 
| 45 51 | 
             
                    raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
         | 
| 52 | 
            +
             | 
| 46 53 | 
             
                    addrs.map { |addr| parse_node_addr(addr) }
         | 
| 47 54 | 
             
                  end
         | 
| 48 55 |  | 
| @@ -62,21 +69,27 @@ class Redis | |
| 62 69 | 
             
                    raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
         | 
| 63 70 |  | 
| 64 71 | 
             
                    db = uri.path.split('/')[1]&.to_i
         | 
| 65 | 
            -
                     | 
| 72 | 
            +
                    username = uri.user ? URI.decode_www_form_component(uri.user) : nil
         | 
| 73 | 
            +
                    password = uri.password ? URI.decode_www_form_component(uri.password) : nil
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    { scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db }
         | 
| 76 | 
            +
                      .reject { |_, v| v.nil? || v == '' }
         | 
| 66 77 | 
             
                  rescue URI::InvalidURIError => err
         | 
| 67 78 | 
             
                    raise InvalidClientOptionError, err.message
         | 
| 68 79 | 
             
                  end
         | 
| 69 80 |  | 
| 70 81 | 
             
                  def parse_node_option(addr)
         | 
| 71 82 | 
             
                    addr = addr.map { |k, v| [k.to_sym, v] }.to_h
         | 
| 72 | 
            -
                     | 
| 83 | 
            +
                    if addr.values_at(:host, :port).any?(&:nil?)
         | 
| 84 | 
            +
                      raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
         | 
| 85 | 
            +
                    end
         | 
| 73 86 |  | 
| 74 87 | 
             
                    addr
         | 
| 75 88 | 
             
                  end
         | 
| 76 89 |  | 
| 77 90 | 
             
                  # Redis cluster node returns only host and port information.
         | 
| 78 91 | 
             
                  # So we should complement additional information such as:
         | 
| 79 | 
            -
                  #   scheme, password and so on.
         | 
| 92 | 
            +
                  #   scheme, username, password and so on.
         | 
| 80 93 | 
             
                  def add_common_node_option_if_needed(options, node_opts, key)
         | 
| 81 94 | 
             
                    return options if options[key].nil? && node_opts.first[key].nil?
         | 
| 82 95 |  |