redis 4.0.1 → 4.0.3

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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +17 -29
  4. data/.travis/Gemfile +5 -0
  5. data/CHANGELOG.md +29 -0
  6. data/Gemfile +5 -0
  7. data/README.md +1 -1
  8. data/bin/build +71 -0
  9. data/lib/redis.rb +198 -12
  10. data/lib/redis/client.rb +26 -12
  11. data/lib/redis/cluster.rb +285 -0
  12. data/lib/redis/cluster/command.rb +81 -0
  13. data/lib/redis/cluster/command_loader.rb +32 -0
  14. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  15. data/lib/redis/cluster/node.rb +104 -0
  16. data/lib/redis/cluster/node_key.rb +35 -0
  17. data/lib/redis/cluster/node_loader.rb +35 -0
  18. data/lib/redis/cluster/option.rb +76 -0
  19. data/lib/redis/cluster/slot.rb +69 -0
  20. data/lib/redis/cluster/slot_loader.rb +47 -0
  21. data/lib/redis/connection/ruby.rb +5 -2
  22. data/lib/redis/distributed.rb +10 -2
  23. data/lib/redis/errors.rb +46 -0
  24. data/lib/redis/pipeline.rb +9 -1
  25. data/lib/redis/version.rb +1 -1
  26. data/makefile +54 -22
  27. data/redis.gemspec +2 -1
  28. data/test/client_test.rb +17 -0
  29. data/test/cluster_abnormal_state_test.rb +38 -0
  30. data/test/cluster_blocking_commands_test.rb +15 -0
  31. data/test/cluster_client_internals_test.rb +77 -0
  32. data/test/cluster_client_key_hash_tags_test.rb +88 -0
  33. data/test/cluster_client_options_test.rb +147 -0
  34. data/test/cluster_client_pipelining_test.rb +59 -0
  35. data/test/cluster_client_replicas_test.rb +36 -0
  36. data/test/cluster_client_slots_test.rb +94 -0
  37. data/test/cluster_client_transactions_test.rb +71 -0
  38. data/test/cluster_commands_on_cluster_test.rb +165 -0
  39. data/test/cluster_commands_on_connection_test.rb +40 -0
  40. data/test/cluster_commands_on_geo_test.rb +74 -0
  41. data/test/cluster_commands_on_hashes_test.rb +11 -0
  42. data/test/cluster_commands_on_hyper_log_log_test.rb +17 -0
  43. data/test/cluster_commands_on_keys_test.rb +134 -0
  44. data/test/cluster_commands_on_lists_test.rb +15 -0
  45. data/test/cluster_commands_on_pub_sub_test.rb +101 -0
  46. data/test/cluster_commands_on_scripting_test.rb +56 -0
  47. data/test/cluster_commands_on_server_test.rb +221 -0
  48. data/test/cluster_commands_on_sets_test.rb +39 -0
  49. data/test/cluster_commands_on_sorted_sets_test.rb +35 -0
  50. data/test/cluster_commands_on_streams_test.rb +196 -0
  51. data/test/cluster_commands_on_strings_test.rb +15 -0
  52. data/test/cluster_commands_on_transactions_test.rb +41 -0
  53. data/test/cluster_commands_on_value_types_test.rb +14 -0
  54. data/test/commands_on_geo_test.rb +116 -0
  55. data/test/commands_on_hashes_test.rb +2 -14
  56. data/test/commands_on_hyper_log_log_test.rb +2 -14
  57. data/test/commands_on_lists_test.rb +2 -13
  58. data/test/commands_on_sets_test.rb +2 -70
  59. data/test/commands_on_sorted_sets_test.rb +2 -145
  60. data/test/commands_on_strings_test.rb +2 -94
  61. data/test/commands_on_value_types_test.rb +36 -0
  62. data/test/distributed_blocking_commands_test.rb +8 -0
  63. data/test/distributed_commands_on_hashes_test.rb +16 -3
  64. data/test/distributed_commands_on_hyper_log_log_test.rb +8 -13
  65. data/test/distributed_commands_on_lists_test.rb +4 -5
  66. data/test/distributed_commands_on_sets_test.rb +45 -46
  67. data/test/distributed_commands_on_sorted_sets_test.rb +51 -8
  68. data/test/distributed_commands_on_strings_test.rb +10 -0
  69. data/test/distributed_commands_on_value_types_test.rb +36 -0
  70. data/test/helper.rb +176 -32
  71. data/test/internals_test.rb +20 -1
  72. data/test/lint/blocking_commands.rb +40 -16
  73. data/test/lint/hashes.rb +41 -0
  74. data/test/lint/hyper_log_log.rb +15 -1
  75. data/test/lint/lists.rb +16 -0
  76. data/test/lint/sets.rb +142 -0
  77. data/test/lint/sorted_sets.rb +183 -2
  78. data/test/lint/strings.rb +102 -0
  79. data/test/pipelining_commands_test.rb +8 -0
  80. data/test/support/cluster/orchestrator.rb +199 -0
  81. data/test/support/redis_mock.rb +1 -1
  82. data/test/transactions_test.rb +10 -0
  83. metadata +81 -2
@@ -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
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'node_key'
5
+
6
+ class Redis
7
+ class Cluster
8
+ # Load and hashify slot info for Redis Cluster Client
9
+ module SlotLoader
10
+ module_function
11
+
12
+ def load(nodes)
13
+ info = {}
14
+
15
+ nodes.each do |node|
16
+ info = Hash[*fetch_slot_info(node)]
17
+ info.empty? ? next : break
18
+ end
19
+
20
+ return info unless info.empty?
21
+
22
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
23
+ end
24
+
25
+ def fetch_slot_info(node)
26
+ node.call(%i[cluster slots])
27
+ .map { |arr| parse_slot_info(arr, default_ip: node.host) }
28
+ .flatten
29
+ rescue CannotConnectError, ConnectionError, CommandError
30
+ {} # can retry on another node
31
+ end
32
+
33
+ def parse_slot_info(arr, default_ip:)
34
+ first_slot, last_slot = arr[0..1]
35
+ slot_range = (first_slot..last_slot).freeze
36
+ arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
37
+ .flatten
38
+ end
39
+
40
+ def stringify_node_key(arr, default_ip)
41
+ ip, port = arr
42
+ ip = default_ip if ip.empty? # When cluster is down
43
+ NodeKey.build_from_host_port(ip, port)
44
+ end
45
+ end
46
+ end
47
+ end