redis-cluster-client 0.16.2 → 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 +47 -48
- 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/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 +22 -3
- data/lib/redis_client/cluster/pipeline.rb +3 -3
- data/lib/redis_client/cluster/router.rb +17 -8
- data/lib/redis_client/cluster.rb +1 -2
- 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
|
|
@@ -65,7 +104,7 @@ class RedisClient
|
|
|
65
104
|
else row[2].include?('write')
|
|
66
105
|
end
|
|
67
106
|
|
|
68
|
-
acc[row.first] = ::RedisClient::Cluster::Command::
|
|
107
|
+
acc[row.first] = ::RedisClient::Cluster::Command::Spec.new(
|
|
69
108
|
first_key_position: pos,
|
|
70
109
|
key_step: row[5],
|
|
71
110
|
write?: writable,
|
|
@@ -79,53 +118,13 @@ class RedisClient
|
|
|
79
118
|
@commands = commands || EMPTY_HASH
|
|
80
119
|
end
|
|
81
120
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
return EMPTY_STRING if i == 0
|
|
85
|
-
|
|
86
|
-
command[i]
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def should_send_to_primary?(command)
|
|
90
|
-
find_command_info(command.first)&.write?
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def should_send_to_replica?(command)
|
|
94
|
-
find_command_info(command.first)&.readonly?
|
|
121
|
+
def get_spec(name)
|
|
122
|
+
@commands[name] || @commands[name.to_s.downcase(:ascii)]
|
|
95
123
|
end
|
|
96
124
|
|
|
97
125
|
def exists?(name)
|
|
98
126
|
@commands.key?(name) || @commands.key?(name.to_s.downcase(:ascii))
|
|
99
127
|
end
|
|
100
|
-
|
|
101
|
-
private
|
|
102
|
-
|
|
103
|
-
def find_command_info(name)
|
|
104
|
-
@commands[name] || @commands[name.to_s.downcase(:ascii)]
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
|
|
108
|
-
i = find_command_info(command.first)&.first_key_position.to_i
|
|
109
|
-
return i if i > 0
|
|
110
|
-
|
|
111
|
-
cmd_name = command.first
|
|
112
|
-
if cmd_name.casecmp('xread').zero?
|
|
113
|
-
determine_optional_key_position(command, 'streams')
|
|
114
|
-
elsif cmd_name.casecmp('xreadgroup').zero?
|
|
115
|
-
determine_optional_key_position(command, 'streams')
|
|
116
|
-
elsif cmd_name.casecmp('migrate').zero?
|
|
117
|
-
command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
|
|
118
|
-
elsif cmd_name.casecmp('memory').zero?
|
|
119
|
-
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
|
120
|
-
else
|
|
121
|
-
i
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def determine_optional_key_position(command, option_name)
|
|
126
|
-
i = command.index { |v| v.to_s.casecmp(option_name).zero? }
|
|
127
|
-
i.nil? ? 0 : i + 1
|
|
128
|
-
end
|
|
129
128
|
end
|
|
130
129
|
end
|
|
131
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
|
|
@@ -412,13 +412,13 @@ class RedisClient
|
|
|
412
412
|
primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
|
|
413
413
|
|
|
414
414
|
nodes.each do |node|
|
|
415
|
-
|
|
416
|
-
next if node.fetch('health') != 'online' ||
|
|
415
|
+
host = pick_shard_host(node)
|
|
416
|
+
next if node.fetch('health') != 'online' || host.nil? || host.empty? || host == '?'
|
|
417
417
|
|
|
418
418
|
role = node.fetch('role')
|
|
419
419
|
acc << ::RedisClient::Cluster::Node::Info.new(
|
|
420
420
|
id: node.fetch('id'),
|
|
421
|
-
node_key: NodeKey.build_from_host_port(
|
|
421
|
+
node_key: NodeKey.build_from_host_port(host, node['port'] || node['tls-port']),
|
|
422
422
|
role: role == 'master' ? role : 'slave',
|
|
423
423
|
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
424
424
|
slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
|
|
@@ -427,6 +427,25 @@ class RedisClient
|
|
|
427
427
|
end
|
|
428
428
|
end
|
|
429
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']
|
|
447
|
+
end
|
|
448
|
+
|
|
430
449
|
# As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
|
|
431
450
|
# node_key should use hostname if present in CLUSTER NODES output.
|
|
432
451
|
#
|
|
@@ -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)
|
|
@@ -246,23 +246,27 @@ class RedisClient
|
|
|
246
246
|
end
|
|
247
247
|
|
|
248
248
|
def find_node_key(command, seed: nil)
|
|
249
|
-
|
|
250
|
-
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
|
+
)
|
|
251
255
|
end
|
|
252
256
|
|
|
253
257
|
def find_primary_node_key(command)
|
|
254
|
-
key = @command.extract_first_key(command)
|
|
255
|
-
return
|
|
258
|
+
key = @command.get_spec(command.first)&.extract_first_key(command)
|
|
259
|
+
return unless key&.size&.> 0
|
|
256
260
|
|
|
257
261
|
find_node_key_by_key(key, primary: true)
|
|
258
262
|
end
|
|
259
263
|
|
|
260
264
|
def find_slot(command)
|
|
261
|
-
find_slot_by_key(@command.extract_first_key(command))
|
|
265
|
+
find_slot_by_key(@command.get_spec(command.first)&.extract_first_key(command))
|
|
262
266
|
end
|
|
263
267
|
|
|
264
268
|
def find_slot_by_key(key)
|
|
265
|
-
return if key.empty?
|
|
269
|
+
return if key.nil? || key.empty?
|
|
266
270
|
|
|
267
271
|
::RedisClient::Cluster::KeySlotConverter.convert(key)
|
|
268
272
|
end
|
|
@@ -467,8 +471,13 @@ class RedisClient
|
|
|
467
471
|
|
|
468
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])
|
|
469
473
|
|
|
470
|
-
|
|
471
|
-
|
|
474
|
+
pipeline = ::RedisClient::Cluster::Pipeline.new(
|
|
475
|
+
self,
|
|
476
|
+
@command_builder,
|
|
477
|
+
@concurrent_worker,
|
|
478
|
+
exception: true,
|
|
479
|
+
seed: Random.new_seed
|
|
480
|
+
)
|
|
472
481
|
|
|
473
482
|
single_command = Array.new(keys_step + 1)
|
|
474
483
|
single_command[0] = single_key_cmd
|
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
|