From 54d82091aa0689fdb2d722f7d99205345b4b9cb5 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 15 Aug 2022 16:07:41 +0200 Subject: [PATCH] Update cluster tests --- .github/workflows/test.yaml | 5 +- Gemfile | 5 +- benchmarking/cluster_slot.rb | 36 --- bin/cluster_creator | 3 +- lib/redis.rb | 19 +- lib/redis/client.rb | 15 +- lib/redis/cluster.rb | 307 ---------------------- lib/redis/cluster/command.rb | 79 ------ lib/redis/cluster/command_loader.rb | 31 --- lib/redis/cluster/key_slot_converter.rb | 72 ----- lib/redis/cluster/node.rb | 118 --------- lib/redis/cluster/node_key.rb | 31 --- lib/redis/cluster/node_loader.rb | 32 --- lib/redis/cluster/option.rb | 100 ------- lib/redis/cluster/slot.rb | 86 ------ lib/redis/cluster/slot_loader.rb | 44 ---- lib/redis/cluster_client.rb | 85 ++++++ lib/redis/errors.rb | 15 -- makefile | 4 +- test/cluster/abnormal_state_test.rb | 1 + test/cluster/blocking_commands_test.rb | 2 +- test/cluster/client_internals_test.rb | 38 +-- test/cluster/client_key_hash_tags_test.rb | 105 -------- test/cluster/client_options_test.rb | 181 ------------- test/cluster/client_pipelining_test.rb | 15 +- test/cluster/client_slots_test.rb | 150 ----------- test/cluster/client_transactions_test.rb | 5 +- test/cluster/commands_on_cluster_test.rb | 8 - test/helper.rb | 5 +- test/lint/blocking_commands.rb | 44 ++-- test/lint/hashes.rb | 6 +- test/lint/streams.rb | 5 +- test/redis/blocking_commands_test.rb | 3 +- test/support/cluster/orchestrator.rb | 18 +- test/support/redis_mock.rb | 23 +- 35 files changed, 188 insertions(+), 1508 deletions(-) delete mode 100644 benchmarking/cluster_slot.rb delete mode 100644 lib/redis/cluster.rb delete mode 100644 lib/redis/cluster/command.rb delete mode 100644 lib/redis/cluster/command_loader.rb delete mode 100644 lib/redis/cluster/key_slot_converter.rb delete mode 100644 lib/redis/cluster/node.rb delete mode 100644 lib/redis/cluster/node_key.rb delete mode 100644 lib/redis/cluster/node_loader.rb delete mode 100644 lib/redis/cluster/option.rb delete mode 100644 lib/redis/cluster/slot.rb delete mode 100644 lib/redis/cluster/slot_loader.rb create mode 100644 lib/redis/cluster_client.rb delete mode 100644 test/cluster/client_key_hash_tags_test.rb delete mode 100644 test/cluster/client_options_test.rb delete mode 100644 test/cluster/client_slots_test.rb diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 3a1677c22..b96f105b9 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -197,6 +197,7 @@ jobs: LOW_TIMEOUT: "0.14" DRIVER: ruby REDIS_BRANCH: "7.0" + REDIS_CLUSTER: "true" steps: - name: Check out code uses: actions/checkout@v3 @@ -209,7 +210,7 @@ jobs: - name: Set up Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: "2.5" + ruby-version: "2.7" bundler-cache: true - name: Cache local temporary directory uses: actions/cache@v3 @@ -217,7 +218,7 @@ jobs: path: tmp key: "local-tmp-redis-7.0-on-ubuntu-latest" - name: Booting up Redis - run: make start_cluster + run: make start start_cluster create_cluster - name: Test run: bundle exec rake test:cluster - name: Shutting down Redis diff --git a/Gemfile b/Gemfile index 0a38b3e54..d42338aa1 100644 --- a/Gemfile +++ b/Gemfile @@ -9,5 +9,6 @@ gem 'rake' gem 'rubocop', '~> 1.25.1' gem 'mocha' -gem 'redis-client' -gem 'hiredis-client', platform: :ruby +gem 'redis-client', github: "redis-rb/redis-client" +gem 'hiredis-client', platform: :ruby, github: "redis-rb/redis-client" +gem 'redis-cluster-client', github: 'redis-rb/redis-cluster-client', branch: 'redis-rb-5.0' if ENV['REDIS_CLUSTER'] diff --git a/benchmarking/cluster_slot.rb b/benchmarking/cluster_slot.rb deleted file mode 100644 index a374aff86..000000000 --- a/benchmarking/cluster_slot.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'redis' -require 'benchmark' - -N = (ARGV.first || 100_000).to_i - -available_slots = { - "127.0.0.1:7000" => [0..5460], - "127.0.0.1:7003" => [0..5460], - "127.0.0.1:7001" => [5461..10_922], - "127.0.0.1:7004" => [5461..10_922], - "127.0.0.1:7002" => [10_923..16_383], - "127.0.0.1:7005" => [10_923..16_383] -} - -node_flags = { - "127.0.0.1:7000" => "master", - "127.0.0.1:7002" => "master", - "127.0.0.1:7001" => "master", - "127.0.0.1:7005" => "slave", - "127.0.0.1:7004" => "slave", - "127.0.0.1:7003" => "slave" -} - -Benchmark.bmbm do |bm| - bm.report('Slot.new') do - allocs = GC.stat(:total_allocated_objects) - - N.times do - Redis::Cluster::Slot.new(available_slots, node_flags, false) - end - - puts GC.stat(:total_allocated_objects) - allocs - end -end diff --git a/bin/cluster_creator b/bin/cluster_creator index 220bf55c4..a4d79353a 100755 --- a/bin/cluster_creator +++ b/bin/cluster_creator @@ -1,12 +1,13 @@ #!/usr/bin/env ruby # frozen_string_literal: true +puts ARGV.join(" ") require 'bundler/setup' $LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) require_relative '../test/support/cluster/orchestrator' urls = ARGV.map { |host_port| "redis://#{host_port}" } -orchestrator = ClusterOrchestrator.new(urls, timeout: 30.0) +orchestrator = ClusterOrchestrator.new(urls, timeout: 3.0) orchestrator.rebuild orchestrator.close diff --git a/lib/redis.rb b/lib/redis.rb index e5b6175fd..5423e47e5 100644 --- a/lib/redis.rb +++ b/lib/redis.rb @@ -8,6 +8,8 @@ class Redis BASE_PATH = __dir__ Deprecated = Class.new(StandardError) + autoload :ClusterClient, "redis/cluster_client" + class << self attr_accessor :silence_deprecations, :raise_deprecations @@ -67,7 +69,13 @@ def initialize(options = {}) @subscription_client = nil @client = if @cluster_mode = options.key?(:cluster) - Cluster.new(@options) + @options[:nodes] ||= @options.delete(:cluster) + cluster_config = RedisClient.cluster(**@options, protocol: 2, client_implementation: ClusterClient) + begin + cluster_config.new_client + rescue ::RedisClient::Error => error + raise ClusterClient::ERROR_MAPPING.fetch(error.class), error.message, error.backtrace + end elsif @options.key?(:sentinels) if url = @options.delete(:url) uri = URI.parse(url) @@ -86,7 +94,7 @@ def initialize(options = {}) Client.sentinel(**@options).new_client else - Client.new(@options) + Client.config(**@options).new_client end @client.inherit_socket! if inherit_socket end @@ -125,7 +133,7 @@ def pipelined end def id - @client.config.id || @client.config.server_url + @client.id || @client.server_url end def inspect @@ -137,7 +145,9 @@ def dup end def connection - return @client.connection_info if @cluster_mode + if @cluster_mode + raise NotImplementedError, "Redis::Cluster doesn't implement #connection" + end { host: @client.host, @@ -186,6 +196,5 @@ def _subscription(method, timeout, channels, block) require "redis/version" require "redis/client" -require "redis/cluster" require "redis/pipeline" require "redis/subscribe" diff --git a/lib/redis/client.rb b/lib/redis/client.rb index 8a32a7764..81c4cb21c 100644 --- a/lib/redis/client.rb +++ b/lib/redis/client.rb @@ -34,6 +34,10 @@ def initialize(*) end ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) + def id + config.id + end + def server_url config.server_url end @@ -68,6 +72,7 @@ def password undef_method :call undef_method :call_once + undef_method :call_once_v undef_method :blocking_call def call_v(command, &block) @@ -76,8 +81,9 @@ def call_v(command, &block) raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace end - def multi - super + def blocking_call_v(timeout, command, &block) + timeout += self.timeout if timeout && timeout > 0 + super(timeout, command, &block) rescue ::RedisClient::Error => error raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace end @@ -88,9 +94,8 @@ def pipelined raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace end - def blocking_call_v(timeout, command, &block) - timeout += self.timeout if timeout && timeout > 0 - super(timeout, command, &block) + def multi + super rescue ::RedisClient::Error => error raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace end diff --git a/lib/redis/cluster.rb b/lib/redis/cluster.rb deleted file mode 100644 index 07ce931dc..000000000 --- a/lib/redis/cluster.rb +++ /dev/null @@ -1,307 +0,0 @@ -# frozen_string_literal: true - -require 'redis/errors' -require 'redis/client' -require 'redis/cluster/command' -require 'redis/cluster/command_loader' -require 'redis/cluster/key_slot_converter' -require 'redis/cluster/node' -require 'redis/cluster/node_key' -require 'redis/cluster/node_loader' -require 'redis/cluster/option' -require 'redis/cluster/slot' -require 'redis/cluster/slot_loader' - -class Redis - # Redis Cluster client - # - # @see https://github.com/antirez/redis-rb-cluster POC implementation - # @see https://redis.io/topics/cluster-spec Redis Cluster specification - # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial - # - # Copyright (C) 2013 Salvatore Sanfilippo - class Cluster - def initialize(options = {}) - @option = Option.new(options) - @node, @slot = fetch_cluster_info!(@option) - @command = fetch_command_details(@node) - end - - def id - @node.map(&:id).sort.join(' ') - end - - def timeout - @node.first.timeout - end - - def connected? - @node.any?(&:connected?) - end - - def disconnect - @node.each(&:disconnect) - true - end - - def connection_info - @node.sort_by(&:id).map do |client| - { - host: client.host, - port: client.port, - db: client.db, - id: client.id, - location: client.location - } - end - end - - def with_reconnect(val = true, &block) - try_send(@node.sample, :with_reconnect, val, &block) - end - - def call(command, &block) - send_command(command, &block) - end - - def call_loop(command, timeout = 0, &block) - node = assign_node(command) - try_send(node, :call_loop, command, timeout, &block) - end - - def call_pipeline(pipeline) - node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq - if node_keys.size > 1 - raise(CrossSlotPipeliningError, - pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq) - end - - try_send(find_node(node_keys.first), :call_pipeline, pipeline) - end - - def call_with_timeout(command, timeout, &block) - node = assign_node(command) - try_send(node, :call_with_timeout, command, timeout, &block) - end - - def call_without_timeout(command, &block) - call_with_timeout(command, 0, &block) - end - - def process(commands, &block) - if commands.size == 1 && - %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) && - commands.first.size == 1 - - # Node is indeterminate. We do just a best-effort try here. - @node.process_all(commands, &block) - else - node = assign_node(commands.first) - try_send(node, :process, commands, &block) - end - end - - private - - def fetch_cluster_info!(option) - node = Node.new(option.per_node_key) - available_slots = SlotLoader.load(node) - node_flags = NodeLoader.load_flags(node) - option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) }) - [Node.new(option.per_node_key, node_flags, option.use_replica?), - Slot.new(available_slots, node_flags, option.use_replica?)] - ensure - node&.each(&:disconnect) - end - - def fetch_command_details(nodes) - details = CommandLoader.load(nodes) - Command.new(details) - end - - def send_command(command, &block) - cmd = command.first.to_s.downcase - case cmd - when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save' - @node.call_all(command, &block).first - when 'flushall', 'flushdb' - @node.call_master(command, &block).first - when 'wait' then @node.call_master(command, &block).reduce(:+) - when 'keys' then @node.call_slave(command, &block).flatten.sort - when 'dbsize' then @node.call_slave(command, &block).reduce(:+) - when 'scan' then _scan(command, &block) - when 'lastsave' then @node.call_all(command, &block).sort - when 'role' then @node.call_all(command, &block) - when 'config' then send_config_command(command, &block) - when 'client' then send_client_command(command, &block) - when 'cluster' then send_cluster_command(command, &block) - when 'readonly', 'readwrite', 'shutdown' - raise OrchestrationCommandNotSupported, cmd - when 'memory' then send_memory_command(command, &block) - when 'script' then send_script_command(command, &block) - when 'pubsub' then send_pubsub_command(command, &block) - when 'discard', 'exec', 'multi', 'unwatch' - raise AmbiguousNodeError, cmd - else - node = assign_node(command) - try_send(node, :call, command, &block) - end - end - - def send_config_command(command, &block) - case command[1].to_s.downcase - when 'resetstat', 'rewrite', 'set' - @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_memory_command(command, &block) - case command[1].to_s.downcase - when 'stats' then @node.call_all(command, &block) - when 'purge' then @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_client_command(command, &block) - case command[1].to_s.downcase - when 'list' then @node.call_all(command, &block).flatten - when 'pause', 'reply', 'setname' - @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_cluster_command(command, &block) - subcommand = command[1].to_s.downcase - case subcommand - when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate', - 'reset', 'set-config-epoch', 'setslot' - raise OrchestrationCommandNotSupported, 'cluster', subcommand - when 'saveconfig' then @node.call_all(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_script_command(command, &block) - case command[1].to_s.downcase - when 'debug', 'kill' - @node.call_all(command, &block).first - when 'flush', 'load' - @node.call_master(command, &block).first - else assign_node(command).call(command, &block) - end - end - - def send_pubsub_command(command, &block) - case command[1].to_s.downcase - when 'channels' then @node.call_all(command, &block).flatten.uniq.sort - when 'numsub' - @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] } - .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } } - when 'numpat' then @node.call_all(command, &block).reduce(:+) - else assign_node(command).call(command, &block) - end - end - - # @see https://redis.io/topics/cluster-spec#redirection-and-resharding - # Redirection and resharding - def try_send(node, method_name, *args, retry_count: 3, &block) - node.public_send(method_name, *args, &block) - rescue CommandError => err - if err.message.start_with?('MOVED') - raise if retry_count <= 0 - - node = assign_redirection_node(err.message) - retry_count -= 1 - retry - elsif err.message.start_with?('ASK') - raise if retry_count <= 0 - - node = assign_asking_node(err.message) - node.call(%i[asking]) - retry_count -= 1 - retry - else - raise - end - rescue CannotConnectError - update_cluster_info! - raise - end - - def _scan(command, &block) - input_cursor = Integer(command[1]) - - client_index = input_cursor % 256 - raw_cursor = input_cursor >> 8 - - clients = @node.scale_reading_clients - - client = clients[client_index] - return ['0', []] unless client - - command[1] = raw_cursor.to_s - - result_cursor, result_keys = client.call_v(command, &block) - result_cursor = Integer(result_cursor) - - if result_cursor == 0 - client_index += 1 - end - - [((result_cursor << 8) + client_index).to_s, result_keys] - end - - def assign_redirection_node(err_msg) - _, slot, node_key = err_msg.split(' ') - slot = slot.to_i - @slot.put(slot, node_key) - find_node(node_key) - end - - def assign_asking_node(err_msg) - _, _, node_key = err_msg.split(' ') - find_node(node_key) - end - - def assign_node(command) - node_key = find_node_key(command) - find_node(node_key) - end - - def find_node_key(command, primary_only: false) - key = @command.extract_first_key(command) - return if key.empty? - - slot = KeySlotConverter.convert(key) - return unless @slot.exists?(slot) - - if @command.should_send_to_master?(command) || primary_only - @slot.find_node_key_of_master(slot) - else - @slot.find_node_key_of_slave(slot) - end - end - - def find_node(node_key) - return @node.sample if node_key.nil? - - @node.find_by(node_key) - rescue Node::ReloadNeeded - update_cluster_info!(node_key) - @node.find_by(node_key) - end - - def update_cluster_info!(node_key = nil) - unless node_key.nil? - host, port = NodeKey.split(node_key) - @option.add_node(host, port) - end - - @node.map(&:disconnect) - @node, @slot = fetch_cluster_info!(@option) - end - end -end diff --git a/lib/redis/cluster/command.rb b/lib/redis/cluster/command.rb deleted file mode 100644 index 9386136e0..000000000 --- a/lib/redis/cluster/command.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require_relative '../errors' - -class Redis - class Cluster - # Keep details about Redis commands for Redis Cluster Client. - # @see https://redis.io/commands/command - class Command - def initialize(details) - @details = pick_details(details) - end - - def extract_first_key(command) - i = determine_first_key_position(command) - return '' if i == 0 - - key = command[i].to_s - hash_tag = extract_hash_tag(key) - hash_tag.empty? ? key : hash_tag - end - - def should_send_to_master?(command) - dig_details(command, :write) - end - - def should_send_to_slave?(command) - dig_details(command, :readonly) - end - - private - - def pick_details(details) - details.transform_values do |detail| - { - first_key_position: detail[:first], - write: detail[:flags].include?('write'), - readonly: detail[:flags].include?('readonly') - } - end - end - - def dig_details(command, key) - name = command.first.to_s - return unless @details.key?(name) - - @details.fetch(name).fetch(key) - end - - def determine_first_key_position(command) - case command.first.to_s.downcase - when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3 - when 'object' then 2 - when 'memory' - command[1].to_s.casecmp('usage').zero? ? 2 : 0 - when 'xread', 'xreadgroup' - determine_optional_key_position(command, 'streams') - else - dig_details(command, :first_key_position).to_i - end - end - - def determine_optional_key_position(command, option_name) - idx = command.map(&:to_s).map(&:downcase).index(option_name) - idx.nil? ? 0 : idx + 1 - end - - # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags - def extract_hash_tag(key) - s = key.index('{') - e = key.index('}', s.to_i + 1) - - return '' if s.nil? || e.nil? - - key[s + 1..e - 1] - end - end - end -end diff --git a/lib/redis/cluster/command_loader.rb b/lib/redis/cluster/command_loader.rb deleted file mode 100644 index b2811e2a4..000000000 --- a/lib/redis/cluster/command_loader.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'redis/errors' - -class Redis - class Cluster - # Load details about Redis commands for Redis Cluster Client - # @see https://redis.io/commands/command - module CommandLoader - module_function - - def load(nodes) - errors = nodes.map do |node| - return fetch_command_details(node) - rescue CannotConnectError, ConnectionError, CommandError => error - error - end - - raise InitialSetupError, errors - end - - def fetch_command_details(node) - node.call(%i[command]).map do |reply| - [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }] - end.to_h - end - - private_class_method :fetch_command_details - end - end -end diff --git a/lib/redis/cluster/key_slot_converter.rb b/lib/redis/cluster/key_slot_converter.rb deleted file mode 100644 index 72ae3f674..000000000 --- a/lib/redis/cluster/key_slot_converter.rb +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -class Redis - class Cluster - # Key to slot converter for Redis Cluster Client - # - # We can test it by `CLUSTER KEYSLOT` command. - # - # @see https://github.com/antirez/redis-rb-cluster - # Reference implementation in Ruby - # @see https://redis.io/topics/cluster-spec#appendix - # Reference implementation in ANSI C - # @see https://redis.io/commands/cluster-keyslot - # CLUSTER KEYSLOT command reference - # - # Copyright (C) 2013 Salvatore Sanfilippo - module KeySlotConverter - XMODEM_CRC16_LOOKUP = [ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 - ].freeze - - HASH_SLOTS = 16_384 - - module_function - - # Convert key into slot. - # - # @param key [String] the key of the redis command - # - # @return [Integer] slot number - def convert(key) - crc = 0 - key.each_byte do |b| - crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff] - end - - crc % HASH_SLOTS - end - end - end -end diff --git a/lib/redis/cluster/node.rb b/lib/redis/cluster/node.rb deleted file mode 100644 index 70f4f8a46..000000000 --- a/lib/redis/cluster/node.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -require_relative '../errors' - -class Redis - class Cluster - # Keep client list of node for Redis Cluster Client - class Node - include Enumerable - - ReloadNeeded = Class.new(StandardError) - - ROLE_SLAVE = 'slave' - - def initialize(options, node_flags = {}, with_replica = false) - @with_replica = with_replica - @node_flags = node_flags - @clients = build_clients(options) - end - - def each(&block) - @clients.values.each(&block) - end - - def sample - @clients.values.sample - end - - def find_by(node_key) - @clients.fetch(node_key) - rescue KeyError - raise ReloadNeeded - end - - def call_all(command, &block) - try_map { |_, client| client.call(command, &block) }.values - end - - def call_master(command, &block) - try_map do |node_key, client| - next if slave?(node_key) - - client.call(command, &block) - end.values - end - - def call_slave(command, &block) - return call_master(command, &block) if replica_disabled? - - try_map do |node_key, client| - next if master?(node_key) - - client.call(command, &block) - end.values - end - - def process_all(commands, &block) - try_map { |_, client| client.process(commands, &block) }.values - end - - def scale_reading_clients - reading_clients = [] - - @clients.each do |node_key, client| - next unless replica_disabled? ? master?(node_key) : slave?(node_key) - - reading_clients << client - end - - reading_clients - end - - private - - def replica_disabled? - !@with_replica - end - - def master?(node_key) - !slave?(node_key) - end - - def slave?(node_key) - @node_flags[node_key] == ROLE_SLAVE - end - - def build_clients(options) - clients = options.map do |node_key, option| - next if replica_disabled? && slave?(node_key) - - option = option.merge(readonly: true) if slave?(node_key) - - client = Client.new(option) - [node_key, client] - end - - clients.compact.to_h - end - - def try_map - errors = {} - results = {} - - @clients.each do |node_key, client| - reply = yield(node_key, client) - results[node_key] = reply unless reply.nil? - rescue CommandError => err - errors[node_key] = err - next - end - - return results if errors.empty? - - raise CommandErrorCollection, errors - end - end - end -end diff --git a/lib/redis/cluster/node_key.rb b/lib/redis/cluster/node_key.rb deleted file mode 100644 index f0e825a60..000000000 --- a/lib/redis/cluster/node_key.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -class Redis - class Cluster - # Node key's format is `:`. - # It is different from node id. - # Node id is internal identifying code in Redis Cluster. - module NodeKey - DELIMITER = ':' - - module_function - - def optionize(node_key) - host, port = split(node_key) - { host: host, port: port } - end - - def split(node_key) - node_key.split(DELIMITER) - end - - def build_from_uri(uri) - "#{uri.host}#{DELIMITER}#{uri.port}" - end - - def build_from_host_port(host, port) - "#{host}#{DELIMITER}#{port}" - end - end - end -end diff --git a/lib/redis/cluster/node_loader.rb b/lib/redis/cluster/node_loader.rb deleted file mode 100644 index 3194da28b..000000000 --- a/lib/redis/cluster/node_loader.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -require 'redis/errors' - -class Redis - class Cluster - # Load and hashify node info for Redis Cluster Client - module NodeLoader - module_function - - def load_flags(nodes) - errors = nodes.map do |node| - return fetch_node_info(node) - rescue CannotConnectError, ConnectionError, CommandError => error - error - end - - raise InitialSetupError, errors - end - - def fetch_node_info(node) - node.call(%i[cluster nodes]) - .split("\n") - .map { |str| str.split(' ') } - .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] } - .to_h - end - - private_class_method :fetch_node_info - end - end -end diff --git a/lib/redis/cluster/option.rb b/lib/redis/cluster/option.rb deleted file mode 100644 index 0a3468c41..000000000 --- a/lib/redis/cluster/option.rb +++ /dev/null @@ -1,100 +0,0 @@ -# frozen_string_literal: true - -require_relative '../errors' -require_relative 'node_key' -require 'uri' - -class Redis - class Cluster - # Keep options for Redis Cluster Client - class Option - DEFAULT_SCHEME = 'redis' - SECURE_SCHEME = 'rediss' - VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze - - def initialize(options) - options = options.dup - node_addrs = options.delete(:cluster) - @node_opts = build_node_options(node_addrs) - @replica = options.delete(:replica) == true - @fixed_hostname = options.delete(:fixed_hostname) - add_common_node_option_if_needed(options, @node_opts, :scheme) - add_common_node_option_if_needed(options, @node_opts, :username) - add_common_node_option_if_needed(options, @node_opts, :password) - @options = options - end - - def per_node_key - @node_opts.map do |opt| - node_key = NodeKey.build_from_host_port(opt[:host], opt[:port]) - options = @options.merge(opt) - options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty? - [node_key, options] - end.to_h - end - - def use_replica? - @replica - end - - def update_node(addrs) - @node_opts = build_node_options(addrs) - end - - def add_node(host, port) - @node_opts << { host: host, port: port } - end - - private - - def build_node_options(addrs) - raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array) - - addrs.map { |addr| parse_node_addr(addr) } - end - - def parse_node_addr(addr) - case addr - when String - parse_node_url(addr) - when Hash - parse_node_option(addr) - else - raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash' - end - end - - def parse_node_url(addr) - uri = URI(addr) - raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme) - - db = uri.path.split('/')[1]&.to_i - username = uri.user ? URI.decode_www_form_component(uri.user) : nil - password = uri.password ? URI.decode_www_form_component(uri.password) : nil - - { scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db } - .reject { |_, v| v.nil? || v == '' } - rescue URI::InvalidURIError => err - raise InvalidClientOptionError, err.message - end - - def parse_node_option(addr) - addr = addr.to_h { |k, v| [k.to_sym, v] } - if addr.values_at(:host, :port).any?(&:nil?) - raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' - end - - addr - end - - # Redis cluster node returns only host and port information. - # So we should complement additional information such as: - # scheme, username, password and so on. - def add_common_node_option_if_needed(options, node_opts, key) - return options if options[key].nil? && node_opts.first[key].nil? - - options[key] ||= node_opts.first[key] - end - end - end -end diff --git a/lib/redis/cluster/slot.rb b/lib/redis/cluster/slot.rb deleted file mode 100644 index c89cc1a97..000000000 --- a/lib/redis/cluster/slot.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -class Redis - class Cluster - # Keep slot and node key map for Redis Cluster Client - class Slot - ROLE_SLAVE = 'slave' - - def initialize(available_slots, node_flags = {}, with_replica = false) - @with_replica = with_replica - @node_flags = node_flags - @map = build_slot_node_key_map(available_slots) - end - - def exists?(slot) - @map.key?(slot) - end - - def find_node_key_of_master(slot) - return nil unless exists?(slot) - - @map[slot][:master] - end - - def find_node_key_of_slave(slot) - return nil unless exists?(slot) - return find_node_key_of_master(slot) if replica_disabled? - - @map[slot][:slaves].sample - end - - def put(slot, node_key) - # Since we're sharing a hash for build_slot_node_key_map, duplicate it - # if it already exists instead of preserving as-is. - @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] } - - if master?(node_key) - @map[slot][:master] = node_key - elsif !@map[slot][:slaves].include?(node_key) - @map[slot][:slaves] << node_key - end - - nil - end - - private - - def replica_disabled? - !@with_replica - end - - def master?(node_key) - !slave?(node_key) - end - - def slave?(node_key) - @node_flags[node_key] == ROLE_SLAVE - end - - # available_slots is mapping of node_key to list of slot ranges - def build_slot_node_key_map(available_slots) - by_ranges = {} - available_slots.each do |node_key, slots_arr| - by_ranges[slots_arr] ||= { master: nil, slaves: [] } - - if master?(node_key) - by_ranges[slots_arr][:master] = node_key - elsif !by_ranges[slots_arr][:slaves].include?(node_key) - by_ranges[slots_arr][:slaves] << node_key - end - end - - by_slot = {} - by_ranges.each do |slots_arr, nodes| - slots_arr.each do |slots| - slots.each do |slot| - by_slot[slot] = nodes - end - end - end - - by_slot - end - end - end -end diff --git a/lib/redis/cluster/slot_loader.rb b/lib/redis/cluster/slot_loader.rb deleted file mode 100644 index 7b1ba5491..000000000 --- a/lib/redis/cluster/slot_loader.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'redis/errors' -require 'redis/cluster/node_key' - -class Redis - class Cluster - # Load and hashify slot info for Redis Cluster Client - module SlotLoader - module_function - - def load(nodes) - errors = nodes.map do |node| - return fetch_slot_info(node) - rescue CannotConnectError, ConnectionError, CommandError => error - error - end - - raise InitialSetupError, errors - end - - def fetch_slot_info(node) - hash_with_default_arr = Hash.new { |h, k| h[k] = [] } - node.call(%i[cluster slots]) - .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) } - .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] } - end - - def parse_slot_info(arr, default_ip:) - first_slot, last_slot = arr[0..1] - slot_range = (first_slot..last_slot).freeze - arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] } - end - - def stringify_node_key(arr, default_ip) - ip, port = arr - ip = default_ip if ip.empty? # When cluster is down - NodeKey.build_from_host_port(ip, port) - end - - private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key - end - end -end diff --git a/lib/redis/cluster_client.rb b/lib/redis/cluster_client.rb new file mode 100644 index 000000000..5043853da --- /dev/null +++ b/lib/redis/cluster_client.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'redis-cluster-client' + +class Redis + class ClusterClient < RedisClient::Cluster + ERROR_MAPPING = ::Redis::Client::ERROR_MAPPING.merge( + RedisClient::Cluster::InitialSetupError => Redis::Cluster::InitialSetupError, + RedisClient::Cluster::OrchestrationCommandNotSupported => Redis::Cluster::OrchestrationCommandNotSupported, + RedisClient::Cluster::AmbiguousNodeError => Redis::Cluster::AmbiguousNodeError, + RedisClient::Cluster::ErrorCollection => Redis::Cluster::CommandErrorCollection + ).freeze + + class << self + def config(**kwargs) + super(protocol: 2, **kwargs) + end + + def sentinel(**kwargs) + super(protocol: 2, **kwargs) + end + end + + def initialize(*) + super + @inherit_socket = false + @pid = Process.pid + end + ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true) + + def id + @router.node.node_keys.join(' ') + end + + def server_url + @router.node.node_keys + end + + def connected? + true + end + + def disable_reconnection + yield # TODO: do we need this, is it doable? + end + + def timeout + config.read_timeout + end + + def db + 0 + end + + undef_method :call + undef_method :call_once + undef_method :call_once_v + undef_method :blocking_call + + def call_v(command, &block) + super(command, &block) + rescue ::RedisClient::Error => error + raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace + end + + def blocking_call_v(timeout, command, &block) + timeout += self.timeout if timeout && timeout > 0 + super(timeout, command, &block) + rescue ::RedisClient::Error => error + raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace + end + + def pipelined + super + rescue ::RedisClient::Error => error + raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace + end + + def multi + super + rescue ::RedisClient::Error => error + raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace + end + end +end diff --git a/lib/redis/errors.rb b/lib/redis/errors.rb index 3bda75d76..dbd65d9f2 100644 --- a/lib/redis/errors.rb +++ b/lib/redis/errors.rb @@ -57,10 +57,6 @@ class Cluster # Raised when client connected to redis as cluster mode # and failed to fetch cluster state information by commands. class InitialSetupError < BaseError - # @param errors [Array] - def initialize(errors) - super("Redis client could not fetch cluster information: #{errors.map(&:message).uniq.join(',')}") - end end # Raised when client connected to redis as cluster mode @@ -90,17 +86,6 @@ def initialize(errors, error_message = 'Command errors were replied on any node' # Raised when cluster client can't select node. class AmbiguousNodeError < BaseError - def initialize(command) - super("Cluster client doesn't know which node the #{command} command should be sent to.") - end - end - - # Raised when commands in pipelining include cross slot keys. - class CrossSlotPipeliningError < BaseError - def initialize(keys) - super("Cluster client couldn't send pipelining to single node. "\ - "The commands include cross slot keys. #{keys}") - end end end end diff --git a/makefile b/makefile index eb72009e1..39d5a7429 100644 --- a/makefile +++ b/makefile @@ -16,7 +16,7 @@ SLAVE_SOCKET_PATH := ${BUILD_DIR}/redis_slave.sock HA_GROUP_NAME := master1 SENTINEL_PORTS := 6400 6401 6402 SENTINEL_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${SENTINEL_PORTS})) -CLUSTER_PORTS := 7000 7001 7002 7003 7004 7005 +CLUSTER_PORTS := 16380 16381 16382 16383 16384 16385 CLUSTER_PID_PATHS := $(addprefix ${TMP}/redis,$(addsuffix .pid,${CLUSTER_PORTS})) CLUSTER_CONF_PATHS := $(addprefix ${TMP}/nodes,$(addsuffix .conf,${CLUSTER_PORTS})) CLUSTER_ADDRS := $(addprefix 127.0.0.1:,${CLUSTER_PORTS}) @@ -109,7 +109,7 @@ start_cluster: ${BINARY} @for port in ${CLUSTER_PORTS}; do\ ${BINARY}\ --daemonize yes\ - --appendonly yes\ + --appendonly no\ --cluster-enabled yes\ --cluster-config-file ${TMP}/nodes$$port.conf\ --cluster-node-timeout 5000\ diff --git a/test/cluster/abnormal_state_test.rb b/test/cluster/abnormal_state_test.rb index 081ecc99b..72f579927 100644 --- a/test/cluster/abnormal_state_test.rb +++ b/test/cluster/abnormal_state_test.rb @@ -31,6 +31,7 @@ def test_the_state_of_cluster_failover end def test_the_state_of_cluster_node_failure + # FIXME: this test takes 60 seconds redis_cluster_fail_master do assert_raises(Redis::CannotConnectError, 'Error connecting to Redis on 127.0.0.1:7002') do r.set('key0', 0) diff --git a/test/cluster/blocking_commands_test.rb b/test/cluster/blocking_commands_test.rb index 1e6acf6f5..6adc02656 100644 --- a/test/cluster/blocking_commands_test.rb +++ b/test/cluster/blocking_commands_test.rb @@ -9,6 +9,6 @@ class TestClusterBlockingCommands < Minitest::Test def mock(options = {}, &blk) commands = build_mock_commands(options) - redis_cluster_mock(commands, { timeout: LOW_TIMEOUT }, &blk) + redis_cluster_mock(commands, { timeout: LOW_TIMEOUT, concurrent: true }, &blk) end end diff --git a/test/cluster/client_internals_test.rb b/test/cluster/client_internals_test.rb index 02e3eeb21..e2e0acff6 100644 --- a/test/cluster/client_internals_test.rb +++ b/test/cluster/client_internals_test.rb @@ -21,24 +21,16 @@ def test_unknown_commands_does_not_work_by_default end end - def test_with_reconnect - assert_equal('Hello World', redis.with_reconnect { 'Hello World' }) - end - - def test_without_reconnect - assert_equal('Hello World', redis.without_reconnect { 'Hello World' }) - end - def test_connected? assert_equal true, redis.connected? end def test_close - assert_equal true, redis.close + redis.close end def test_disconnect! - assert_equal true, redis.disconnect! + redis.disconnect! end def test_asking @@ -46,35 +38,21 @@ def test_asking end def test_id - expected = 'redis://127.0.0.1:7000/0 '\ - 'redis://127.0.0.1:7001/0 '\ - 'redis://127.0.0.1:7002/0' + expected = '127.0.0.1:16380 '\ + '127.0.0.1:16381 '\ + '127.0.0.1:16382' assert_equal expected, redis.id end def test_inspect expected = "#' + '127.0.0.1:16380 '\ + '127.0.0.1:16381 '\ + '127.0.0.1:16382>' assert_equal expected, redis.inspect end - def test_dup - assert_instance_of Redis, redis.dup - end - - def test_connection - expected = [ - { host: '127.0.0.1', port: 7000, db: 0, id: 'redis://127.0.0.1:7000/0', location: '127.0.0.1:7000' }, - { host: '127.0.0.1', port: 7001, db: 0, id: 'redis://127.0.0.1:7001/0', location: '127.0.0.1:7001' }, - { host: '127.0.0.1', port: 7002, db: 0, id: 'redis://127.0.0.1:7002/0', location: '127.0.0.1:7002' } - ] - - assert_equal expected, redis.connection - end - def test_acl_auth_success target_version "6.0.0" do with_acl do |username, password| diff --git a/test/cluster/client_key_hash_tags_test.rb b/test/cluster/client_key_hash_tags_test.rb deleted file mode 100644 index a878cca36..000000000 --- a/test/cluster/client_key_hash_tags_test.rb +++ /dev/null @@ -1,105 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -# ruby -w -Itest test/cluster_client_key_hash_tags_test.rb -class TestClusterClientKeyHashTags < Minitest::Test - include Helper::Cluster - - def build_described_class(urls = ['redis://127.0.0.1:7000']) - option = Redis::Cluster::Option.new(cluster: urls) - node = Redis::Cluster::Node.new(option.per_node_key) - details = Redis::Cluster::CommandLoader.load(node) - Redis::Cluster::Command.new(details) - end - - def test_key_extraction - described_class = build_described_class - - assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1]) - assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.following]) - assert_equal 'user1000', described_class.extract_first_key(%w[get {user1000}.followers]) - assert_equal 'foo{}{bar}', described_class.extract_first_key(%w[get foo{}{bar}]) - assert_equal '{bar', described_class.extract_first_key(%w[get foo{{bar}}zap]) - assert_equal 'bar', described_class.extract_first_key(%w[get foo{bar}{zap}]) - assert_equal 'dogs', described_class.extract_first_key([:sscan, 'dogs', 0]) - assert_equal 'dogs', described_class.extract_first_key([:sscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10]) - assert_equal 'dogs', described_class.extract_first_key([:hscan, 'dogs', 0]) - assert_equal 'dogs', described_class.extract_first_key([:hscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10]) - assert_equal 'dogs', described_class.extract_first_key([:zscan, 'dogs', 0]) - assert_equal 'dogs', described_class.extract_first_key([:zscan, 'dogs', 0, 'MATCH', '/poodle/', 'COUNT', 10]) - - assert_equal '', described_class.extract_first_key([:get, '']) - assert_equal '', described_class.extract_first_key([:get, nil]) - assert_equal '', described_class.extract_first_key([:get]) - - assert_equal '', described_class.extract_first_key([:set, '', 1]) - assert_equal '', described_class.extract_first_key([:set, nil, 1]) - assert_equal '', described_class.extract_first_key([:set]) - - # Keyless commands - assert_equal '', described_class.extract_first_key([:scan, 0]) - assert_equal '', described_class.extract_first_key([:scan, 0, 'MATCH', '/poodle/', 'COUNT', 10]) - assert_equal '', described_class.extract_first_key([:auth, 'password']) - assert_equal '', described_class.extract_first_key(%i[client kill]) - assert_equal '', described_class.extract_first_key(%i[cluster addslots]) - assert_equal '', described_class.extract_first_key(%i[command]) - assert_equal '', described_class.extract_first_key(%i[command count]) - assert_equal '', described_class.extract_first_key(%i[config get]) - assert_equal '', described_class.extract_first_key(%i[debug segfault]) - assert_equal '', described_class.extract_first_key([:echo, 'Hello World']) - assert_equal '', described_class.extract_first_key([:flushall, 'ASYNC']) - assert_equal '', described_class.extract_first_key([:flushdb, 'ASYNC']) - assert_equal '', described_class.extract_first_key([:info, 'cluster']) - assert_equal '', described_class.extract_first_key(%i[memory doctor]) - assert_equal '', described_class.extract_first_key([:ping, 'Hi']) - assert_equal '', described_class.extract_first_key([:psubscribe, 'channel']) - assert_equal '', described_class.extract_first_key([:pubsub, 'channels', '*']) - assert_equal '', described_class.extract_first_key([:publish, 'channel', 'Hi']) - assert_equal '', described_class.extract_first_key([:punsubscribe, 'channel']) - assert_equal '', described_class.extract_first_key([:subscribe, 'channel']) - assert_equal '', described_class.extract_first_key([:unsubscribe, 'channel']) - assert_equal '', described_class.extract_first_key(%w[script exists sha1 sha1]) - assert_equal '', described_class.extract_first_key([:select, 1]) - assert_equal '', described_class.extract_first_key([:shutdown, 'SAVE']) - assert_equal '', described_class.extract_first_key([:slaveof, '127.0.0.1', 6379]) - assert_equal '', described_class.extract_first_key([:slowlog, 'get', 2]) - assert_equal '', described_class.extract_first_key([:swapdb, 0, 1]) - assert_equal '', described_class.extract_first_key([:wait, 1, 0]) - - # 2nd argument is not a key - assert_equal 'key1', described_class.extract_first_key([:eval, 'script', 2, 'key1', 'key2', 'first', 'second']) - assert_equal '', described_class.extract_first_key([:eval, 'return 0', 0]) - assert_equal 'key1', described_class.extract_first_key([:evalsha, 'sha1', 2, 'key1', 'key2', 'first', 'second']) - assert_equal '', described_class.extract_first_key([:evalsha, 'return 0', 0]) - assert_equal 'key1', described_class.extract_first_key([:migrate, '127.0.0.1', 6379, 'key1', 0, 5000]) - assert_equal 'key1', described_class.extract_first_key([:memory, :usage, 'key1']) - assert_equal 'key1', described_class.extract_first_key([:object, 'refcount', 'key1']) - assert_equal 'mystream', described_class.extract_first_key([:xread, 'COUNT', 2, 'STREAMS', 'mystream', 0]) - assert_equal 'mystream', described_class.extract_first_key([:xreadgroup, 'GROUP', 'mygroup', 'Bob', 'COUNT', 2, 'STREAMS', 'mystream', '>']) - end - - def test_whether_the_command_effect_is_readonly_or_not - described_class = build_described_class - - assert_equal true, described_class.should_send_to_master?([:set]) - assert_equal false, described_class.should_send_to_slave?([:set]) - - assert_equal false, described_class.should_send_to_master?([:get]) - assert_equal true, described_class.should_send_to_slave?([:get]) - - assert_equal false, described_class.should_send_to_master?([:info]) - assert_equal false, described_class.should_send_to_slave?([:info]) - end - - def test_cannot_build_details_from_bad_urls - assert_raises(Redis::Cluster::InitialSetupError) do - build_described_class(['redis://127.0.0.1:7006']) - end - end - - def test_builds_details_from_a_mix_of_good_and_bad_urls - described_class = build_described_class(['redis://127.0.0.1:7006', 'redis://127.0.0.1:7000']) - assert_equal 'dogs:1', described_class.extract_first_key(%w[get dogs:1]) - end -end diff --git a/test/cluster/client_options_test.rb b/test/cluster/client_options_test.rb deleted file mode 100644 index 01ba7311f..000000000 --- a/test/cluster/client_options_test.rb +++ /dev/null @@ -1,181 +0,0 @@ -# frozen_string_literal: true - -require 'uri' -require 'helper' - -# ruby -w -Itest test/cluster_client_options_test.rb -class TestClusterClientOptions < Minitest::Test - include Helper::Cluster - - def test_option_class - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: true) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - assert_equal true, option.use_replica? - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], replica: false) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - assert_equal false, option.use_replica? - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - assert_equal false, option.use_replica? - - option = Redis::Cluster::Option.new(cluster: %w[rediss://johndoe:foobar@127.0.0.1:7000/1/namespace]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', username: 'johndoe', password: 'foobar', host: '127.0.0.1', port: 7000, db: 1 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[rediss://127.0.0.1:7000], scheme: 'redis') - assert_equal({ '127.0.0.1:7000' => { scheme: 'rediss', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://bazzap:@127.0.0.1:7000], username: 'foobar') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', username: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://:bazzap@127.0.0.1:7000], password: 'foobar') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: 'bazzap', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %W[redis://#{URI.encode_www_form_component('!&<123-abc>')}:@127.0.0.1:7000]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', username: '!&<123-abc>', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %W[redis://:#{URI.encode_www_form_component('!&<123-abc>')}@127.0.0.1:7000]) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', password: '!&<123-abc>', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000/0], db: 1) - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000, db: 0 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1', port: 7000 }]) - assert_equal({ '127.0.0.1:7000' => { host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], fixed_hostname: 'foo-endpoint.example.com') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: 'foo-endpoint.example.com', port: 7000 } }, option.per_node_key) - - option = Redis::Cluster::Option.new(cluster: %w[redis://127.0.0.1:7000], fixed_hostname: '') - assert_equal({ '127.0.0.1:7000' => { scheme: 'redis', host: '127.0.0.1', port: 7000 } }, option.per_node_key) - - assert_raises(Redis::InvalidClientOptionError) do - Redis::Cluster::Option.new(cluster: nil) - end - - assert_raises(Redis::InvalidClientOptionError) do - Redis::Cluster::Option.new(cluster: %w[invalid_uri]) - end - - assert_raises(Redis::InvalidClientOptionError) do - Redis::Cluster::Option.new(cluster: [{ host: '127.0.0.1' }]) - end - end - - def test_client_accepts_valid_node_configs - nodes = ['redis://127.0.0.1:7000', - 'redis://127.0.0.1:7001', - { host: '127.0.0.1', port: '7002' }, - { 'host' => '127.0.0.1', port: 7003 }, - 'redis://127.0.0.1:7004', - 'redis://127.0.0.1:7005'] - - build_another_client(cluster: nodes) - end - - def test_client_accepts_valid_options - build_another_client(timeout: TIMEOUT) - end - - def test_client_ignores_invalid_options - build_another_client(invalid_option: true) - end - - def test_client_works_even_if_so_many_unavailable_nodes_specified - min = 7000 - max = min + Process.getrlimit(Process::RLIMIT_NOFILE).first / 3 * 2 - nodes = (min..max).map { |port| "redis://127.0.0.1:#{port}" } - redis = build_another_client(cluster: nodes) - - assert_equal 'PONG', redis.ping - end - - def test_client_does_not_accept_db_specified_url - assert_raises(Redis::Cluster::InitialSetupError) do - build_another_client(cluster: ['redis://127.0.0.1:7000/1/namespace']) - end - - assert_raises(Redis::Cluster::InitialSetupError) do - build_another_client(cluster: [{ host: '127.0.0.1', port: '7000' }], db: 1) - end - end - - def test_client_does_not_accept_unconnectable_node_url_only - nodes = ['redis://127.0.0.1:7006'] - - assert_raises(Redis::Cluster::InitialSetupError) do - build_another_client(cluster: nodes) - end - end - - def test_client_accepts_unconnectable_node_url_included - nodes = ['redis://127.0.0.1:7000', 'redis://127.0.0.1:7006'] - - build_another_client(cluster: nodes) - end - - def test_client_does_not_accept_http_scheme_url - nodes = ['http://127.0.0.1:80'] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme 'http'") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_blank_included_config - nodes = [''] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_bool_included_config - nodes = [true] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_nil_included_config - nodes = [nil] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_array_included_config - nodes = [[]] - - assert_raises(Redis::InvalidClientOptionError, "invalid uri scheme ''") do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_empty_hash_included_config - nodes = [{}] - - assert_raises(Redis::InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys') do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_object_included_config - nodes = [Object.new] - - assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must includes String or Hash') do - build_another_client(cluster: nodes) - end - end - - def test_client_does_not_accept_not_array_config - nodes = :not_array - - assert_raises(Redis::InvalidClientOptionError, 'Redis Cluster node config must be Array') do - build_another_client(cluster: nodes) - end - end -end diff --git a/test/cluster/client_pipelining_test.rb b/test/cluster/client_pipelining_test.rb index 62d6bcbae..d1ab76d7b 100644 --- a/test/cluster/client_pipelining_test.rb +++ b/test/cluster/client_pipelining_test.rb @@ -38,7 +38,7 @@ def test_pipelining_with_a_hash_tag end def test_pipelining_without_hash_tags - assert_raises(Redis::Cluster::CrossSlotPipeliningError) do + assert_raises(Redis::Cluster::AmbiguousNodeError) do redis.pipelined do redis.set(:a, 1) redis.set(:b, 2) @@ -56,17 +56,4 @@ def test_pipelining_without_hash_tags end end end - - def test_pipelining_with_multiple_replicas - rc = build_another_client(replica: true) - rc.instance_variable_get(:@client).instance_variable_get(:@slot).instance_variable_get(:@map).each do |_, v| - v[:slaves] << v[:master] if v[:slaves].size < 2 # reproducing multiple replicas - end - - rc.pipelined do |r| - 10.times { r.get('key1') } - end - - rc.close - end end diff --git a/test/cluster/client_slots_test.rb b/test/cluster/client_slots_test.rb deleted file mode 100644 index 95cf28aba..000000000 --- a/test/cluster/client_slots_test.rb +++ /dev/null @@ -1,150 +0,0 @@ -# frozen_string_literal: true - -require "helper" - -# ruby -w -Itest test/cluster_client_slots_test.rb -class TestClusterClientSlots < Minitest::Test - include Helper::Cluster - - def test_slot_class - slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10]) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - - assert_nil slot.put(1, '127.0.0.1:7001') - end - - def test_slot_class_with_multiple_slot_ranges - slot = Redis::Cluster::Slot.new('127.0.0.1:7000' => [1..10, 30..40]) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - assert_equal true, slot.exists?(30) - assert_equal true, slot.exists?(40) - assert_equal false, slot.exists?(41) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(30) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(40) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - assert_nil slot.find_node_key_of_master(41) - assert_nil slot.find_node_key_of_slave(41) - - assert_nil slot.put(1, '127.0.0.1:7001') - assert_nil slot.put(30, '127.0.0.1:7001') - end - - def test_slot_class_with_node_flags_and_replicas - slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] }, - { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, - true) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - - assert_nil slot.put(1, '127.0.0.1:7002') - end - - def test_slot_class_with_node_flags_replicas_and_slot_range - slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10, 30..40], '127.0.0.1:7001' => [1..10, 30..40] }, - { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, - true) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - assert_equal true, slot.exists?(30) - assert_equal false, slot.exists?(41) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(30) - assert_equal '127.0.0.1:7001', slot.find_node_key_of_slave(30) - assert_nil slot.find_node_key_of_master(41) - assert_nil slot.find_node_key_of_slave(41) - - assert_nil slot.put(1, '127.0.0.1:7002') - end - - def test_slot_class_with_node_flags_and_without_replicas - slot = Redis::Cluster::Slot.new({ '127.0.0.1:7000' => [1..10], '127.0.0.1:7001' => [1..10] }, - { '127.0.0.1:7000' => 'master', '127.0.0.1:7001' => 'slave' }, - false) - - assert_equal false, slot.exists?(0) - assert_equal true, slot.exists?(1) - assert_equal true, slot.exists?(10) - assert_equal false, slot.exists?(11) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(1) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_master(10) - assert_equal '127.0.0.1:7000', slot.find_node_key_of_slave(10) - assert_nil slot.find_node_key_of_master(11) - assert_nil slot.find_node_key_of_slave(11) - - assert_nil slot.put(1, '127.0.0.1:7002') - end - - def test_slot_class_with_empty_slots - slot = Redis::Cluster::Slot.new({}) - - assert_equal false, slot.exists?(0) - assert_equal false, slot.exists?(1) - - assert_nil slot.find_node_key_of_master(0) - assert_nil slot.find_node_key_of_slave(0) - assert_nil slot.find_node_key_of_master(1) - assert_nil slot.find_node_key_of_slave(1) - - assert_nil slot.put(1, '127.0.0.1:7001') - end - - def test_redirection_when_slot_is_resharding - 100.times { |i| redis.set("{key}#{i}", i) } - - redis_cluster_resharding(12_539, src: '127.0.0.1:7002', dest: '127.0.0.1:7000') do - 100.times { |i| assert_equal i.to_s, redis.get("{key}#{i}") } - end - end -end diff --git a/test/cluster/client_transactions_test.rb b/test/cluster/client_transactions_test.rb index bce56fc7e..42a69bae7 100644 --- a/test/cluster/client_transactions_test.rb +++ b/test/cluster/client_transactions_test.rb @@ -7,6 +7,7 @@ class TestClusterClientTransactions < Minitest::Test include Helper::Cluster def test_transaction_with_hash_tag + skip("redis-cluster-client doesn't support transaction") rc1 = redis rc2 = build_another_client @@ -22,7 +23,7 @@ def test_transaction_without_hash_tag rc1 = redis rc2 = build_another_client - assert_raises(Redis::Cluster::CrossSlotPipeliningError) do + assert_raises(Redis::Cluster::AmbiguousNodeError) do rc1.multi do |cli| 100.times { |i| cli.set("key#{i}", i) } end @@ -33,6 +34,7 @@ def test_transaction_without_hash_tag end def test_transaction_with_replicas + skip("redis-cluster-client doesn't support transaction") rc1 = build_another_client(replica: true) rc2 = build_another_client(replica: true) @@ -47,6 +49,7 @@ def test_transaction_with_replicas end def test_transaction_with_watch + skip("redis-cluster-client doesn't support transaction") rc1 = redis rc2 = build_another_client diff --git a/test/cluster/commands_on_cluster_test.rb b/test/cluster/commands_on_cluster_test.rb index 4500e35e2..5f2462ca8 100644 --- a/test/cluster/commands_on_cluster_test.rb +++ b/test/cluster/commands_on_cluster_test.rb @@ -63,14 +63,6 @@ def test_cluster_info assert_equal '3', info.fetch('cluster_size') end - def test_cluster_keyslot - assert_equal Redis::Cluster::KeySlotConverter.convert('hogehoge'), redis.cluster(:keyslot, 'hogehoge') - assert_equal Redis::Cluster::KeySlotConverter.convert('12345'), redis.cluster(:keyslot, '12345') - assert_equal Redis::Cluster::KeySlotConverter.convert('foo'), redis.cluster(:keyslot, 'boo{foo}woo') - assert_equal Redis::Cluster::KeySlotConverter.convert('antirez.is.cool'), redis.cluster(:keyslot, 'antirez.is.cool') - assert_equal Redis::Cluster::KeySlotConverter.convert(''), redis.cluster(:keyslot, '') - end - def test_cluster_meet assert_raises(Redis::Cluster::OrchestrationCommandNotSupported, 'CLUSTER MEET command should be...') do redis.cluster(:meet, '127.0.0.1', 11_211) diff --git a/test/helper.rb b/test/helper.rb index 456908a63..0ab6f2fa0 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -282,7 +282,7 @@ module Cluster include Generic DEFAULT_HOST = '127.0.0.1' - DEFAULT_PORTS = (7000..7005).freeze + DEFAULT_PORTS = (16_380..16_385).freeze ClusterSlotsRawReply = lambda { |host, port| # @see https://redis.io/topics/protocol @@ -350,10 +350,11 @@ def redis_cluster_mock(commands, options = {}) end commands[:cluster] = lambda { |subcommand, *args| + subcommand = subcommand.downcase if cluster_subcommands.key?(subcommand) cluster_subcommands[subcommand].call(*args) else - case subcommand + case subcommand.downcase when 'slots' then ClusterSlotsRawReply.call(host, port) when 'nodes' then ClusterNodesRawReply.call(host, port) else '+OK' diff --git a/test/lint/blocking_commands.rb b/test/lint/blocking_commands.rb index 9652a1c07..201cd3dee 100644 --- a/test/lint/blocking_commands.rb +++ b/test/lint/blocking_commands.rb @@ -27,7 +27,7 @@ def to_protocol(obj) def mock(options = {}, &blk) commands = build_mock_commands(options) - redis_mock(commands, { timeout: LOW_TIMEOUT }, &blk) + redis_mock(commands, { timeout: TIMEOUT }, &blk) end def build_mock_commands(options = {}) @@ -182,38 +182,36 @@ def test_bzpopmax assert_nil r.bzpopmax('{szap}aaa', '{szap}bbb', 1) end - driver(:ruby, :hiredis) do - def test_blmove_socket_timeout - target_version "6.2" do - mock(delay: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT) - end + def test_blmove_socket_timeout + target_version "6.2" do + mock(delay: TIMEOUT * 5) do |r| + assert_raises(Redis::TimeoutError) do + r.blmove('{zap}foo', '{zap}bar', 'LEFT', 'RIGHT', timeout: LOW_TIMEOUT) end end end + end - def test_blpop_socket_timeout - mock(delay: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.blpop('{zap}foo', timeout: LOW_TIMEOUT) - end + def test_blpop_socket_timeout + mock(delay: TIMEOUT * 5) do |r| + assert_raises(Redis::TimeoutError) do + r.blpop('{zap}foo', timeout: LOW_TIMEOUT) end end + end - def test_brpop_socket_timeout - mock(delay: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.brpop('{zap}foo', timeout: LOW_TIMEOUT) - end + def test_brpop_socket_timeout + mock(delay: TIMEOUT * 5) do |r| + assert_raises(Redis::TimeoutError) do + r.brpop('{zap}foo', timeout: LOW_TIMEOUT) end end + end - def test_brpoplpush_socket_timeout - mock(delay: LOW_TIMEOUT * 5) do |r| - assert_raises(Redis::TimeoutError) do - r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT) - end + def test_brpoplpush_socket_timeout + mock(delay: TIMEOUT * 5) do |r| + assert_raises(Redis::TimeoutError) do + r.brpoplpush('{zap}foo', '{zap}bar', timeout: LOW_TIMEOUT) end end end diff --git a/test/lint/hashes.rb b/test/lint/hashes.rb index 5e22c9e56..a6d470220 100644 --- a/test/lint/hashes.rb +++ b/test/lint/hashes.rb @@ -172,8 +172,8 @@ def test_hmget_mapped r.hset("foo", "f2", "s2") r.hset("foo", "f3", "s3") - assert(r.mapped_hmget("foo", "f1") == { "f1" => "s1" }) - assert(r.mapped_hmget("foo", "f1", "f2") == { "f1" => "s1", "f2" => "s2" }) + assert_equal({ "f1" => "s1" }, r.mapped_hmget("foo", "f1")) + assert_equal({ "f1" => "s1", "f2" => "s2" }, r.mapped_hmget("foo", "f1", "f2")) end def test_mapped_hmget_in_a_pipeline_returns_hash @@ -184,7 +184,7 @@ def test_mapped_hmget_in_a_pipeline_returns_hash pipeline.mapped_hmget("foo", "f1", "f2") end - assert_equal result[0], { "f1" => "s1", "f2" => "s2" } + assert_equal({ "f1" => "s1", "f2" => "s2" }, result[0]) end def test_hincrby diff --git a/test/lint/streams.rb b/test/lint/streams.rb index 8bc761b1d..fbeaa6b0c 100644 --- a/test/lint/streams.rb +++ b/test/lint/streams.rb @@ -362,8 +362,9 @@ def test_xread_does_not_raise_timeout_error_when_the_block_option_is_zero_msec actual = redis.xread('s1', 0, block: 0) end Thread.pass until prepared - redis.dup.xadd('s1', { f: 'v1' }, id: '0-1') - thread.join + redis2 = init _new_client + redis2.xadd('s1', { f: 'v1' }, id: '0-1') + thread.join(3) assert_equal(['v1'], actual.fetch('s1').map { |i| i.last['f'] }) end diff --git a/test/redis/blocking_commands_test.rb b/test/redis/blocking_commands_test.rb index fe8a3f81b..64221ab4a 100644 --- a/test/redis/blocking_commands_test.rb +++ b/test/redis/blocking_commands_test.rb @@ -15,8 +15,7 @@ def assert_takes_longer_than_client_timeout yield(r) t2 = Time.now - assert timeout == r._client.timeout - assert delay <= (t2 - t1) + assert_operator delay, :<=, (t2 - t1) end end diff --git a/test/support/cluster/orchestrator.rb b/test/support/cluster/orchestrator.rb index 30152fff2..fd52dec35 100644 --- a/test/support/cluster/orchestrator.rb +++ b/test/support/cluster/orchestrator.rb @@ -9,11 +9,7 @@ def initialize(node_addrs, timeout: 30.0) raise 'Redis Cluster requires at least 3 master nodes.' if node_addrs.size < 3 @clients = node_addrs.map do |addr| - Redis.new(url: addr, - timeout: timeout, - reconnect_attempts: 10, - reconnect_delay: 1.5, - reconnect_delay_max: 10.0) + Redis.new(url: addr, timeout: timeout, reconnect_attempts: [0, 0.5, 1, 1.5]) end @timeout = timeout end @@ -106,7 +102,7 @@ def close def flush_all_data(clients) clients.each do |c| - c.flushall + c.flushall(async: true) rescue Redis::CommandError # READONLY You can't write against a read only slave. nil @@ -154,7 +150,7 @@ def meet_each_other(clients) end end - def wait_meeting(clients, max_attempts: 600) + def wait_meeting(clients, max_attempts: 60) size = clients.size.to_s wait_for_state(clients, max_attempts) do |client| @@ -192,21 +188,21 @@ def save_config(clients) clients.each { |c| c.cluster(:saveconfig) } end - def wait_cluster_building(clients, max_attempts: 600) + def wait_cluster_building(clients, max_attempts: 60) wait_for_state(clients, max_attempts) do |client| info = hashify_cluster_info(client) info['cluster_state'] == 'ok' end end - def wait_replication(clients, max_attempts: 600) + def wait_replication(clients, max_attempts: 60) wait_for_state(clients, max_attempts) do |client| flags = hashify_cluster_node_flags(client) flags.values.select { |f| f == 'slave' }.size == 3 end end - def wait_failover(master_key, slave_key, clients, max_attempts: 600) + def wait_failover(master_key, slave_key, clients, max_attempts: 60) wait_for_state(clients, max_attempts) do |client| flags = hashify_cluster_node_flags(client) flags[master_key] == 'slave' && flags[slave_key] == 'master' @@ -221,7 +217,7 @@ def wait_replication_delay(clients, timeout_sec) end end - def wait_cluster_recovering(clients, max_attempts: 600) + def wait_cluster_recovering(clients, max_attempts: 60) key = 0 wait_for_state(clients, max_attempts) do |client| client.get(key) if client.role.first == 'master' diff --git a/test/support/redis_mock.rb b/test/support/redis_mock.rb index 89e47de00..ac1029833 100644 --- a/test/support/redis_mock.rb +++ b/test/support/redis_mock.rb @@ -8,6 +8,8 @@ def initialize(options = {}) tcp_server = TCPServer.new(options[:host] || "127.0.0.1", 0) tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true) + @concurrent = options.delete(:concurrent) + if options[:ssl] ctx = OpenSSL::SSL::SSLContext.new @@ -32,14 +34,21 @@ def shutdown @thread.kill end - def run + def run(&block) loop do - session = @server.accept - - begin - return if yield(session) == :exit - ensure - session.close + if @concurrent + Thread.new(@server.accept) do |session| + block.call(session) + ensure + session.close + end + else + session = @server.accept + begin + return if yield(session) == :exit + ensure + session.close + end end end rescue => ex