redis-cluster-client 0.12.1 → 0.13.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/lib/redis_client/cluster/command.rb +45 -24
- data/lib/redis_client/cluster/concurrent_worker/on_demand.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker/pooled.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker.rb +4 -6
- data/lib/redis_client/cluster/errors.rb +7 -2
- data/lib/redis_client/cluster/node/latency_replica.rb +1 -1
- data/lib/redis_client/cluster/node.rb +50 -5
- data/lib/redis_client/cluster/optimistic_locking.rb +3 -3
- data/lib/redis_client/cluster/pipeline.rb +3 -3
- data/lib/redis_client/cluster/pub_sub.rb +15 -7
- data/lib/redis_client/cluster/router.rb +177 -79
- data/lib/redis_client/cluster/transaction.rb +5 -5
- data/lib/redis_client/cluster.rb +7 -6
- data/lib/redis_client/cluster_config.rb +10 -7
- metadata +2 -3
- data/lib/redis_client/cluster/normalized_cmd_name.rb +0 -71
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 2bc3348020af7012ab193ef18128f2cd37028c9225f1e7fa9555d3609ff92362
         | 
| 4 | 
            +
              data.tar.gz: 3c08890f1c50f297f0d60f0503bd268742bbcd242a32ac73df733496a013660e
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 8a4036ef8be3c29c747abc7deb59698fbf5d5e581ab9913fd3e06d530a9bae7eba13a397929bb9e3eaf611d5ee01960378ffef54bc6f69706aaf672bc9f38f89
         | 
| 7 | 
            +
              data.tar.gz: '08897fd0c13bb175399b9118783e812f20555e0e09d021cf24fab236168d8ffd5a3ad4599f2ba1db56ccebaf501f9319c605940069c963f54b2268e1acad5dae'
         | 
| @@ -3,7 +3,6 @@ | |
| 3 3 | 
             
            require 'redis_client'
         | 
| 4 4 | 
             
            require 'redis_client/cluster/errors'
         | 
| 5 5 | 
             
            require 'redis_client/cluster/key_slot_converter'
         | 
| 6 | 
            -
            require 'redis_client/cluster/normalized_cmd_name'
         | 
| 7 6 |  | 
| 8 7 | 
             
            class RedisClient
         | 
| 9 8 | 
             
              class Cluster
         | 
| @@ -30,7 +29,7 @@ class RedisClient | |
| 30 29 | 
             
                      nodes&.each do |node|
         | 
| 31 30 | 
             
                        regular_timeout = node.read_timeout
         | 
| 32 31 | 
             
                        node.read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
         | 
| 33 | 
            -
                        reply = node.call(' | 
| 32 | 
            +
                        reply = node.call('command')
         | 
| 34 33 | 
             
                        node.read_timeout = regular_timeout
         | 
| 35 34 | 
             
                        commands = parse_command_reply(reply)
         | 
| 36 35 | 
             
                        cmd = ::RedisClient::Cluster::Command.new(commands)
         | 
| @@ -47,14 +46,28 @@ class RedisClient | |
| 47 46 |  | 
| 48 47 | 
             
                    private
         | 
| 49 48 |  | 
| 50 | 
            -
                    def parse_command_reply(rows)
         | 
| 49 | 
            +
                    def parse_command_reply(rows) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
         | 
| 51 50 | 
             
                      rows&.each_with_object({}) do |row, acc|
         | 
| 52 | 
            -
                        next if row | 
| 51 | 
            +
                        next if row.first.nil?
         | 
| 53 52 |  | 
| 54 | 
            -
                         | 
| 55 | 
            -
             | 
| 53 | 
            +
                        # TODO: in redis 7.0 or later, subcommand information included in the command reply
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                        pos = case row.first
         | 
| 56 | 
            +
                              when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
         | 
| 57 | 
            +
                              when 'object', 'xgroup' then 2
         | 
| 58 | 
            +
                              when 'migrate', 'xread', 'xreadgroup' then 0
         | 
| 59 | 
            +
                              else row[3]
         | 
| 60 | 
            +
                              end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                        writable = case row.first
         | 
| 63 | 
            +
                                   when 'xgroup' then true
         | 
| 64 | 
            +
                                   else row[2].include?('write')
         | 
| 65 | 
            +
                                   end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                        acc[row.first] = ::RedisClient::Cluster::Command::Detail.new(
         | 
| 68 | 
            +
                          first_key_position: pos,
         | 
| 56 69 | 
             
                          key_step: row[5],
         | 
| 57 | 
            -
                          write?:  | 
| 70 | 
            +
                          write?: writable,
         | 
| 58 71 | 
             
                          readonly?: row[2].include?('readonly')
         | 
| 59 72 | 
             
                        )
         | 
| 60 73 | 
             
                      end.freeze || EMPTY_HASH
         | 
| @@ -73,39 +86,47 @@ class RedisClient | |
| 73 86 | 
             
                  end
         | 
| 74 87 |  | 
| 75 88 | 
             
                  def should_send_to_primary?(command)
         | 
| 76 | 
            -
                     | 
| 77 | 
            -
                    @commands[name]&.write?
         | 
| 89 | 
            +
                    find_command_info(command.first)&.write?
         | 
| 78 90 | 
             
                  end
         | 
| 79 91 |  | 
| 80 92 | 
             
                  def should_send_to_replica?(command)
         | 
| 81 | 
            -
                     | 
| 82 | 
            -
                    @commands[name]&.readonly?
         | 
| 93 | 
            +
                    find_command_info(command.first)&.readonly?
         | 
| 83 94 | 
             
                  end
         | 
| 84 95 |  | 
| 85 96 | 
             
                  def exists?(name)
         | 
| 86 | 
            -
                    @commands.key?( | 
| 97 | 
            +
                    @commands.key?(name) || @commands.key?(name.to_s.downcase(:ascii))
         | 
| 87 98 | 
             
                  end
         | 
| 88 99 |  | 
| 89 100 | 
             
                  private
         | 
| 90 101 |  | 
| 91 | 
            -
                  def  | 
| 92 | 
            -
                     | 
| 93 | 
            -
             | 
| 94 | 
            -
             | 
| 95 | 
            -
             | 
| 96 | 
            -
             | 
| 97 | 
            -
                     | 
| 98 | 
            -
             | 
| 99 | 
            -
                     | 
| 102 | 
            +
                  def find_command_info(name)
         | 
| 103 | 
            +
                    @commands[name] || @commands[name.to_s.downcase(:ascii)]
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
             | 
| 106 | 
            +
                  def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
         | 
| 107 | 
            +
                    i = find_command_info(command.first)&.first_key_position.to_i
         | 
| 108 | 
            +
                    return i if i > 0
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                    cmd_name = command.first
         | 
| 111 | 
            +
                    if cmd_name.casecmp('xread').zero?
         | 
| 112 | 
            +
                      determine_optional_key_position(command, 'streams')
         | 
| 113 | 
            +
                    elsif cmd_name.casecmp('xreadgroup').zero?
         | 
| 100 114 | 
             
                      determine_optional_key_position(command, 'streams')
         | 
| 115 | 
            +
                    elsif cmd_name.casecmp('migrate').zero?
         | 
| 116 | 
            +
                      command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
         | 
| 117 | 
            +
                    elsif cmd_name.casecmp('memory').zero?
         | 
| 118 | 
            +
                      command[1].to_s.casecmp('usage').zero? ? 2 : 0
         | 
| 101 119 | 
             
                    else
         | 
| 102 | 
            -
                       | 
| 120 | 
            +
                      i
         | 
| 103 121 | 
             
                    end
         | 
| 104 122 | 
             
                  end
         | 
| 105 123 |  | 
| 106 124 | 
             
                  def determine_optional_key_position(command, option_name)
         | 
| 107 | 
            -
                     | 
| 108 | 
            -
             | 
| 125 | 
            +
                    command.each_with_index do |e, i|
         | 
| 126 | 
            +
                      return i + 1 if e.to_s.downcase(:ascii) == option_name
         | 
| 127 | 
            +
                    end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                    0
         | 
| 109 130 | 
             
                  end
         | 
| 110 131 | 
             
                end
         | 
| 111 132 | 
             
              end
         | 
| @@ -71,14 +71,12 @@ class RedisClient | |
| 71 71 |  | 
| 72 72 | 
             
                  module_function
         | 
| 73 73 |  | 
| 74 | 
            -
                  def create(model: : | 
| 75 | 
            -
                    size = size.positive? ? size : 5
         | 
| 76 | 
            -
             | 
| 74 | 
            +
                  def create(model: :none, size: 5)
         | 
| 77 75 | 
             
                    case model
         | 
| 78 | 
            -
                    when :on_demand, nil then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
         | 
| 79 | 
            -
                    when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
         | 
| 80 76 | 
             
                    when :none then ::RedisClient::Cluster::ConcurrentWorker::None.new
         | 
| 81 | 
            -
                     | 
| 77 | 
            +
                    when :on_demand then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
         | 
| 78 | 
            +
                    when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
         | 
| 79 | 
            +
                    else raise ArgumentError, "unknown model: #{model}"
         | 
| 82 80 | 
             
                    end
         | 
| 83 81 | 
             
                  end
         | 
| 84 82 | 
             
                end
         | 
| @@ -43,11 +43,16 @@ class RedisClient | |
| 43 43 | 
             
                    if !errors.is_a?(Hash) || errors.empty?
         | 
| 44 44 | 
             
                      new(errors.to_s).with_errors(EMPTY_HASH)
         | 
| 45 45 | 
             
                    else
         | 
| 46 | 
            -
                      messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }
         | 
| 46 | 
            +
                      messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }.freeze
         | 
| 47 47 | 
             
                      new(messages.join(', ')).with_errors(errors)
         | 
| 48 48 | 
             
                    end
         | 
| 49 49 | 
             
                  end
         | 
| 50 50 |  | 
| 51 | 
            +
                  def initialize(error_message = nil)
         | 
| 52 | 
            +
                    @errors = nil
         | 
| 53 | 
            +
                    super
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 51 56 | 
             
                  def with_errors(errors)
         | 
| 52 57 | 
             
                    @errors = errors if @errors.nil?
         | 
| 53 58 | 
             
                    self
         | 
| @@ -61,7 +66,7 @@ class RedisClient | |
| 61 66 | 
             
                end
         | 
| 62 67 |  | 
| 63 68 | 
             
                class NodeMightBeDown < Error
         | 
| 64 | 
            -
                  def initialize( | 
| 69 | 
            +
                  def initialize(_error_message = nil)
         | 
| 65 70 | 
             
                    super(
         | 
| 66 71 | 
             
                      'The client is trying to fetch the latest cluster state ' \
         | 
| 67 72 | 
             
                      'because a subset of nodes might be down. ' \
         | 
| @@ -7,6 +7,7 @@ require 'redis_client/cluster/node/primary_only' | |
| 7 7 | 
             
            require 'redis_client/cluster/node/random_replica'
         | 
| 8 8 | 
             
            require 'redis_client/cluster/node/random_replica_or_primary'
         | 
| 9 9 | 
             
            require 'redis_client/cluster/node/latency_replica'
         | 
| 10 | 
            +
            require 'redis_client/cluster/node_key'
         | 
| 10 11 |  | 
| 11 12 | 
             
            class RedisClient
         | 
| 12 13 | 
             
              class Cluster
         | 
| @@ -43,6 +44,10 @@ class RedisClient | |
| 43 44 | 
             
                    def replica?
         | 
| 44 45 | 
             
                      role == 'slave'
         | 
| 45 46 | 
             
                    end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    def serialize(str)
         | 
| 49 | 
            +
                      str << id << node_key << role << primary_id << config_epoch
         | 
| 50 | 
            +
                    end
         | 
| 46 51 | 
             
                  end
         | 
| 47 52 |  | 
| 48 53 | 
             
                  class CharArray
         | 
| @@ -90,7 +95,7 @@ class RedisClient | |
| 90 95 |  | 
| 91 96 | 
             
                    def build_connection_prelude
         | 
| 92 97 | 
             
                      prelude = super.dup
         | 
| 93 | 
            -
                      prelude << [' | 
| 98 | 
            +
                      prelude << ['readonly'] if @scale_read
         | 
| 94 99 | 
             
                      prelude.freeze
         | 
| 95 100 | 
             
                    end
         | 
| 96 101 | 
             
                  end
         | 
| @@ -309,7 +314,7 @@ class RedisClient | |
| 309 314 | 
             
                      work_group.push(i, raw_client) do |client|
         | 
| 310 315 | 
             
                        regular_timeout = client.read_timeout
         | 
| 311 316 | 
             
                        client.read_timeout = @config.slow_command_timeout > 0.0 ? @config.slow_command_timeout : regular_timeout
         | 
| 312 | 
            -
                        reply = client.call_once(' | 
| 317 | 
            +
                        reply = client.call_once('cluster', 'nodes')
         | 
| 313 318 | 
             
                        client.read_timeout = regular_timeout
         | 
| 314 319 | 
             
                        parse_cluster_node_reply(reply)
         | 
| 315 320 | 
             
                      rescue StandardError => e
         | 
| @@ -338,9 +343,7 @@ class RedisClient | |
| 338 343 |  | 
| 339 344 | 
             
                    grouped = node_info_list.compact.group_by do |info_list|
         | 
| 340 345 | 
             
                      info_list.sort_by!(&:id)
         | 
| 341 | 
            -
                      info_list.each_with_object(String.new(capacity: 128 * info_list.size))  | 
| 342 | 
            -
                        a << e.id << e.node_key << e.role << e.primary_id << e.config_epoch
         | 
| 343 | 
            -
                      end
         | 
| 346 | 
            +
                      info_list.each_with_object(String.new(capacity: 128 * info_list.size)) { |e, a| e.serialize(a) }
         | 
| 344 347 | 
             
                    end
         | 
| 345 348 |  | 
| 346 349 | 
             
                    grouped.max_by { |_, v| v.size }[1].first
         | 
| @@ -375,6 +378,48 @@ class RedisClient | |
| 375 378 | 
             
                    end
         | 
| 376 379 | 
             
                  end
         | 
| 377 380 |  | 
| 381 | 
            +
                  def parse_cluster_slots_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         | 
| 382 | 
            +
                    reply.group_by { |e| e[2][2] }.each_with_object([]) do |(primary_id, group), acc|
         | 
| 383 | 
            +
                      slots = group.map { |e| e[0, 2] }.freeze
         | 
| 384 | 
            +
             | 
| 385 | 
            +
                      group.first[2..].each do |arr|
         | 
| 386 | 
            +
                        ip = arr[0]
         | 
| 387 | 
            +
                        next if ip.nil? || ip.empty? || ip == '?'
         | 
| 388 | 
            +
             | 
| 389 | 
            +
                        id = arr[2]
         | 
| 390 | 
            +
                        role = id == primary_id ? 'master' : 'slave'
         | 
| 391 | 
            +
                        acc << ::RedisClient::Cluster::Node::Info.new(
         | 
| 392 | 
            +
                          id: id,
         | 
| 393 | 
            +
                          node_key: NodeKey.build_from_host_port(ip, arr[1]),
         | 
| 394 | 
            +
                          role: role,
         | 
| 395 | 
            +
                          primary_id: role == 'master' ? nil : primary_id,
         | 
| 396 | 
            +
                          slots: role == 'master' ? slots : EMPTY_ARRAY
         | 
| 397 | 
            +
                        )
         | 
| 398 | 
            +
                      end
         | 
| 399 | 
            +
                    end.freeze
         | 
| 400 | 
            +
                  end
         | 
| 401 | 
            +
             | 
| 402 | 
            +
                  def parse_cluster_shards_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         | 
| 403 | 
            +
                    reply.each_with_object([]) do |shard, acc|
         | 
| 404 | 
            +
                      nodes = shard.fetch('nodes')
         | 
| 405 | 
            +
                      primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
         | 
| 406 | 
            +
             | 
| 407 | 
            +
                      nodes.each do |node|
         | 
| 408 | 
            +
                        ip = node.fetch('ip')
         | 
| 409 | 
            +
                        next if node.fetch('health') != 'online' || ip.nil? || ip.empty? || ip == '?'
         | 
| 410 | 
            +
             | 
| 411 | 
            +
                        role = node.fetch('role')
         | 
| 412 | 
            +
                        acc << ::RedisClient::Cluster::Node::Info.new(
         | 
| 413 | 
            +
                          id: node.fetch('id'),
         | 
| 414 | 
            +
                          node_key: NodeKey.build_from_host_port(ip, node['port'] || node['tls-port']),
         | 
| 415 | 
            +
                          role: role == 'master' ? role : 'slave',
         | 
| 416 | 
            +
                          primary_id: role == 'master' ? nil : primary_id,
         | 
| 417 | 
            +
                          slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
         | 
| 418 | 
            +
                        )
         | 
| 419 | 
            +
                      end
         | 
| 420 | 
            +
                    end.freeze
         | 
| 421 | 
            +
                  end
         | 
| 422 | 
            +
             | 
| 378 423 | 
             
                  # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
         | 
| 379 424 | 
             
                  # node_key should use hostname if present in CLUSTER NODES output.
         | 
| 380 425 | 
             
                  #
         | 
| @@ -18,15 +18,15 @@ class RedisClient | |
| 18 18 | 
             
                    handle_redirection(slot, retry_count: 1) do |nd|
         | 
| 19 19 | 
             
                      nd.with do |c|
         | 
| 20 20 | 
             
                        c.ensure_connected_cluster_scoped(retryable: false) do
         | 
| 21 | 
            -
                          c.call(' | 
| 22 | 
            -
                          c.call(' | 
| 21 | 
            +
                          c.call('asking') if @asking
         | 
| 22 | 
            +
                          c.call('watch', *keys)
         | 
| 23 23 | 
             
                          begin
         | 
| 24 24 | 
             
                            yield(c, slot, @asking)
         | 
| 25 25 | 
             
                          rescue ::RedisClient::ConnectionError
         | 
| 26 26 | 
             
                            # No need to unwatch on a connection error.
         | 
| 27 27 | 
             
                            raise
         | 
| 28 28 | 
             
                          rescue StandardError
         | 
| 29 | 
            -
                            c.call(' | 
| 29 | 
            +
                            c.call('unwatch')
         | 
| 30 30 | 
             
                            raise
         | 
| 31 31 | 
             
                          end
         | 
| 32 32 | 
             
                        rescue ::RedisClient::CommandError => e
         | 
| @@ -283,14 +283,14 @@ class RedisClient | |
| 283 283 | 
             
                    args = timeout.nil? ? [] : [timeout]
         | 
| 284 284 |  | 
| 285 285 | 
             
                    if block.nil?
         | 
| 286 | 
            -
                      @router. | 
| 286 | 
            +
                      @router.send_command_to_node(node, method, command, args)
         | 
| 287 287 | 
             
                    else
         | 
| 288 | 
            -
                      @router. | 
| 288 | 
            +
                      @router.send_command_to_node(node, method, command, args, &block)
         | 
| 289 289 | 
             
                    end
         | 
| 290 290 | 
             
                  end
         | 
| 291 291 |  | 
| 292 292 | 
             
                  def try_asking(node)
         | 
| 293 | 
            -
                    node.call(' | 
| 293 | 
            +
                    node.call('asking') == 'OK'
         | 
| 294 294 | 
             
                  rescue StandardError
         | 
| 295 295 | 
             
                    false
         | 
| 296 296 | 
             
                  end
         | 
| @@ -2,7 +2,6 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            require 'redis_client'
         | 
| 4 4 | 
             
            require 'redis_client/cluster/errors'
         | 
| 5 | 
            -
            require 'redis_client/cluster/normalized_cmd_name'
         | 
| 6 5 |  | 
| 7 6 | 
             
            class RedisClient
         | 
| 8 7 | 
             
              class Cluster
         | 
| @@ -108,12 +107,21 @@ class RedisClient | |
| 108 107 |  | 
| 109 108 | 
             
                  private
         | 
| 110 109 |  | 
| 111 | 
            -
                  def _call(command)
         | 
| 112 | 
            -
                     | 
| 113 | 
            -
             | 
| 114 | 
            -
                     | 
| 115 | 
            -
             | 
| 116 | 
            -
                     | 
| 110 | 
            +
                  def _call(command) # rubocop:disable Metrics/AbcSize
         | 
| 111 | 
            +
                    if command.first.casecmp('subscribe').zero?
         | 
| 112 | 
            +
                      call_to_single_state(command)
         | 
| 113 | 
            +
                    elsif command.first.casecmp('psubscribe').zero?
         | 
| 114 | 
            +
                      call_to_single_state(command)
         | 
| 115 | 
            +
                    elsif command.first.casecmp('ssubscribe').zero?
         | 
| 116 | 
            +
                      call_to_single_state(command)
         | 
| 117 | 
            +
                    elsif command.first.casecmp('unsubscribe').zero?
         | 
| 118 | 
            +
                      call_to_all_states(command)
         | 
| 119 | 
            +
                    elsif command.first.casecmp('punsubscribe').zero?
         | 
| 120 | 
            +
                      call_to_all_states(command)
         | 
| 121 | 
            +
                    elsif command.first.casecmp('sunsubscribe').zero?
         | 
| 122 | 
            +
                      call_for_sharded_states(command)
         | 
| 123 | 
            +
                    else
         | 
| 124 | 
            +
                      call_to_single_state(command)
         | 
| 117 125 | 
             
                    end
         | 
| 118 126 | 
             
                  end
         | 
| 119 127 |  | 
| @@ -7,7 +7,6 @@ require 'redis_client/cluster/errors' | |
| 7 7 | 
             
            require 'redis_client/cluster/key_slot_converter'
         | 
| 8 8 | 
             
            require 'redis_client/cluster/node'
         | 
| 9 9 | 
             
            require 'redis_client/cluster/node_key'
         | 
| 10 | 
            -
            require 'redis_client/cluster/normalized_cmd_name'
         | 
| 11 10 | 
             
            require 'redis_client/cluster/transaction'
         | 
| 12 11 | 
             
            require 'redis_client/cluster/optimistic_locking'
         | 
| 13 12 | 
             
            require 'redis_client/cluster/pipeline'
         | 
| @@ -23,6 +22,13 @@ class RedisClient | |
| 23 22 |  | 
| 24 23 | 
             
                  attr_reader :config
         | 
| 25 24 |  | 
| 25 | 
            +
                  Action = Struct.new(
         | 
| 26 | 
            +
                    'RedisCommandRoutingAction',
         | 
| 27 | 
            +
                    :method_name,
         | 
| 28 | 
            +
                    :reply_transformer,
         | 
| 29 | 
            +
                    keyword_init: true
         | 
| 30 | 
            +
                  )
         | 
| 31 | 
            +
             | 
| 26 32 | 
             
                  def initialize(config, concurrent_worker, pool: nil, **kwargs)
         | 
| 27 33 | 
             
                    @config = config
         | 
| 28 34 | 
             
                    @concurrent_worker = concurrent_worker
         | 
| @@ -32,42 +38,20 @@ class RedisClient | |
| 32 38 | 
             
                    @node.reload!
         | 
| 33 39 | 
             
                    @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
         | 
| 34 40 | 
             
                    @command_builder = @config.command_builder
         | 
| 41 | 
            +
                    @dedicated_actions = build_dedicated_actions
         | 
| 35 42 | 
             
                  rescue ::RedisClient::Cluster::InitialSetupError => e
         | 
| 36 43 | 
             
                    e.with_config(config)
         | 
| 37 44 | 
             
                    raise
         | 
| 38 45 | 
             
                  end
         | 
| 39 46 |  | 
| 40 47 | 
             
                  def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         | 
| 41 | 
            -
                     | 
| 42 | 
            -
             | 
| 43 | 
            -
                     | 
| 44 | 
            -
                     | 
| 45 | 
            -
             | 
| 46 | 
            -
                     | 
| 47 | 
            -
                     | 
| 48 | 
            -
                    when 'lastsave' then @node.call_all(method, command, args).sort_by(&:to_i).then(&TSF.call(block))
         | 
| 49 | 
            -
                    when 'role'     then @node.call_all(method, command, args, &block)
         | 
| 50 | 
            -
                    when 'config'   then send_config_command(method, command, args, &block)
         | 
| 51 | 
            -
                    when 'client'   then send_client_command(method, command, args, &block)
         | 
| 52 | 
            -
                    when 'cluster'  then send_cluster_command(method, command, args, &block)
         | 
| 53 | 
            -
                    when 'memory'   then send_memory_command(method, command, args, &block)
         | 
| 54 | 
            -
                    when 'script'   then send_script_command(method, command, args, &block)
         | 
| 55 | 
            -
                    when 'pubsub'   then send_pubsub_command(method, command, args, &block)
         | 
| 56 | 
            -
                    when 'watch'    then send_watch_command(command, &block)
         | 
| 57 | 
            -
                    when 'mset', 'mget', 'del'
         | 
| 58 | 
            -
                      send_multiple_keys_command(cmd, method, command, args, &block)
         | 
| 59 | 
            -
                    when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
         | 
| 60 | 
            -
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 61 | 
            -
                    when 'flushall', 'flushdb'
         | 
| 62 | 
            -
                      @node.call_primaries(method, command, args).first.then(&TSF.call(block))
         | 
| 63 | 
            -
                    when 'readonly', 'readwrite', 'shutdown'
         | 
| 64 | 
            -
                      raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(cmd).with_config(@config)
         | 
| 65 | 
            -
                    when 'discard', 'exec', 'multi', 'unwatch'
         | 
| 66 | 
            -
                      raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(cmd).with_config(@config)
         | 
| 67 | 
            -
                    else
         | 
| 68 | 
            -
                      node = assign_node(command)
         | 
| 69 | 
            -
                      try_send(node, method, command, args, &block)
         | 
| 70 | 
            -
                    end
         | 
| 48 | 
            +
                    return assign_node_and_send_command(method, command, args, &block) unless @dedicated_actions.key?(command.first)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    action = @dedicated_actions[command.first]
         | 
| 51 | 
            +
                    return send(action.method_name, method, command, args, &block) if action.reply_transformer.nil?
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    reply = send(action.method_name, method, command, args)
         | 
| 54 | 
            +
                    action.reply_transformer.call(reply).then(&TSF.call(block))
         | 
| 71 55 | 
             
                  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
         | 
| 72 56 | 
             
                    raise
         | 
| 73 57 | 
             
                  rescue ::RedisClient::Cluster::Node::ReloadNeeded
         | 
| @@ -93,7 +77,12 @@ class RedisClient | |
| 93 77 | 
             
                  end
         | 
| 94 78 |  | 
| 95 79 | 
             
                  # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
         | 
| 96 | 
            -
                  def  | 
| 80 | 
            +
                  def assign_node_and_send_command(method, command, args, retry_count: 3, &block)
         | 
| 81 | 
            +
                    node = assign_node(command)
         | 
| 82 | 
            +
                    send_command_to_node(node, method, command, args, retry_count: retry_count, &block)
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                  def send_command_to_node(node, method, command, args, retry_count: 3, &block)
         | 
| 97 86 | 
             
                    handle_redirection(node, command, retry_count: retry_count) do |on_node|
         | 
| 98 87 | 
             
                      if args.empty?
         | 
| 99 88 | 
             
                        # prevent memory allocation for variable-length args
         | 
| @@ -118,7 +107,7 @@ class RedisClient | |
| 118 107 | 
             
                    elsif e.message.start_with?('ASK')
         | 
| 119 108 | 
             
                      node = assign_asking_node(e.message)
         | 
| 120 109 | 
             
                      if retry_count >= 0
         | 
| 121 | 
            -
                        node.call(' | 
| 110 | 
            +
                        node.call('asking')
         | 
| 122 111 | 
             
                        retry
         | 
| 123 112 | 
             
                      end
         | 
| 124 113 | 
             
                    elsif e.message.start_with?('CLUSTERDOWN')
         | 
| @@ -268,6 +257,81 @@ class RedisClient | |
| 268 257 |  | 
| 269 258 | 
             
                  private
         | 
| 270 259 |  | 
| 260 | 
            +
                  def build_dedicated_actions # rubocop:disable Metrics/AbcSize
         | 
| 261 | 
            +
                    pick_first = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
         | 
| 262 | 
            +
                    multiple_key_action = Action.new(method_name: :send_multiple_keys_command)
         | 
| 263 | 
            +
                    all_node_first_action = Action.new(method_name: :send_command_to_all_nodes, reply_transformer: pick_first)
         | 
| 264 | 
            +
                    primary_first_action = Action.new(method_name: :send_command_to_primaries, reply_transformer: pick_first)
         | 
| 265 | 
            +
                    not_supported_action = Action.new(method_name: :fail_not_supported_command)
         | 
| 266 | 
            +
                    keyless_action = Action.new(method_name: :fail_keyless_command)
         | 
| 267 | 
            +
                    actions = {
         | 
| 268 | 
            +
                      'ping' => Action.new(method_name: :send_ping_command, reply_transformer: pick_first),
         | 
| 269 | 
            +
                      'wait' => Action.new(method_name: :send_wait_command),
         | 
| 270 | 
            +
                      'keys' => Action.new(method_name: :send_command_to_replicas, reply_transformer: ->(reply) { reply.flatten.sort_by(&:to_s) }),
         | 
| 271 | 
            +
                      'dbsize' => Action.new(method_name: :send_command_to_replicas, reply_transformer: ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }),
         | 
| 272 | 
            +
                      'scan' => Action.new(method_name: :send_scan_command),
         | 
| 273 | 
            +
                      'lastsave' => Action.new(method_name: :send_command_to_all_nodes, reply_transformer: ->(reply) { reply.sort_by(&:to_i) }),
         | 
| 274 | 
            +
                      'role' => Action.new(method_name: :send_command_to_all_nodes),
         | 
| 275 | 
            +
                      'config' => Action.new(method_name: :send_config_command),
         | 
| 276 | 
            +
                      'client' => Action.new(method_name: :send_client_command),
         | 
| 277 | 
            +
                      'cluster' => Action.new(method_name: :send_cluster_command),
         | 
| 278 | 
            +
                      'memory' => Action.new(method_name: :send_memory_command),
         | 
| 279 | 
            +
                      'script' => Action.new(method_name: :send_script_command),
         | 
| 280 | 
            +
                      'pubsub' => Action.new(method_name: :send_pubsub_command),
         | 
| 281 | 
            +
                      'watch' => Action.new(method_name: :send_watch_command),
         | 
| 282 | 
            +
                      'mget' => multiple_key_action,
         | 
| 283 | 
            +
                      'mset' => multiple_key_action,
         | 
| 284 | 
            +
                      'del' => multiple_key_action,
         | 
| 285 | 
            +
                      'acl' => all_node_first_action,
         | 
| 286 | 
            +
                      'auth' => all_node_first_action,
         | 
| 287 | 
            +
                      'bgrewriteaof' => all_node_first_action,
         | 
| 288 | 
            +
                      'bgsave' => all_node_first_action,
         | 
| 289 | 
            +
                      'quit' => all_node_first_action,
         | 
| 290 | 
            +
                      'save' => all_node_first_action,
         | 
| 291 | 
            +
                      'flushall' => primary_first_action,
         | 
| 292 | 
            +
                      'flushdb' => primary_first_action,
         | 
| 293 | 
            +
                      'readonly' => not_supported_action,
         | 
| 294 | 
            +
                      'readwrite' => not_supported_action,
         | 
| 295 | 
            +
                      'shutdown' => not_supported_action,
         | 
| 296 | 
            +
                      'discard' => keyless_action,
         | 
| 297 | 
            +
                      'exec' => keyless_action,
         | 
| 298 | 
            +
                      'multi' => keyless_action,
         | 
| 299 | 
            +
                      'unwatch' => keyless_action
         | 
| 300 | 
            +
                    }.freeze
         | 
| 301 | 
            +
                    actions.each_with_object({}) do |(k, v), acc|
         | 
| 302 | 
            +
                      acc[k] = v
         | 
| 303 | 
            +
                      acc[k.upcase] = v
         | 
| 304 | 
            +
                    end.freeze
         | 
| 305 | 
            +
                  end
         | 
| 306 | 
            +
             | 
| 307 | 
            +
                  def send_command_to_all_nodes(method, command, args, &block)
         | 
| 308 | 
            +
                    @node.call_all(method, command, args, &block)
         | 
| 309 | 
            +
                  end
         | 
| 310 | 
            +
             | 
| 311 | 
            +
                  def send_command_to_primaries(method, command, args, &block)
         | 
| 312 | 
            +
                    @node.call_primaries(method, command, args, &block)
         | 
| 313 | 
            +
                  end
         | 
| 314 | 
            +
             | 
| 315 | 
            +
                  def send_command_to_replicas(method, command, args, &block)
         | 
| 316 | 
            +
                    @node.call_replicas(method, command, args, &block)
         | 
| 317 | 
            +
                  end
         | 
| 318 | 
            +
             | 
| 319 | 
            +
                  def send_ping_command(method, command, args, &block)
         | 
| 320 | 
            +
                    @node.send_ping(method, command, args, &block)
         | 
| 321 | 
            +
                  end
         | 
| 322 | 
            +
             | 
| 323 | 
            +
                  def send_scan_command(_method, command, _args, &_block)
         | 
| 324 | 
            +
                    scan(command, seed: 1)
         | 
| 325 | 
            +
                  end
         | 
| 326 | 
            +
             | 
| 327 | 
            +
                  def fail_not_supported_command(_method, command, _args, &_block)
         | 
| 328 | 
            +
                    raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(command.first).with_config(@config)
         | 
| 329 | 
            +
                  end
         | 
| 330 | 
            +
             | 
| 331 | 
            +
                  def fail_keyless_command(_method, command, _args, &_block)
         | 
| 332 | 
            +
                    raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(command.first).with_config(@config)
         | 
| 333 | 
            +
                  end
         | 
| 334 | 
            +
             | 
| 271 335 | 
             
                  def send_wait_command(method, command, args, retry_count: 1, &block) # rubocop:disable Metrics/AbcSize
         | 
| 272 336 | 
             
                    @node.call_primaries(method, command, args).select { |r| r.is_a?(Integer) }.sum.then(&TSF.call(block))
         | 
| 273 337 | 
             
                  rescue ::RedisClient::Cluster::ErrorCollection => e
         | 
| @@ -280,79 +344,110 @@ class RedisClient | |
| 280 344 | 
             
                    retry
         | 
| 281 345 | 
             
                  end
         | 
| 282 346 |  | 
| 283 | 
            -
                  def send_config_command(method, command, args, &block)
         | 
| 284 | 
            -
                     | 
| 285 | 
            -
                    when 'resetstat', 'rewrite', 'set'
         | 
| 347 | 
            +
                  def send_config_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
         | 
| 348 | 
            +
                    if command[1].casecmp('resetstat').zero?
         | 
| 286 349 | 
             
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 287 | 
            -
                     | 
| 350 | 
            +
                    elsif command[1].casecmp('rewrite').zero?
         | 
| 351 | 
            +
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 352 | 
            +
                    elsif command[1].casecmp('set').zero?
         | 
| 353 | 
            +
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 354 | 
            +
                    else
         | 
| 355 | 
            +
                      assign_node(command).public_send(method, *args, command, &block)
         | 
| 288 356 | 
             
                    end
         | 
| 289 357 | 
             
                  end
         | 
| 290 358 |  | 
| 291 359 | 
             
                  def send_memory_command(method, command, args, &block)
         | 
| 292 | 
            -
                     | 
| 293 | 
            -
             | 
| 294 | 
            -
                     | 
| 295 | 
            -
             | 
| 360 | 
            +
                    if command[1].casecmp('stats').zero?
         | 
| 361 | 
            +
                      @node.call_all(method, command, args, &block)
         | 
| 362 | 
            +
                    elsif command[1].casecmp('purge').zero?
         | 
| 363 | 
            +
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 364 | 
            +
                    else
         | 
| 365 | 
            +
                      assign_node(command).public_send(method, *args, command, &block)
         | 
| 296 366 | 
             
                    end
         | 
| 297 367 | 
             
                  end
         | 
| 298 368 |  | 
| 299 | 
            -
                  def send_client_command(method, command, args, &block)
         | 
| 300 | 
            -
                     | 
| 301 | 
            -
             | 
| 302 | 
            -
                     | 
| 369 | 
            +
                  def send_client_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
         | 
| 370 | 
            +
                    if command[1].casecmp('list').zero?
         | 
| 371 | 
            +
                      @node.call_all(method, command, args, &block).flatten
         | 
| 372 | 
            +
                    elsif command[1].casecmp('pause').zero?
         | 
| 303 373 | 
             
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 304 | 
            -
                     | 
| 374 | 
            +
                    elsif command[1].casecmp('reply').zero?
         | 
| 375 | 
            +
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 376 | 
            +
                    elsif command[1].casecmp('setname').zero?
         | 
| 377 | 
            +
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 378 | 
            +
                    else
         | 
| 379 | 
            +
                      assign_node(command).public_send(method, *args, command, &block)
         | 
| 305 380 | 
             
                    end
         | 
| 306 381 | 
             
                  end
         | 
| 307 382 |  | 
| 308 | 
            -
                  def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
         | 
| 309 | 
            -
                     | 
| 310 | 
            -
             | 
| 311 | 
            -
             | 
| 312 | 
            -
                       | 
| 313 | 
            -
                     | 
| 314 | 
            -
             | 
| 383 | 
            +
                  def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         | 
| 384 | 
            +
                    if command[1].casecmp('addslots').zero?
         | 
| 385 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 386 | 
            +
                    elsif command[1].casecmp('delslots').zero?
         | 
| 387 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 388 | 
            +
                    elsif command[1].casecmp('failover').zero?
         | 
| 389 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 390 | 
            +
                    elsif command[1].casecmp('forget').zero?
         | 
| 391 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 392 | 
            +
                    elsif command[1].casecmp('meet').zero?
         | 
| 393 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 394 | 
            +
                    elsif command[1].casecmp('replicate').zero?
         | 
| 395 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 396 | 
            +
                    elsif command[1].casecmp('reset').zero?
         | 
| 397 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 398 | 
            +
                    elsif command[1].casecmp('set-config-epoch').zero?
         | 
| 399 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 400 | 
            +
                    elsif command[1].casecmp('setslot').zero?
         | 
| 401 | 
            +
                      fail_not_supported_command(method, command, args, &block)
         | 
| 402 | 
            +
                    elsif command[1].casecmp('saveconfig').zero?
         | 
| 403 | 
            +
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 404 | 
            +
                    elsif command[1].casecmp('getkeysinslot').zero?
         | 
| 315 405 | 
             
                      raise ArgumentError, command.join(' ') if command.size != 4
         | 
| 316 406 |  | 
| 317 407 | 
             
                      handle_node_reload_error do
         | 
| 318 408 | 
             
                        node_key = @node.find_node_key_of_replica(command[2])
         | 
| 319 409 | 
             
                        @node.find_by(node_key).public_send(method, *args, command, &block)
         | 
| 320 410 | 
             
                      end
         | 
| 321 | 
            -
                    else | 
| 411 | 
            +
                    else
         | 
| 412 | 
            +
                      assign_node(command).public_send(method, *args, command, &block)
         | 
| 322 413 | 
             
                    end
         | 
| 323 414 | 
             
                  end
         | 
| 324 415 |  | 
| 325 | 
            -
                  def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
         | 
| 326 | 
            -
                     | 
| 327 | 
            -
                    when 'debug', 'kill'
         | 
| 416 | 
            +
                  def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         | 
| 417 | 
            +
                    if command[1].casecmp('debug').zero?
         | 
| 328 418 | 
             
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 329 | 
            -
                     | 
| 419 | 
            +
                    elsif command[1].casecmp('kill').zero?
         | 
| 420 | 
            +
                      @node.call_all(method, command, args).first.then(&TSF.call(block))
         | 
| 421 | 
            +
                    elsif command[1].casecmp('flush').zero?
         | 
| 422 | 
            +
                      @node.call_primaries(method, command, args).first.then(&TSF.call(block))
         | 
| 423 | 
            +
                    elsif command[1].casecmp('load').zero?
         | 
| 330 424 | 
             
                      @node.call_primaries(method, command, args).first.then(&TSF.call(block))
         | 
| 331 | 
            -
                     | 
| 425 | 
            +
                    elsif command[1].casecmp('exists').zero?
         | 
| 332 426 | 
             
                      @node.call_all(method, command, args).transpose.map { |arr| arr.any?(&:zero?) ? 0 : 1 }.then(&TSF.call(block))
         | 
| 333 | 
            -
                    else | 
| 427 | 
            +
                    else
         | 
| 428 | 
            +
                      assign_node(command).public_send(method, *args, command, &block)
         | 
| 334 429 | 
             
                    end
         | 
| 335 430 | 
             
                  end
         | 
| 336 431 |  | 
| 337 432 | 
             
                  def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         | 
| 338 | 
            -
                     | 
| 339 | 
            -
                    when 'channels'
         | 
| 433 | 
            +
                    if command[1].casecmp('channels').zero?
         | 
| 340 434 | 
             
                      @node.call_all(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
         | 
| 341 | 
            -
                     | 
| 435 | 
            +
                    elsif command[1].casecmp('shardchannels').zero?
         | 
| 342 436 | 
             
                      @node.call_replicas(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
         | 
| 343 | 
            -
                     | 
| 437 | 
            +
                    elsif command[1].casecmp('numpat').zero?
         | 
| 344 438 | 
             
                      @node.call_all(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
         | 
| 345 | 
            -
                     | 
| 439 | 
            +
                    elsif command[1].casecmp('numsub').zero?
         | 
| 346 440 | 
             
                      @node.call_all(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
         | 
| 347 441 | 
             
                           .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
         | 
| 348 | 
            -
                     | 
| 442 | 
            +
                    elsif command[1].casecmp('shardnumsub').zero?
         | 
| 349 443 | 
             
                      @node.call_replicas(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
         | 
| 350 444 | 
             
                           .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
         | 
| 351 | 
            -
                    else | 
| 445 | 
            +
                    else
         | 
| 446 | 
            +
                      assign_node(command).public_send(method, *args, command, &block)
         | 
| 352 447 | 
             
                    end
         | 
| 353 448 | 
             
                  end
         | 
| 354 449 |  | 
| 355 | 
            -
                  def send_watch_command(command)
         | 
| 450 | 
            +
                  def send_watch_command(_method, command, _args, &_block)
         | 
| 356 451 | 
             
                    unless block_given?
         | 
| 357 452 | 
             
                      msg = 'A block required. And you need to use the block argument as a client for the transaction.'
         | 
| 358 453 | 
             
                      raise ::RedisClient::Cluster::Transaction::ConsistencyError.new(msg).with_config(@config)
         | 
| @@ -367,22 +462,23 @@ class RedisClient | |
| 367 462 | 
             
                    end
         | 
| 368 463 | 
             
                  end
         | 
| 369 464 |  | 
| 370 | 
            -
                  def send_multiple_keys_command( | 
| 465 | 
            +
                  def send_multiple_keys_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
         | 
| 371 466 | 
             
                    # This implementation is prioritized performance rather than readability or so.
         | 
| 372 | 
            -
                     | 
| 373 | 
            -
                     | 
| 467 | 
            +
                    cmd = command.first
         | 
| 468 | 
            +
                    if cmd.casecmp('mget').zero?
         | 
| 374 469 | 
             
                      single_key_cmd = 'get'
         | 
| 375 470 | 
             
                      keys_step = 1
         | 
| 376 | 
            -
                     | 
| 471 | 
            +
                    elsif cmd.casecmp('mset').zero?
         | 
| 377 472 | 
             
                      single_key_cmd = 'set'
         | 
| 378 473 | 
             
                      keys_step = 2
         | 
| 379 | 
            -
                     | 
| 474 | 
            +
                    elsif cmd.casecmp('del').zero?
         | 
| 380 475 | 
             
                      single_key_cmd = 'del'
         | 
| 381 476 | 
             
                      keys_step = 1
         | 
| 382 | 
            -
                    else | 
| 477 | 
            +
                    else
         | 
| 478 | 
            +
                      raise NotImplementedError, cmd
         | 
| 383 479 | 
             
                    end
         | 
| 384 480 |  | 
| 385 | 
            -
                    return  | 
| 481 | 
            +
                    return assign_node_and_send_command(method, command, args, &block) if command.size <= keys_step + 1 || ::RedisClient::Cluster::KeySlotConverter.hash_tag_included?(command[1])
         | 
| 386 482 |  | 
| 387 483 | 
             
                    seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
         | 
| 388 484 | 
             
                    pipeline = ::RedisClient::Cluster::Pipeline.new(self, @command_builder, @concurrent_worker, exception: true, seed: seed)
         | 
| @@ -402,10 +498,12 @@ class RedisClient | |
| 402 498 | 
             
                    end
         | 
| 403 499 |  | 
| 404 500 | 
             
                    replies = pipeline.execute
         | 
| 405 | 
            -
                    result =  | 
| 406 | 
            -
             | 
| 407 | 
            -
                              | 
| 408 | 
            -
             | 
| 501 | 
            +
                    result = if cmd.casecmp('mset').zero?
         | 
| 502 | 
            +
                               replies.first
         | 
| 503 | 
            +
                             elsif cmd.casecmp('del').zero?
         | 
| 504 | 
            +
                               replies.sum
         | 
| 505 | 
            +
                             else
         | 
| 506 | 
            +
                               replies
         | 
| 409 507 | 
             
                             end
         | 
| 410 508 | 
             
                    block_given? ? yield(result) : result
         | 
| 411 509 | 
             
                  end
         | 
| @@ -93,24 +93,24 @@ class RedisClient | |
| 93 93 | 
             
                  end
         | 
| 94 94 |  | 
| 95 95 | 
             
                  def prepare_tx
         | 
| 96 | 
            -
                    @pipeline.call(' | 
| 96 | 
            +
                    @pipeline.call('multi')
         | 
| 97 97 | 
             
                    @pending_commands.each(&:call)
         | 
| 98 98 | 
             
                    @pending_commands.clear
         | 
| 99 99 | 
             
                  end
         | 
| 100 100 |  | 
| 101 101 | 
             
                  def commit
         | 
| 102 | 
            -
                    @pipeline.call(' | 
| 102 | 
            +
                    @pipeline.call('exec')
         | 
| 103 103 | 
             
                    settle
         | 
| 104 104 | 
             
                  end
         | 
| 105 105 |  | 
| 106 106 | 
             
                  def cancel
         | 
| 107 | 
            -
                    @pipeline.call(' | 
| 107 | 
            +
                    @pipeline.call('discard')
         | 
| 108 108 | 
             
                    settle
         | 
| 109 109 | 
             
                  end
         | 
| 110 110 |  | 
| 111 111 | 
             
                  def settle
         | 
| 112 112 | 
             
                    # If we needed ASKING on the watch, we need ASKING on the multi as well.
         | 
| 113 | 
            -
                    @node.call(' | 
| 113 | 
            +
                    @node.call('asking') if @asking
         | 
| 114 114 | 
             
                    # Don't handle redirections at this level if we're in a watch (the watcher handles redirections
         | 
| 115 115 | 
             
                    # at the whole-transaction level.)
         | 
| 116 116 | 
             
                    send_transaction(@node, redirect: !!@watching_slot ? 0 : MAX_REDIRECTION)
         | 
| @@ -189,7 +189,7 @@ class RedisClient | |
| 189 189 | 
             
                  end
         | 
| 190 190 |  | 
| 191 191 | 
             
                  def try_asking(node)
         | 
| 192 | 
            -
                    node.call(' | 
| 192 | 
            +
                    node.call('asking') == 'OK'
         | 
| 193 193 | 
             
                  rescue StandardError
         | 
| 194 194 | 
             
                    false
         | 
| 195 195 | 
             
                  end
         | 
    
        data/lib/redis_client/cluster.rb
    CHANGED
    
    | @@ -64,7 +64,7 @@ class RedisClient | |
| 64 64 | 
             
                def scan(*args, **kwargs, &block)
         | 
| 65 65 | 
             
                  return to_enum(__callee__, *args, **kwargs) unless block_given?
         | 
| 66 66 |  | 
| 67 | 
            -
                  command = @command_builder.generate([' | 
| 67 | 
            +
                  command = @command_builder.generate(['scan', ZERO_CURSOR_FOR_SCAN] + args, kwargs)
         | 
| 68 68 | 
             
                  seed = Random.new_seed
         | 
| 69 69 | 
             
                  loop do
         | 
| 70 70 | 
             
                    cursor, keys = router.scan(command, seed: seed)
         | 
| @@ -77,21 +77,21 @@ class RedisClient | |
| 77 77 | 
             
                def sscan(key, *args, **kwargs, &block)
         | 
| 78 78 | 
             
                  return to_enum(__callee__, key, *args, **kwargs) unless block_given?
         | 
| 79 79 |  | 
| 80 | 
            -
                  command = @command_builder.generate([' | 
| 80 | 
            +
                  command = @command_builder.generate(['sscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
         | 
| 81 81 | 
             
                  router.scan_single_key(command, arity: 1, &block)
         | 
| 82 82 | 
             
                end
         | 
| 83 83 |  | 
| 84 84 | 
             
                def hscan(key, *args, **kwargs, &block)
         | 
| 85 85 | 
             
                  return to_enum(__callee__, key, *args, **kwargs) unless block_given?
         | 
| 86 86 |  | 
| 87 | 
            -
                  command = @command_builder.generate([' | 
| 87 | 
            +
                  command = @command_builder.generate(['hscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
         | 
| 88 88 | 
             
                  router.scan_single_key(command, arity: 2, &block)
         | 
| 89 89 | 
             
                end
         | 
| 90 90 |  | 
| 91 91 | 
             
                def zscan(key, *args, **kwargs, &block)
         | 
| 92 92 | 
             
                  return to_enum(__callee__, key, *args, **kwargs) unless block_given?
         | 
| 93 93 |  | 
| 94 | 
            -
                  command = @command_builder.generate([' | 
| 94 | 
            +
                  command = @command_builder.generate(['zscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
         | 
| 95 95 | 
             
                  router.scan_single_key(command, arity: 2, &block)
         | 
| 96 96 | 
             
                end
         | 
| 97 97 |  | 
| @@ -152,8 +152,9 @@ class RedisClient | |
| 152 152 | 
             
                end
         | 
| 153 153 |  | 
| 154 154 | 
             
                def method_missing(name, *args, **kwargs, &block)
         | 
| 155 | 
            -
                   | 
| 156 | 
            -
             | 
| 155 | 
            +
                  cmd = name.respond_to?(:name) ? name.name : name.to_s
         | 
| 156 | 
            +
                  if router.command_exists?(cmd)
         | 
| 157 | 
            +
                    args.unshift(cmd)
         | 
| 157 158 | 
             
                    command = @command_builder.generate(args, kwargs)
         | 
| 158 159 | 
             
                    return router.send_command(:call_v, command, &block)
         | 
| 159 160 | 
             
                  end
         | 
| @@ -19,7 +19,7 @@ class RedisClient | |
| 19 19 | 
             
                VALID_NODES_KEYS = %i[ssl username password host port db].freeze
         | 
| 20 20 | 
             
                MERGE_CONFIG_KEYS = %i[ssl username password].freeze
         | 
| 21 21 | 
             
                IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
         | 
| 22 | 
            -
                MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS',  | 
| 22 | 
            +
                MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', -1)) # for backward compatibility
         | 
| 23 23 | 
             
                # It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
         | 
| 24 24 | 
             
                SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
         | 
| 25 25 | 
             
                # It affects to strike a balance between load and stability in initialization or changed states.
         | 
| @@ -47,7 +47,6 @@ class RedisClient | |
| 47 47 | 
             
                  max_startup_sample: MAX_STARTUP_SAMPLE,
         | 
| 48 48 | 
             
                  **client_config
         | 
| 49 49 | 
             
                )
         | 
| 50 | 
            -
             | 
| 51 50 | 
             
                  @replica = true & replica
         | 
| 52 51 | 
             
                  @replica_affinity = replica_affinity.to_s.to_sym
         | 
| 53 52 | 
             
                  @fixed_hostname = fixed_hostname.to_s
         | 
| @@ -110,12 +109,16 @@ class RedisClient | |
| 110 109 | 
             
                private
         | 
| 111 110 |  | 
| 112 111 | 
             
                def merge_concurrency_option(option)
         | 
| 113 | 
            -
                   | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
                     | 
| 117 | 
            -
             | 
| 112 | 
            +
                  opts = {}
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                  if MAX_WORKERS.positive?
         | 
| 115 | 
            +
                    opts[:model] = :on_demand
         | 
| 116 | 
            +
                    opts[:size] = MAX_WORKERS
         | 
| 118 117 | 
             
                  end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                  opts.merge!(option.transform_keys(&:to_sym)) if option.is_a?(Hash)
         | 
| 120 | 
            +
                  opts[:model] = :none if opts.empty?
         | 
| 121 | 
            +
                  opts.freeze
         | 
| 119 122 | 
             
                end
         | 
| 120 123 |  | 
| 121 124 | 
             
                def build_node_configs(addrs)
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: redis-cluster-client
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.13.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Taishi Kasuga
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2024- | 
| 11 | 
            +
            date: 2024-12-08 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: redis-client
         | 
| @@ -49,7 +49,6 @@ files: | |
| 49 49 | 
             
            - lib/redis_client/cluster/node/random_replica_or_primary.rb
         | 
| 50 50 | 
             
            - lib/redis_client/cluster/node_key.rb
         | 
| 51 51 | 
             
            - lib/redis_client/cluster/noop_command_builder.rb
         | 
| 52 | 
            -
            - lib/redis_client/cluster/normalized_cmd_name.rb
         | 
| 53 52 | 
             
            - lib/redis_client/cluster/optimistic_locking.rb
         | 
| 54 53 | 
             
            - lib/redis_client/cluster/pipeline.rb
         | 
| 55 54 | 
             
            - lib/redis_client/cluster/pub_sub.rb
         | 
| @@ -1,71 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            require 'singleton'
         | 
| 4 | 
            -
             | 
| 5 | 
            -
            class RedisClient
         | 
| 6 | 
            -
              class Cluster
         | 
| 7 | 
            -
                class NormalizedCmdName
         | 
| 8 | 
            -
                  include Singleton
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                  EMPTY_STRING = ''
         | 
| 11 | 
            -
             | 
| 12 | 
            -
                  private_constant :EMPTY_STRING
         | 
| 13 | 
            -
             | 
| 14 | 
            -
                  def initialize
         | 
| 15 | 
            -
                    @cache = {}
         | 
| 16 | 
            -
                    @mutex = Mutex.new
         | 
| 17 | 
            -
                  end
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                  def get_by_command(command)
         | 
| 20 | 
            -
                    get(command, index: 0)
         | 
| 21 | 
            -
                  end
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                  def get_by_subcommand(command)
         | 
| 24 | 
            -
                    get(command, index: 1)
         | 
| 25 | 
            -
                  end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                  def get_by_name(name)
         | 
| 28 | 
            -
                    get(name, index: 0)
         | 
| 29 | 
            -
                  end
         | 
| 30 | 
            -
             | 
| 31 | 
            -
                  def clear
         | 
| 32 | 
            -
                    @mutex.synchronize { @cache.clear }
         | 
| 33 | 
            -
                    true
         | 
| 34 | 
            -
                  end
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                  private
         | 
| 37 | 
            -
             | 
| 38 | 
            -
                  def get(command, index:)
         | 
| 39 | 
            -
                    name = extract_name(command, index: index)
         | 
| 40 | 
            -
                    return EMPTY_STRING if name.nil? || name.empty?
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                    normalize(name)
         | 
| 43 | 
            -
                  end
         | 
| 44 | 
            -
             | 
| 45 | 
            -
                  def extract_name(command, index:)
         | 
| 46 | 
            -
                    case command
         | 
| 47 | 
            -
                    when String, Symbol then index.zero? ? command : nil
         | 
| 48 | 
            -
                    when Array then extract_name_from_array(command, index: index)
         | 
| 49 | 
            -
                    end
         | 
| 50 | 
            -
                  end
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                  def extract_name_from_array(command, index:)
         | 
| 53 | 
            -
                    return if command.size - 1 < index
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                    case e = command[index]
         | 
| 56 | 
            -
                    when String, Symbol then e
         | 
| 57 | 
            -
                    when Array then e[index]
         | 
| 58 | 
            -
                    end
         | 
| 59 | 
            -
                  end
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                  def normalize(name)
         | 
| 62 | 
            -
                    return @cache[name] || name.to_s.downcase if @cache.key?(name)
         | 
| 63 | 
            -
                    return name.to_s.downcase if @mutex.locked?
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                    str = name.to_s.downcase
         | 
| 66 | 
            -
                    @mutex.synchronize { @cache[name] = str }
         | 
| 67 | 
            -
                    str
         | 
| 68 | 
            -
                  end
         | 
| 69 | 
            -
                end
         | 
| 70 | 
            -
              end
         | 
| 71 | 
            -
            end
         |