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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5fbdcc076f2a4980b7511a7e49a9b9d7ecc44379bb90c7a3ef38e2452da69d78
|
|
4
|
+
data.tar.gz: b4e5c3afa0d587aef4fc52af7f5365c44a57c9cb7d80fe31bc237b3f04d78992
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17b0a9668dfbccd0f407e86085937ca4604ff380d719fac340d2b4f9b30ea39322a701167482fdc696b22dd8f63f0836c9d94d35b1822049f5cb2f8f24ca609f
|
|
7
|
+
data.tar.gz: 9a062aac27d5b8e5d7c907e034706b7671c4262bb692d3a9a00787ecc9bec4b12744429f3ea73e5deaedcf69c412df3591a5f6423233d5adff35cc300ac606d8
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
@
|
|
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
|
|
197
|
+
return unless @mutex.try_lock
|
|
197
198
|
|
|
198
|
-
@
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
207
|
+
def try_reload!
|
|
207
208
|
with_reload_lock do
|
|
208
|
-
|
|
209
|
-
@
|
|
210
|
-
|
|
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
|
-
|
|
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' ?
|
|
399
|
+
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
394
400
|
slots: role == 'master' ? slots : EMPTY_ARRAY
|
|
395
401
|
)
|
|
396
402
|
end
|
|
397
|
-
end
|
|
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' ?
|
|
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
|
|
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
|
|
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 #
|
|
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
|
|
464
|
-
# For now, if a reload is in progress, wait for that to complete, and
|
|
465
|
-
#
|
|
466
|
-
# Probably in the future we should add a circuit breaker to #
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
data/lib/redis_client/cluster.rb
CHANGED
|
@@ -130,8 +130,26 @@ class RedisClient
|
|
|
130
130
|
::RedisClient::Cluster::PubSub.new(router, @command_builder)
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
77
|
+
rubygems_version: 4.0.6
|
|
78
78
|
specification_version: 4
|
|
79
79
|
summary: Redis cluster-aware client for Ruby
|
|
80
80
|
test_files: []
|