redis-cluster-client 0.11.0 → 0.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/redis_client/cluster/command.rb +47 -75
- data/lib/redis_client/cluster/concurrent_worker/on_demand.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker/pooled.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker.rb +4 -6
- data/lib/redis_client/cluster/errors.rb +39 -19
- data/lib/redis_client/cluster/key_slot_converter.rb +3 -1
- data/lib/redis_client/cluster/node/base_topology.rb +3 -1
- data/lib/redis_client/cluster/node/latency_replica.rb +3 -1
- data/lib/redis_client/cluster/node.rb +77 -14
- data/lib/redis_client/cluster/node_key.rb +2 -0
- data/lib/redis_client/cluster/noop_command_builder.rb +13 -0
- data/lib/redis_client/cluster/optimistic_locking.rb +25 -10
- data/lib/redis_client/cluster/pipeline.rb +53 -18
- data/lib/redis_client/cluster/pub_sub.rb +70 -31
- data/lib/redis_client/cluster/router.rb +282 -134
- data/lib/redis_client/cluster/transaction.rb +22 -11
- data/lib/redis_client/cluster.rb +24 -15
- data/lib/redis_client/cluster_config.rb +44 -11
- metadata +7 -11
- data/lib/redis_client/cluster/normalized_cmd_name.rb +0 -69
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0abe95ee2079026038121cd93e8ae7929726ee4206b3d1f401dc7ed82f1da2c8
|
4
|
+
data.tar.gz: 7fa2e9c6c20d814f54e2fcf8045094869b4b5fc6c39144a8900328a9f5401abc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26be35f5eb57e7280b8a38441f3d80c4956638a68418cf6964bdbf27d4cb3051f5955179b90066409c908ce771f3ee4286d0e569711d198ffcbfc22a70cc49bf
|
7
|
+
data.tar.gz: 976db1b41a2d3592dac70bf25ea8f44308824bbc7009082dd54fe68ad01b934e1b572b89df1f3b9e45b791851966c909a022defae45f4d9c3270469e9d6acc88
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'redis_client'
|
4
4
|
require 'redis_client/cluster/errors'
|
5
5
|
require 'redis_client/cluster/key_slot_converter'
|
6
|
-
require 'redis_client/cluster/normalized_cmd_name'
|
7
6
|
|
8
7
|
class RedisClient
|
9
8
|
class Cluster
|
@@ -12,10 +11,11 @@ class RedisClient
|
|
12
11
|
EMPTY_HASH = {}.freeze
|
13
12
|
EMPTY_ARRAY = [].freeze
|
14
13
|
|
14
|
+
private_constant :EMPTY_STRING, :EMPTY_HASH, :EMPTY_ARRAY
|
15
|
+
|
15
16
|
Detail = Struct.new(
|
16
17
|
'RedisCommand',
|
17
18
|
:first_key_position,
|
18
|
-
:last_key_position,
|
19
19
|
:key_step,
|
20
20
|
:write?,
|
21
21
|
:readonly?,
|
@@ -23,13 +23,13 @@ class RedisClient
|
|
23
23
|
)
|
24
24
|
|
25
25
|
class << self
|
26
|
-
def load(nodes, slow_command_timeout: -1)
|
26
|
+
def load(nodes, slow_command_timeout: -1) # rubocop:disable Metrics/AbcSize
|
27
27
|
cmd = errors = nil
|
28
28
|
|
29
29
|
nodes&.each do |node|
|
30
30
|
regular_timeout = node.read_timeout
|
31
31
|
node.read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
|
32
|
-
reply = node.call('
|
32
|
+
reply = node.call('command')
|
33
33
|
node.read_timeout = regular_timeout
|
34
34
|
commands = parse_command_reply(reply)
|
35
35
|
cmd = ::RedisClient::Cluster::Command.new(commands)
|
@@ -41,20 +41,33 @@ class RedisClient
|
|
41
41
|
|
42
42
|
return cmd unless cmd.nil?
|
43
43
|
|
44
|
-
raise ::RedisClient::Cluster::InitialSetupError
|
44
|
+
raise ::RedisClient::Cluster::InitialSetupError.from_errors(errors)
|
45
45
|
end
|
46
46
|
|
47
47
|
private
|
48
48
|
|
49
|
-
def parse_command_reply(rows)
|
49
|
+
def parse_command_reply(rows) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
50
50
|
rows&.each_with_object({}) do |row, acc|
|
51
|
-
next if row
|
51
|
+
next if row.first.nil?
|
52
|
+
|
53
|
+
# TODO: in redis 7.0 or later, subcommand information included in the command reply
|
54
|
+
|
55
|
+
pos = case row.first
|
56
|
+
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
|
57
|
+
when 'object', 'xgroup' then 2
|
58
|
+
when 'migrate', 'xread', 'xreadgroup' then 0
|
59
|
+
else row[3]
|
60
|
+
end
|
52
61
|
|
53
|
-
|
54
|
-
|
55
|
-
|
62
|
+
writable = case row.first
|
63
|
+
when 'xgroup' then true
|
64
|
+
else row[2].include?('write')
|
65
|
+
end
|
66
|
+
|
67
|
+
acc[row.first] = ::RedisClient::Cluster::Command::Detail.new(
|
68
|
+
first_key_position: pos,
|
56
69
|
key_step: row[5],
|
57
|
-
write?:
|
70
|
+
write?: writable,
|
58
71
|
readonly?: row[2].include?('readonly')
|
59
72
|
)
|
60
73
|
end.freeze || EMPTY_HASH
|
@@ -69,89 +82,48 @@ class RedisClient
|
|
69
82
|
i = determine_first_key_position(command)
|
70
83
|
return EMPTY_STRING if i == 0
|
71
84
|
|
72
|
-
|
73
|
-
end
|
74
|
-
|
75
|
-
def extract_all_keys(command)
|
76
|
-
keys_start = determine_first_key_position(command)
|
77
|
-
keys_end = determine_last_key_position(command, keys_start)
|
78
|
-
keys_step = determine_key_step(command)
|
79
|
-
return EMPTY_ARRAY if [keys_start, keys_end, keys_step].any?(&:zero?)
|
80
|
-
|
81
|
-
keys_end = [keys_end, command.size - 1].min
|
82
|
-
# use .. inclusive range because keys_end is a valid index.
|
83
|
-
(keys_start..keys_end).step(keys_step).map { |i| command[i] }
|
85
|
+
command[i]
|
84
86
|
end
|
85
87
|
|
86
88
|
def should_send_to_primary?(command)
|
87
|
-
|
88
|
-
@commands[name]&.write?
|
89
|
+
find_command_info(command.first)&.write?
|
89
90
|
end
|
90
91
|
|
91
92
|
def should_send_to_replica?(command)
|
92
|
-
|
93
|
-
@commands[name]&.readonly?
|
93
|
+
find_command_info(command.first)&.readonly?
|
94
94
|
end
|
95
95
|
|
96
96
|
def exists?(name)
|
97
|
-
@commands.key?(
|
97
|
+
@commands.key?(name) || @commands.key?(name.to_s.downcase(:ascii))
|
98
98
|
end
|
99
99
|
|
100
100
|
private
|
101
101
|
|
102
|
-
def
|
103
|
-
|
104
|
-
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
|
105
|
-
when 'object' then 2
|
106
|
-
when 'memory'
|
107
|
-
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
108
|
-
when 'migrate'
|
109
|
-
command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
|
110
|
-
when 'xread', 'xreadgroup'
|
111
|
-
determine_optional_key_position(command, 'streams')
|
112
|
-
else
|
113
|
-
@commands[name]&.first_key_position.to_i
|
114
|
-
end
|
102
|
+
def find_command_info(name)
|
103
|
+
@commands[name] || @commands[name.to_s.downcase(:ascii)]
|
115
104
|
end
|
116
105
|
|
117
|
-
#
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
command
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
when 'migrate'
|
131
|
-
# MIGRATE host port <key | ""> destination-db timeout [COPY] [REPLACE] [AUTH password | AUTH2 username password] [KEYS key [key ...]]
|
132
|
-
command[3].empty? ? (command.length - 1) : 3
|
133
|
-
when 'xread', 'xreadgroup'
|
134
|
-
# XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
|
135
|
-
keys_start + ((command.length - keys_start) / 2) - 1
|
106
|
+
def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
|
107
|
+
i = find_command_info(command.first)&.first_key_position.to_i
|
108
|
+
return i if i > 0
|
109
|
+
|
110
|
+
cmd_name = command.first
|
111
|
+
if cmd_name.casecmp('xread').zero?
|
112
|
+
determine_optional_key_position(command, 'streams')
|
113
|
+
elsif cmd_name.casecmp('xreadgroup').zero?
|
114
|
+
determine_optional_key_position(command, 'streams')
|
115
|
+
elsif cmd_name.casecmp('migrate').zero?
|
116
|
+
command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
|
117
|
+
elsif cmd_name.casecmp('memory').zero?
|
118
|
+
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
136
119
|
else
|
137
|
-
|
138
|
-
if @commands[name].last_key_position >= 0
|
139
|
-
@commands[name].last_key_position
|
140
|
-
else
|
141
|
-
command.length + @commands[name].last_key_position
|
142
|
-
end
|
120
|
+
i
|
143
121
|
end
|
144
122
|
end
|
145
123
|
|
146
|
-
def determine_optional_key_position(command, option_name)
|
147
|
-
|
148
|
-
|
149
|
-
end
|
150
|
-
|
151
|
-
def determine_key_step(command)
|
152
|
-
name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
153
|
-
# Some commands like EVALSHA have zero as the step in COMMANDS somehow.
|
154
|
-
@commands[name].key_step == 0 ? 1 : @commands[name].key_step
|
124
|
+
def determine_optional_key_position(command, option_name)
|
125
|
+
i = command.index { |v| v.to_s.casecmp(option_name).zero? }
|
126
|
+
i.nil? ? 0 : i + 1
|
155
127
|
end
|
156
128
|
end
|
157
129
|
end
|
@@ -71,14 +71,12 @@ class RedisClient
|
|
71
71
|
|
72
72
|
module_function
|
73
73
|
|
74
|
-
def create(model: :
|
75
|
-
size = size.positive? ? size : 5
|
76
|
-
|
74
|
+
def create(model: :none, size: 5)
|
77
75
|
case model
|
78
|
-
when :on_demand, nil then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
|
79
|
-
when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
|
80
76
|
when :none then ::RedisClient::Cluster::ConcurrentWorker::None.new
|
81
|
-
|
77
|
+
when :on_demand then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
|
78
|
+
when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
|
79
|
+
else raise ArgumentError, "unknown model: #{model}"
|
82
80
|
end
|
83
81
|
end
|
84
82
|
end
|
@@ -4,50 +4,70 @@ require 'redis_client'
|
|
4
4
|
|
5
5
|
class RedisClient
|
6
6
|
class Cluster
|
7
|
+
class Error < ::RedisClient::Error
|
8
|
+
def with_config(config)
|
9
|
+
@config = config
|
10
|
+
self
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
7
14
|
ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
|
15
|
+
Ractor.make_shareable(ERR_ARG_NORMALIZATION) if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
|
16
|
+
|
17
|
+
private_constant :ERR_ARG_NORMALIZATION
|
8
18
|
|
9
|
-
class InitialSetupError <
|
10
|
-
def
|
19
|
+
class InitialSetupError < Error
|
20
|
+
def self.from_errors(errors)
|
11
21
|
msg = ERR_ARG_NORMALIZATION.call(errors).map(&:message).uniq.join(',')
|
12
|
-
|
22
|
+
new("Redis client could not fetch cluster information: #{msg}")
|
13
23
|
end
|
14
24
|
end
|
15
25
|
|
16
|
-
class OrchestrationCommandNotSupported <
|
17
|
-
def
|
26
|
+
class OrchestrationCommandNotSupported < Error
|
27
|
+
def self.from_command(command)
|
18
28
|
str = ERR_ARG_NORMALIZATION.call(command).map(&:to_s).join(' ').upcase
|
19
29
|
msg = "#{str} command should be used with care " \
|
20
30
|
'only by applications orchestrating Redis Cluster, like redis-cli, ' \
|
21
31
|
'and the command if used out of the right context can leave the cluster ' \
|
22
32
|
'in a wrong state or cause data loss.'
|
23
|
-
|
33
|
+
new(msg)
|
24
34
|
end
|
25
35
|
end
|
26
36
|
|
27
|
-
class ErrorCollection <
|
37
|
+
class ErrorCollection < Error
|
38
|
+
EMPTY_HASH = {}.freeze
|
39
|
+
|
40
|
+
private_constant :EMPTY_HASH
|
28
41
|
attr_reader :errors
|
29
42
|
|
30
|
-
def
|
31
|
-
@errors = {}
|
43
|
+
def self.with_errors(errors)
|
32
44
|
if !errors.is_a?(Hash) || errors.empty?
|
33
|
-
|
34
|
-
|
45
|
+
new(errors.to_s).with_errors(EMPTY_HASH)
|
46
|
+
else
|
47
|
+
messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }.freeze
|
48
|
+
new(messages.join(', ')).with_errors(errors)
|
35
49
|
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(error_message = nil)
|
53
|
+
@errors = nil
|
54
|
+
super
|
55
|
+
end
|
36
56
|
|
37
|
-
|
38
|
-
|
39
|
-
|
57
|
+
def with_errors(errors)
|
58
|
+
@errors = errors if @errors.nil?
|
59
|
+
self
|
40
60
|
end
|
41
61
|
end
|
42
62
|
|
43
|
-
class AmbiguousNodeError <
|
44
|
-
def
|
45
|
-
|
63
|
+
class AmbiguousNodeError < Error
|
64
|
+
def self.from_command(command)
|
65
|
+
new("Cluster client doesn't know which node the #{command} command should be sent to.")
|
46
66
|
end
|
47
67
|
end
|
48
68
|
|
49
|
-
class NodeMightBeDown <
|
50
|
-
def initialize(
|
69
|
+
class NodeMightBeDown < Error
|
70
|
+
def initialize(_error_message = nil)
|
51
71
|
super(
|
52
72
|
'The client is trying to fetch the latest cluster state ' \
|
53
73
|
'because a subset of nodes might be down. ' \
|
@@ -3,6 +3,7 @@
|
|
3
3
|
class RedisClient
|
4
4
|
class Cluster
|
5
5
|
module KeySlotConverter
|
6
|
+
HASH_SLOTS = 16_384
|
6
7
|
EMPTY_STRING = ''
|
7
8
|
LEFT_BRACKET = '{'
|
8
9
|
RIGHT_BRACKET = '}'
|
@@ -41,7 +42,8 @@ class RedisClient
|
|
41
42
|
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
42
43
|
].freeze
|
43
44
|
|
44
|
-
HASH_SLOTS
|
45
|
+
private_constant :HASH_SLOTS, :EMPTY_STRING,
|
46
|
+
:LEFT_BRACKET, :RIGHT_BRACKET, :XMODEM_CRC16_LOOKUP
|
45
47
|
|
46
48
|
module_function
|
47
49
|
|
@@ -8,6 +8,8 @@ class RedisClient
|
|
8
8
|
EMPTY_HASH = {}.freeze
|
9
9
|
EMPTY_ARRAY = [].freeze
|
10
10
|
|
11
|
+
private_constant :IGNORE_GENERIC_CONFIG_KEYS, :EMPTY_HASH, :EMPTY_ARRAY
|
12
|
+
|
11
13
|
attr_reader :clients, :primary_clients, :replica_clients
|
12
14
|
|
13
15
|
def initialize(pool, concurrent_worker, **kwargs)
|
@@ -52,7 +54,7 @@ class RedisClient
|
|
52
54
|
def connect_to_new_nodes(options)
|
53
55
|
(options.keys - @clients.keys).each do |node_key|
|
54
56
|
option = options[node_key].merge(@client_options)
|
55
|
-
config = ::RedisClient::Cluster::Node::Config.new(scale_read:
|
57
|
+
config = ::RedisClient::Cluster::Node::Config.new(scale_read: @replica_node_keys.include?(node_key), **option)
|
56
58
|
client = @pool.nil? ? config.new_client : config.new_pool(**@pool)
|
57
59
|
@clients[node_key] = client
|
58
60
|
end
|
@@ -9,6 +9,8 @@ class RedisClient
|
|
9
9
|
DUMMY_LATENCY_MSEC = 100 * 1000 * 1000
|
10
10
|
MEASURE_ATTEMPT_COUNT = 10
|
11
11
|
|
12
|
+
private_constant :DUMMY_LATENCY_MSEC, :MEASURE_ATTEMPT_COUNT
|
13
|
+
|
12
14
|
def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
13
15
|
@clients_for_scanning
|
14
16
|
end
|
@@ -45,7 +47,7 @@ class RedisClient
|
|
45
47
|
min = DUMMY_LATENCY_MSEC
|
46
48
|
MEASURE_ATTEMPT_COUNT.times do
|
47
49
|
starting = obtain_current_time
|
48
|
-
cli.call_once('
|
50
|
+
cli.call_once('ping')
|
49
51
|
duration = obtain_current_time - starting
|
50
52
|
min = duration if duration < min
|
51
53
|
end
|
@@ -7,6 +7,7 @@ require 'redis_client/cluster/node/primary_only'
|
|
7
7
|
require 'redis_client/cluster/node/random_replica'
|
8
8
|
require 'redis_client/cluster/node/random_replica_or_primary'
|
9
9
|
require 'redis_client/cluster/node/latency_replica'
|
10
|
+
require 'redis_client/cluster/node_key'
|
10
11
|
|
11
12
|
class RedisClient
|
12
13
|
class Cluster
|
@@ -23,8 +24,12 @@ class RedisClient
|
|
23
24
|
ROLE_FLAGS = %w[master slave].freeze
|
24
25
|
EMPTY_ARRAY = [].freeze
|
25
26
|
EMPTY_HASH = {}.freeze
|
27
|
+
STATE_REFRESH_INTERVAL = (3..10).freeze
|
26
28
|
|
27
|
-
|
29
|
+
private_constant :USE_CHAR_ARRAY_SLOT, :SLOT_SIZE, :MIN_SLOT, :MAX_SLOT,
|
30
|
+
:DEAD_FLAGS, :ROLE_FLAGS, :EMPTY_ARRAY, :EMPTY_HASH
|
31
|
+
|
32
|
+
ReloadNeeded = Class.new(::RedisClient::Cluster::Error)
|
28
33
|
|
29
34
|
Info = Struct.new(
|
30
35
|
'RedisClusterNode',
|
@@ -39,12 +44,18 @@ class RedisClient
|
|
39
44
|
def replica?
|
40
45
|
role == 'slave'
|
41
46
|
end
|
47
|
+
|
48
|
+
def serialize(str)
|
49
|
+
str << id << node_key << role << primary_id << config_epoch
|
50
|
+
end
|
42
51
|
end
|
43
52
|
|
44
53
|
class CharArray
|
45
54
|
BASE = ''
|
46
55
|
PADDING = '0'
|
47
56
|
|
57
|
+
private_constant :BASE, :PADDING
|
58
|
+
|
48
59
|
def initialize(size, elements)
|
49
60
|
@elements = elements
|
50
61
|
@string = String.new(BASE, encoding: Encoding::BINARY, capacity: size)
|
@@ -80,11 +91,9 @@ class RedisClient
|
|
80
91
|
super(**kwargs)
|
81
92
|
end
|
82
93
|
|
83
|
-
|
84
|
-
|
85
|
-
def build_connection_prelude
|
94
|
+
def connection_prelude
|
86
95
|
prelude = super.dup
|
87
|
-
prelude << ['
|
96
|
+
prelude << ['readonly'] if @scale_read
|
88
97
|
prelude.freeze
|
89
98
|
end
|
90
99
|
end
|
@@ -98,6 +107,8 @@ class RedisClient
|
|
98
107
|
@config = config
|
99
108
|
@mutex = Mutex.new
|
100
109
|
@last_reloaded_at = nil
|
110
|
+
@reload_times = 0
|
111
|
+
@random = Random.new
|
101
112
|
end
|
102
113
|
|
103
114
|
def inspect
|
@@ -140,7 +151,7 @@ class RedisClient
|
|
140
151
|
|
141
152
|
raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
|
142
153
|
|
143
|
-
raise ::RedisClient::Cluster::ErrorCollection
|
154
|
+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
|
144
155
|
end
|
145
156
|
|
146
157
|
def clients_for_scanning(seed: nil)
|
@@ -259,7 +270,7 @@ class RedisClient
|
|
259
270
|
result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
|
260
271
|
return result_values if errors.nil? || errors.empty?
|
261
272
|
|
262
|
-
raise ::RedisClient::Cluster::ErrorCollection
|
273
|
+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
|
263
274
|
end
|
264
275
|
|
265
276
|
def try_map(clients, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
@@ -301,7 +312,7 @@ class RedisClient
|
|
301
312
|
work_group.push(i, raw_client) do |client|
|
302
313
|
regular_timeout = client.read_timeout
|
303
314
|
client.read_timeout = @config.slow_command_timeout > 0.0 ? @config.slow_command_timeout : regular_timeout
|
304
|
-
reply = client.
|
315
|
+
reply = client.call_once('cluster', 'nodes')
|
305
316
|
client.read_timeout = regular_timeout
|
306
317
|
parse_cluster_node_reply(reply)
|
307
318
|
rescue StandardError => e
|
@@ -326,13 +337,11 @@ class RedisClient
|
|
326
337
|
|
327
338
|
work_group.close
|
328
339
|
|
329
|
-
raise ::RedisClient::Cluster::InitialSetupError
|
340
|
+
raise ::RedisClient::Cluster::InitialSetupError.from_errors(errors) if node_info_list.nil?
|
330
341
|
|
331
342
|
grouped = node_info_list.compact.group_by do |info_list|
|
332
343
|
info_list.sort_by!(&:id)
|
333
|
-
info_list.each_with_object(String.new(capacity: 128 * info_list.size))
|
334
|
-
a << e.id << e.node_key << e.role << e.primary_id << e.config_epoch
|
335
|
-
end
|
344
|
+
info_list.each_with_object(String.new(capacity: 128 * info_list.size)) { |e, a| e.serialize(a) }
|
336
345
|
end
|
337
346
|
|
338
347
|
grouped.max_by { |_, v| v.size }[1].first
|
@@ -367,6 +376,48 @@ class RedisClient
|
|
367
376
|
end
|
368
377
|
end
|
369
378
|
|
379
|
+
def parse_cluster_slots_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
380
|
+
reply.group_by { |e| e[2][2] }.each_with_object([]) do |(primary_id, group), acc|
|
381
|
+
slots = group.map { |e| e[0, 2] }.freeze
|
382
|
+
|
383
|
+
group.first[2..].each do |arr|
|
384
|
+
ip = arr[0]
|
385
|
+
next if ip.nil? || ip.empty? || ip == '?'
|
386
|
+
|
387
|
+
id = arr[2]
|
388
|
+
role = id == primary_id ? 'master' : 'slave'
|
389
|
+
acc << ::RedisClient::Cluster::Node::Info.new(
|
390
|
+
id: id,
|
391
|
+
node_key: NodeKey.build_from_host_port(ip, arr[1]),
|
392
|
+
role: role,
|
393
|
+
primary_id: role == 'master' ? nil : primary_id,
|
394
|
+
slots: role == 'master' ? slots : EMPTY_ARRAY
|
395
|
+
)
|
396
|
+
end
|
397
|
+
end.freeze
|
398
|
+
end
|
399
|
+
|
400
|
+
def parse_cluster_shards_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
401
|
+
reply.each_with_object([]) do |shard, acc|
|
402
|
+
nodes = shard.fetch('nodes')
|
403
|
+
primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
|
404
|
+
|
405
|
+
nodes.each do |node|
|
406
|
+
ip = node.fetch('ip')
|
407
|
+
next if node.fetch('health') != 'online' || ip.nil? || ip.empty? || ip == '?'
|
408
|
+
|
409
|
+
role = node.fetch('role')
|
410
|
+
acc << ::RedisClient::Cluster::Node::Info.new(
|
411
|
+
id: node.fetch('id'),
|
412
|
+
node_key: NodeKey.build_from_host_port(ip, node['port'] || node['tls-port']),
|
413
|
+
role: role == 'master' ? role : 'slave',
|
414
|
+
primary_id: role == 'master' ? nil : primary_id,
|
415
|
+
slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
|
416
|
+
)
|
417
|
+
end
|
418
|
+
end.freeze
|
419
|
+
end
|
420
|
+
|
370
421
|
# As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
|
371
422
|
# node_key should use hostname if present in CLUSTER NODES output.
|
372
423
|
#
|
@@ -414,15 +465,27 @@ class RedisClient
|
|
414
465
|
# performed the reload.
|
415
466
|
# Probably in the future we should add a circuit breaker to #reload itself, and stop trying if the cluster is
|
416
467
|
# obviously not working.
|
417
|
-
wait_start =
|
468
|
+
wait_start = obtain_current_time
|
418
469
|
@mutex.synchronize do
|
419
470
|
return if @last_reloaded_at && @last_reloaded_at > wait_start
|
420
471
|
|
472
|
+
if @last_reloaded_at && @reload_times > 1
|
473
|
+
# Mitigate load of servers by naive logic. Don't sleep with exponential backoff.
|
474
|
+
now = obtain_current_time
|
475
|
+
elapsed = @last_reloaded_at + @random.rand(STATE_REFRESH_INTERVAL) * 1_000_000
|
476
|
+
return if now < elapsed
|
477
|
+
end
|
478
|
+
|
421
479
|
r = yield
|
422
|
-
@last_reloaded_at =
|
480
|
+
@last_reloaded_at = obtain_current_time
|
481
|
+
@reload_times += 1
|
423
482
|
r
|
424
483
|
end
|
425
484
|
end
|
485
|
+
|
486
|
+
def obtain_current_time
|
487
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
488
|
+
end
|
426
489
|
end
|
427
490
|
end
|
428
491
|
end
|
@@ -11,38 +11,53 @@ class RedisClient
|
|
11
11
|
@asking = false
|
12
12
|
end
|
13
13
|
|
14
|
-
def watch(keys)
|
14
|
+
def watch(keys) # rubocop:disable Metrics/AbcSize
|
15
15
|
slot = find_slot(keys)
|
16
16
|
raise ::RedisClient::Cluster::Transaction::ConsistencyError, "unsafe watch: #{keys.join(' ')}" if slot.nil?
|
17
17
|
|
18
|
-
|
19
|
-
# redirections freely initially (i.e. for the first WATCH call)
|
20
|
-
node = @router.find_primary_node_by_slot(slot)
|
21
|
-
handle_redirection(node, retry_count: 1) do |nd|
|
18
|
+
handle_redirection(slot, retry_count: 1) do |nd|
|
22
19
|
nd.with do |c|
|
23
20
|
c.ensure_connected_cluster_scoped(retryable: false) do
|
24
|
-
c.call('
|
25
|
-
c.call('
|
21
|
+
c.call('asking') if @asking
|
22
|
+
c.call('watch', *keys)
|
26
23
|
begin
|
27
24
|
yield(c, slot, @asking)
|
28
25
|
rescue ::RedisClient::ConnectionError
|
29
26
|
# No need to unwatch on a connection error.
|
30
27
|
raise
|
31
28
|
rescue StandardError
|
32
|
-
c.call('
|
29
|
+
c.call('unwatch')
|
33
30
|
raise
|
34
31
|
end
|
32
|
+
rescue ::RedisClient::CommandError => e
|
33
|
+
@router.renew_cluster_state if e.message.start_with?('CLUSTERDOWN')
|
34
|
+
raise
|
35
35
|
end
|
36
|
+
rescue ::RedisClient::ConnectionError
|
37
|
+
@router.renew_cluster_state
|
38
|
+
raise
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
40
43
|
private
|
41
44
|
|
42
|
-
def handle_redirection(
|
43
|
-
|
45
|
+
def handle_redirection(slot, retry_count: 1, &blk)
|
46
|
+
# We have not yet selected a node for this transaction, initially, which means we can handle
|
47
|
+
# redirections freely initially (i.e. for the first WATCH call)
|
48
|
+
node = @router.find_primary_node_by_slot(slot)
|
49
|
+
times_block_executed = 0
|
50
|
+
@router.handle_redirection(node, nil, retry_count: retry_count) do |nd|
|
51
|
+
times_block_executed += 1
|
44
52
|
handle_asking_once(nd, &blk)
|
45
53
|
end
|
54
|
+
rescue ::RedisClient::ConnectionError
|
55
|
+
# Deduct the number of retries that happened _inside_ router#handle_redirection from our remaining
|
56
|
+
# _external_ retries. Always deduct at least one in case handle_redirection raises without trying the block.
|
57
|
+
retry_count -= [times_block_executed, 1].min
|
58
|
+
raise if retry_count < 0
|
59
|
+
|
60
|
+
retry
|
46
61
|
end
|
47
62
|
|
48
63
|
def handle_asking_once(node)
|