redis-cluster-client 0.15.0 → 0.16.4
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 +49 -49
- 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 +97 -51
- data/lib/redis_client/cluster/node_key.rb +19 -1
- data/lib/redis_client/cluster/optimistic_locking.rb +1 -1
- data/lib/redis_client/cluster/pipeline.rb +3 -3
- data/lib/redis_client/cluster/pub_sub.rb +51 -4
- data/lib/redis_client/cluster/router.rb +24 -17
- data/lib/redis_client/cluster.rb +1 -2
- data/lib/redis_client/cluster_config.rb +17 -3
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 80b5e1d6525a06ecf8747239269b110528bf9efc99ff3424108033c85cdc011a
|
|
4
|
+
data.tar.gz: 35b5aba54bc61e3731509c7ba3959289ddcfb30e40193f365fc26f62f4c7e60d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab546dc6dc75d1ec1733c5cd5f29a451881d71db8c21e587be92ab77c2f12f602100468626b742a1d87caa98aa27699906f9aefd7d9cff02512c3be1e355bdd4
|
|
7
|
+
data.tar.gz: 741326d16416834940e08c7084019080188d3e6b252c6dafdf3037bb9a40e2f9aafc0dc771fce7ac4edbb50af6d630067fb3646f4e7f1016ab72d7ceafb04940
|
|
@@ -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,7 +104,7 @@ 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,
|
|
@@ -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,7 +48,7 @@ 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
|
|
|
@@ -106,8 +108,7 @@ class RedisClient
|
|
|
106
108
|
@topology = klass.new(pool, @concurrent_worker, **kwargs)
|
|
107
109
|
@config = config
|
|
108
110
|
@mutex = Mutex.new
|
|
109
|
-
@
|
|
110
|
-
@reload_times = 0
|
|
111
|
+
@next_reload_time = nil
|
|
111
112
|
@random = Random.new
|
|
112
113
|
end
|
|
113
114
|
|
|
@@ -193,26 +194,22 @@ class RedisClient
|
|
|
193
194
|
end
|
|
194
195
|
|
|
195
196
|
def update_slot(slot, node_key)
|
|
196
|
-
return
|
|
197
|
+
return unless @mutex.try_lock
|
|
197
198
|
|
|
198
|
-
@
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
199
|
+
@slots[slot] = node_key
|
|
200
|
+
rescue RangeError
|
|
201
|
+
@slots = Array.new(SLOT_SIZE) { |i| @slots[i] }
|
|
202
|
+
@slots[slot] = node_key
|
|
203
|
+
ensure
|
|
204
|
+
@mutex.unlock if @mutex.owned?
|
|
204
205
|
end
|
|
205
206
|
|
|
206
|
-
def
|
|
207
|
+
def try_reload!
|
|
207
208
|
with_reload_lock do
|
|
208
|
-
|
|
209
|
-
@
|
|
210
|
-
|
|
211
|
-
[node_info.node_key, @config.client_config_for_node(node_info.node_key)]
|
|
209
|
+
with_reload_jitter do
|
|
210
|
+
with_startup_clients(@config.max_startup_sample) do |clients|
|
|
211
|
+
reload!(clients)
|
|
212
212
|
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
213
|
end
|
|
217
214
|
end
|
|
218
215
|
end
|
|
@@ -312,12 +309,11 @@ class RedisClient
|
|
|
312
309
|
work_group.push(i, raw_client) do |client|
|
|
313
310
|
regular_timeout = client.read_timeout
|
|
314
311
|
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)
|
|
312
|
+
fetch_cluster_state(client)
|
|
318
313
|
rescue StandardError => e
|
|
319
314
|
e
|
|
320
315
|
ensure
|
|
316
|
+
client.read_timeout = regular_timeout
|
|
321
317
|
client&.close
|
|
322
318
|
end
|
|
323
319
|
end
|
|
@@ -347,6 +343,16 @@ class RedisClient
|
|
|
347
343
|
grouped.max_by { |_, v| v.size }[1].first
|
|
348
344
|
end
|
|
349
345
|
|
|
346
|
+
def fetch_cluster_state(client)
|
|
347
|
+
reply = client.call_once('cluster', 'shards')
|
|
348
|
+
parse_cluster_shards_reply(reply)
|
|
349
|
+
rescue ::RedisClient::CommandError => e
|
|
350
|
+
raise unless e.message.start_with?('ERR Unknown subcommand')
|
|
351
|
+
|
|
352
|
+
reply = client.call_once('cluster', 'nodes')
|
|
353
|
+
parse_cluster_node_reply(reply)
|
|
354
|
+
end
|
|
355
|
+
|
|
350
356
|
def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
351
357
|
reply.each_line("\n", chomp: true).filter_map do |line|
|
|
352
358
|
fields = line.split
|
|
@@ -390,39 +396,61 @@ class RedisClient
|
|
|
390
396
|
id: id,
|
|
391
397
|
node_key: NodeKey.build_from_host_port(ip, arr[1]),
|
|
392
398
|
role: role,
|
|
393
|
-
primary_id: role == 'master' ?
|
|
399
|
+
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
394
400
|
slots: role == 'master' ? slots : EMPTY_ARRAY
|
|
395
401
|
)
|
|
396
402
|
end
|
|
397
|
-
end
|
|
403
|
+
end
|
|
398
404
|
end
|
|
399
405
|
|
|
400
406
|
def parse_cluster_shards_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
401
407
|
reply.each_with_object([]) do |shard, acc|
|
|
408
|
+
resp2 = shard.is_a?(Array)
|
|
409
|
+
shard = shard.each_slice(2).to_h if resp2
|
|
402
410
|
nodes = shard.fetch('nodes')
|
|
411
|
+
nodes = nodes.map { |n| n.each_slice(2).to_h } if resp2
|
|
403
412
|
primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
|
|
404
413
|
|
|
405
414
|
nodes.each do |node|
|
|
406
|
-
|
|
407
|
-
next if node.fetch('health') != 'online' ||
|
|
415
|
+
host = pick_shard_host(node)
|
|
416
|
+
next if node.fetch('health') != 'online' || host.nil? || host.empty? || host == '?'
|
|
408
417
|
|
|
409
418
|
role = node.fetch('role')
|
|
410
419
|
acc << ::RedisClient::Cluster::Node::Info.new(
|
|
411
420
|
id: node.fetch('id'),
|
|
412
|
-
node_key: NodeKey.build_from_host_port(
|
|
421
|
+
node_key: NodeKey.build_from_host_port(host, node['port'] || node['tls-port']),
|
|
413
422
|
role: role == 'master' ? role : 'slave',
|
|
414
|
-
primary_id: role == 'master' ?
|
|
423
|
+
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
415
424
|
slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
|
|
416
425
|
)
|
|
417
426
|
end
|
|
418
|
-
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Pick the host for a CLUSTER SHARDS node entry.
|
|
431
|
+
#
|
|
432
|
+
# `endpoint` is the server-selected preferred endpoint (driven by the
|
|
433
|
+
# `cluster-preferred-endpoint-type` config), so prefer it when present and
|
|
434
|
+
# usable. Some managed services (e.g. AWS ElastiCache Serverless) report
|
|
435
|
+
# `127.0.0.1` in `ip` while exposing the reachable address only via
|
|
436
|
+
# `endpoint` / `hostname`; falling through to `ip` in that case would build
|
|
437
|
+
# an unreachable topology. This mirrors the precedence `parse_node_key`
|
|
438
|
+
# uses for CLUSTER NODES output (see #207).
|
|
439
|
+
def pick_shard_host(node)
|
|
440
|
+
endpoint = node['endpoint']
|
|
441
|
+
return endpoint if endpoint && !endpoint.empty? && endpoint != '?'
|
|
442
|
+
|
|
443
|
+
hostname = node['hostname']
|
|
444
|
+
return hostname if hostname && !hostname.empty?
|
|
445
|
+
|
|
446
|
+
node['ip']
|
|
419
447
|
end
|
|
420
448
|
|
|
421
449
|
# As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
|
|
422
450
|
# node_key should use hostname if present in CLUSTER NODES output.
|
|
423
451
|
#
|
|
424
452
|
# See https://redis.io/commands/cluster-nodes/ for details on the output format.
|
|
425
|
-
# node_address matches
|
|
453
|
+
# node_address matches the format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
|
|
426
454
|
def parse_node_key(node_address)
|
|
427
455
|
ip_chunk, hostname, _auxiliaries = node_address.split(',')
|
|
428
456
|
ip_port_string = ip_chunk.split('@').first
|
|
@@ -432,6 +460,16 @@ class RedisClient
|
|
|
432
460
|
"#{hostname}:#{port}"
|
|
433
461
|
end
|
|
434
462
|
|
|
463
|
+
def reload!(clients)
|
|
464
|
+
@node_info = refetch_node_info_list(clients)
|
|
465
|
+
@node_configs = @node_info.to_h do |node_info|
|
|
466
|
+
[node_info.node_key, @config.client_config_for_node(node_info.node_key)]
|
|
467
|
+
end
|
|
468
|
+
@slots = build_slot_node_mappings(@node_info)
|
|
469
|
+
@replications = build_replication_mappings(@node_info)
|
|
470
|
+
@topology.process_topology_update!(@replications, @node_configs)
|
|
471
|
+
end
|
|
472
|
+
|
|
435
473
|
def with_startup_clients(count) # rubocop:disable Metrics/AbcSize
|
|
436
474
|
if @config.connect_with_original_config
|
|
437
475
|
# If connect_with_original_config is set, that means we need to build actual client objects
|
|
@@ -457,35 +495,43 @@ class RedisClient
|
|
|
457
495
|
end
|
|
458
496
|
end
|
|
459
497
|
|
|
498
|
+
def with_reload_jitter
|
|
499
|
+
return unless @next_reload_time.nil? || obtain_current_time >= @next_reload_time
|
|
500
|
+
|
|
501
|
+
begin
|
|
502
|
+
yield
|
|
503
|
+
ensure
|
|
504
|
+
@next_reload_time = obtain_current_time + @random.rand(JITTER_WINDOW)
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
460
508
|
def with_reload_lock
|
|
461
|
-
# What should happen with concurrent calls #
|
|
509
|
+
# What should happen with concurrent calls #try_reload! This is a realistic possibility if the cluster goes into
|
|
462
510
|
# 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 #
|
|
511
|
+
# call #try_reload!.
|
|
512
|
+
# For now, if a reload is in progress by a thread, the other threads do not wait for that to complete, and
|
|
513
|
+
# they throw an error.
|
|
514
|
+
# Probably in the future we should add a circuit breaker to #try_reload! itself, and stop trying if the cluster is
|
|
467
515
|
# 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
|
|
516
|
+
return unless @mutex.try_lock
|
|
478
517
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
r
|
|
483
|
-
end
|
|
518
|
+
yield
|
|
519
|
+
ensure
|
|
520
|
+
@mutex.unlock if @mutex.owned?
|
|
484
521
|
end
|
|
485
522
|
|
|
486
523
|
def obtain_current_time
|
|
487
524
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
|
488
525
|
end
|
|
526
|
+
|
|
527
|
+
def bypass_reload!
|
|
528
|
+
# DO NOT USE THIS METHOD
|
|
529
|
+
with_reload_lock do
|
|
530
|
+
with_startup_clients(@config.max_startup_sample) do |clients|
|
|
531
|
+
reload!(clients)
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
end
|
|
489
535
|
end
|
|
490
536
|
end
|
|
491
537
|
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
|
|
|
@@ -54,7 +54,7 @@ class RedisClient
|
|
|
54
54
|
rescue ::RedisClient::ConnectionError
|
|
55
55
|
# Deduct the number of retries that happened _inside_ router#handle_redirection from our remaining
|
|
56
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
|
|
57
|
+
retry_count -= times_block_executed == 0 ? 1 : [times_block_executed, 1].min
|
|
58
58
|
raise if retry_count < 0
|
|
59
59
|
|
|
60
60
|
retry
|
|
@@ -230,10 +230,10 @@ class RedisClient
|
|
|
230
230
|
|
|
231
231
|
def append_pipeline(node_key)
|
|
232
232
|
@pipelines ||= {}
|
|
233
|
-
@pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(::RedisClient::Cluster::NoopCommandBuilder)
|
|
234
|
-
|
|
233
|
+
pi = (@pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(::RedisClient::Cluster::NoopCommandBuilder))
|
|
234
|
+
pi.add_outer_index(@size)
|
|
235
235
|
@size += 1
|
|
236
|
-
|
|
236
|
+
pi
|
|
237
237
|
end
|
|
238
238
|
|
|
239
239
|
def do_pipelining(client, pipeline)
|
|
@@ -61,7 +61,17 @@ class RedisClient
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
|
|
64
|
-
|
|
64
|
+
RECOVERY_BASE_INTERVAL = Float(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_INTERVAL_SEC', 1.0))
|
|
65
|
+
RECOVERY_MAX_INTERVAL = Float(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_MAX_INTERVAL_SEC', 30.0))
|
|
66
|
+
RECOVERY_MAX_ATTEMPTS = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_MAX_ATTEMPTS', 10))
|
|
67
|
+
SUBSCRIBE_COMMANDS = %w[subscribe psubscribe ssubscribe].freeze
|
|
68
|
+
UNSUBSCRIBE_TO_SUBSCRIBE = {
|
|
69
|
+
'unsubscribe' => 'subscribe',
|
|
70
|
+
'punsubscribe' => 'psubscribe',
|
|
71
|
+
'sunsubscribe' => 'ssubscribe'
|
|
72
|
+
}.freeze
|
|
73
|
+
private_constant :BUF_SIZE, :RECOVERY_BASE_INTERVAL, :RECOVERY_MAX_INTERVAL, :RECOVERY_MAX_ATTEMPTS,
|
|
74
|
+
:SUBSCRIBE_COMMANDS, :UNSUBSCRIBE_TO_SUBSCRIBE
|
|
65
75
|
|
|
66
76
|
def initialize(router, command_builder)
|
|
67
77
|
@router = router
|
|
@@ -74,14 +84,14 @@ class RedisClient
|
|
|
74
84
|
def call(*args, **kwargs)
|
|
75
85
|
command = @command_builder.generate(args, kwargs)
|
|
76
86
|
_call(command)
|
|
77
|
-
|
|
87
|
+
remember(command)
|
|
78
88
|
nil
|
|
79
89
|
end
|
|
80
90
|
|
|
81
91
|
def call_v(command)
|
|
82
92
|
command = @command_builder.generate(command)
|
|
83
93
|
_call(command)
|
|
84
|
-
|
|
94
|
+
remember(command)
|
|
85
95
|
nil
|
|
86
96
|
end
|
|
87
97
|
|
|
@@ -118,6 +128,35 @@ class RedisClient
|
|
|
118
128
|
|
|
119
129
|
private
|
|
120
130
|
|
|
131
|
+
def remember(command)
|
|
132
|
+
cmd = command.first.to_s.downcase
|
|
133
|
+
if SUBSCRIBE_COMMANDS.include?(cmd)
|
|
134
|
+
@commands << command
|
|
135
|
+
elsif (subscribe_cmd = UNSUBSCRIBE_TO_SUBSCRIBE[cmd])
|
|
136
|
+
forget_subscriptions(subscribe_cmd, command[1..])
|
|
137
|
+
else
|
|
138
|
+
@commands << command
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def forget_subscriptions(subscribe_cmd, channels)
|
|
143
|
+
if channels.nil? || channels.empty?
|
|
144
|
+
@commands.reject! { |c| c.first.to_s.casecmp(subscribe_cmd).zero? }
|
|
145
|
+
else
|
|
146
|
+
@commands.reject! { |c| prune_entry(c, subscribe_cmd, channels) }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def prune_entry(entry, subscribe_cmd, channels)
|
|
151
|
+
return false unless entry.first.to_s.casecmp(subscribe_cmd).zero?
|
|
152
|
+
|
|
153
|
+
remaining = entry[1..] - channels
|
|
154
|
+
return true if remaining.empty?
|
|
155
|
+
|
|
156
|
+
entry.replace([entry.first, *remaining])
|
|
157
|
+
false
|
|
158
|
+
end
|
|
159
|
+
|
|
121
160
|
def _call(command) # rubocop:disable Metrics/AbcSize
|
|
122
161
|
if command.first.casecmp('subscribe').zero?
|
|
123
162
|
call_to_single_state(command)
|
|
@@ -179,6 +218,7 @@ class RedisClient
|
|
|
179
218
|
end
|
|
180
219
|
|
|
181
220
|
def start_over
|
|
221
|
+
attempt = 0
|
|
182
222
|
loop do
|
|
183
223
|
@router.renew_cluster_state
|
|
184
224
|
@state_dict.each_value(&:close)
|
|
@@ -186,9 +226,16 @@ class RedisClient
|
|
|
186
226
|
@commands.each { |command| _call(command) }
|
|
187
227
|
break
|
|
188
228
|
rescue ::RedisClient::ConnectionError, ::RedisClient::Cluster::NodeMightBeDown
|
|
189
|
-
|
|
229
|
+
attempt += 1
|
|
230
|
+
raise if attempt >= RECOVERY_MAX_ATTEMPTS
|
|
231
|
+
|
|
232
|
+
sleep recovery_interval(attempt)
|
|
190
233
|
end
|
|
191
234
|
end
|
|
235
|
+
|
|
236
|
+
def recovery_interval(attempt)
|
|
237
|
+
[RECOVERY_BASE_INTERVAL * (2**(attempt - 1)), RECOVERY_MAX_INTERVAL].min
|
|
238
|
+
end
|
|
192
239
|
end
|
|
193
240
|
end
|
|
194
241
|
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,8 +166,8 @@ 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
|
+
retry_count -= 1
|
|
172
171
|
|
|
173
172
|
if retry_count >= 0
|
|
174
173
|
# Find the node to use for this command - if this fails for some reason, though, re-use
|
|
@@ -180,7 +179,6 @@ class RedisClient
|
|
|
180
179
|
retry
|
|
181
180
|
end
|
|
182
181
|
|
|
183
|
-
retry if retry_count >= 0
|
|
184
182
|
raise
|
|
185
183
|
end
|
|
186
184
|
|
|
@@ -248,23 +246,27 @@ class RedisClient
|
|
|
248
246
|
end
|
|
249
247
|
|
|
250
248
|
def find_node_key(command, seed: nil)
|
|
251
|
-
|
|
252
|
-
find_node_key_by_key(
|
|
249
|
+
cmd_spec = @command.get_spec(command.first)
|
|
250
|
+
find_node_key_by_key(
|
|
251
|
+
cmd_spec&.extract_first_key(command),
|
|
252
|
+
seed: seed,
|
|
253
|
+
primary: cmd_spec&.should_send_to_primary?
|
|
254
|
+
)
|
|
253
255
|
end
|
|
254
256
|
|
|
255
257
|
def find_primary_node_key(command)
|
|
256
|
-
key = @command.extract_first_key(command)
|
|
257
|
-
return
|
|
258
|
+
key = @command.get_spec(command.first)&.extract_first_key(command)
|
|
259
|
+
return unless key&.size&.> 0
|
|
258
260
|
|
|
259
261
|
find_node_key_by_key(key, primary: true)
|
|
260
262
|
end
|
|
261
263
|
|
|
262
264
|
def find_slot(command)
|
|
263
|
-
find_slot_by_key(@command.extract_first_key(command))
|
|
265
|
+
find_slot_by_key(@command.get_spec(command.first)&.extract_first_key(command))
|
|
264
266
|
end
|
|
265
267
|
|
|
266
268
|
def find_slot_by_key(key)
|
|
267
|
-
return if key.empty?
|
|
269
|
+
return if key.nil? || key.empty?
|
|
268
270
|
|
|
269
271
|
::RedisClient::Cluster::KeySlotConverter.convert(key)
|
|
270
272
|
end
|
|
@@ -294,7 +296,7 @@ class RedisClient
|
|
|
294
296
|
end
|
|
295
297
|
|
|
296
298
|
def renew_cluster_state
|
|
297
|
-
@node.
|
|
299
|
+
@node.try_reload!
|
|
298
300
|
rescue ::RedisClient::Cluster::InitialSetupError
|
|
299
301
|
# ignore
|
|
300
302
|
end
|
|
@@ -340,8 +342,8 @@ class RedisClient
|
|
|
340
342
|
raise if retry_count <= 0
|
|
341
343
|
raise if e.errors.values.none? { |err| err.message.include?('WAIT cannot be used with replica instances') }
|
|
342
344
|
|
|
343
|
-
retry_count -= 1
|
|
344
345
|
renew_cluster_state
|
|
346
|
+
retry_count -= 1
|
|
345
347
|
retry
|
|
346
348
|
end
|
|
347
349
|
|
|
@@ -452,7 +454,7 @@ class RedisClient
|
|
|
452
454
|
end
|
|
453
455
|
|
|
454
456
|
def send_multiple_keys_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
455
|
-
# This implementation
|
|
457
|
+
# This implementation prioritizes performance over readability.
|
|
456
458
|
cmd = command.first
|
|
457
459
|
if cmd.casecmp('mget').zero?
|
|
458
460
|
single_key_cmd = 'get'
|
|
@@ -469,8 +471,13 @@ class RedisClient
|
|
|
469
471
|
|
|
470
472
|
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
473
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
+
pipeline = ::RedisClient::Cluster::Pipeline.new(
|
|
475
|
+
self,
|
|
476
|
+
@command_builder,
|
|
477
|
+
@concurrent_worker,
|
|
478
|
+
exception: true,
|
|
479
|
+
seed: Random.new_seed
|
|
480
|
+
)
|
|
474
481
|
|
|
475
482
|
single_command = Array.new(keys_step + 1)
|
|
476
483
|
single_command[0] = single_key_cmd
|
|
@@ -502,8 +509,8 @@ class RedisClient
|
|
|
502
509
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
|
503
510
|
raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config) if retry_count <= 0
|
|
504
511
|
|
|
505
|
-
retry_count -= 1
|
|
506
512
|
renew_cluster_state
|
|
513
|
+
retry_count -= 1
|
|
507
514
|
retry
|
|
508
515
|
end
|
|
509
516
|
end
|
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
|
|