redis-cluster-client 0.15.0 → 0.16.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 +50 -50
- data/lib/redis_client/cluster/concurrent_worker/none.rb +48 -8
- data/lib/redis_client/cluster/concurrent_worker.rb +1 -1
- data/lib/redis_client/cluster/key_slot_converter.rb +1 -1
- data/lib/redis_client/cluster/node/base_topology.rb +6 -2
- data/lib/redis_client/cluster/node/latency_replica.rb +1 -2
- data/lib/redis_client/cluster/node/primary_only.rb +1 -2
- data/lib/redis_client/cluster/node/random_replica.rb +5 -5
- data/lib/redis_client/cluster/node/random_replica_or_primary.rb +3 -4
- data/lib/redis_client/cluster/node.rb +102 -62
- data/lib/redis_client/cluster/node_key.rb +19 -1
- data/lib/redis_client/cluster/optimistic_locking.rb +2 -1
- data/lib/redis_client/cluster/pipeline.rb +4 -7
- data/lib/redis_client/cluster/pub_sub.rb +41 -3
- data/lib/redis_client/cluster/router.rb +35 -27
- data/lib/redis_client/cluster/transaction.rb +1 -1
- data/lib/redis_client/cluster.rb +1 -2
- data/lib/redis_client/cluster_config.rb +17 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee5d5f4c7bb89d0899ef2c3aec48503580d01a1e84a369f1e9ee0c1f60011f15
|
|
4
|
+
data.tar.gz: b17966f3f4c74ce89005b49188d769c4e8b6e399d6d65e0c4d68e14868cee9c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 580dbda062e93296be5869169a3f8f4d389f7bc7aba34cb473cc1a1877cd8a552a29617bf6e09cc2bea8e372cbf895f28ffd086bf75ce986a2dd448bd7170dae
|
|
7
|
+
data.tar.gz: d3abefad63034a97613319637fe617b66f64b2053f67ed8c6de7d81350976645c0eec250037fd100b0a4d5e518c3674c96e07fb043c56ffd327a8e918626405d
|
|
@@ -9,18 +9,57 @@ class RedisClient
|
|
|
9
9
|
class Command
|
|
10
10
|
EMPTY_STRING = ''
|
|
11
11
|
EMPTY_HASH = {}.freeze
|
|
12
|
-
EMPTY_ARRAY = [].freeze
|
|
13
12
|
|
|
14
|
-
private_constant :
|
|
13
|
+
private_constant :EMPTY_HASH
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
'
|
|
15
|
+
Spec = Struct.new(
|
|
16
|
+
'RedisCommandSpec',
|
|
18
17
|
:first_key_position,
|
|
19
18
|
:key_step,
|
|
20
19
|
:write?,
|
|
21
20
|
:readonly?,
|
|
22
21
|
keyword_init: true
|
|
23
|
-
)
|
|
22
|
+
) do
|
|
23
|
+
def extract_first_key(command)
|
|
24
|
+
i = first_key_position.to_i
|
|
25
|
+
return command[i] if i > 0
|
|
26
|
+
|
|
27
|
+
i = determine_first_key_position(command)
|
|
28
|
+
return ::RedisClient::Cluster::Command::EMPTY_STRING if i == 0
|
|
29
|
+
|
|
30
|
+
command[i]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def should_send_to_primary?
|
|
34
|
+
write?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def should_send_to_replica?
|
|
38
|
+
readonly?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def determine_first_key_position(command) # rubocop:disable Metrics/AbcSize
|
|
44
|
+
cmd_name = command.first
|
|
45
|
+
if cmd_name.casecmp('xread').zero?
|
|
46
|
+
determine_optional_key_position(command, 'streams')
|
|
47
|
+
elsif cmd_name.casecmp('xreadgroup').zero?
|
|
48
|
+
determine_optional_key_position(command, 'streams')
|
|
49
|
+
elsif cmd_name.casecmp('migrate').zero?
|
|
50
|
+
command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
|
|
51
|
+
elsif cmd_name.casecmp('memory').zero?
|
|
52
|
+
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
|
53
|
+
else
|
|
54
|
+
0
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def determine_optional_key_position(command, option_name)
|
|
59
|
+
i = command.index { |v| v.to_s.casecmp(option_name).zero? }
|
|
60
|
+
i.nil? ? 0 : i + 1
|
|
61
|
+
end
|
|
62
|
+
end
|
|
24
63
|
|
|
25
64
|
class << self
|
|
26
65
|
def load(nodes, slow_command_timeout: -1) # rubocop:disable Metrics/AbcSize
|
|
@@ -30,13 +69,14 @@ class RedisClient
|
|
|
30
69
|
regular_timeout = node.read_timeout
|
|
31
70
|
node.read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
|
|
32
71
|
reply = node.call('command')
|
|
33
|
-
node.read_timeout = regular_timeout
|
|
34
72
|
commands = parse_command_reply(reply)
|
|
35
73
|
cmd = ::RedisClient::Cluster::Command.new(commands)
|
|
36
74
|
break
|
|
37
75
|
rescue ::RedisClient::Error => e
|
|
38
76
|
errors ||= []
|
|
39
77
|
errors << e
|
|
78
|
+
ensure
|
|
79
|
+
node.read_timeout = regular_timeout
|
|
40
80
|
end
|
|
41
81
|
|
|
42
82
|
return cmd unless cmd.nil?
|
|
@@ -64,12 +104,12 @@ class RedisClient
|
|
|
64
104
|
else row[2].include?('write')
|
|
65
105
|
end
|
|
66
106
|
|
|
67
|
-
acc[row.first] = ::RedisClient::Cluster::Command::
|
|
107
|
+
acc[row.first] = ::RedisClient::Cluster::Command::Spec.new(
|
|
68
108
|
first_key_position: pos,
|
|
69
109
|
key_step: row[5],
|
|
70
110
|
write?: writable,
|
|
71
111
|
readonly?: row[2].include?('readonly')
|
|
72
|
-
)
|
|
112
|
+
).freeze
|
|
73
113
|
end.freeze || EMPTY_HASH
|
|
74
114
|
end
|
|
75
115
|
end
|
|
@@ -78,53 +118,13 @@ class RedisClient
|
|
|
78
118
|
@commands = commands || EMPTY_HASH
|
|
79
119
|
end
|
|
80
120
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
return EMPTY_STRING if i == 0
|
|
84
|
-
|
|
85
|
-
command[i]
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def should_send_to_primary?(command)
|
|
89
|
-
find_command_info(command.first)&.write?
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def should_send_to_replica?(command)
|
|
93
|
-
find_command_info(command.first)&.readonly?
|
|
121
|
+
def get_spec(name)
|
|
122
|
+
@commands[name] || @commands[name.to_s.downcase(:ascii)]
|
|
94
123
|
end
|
|
95
124
|
|
|
96
125
|
def exists?(name)
|
|
97
126
|
@commands.key?(name) || @commands.key?(name.to_s.downcase(:ascii))
|
|
98
127
|
end
|
|
99
|
-
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
def find_command_info(name)
|
|
103
|
-
@commands[name] || @commands[name.to_s.downcase(:ascii)]
|
|
104
|
-
end
|
|
105
|
-
|
|
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
|
|
119
|
-
else
|
|
120
|
-
i
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
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
|
|
127
|
-
end
|
|
128
128
|
end
|
|
129
129
|
end
|
|
130
130
|
end
|
|
@@ -3,17 +3,57 @@
|
|
|
3
3
|
class RedisClient
|
|
4
4
|
class Cluster
|
|
5
5
|
module ConcurrentWorker
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
size: size
|
|
6
|
+
module None
|
|
7
|
+
class Group
|
|
8
|
+
Task = Struct.new(
|
|
9
|
+
'RedisClusterClientSingleThreadTask',
|
|
10
|
+
:id, :result, keyword_init: true
|
|
12
11
|
)
|
|
12
|
+
|
|
13
|
+
def initialize(size:)
|
|
14
|
+
@tasks = Array.new(size)
|
|
15
|
+
@idx = 0
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def push(id, *args, **kwargs, &block)
|
|
19
|
+
raise InvalidNumberOfTasks, "max size reached: #{@idx}" if @idx == @tasks.size
|
|
20
|
+
|
|
21
|
+
result = exec(*args, **kwargs, &block)
|
|
22
|
+
@tasks[@idx] = Task.new(id: id, result: result)
|
|
23
|
+
@idx += 1
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def each
|
|
28
|
+
raise InvalidNumberOfTasks, "expected: #{@tasks.size}, actual: #{@idx}" if @idx != @tasks.size
|
|
29
|
+
|
|
30
|
+
@tasks.each { |task| yield(task.id, task.result) }
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close
|
|
35
|
+
@idx = 0
|
|
36
|
+
@tasks.clear
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def inspect
|
|
41
|
+
"#<#{self.class.name} size: #{@idx}, max: #{@tasks.size}>"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def exec(*args, **kwargs)
|
|
47
|
+
yield(*args, **kwargs) if block_given?
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
e
|
|
50
|
+
end
|
|
13
51
|
end
|
|
14
52
|
|
|
15
|
-
|
|
16
|
-
|
|
53
|
+
module_function
|
|
54
|
+
|
|
55
|
+
def new_group(size:)
|
|
56
|
+
Group.new(size: size)
|
|
17
57
|
end
|
|
18
58
|
|
|
19
59
|
def close; end
|
|
@@ -73,7 +73,7 @@ class RedisClient
|
|
|
73
73
|
|
|
74
74
|
def create(model: :none, size: 5)
|
|
75
75
|
case model
|
|
76
|
-
when :none then ::RedisClient::Cluster::ConcurrentWorker::None
|
|
76
|
+
when :none then ::RedisClient::Cluster::ConcurrentWorker::None
|
|
77
77
|
when :on_demand then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
|
|
78
78
|
when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
|
|
79
79
|
else raise ArgumentError, "unknown model: #{model}"
|
|
@@ -25,8 +25,7 @@ class RedisClient
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def any_primary_node_key(seed: nil)
|
|
28
|
-
random
|
|
29
|
-
@primary_node_keys.sample(random: random)
|
|
28
|
+
@primary_node_keys.sample(random: make_random(seed))
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
def process_topology_update!(replications, options) # rubocop:disable Metrics/AbcSize
|
|
@@ -59,6 +58,11 @@ class RedisClient
|
|
|
59
58
|
@clients[node_key] = client
|
|
60
59
|
end
|
|
61
60
|
end
|
|
61
|
+
|
|
62
|
+
def make_random(seed)
|
|
63
|
+
# OPTIMIZE: Figure out the most elegant way to pin a node during a pipeline or scan.
|
|
64
|
+
seed.nil? ? Random : Random.new(seed)
|
|
65
|
+
end
|
|
62
66
|
end
|
|
63
67
|
end
|
|
64
68
|
end
|
|
@@ -20,8 +20,7 @@ class RedisClient
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def any_replica_node_key(seed: nil)
|
|
23
|
-
random
|
|
24
|
-
@existed_replicas.sample(random: random)&.first || any_primary_node_key(seed: seed)
|
|
23
|
+
@existed_replicas.sample(random: make_random(seed))&.first || any_primary_node_key(seed: seed)
|
|
25
24
|
end
|
|
26
25
|
|
|
27
26
|
def process_topology_update!(replications, options)
|
|
@@ -18,8 +18,7 @@ class RedisClient
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def any_primary_node_key(seed: nil)
|
|
21
|
-
random
|
|
22
|
-
@primary_node_keys.sample(random: random)
|
|
21
|
+
@primary_node_keys.sample(random: make_random(seed))
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
alias any_replica_node_key any_primary_node_key
|
|
@@ -12,7 +12,7 @@ class RedisClient
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def clients_for_scanning(seed: nil)
|
|
15
|
-
random =
|
|
15
|
+
random = make_random(seed)
|
|
16
16
|
keys = @replications.map do |primary_node_key, replica_node_keys|
|
|
17
17
|
replica_node_keys.empty? ? primary_node_key : replica_node_keys.sample(random: random)
|
|
18
18
|
end
|
|
@@ -21,13 +21,13 @@ class RedisClient
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def find_node_key_of_replica(primary_node_key, seed: nil)
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
replica_node_keys = @replications.fetch(primary_node_key, EMPTY_ARRAY)
|
|
25
|
+
replica_node_key = replica_node_keys.size <= 1 ? replica_node_keys.first : replica_node_keys.sample(random: make_random(seed))
|
|
26
|
+
replica_node_key || primary_node_key
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
def any_replica_node_key(seed: nil)
|
|
29
|
-
random
|
|
30
|
-
@replica_node_keys.sample(random: random) || any_primary_node_key(seed: seed)
|
|
30
|
+
@replica_node_keys.sample(random: make_random(seed)) || any_primary_node_key(seed: seed)
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -12,7 +12,7 @@ class RedisClient
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def clients_for_scanning(seed: nil)
|
|
15
|
-
random =
|
|
15
|
+
random = make_random(seed)
|
|
16
16
|
keys = @replications.map do |primary_node_key, replica_node_keys|
|
|
17
17
|
decide_use_primary?(random, replica_node_keys.size) ? primary_node_key : replica_node_keys.sample(random: random)
|
|
18
18
|
end
|
|
@@ -21,7 +21,7 @@ class RedisClient
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def find_node_key_of_replica(primary_node_key, seed: nil)
|
|
24
|
-
random =
|
|
24
|
+
random = make_random(seed)
|
|
25
25
|
|
|
26
26
|
replica_node_keys = @replications.fetch(primary_node_key, EMPTY_ARRAY)
|
|
27
27
|
if decide_use_primary?(random, replica_node_keys.size)
|
|
@@ -32,8 +32,7 @@ class RedisClient
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def any_replica_node_key(seed: nil)
|
|
35
|
-
random
|
|
36
|
-
@replica_node_keys.sample(random: random) || any_primary_node_key(seed: seed)
|
|
35
|
+
@replica_node_keys.sample(random: make_random(seed)) || any_primary_node_key(seed: seed)
|
|
37
36
|
end
|
|
38
37
|
|
|
39
38
|
private
|
|
@@ -24,10 +24,12 @@ class RedisClient
|
|
|
24
24
|
ROLE_FLAGS = %w[master slave].freeze
|
|
25
25
|
EMPTY_ARRAY = [].freeze
|
|
26
26
|
EMPTY_HASH = {}.freeze
|
|
27
|
-
|
|
27
|
+
EMPTY_STRING = ''
|
|
28
|
+
JITTER_WINDOW = (3_000_000...10_000_000).freeze # micro seconds
|
|
28
29
|
|
|
29
30
|
private_constant :USE_CHAR_ARRAY_SLOT, :SLOT_SIZE, :MIN_SLOT, :MAX_SLOT,
|
|
30
|
-
:DEAD_FLAGS, :ROLE_FLAGS, :EMPTY_ARRAY, :EMPTY_HASH
|
|
31
|
+
:DEAD_FLAGS, :ROLE_FLAGS, :EMPTY_ARRAY, :EMPTY_HASH, :EMPTY_STRING,
|
|
32
|
+
:JITTER_WINDOW
|
|
31
33
|
|
|
32
34
|
ReloadNeeded = Class.new(::RedisClient::Cluster::Error)
|
|
33
35
|
|
|
@@ -46,20 +48,15 @@ class RedisClient
|
|
|
46
48
|
end
|
|
47
49
|
|
|
48
50
|
def serialize(str)
|
|
49
|
-
str << id << node_key << role << primary_id
|
|
51
|
+
str << id << node_key << role << primary_id
|
|
50
52
|
end
|
|
51
53
|
end
|
|
52
54
|
|
|
53
55
|
class CharArray
|
|
54
|
-
BASE = ''
|
|
55
|
-
PADDING = '0'
|
|
56
|
-
|
|
57
|
-
private_constant :BASE, :PADDING
|
|
58
|
-
|
|
59
56
|
def initialize(size, elements)
|
|
60
57
|
@elements = elements
|
|
61
|
-
@string = String.new(
|
|
62
|
-
size.times { @string <<
|
|
58
|
+
@string = String.new('', encoding: Encoding::BINARY, capacity: size)
|
|
59
|
+
size.times { @string << 0xff }
|
|
63
60
|
end
|
|
64
61
|
|
|
65
62
|
def [](index)
|
|
@@ -106,8 +103,7 @@ class RedisClient
|
|
|
106
103
|
@topology = klass.new(pool, @concurrent_worker, **kwargs)
|
|
107
104
|
@config = config
|
|
108
105
|
@mutex = Mutex.new
|
|
109
|
-
@
|
|
110
|
-
@reload_times = 0
|
|
106
|
+
@next_reload_time = nil
|
|
111
107
|
@random = Random.new
|
|
112
108
|
end
|
|
113
109
|
|
|
@@ -193,26 +189,22 @@ class RedisClient
|
|
|
193
189
|
end
|
|
194
190
|
|
|
195
191
|
def update_slot(slot, node_key)
|
|
196
|
-
return
|
|
192
|
+
return unless @mutex.try_lock
|
|
197
193
|
|
|
198
|
-
@
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
194
|
+
@slots[slot] = node_key
|
|
195
|
+
rescue RangeError
|
|
196
|
+
@slots = Array.new(SLOT_SIZE) { |i| @slots[i] }
|
|
197
|
+
@slots[slot] = node_key
|
|
198
|
+
ensure
|
|
199
|
+
@mutex.unlock if @mutex.owned?
|
|
204
200
|
end
|
|
205
201
|
|
|
206
|
-
def
|
|
202
|
+
def try_reload!
|
|
207
203
|
with_reload_lock do
|
|
208
|
-
|
|
209
|
-
@
|
|
210
|
-
|
|
211
|
-
[node_info.node_key, @config.client_config_for_node(node_info.node_key)]
|
|
204
|
+
with_reload_jitter do
|
|
205
|
+
with_startup_clients(@config.max_startup_sample) do |clients|
|
|
206
|
+
reload!(clients)
|
|
212
207
|
end
|
|
213
|
-
@slots = build_slot_node_mappings(@node_info)
|
|
214
|
-
@replications = build_replication_mappings(@node_info)
|
|
215
|
-
@topology.process_topology_update!(@replications, @node_configs)
|
|
216
208
|
end
|
|
217
209
|
end
|
|
218
210
|
end
|
|
@@ -312,13 +304,11 @@ class RedisClient
|
|
|
312
304
|
work_group.push(i, raw_client) do |client|
|
|
313
305
|
regular_timeout = client.read_timeout
|
|
314
306
|
client.read_timeout = @config.slow_command_timeout > 0.0 ? @config.slow_command_timeout : regular_timeout
|
|
315
|
-
|
|
316
|
-
client.read_timeout = regular_timeout
|
|
317
|
-
parse_cluster_node_reply(reply)
|
|
307
|
+
fetch_cluster_state(client)
|
|
318
308
|
rescue StandardError => e
|
|
319
309
|
e
|
|
320
310
|
ensure
|
|
321
|
-
client
|
|
311
|
+
client.read_timeout = regular_timeout
|
|
322
312
|
end
|
|
323
313
|
end
|
|
324
314
|
|
|
@@ -347,6 +337,16 @@ class RedisClient
|
|
|
347
337
|
grouped.max_by { |_, v| v.size }[1].first
|
|
348
338
|
end
|
|
349
339
|
|
|
340
|
+
def fetch_cluster_state(client)
|
|
341
|
+
reply = client.call_once('cluster', 'shards')
|
|
342
|
+
parse_cluster_shards_reply(reply)
|
|
343
|
+
rescue ::RedisClient::CommandError => e
|
|
344
|
+
raise unless e.message.start_with?('ERR Unknown subcommand')
|
|
345
|
+
|
|
346
|
+
reply = client.call_once('cluster', 'nodes')
|
|
347
|
+
parse_cluster_node_reply(reply)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
350
|
def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
351
351
|
reply.each_line("\n", chomp: true).filter_map do |line|
|
|
352
352
|
fields = line.split
|
|
@@ -372,7 +372,7 @@ class RedisClient
|
|
|
372
372
|
config_epoch: fields[6],
|
|
373
373
|
link_state: fields[7],
|
|
374
374
|
slots: slots
|
|
375
|
-
)
|
|
375
|
+
).freeze
|
|
376
376
|
end
|
|
377
377
|
end
|
|
378
378
|
|
|
@@ -390,39 +390,61 @@ class RedisClient
|
|
|
390
390
|
id: id,
|
|
391
391
|
node_key: NodeKey.build_from_host_port(ip, arr[1]),
|
|
392
392
|
role: role,
|
|
393
|
-
primary_id: role == 'master' ?
|
|
393
|
+
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
394
394
|
slots: role == 'master' ? slots : EMPTY_ARRAY
|
|
395
|
-
)
|
|
395
|
+
).freeze
|
|
396
396
|
end
|
|
397
|
-
end
|
|
397
|
+
end
|
|
398
398
|
end
|
|
399
399
|
|
|
400
400
|
def parse_cluster_shards_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
401
401
|
reply.each_with_object([]) do |shard, acc|
|
|
402
|
+
resp2 = shard.is_a?(Array)
|
|
403
|
+
shard = shard.each_slice(2).to_h if resp2
|
|
402
404
|
nodes = shard.fetch('nodes')
|
|
405
|
+
nodes = nodes.map { |n| n.each_slice(2).to_h } if resp2
|
|
403
406
|
primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
|
|
404
407
|
|
|
405
408
|
nodes.each do |node|
|
|
406
|
-
|
|
407
|
-
next if node.fetch('health') != 'online' ||
|
|
409
|
+
host = pick_shard_host(node)
|
|
410
|
+
next if node.fetch('health') != 'online' || host.nil? || host.empty? || host == '?'
|
|
408
411
|
|
|
409
412
|
role = node.fetch('role')
|
|
410
413
|
acc << ::RedisClient::Cluster::Node::Info.new(
|
|
411
414
|
id: node.fetch('id'),
|
|
412
|
-
node_key: NodeKey.build_from_host_port(
|
|
415
|
+
node_key: NodeKey.build_from_host_port(host, node['port'] || node['tls-port']),
|
|
413
416
|
role: role == 'master' ? role : 'slave',
|
|
414
|
-
primary_id: role == 'master' ?
|
|
417
|
+
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
415
418
|
slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
|
|
416
|
-
)
|
|
419
|
+
).freeze
|
|
417
420
|
end
|
|
418
|
-
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Pick the host for a CLUSTER SHARDS node entry.
|
|
425
|
+
#
|
|
426
|
+
# `endpoint` is the server-selected preferred endpoint (driven by the
|
|
427
|
+
# `cluster-preferred-endpoint-type` config), so prefer it when present and
|
|
428
|
+
# usable. Some managed services (e.g. AWS ElastiCache Serverless) report
|
|
429
|
+
# `127.0.0.1` in `ip` while exposing the reachable address only via
|
|
430
|
+
# `endpoint` / `hostname`; falling through to `ip` in that case would build
|
|
431
|
+
# an unreachable topology. This mirrors the precedence `parse_node_key`
|
|
432
|
+
# uses for CLUSTER NODES output (see #207).
|
|
433
|
+
def pick_shard_host(node)
|
|
434
|
+
endpoint = node['endpoint']
|
|
435
|
+
return endpoint if endpoint && !endpoint.empty? && endpoint != '?'
|
|
436
|
+
|
|
437
|
+
hostname = node['hostname']
|
|
438
|
+
return hostname if hostname && !hostname.empty?
|
|
439
|
+
|
|
440
|
+
node['ip']
|
|
419
441
|
end
|
|
420
442
|
|
|
421
443
|
# As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
|
|
422
444
|
# node_key should use hostname if present in CLUSTER NODES output.
|
|
423
445
|
#
|
|
424
446
|
# See https://redis.io/commands/cluster-nodes/ for details on the output format.
|
|
425
|
-
# node_address matches
|
|
447
|
+
# node_address matches the format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
|
|
426
448
|
def parse_node_key(node_address)
|
|
427
449
|
ip_chunk, hostname, _auxiliaries = node_address.split(',')
|
|
428
450
|
ip_port_string = ip_chunk.split('@').first
|
|
@@ -432,6 +454,16 @@ class RedisClient
|
|
|
432
454
|
"#{hostname}:#{port}"
|
|
433
455
|
end
|
|
434
456
|
|
|
457
|
+
def reload!(clients)
|
|
458
|
+
@node_info = refetch_node_info_list(clients)
|
|
459
|
+
@node_configs = @node_info.to_h do |node_info|
|
|
460
|
+
[node_info.node_key, @config.client_config_for_node(node_info.node_key)]
|
|
461
|
+
end
|
|
462
|
+
@slots = build_slot_node_mappings(@node_info)
|
|
463
|
+
@replications = build_replication_mappings(@node_info)
|
|
464
|
+
@topology.process_topology_update!(@replications, @node_configs)
|
|
465
|
+
end
|
|
466
|
+
|
|
435
467
|
def with_startup_clients(count) # rubocop:disable Metrics/AbcSize
|
|
436
468
|
if @config.connect_with_original_config
|
|
437
469
|
# If connect_with_original_config is set, that means we need to build actual client objects
|
|
@@ -457,35 +489,43 @@ class RedisClient
|
|
|
457
489
|
end
|
|
458
490
|
end
|
|
459
491
|
|
|
492
|
+
def with_reload_jitter
|
|
493
|
+
return unless @next_reload_time.nil? || obtain_current_time >= @next_reload_time
|
|
494
|
+
|
|
495
|
+
begin
|
|
496
|
+
yield
|
|
497
|
+
ensure
|
|
498
|
+
@next_reload_time = obtain_current_time + @random.rand(JITTER_WINDOW)
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
|
|
460
502
|
def with_reload_lock
|
|
461
|
-
# What should happen with concurrent calls #
|
|
503
|
+
# What should happen with concurrent calls #try_reload! This is a realistic possibility if the cluster goes into
|
|
462
504
|
# a CLUSTERDOWN state, and we're using a pooled backend. Every thread will independently discover this, and
|
|
463
|
-
# call
|
|
464
|
-
# For now, if a reload is in progress, wait for that to complete, and
|
|
465
|
-
#
|
|
466
|
-
# Probably in the future we should add a circuit breaker to #
|
|
505
|
+
# call #try_reload!.
|
|
506
|
+
# For now, if a reload is in progress by a thread, the other threads do not wait for that to complete, and
|
|
507
|
+
# they throw an error.
|
|
508
|
+
# Probably in the future we should add a circuit breaker to #try_reload! itself, and stop trying if the cluster is
|
|
467
509
|
# obviously not working.
|
|
468
|
-
|
|
469
|
-
@mutex.synchronize do
|
|
470
|
-
return if @last_reloaded_at && @last_reloaded_at > wait_start
|
|
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
|
|
510
|
+
return unless @mutex.try_lock
|
|
478
511
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
r
|
|
483
|
-
end
|
|
512
|
+
yield
|
|
513
|
+
ensure
|
|
514
|
+
@mutex.unlock if @mutex.owned?
|
|
484
515
|
end
|
|
485
516
|
|
|
486
517
|
def obtain_current_time
|
|
487
518
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
488
519
|
end
|
|
520
|
+
|
|
521
|
+
def bypass_reload!
|
|
522
|
+
# DO NOT USE THIS METHOD
|
|
523
|
+
with_reload_lock do
|
|
524
|
+
with_startup_clients(@config.max_startup_sample) do |clients|
|
|
525
|
+
reload!(clients)
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
end
|
|
489
529
|
end
|
|
490
530
|
end
|
|
491
531
|
end
|
|
@@ -18,12 +18,30 @@ class RedisClient
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def split(node_key)
|
|
21
|
-
|
|
21
|
+
return [node_key, nil] if node_key.nil? || node_key.empty?
|
|
22
|
+
|
|
23
|
+
bracketed = split_bracketed(node_key)
|
|
24
|
+
return bracketed unless bracketed.nil?
|
|
25
|
+
|
|
26
|
+
pos = node_key.rindex(DELIMITER, -1)
|
|
22
27
|
return [node_key, nil] if pos.nil?
|
|
23
28
|
|
|
24
29
|
[node_key[0, pos], node_key[(pos + 1)..]]
|
|
25
30
|
end
|
|
26
31
|
|
|
32
|
+
def split_bracketed(node_key)
|
|
33
|
+
return nil unless node_key.start_with?('[')
|
|
34
|
+
|
|
35
|
+
end_bracket = node_key.index(']')
|
|
36
|
+
return nil if end_bracket.nil?
|
|
37
|
+
|
|
38
|
+
host = node_key[1, end_bracket - 1]
|
|
39
|
+
remainder = node_key[(end_bracket + 1)..]
|
|
40
|
+
port = remainder.start_with?(DELIMITER) ? remainder[1..] : nil
|
|
41
|
+
[host, port]
|
|
42
|
+
end
|
|
43
|
+
private_class_method :split_bracketed
|
|
44
|
+
|
|
27
45
|
def build_from_uri(uri)
|
|
28
46
|
return '' if uri.nil?
|
|
29
47
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'redis_client'
|
|
4
4
|
require 'redis_client/cluster/transaction'
|
|
5
|
+
require 'redis_client/cluster/error_identification'
|
|
5
6
|
|
|
6
7
|
class RedisClient
|
|
7
8
|
class Cluster
|
|
@@ -54,7 +55,7 @@ class RedisClient
|
|
|
54
55
|
rescue ::RedisClient::ConnectionError
|
|
55
56
|
# Deduct the number of retries that happened _inside_ router#handle_redirection from our remaining
|
|
56
57
|
# _external_ retries. Always deduct at least one in case handle_redirection raises without trying the block.
|
|
57
|
-
retry_count -=
|
|
58
|
+
retry_count -= times_block_executed > 1 ? times_block_executed : 1
|
|
58
59
|
raise if retry_count < 0
|
|
59
60
|
|
|
60
61
|
retry
|
|
@@ -23,10 +23,6 @@ class RedisClient
|
|
|
23
23
|
@outer_indices << index
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def get_inner_index(outer_index)
|
|
27
|
-
@outer_indices&.find_index(outer_index)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
26
|
def get_callee_method(inner_index)
|
|
31
27
|
if @timeouts.is_a?(Array) && !@timeouts[inner_index].nil?
|
|
32
28
|
:blocking_call_v
|
|
@@ -66,6 +62,7 @@ class RedisClient
|
|
|
66
62
|
if result.is_a?(::RedisClient::Error)
|
|
67
63
|
result._set_command(commands[index])
|
|
68
64
|
result._set_config(config)
|
|
65
|
+
result._set_retry_attempt(@retry_attempt)
|
|
69
66
|
|
|
70
67
|
if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
|
|
71
68
|
redirection_indices ||= []
|
|
@@ -230,10 +227,10 @@ class RedisClient
|
|
|
230
227
|
|
|
231
228
|
def append_pipeline(node_key)
|
|
232
229
|
@pipelines ||= {}
|
|
233
|
-
@pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(::RedisClient::Cluster::NoopCommandBuilder)
|
|
234
|
-
|
|
230
|
+
pi = (@pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(::RedisClient::Cluster::NoopCommandBuilder))
|
|
231
|
+
pi.add_outer_index(@size)
|
|
235
232
|
@size += 1
|
|
236
|
-
|
|
233
|
+
pi
|
|
237
234
|
end
|
|
238
235
|
|
|
239
236
|
def do_pipelining(client, pipeline)
|
|
@@ -74,14 +74,14 @@ class RedisClient
|
|
|
74
74
|
def call(*args, **kwargs)
|
|
75
75
|
command = @command_builder.generate(args, kwargs)
|
|
76
76
|
_call(command)
|
|
77
|
-
|
|
77
|
+
remember_subscription(command)
|
|
78
78
|
nil
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def call_v(command)
|
|
82
82
|
command = @command_builder.generate(command)
|
|
83
83
|
_call(command)
|
|
84
|
-
|
|
84
|
+
remember_subscription(command)
|
|
85
85
|
nil
|
|
86
86
|
end
|
|
87
87
|
|
|
@@ -179,6 +179,7 @@ class RedisClient
|
|
|
179
179
|
end
|
|
180
180
|
|
|
181
181
|
def start_over
|
|
182
|
+
attempt = 0
|
|
182
183
|
loop do
|
|
183
184
|
@router.renew_cluster_state
|
|
184
185
|
@state_dict.each_value(&:close)
|
|
@@ -186,9 +187,46 @@ class RedisClient
|
|
|
186
187
|
@commands.each { |command| _call(command) }
|
|
187
188
|
break
|
|
188
189
|
rescue ::RedisClient::ConnectionError, ::RedisClient::Cluster::NodeMightBeDown
|
|
189
|
-
|
|
190
|
+
attempt += 1
|
|
191
|
+
raise if attempt >= 10
|
|
192
|
+
|
|
193
|
+
sleep recovery_interval(attempt)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def recovery_interval(attempt)
|
|
198
|
+
[1.0 * (2**(attempt - 1)), 30.0].min
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def remember_subscription(command) # rubocop:disable Metrics/AbcSize
|
|
202
|
+
if command.first.casecmp('subscribe').zero?
|
|
203
|
+
@commands << command
|
|
204
|
+
elsif command.first.casecmp('psubscribe').zero?
|
|
205
|
+
@commands << command
|
|
206
|
+
elsif command.first.casecmp('ssubscribe').zero?
|
|
207
|
+
@commands << command
|
|
208
|
+
elsif command.first.casecmp('unsubscribe').zero?
|
|
209
|
+
forget_subscriptions('subscribe', command[1, command.size])
|
|
210
|
+
elsif command.first.casecmp('punsubscribe').zero?
|
|
211
|
+
forget_subscriptions('psubscribe', command[1, command.size])
|
|
212
|
+
elsif command.first.casecmp('sunsubscribe').zero?
|
|
213
|
+
forget_subscriptions('ssubscribe', command[1, command.size])
|
|
190
214
|
end
|
|
191
215
|
end
|
|
216
|
+
|
|
217
|
+
def forget_subscriptions(subscribe_cmd, channels)
|
|
218
|
+
@commands.map! do |command|
|
|
219
|
+
next command unless command.first.casecmp(subscribe_cmd).zero?
|
|
220
|
+
next if channels.nil? || channels.empty?
|
|
221
|
+
|
|
222
|
+
remaining = command[1, command.size] - channels
|
|
223
|
+
next if remaining.empty?
|
|
224
|
+
|
|
225
|
+
[command.first, *remaining]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@commands.compact!
|
|
229
|
+
end
|
|
192
230
|
end
|
|
193
231
|
end
|
|
194
232
|
end
|
|
@@ -84,7 +84,7 @@ class RedisClient
|
|
|
84
84
|
@pool = pool
|
|
85
85
|
@client_kwargs = kwargs
|
|
86
86
|
@node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
|
|
87
|
-
@node.
|
|
87
|
+
@node.try_reload!
|
|
88
88
|
@command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
|
|
89
89
|
@command_builder = @config.command_builder
|
|
90
90
|
rescue ::RedisClient::Cluster::InitialSetupError => e
|
|
@@ -93,9 +93,8 @@ class RedisClient
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
96
|
-
return assign_node_and_send_command(method, command, args, &block) unless DEDICATED_ACTIONS.key?(command.first)
|
|
97
|
-
|
|
98
96
|
action = DEDICATED_ACTIONS[command.first]
|
|
97
|
+
return assign_node_and_send_command(method, command, args, &block) if action.nil?
|
|
99
98
|
return send(action.method_name, method, command, args, &block) if action.reply_transformer.nil?
|
|
100
99
|
|
|
101
100
|
reply = send(action.method_name, method, command, args)
|
|
@@ -167,21 +166,21 @@ class RedisClient
|
|
|
167
166
|
rescue ::RedisClient::ConnectionError => e
|
|
168
167
|
raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
|
|
169
168
|
|
|
170
|
-
retry_count -= 1
|
|
171
169
|
renew_cluster_state
|
|
170
|
+
raise if command.nil? || command.empty?
|
|
172
171
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
172
|
+
retry_count -= 1
|
|
173
|
+
raise if retry_count < 0
|
|
174
|
+
|
|
175
|
+
# Find the node to use for this command - if this fails for some reason, though, re-use
|
|
176
|
+
# the old node.
|
|
177
|
+
begin
|
|
178
|
+
node = find_node(find_node_key(command))
|
|
179
|
+
rescue StandardError
|
|
180
|
+
raise e
|
|
181
181
|
end
|
|
182
182
|
|
|
183
|
-
retry
|
|
184
|
-
raise
|
|
183
|
+
retry
|
|
185
184
|
end
|
|
186
185
|
|
|
187
186
|
def scan(command, seed: nil) # rubocop:disable Metrics/AbcSize
|
|
@@ -229,7 +228,7 @@ class RedisClient
|
|
|
229
228
|
def find_node_key_by_key(key, seed: nil, primary: false)
|
|
230
229
|
if key && !key.empty?
|
|
231
230
|
slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
|
232
|
-
node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
|
|
231
|
+
node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot, seed: seed)
|
|
233
232
|
if node_key.nil?
|
|
234
233
|
renew_cluster_state
|
|
235
234
|
raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
|
|
@@ -248,23 +247,27 @@ class RedisClient
|
|
|
248
247
|
end
|
|
249
248
|
|
|
250
249
|
def find_node_key(command, seed: nil)
|
|
251
|
-
|
|
252
|
-
find_node_key_by_key(
|
|
250
|
+
cmd_spec = @command.get_spec(command.first)
|
|
251
|
+
find_node_key_by_key(
|
|
252
|
+
cmd_spec&.extract_first_key(command),
|
|
253
|
+
seed: seed,
|
|
254
|
+
primary: cmd_spec&.should_send_to_primary?
|
|
255
|
+
)
|
|
253
256
|
end
|
|
254
257
|
|
|
255
258
|
def find_primary_node_key(command)
|
|
256
|
-
key = @command.extract_first_key(command)
|
|
257
|
-
return
|
|
259
|
+
key = @command.get_spec(command.first)&.extract_first_key(command)
|
|
260
|
+
return unless key&.size&.> 0
|
|
258
261
|
|
|
259
262
|
find_node_key_by_key(key, primary: true)
|
|
260
263
|
end
|
|
261
264
|
|
|
262
265
|
def find_slot(command)
|
|
263
|
-
find_slot_by_key(@command.extract_first_key(command))
|
|
266
|
+
find_slot_by_key(@command.get_spec(command.first)&.extract_first_key(command))
|
|
264
267
|
end
|
|
265
268
|
|
|
266
269
|
def find_slot_by_key(key)
|
|
267
|
-
return if key.empty?
|
|
270
|
+
return if key.nil? || key.empty?
|
|
268
271
|
|
|
269
272
|
::RedisClient::Cluster::KeySlotConverter.convert(key)
|
|
270
273
|
end
|
|
@@ -294,7 +297,7 @@ class RedisClient
|
|
|
294
297
|
end
|
|
295
298
|
|
|
296
299
|
def renew_cluster_state
|
|
297
|
-
@node.
|
|
300
|
+
@node.try_reload!
|
|
298
301
|
rescue ::RedisClient::Cluster::InitialSetupError
|
|
299
302
|
# ignore
|
|
300
303
|
end
|
|
@@ -340,8 +343,8 @@ class RedisClient
|
|
|
340
343
|
raise if retry_count <= 0
|
|
341
344
|
raise if e.errors.values.none? { |err| err.message.include?('WAIT cannot be used with replica instances') }
|
|
342
345
|
|
|
343
|
-
retry_count -= 1
|
|
344
346
|
renew_cluster_state
|
|
347
|
+
retry_count -= 1
|
|
345
348
|
retry
|
|
346
349
|
end
|
|
347
350
|
|
|
@@ -452,7 +455,7 @@ class RedisClient
|
|
|
452
455
|
end
|
|
453
456
|
|
|
454
457
|
def send_multiple_keys_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
455
|
-
# This implementation
|
|
458
|
+
# This implementation prioritizes performance over readability.
|
|
456
459
|
cmd = command.first
|
|
457
460
|
if cmd.casecmp('mget').zero?
|
|
458
461
|
single_key_cmd = 'get'
|
|
@@ -469,8 +472,13 @@ class RedisClient
|
|
|
469
472
|
|
|
470
473
|
return assign_node_and_send_command(method, command, args, &block) if command.size <= keys_step + 1 || ::RedisClient::Cluster::KeySlotConverter.hash_tag_included?(command[1])
|
|
471
474
|
|
|
472
|
-
|
|
473
|
-
|
|
475
|
+
pipeline = ::RedisClient::Cluster::Pipeline.new(
|
|
476
|
+
self,
|
|
477
|
+
@command_builder,
|
|
478
|
+
@concurrent_worker,
|
|
479
|
+
exception: true,
|
|
480
|
+
seed: Random.new_seed
|
|
481
|
+
)
|
|
474
482
|
|
|
475
483
|
single_command = Array.new(keys_step + 1)
|
|
476
484
|
single_command[0] = single_key_cmd
|
|
@@ -502,8 +510,8 @@ class RedisClient
|
|
|
502
510
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
|
503
511
|
raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config) if retry_count <= 0
|
|
504
512
|
|
|
505
|
-
retry_count -= 1
|
|
506
513
|
renew_cluster_state
|
|
514
|
+
retry_count -= 1
|
|
507
515
|
retry
|
|
508
516
|
end
|
|
509
517
|
end
|
|
@@ -171,7 +171,7 @@ class RedisClient
|
|
|
171
171
|
send_transaction(node, redirect: redirect - 1)
|
|
172
172
|
elsif err.message.start_with?('ASK')
|
|
173
173
|
node = @router.assign_asking_node(err.message)
|
|
174
|
-
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
|
|
174
|
+
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : raise(err)
|
|
175
175
|
elsif err.message.start_with?('CLUSTERDOWN')
|
|
176
176
|
@router.renew_cluster_state if @watching_slot.nil?
|
|
177
177
|
raise err
|
data/lib/redis_client/cluster.rb
CHANGED
|
@@ -95,13 +95,12 @@ class RedisClient
|
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def pipelined(exception: true)
|
|
98
|
-
seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
|
|
99
98
|
pipeline = ::RedisClient::Cluster::Pipeline.new(
|
|
100
99
|
router,
|
|
101
100
|
@command_builder,
|
|
102
101
|
@concurrent_worker,
|
|
103
102
|
exception: exception,
|
|
104
|
-
seed:
|
|
103
|
+
seed: Random.new_seed
|
|
105
104
|
)
|
|
106
105
|
|
|
107
106
|
yield pipeline
|
|
@@ -21,9 +21,9 @@ class RedisClient
|
|
|
21
21
|
MERGE_CONFIG_KEYS = %i[ssl username password db].freeze
|
|
22
22
|
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
|
23
23
|
MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', -1)) # for backward compatibility
|
|
24
|
-
#
|
|
24
|
+
# Used for slow commands that fetch metadata, e.g. CLUSTER NODES, COMMAND.
|
|
25
25
|
SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
|
|
26
|
-
#
|
|
26
|
+
# Controls the balance between startup load and stability during initialization or cluster state changes.
|
|
27
27
|
MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
|
|
28
28
|
|
|
29
29
|
private_constant :DEFAULT_HOST, :DEFAULT_PORT, :DEFAULT_SCHEME, :SECURE_SCHEME, :DEFAULT_NODES,
|
|
@@ -32,6 +32,12 @@ class RedisClient
|
|
|
32
32
|
|
|
33
33
|
InvalidClientConfigError = Class.new(::RedisClient::Cluster::Error)
|
|
34
34
|
|
|
35
|
+
SENSITIVE_INSPECT_KEYS = %i[username password].freeze
|
|
36
|
+
INSPECT_REDACTED_KEYS = %i[command_builder].freeze
|
|
37
|
+
INSPECT_PLACEHOLDER = '[FILTERED]'
|
|
38
|
+
|
|
39
|
+
private_constant :SENSITIVE_INSPECT_KEYS, :INSPECT_REDACTED_KEYS, :INSPECT_PLACEHOLDER
|
|
40
|
+
|
|
35
41
|
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
|
36
42
|
:connect_with_original_config, :startup_nodes, :max_startup_sample, :id
|
|
37
43
|
|
|
@@ -65,7 +71,7 @@ class RedisClient
|
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
def inspect
|
|
68
|
-
"#<#{self.class.name} #{startup_nodes.values.map { |v| v
|
|
74
|
+
"#<#{self.class.name} #{startup_nodes.values.map { |v| redact_for_inspect(v) }}>"
|
|
69
75
|
end
|
|
70
76
|
|
|
71
77
|
def connect_timeout
|
|
@@ -117,6 +123,14 @@ class RedisClient
|
|
|
117
123
|
|
|
118
124
|
private
|
|
119
125
|
|
|
126
|
+
def redact_for_inspect(node_config)
|
|
127
|
+
node_config.each_with_object({}) do |(key, value), redacted|
|
|
128
|
+
next if INSPECT_REDACTED_KEYS.include?(key)
|
|
129
|
+
|
|
130
|
+
redacted[key] = SENSITIVE_INSPECT_KEYS.include?(key) ? INSPECT_PLACEHOLDER : value
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
120
134
|
def merge_concurrency_option(option)
|
|
121
135
|
opts = {}
|
|
122
136
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redis-cluster-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.16.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Taishi Kasuga
|
|
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '0'
|
|
76
76
|
requirements: []
|
|
77
|
-
rubygems_version: 4.0.
|
|
77
|
+
rubygems_version: 4.0.10
|
|
78
78
|
specification_version: 4
|
|
79
79
|
summary: Redis cluster-aware client for Ruby
|
|
80
80
|
test_files: []
|