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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24e153973525e8cd3905e92e851194a5c1f4349cecd71ca4a71db34fa678d1d8
4
- data.tar.gz: 963ac53058e861d50c55160f55f9b064e853c49b29811075ece80ce34fa7b0f8
3
+ metadata.gz: 18f675a5c604e59eed84fc2b90de0b42ddfbddfe947ccdee442fdf39f379270a
4
+ data.tar.gz: bf3962c29e9df010c2ef25ea226c5e041bf120ca65ae2392ab0a251a21276b44
5
5
  SHA512:
6
- metadata.gz: 841bdd5aa580dcd7c3169fe88f36b8ed888ffffd3cf50b9f75449365c430afb41d79d88f2265183119f0ed012ace418868679cad468bcf39b25aed30b6207da1
7
- data.tar.gz: 5f36751289ba73d5471cfe44d9e3d10cb70fd517249ce48ad7ea4254de0595c869fb900fecd3b0abd690fe977a732650ebf8da80a5d95ed1464a3345b79243fa
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
- details = parse_command_details(reply)
21
- cmd = ::RedisClient::Cluster::Command.new(details)
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 parse_command_details(rows)
41
+ def parse_command_reply(rows)
34
42
  rows&.reject { |row| row[0].nil? }.to_h do |row|
35
- [row[0].downcase, { arity: row[1], flags: row[2], first: row[3], last: row[4], step: row[5] }]
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(details)
41
- @details = pick_details(details)
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
- dig_details(command, :write)
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
- dig_details(command, :readonly)
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 = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_name(name)
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
- dig_details(command, :first_key_position).to_i
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
@@ -29,7 +29,7 @@ class RedisClient
29
29
 
30
30
  def any_replica_node_key(seed: nil)
31
31
  random = seed.nil? ? Random : Random.new(seed)
32
- @replica_node_keys.sample(random: random)
32
+ @replica_node_keys.sample(random: random) || any_primary_node_key(seed: seed)
33
33
  end
34
34
  end
35
35
  end
@@ -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, parse_node_info(reply))
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 |rows|
74
- rows.sort_by { |row| row[:id] }
75
- .map { |r| "#{r[:id]}#{r[:node_key]}#{r[:role]}#{r[:primary_id]}#{r[:config_epoch]}" }
76
- .join
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 parse_node_info(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
87
- rows = info.split("\n").map(&:split)
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
- { id: arr[0], node_key: arr[1], role: arr[2], primary_id: arr[3], ping_sent: arr[4],
103
- pong_recv: arr[5], config_epoch: arr[6], link_state: arr[7], slots: arr[8] }
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( # rubocop:disable Metrics/ParameterLists
145
+ def initialize(
109
146
  options,
110
- node_info: [],
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(node_info)
118
- @replications = build_replication_mappings(node_info)
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(node_info)
211
- slots = Array.new(SLOT_SIZE)
212
- node_info.each do |info|
213
- next if info[:slots].nil? || info[:slots].empty?
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[:slots].each { |start, last| (start..last).each { |i| slots[i] = info[:node_key] } }
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 build_replication_mappings(node_info) # rubocop:disable Metrics/AbcSize
222
- dict = node_info.to_h { |info| [info[:id], info] }
223
- node_info.each_with_object(Hash.new { |h, k| h[k] = [] }) do |info, acc|
224
- primary_info = dict[info[:primary_id]]
225
- acc[primary_info[:node_key]] << info[:node_key] unless primary_info.nil?
226
- acc[info[:node_key]] if info[:role] == 'master' # for the primary which have no replicas
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.values, errors]
281
+ [results&.values, errors]
236
282
  end
237
283
 
238
284
  def call_multiple_nodes!(clients, method, command, args, &block)
@@ -155,9 +155,8 @@ class RedisClient
155
155
  end
156
156
  end
157
157
 
158
+ threads.each(&:join)
158
159
  threads.each do |t|
159
- t.join
160
-
161
160
  if t.thread_variable?(:replies)
162
161
  all_replies ||= Array.new(@size)
163
162
  @pipelines[t.thread_variable_get(:node_key)]
@@ -63,8 +63,7 @@ class RedisClient
63
63
  raise
64
64
  end
65
65
 
66
- # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
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
- node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
275
- node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
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
- node_info: node_info,
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,
@@ -22,7 +22,7 @@ class RedisClient
22
22
 
23
23
  attr_reader :command_builder, :client_config, :replica_affinity
24
24
 
25
- def initialize( # rubocop:disable Metrics/ParameterLists
25
+ def initialize(
26
26
  nodes: DEFAULT_NODES,
27
27
  replica: false,
28
28
  replica_affinity: :random,
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.6
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-22 00:00:00.000000000 Z
11
+ date: 2022-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client