redis-cluster-client 0.14.1 → 0.16.0

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: '0941b5506846f311cfce98389972b5fccca807384309821ea9df7b61148cc818'
4
- data.tar.gz: ba7654bdd8d03059a7953ed1772e8281dc3dd9a06c4e0e9969dc127d0df21935
3
+ metadata.gz: 5fbdcc076f2a4980b7511a7e49a9b9d7ecc44379bb90c7a3ef38e2452da69d78
4
+ data.tar.gz: b4e5c3afa0d587aef4fc52af7f5365c44a57c9cb7d80fe31bc237b3f04d78992
5
5
  SHA512:
6
- metadata.gz: 27fc5ca5d00b957b8494ea1e9bd72ba32287f0e1df4c4ab64106dfd7a183b95181156a6e6f4685a1b47ec571b7650614bda4d0331a4ed45b13f5b5c0fa894ae0
7
- data.tar.gz: 61563e73385b147b1f0b3d5be4ac99287ddb476912bd7e5b7689df0ae7e875c4372f69a2bcf8964e90a9ce9e867f0b9a160decce2089318f78ab200334ff6bc3
6
+ metadata.gz: 17b0a9668dfbccd0f407e86085937ca4604ff380d719fac340d2b4f9b30ea39322a701167482fdc696b22dd8f63f0836c9d94d35b1822049f5cb2f8f24ca609f
7
+ data.tar.gz: 9a062aac27d5b8e5d7c907e034706b7671c4262bb692d3a9a00787ecc9bec4b12744429f3ea73e5deaedcf69c412df3591a5f6423233d5adff35cc300ac606d8
@@ -70,7 +70,7 @@ class RedisClient
70
70
  e = key.index(RIGHT_BRACKET, s + 1)
71
71
  return EMPTY_STRING if e.nil?
72
72
 
73
- key[s + 1..e - 1]
73
+ s + 1 < e ? key[s + 1, e - s - 1] : EMPTY_STRING
74
74
  end
75
75
 
76
76
  def hash_tag_included?(key)
@@ -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
- STATE_REFRESH_INTERVAL = (3..10).freeze
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 << config_epoch
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
- @last_reloaded_at = nil
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 if @mutex.locked?
197
+ return unless @mutex.try_lock
197
198
 
198
- @mutex.synchronize do
199
- @slots[slot] = node_key
200
- rescue RangeError
201
- @slots = Array.new(SLOT_SIZE) { |i| @slots[i] }
202
- @slots[slot] = node_key
203
- end
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 reload!
207
+ def try_reload!
207
208
  with_reload_lock do
208
- with_startup_clients(@config.max_startup_sample) do |startup_clients|
209
- @node_info = refetch_node_info_list(startup_clients)
210
- @node_configs = @node_info.to_h do |node_info|
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
- reply = client.call_once('cluster', 'nodes')
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,11 +396,11 @@ 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' ? nil : primary_id,
399
+ primary_id: role == 'master' ? EMPTY_STRING : primary_id,
394
400
  slots: role == 'master' ? slots : EMPTY_ARRAY
395
401
  )
396
402
  end
397
- end.freeze
403
+ end
398
404
  end
399
405
 
400
406
  def parse_cluster_shards_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -411,18 +417,18 @@ class RedisClient
411
417
  id: node.fetch('id'),
412
418
  node_key: NodeKey.build_from_host_port(ip, node['port'] || node['tls-port']),
413
419
  role: role == 'master' ? role : 'slave',
414
- primary_id: role == 'master' ? nil : primary_id,
420
+ primary_id: role == 'master' ? EMPTY_STRING : primary_id,
415
421
  slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
416
422
  )
417
423
  end
418
- end.freeze
424
+ end
419
425
  end
420
426
 
421
427
  # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
422
428
  # node_key should use hostname if present in CLUSTER NODES output.
423
429
  #
424
430
  # See https://redis.io/commands/cluster-nodes/ for details on the output format.
425
- # node_address matches fhe format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
431
+ # node_address matches the format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
426
432
  def parse_node_key(node_address)
427
433
  ip_chunk, hostname, _auxiliaries = node_address.split(',')
428
434
  ip_port_string = ip_chunk.split('@').first
@@ -432,6 +438,16 @@ class RedisClient
432
438
  "#{hostname}:#{port}"
433
439
  end
434
440
 
441
+ def reload!(clients)
442
+ @node_info = refetch_node_info_list(clients)
443
+ @node_configs = @node_info.to_h do |node_info|
444
+ [node_info.node_key, @config.client_config_for_node(node_info.node_key)]
445
+ end
446
+ @slots = build_slot_node_mappings(@node_info)
447
+ @replications = build_replication_mappings(@node_info)
448
+ @topology.process_topology_update!(@replications, @node_configs)
449
+ end
450
+
435
451
  def with_startup_clients(count) # rubocop:disable Metrics/AbcSize
436
452
  if @config.connect_with_original_config
437
453
  # If connect_with_original_config is set, that means we need to build actual client objects
@@ -457,35 +473,41 @@ class RedisClient
457
473
  end
458
474
  end
459
475
 
476
+ def with_reload_jitter
477
+ return unless @next_reload_time.nil? || obtain_current_time >= @next_reload_time
478
+
479
+ yield
480
+
481
+ @next_reload_time = obtain_current_time + @random.rand(JITTER_WINDOW)
482
+ end
483
+
460
484
  def with_reload_lock
461
- # What should happen with concurrent calls #reload? This is a realistic possibility if the cluster goes into
485
+ # What should happen with concurrent calls #try_reload! This is a realistic possibility if the cluster goes into
462
486
  # a CLUSTERDOWN state, and we're using a pooled backend. Every thread will independently discover this, and
463
- # call reload!.
464
- # For now, if a reload is in progress, wait for that to complete, and consider that the same as us having
465
- # performed the reload.
466
- # Probably in the future we should add a circuit breaker to #reload itself, and stop trying if the cluster is
487
+ # call #try_reload!.
488
+ # For now, if a reload is in progress by a thread, the other threads do not wait for that to complete, and
489
+ # they throw an error.
490
+ # Probably in the future we should add a circuit breaker to #try_reload! itself, and stop trying if the cluster is
467
491
  # obviously not working.
468
- wait_start = obtain_current_time
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
492
+ return unless @mutex.try_lock
478
493
 
479
- r = yield
480
- @last_reloaded_at = obtain_current_time
481
- @reload_times += 1
482
- r
483
- end
494
+ yield
495
+ ensure
496
+ @mutex.unlock if @mutex.owned?
484
497
  end
485
498
 
486
499
  def obtain_current_time
487
500
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
488
501
  end
502
+
503
+ def bypass_reload!
504
+ # DO NOT USE THIS METHOD
505
+ with_reload_lock do
506
+ with_startup_clients(@config.max_startup_sample) do |clients|
507
+ reload!(clients)
508
+ end
509
+ end
510
+ end
489
511
  end
490
512
  end
491
513
  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.reload!
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
 
@@ -294,7 +292,7 @@ class RedisClient
294
292
  end
295
293
 
296
294
  def renew_cluster_state
297
- @node.reload!
295
+ @node.try_reload!
298
296
  rescue ::RedisClient::Cluster::InitialSetupError
299
297
  # ignore
300
298
  end
@@ -340,8 +338,8 @@ class RedisClient
340
338
  raise if retry_count <= 0
341
339
  raise if e.errors.values.none? { |err| err.message.include?('WAIT cannot be used with replica instances') }
342
340
 
343
- retry_count -= 1
344
341
  renew_cluster_state
342
+ retry_count -= 1
345
343
  retry
346
344
  end
347
345
 
@@ -452,7 +450,7 @@ class RedisClient
452
450
  end
453
451
 
454
452
  def send_multiple_keys_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
455
- # This implementation is prioritized performance rather than readability or so.
453
+ # This implementation prioritizes performance over readability.
456
454
  cmd = command.first
457
455
  if cmd.casecmp('mget').zero?
458
456
  single_key_cmd = 'get'
@@ -502,8 +500,8 @@ class RedisClient
502
500
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
503
501
  raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config) if retry_count <= 0
504
502
 
505
- retry_count -= 1
506
503
  renew_cluster_state
504
+ retry_count -= 1
507
505
  retry
508
506
  end
509
507
  end
@@ -130,8 +130,26 @@ class RedisClient
130
130
  ::RedisClient::Cluster::PubSub.new(router, @command_builder)
131
131
  end
132
132
 
133
- def with(...)
134
- raise NotImplementedError, 'No way to use'
133
+ # Compatibility layer for RedisClient::Pooled
134
+ def with(_options = nil)
135
+ yield self
136
+ end
137
+ alias then with
138
+
139
+ # Compatibility layer for RedisClient::HashRing
140
+ def node_for(_key)
141
+ self
142
+ end
143
+
144
+ # Compatibility layer for RedisClient::HashRing
145
+ def nodes_for(*keys)
146
+ keys.flatten!
147
+ { self => keys }
148
+ end
149
+
150
+ # Compatibility layer for RedisClient::HashRing
151
+ def nodes
152
+ [self].freeze
135
153
  end
136
154
 
137
155
  def close
@@ -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
- # It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
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
- # It affects to strike a balance between load and stability in initialization or changed states.
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,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-cluster-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.1
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '0.24'
18
+ version: '0.28'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '0.24'
25
+ version: '0.28'
26
26
  email:
27
27
  - proxy0721@gmail.com
28
28
  executables: []
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  requirements: []
77
- rubygems_version: 4.0.3
77
+ rubygems_version: 4.0.6
78
78
  specification_version: 4
79
79
  summary: Redis cluster-aware client for Ruby
80
80
  test_files: []