redis-cluster-client 0.0.7 → 0.0.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: aff938c0517542c32f98206c28f1f605a52751e2e2e9a1bd10f5576285f568ee
4
- data.tar.gz: 8a3d45fba93334959b1ee67504d2703bb116a8133ef8dac2aa8f314fdb74b268
3
+ metadata.gz: d13300ba8c7f294d3e4c86932d13775ad22060c172900c01d521b1ed8c2278d6
4
+ data.tar.gz: d5b457978bb312a718ba57b48b56fac1ceb07a463889835c8190794007ecbf7d
5
5
  SHA512:
6
- metadata.gz: 63dec0155a50a0dbb7a17763c43291d371abe4e3df407340ba8edf125cf2c2bb338f607026a1d004dca489761cb2e6b5450e68a468f18799e5c3443b52a21be7
7
- data.tar.gz: 4de6c4688ca120d7f4b636bd884195b3cddc3bf8d217ea662a1f382ced46de2c9e24b0e967a801d175f0572a083b9e5ea835d54797422cf60ce799cda3002f74
6
+ metadata.gz: 37fb06428eed5a9cca5b42d27c034f057dd7d6f0c5d0c17e4b42c60a8fa3711127c5ed81f06e0565331f64aa762ccc30ee6674fb541e56a8b322ee50ab7a75ce
7
+ data.tar.gz: 06d46f93c8c7b7116378d2ef9e4380ccce3aac74c071b709cbacd677ec0b302302b52160c04b2c3523771f69446b96d84e8b4f729765073386c5e5f1f3f27f5b
@@ -68,12 +68,14 @@ class RedisClient
68
68
  @details.fetch(name).fetch(key)
69
69
  end
70
70
 
71
- def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity
71
+ def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
72
72
  case command&.flatten&.first.to_s.downcase
73
- when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
73
+ when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
74
74
  when 'object' then 2
75
75
  when 'memory'
76
76
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
77
+ when 'migrate'
78
+ command[3] == '""' ? determine_optional_key_position(command, 'keys') : 3
77
79
  when 'xread', 'xreadgroup'
78
80
  determine_optional_key_position(command, 'streams')
79
81
  else
@@ -51,5 +51,15 @@ class RedisClient
51
51
  super("Cluster client doesn't know which node the #{command} command should be sent to.")
52
52
  end
53
53
  end
54
+
55
+ class NodeMightBeDown < ::RedisClient::Error
56
+ def initialize(_ = '')
57
+ super(
58
+ 'The client is trying to fetch the latest cluster state '\
59
+ 'because a subset of nodes might be down. '\
60
+ 'It might continue to raise errors for a while.'
61
+ )
62
+ end
63
+ end
54
64
  end
55
65
  end
@@ -43,6 +43,8 @@ class RedisClient
43
43
  module_function
44
44
 
45
45
  def convert(key)
46
+ return nil if key.nil?
47
+
46
48
  crc = 0
47
49
  key.each_byte do |b|
48
50
  crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
@@ -12,6 +12,7 @@ class RedisClient
12
12
  SLOT_SIZE = 16_384
13
13
  MIN_SLOT = 0
14
14
  MAX_SLOT = SLOT_SIZE - 1
15
+ MAX_STARTUP_SAMPLE = 37
15
16
  IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
16
17
 
17
18
  ReloadNeeded = Class.new(::RedisClient::Error)
@@ -32,19 +33,33 @@ class RedisClient
32
33
  end
33
34
 
34
35
  class << self
35
- def load_info(options, **kwargs)
36
- startup_nodes = ::RedisClient::Cluster::Node.new(options, **kwargs)
37
-
38
- errors = startup_nodes.map do |n|
39
- reply = n.call('CLUSTER', 'NODES')
40
- return parse_node_info(reply)
41
- rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
42
- e
36
+ def load_info(options, **kwargs) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
37
+ startup_size = options.size > MAX_STARTUP_SAMPLE ? MAX_STARTUP_SAMPLE : options.size
38
+ node_info_list = Array.new(startup_size)
39
+ errors = Array.new(startup_size)
40
+ startup_options = options.to_a.sample(MAX_STARTUP_SAMPLE).to_h
41
+ startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, **kwargs)
42
+ threads = startup_nodes.each_with_index.map do |raw_client, idx|
43
+ Thread.new(raw_client, idx) do |cli, i|
44
+ Thread.pass
45
+ reply = cli.call('CLUSTER', 'NODES')
46
+ node_info_list[i] = parse_node_info(reply)
47
+ rescue StandardError => e
48
+ errors[i] = e
49
+ ensure
50
+ cli&.close
51
+ end
52
+ end
53
+ threads.each(&:join)
54
+ raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.all?(&:nil?)
55
+
56
+ grouped = node_info_list.compact.group_by do |rows|
57
+ rows.sort_by { |row| row[:id] }
58
+ .map { |r| "#{r[:id]}#{r[:node_key]}#{r[:role]}#{r[:primary_id]}#{r[:config_epoch]}" }
59
+ .join
43
60
  end
44
61
 
45
- raise ::RedisClient::Cluster::InitialSetupError, errors
46
- ensure
47
- startup_nodes&.each(&:close)
62
+ grouped.max_by { |_, v| v.size }[1].first
48
63
  end
49
64
 
50
65
  private
@@ -109,32 +124,58 @@ class RedisClient
109
124
  end
110
125
 
111
126
  def find_by(node_key)
127
+ raise ReloadNeeded if node_key.nil? || !@clients.key?(node_key)
128
+
112
129
  @clients.fetch(node_key)
113
- rescue KeyError
114
- raise ReloadNeeded
115
130
  end
116
131
 
117
132
  def call_all(method, *command, **kwargs, &block)
118
- try_map { |_, client| client.send(method, *command, **kwargs, &block) }.values
133
+ results, errors = try_map do |_, client|
134
+ client.send(method, *command, **kwargs, &block)
135
+ end
136
+
137
+ return results.values if errors.empty?
138
+
139
+ raise ::RedisClient::Cluster::ErrorCollection, errors
119
140
  end
120
141
 
121
142
  def call_primaries(method, *command, **kwargs, &block)
122
- try_map do |node_key, client|
143
+ results, errors = try_map do |node_key, client|
123
144
  next if replica?(node_key)
124
145
 
125
146
  client.send(method, *command, **kwargs, &block)
126
- end.values
147
+ end
148
+
149
+ return results.values if errors.empty?
150
+
151
+ raise ::RedisClient::Cluster::ErrorCollection, errors
127
152
  end
128
153
 
129
154
  def call_replicas(method, *command, **kwargs, &block)
130
155
  return call_primaries(method, *command, **kwargs, &block) if replica_disabled?
131
156
 
132
157
  replica_node_keys = @replications.values.map(&:sample)
133
- try_map do |node_key, client|
158
+ results, errors = try_map do |node_key, client|
134
159
  next if primary?(node_key) || !replica_node_keys.include?(node_key)
135
160
 
136
161
  client.send(method, *command, **kwargs, &block)
137
- end.values
162
+ end
163
+
164
+ return results.values if errors.empty?
165
+
166
+ raise ::RedisClient::Cluster::ErrorCollection, errors
167
+ end
168
+
169
+ def send_ping(method, *command, **kwargs, &block)
170
+ results, errors = try_map do |_, client|
171
+ client.send(method, *command, **kwargs, &block)
172
+ end
173
+
174
+ return results.values if errors.empty?
175
+
176
+ raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
177
+
178
+ raise ::RedisClient::Cluster::ErrorCollection, errors
138
179
  end
139
180
 
140
181
  def scale_reading_clients
@@ -144,14 +185,9 @@ class RedisClient
144
185
  end
145
186
  end
146
187
 
147
- def slot_exists?(slot)
148
- slot = Integer(slot)
149
- return false if slot < MIN_SLOT || slot > MAX_SLOT
150
-
151
- !@slots[slot].nil?
152
- end
153
-
154
188
  def find_node_key_of_primary(slot)
189
+ return if slot.nil?
190
+
155
191
  slot = Integer(slot)
156
192
  return if slot < MIN_SLOT || slot > MAX_SLOT
157
193
 
@@ -159,6 +195,8 @@ class RedisClient
159
195
  end
160
196
 
161
197
  def find_node_key_of_replica(slot)
198
+ return if slot.nil?
199
+
162
200
  slot = Integer(slot)
163
201
  return if slot < MIN_SLOT || slot > MAX_SLOT
164
202
 
@@ -241,9 +279,7 @@ class RedisClient
241
279
  end
242
280
 
243
281
  threads.each(&:join)
244
- return results if errors.empty?
245
-
246
- raise ::RedisClient::Cluster::ErrorCollection, errors
282
+ [results, errors]
247
283
  end
248
284
  end
249
285
  end
@@ -170,8 +170,10 @@ class RedisClient
170
170
  end
171
171
 
172
172
  def close
173
- @node.each(&:close)
173
+ @node.call_all(:close)
174
174
  nil
175
+ rescue StandardError
176
+ # ignore
175
177
  end
176
178
 
177
179
  private
@@ -187,10 +189,11 @@ class RedisClient
187
189
  def send_command(method, *command, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
188
190
  cmd = command.first.to_s.downcase
189
191
  case cmd
190
- when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save', 'ping'
192
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
191
193
  @node.call_all(method, *command, **kwargs, &block).first
192
194
  when 'flushall', 'flushdb'
193
195
  @node.call_primaries(method, *command, **kwargs, &block).first
196
+ when 'ping' then @node.send_ping(method, *command, **kwargs, &block).first
194
197
  when 'wait' then send_wait_command(method, *command, **kwargs, &block)
195
198
  when 'keys' then @node.call_replicas(method, *command, **kwargs, &block).flatten.sort
196
199
  when 'dbsize' then @node.call_replicas(method, *command, **kwargs, &block).sum
@@ -211,15 +214,18 @@ class RedisClient
211
214
  node = assign_node(*command)
212
215
  try_send(node, method, *command, **kwargs, &block)
213
216
  end
214
- rescue RedisClient::Cluster::ErrorCollection => e
215
- update_cluster_info! if e.errors.values.map(&:class).any?(::RedisClient::ConnectionError)
216
- raise
217
+ rescue RedisClient::Cluster::Node::ReloadNeeded
218
+ update_cluster_info!
219
+ raise ::RedisClient::Cluster::NodeMightBeDown
217
220
  end
218
221
 
219
222
  def send_wait_command(method, *command, retry_count: 3, **kwargs, &block)
220
223
  @node.call_primaries(method, *command, **kwargs, &block).sum
221
224
  rescue RedisClient::Cluster::ErrorCollection => e
222
- raise if retry_count <= 0 || e.errors.values.map(&:message).grep(/ERR WAIT cannot be used with replica instances/).empty?
225
+ raise if retry_count <= 0
226
+ raise if e.errors.values.none? do |err|
227
+ err.message.include?('WAIT cannot be used with replica instances')
228
+ end
223
229
 
224
230
  update_cluster_info!
225
231
  retry_count -= 1
@@ -353,44 +359,30 @@ class RedisClient
353
359
  find_node(node_key)
354
360
  end
355
361
 
356
- def find_node_key(*command, primary_only: false) # rubocop:disable Metrics/MethodLength
362
+ def find_node_key(*command, primary_only: false)
357
363
  key = @command.extract_first_key(command)
358
- if key.empty?
359
- return @node.primary_node_keys.sample if @command.should_send_to_primary?(command) || primary_only
360
-
361
- return @node.replica_node_keys.sample
362
- end
363
-
364
- slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
365
- return unless @node.slot_exists?(slot)
364
+ slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
366
365
 
367
366
  if @command.should_send_to_primary?(command) || primary_only
368
- @node.find_node_key_of_primary(slot)
367
+ @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
369
368
  else
370
- @node.find_node_key_of_replica(slot)
369
+ @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
371
370
  end
372
371
  end
373
372
 
374
373
  def find_node(node_key, retry_count: 3)
375
- return @node.sample if node_key.nil?
376
-
377
374
  @node.find_by(node_key)
378
375
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
379
- raise(::RedisClient::ConnectionError, 'unstable cluster state') if retry_count <= 0
376
+ raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
380
377
 
381
- update_cluster_info!(node_key)
378
+ update_cluster_info!
382
379
  retry_count -= 1
383
380
  retry
384
381
  end
385
382
 
386
- def update_cluster_info!(node_key = nil)
383
+ def update_cluster_info!
387
384
  @mutex.synchronize do
388
- unless node_key.nil?
389
- host, port = ::RedisClient::Cluster::NodeKey.split(node_key)
390
- @config.add_node(host, port)
391
- end
392
-
393
- @node.each(&:close)
385
+ close
394
386
  @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
395
387
  end
396
388
  end
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.0.7
4
+ version: 0.0.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-06-19 00:00:00.000000000 Z
11
+ date: 2022-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client