redis 3.3.5 → 4.1.0
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 +54 -2
- data/README.md +77 -76
- data/lib/redis.rb +779 -63
- data/lib/redis/client.rb +41 -20
- data/lib/redis/cluster.rb +286 -0
- data/lib/redis/cluster/command.rb +81 -0
- data/lib/redis/cluster/command_loader.rb +34 -0
- data/lib/redis/cluster/key_slot_converter.rb +72 -0
- data/lib/redis/cluster/node.rb +104 -0
- data/lib/redis/cluster/node_key.rb +35 -0
- data/lib/redis/cluster/node_loader.rb +37 -0
- data/lib/redis/cluster/option.rb +77 -0
- data/lib/redis/cluster/slot.rb +69 -0
- data/lib/redis/cluster/slot_loader.rb +49 -0
- data/lib/redis/connection.rb +2 -2
- data/lib/redis/connection/command_helper.rb +2 -8
- data/lib/redis/connection/hiredis.rb +2 -2
- data/lib/redis/connection/ruby.rb +13 -30
- data/lib/redis/connection/synchrony.rb +12 -4
- data/lib/redis/distributed.rb +32 -12
- data/lib/redis/errors.rb +46 -0
- data/lib/redis/hash_ring.rb +20 -64
- data/lib/redis/pipeline.rb +9 -7
- data/lib/redis/version.rb +1 -1
- metadata +53 -196
- data/.gitignore +0 -16
- data/.travis.yml +0 -89
- data/.travis/Gemfile +0 -11
- data/.yardopts +0 -3
- data/Gemfile +0 -4
- data/Rakefile +0 -87
- data/benchmarking/logging.rb +0 -71
- data/benchmarking/pipeline.rb +0 -51
- data/benchmarking/speed.rb +0 -21
- data/benchmarking/suite.rb +0 -24
- data/benchmarking/worker.rb +0 -71
- data/examples/basic.rb +0 -15
- data/examples/consistency.rb +0 -114
- data/examples/dist_redis.rb +0 -43
- data/examples/incr-decr.rb +0 -17
- data/examples/list.rb +0 -26
- data/examples/pubsub.rb +0 -37
- data/examples/sentinel.rb +0 -41
- data/examples/sentinel/start +0 -49
- data/examples/sets.rb +0 -36
- data/examples/unicorn/config.ru +0 -3
- data/examples/unicorn/unicorn.rb +0 -20
- data/redis.gemspec +0 -44
- data/test/bitpos_test.rb +0 -69
- data/test/blocking_commands_test.rb +0 -42
- data/test/client_test.rb +0 -59
- data/test/command_map_test.rb +0 -30
- data/test/commands_on_hashes_test.rb +0 -21
- data/test/commands_on_hyper_log_log_test.rb +0 -21
- data/test/commands_on_lists_test.rb +0 -20
- data/test/commands_on_sets_test.rb +0 -77
- data/test/commands_on_sorted_sets_test.rb +0 -137
- data/test/commands_on_strings_test.rb +0 -101
- data/test/commands_on_value_types_test.rb +0 -133
- data/test/connection_handling_test.rb +0 -277
- data/test/connection_test.rb +0 -57
- data/test/distributed_blocking_commands_test.rb +0 -46
- data/test/distributed_commands_on_hashes_test.rb +0 -10
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
- data/test/distributed_commands_on_lists_test.rb +0 -22
- data/test/distributed_commands_on_sets_test.rb +0 -83
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
- data/test/distributed_commands_on_strings_test.rb +0 -59
- data/test/distributed_commands_on_value_types_test.rb +0 -95
- data/test/distributed_commands_requiring_clustering_test.rb +0 -164
- data/test/distributed_connection_handling_test.rb +0 -23
- data/test/distributed_internals_test.rb +0 -79
- data/test/distributed_key_tags_test.rb +0 -52
- data/test/distributed_persistence_control_commands_test.rb +0 -26
- data/test/distributed_publish_subscribe_test.rb +0 -92
- data/test/distributed_remote_server_control_commands_test.rb +0 -66
- data/test/distributed_scripting_test.rb +0 -102
- data/test/distributed_sorting_test.rb +0 -20
- data/test/distributed_test.rb +0 -58
- data/test/distributed_transactions_test.rb +0 -32
- data/test/encoding_test.rb +0 -18
- data/test/error_replies_test.rb +0 -59
- data/test/fork_safety_test.rb +0 -65
- data/test/helper.rb +0 -232
- data/test/helper_test.rb +0 -24
- data/test/internals_test.rb +0 -417
- data/test/lint/blocking_commands.rb +0 -150
- data/test/lint/hashes.rb +0 -162
- data/test/lint/hyper_log_log.rb +0 -60
- data/test/lint/lists.rb +0 -143
- data/test/lint/sets.rb +0 -140
- data/test/lint/sorted_sets.rb +0 -316
- data/test/lint/strings.rb +0 -260
- data/test/lint/value_types.rb +0 -122
- data/test/persistence_control_commands_test.rb +0 -26
- data/test/pipelining_commands_test.rb +0 -242
- data/test/publish_subscribe_test.rb +0 -282
- data/test/remote_server_control_commands_test.rb +0 -118
- data/test/scanning_test.rb +0 -413
- data/test/scripting_test.rb +0 -78
- data/test/sentinel_command_test.rb +0 -80
- data/test/sentinel_test.rb +0 -255
- data/test/sorting_test.rb +0 -59
- data/test/ssl_test.rb +0 -73
- data/test/support/connection/hiredis.rb +0 -1
- data/test/support/connection/ruby.rb +0 -1
- data/test/support/connection/synchrony.rb +0 -17
- data/test/support/redis_mock.rb +0 -130
- data/test/support/ssl/gen_certs.sh +0 -31
- data/test/support/ssl/trusted-ca.crt +0 -25
- data/test/support/ssl/trusted-ca.key +0 -27
- data/test/support/ssl/trusted-cert.crt +0 -81
- data/test/support/ssl/trusted-cert.key +0 -28
- data/test/support/ssl/untrusted-ca.crt +0 -26
- data/test/support/ssl/untrusted-ca.key +0 -27
- data/test/support/ssl/untrusted-cert.crt +0 -82
- data/test/support/ssl/untrusted-cert.key +0 -28
- data/test/support/wire/synchrony.rb +0 -24
- data/test/support/wire/thread.rb +0 -5
- data/test/synchrony_driver.rb +0 -88
- data/test/test.conf.erb +0 -9
- data/test/thread_safety_test.rb +0 -62
- data/test/transactions_test.rb +0 -264
- data/test/unknown_commands_test.rb +0 -14
- data/test/url_param_test.rb +0 -138
    
        data/lib/redis/client.rb
    CHANGED
    
    | @@ -1,4 +1,4 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            require_relative "errors"
         | 
| 2 2 | 
             
            require "socket"
         | 
| 3 3 | 
             
            require "cgi"
         | 
| 4 4 |  | 
| @@ -18,12 +18,12 @@ class Redis | |
| 18 18 | 
             
                  :id => nil,
         | 
| 19 19 | 
             
                  :tcp_keepalive => 0,
         | 
| 20 20 | 
             
                  :reconnect_attempts => 1,
         | 
| 21 | 
            +
                  :reconnect_delay => 0,
         | 
| 22 | 
            +
                  :reconnect_delay_max => 0.5,
         | 
| 21 23 | 
             
                  :inherit_socket => false
         | 
| 22 24 | 
             
                }
         | 
| 23 25 |  | 
| 24 | 
            -
                 | 
| 25 | 
            -
                  Marshal.load(Marshal.dump(@options))
         | 
| 26 | 
            -
                end
         | 
| 26 | 
            +
                attr_reader :options
         | 
| 27 27 |  | 
| 28 28 | 
             
                def scheme
         | 
| 29 29 | 
             
                  @options[:scheme]
         | 
| @@ -86,11 +86,14 @@ class Redis | |
| 86 86 |  | 
| 87 87 | 
             
                  @pending_reads = 0
         | 
| 88 88 |  | 
| 89 | 
            -
                   | 
| 90 | 
            -
                     | 
| 91 | 
            -
             | 
| 92 | 
            -
                     | 
| 93 | 
            -
             | 
| 89 | 
            +
                  @connector =
         | 
| 90 | 
            +
                    if options.include?(:sentinels)
         | 
| 91 | 
            +
                      Connector::Sentinel.new(@options)
         | 
| 92 | 
            +
                    elsif options.include?(:connector) && options[:connector].respond_to?(:new)
         | 
| 93 | 
            +
                      options.delete(:connector).new(@options)
         | 
| 94 | 
            +
                    else
         | 
| 95 | 
            +
                      Connector.new(@options)
         | 
| 96 | 
            +
                    end
         | 
| 94 97 | 
             
                end
         | 
| 95 98 |  | 
| 96 99 | 
             
                def connect
         | 
| @@ -152,9 +155,12 @@ class Redis | |
| 152 155 | 
             
                end
         | 
| 153 156 |  | 
| 154 157 | 
             
                def call_pipeline(pipeline)
         | 
| 158 | 
            +
                  commands = pipeline.commands
         | 
| 159 | 
            +
                  return [] if commands.empty?
         | 
| 160 | 
            +
             | 
| 155 161 | 
             
                  with_reconnect pipeline.with_reconnect? do
         | 
| 156 162 | 
             
                    begin
         | 
| 157 | 
            -
                      pipeline.finish(call_pipelined( | 
| 163 | 
            +
                      pipeline.finish(call_pipelined(commands)).tap do
         | 
| 158 164 | 
             
                        self.db = pipeline.db if pipeline.db
         | 
| 159 165 | 
             
                      end
         | 
| 160 166 | 
             
                    rescue ConnectionError => e
         | 
| @@ -185,13 +191,10 @@ class Redis | |
| 185 191 | 
             
                    exception = nil
         | 
| 186 192 |  | 
| 187 193 | 
             
                    process(commands) do
         | 
| 188 | 
            -
                       | 
| 189 | 
            -
             | 
| 190 | 
            -
                      @reconnect = false
         | 
| 191 | 
            -
             | 
| 192 | 
            -
                      (commands.size - 1).times do |i|
         | 
| 194 | 
            +
                      commands.size.times do |i|
         | 
| 193 195 | 
             
                        reply = read
         | 
| 194 | 
            -
                        result[i | 
| 196 | 
            +
                        result[i] = reply
         | 
| 197 | 
            +
                        @reconnect = false
         | 
| 195 198 | 
             
                        exception = reply if exception.nil? && reply.is_a?(CommandError)
         | 
| 196 199 | 
             
                      end
         | 
| 197 200 | 
             
                    end
         | 
| @@ -274,12 +277,15 @@ class Redis | |
| 274 277 |  | 
| 275 278 | 
             
                def with_socket_timeout(timeout)
         | 
| 276 279 | 
             
                  connect unless connected?
         | 
| 280 | 
            +
                  original = @options[:read_timeout]
         | 
| 277 281 |  | 
| 278 282 | 
             
                  begin
         | 
| 279 283 | 
             
                    connection.timeout = timeout
         | 
| 284 | 
            +
                    @options[:read_timeout] = timeout # for reconnection
         | 
| 280 285 | 
             
                    yield
         | 
| 281 286 | 
             
                  ensure
         | 
| 282 287 | 
             
                    connection.timeout = self.timeout if connected?
         | 
| 288 | 
            +
                    @options[:read_timeout] = original
         | 
| 283 289 | 
             
                  end
         | 
| 284 290 | 
             
                end
         | 
| 285 291 |  | 
| @@ -336,10 +342,12 @@ class Redis | |
| 336 342 | 
             
                  @connection = @options[:driver].connect(@options)
         | 
| 337 343 | 
             
                  @pending_reads = 0
         | 
| 338 344 | 
             
                rescue TimeoutError,
         | 
| 345 | 
            +
                       SocketError,
         | 
| 339 346 | 
             
                       Errno::ECONNREFUSED,
         | 
| 340 347 | 
             
                       Errno::EHOSTDOWN,
         | 
| 341 348 | 
             
                       Errno::EHOSTUNREACH,
         | 
| 342 349 | 
             
                       Errno::ENETUNREACH,
         | 
| 350 | 
            +
                       Errno::ENOENT,
         | 
| 343 351 | 
             
                       Errno::ETIMEDOUT
         | 
| 344 352 |  | 
| 345 353 | 
             
                  raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
         | 
| @@ -369,6 +377,10 @@ class Redis | |
| 369 377 | 
             
                    disconnect
         | 
| 370 378 |  | 
| 371 379 | 
             
                    if attempts <= @options[:reconnect_attempts] && @reconnect
         | 
| 380 | 
            +
                      sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
         | 
| 381 | 
            +
                                 @options[:reconnect_delay_max]].min
         | 
| 382 | 
            +
             | 
| 383 | 
            +
                      Kernel.sleep(sleep_t)
         | 
| 372 384 | 
             
                      retry
         | 
| 373 385 | 
             
                    else
         | 
| 374 386 | 
             
                      raise
         | 
| @@ -445,6 +457,10 @@ class Redis | |
| 445 457 | 
             
                  options[:read_timeout]    = Float(options[:read_timeout])
         | 
| 446 458 | 
             
                  options[:write_timeout]   = Float(options[:write_timeout])
         | 
| 447 459 |  | 
| 460 | 
            +
                  options[:reconnect_attempts] = options[:reconnect_attempts].to_i
         | 
| 461 | 
            +
                  options[:reconnect_delay] = options[:reconnect_delay].to_f
         | 
| 462 | 
            +
                  options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
         | 
| 463 | 
            +
             | 
| 448 464 | 
             
                  options[:db] = options[:db].to_i
         | 
| 449 465 | 
             
                  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
         | 
| 450 466 |  | 
| @@ -478,11 +494,16 @@ class Redis | |
| 478 494 |  | 
| 479 495 | 
             
                  if driver.kind_of?(String)
         | 
| 480 496 | 
             
                    begin
         | 
| 481 | 
            -
                       | 
| 482 | 
            -
             | 
| 483 | 
            -
             | 
| 484 | 
            -
             | 
| 497 | 
            +
                      require_relative "connection/#{driver}"
         | 
| 498 | 
            +
                    rescue LoadError, NameError => e
         | 
| 499 | 
            +
                      begin
         | 
| 500 | 
            +
                        require "connection/#{driver}"
         | 
| 501 | 
            +
                      rescue LoadError, NameError => e
         | 
| 502 | 
            +
                        raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
         | 
| 503 | 
            +
                      end
         | 
| 485 504 | 
             
                    end
         | 
| 505 | 
            +
             | 
| 506 | 
            +
                    driver = Connection.const_get(driver.capitalize)
         | 
| 486 507 | 
             
                  end
         | 
| 487 508 |  | 
| 488 509 | 
             
                  driver
         | 
| @@ -0,0 +1,286 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative 'errors'
         | 
| 4 | 
            +
            require_relative 'client'
         | 
| 5 | 
            +
            require_relative 'cluster/command'
         | 
| 6 | 
            +
            require_relative 'cluster/command_loader'
         | 
| 7 | 
            +
            require_relative 'cluster/key_slot_converter'
         | 
| 8 | 
            +
            require_relative 'cluster/node'
         | 
| 9 | 
            +
            require_relative 'cluster/node_key'
         | 
| 10 | 
            +
            require_relative 'cluster/node_loader'
         | 
| 11 | 
            +
            require_relative 'cluster/option'
         | 
| 12 | 
            +
            require_relative 'cluster/slot'
         | 
| 13 | 
            +
            require_relative 'cluster/slot_loader'
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            class Redis
         | 
| 16 | 
            +
              # Redis Cluster client
         | 
| 17 | 
            +
              #
         | 
| 18 | 
            +
              # @see https://github.com/antirez/redis-rb-cluster POC implementation
         | 
| 19 | 
            +
              # @see https://redis.io/topics/cluster-spec Redis Cluster specification
         | 
| 20 | 
            +
              # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial
         | 
| 21 | 
            +
              #
         | 
| 22 | 
            +
              # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
         | 
| 23 | 
            +
              class Cluster
         | 
| 24 | 
            +
                def initialize(options = {})
         | 
| 25 | 
            +
                  @option = Option.new(options)
         | 
| 26 | 
            +
                  @node, @slot = fetch_cluster_info!(@option)
         | 
| 27 | 
            +
                  @command = fetch_command_details(@node)
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def id
         | 
| 31 | 
            +
                  @node.map(&:id).sort.join(' ')
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                # db feature is disabled in cluster mode
         | 
| 35 | 
            +
                def db
         | 
| 36 | 
            +
                  0
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                # db feature is disabled in cluster mode
         | 
| 40 | 
            +
                def db=(_db); end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                def timeout
         | 
| 43 | 
            +
                  @node.first.timeout
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def connected?
         | 
| 47 | 
            +
                  @node.any?(&:connected?)
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                def disconnect
         | 
| 51 | 
            +
                  @node.each(&:disconnect)
         | 
| 52 | 
            +
                  true
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                def connection_info
         | 
| 56 | 
            +
                  @node.sort_by(&:id).map do |client|
         | 
| 57 | 
            +
                    {
         | 
| 58 | 
            +
                      host: client.host,
         | 
| 59 | 
            +
                      port: client.port,
         | 
| 60 | 
            +
                      db: client.db,
         | 
| 61 | 
            +
                      id: client.id,
         | 
| 62 | 
            +
                      location: client.location
         | 
| 63 | 
            +
                    }
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def with_reconnect(val = true, &block)
         | 
| 68 | 
            +
                  try_send(@node.sample, :with_reconnect, val, &block)
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                def call(command, &block)
         | 
| 72 | 
            +
                  send_command(command, &block)
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                def call_loop(command, timeout = 0, &block)
         | 
| 76 | 
            +
                  node = assign_node(command)
         | 
| 77 | 
            +
                  try_send(node, :call_loop, command, timeout, &block)
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                def call_pipeline(pipeline)
         | 
| 81 | 
            +
                  node_keys, command_keys = extract_keys_in_pipeline(pipeline)
         | 
| 82 | 
            +
                  raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
         | 
| 83 | 
            +
                  node = find_node(node_keys.first)
         | 
| 84 | 
            +
                  try_send(node, :call_pipeline, pipeline)
         | 
| 85 | 
            +
                end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                def call_with_timeout(command, timeout, &block)
         | 
| 88 | 
            +
                  node = assign_node(command)
         | 
| 89 | 
            +
                  try_send(node, :call_with_timeout, command, timeout, &block)
         | 
| 90 | 
            +
                end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                def call_without_timeout(command, &block)
         | 
| 93 | 
            +
                  call_with_timeout(command, 0, &block)
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def process(commands, &block)
         | 
| 97 | 
            +
                  if commands.size == 1 &&
         | 
| 98 | 
            +
                     %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
         | 
| 99 | 
            +
                     commands.first.size == 1
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    # Node is indeterminate. We do just a best-effort try here.
         | 
| 102 | 
            +
                    @node.process_all(commands, &block)
         | 
| 103 | 
            +
                  else
         | 
| 104 | 
            +
                    node = assign_node(commands.first)
         | 
| 105 | 
            +
                    try_send(node, :process, commands, &block)
         | 
| 106 | 
            +
                  end
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                private
         | 
| 110 | 
            +
             | 
| 111 | 
            +
                def fetch_cluster_info!(option)
         | 
| 112 | 
            +
                  node = Node.new(option.per_node_key)
         | 
| 113 | 
            +
                  available_slots = SlotLoader.load(node)
         | 
| 114 | 
            +
                  node_flags = NodeLoader.load_flags(node)
         | 
| 115 | 
            +
                  available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
         | 
| 116 | 
            +
                  option.update_node(available_node_urls)
         | 
| 117 | 
            +
                  [Node.new(option.per_node_key, node_flags, option.use_replica?),
         | 
| 118 | 
            +
                   Slot.new(available_slots, node_flags, option.use_replica?)]
         | 
| 119 | 
            +
                ensure
         | 
| 120 | 
            +
                  node.map(&:disconnect)
         | 
| 121 | 
            +
                end
         | 
| 122 | 
            +
             | 
| 123 | 
            +
                def fetch_command_details(nodes)
         | 
| 124 | 
            +
                  details = CommandLoader.load(nodes)
         | 
| 125 | 
            +
                  Command.new(details)
         | 
| 126 | 
            +
                end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                def send_command(command, &block)
         | 
| 129 | 
            +
                  cmd = command.first.to_s.downcase
         | 
| 130 | 
            +
                  case cmd
         | 
| 131 | 
            +
                  when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
         | 
| 132 | 
            +
                    @node.call_all(command, &block).first
         | 
| 133 | 
            +
                  when 'flushall', 'flushdb'
         | 
| 134 | 
            +
                    @node.call_master(command, &block).first
         | 
| 135 | 
            +
                  when 'wait'     then @node.call_master(command, &block).reduce(:+)
         | 
| 136 | 
            +
                  when 'keys'     then @node.call_slave(command, &block).flatten.sort
         | 
| 137 | 
            +
                  when 'dbsize'   then @node.call_slave(command, &block).reduce(:+)
         | 
| 138 | 
            +
                  when 'lastsave' then @node.call_all(command, &block).sort
         | 
| 139 | 
            +
                  when 'role'     then @node.call_all(command, &block)
         | 
| 140 | 
            +
                  when 'config'   then send_config_command(command, &block)
         | 
| 141 | 
            +
                  when 'client'   then send_client_command(command, &block)
         | 
| 142 | 
            +
                  when 'cluster'  then send_cluster_command(command, &block)
         | 
| 143 | 
            +
                  when 'readonly', 'readwrite', 'shutdown'
         | 
| 144 | 
            +
                    raise OrchestrationCommandNotSupported, cmd
         | 
| 145 | 
            +
                  when 'memory'   then send_memory_command(command, &block)
         | 
| 146 | 
            +
                  when 'script'   then send_script_command(command, &block)
         | 
| 147 | 
            +
                  when 'pubsub'   then send_pubsub_command(command, &block)
         | 
| 148 | 
            +
                  when 'discard', 'exec', 'multi', 'unwatch'
         | 
| 149 | 
            +
                    raise AmbiguousNodeError, cmd
         | 
| 150 | 
            +
                  else
         | 
| 151 | 
            +
                    node = assign_node(command)
         | 
| 152 | 
            +
                    try_send(node, :call, command, &block)
         | 
| 153 | 
            +
                  end
         | 
| 154 | 
            +
                end
         | 
| 155 | 
            +
             | 
| 156 | 
            +
                def send_config_command(command, &block)
         | 
| 157 | 
            +
                  case command[1].to_s.downcase
         | 
| 158 | 
            +
                  when 'resetstat', 'rewrite', 'set'
         | 
| 159 | 
            +
                    @node.call_all(command, &block).first
         | 
| 160 | 
            +
                  else assign_node(command).call(command, &block)
         | 
| 161 | 
            +
                  end
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                def send_memory_command(command, &block)
         | 
| 165 | 
            +
                  case command[1].to_s.downcase
         | 
| 166 | 
            +
                  when 'stats' then @node.call_all(command, &block)
         | 
| 167 | 
            +
                  when 'purge' then @node.call_all(command, &block).first
         | 
| 168 | 
            +
                  else assign_node(command).call(command, &block)
         | 
| 169 | 
            +
                  end
         | 
| 170 | 
            +
                end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
                def send_client_command(command, &block)
         | 
| 173 | 
            +
                  case command[1].to_s.downcase
         | 
| 174 | 
            +
                  when 'list' then @node.call_all(command, &block).flatten
         | 
| 175 | 
            +
                  when 'pause', 'reply', 'setname'
         | 
| 176 | 
            +
                    @node.call_all(command, &block).first
         | 
| 177 | 
            +
                  else assign_node(command).call(command, &block)
         | 
| 178 | 
            +
                  end
         | 
| 179 | 
            +
                end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
                def send_cluster_command(command, &block)
         | 
| 182 | 
            +
                  subcommand = command[1].to_s.downcase
         | 
| 183 | 
            +
                  case subcommand
         | 
| 184 | 
            +
                  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
         | 
| 185 | 
            +
                       'reset', 'set-config-epoch', 'setslot'
         | 
| 186 | 
            +
                    raise OrchestrationCommandNotSupported, 'cluster', subcommand
         | 
| 187 | 
            +
                  when 'saveconfig' then @node.call_all(command, &block).first
         | 
| 188 | 
            +
                  else assign_node(command).call(command, &block)
         | 
| 189 | 
            +
                  end
         | 
| 190 | 
            +
                end
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                def send_script_command(command, &block)
         | 
| 193 | 
            +
                  case command[1].to_s.downcase
         | 
| 194 | 
            +
                  when 'debug', 'kill'
         | 
| 195 | 
            +
                    @node.call_all(command, &block).first
         | 
| 196 | 
            +
                  when 'flush', 'load'
         | 
| 197 | 
            +
                    @node.call_master(command, &block).first
         | 
| 198 | 
            +
                  else assign_node(command).call(command, &block)
         | 
| 199 | 
            +
                  end
         | 
| 200 | 
            +
                end
         | 
| 201 | 
            +
             | 
| 202 | 
            +
                def send_pubsub_command(command, &block)
         | 
| 203 | 
            +
                  case command[1].to_s.downcase
         | 
| 204 | 
            +
                  when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
         | 
| 205 | 
            +
                  when 'numsub'
         | 
| 206 | 
            +
                    @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
         | 
| 207 | 
            +
                         .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
         | 
| 208 | 
            +
                  when 'numpat' then @node.call_all(command, &block).reduce(:+)
         | 
| 209 | 
            +
                  else assign_node(command).call(command, &block)
         | 
| 210 | 
            +
                  end
         | 
| 211 | 
            +
                end
         | 
| 212 | 
            +
             | 
| 213 | 
            +
                # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
         | 
| 214 | 
            +
                #   Redirection and resharding
         | 
| 215 | 
            +
                def try_send(node, method_name, *args, retry_count: 3, &block)
         | 
| 216 | 
            +
                  node.public_send(method_name, *args, &block)
         | 
| 217 | 
            +
                rescue CommandError => err
         | 
| 218 | 
            +
                  if err.message.start_with?('MOVED')
         | 
| 219 | 
            +
                    assign_redirection_node(err.message).public_send(method_name, *args, &block)
         | 
| 220 | 
            +
                  elsif err.message.start_with?('ASK')
         | 
| 221 | 
            +
                    raise if retry_count <= 0
         | 
| 222 | 
            +
                    node = assign_asking_node(err.message)
         | 
| 223 | 
            +
                    node.call(%i[asking])
         | 
| 224 | 
            +
                    retry_count -= 1
         | 
| 225 | 
            +
                    retry
         | 
| 226 | 
            +
                  else
         | 
| 227 | 
            +
                    raise
         | 
| 228 | 
            +
                  end
         | 
| 229 | 
            +
                end
         | 
| 230 | 
            +
             | 
| 231 | 
            +
                def assign_redirection_node(err_msg)
         | 
| 232 | 
            +
                  _, slot, node_key = err_msg.split(' ')
         | 
| 233 | 
            +
                  slot = slot.to_i
         | 
| 234 | 
            +
                  @slot.put(slot, node_key)
         | 
| 235 | 
            +
                  find_node(node_key)
         | 
| 236 | 
            +
                end
         | 
| 237 | 
            +
             | 
| 238 | 
            +
                def assign_asking_node(err_msg)
         | 
| 239 | 
            +
                  _, _, node_key = err_msg.split(' ')
         | 
| 240 | 
            +
                  find_node(node_key)
         | 
| 241 | 
            +
                end
         | 
| 242 | 
            +
             | 
| 243 | 
            +
                def assign_node(command)
         | 
| 244 | 
            +
                  node_key = find_node_key(command)
         | 
| 245 | 
            +
                  find_node(node_key)
         | 
| 246 | 
            +
                end
         | 
| 247 | 
            +
             | 
| 248 | 
            +
                def find_node_key(command)
         | 
| 249 | 
            +
                  key = @command.extract_first_key(command)
         | 
| 250 | 
            +
                  return if key.empty?
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                  slot = KeySlotConverter.convert(key)
         | 
| 253 | 
            +
                  return unless @slot.exists?(slot)
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                  if @command.should_send_to_master?(command)
         | 
| 256 | 
            +
                    @slot.find_node_key_of_master(slot)
         | 
| 257 | 
            +
                  else
         | 
| 258 | 
            +
                    @slot.find_node_key_of_slave(slot)
         | 
| 259 | 
            +
                  end
         | 
| 260 | 
            +
                end
         | 
| 261 | 
            +
             | 
| 262 | 
            +
                def find_node(node_key)
         | 
| 263 | 
            +
                  return @node.sample if node_key.nil?
         | 
| 264 | 
            +
                  @node.find_by(node_key)
         | 
| 265 | 
            +
                rescue Node::ReloadNeeded
         | 
| 266 | 
            +
                  update_cluster_info!(node_key)
         | 
| 267 | 
            +
                  @node.find_by(node_key)
         | 
| 268 | 
            +
                end
         | 
| 269 | 
            +
             | 
| 270 | 
            +
                def update_cluster_info!(node_key = nil)
         | 
| 271 | 
            +
                  unless node_key.nil?
         | 
| 272 | 
            +
                    host, port = NodeKey.split(node_key)
         | 
| 273 | 
            +
                    @option.add_node(host, port)
         | 
| 274 | 
            +
                  end
         | 
| 275 | 
            +
             | 
| 276 | 
            +
                  @node.map(&:disconnect)
         | 
| 277 | 
            +
                  @node, @slot = fetch_cluster_info!(@option)
         | 
| 278 | 
            +
                end
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                def extract_keys_in_pipeline(pipeline)
         | 
| 281 | 
            +
                  node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
         | 
| 282 | 
            +
                  command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
         | 
| 283 | 
            +
                  [node_keys, command_keys]
         | 
| 284 | 
            +
                end
         | 
| 285 | 
            +
              end
         | 
| 286 | 
            +
            end
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative '../errors'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            class Redis
         | 
| 6 | 
            +
              class Cluster
         | 
| 7 | 
            +
                # Keep details about Redis commands for Redis Cluster Client.
         | 
| 8 | 
            +
                # @see https://redis.io/commands/command
         | 
| 9 | 
            +
                class Command
         | 
| 10 | 
            +
                  def initialize(details)
         | 
| 11 | 
            +
                    @details = pick_details(details)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def extract_first_key(command)
         | 
| 15 | 
            +
                    i = determine_first_key_position(command)
         | 
| 16 | 
            +
                    return '' if i == 0
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    key = command[i].to_s
         | 
| 19 | 
            +
                    hash_tag = extract_hash_tag(key)
         | 
| 20 | 
            +
                    hash_tag.empty? ? key : hash_tag
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def should_send_to_master?(command)
         | 
| 24 | 
            +
                    dig_details(command, :write)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def should_send_to_slave?(command)
         | 
| 28 | 
            +
                    dig_details(command, :readonly)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  private
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  def pick_details(details)
         | 
| 34 | 
            +
                    details.map do |command, detail|
         | 
| 35 | 
            +
                      [command, {
         | 
| 36 | 
            +
                        first_key_position: detail[:first],
         | 
| 37 | 
            +
                        write: detail[:flags].include?('write'),
         | 
| 38 | 
            +
                        readonly: detail[:flags].include?('readonly')
         | 
| 39 | 
            +
                      }]
         | 
| 40 | 
            +
                    end.to_h
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def dig_details(command, key)
         | 
| 44 | 
            +
                    name = command.first.to_s
         | 
| 45 | 
            +
                    return unless @details.key?(name)
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    @details.fetch(name).fetch(key)
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                  def determine_first_key_position(command)
         | 
| 51 | 
            +
                    case command.first.to_s.downcase
         | 
| 52 | 
            +
                    when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
         | 
| 53 | 
            +
                    when 'object' then 2
         | 
| 54 | 
            +
                    when 'memory'
         | 
| 55 | 
            +
                      command[1].to_s.casecmp('usage').zero? ? 2 : 0
         | 
| 56 | 
            +
                    when 'scan', 'sscan', 'hscan', 'zscan'
         | 
| 57 | 
            +
                      determine_optional_key_position(command, 'match')
         | 
| 58 | 
            +
                    when 'xread', 'xreadgroup'
         | 
| 59 | 
            +
                      determine_optional_key_position(command, 'streams')
         | 
| 60 | 
            +
                    else
         | 
| 61 | 
            +
                      dig_details(command, :first_key_position).to_i
         | 
| 62 | 
            +
                    end
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def determine_optional_key_position(command, option_name)
         | 
| 66 | 
            +
                    idx = command.map(&:to_s).map(&:downcase).index(option_name)
         | 
| 67 | 
            +
                    idx.nil? ? 0 : idx + 1
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
         | 
| 71 | 
            +
                  def extract_hash_tag(key)
         | 
| 72 | 
            +
                    s = key.index('{')
         | 
| 73 | 
            +
                    e = key.index('}', s.to_i + 1)
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    return '' if s.nil? || e.nil?
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                    key[s + 1..e - 1]
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         |