redis 4.0.2 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +2 -2
  4. data/CHANGELOG.md +6 -0
  5. data/lib/redis.rb +97 -11
  6. data/lib/redis/client.rb +19 -11
  7. data/lib/redis/cluster.rb +285 -0
  8. data/lib/redis/cluster/command.rb +81 -0
  9. data/lib/redis/cluster/command_loader.rb +32 -0
  10. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  11. data/lib/redis/cluster/node.rb +104 -0
  12. data/lib/redis/cluster/node_key.rb +35 -0
  13. data/lib/redis/cluster/node_loader.rb +35 -0
  14. data/lib/redis/cluster/option.rb +76 -0
  15. data/lib/redis/cluster/slot.rb +69 -0
  16. data/lib/redis/cluster/slot_loader.rb +47 -0
  17. data/lib/redis/errors.rb +46 -0
  18. data/lib/redis/version.rb +1 -1
  19. data/makefile +54 -16
  20. data/redis.gemspec +2 -1
  21. data/test/client_test.rb +17 -0
  22. data/test/cluster_abnormal_state_test.rb +38 -0
  23. data/test/cluster_blocking_commands_test.rb +15 -0
  24. data/test/cluster_client_internals_test.rb +77 -0
  25. data/test/cluster_client_key_hash_tags_test.rb +88 -0
  26. data/test/cluster_client_options_test.rb +147 -0
  27. data/test/cluster_client_pipelining_test.rb +59 -0
  28. data/test/cluster_client_replicas_test.rb +36 -0
  29. data/test/cluster_client_slots_test.rb +94 -0
  30. data/test/cluster_client_transactions_test.rb +71 -0
  31. data/test/cluster_commands_on_cluster_test.rb +165 -0
  32. data/test/cluster_commands_on_connection_test.rb +40 -0
  33. data/test/cluster_commands_on_geo_test.rb +74 -0
  34. data/test/cluster_commands_on_hashes_test.rb +11 -0
  35. data/test/cluster_commands_on_hyper_log_log_test.rb +17 -0
  36. data/test/cluster_commands_on_keys_test.rb +134 -0
  37. data/test/cluster_commands_on_lists_test.rb +15 -0
  38. data/test/cluster_commands_on_pub_sub_test.rb +101 -0
  39. data/test/cluster_commands_on_scripting_test.rb +56 -0
  40. data/test/cluster_commands_on_server_test.rb +221 -0
  41. data/test/cluster_commands_on_sets_test.rb +39 -0
  42. data/test/cluster_commands_on_sorted_sets_test.rb +35 -0
  43. data/test/cluster_commands_on_streams_test.rb +196 -0
  44. data/test/cluster_commands_on_strings_test.rb +15 -0
  45. data/test/cluster_commands_on_transactions_test.rb +41 -0
  46. data/test/cluster_commands_on_value_types_test.rb +14 -0
  47. data/test/commands_on_hashes_test.rb +2 -14
  48. data/test/commands_on_hyper_log_log_test.rb +2 -14
  49. data/test/commands_on_lists_test.rb +2 -13
  50. data/test/commands_on_sets_test.rb +2 -70
  51. data/test/commands_on_sorted_sets_test.rb +2 -145
  52. data/test/commands_on_strings_test.rb +2 -94
  53. data/test/distributed_blocking_commands_test.rb +8 -0
  54. data/test/distributed_commands_on_hashes_test.rb +16 -3
  55. data/test/distributed_commands_on_hyper_log_log_test.rb +8 -13
  56. data/test/distributed_commands_on_lists_test.rb +4 -5
  57. data/test/distributed_commands_on_sets_test.rb +45 -46
  58. data/test/distributed_commands_on_sorted_sets_test.rb +51 -8
  59. data/test/distributed_commands_on_strings_test.rb +10 -0
  60. data/test/helper.rb +176 -32
  61. data/test/internals_test.rb +13 -0
  62. data/test/lint/blocking_commands.rb +40 -16
  63. data/test/lint/hashes.rb +26 -0
  64. data/test/lint/hyper_log_log.rb +15 -1
  65. data/test/lint/lists.rb +16 -0
  66. data/test/lint/sets.rb +142 -0
  67. data/test/lint/sorted_sets.rb +183 -2
  68. data/test/lint/strings.rb +102 -0
  69. data/test/support/cluster/orchestrator.rb +199 -0
  70. metadata +79 -4
@@ -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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Load details about Redis commands for Redis Cluster Client
8
+ # @see https://redis.io/commands/command
9
+ module CommandLoader
10
+ module_function
11
+
12
+ def load(nodes)
13
+ details = {}
14
+
15
+ nodes.each do |node|
16
+ details = fetch_command_details(node)
17
+ details.empty? ? next : break
18
+ end
19
+
20
+ details
21
+ end
22
+
23
+ def fetch_command_details(node)
24
+ node.call(%i[command]).map do |reply|
25
+ [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
26
+ end.to_h
27
+ rescue CannotConnectError, ConnectionError, CommandError
28
+ {} # can retry on another node
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ class Cluster
5
+ # Key to slot converter for Redis Cluster Client
6
+ #
7
+ # We can test it by `CLUSTER KEYSLOT` command.
8
+ #
9
+ # @see https://github.com/antirez/redis-rb-cluster
10
+ # Reference implementation in Ruby
11
+ # @see https://redis.io/topics/cluster-spec#appendix
12
+ # Reference implementation in ANSI C
13
+ # @see https://redis.io/commands/cluster-keyslot
14
+ # CLUSTER KEYSLOT command reference
15
+ #
16
+ # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
17
+ module KeySlotConverter
18
+ XMODEM_CRC16_LOOKUP = [
19
+ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
20
+ 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
21
+ 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
22
+ 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
23
+ 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
24
+ 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
25
+ 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
26
+ 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
27
+ 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
28
+ 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
29
+ 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
30
+ 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
31
+ 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
32
+ 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
33
+ 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
34
+ 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
35
+ 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
36
+ 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
37
+ 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
38
+ 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
39
+ 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
40
+ 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
41
+ 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
42
+ 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
43
+ 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
44
+ 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
45
+ 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
46
+ 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
47
+ 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
48
+ 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
49
+ 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
50
+ 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
51
+ ].freeze
52
+
53
+ HASH_SLOTS = 16_384
54
+
55
+ module_function
56
+
57
+ # Convert key into slot.
58
+ #
59
+ # @param key [String] the key of the redis command
60
+ #
61
+ # @return [Integer] slot number
62
+ def convert(key)
63
+ crc = 0
64
+ key.each_byte do |b|
65
+ crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
66
+ end
67
+
68
+ crc % HASH_SLOTS
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep client list of node for Redis Cluster Client
8
+ class Node
9
+ include Enumerable
10
+
11
+ ReloadNeeded = Class.new(StandardError)
12
+
13
+ ROLE_SLAVE = 'slave'
14
+
15
+ def initialize(options, node_flags = {}, with_replica = false)
16
+ @with_replica = with_replica
17
+ @node_flags = node_flags
18
+ @clients = build_clients(options)
19
+ end
20
+
21
+ def each(&block)
22
+ @clients.values.each(&block)
23
+ end
24
+
25
+ def sample
26
+ @clients.values.sample
27
+ end
28
+
29
+ def find_by(node_key)
30
+ @clients.fetch(node_key)
31
+ rescue KeyError
32
+ raise ReloadNeeded
33
+ end
34
+
35
+ def call_all(command, &block)
36
+ try_map { |_, client| client.call(command, &block) }.values
37
+ end
38
+
39
+ def call_master(command, &block)
40
+ try_map do |node_key, client|
41
+ next if slave?(node_key)
42
+ client.call(command, &block)
43
+ end.values
44
+ end
45
+
46
+ def call_slave(command, &block)
47
+ return call_master(command, &block) if replica_disabled?
48
+
49
+ try_map do |node_key, client|
50
+ next if master?(node_key)
51
+ client.call(command, &block)
52
+ end.values
53
+ end
54
+
55
+ def process_all(commands, &block)
56
+ try_map { |_, client| client.process(commands, &block) }.values
57
+ end
58
+
59
+ private
60
+
61
+ def replica_disabled?
62
+ !@with_replica
63
+ end
64
+
65
+ def master?(node_key)
66
+ !slave?(node_key)
67
+ end
68
+
69
+ def slave?(node_key)
70
+ @node_flags[node_key] == ROLE_SLAVE
71
+ end
72
+
73
+ def build_clients(options)
74
+ clients = options.map do |node_key, option|
75
+ next if replica_disabled? && slave?(node_key)
76
+
77
+ client = Client.new(option)
78
+ client.call(%i[readonly]) if slave?(node_key)
79
+ [node_key, client]
80
+ end
81
+
82
+ clients.compact.to_h
83
+ end
84
+
85
+ def try_map
86
+ errors = {}
87
+ results = {}
88
+
89
+ @clients.each do |node_key, client|
90
+ begin
91
+ reply = yield(node_key, client)
92
+ results[node_key] = reply unless reply.nil?
93
+ rescue CommandError => err
94
+ errors[node_key] = err
95
+ next
96
+ end
97
+ end
98
+
99
+ return results if errors.empty?
100
+ raise CommandErrorCollection, errors
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ class Cluster
5
+ # Node key's format is `<ip>:<port>`.
6
+ # It is different from node id.
7
+ # Node id is internal identifying code in Redis Cluster.
8
+ module NodeKey
9
+ DEFAULT_SCHEME = 'redis'
10
+ SECURE_SCHEME = 'rediss'
11
+ DELIMITER = ':'
12
+
13
+ module_function
14
+
15
+ def to_node_urls(node_keys, secure:)
16
+ scheme = secure ? SECURE_SCHEME : DEFAULT_SCHEME
17
+ node_keys
18
+ .map { |k| k.split(DELIMITER) }
19
+ .map { |k| URI::Generic.build(scheme: scheme, host: k[0], port: k[1].to_i).to_s }
20
+ end
21
+
22
+ def split(node_key)
23
+ node_key.split(DELIMITER)
24
+ end
25
+
26
+ def build_from_uri(uri)
27
+ "#{uri.host}#{DELIMITER}#{uri.port}"
28
+ end
29
+
30
+ def build_from_host_port(host, port)
31
+ "#{host}#{DELIMITER}#{port}"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Load and hashify node info for Redis Cluster Client
8
+ module NodeLoader
9
+ module_function
10
+
11
+ def load_flags(nodes)
12
+ info = {}
13
+
14
+ nodes.each do |node|
15
+ info = fetch_node_info(node)
16
+ info.empty? ? next : break
17
+ end
18
+
19
+ return info unless info.empty?
20
+
21
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
22
+ end
23
+
24
+ def fetch_node_info(node)
25
+ node.call(%i[cluster nodes])
26
+ .split("\n")
27
+ .map { |str| str.split(' ') }
28
+ .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
29
+ .to_h
30
+ rescue CannotConnectError, ConnectionError, CommandError
31
+ {} # can retry on another node
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'node_key'
5
+
6
+ class Redis
7
+ class Cluster
8
+ # Keep options for Redis Cluster Client
9
+ class Option
10
+ DEFAULT_SCHEME = 'redis'
11
+ SECURE_SCHEME = 'rediss'
12
+ VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
13
+
14
+ def initialize(options)
15
+ options = options.dup
16
+ node_addrs = options.delete(:cluster)
17
+ @node_uris = build_node_uris(node_addrs)
18
+ @replica = options.delete(:replica) == true
19
+ @options = options
20
+ end
21
+
22
+ def per_node_key
23
+ @node_uris.map { |uri| [NodeKey.build_from_uri(uri), @options.merge(url: uri.to_s)] }
24
+ .to_h
25
+ end
26
+
27
+ def secure?
28
+ @node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
29
+ end
30
+
31
+ def use_replica?
32
+ @replica
33
+ end
34
+
35
+ def update_node(addrs)
36
+ @node_uris = build_node_uris(addrs)
37
+ end
38
+
39
+ def add_node(host, port)
40
+ @node_uris << parse_node_hash(host: host, port: port)
41
+ end
42
+
43
+ private
44
+
45
+ def build_node_uris(addrs)
46
+ raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
47
+ addrs.map { |addr| parse_node_addr(addr) }
48
+ end
49
+
50
+ def parse_node_addr(addr)
51
+ case addr
52
+ when String
53
+ parse_node_url(addr)
54
+ when Hash
55
+ parse_node_hash(addr)
56
+ else
57
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
58
+ end
59
+ end
60
+
61
+ def parse_node_url(addr)
62
+ uri = URI(addr)
63
+ raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
64
+ uri
65
+ rescue URI::InvalidURIError => err
66
+ raise InvalidClientOptionError, err.message
67
+ end
68
+
69
+ def parse_node_hash(addr)
70
+ addr = addr.map { |k, v| [k.to_sym, v] }.to_h
71
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
72
+ URI::Generic.build(scheme: DEFAULT_SCHEME, host: addr[:host], port: addr[:port].to_i)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep slot and node key map for Redis Cluster Client
8
+ class Slot
9
+ ROLE_SLAVE = 'slave'
10
+
11
+ def initialize(available_slots, node_flags = {}, with_replica = false)
12
+ @with_replica = with_replica
13
+ @node_flags = node_flags
14
+ @map = build_slot_node_key_map(available_slots)
15
+ end
16
+
17
+ def exists?(slot)
18
+ @map.key?(slot)
19
+ end
20
+
21
+ def find_node_key_of_master(slot)
22
+ return nil unless exists?(slot)
23
+
24
+ @map[slot][:master]
25
+ end
26
+
27
+ def find_node_key_of_slave(slot)
28
+ return nil unless exists?(slot)
29
+ return find_node_key_of_master(slot) if replica_disabled?
30
+
31
+ @map[slot][:slaves].to_a.sample
32
+ end
33
+
34
+ def put(slot, node_key)
35
+ assign_node_key(@map, slot, node_key)
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ def replica_disabled?
42
+ !@with_replica
43
+ end
44
+
45
+ def master?(node_key)
46
+ !slave?(node_key)
47
+ end
48
+
49
+ def slave?(node_key)
50
+ @node_flags[node_key] == ROLE_SLAVE
51
+ end
52
+
53
+ def build_slot_node_key_map(available_slots)
54
+ available_slots.each_with_object({}) do |(node_key, slots), acc|
55
+ slots.each { |slot| assign_node_key(acc, slot, node_key) }
56
+ end
57
+ end
58
+
59
+ def assign_node_key(mappings, slot, node_key)
60
+ mappings[slot] ||= { master: nil, slaves: Set.new }
61
+ if master?(node_key)
62
+ mappings[slot][:master] = node_key
63
+ else
64
+ mappings[slot][:slaves].add(node_key)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end