redis-cluster-client 0.3.6 → 0.3.8
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 +28 -29
- data/lib/redis_client/cluster/node/latency_replica.rb +1 -1
- data/lib/redis_client/cluster/node/random_replica.rb +1 -1
- data/lib/redis_client/cluster/node.rb +71 -25
- data/lib/redis_client/cluster/pipeline.rb +1 -2
- data/lib/redis_client/cluster/router.rb +4 -5
- data/lib/redis_client/cluster_config.rb +1 -1
- 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: 18f675a5c604e59eed84fc2b90de0b42ddfbddfe947ccdee442fdf39f379270a
|
4
|
+
data.tar.gz: bf3962c29e9df010c2ef25ea226c5e041bf120ca65ae2392ab0a251a21276b44
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a8894042ac085a832548195a519095c73036d0da1ce4f3f543d675f272751fb46af42defe9a83f5dc0aea1681c1abc6b82a98c44d9b855ddbe7005a04b8cfa8
|
7
|
+
data.tar.gz: ef2f92ee572dae262b3eb4e94c198e5618ad013343636fd5442d3da46b0ec48f433fd977a723d624d8a14e93a5315631ad8cb72a16ad3d46ad918186197a0070
|
@@ -9,6 +9,14 @@ class RedisClient
|
|
9
9
|
class Command
|
10
10
|
EMPTY_STRING = ''
|
11
11
|
|
12
|
+
Detail = Struct.new(
|
13
|
+
'RedisCommand',
|
14
|
+
:first_key_position,
|
15
|
+
:write?,
|
16
|
+
:readonly?,
|
17
|
+
keyword_init: true
|
18
|
+
)
|
19
|
+
|
12
20
|
class << self
|
13
21
|
def load(nodes)
|
14
22
|
errors = []
|
@@ -17,8 +25,8 @@ class RedisClient
|
|
17
25
|
break unless cmd.nil?
|
18
26
|
|
19
27
|
reply = node.call('COMMAND')
|
20
|
-
|
21
|
-
cmd = ::RedisClient::Cluster::Command.new(
|
28
|
+
commands = parse_command_reply(reply)
|
29
|
+
cmd = ::RedisClient::Cluster::Command.new(commands)
|
22
30
|
rescue ::RedisClient::Error => e
|
23
31
|
errors << e
|
24
32
|
end
|
@@ -30,15 +38,22 @@ class RedisClient
|
|
30
38
|
|
31
39
|
private
|
32
40
|
|
33
|
-
def
|
41
|
+
def parse_command_reply(rows)
|
34
42
|
rows&.reject { |row| row[0].nil? }.to_h do |row|
|
35
|
-
[
|
43
|
+
[
|
44
|
+
row[0].downcase,
|
45
|
+
::RedisClient::Cluster::Command::Detail.new(
|
46
|
+
first_key_position: row[3],
|
47
|
+
write?: row[2].include?('write'),
|
48
|
+
readonly?: row[2].include?('readonly')
|
49
|
+
)
|
50
|
+
]
|
36
51
|
end
|
37
52
|
end
|
38
53
|
end
|
39
54
|
|
40
|
-
def initialize(
|
41
|
-
@
|
55
|
+
def initialize(commands)
|
56
|
+
@commands = commands || {}
|
42
57
|
end
|
43
58
|
|
44
59
|
def extract_first_key(command)
|
@@ -51,39 +66,23 @@ class RedisClient
|
|
51
66
|
end
|
52
67
|
|
53
68
|
def should_send_to_primary?(command)
|
54
|
-
|
69
|
+
name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
70
|
+
@commands[name]&.write?
|
55
71
|
end
|
56
72
|
|
57
73
|
def should_send_to_replica?(command)
|
58
|
-
|
74
|
+
name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
75
|
+
@commands[name]&.readonly?
|
59
76
|
end
|
60
77
|
|
61
78
|
def exists?(name)
|
62
|
-
key
|
63
|
-
@details.key?(key)
|
79
|
+
@commands.key?(::RedisClient::Cluster::NormalizedCmdName.instance.get_by_name(name))
|
64
80
|
end
|
65
81
|
|
66
82
|
private
|
67
83
|
|
68
|
-
def pick_details(details)
|
69
|
-
(details || {}).transform_values do |detail|
|
70
|
-
{
|
71
|
-
first_key_position: detail[:first],
|
72
|
-
write: detail[:flags].include?('write'),
|
73
|
-
readonly: detail[:flags].include?('readonly')
|
74
|
-
}
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def dig_details(command, key)
|
79
|
-
name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
80
|
-
return if name.empty? || !@details.key?(name)
|
81
|
-
|
82
|
-
@details.fetch(name).fetch(key)
|
83
|
-
end
|
84
|
-
|
85
84
|
def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity
|
86
|
-
case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
85
|
+
case name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
87
86
|
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
|
88
87
|
when 'object' then 2
|
89
88
|
when 'memory'
|
@@ -93,7 +92,7 @@ class RedisClient
|
|
93
92
|
when 'xread', 'xreadgroup'
|
94
93
|
determine_optional_key_position(command, 'streams')
|
95
94
|
else
|
96
|
-
|
95
|
+
@commands[name]&.first_key_position.to_i
|
97
96
|
end
|
98
97
|
end
|
99
98
|
|
@@ -34,7 +34,7 @@ class RedisClient
|
|
34
34
|
|
35
35
|
def any_replica_node_key(seed: nil)
|
36
36
|
random = seed.nil? ? Random : Random.new(seed)
|
37
|
-
@existed_replicas.sample(random: random)&.first
|
37
|
+
@existed_replicas.sample(random: random)&.first || any_primary_node_key(seed: seed)
|
38
38
|
end
|
39
39
|
|
40
40
|
private
|
@@ -20,6 +20,40 @@ class RedisClient
|
|
20
20
|
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
21
21
|
|
22
22
|
ReloadNeeded = Class.new(::RedisClient::Error)
|
23
|
+
Info = Struct.new(
|
24
|
+
'RedisNode',
|
25
|
+
:id, :node_key, :role, :primary_id, :ping_sent,
|
26
|
+
:pong_recv, :config_epoch, :link_state, :slots,
|
27
|
+
keyword_init: true
|
28
|
+
) do
|
29
|
+
def primary?
|
30
|
+
role == 'master'
|
31
|
+
end
|
32
|
+
|
33
|
+
def replica?
|
34
|
+
role == 'slave'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
SLOT_OPTIMIZATION_MAX_SHARD_SIZE = 256
|
39
|
+
SLOT_OPTIMIZATION_STRING = '0' * SLOT_SIZE
|
40
|
+
Slot = Struct.new('RedisSlot', :slots, :primary_node_keys, keyword_init: true) do
|
41
|
+
def [](slot)
|
42
|
+
primary_node_keys[slots.getbyte(slot)]
|
43
|
+
end
|
44
|
+
|
45
|
+
def []=(slot, primary_node_key)
|
46
|
+
index = primary_node_keys.find_index(primary_node_key)
|
47
|
+
if index.nil?
|
48
|
+
raise(::RedisClient::Cluster::Node::ReloadNeeded, primary_node_key) if primary_node_keys.size >= SLOT_OPTIMIZATION_MAX_SHARD_SIZE
|
49
|
+
|
50
|
+
index = primary_node_keys.size
|
51
|
+
primary_node_keys << primary_node_key
|
52
|
+
end
|
53
|
+
|
54
|
+
slots.setbyte(slot, index)
|
55
|
+
end
|
56
|
+
end
|
23
57
|
|
24
58
|
class Config < ::RedisClient::Config
|
25
59
|
def initialize(scale_read: false, **kwargs)
|
@@ -48,7 +82,7 @@ class RedisClient
|
|
48
82
|
Thread.pass
|
49
83
|
Thread.current.thread_variable_set(:index, i)
|
50
84
|
reply = cli.call('CLUSTER', 'NODES')
|
51
|
-
Thread.current.thread_variable_set(:info,
|
85
|
+
Thread.current.thread_variable_set(:info, parse_cluster_node_reply(reply))
|
52
86
|
rescue StandardError => e
|
53
87
|
Thread.current.thread_variable_set(:error, e)
|
54
88
|
ensure
|
@@ -70,10 +104,11 @@ class RedisClient
|
|
70
104
|
|
71
105
|
raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
|
72
106
|
|
73
|
-
grouped = node_info_list.compact.group_by do |
|
74
|
-
|
75
|
-
|
76
|
-
|
107
|
+
grouped = node_info_list.compact.group_by do |info_list|
|
108
|
+
info_list
|
109
|
+
.sort_by(&:id)
|
110
|
+
.map { |i| "#{i.id}#{i.node_key}#{i.role}#{i.primary_id}#{i.config_epoch}" }
|
111
|
+
.join
|
77
112
|
end
|
78
113
|
|
79
114
|
grouped.max_by { |_, v| v.size }[1].first
|
@@ -83,8 +118,8 @@ class RedisClient
|
|
83
118
|
|
84
119
|
# @see https://redis.io/commands/cluster-nodes/
|
85
120
|
# @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
|
86
|
-
def
|
87
|
-
rows =
|
121
|
+
def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
122
|
+
rows = reply.split("\n").map(&:split)
|
88
123
|
rows.each { |arr| arr[2] = arr[2].split(',') }
|
89
124
|
rows.select! { |arr| arr[7] == 'connected' && (arr[2] & %w[fail? fail handshake noaddr noflags]).empty? }
|
90
125
|
rows.each do |arr|
|
@@ -99,23 +134,25 @@ class RedisClient
|
|
99
134
|
end
|
100
135
|
|
101
136
|
rows.map do |arr|
|
102
|
-
|
103
|
-
|
137
|
+
::RedisClient::Cluster::Node::Info.new(
|
138
|
+
id: arr[0], node_key: arr[1], role: arr[2], primary_id: arr[3], ping_sent: arr[4],
|
139
|
+
pong_recv: arr[5], config_epoch: arr[6], link_state: arr[7], slots: arr[8]
|
140
|
+
)
|
104
141
|
end
|
105
142
|
end
|
106
143
|
end
|
107
144
|
|
108
|
-
def initialize(
|
145
|
+
def initialize(
|
109
146
|
options,
|
110
|
-
|
147
|
+
node_info_list: [],
|
111
148
|
with_replica: false,
|
112
149
|
replica_affinity: :random,
|
113
150
|
pool: nil,
|
114
151
|
**kwargs
|
115
152
|
)
|
116
153
|
|
117
|
-
@slots = build_slot_node_mappings(
|
118
|
-
@replications = build_replication_mappings(
|
154
|
+
@slots = build_slot_node_mappings(node_info_list)
|
155
|
+
@replications = build_replication_mappings(node_info_list)
|
119
156
|
@topology = make_topology_class(with_replica, replica_affinity).new(@replications, options, pool, **kwargs)
|
120
157
|
@mutex = Mutex.new
|
121
158
|
end
|
@@ -207,23 +244,32 @@ class RedisClient
|
|
207
244
|
end
|
208
245
|
end
|
209
246
|
|
210
|
-
def build_slot_node_mappings(
|
211
|
-
slots =
|
212
|
-
|
213
|
-
next if info
|
247
|
+
def build_slot_node_mappings(node_info_list)
|
248
|
+
slots = make_array_for_slot_node_mappings(node_info_list)
|
249
|
+
node_info_list.each do |info|
|
250
|
+
next if info.slots.nil? || info.slots.empty?
|
214
251
|
|
215
|
-
info
|
252
|
+
info.slots.each { |start, last| (start..last).each { |i| slots[i] = info.node_key } }
|
216
253
|
end
|
217
254
|
|
218
255
|
slots
|
219
256
|
end
|
220
257
|
|
221
|
-
def
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
258
|
+
def make_array_for_slot_node_mappings(node_info_list)
|
259
|
+
return Array.new(SLOT_SIZE) if node_info_list.count(&:primary?) > SLOT_OPTIMIZATION_MAX_SHARD_SIZE
|
260
|
+
|
261
|
+
::RedisClient::Cluster::Node::Slot.new(
|
262
|
+
slots: String.new(SLOT_OPTIMIZATION_STRING, encoding: Encoding::BINARY, capacity: SLOT_SIZE),
|
263
|
+
primary_node_keys: node_info_list.select(&:primary?).map(&:node_key)
|
264
|
+
)
|
265
|
+
end
|
266
|
+
|
267
|
+
def build_replication_mappings(node_info_list) # rubocop:disable Metrics/AbcSize
|
268
|
+
dict = node_info_list.to_h { |info| [info.id, info] }
|
269
|
+
node_info_list.each_with_object(Hash.new { |h, k| h[k] = [] }) do |info, acc|
|
270
|
+
primary_info = dict[info.primary_id]
|
271
|
+
acc[primary_info.node_key] << info.node_key unless primary_info.nil?
|
272
|
+
acc[info.node_key] if info.primary? # for the primary which have no replicas
|
227
273
|
end
|
228
274
|
end
|
229
275
|
|
@@ -232,7 +278,7 @@ class RedisClient
|
|
232
278
|
client.send(method, *args, command, &block)
|
233
279
|
end
|
234
280
|
|
235
|
-
[results
|
281
|
+
[results&.values, errors]
|
236
282
|
end
|
237
283
|
|
238
284
|
def call_multiple_nodes!(clients, method, command, args, &block)
|
@@ -63,8 +63,7 @@ class RedisClient
|
|
63
63
|
raise
|
64
64
|
end
|
65
65
|
|
66
|
-
# @see https://redis.io/
|
67
|
-
# Redirection and resharding
|
66
|
+
# @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
|
68
67
|
def try_send(node, method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
69
68
|
if args.empty?
|
70
69
|
# prevent memory allocation for variable-length args
|
@@ -271,12 +270,12 @@ class RedisClient
|
|
271
270
|
end
|
272
271
|
|
273
272
|
def fetch_cluster_info(config, pool: nil, **kwargs)
|
274
|
-
|
275
|
-
node_addrs =
|
273
|
+
node_info_list = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
|
274
|
+
node_addrs = node_info_list.map { |i| ::RedisClient::Cluster::NodeKey.hashify(i.node_key) }
|
276
275
|
config.update_node(node_addrs)
|
277
276
|
::RedisClient::Cluster::Node.new(
|
278
277
|
config.per_node_key,
|
279
|
-
|
278
|
+
node_info_list: node_info_list,
|
280
279
|
pool: pool,
|
281
280
|
with_replica: config.use_replica?,
|
282
281
|
replica_affinity: config.replica_affinity,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-cluster-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taishi Kasuga
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-09-
|
11
|
+
date: 2022-09-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|