redis 3.3.5 → 4.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/CHANGELOG.md +220 -2
- data/README.md +169 -89
- data/lib/redis/client.rb +176 -99
- data/lib/redis/cluster/command.rb +79 -0
- data/lib/redis/cluster/command_loader.rb +33 -0
- data/lib/redis/cluster/key_slot_converter.rb +72 -0
- data/lib/redis/cluster/node.rb +120 -0
- data/lib/redis/cluster/node_key.rb +31 -0
- data/lib/redis/cluster/node_loader.rb +34 -0
- data/lib/redis/cluster/option.rb +100 -0
- data/lib/redis/cluster/slot.rb +86 -0
- data/lib/redis/cluster/slot_loader.rb +46 -0
- data/lib/redis/cluster.rb +315 -0
- data/lib/redis/commands/bitmaps.rb +63 -0
- data/lib/redis/commands/cluster.rb +45 -0
- data/lib/redis/commands/connection.rb +58 -0
- data/lib/redis/commands/geo.rb +84 -0
- data/lib/redis/commands/hashes.rb +251 -0
- data/lib/redis/commands/hyper_log_log.rb +37 -0
- data/lib/redis/commands/keys.rb +411 -0
- data/lib/redis/commands/lists.rb +289 -0
- data/lib/redis/commands/pubsub.rb +72 -0
- data/lib/redis/commands/scripting.rb +114 -0
- data/lib/redis/commands/server.rb +188 -0
- data/lib/redis/commands/sets.rb +207 -0
- data/lib/redis/commands/sorted_sets.rb +812 -0
- data/lib/redis/commands/streams.rb +382 -0
- data/lib/redis/commands/strings.rb +313 -0
- data/lib/redis/commands/transactions.rb +139 -0
- data/lib/redis/commands.rb +242 -0
- data/lib/redis/connection/command_helper.rb +7 -10
- data/lib/redis/connection/hiredis.rb +5 -5
- data/lib/redis/connection/registry.rb +2 -1
- data/lib/redis/connection/ruby.rb +130 -128
- data/lib/redis/connection/synchrony.rb +24 -9
- data/lib/redis/connection.rb +3 -1
- data/lib/redis/distributed.rb +231 -72
- data/lib/redis/errors.rb +57 -0
- data/lib/redis/hash_ring.rb +30 -73
- data/lib/redis/pipeline.rb +178 -13
- data/lib/redis/subscribe.rb +11 -12
- data/lib/redis/version.rb +3 -1
- data/lib/redis.rb +173 -2661
- metadata +66 -202
- data/.gitignore +0 -16
- data/.travis/Gemfile +0 -11
- data/.travis.yml +0 -89
- data/.yardopts +0 -3
- data/Gemfile +0 -4
- data/Rakefile +0 -87
- data/benchmarking/logging.rb +0 -71
- data/benchmarking/pipeline.rb +0 -51
- data/benchmarking/speed.rb +0 -21
- data/benchmarking/suite.rb +0 -24
- data/benchmarking/worker.rb +0 -71
- data/examples/basic.rb +0 -15
- data/examples/consistency.rb +0 -114
- data/examples/dist_redis.rb +0 -43
- data/examples/incr-decr.rb +0 -17
- data/examples/list.rb +0 -26
- data/examples/pubsub.rb +0 -37
- data/examples/sentinel/sentinel.conf +0 -9
- data/examples/sentinel/start +0 -49
- data/examples/sentinel.rb +0 -41
- data/examples/sets.rb +0 -36
- data/examples/unicorn/config.ru +0 -3
- data/examples/unicorn/unicorn.rb +0 -20
- data/redis.gemspec +0 -44
- data/test/bitpos_test.rb +0 -69
- data/test/blocking_commands_test.rb +0 -42
- data/test/client_test.rb +0 -59
- data/test/command_map_test.rb +0 -30
- data/test/commands_on_hashes_test.rb +0 -21
- data/test/commands_on_hyper_log_log_test.rb +0 -21
- data/test/commands_on_lists_test.rb +0 -20
- data/test/commands_on_sets_test.rb +0 -77
- data/test/commands_on_sorted_sets_test.rb +0 -137
- data/test/commands_on_strings_test.rb +0 -101
- data/test/commands_on_value_types_test.rb +0 -133
- data/test/connection_handling_test.rb +0 -277
- data/test/connection_test.rb +0 -57
- data/test/db/.gitkeep +0 -0
- data/test/distributed_blocking_commands_test.rb +0 -46
- data/test/distributed_commands_on_hashes_test.rb +0 -10
- data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
- data/test/distributed_commands_on_lists_test.rb +0 -22
- data/test/distributed_commands_on_sets_test.rb +0 -83
- data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
- data/test/distributed_commands_on_strings_test.rb +0 -59
- data/test/distributed_commands_on_value_types_test.rb +0 -95
- data/test/distributed_commands_requiring_clustering_test.rb +0 -164
- data/test/distributed_connection_handling_test.rb +0 -23
- data/test/distributed_internals_test.rb +0 -79
- data/test/distributed_key_tags_test.rb +0 -52
- data/test/distributed_persistence_control_commands_test.rb +0 -26
- data/test/distributed_publish_subscribe_test.rb +0 -92
- data/test/distributed_remote_server_control_commands_test.rb +0 -66
- data/test/distributed_scripting_test.rb +0 -102
- data/test/distributed_sorting_test.rb +0 -20
- data/test/distributed_test.rb +0 -58
- data/test/distributed_transactions_test.rb +0 -32
- data/test/encoding_test.rb +0 -18
- data/test/error_replies_test.rb +0 -59
- data/test/fork_safety_test.rb +0 -65
- data/test/helper.rb +0 -232
- data/test/helper_test.rb +0 -24
- data/test/internals_test.rb +0 -417
- data/test/lint/blocking_commands.rb +0 -150
- data/test/lint/hashes.rb +0 -162
- data/test/lint/hyper_log_log.rb +0 -60
- data/test/lint/lists.rb +0 -143
- data/test/lint/sets.rb +0 -140
- data/test/lint/sorted_sets.rb +0 -316
- data/test/lint/strings.rb +0 -260
- data/test/lint/value_types.rb +0 -122
- data/test/persistence_control_commands_test.rb +0 -26
- data/test/pipelining_commands_test.rb +0 -242
- data/test/publish_subscribe_test.rb +0 -282
- data/test/remote_server_control_commands_test.rb +0 -118
- data/test/scanning_test.rb +0 -413
- data/test/scripting_test.rb +0 -78
- data/test/sentinel_command_test.rb +0 -80
- data/test/sentinel_test.rb +0 -255
- data/test/sorting_test.rb +0 -59
- data/test/ssl_test.rb +0 -73
- data/test/support/connection/hiredis.rb +0 -1
- data/test/support/connection/ruby.rb +0 -1
- data/test/support/connection/synchrony.rb +0 -17
- data/test/support/redis_mock.rb +0 -130
- data/test/support/ssl/gen_certs.sh +0 -31
- data/test/support/ssl/trusted-ca.crt +0 -25
- data/test/support/ssl/trusted-ca.key +0 -27
- data/test/support/ssl/trusted-cert.crt +0 -81
- data/test/support/ssl/trusted-cert.key +0 -28
- data/test/support/ssl/untrusted-ca.crt +0 -26
- data/test/support/ssl/untrusted-ca.key +0 -27
- data/test/support/ssl/untrusted-cert.crt +0 -82
- data/test/support/ssl/untrusted-cert.key +0 -28
- data/test/support/wire/synchrony.rb +0 -24
- data/test/support/wire/thread.rb +0 -5
- data/test/synchrony_driver.rb +0 -88
- data/test/test.conf.erb +0 -9
- data/test/thread_safety_test.rb +0 -62
- data/test/transactions_test.rb +0 -264
- data/test/unknown_commands_test.rb +0 -14
- data/test/url_param_test.rb +0 -138
@@ -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,120 @@
|
|
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
|
+
|
43
|
+
client.call(command, &block)
|
44
|
+
end.values
|
45
|
+
end
|
46
|
+
|
47
|
+
def call_slave(command, &block)
|
48
|
+
return call_master(command, &block) if replica_disabled?
|
49
|
+
|
50
|
+
try_map do |node_key, client|
|
51
|
+
next if master?(node_key)
|
52
|
+
|
53
|
+
client.call(command, &block)
|
54
|
+
end.values
|
55
|
+
end
|
56
|
+
|
57
|
+
def process_all(commands, &block)
|
58
|
+
try_map { |_, client| client.process(commands, &block) }.values
|
59
|
+
end
|
60
|
+
|
61
|
+
def scale_reading_clients
|
62
|
+
reading_clients = []
|
63
|
+
|
64
|
+
@clients.each do |node_key, client|
|
65
|
+
next unless replica_disabled? ? master?(node_key) : slave?(node_key)
|
66
|
+
|
67
|
+
reading_clients << client
|
68
|
+
end
|
69
|
+
|
70
|
+
reading_clients
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def replica_disabled?
|
76
|
+
!@with_replica
|
77
|
+
end
|
78
|
+
|
79
|
+
def master?(node_key)
|
80
|
+
!slave?(node_key)
|
81
|
+
end
|
82
|
+
|
83
|
+
def slave?(node_key)
|
84
|
+
@node_flags[node_key] == ROLE_SLAVE
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_clients(options)
|
88
|
+
clients = options.map do |node_key, option|
|
89
|
+
next if replica_disabled? && slave?(node_key)
|
90
|
+
|
91
|
+
option = option.merge(readonly: true) if slave?(node_key)
|
92
|
+
|
93
|
+
client = Client.new(option)
|
94
|
+
[node_key, client]
|
95
|
+
end
|
96
|
+
|
97
|
+
clients.compact.to_h
|
98
|
+
end
|
99
|
+
|
100
|
+
def try_map
|
101
|
+
errors = {}
|
102
|
+
results = {}
|
103
|
+
|
104
|
+
@clients.each do |node_key, client|
|
105
|
+
begin
|
106
|
+
reply = yield(node_key, client)
|
107
|
+
results[node_key] = reply unless reply.nil?
|
108
|
+
rescue CommandError => err
|
109
|
+
errors[node_key] = err
|
110
|
+
next
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
return results if errors.empty?
|
115
|
+
|
116
|
+
raise CommandErrorCollection, errors
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,31 @@
|
|
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
|
+
DELIMITER = ':'
|
10
|
+
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def optionize(node_key)
|
14
|
+
host, port = split(node_key)
|
15
|
+
{ host: host, port: port }
|
16
|
+
end
|
17
|
+
|
18
|
+
def split(node_key)
|
19
|
+
node_key.split(DELIMITER)
|
20
|
+
end
|
21
|
+
|
22
|
+
def build_from_uri(uri)
|
23
|
+
"#{uri.host}#{DELIMITER}#{uri.port}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_from_host_port(host, port)
|
27
|
+
"#{host}#{DELIMITER}#{port}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis/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
|
+
errors = nodes.map do |node|
|
13
|
+
begin
|
14
|
+
return fetch_node_info(node)
|
15
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
16
|
+
error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
raise InitialSetupError, errors
|
21
|
+
end
|
22
|
+
|
23
|
+
def fetch_node_info(node)
|
24
|
+
node.call(%i[cluster nodes])
|
25
|
+
.split("\n")
|
26
|
+
.map { |str| str.split(' ') }
|
27
|
+
.map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
|
28
|
+
.to_h
|
29
|
+
end
|
30
|
+
|
31
|
+
private_class_method :fetch_node_info
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../errors'
|
4
|
+
require_relative 'node_key'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
class Redis
|
8
|
+
class Cluster
|
9
|
+
# Keep options for Redis Cluster Client
|
10
|
+
class Option
|
11
|
+
DEFAULT_SCHEME = 'redis'
|
12
|
+
SECURE_SCHEME = 'rediss'
|
13
|
+
VALID_SCHEMES = [DEFAULT_SCHEME, SECURE_SCHEME].freeze
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
options = options.dup
|
17
|
+
node_addrs = options.delete(:cluster)
|
18
|
+
@node_opts = build_node_options(node_addrs)
|
19
|
+
@replica = options.delete(:replica) == true
|
20
|
+
@fixed_hostname = options.delete(:fixed_hostname)
|
21
|
+
add_common_node_option_if_needed(options, @node_opts, :scheme)
|
22
|
+
add_common_node_option_if_needed(options, @node_opts, :username)
|
23
|
+
add_common_node_option_if_needed(options, @node_opts, :password)
|
24
|
+
@options = options
|
25
|
+
end
|
26
|
+
|
27
|
+
def per_node_key
|
28
|
+
@node_opts.map do |opt|
|
29
|
+
node_key = NodeKey.build_from_host_port(opt[:host], opt[:port])
|
30
|
+
options = @options.merge(opt)
|
31
|
+
options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
|
32
|
+
[node_key, options]
|
33
|
+
end.to_h
|
34
|
+
end
|
35
|
+
|
36
|
+
def use_replica?
|
37
|
+
@replica
|
38
|
+
end
|
39
|
+
|
40
|
+
def update_node(addrs)
|
41
|
+
@node_opts = build_node_options(addrs)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add_node(host, port)
|
45
|
+
@node_opts << { host: host, port: port }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def build_node_options(addrs)
|
51
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
|
52
|
+
|
53
|
+
addrs.map { |addr| parse_node_addr(addr) }
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_node_addr(addr)
|
57
|
+
case addr
|
58
|
+
when String
|
59
|
+
parse_node_url(addr)
|
60
|
+
when Hash
|
61
|
+
parse_node_option(addr)
|
62
|
+
else
|
63
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def parse_node_url(addr)
|
68
|
+
uri = URI(addr)
|
69
|
+
raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
|
70
|
+
|
71
|
+
db = uri.path.split('/')[1]&.to_i
|
72
|
+
username = uri.user ? URI.decode_www_form_component(uri.user) : nil
|
73
|
+
password = uri.password ? URI.decode_www_form_component(uri.password) : nil
|
74
|
+
|
75
|
+
{ scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db }
|
76
|
+
.reject { |_, v| v.nil? || v == '' }
|
77
|
+
rescue URI::InvalidURIError => err
|
78
|
+
raise InvalidClientOptionError, err.message
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_node_option(addr)
|
82
|
+
addr = addr.map { |k, v| [k.to_sym, v] }.to_h
|
83
|
+
if addr.values_at(:host, :port).any?(&:nil?)
|
84
|
+
raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
|
85
|
+
end
|
86
|
+
|
87
|
+
addr
|
88
|
+
end
|
89
|
+
|
90
|
+
# Redis cluster node returns only host and port information.
|
91
|
+
# So we should complement additional information such as:
|
92
|
+
# scheme, username, password and so on.
|
93
|
+
def add_common_node_option_if_needed(options, node_opts, key)
|
94
|
+
return options if options[key].nil? && node_opts.first[key].nil?
|
95
|
+
|
96
|
+
options[key] ||= node_opts.first[key]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
class Cluster
|
5
|
+
# Keep slot and node key map for Redis Cluster Client
|
6
|
+
class Slot
|
7
|
+
ROLE_SLAVE = 'slave'
|
8
|
+
|
9
|
+
def initialize(available_slots, node_flags = {}, with_replica = false)
|
10
|
+
@with_replica = with_replica
|
11
|
+
@node_flags = node_flags
|
12
|
+
@map = build_slot_node_key_map(available_slots)
|
13
|
+
end
|
14
|
+
|
15
|
+
def exists?(slot)
|
16
|
+
@map.key?(slot)
|
17
|
+
end
|
18
|
+
|
19
|
+
def find_node_key_of_master(slot)
|
20
|
+
return nil unless exists?(slot)
|
21
|
+
|
22
|
+
@map[slot][:master]
|
23
|
+
end
|
24
|
+
|
25
|
+
def find_node_key_of_slave(slot)
|
26
|
+
return nil unless exists?(slot)
|
27
|
+
return find_node_key_of_master(slot) if replica_disabled?
|
28
|
+
|
29
|
+
@map[slot][:slaves].sample
|
30
|
+
end
|
31
|
+
|
32
|
+
def put(slot, node_key)
|
33
|
+
# Since we're sharing a hash for build_slot_node_key_map, duplicate it
|
34
|
+
# if it already exists instead of preserving as-is.
|
35
|
+
@map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
|
36
|
+
|
37
|
+
if master?(node_key)
|
38
|
+
@map[slot][:master] = node_key
|
39
|
+
elsif !@map[slot][:slaves].include?(node_key)
|
40
|
+
@map[slot][:slaves] << node_key
|
41
|
+
end
|
42
|
+
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def replica_disabled?
|
49
|
+
!@with_replica
|
50
|
+
end
|
51
|
+
|
52
|
+
def master?(node_key)
|
53
|
+
!slave?(node_key)
|
54
|
+
end
|
55
|
+
|
56
|
+
def slave?(node_key)
|
57
|
+
@node_flags[node_key] == ROLE_SLAVE
|
58
|
+
end
|
59
|
+
|
60
|
+
# available_slots is mapping of node_key to list of slot ranges
|
61
|
+
def build_slot_node_key_map(available_slots)
|
62
|
+
by_ranges = {}
|
63
|
+
available_slots.each do |node_key, slots_arr|
|
64
|
+
by_ranges[slots_arr] ||= { master: nil, slaves: [] }
|
65
|
+
|
66
|
+
if master?(node_key)
|
67
|
+
by_ranges[slots_arr][:master] = node_key
|
68
|
+
elsif !by_ranges[slots_arr][:slaves].include?(node_key)
|
69
|
+
by_ranges[slots_arr][:slaves] << node_key
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
by_slot = {}
|
74
|
+
by_ranges.each do |slots_arr, nodes|
|
75
|
+
slots_arr.each do |slots|
|
76
|
+
slots.each do |slot|
|
77
|
+
by_slot[slot] = nodes
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
by_slot
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis/errors'
|
4
|
+
require 'redis/cluster/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
|
+
errors = nodes.map do |node|
|
14
|
+
begin
|
15
|
+
return fetch_slot_info(node)
|
16
|
+
rescue CannotConnectError, ConnectionError, CommandError => error
|
17
|
+
error
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
raise InitialSetupError, errors
|
22
|
+
end
|
23
|
+
|
24
|
+
def fetch_slot_info(node)
|
25
|
+
hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
|
26
|
+
node.call(%i[cluster slots])
|
27
|
+
.flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
|
28
|
+
.each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def parse_slot_info(arr, default_ip:)
|
32
|
+
first_slot, last_slot = arr[0..1]
|
33
|
+
slot_range = (first_slot..last_slot).freeze
|
34
|
+
arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
|
35
|
+
end
|
36
|
+
|
37
|
+
def stringify_node_key(arr, default_ip)
|
38
|
+
ip, port = arr
|
39
|
+
ip = default_ip if ip.empty? # When cluster is down
|
40
|
+
NodeKey.build_from_host_port(ip, port)
|
41
|
+
end
|
42
|
+
|
43
|
+
private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|