redis-cluster-client 0.12.1 → 0.13.1
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 +45 -24
- data/lib/redis_client/cluster/concurrent_worker/on_demand.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker/pooled.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker.rb +4 -6
- data/lib/redis_client/cluster/errors.rb +7 -2
- data/lib/redis_client/cluster/node/latency_replica.rb +1 -1
- data/lib/redis_client/cluster/node.rb +50 -5
- data/lib/redis_client/cluster/optimistic_locking.rb +3 -3
- data/lib/redis_client/cluster/pipeline.rb +3 -3
- data/lib/redis_client/cluster/pub_sub.rb +15 -7
- data/lib/redis_client/cluster/router.rb +177 -79
- data/lib/redis_client/cluster/transaction.rb +5 -5
- data/lib/redis_client/cluster.rb +7 -6
- data/lib/redis_client/cluster_config.rb +10 -7
- metadata +2 -3
- data/lib/redis_client/cluster/normalized_cmd_name.rb +0 -71
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2bc3348020af7012ab193ef18128f2cd37028c9225f1e7fa9555d3609ff92362
|
4
|
+
data.tar.gz: 3c08890f1c50f297f0d60f0503bd268742bbcd242a32ac73df733496a013660e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a4036ef8be3c29c747abc7deb59698fbf5d5e581ab9913fd3e06d530a9bae7eba13a397929bb9e3eaf611d5ee01960378ffef54bc6f69706aaf672bc9f38f89
|
7
|
+
data.tar.gz: '08897fd0c13bb175399b9118783e812f20555e0e09d021cf24fab236168d8ffd5a3ad4599f2ba1db56ccebaf501f9319c605940069c963f54b2268e1acad5dae'
|
@@ -3,7 +3,6 @@
|
|
3
3
|
require 'redis_client'
|
4
4
|
require 'redis_client/cluster/errors'
|
5
5
|
require 'redis_client/cluster/key_slot_converter'
|
6
|
-
require 'redis_client/cluster/normalized_cmd_name'
|
7
6
|
|
8
7
|
class RedisClient
|
9
8
|
class Cluster
|
@@ -30,7 +29,7 @@ class RedisClient
|
|
30
29
|
nodes&.each do |node|
|
31
30
|
regular_timeout = node.read_timeout
|
32
31
|
node.read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
|
33
|
-
reply = node.call('
|
32
|
+
reply = node.call('command')
|
34
33
|
node.read_timeout = regular_timeout
|
35
34
|
commands = parse_command_reply(reply)
|
36
35
|
cmd = ::RedisClient::Cluster::Command.new(commands)
|
@@ -47,14 +46,28 @@ class RedisClient
|
|
47
46
|
|
48
47
|
private
|
49
48
|
|
50
|
-
def parse_command_reply(rows)
|
49
|
+
def parse_command_reply(rows) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
51
50
|
rows&.each_with_object({}) do |row, acc|
|
52
|
-
next if row
|
51
|
+
next if row.first.nil?
|
53
52
|
|
54
|
-
|
55
|
-
|
53
|
+
# TODO: in redis 7.0 or later, subcommand information included in the command reply
|
54
|
+
|
55
|
+
pos = case row.first
|
56
|
+
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
|
57
|
+
when 'object', 'xgroup' then 2
|
58
|
+
when 'migrate', 'xread', 'xreadgroup' then 0
|
59
|
+
else row[3]
|
60
|
+
end
|
61
|
+
|
62
|
+
writable = case row.first
|
63
|
+
when 'xgroup' then true
|
64
|
+
else row[2].include?('write')
|
65
|
+
end
|
66
|
+
|
67
|
+
acc[row.first] = ::RedisClient::Cluster::Command::Detail.new(
|
68
|
+
first_key_position: pos,
|
56
69
|
key_step: row[5],
|
57
|
-
write?:
|
70
|
+
write?: writable,
|
58
71
|
readonly?: row[2].include?('readonly')
|
59
72
|
)
|
60
73
|
end.freeze || EMPTY_HASH
|
@@ -73,39 +86,47 @@ class RedisClient
|
|
73
86
|
end
|
74
87
|
|
75
88
|
def should_send_to_primary?(command)
|
76
|
-
|
77
|
-
@commands[name]&.write?
|
89
|
+
find_command_info(command.first)&.write?
|
78
90
|
end
|
79
91
|
|
80
92
|
def should_send_to_replica?(command)
|
81
|
-
|
82
|
-
@commands[name]&.readonly?
|
93
|
+
find_command_info(command.first)&.readonly?
|
83
94
|
end
|
84
95
|
|
85
96
|
def exists?(name)
|
86
|
-
@commands.key?(
|
97
|
+
@commands.key?(name) || @commands.key?(name.to_s.downcase(:ascii))
|
87
98
|
end
|
88
99
|
|
89
100
|
private
|
90
101
|
|
91
|
-
def
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
102
|
+
def find_command_info(name)
|
103
|
+
@commands[name] || @commands[name.to_s.downcase(:ascii)]
|
104
|
+
end
|
105
|
+
|
106
|
+
def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/PerceivedComplexity
|
107
|
+
i = find_command_info(command.first)&.first_key_position.to_i
|
108
|
+
return i if i > 0
|
109
|
+
|
110
|
+
cmd_name = command.first
|
111
|
+
if cmd_name.casecmp('xread').zero?
|
112
|
+
determine_optional_key_position(command, 'streams')
|
113
|
+
elsif cmd_name.casecmp('xreadgroup').zero?
|
100
114
|
determine_optional_key_position(command, 'streams')
|
115
|
+
elsif cmd_name.casecmp('migrate').zero?
|
116
|
+
command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
|
117
|
+
elsif cmd_name.casecmp('memory').zero?
|
118
|
+
command[1].to_s.casecmp('usage').zero? ? 2 : 0
|
101
119
|
else
|
102
|
-
|
120
|
+
i
|
103
121
|
end
|
104
122
|
end
|
105
123
|
|
106
124
|
def determine_optional_key_position(command, option_name)
|
107
|
-
|
108
|
-
|
125
|
+
command.each_with_index do |e, i|
|
126
|
+
return i + 1 if e.to_s.downcase(:ascii) == option_name
|
127
|
+
end
|
128
|
+
|
129
|
+
0
|
109
130
|
end
|
110
131
|
end
|
111
132
|
end
|
@@ -71,14 +71,12 @@ class RedisClient
|
|
71
71
|
|
72
72
|
module_function
|
73
73
|
|
74
|
-
def create(model: :
|
75
|
-
size = size.positive? ? size : 5
|
76
|
-
|
74
|
+
def create(model: :none, size: 5)
|
77
75
|
case model
|
78
|
-
when :on_demand, nil then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
|
79
|
-
when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
|
80
76
|
when :none then ::RedisClient::Cluster::ConcurrentWorker::None.new
|
81
|
-
|
77
|
+
when :on_demand then ::RedisClient::Cluster::ConcurrentWorker::OnDemand.new(size: size)
|
78
|
+
when :pooled then ::RedisClient::Cluster::ConcurrentWorker::Pooled.new(size: size)
|
79
|
+
else raise ArgumentError, "unknown model: #{model}"
|
82
80
|
end
|
83
81
|
end
|
84
82
|
end
|
@@ -43,11 +43,16 @@ class RedisClient
|
|
43
43
|
if !errors.is_a?(Hash) || errors.empty?
|
44
44
|
new(errors.to_s).with_errors(EMPTY_HASH)
|
45
45
|
else
|
46
|
-
messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }
|
46
|
+
messages = errors.map { |node_key, error| "#{node_key}: (#{error.class}) #{error.message}" }.freeze
|
47
47
|
new(messages.join(', ')).with_errors(errors)
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
+
def initialize(error_message = nil)
|
52
|
+
@errors = nil
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
51
56
|
def with_errors(errors)
|
52
57
|
@errors = errors if @errors.nil?
|
53
58
|
self
|
@@ -61,7 +66,7 @@ class RedisClient
|
|
61
66
|
end
|
62
67
|
|
63
68
|
class NodeMightBeDown < Error
|
64
|
-
def initialize(
|
69
|
+
def initialize(_error_message = nil)
|
65
70
|
super(
|
66
71
|
'The client is trying to fetch the latest cluster state ' \
|
67
72
|
'because a subset of nodes might be down. ' \
|
@@ -7,6 +7,7 @@ require 'redis_client/cluster/node/primary_only'
|
|
7
7
|
require 'redis_client/cluster/node/random_replica'
|
8
8
|
require 'redis_client/cluster/node/random_replica_or_primary'
|
9
9
|
require 'redis_client/cluster/node/latency_replica'
|
10
|
+
require 'redis_client/cluster/node_key'
|
10
11
|
|
11
12
|
class RedisClient
|
12
13
|
class Cluster
|
@@ -43,6 +44,10 @@ class RedisClient
|
|
43
44
|
def replica?
|
44
45
|
role == 'slave'
|
45
46
|
end
|
47
|
+
|
48
|
+
def serialize(str)
|
49
|
+
str << id << node_key << role << primary_id << config_epoch
|
50
|
+
end
|
46
51
|
end
|
47
52
|
|
48
53
|
class CharArray
|
@@ -90,7 +95,7 @@ class RedisClient
|
|
90
95
|
|
91
96
|
def build_connection_prelude
|
92
97
|
prelude = super.dup
|
93
|
-
prelude << ['
|
98
|
+
prelude << ['readonly'] if @scale_read
|
94
99
|
prelude.freeze
|
95
100
|
end
|
96
101
|
end
|
@@ -309,7 +314,7 @@ class RedisClient
|
|
309
314
|
work_group.push(i, raw_client) do |client|
|
310
315
|
regular_timeout = client.read_timeout
|
311
316
|
client.read_timeout = @config.slow_command_timeout > 0.0 ? @config.slow_command_timeout : regular_timeout
|
312
|
-
reply = client.call_once('
|
317
|
+
reply = client.call_once('cluster', 'nodes')
|
313
318
|
client.read_timeout = regular_timeout
|
314
319
|
parse_cluster_node_reply(reply)
|
315
320
|
rescue StandardError => e
|
@@ -338,9 +343,7 @@ class RedisClient
|
|
338
343
|
|
339
344
|
grouped = node_info_list.compact.group_by do |info_list|
|
340
345
|
info_list.sort_by!(&:id)
|
341
|
-
info_list.each_with_object(String.new(capacity: 128 * info_list.size))
|
342
|
-
a << e.id << e.node_key << e.role << e.primary_id << e.config_epoch
|
343
|
-
end
|
346
|
+
info_list.each_with_object(String.new(capacity: 128 * info_list.size)) { |e, a| e.serialize(a) }
|
344
347
|
end
|
345
348
|
|
346
349
|
grouped.max_by { |_, v| v.size }[1].first
|
@@ -375,6 +378,48 @@ class RedisClient
|
|
375
378
|
end
|
376
379
|
end
|
377
380
|
|
381
|
+
def parse_cluster_slots_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
382
|
+
reply.group_by { |e| e[2][2] }.each_with_object([]) do |(primary_id, group), acc|
|
383
|
+
slots = group.map { |e| e[0, 2] }.freeze
|
384
|
+
|
385
|
+
group.first[2..].each do |arr|
|
386
|
+
ip = arr[0]
|
387
|
+
next if ip.nil? || ip.empty? || ip == '?'
|
388
|
+
|
389
|
+
id = arr[2]
|
390
|
+
role = id == primary_id ? 'master' : 'slave'
|
391
|
+
acc << ::RedisClient::Cluster::Node::Info.new(
|
392
|
+
id: id,
|
393
|
+
node_key: NodeKey.build_from_host_port(ip, arr[1]),
|
394
|
+
role: role,
|
395
|
+
primary_id: role == 'master' ? nil : primary_id,
|
396
|
+
slots: role == 'master' ? slots : EMPTY_ARRAY
|
397
|
+
)
|
398
|
+
end
|
399
|
+
end.freeze
|
400
|
+
end
|
401
|
+
|
402
|
+
def parse_cluster_shards_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
403
|
+
reply.each_with_object([]) do |shard, acc|
|
404
|
+
nodes = shard.fetch('nodes')
|
405
|
+
primary_id = nodes.find { |n| n.fetch('role') == 'master' }.fetch('id')
|
406
|
+
|
407
|
+
nodes.each do |node|
|
408
|
+
ip = node.fetch('ip')
|
409
|
+
next if node.fetch('health') != 'online' || ip.nil? || ip.empty? || ip == '?'
|
410
|
+
|
411
|
+
role = node.fetch('role')
|
412
|
+
acc << ::RedisClient::Cluster::Node::Info.new(
|
413
|
+
id: node.fetch('id'),
|
414
|
+
node_key: NodeKey.build_from_host_port(ip, node['port'] || node['tls-port']),
|
415
|
+
role: role == 'master' ? role : 'slave',
|
416
|
+
primary_id: role == 'master' ? nil : primary_id,
|
417
|
+
slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
|
418
|
+
)
|
419
|
+
end
|
420
|
+
end.freeze
|
421
|
+
end
|
422
|
+
|
378
423
|
# As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
|
379
424
|
# node_key should use hostname if present in CLUSTER NODES output.
|
380
425
|
#
|
@@ -18,15 +18,15 @@ class RedisClient
|
|
18
18
|
handle_redirection(slot, retry_count: 1) do |nd|
|
19
19
|
nd.with do |c|
|
20
20
|
c.ensure_connected_cluster_scoped(retryable: false) do
|
21
|
-
c.call('
|
22
|
-
c.call('
|
21
|
+
c.call('asking') if @asking
|
22
|
+
c.call('watch', *keys)
|
23
23
|
begin
|
24
24
|
yield(c, slot, @asking)
|
25
25
|
rescue ::RedisClient::ConnectionError
|
26
26
|
# No need to unwatch on a connection error.
|
27
27
|
raise
|
28
28
|
rescue StandardError
|
29
|
-
c.call('
|
29
|
+
c.call('unwatch')
|
30
30
|
raise
|
31
31
|
end
|
32
32
|
rescue ::RedisClient::CommandError => e
|
@@ -283,14 +283,14 @@ class RedisClient
|
|
283
283
|
args = timeout.nil? ? [] : [timeout]
|
284
284
|
|
285
285
|
if block.nil?
|
286
|
-
@router.
|
286
|
+
@router.send_command_to_node(node, method, command, args)
|
287
287
|
else
|
288
|
-
@router.
|
288
|
+
@router.send_command_to_node(node, method, command, args, &block)
|
289
289
|
end
|
290
290
|
end
|
291
291
|
|
292
292
|
def try_asking(node)
|
293
|
-
node.call('
|
293
|
+
node.call('asking') == 'OK'
|
294
294
|
rescue StandardError
|
295
295
|
false
|
296
296
|
end
|
@@ -2,7 +2,6 @@
|
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
4
|
require 'redis_client/cluster/errors'
|
5
|
-
require 'redis_client/cluster/normalized_cmd_name'
|
6
5
|
|
7
6
|
class RedisClient
|
8
7
|
class Cluster
|
@@ -108,12 +107,21 @@ class RedisClient
|
|
108
107
|
|
109
108
|
private
|
110
109
|
|
111
|
-
def _call(command)
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
110
|
+
def _call(command) # rubocop:disable Metrics/AbcSize
|
111
|
+
if command.first.casecmp('subscribe').zero?
|
112
|
+
call_to_single_state(command)
|
113
|
+
elsif command.first.casecmp('psubscribe').zero?
|
114
|
+
call_to_single_state(command)
|
115
|
+
elsif command.first.casecmp('ssubscribe').zero?
|
116
|
+
call_to_single_state(command)
|
117
|
+
elsif command.first.casecmp('unsubscribe').zero?
|
118
|
+
call_to_all_states(command)
|
119
|
+
elsif command.first.casecmp('punsubscribe').zero?
|
120
|
+
call_to_all_states(command)
|
121
|
+
elsif command.first.casecmp('sunsubscribe').zero?
|
122
|
+
call_for_sharded_states(command)
|
123
|
+
else
|
124
|
+
call_to_single_state(command)
|
117
125
|
end
|
118
126
|
end
|
119
127
|
|
@@ -7,7 +7,6 @@ require 'redis_client/cluster/errors'
|
|
7
7
|
require 'redis_client/cluster/key_slot_converter'
|
8
8
|
require 'redis_client/cluster/node'
|
9
9
|
require 'redis_client/cluster/node_key'
|
10
|
-
require 'redis_client/cluster/normalized_cmd_name'
|
11
10
|
require 'redis_client/cluster/transaction'
|
12
11
|
require 'redis_client/cluster/optimistic_locking'
|
13
12
|
require 'redis_client/cluster/pipeline'
|
@@ -23,6 +22,13 @@ class RedisClient
|
|
23
22
|
|
24
23
|
attr_reader :config
|
25
24
|
|
25
|
+
Action = Struct.new(
|
26
|
+
'RedisCommandRoutingAction',
|
27
|
+
:method_name,
|
28
|
+
:reply_transformer,
|
29
|
+
keyword_init: true
|
30
|
+
)
|
31
|
+
|
26
32
|
def initialize(config, concurrent_worker, pool: nil, **kwargs)
|
27
33
|
@config = config
|
28
34
|
@concurrent_worker = concurrent_worker
|
@@ -32,42 +38,20 @@ class RedisClient
|
|
32
38
|
@node.reload!
|
33
39
|
@command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
|
34
40
|
@command_builder = @config.command_builder
|
41
|
+
@dedicated_actions = build_dedicated_actions
|
35
42
|
rescue ::RedisClient::Cluster::InitialSetupError => e
|
36
43
|
e.with_config(config)
|
37
44
|
raise
|
38
45
|
end
|
39
46
|
|
40
47
|
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
when 'lastsave' then @node.call_all(method, command, args).sort_by(&:to_i).then(&TSF.call(block))
|
49
|
-
when 'role' then @node.call_all(method, command, args, &block)
|
50
|
-
when 'config' then send_config_command(method, command, args, &block)
|
51
|
-
when 'client' then send_client_command(method, command, args, &block)
|
52
|
-
when 'cluster' then send_cluster_command(method, command, args, &block)
|
53
|
-
when 'memory' then send_memory_command(method, command, args, &block)
|
54
|
-
when 'script' then send_script_command(method, command, args, &block)
|
55
|
-
when 'pubsub' then send_pubsub_command(method, command, args, &block)
|
56
|
-
when 'watch' then send_watch_command(command, &block)
|
57
|
-
when 'mset', 'mget', 'del'
|
58
|
-
send_multiple_keys_command(cmd, method, command, args, &block)
|
59
|
-
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
60
|
-
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
61
|
-
when 'flushall', 'flushdb'
|
62
|
-
@node.call_primaries(method, command, args).first.then(&TSF.call(block))
|
63
|
-
when 'readonly', 'readwrite', 'shutdown'
|
64
|
-
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(cmd).with_config(@config)
|
65
|
-
when 'discard', 'exec', 'multi', 'unwatch'
|
66
|
-
raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(cmd).with_config(@config)
|
67
|
-
else
|
68
|
-
node = assign_node(command)
|
69
|
-
try_send(node, method, command, args, &block)
|
70
|
-
end
|
48
|
+
return assign_node_and_send_command(method, command, args, &block) unless @dedicated_actions.key?(command.first)
|
49
|
+
|
50
|
+
action = @dedicated_actions[command.first]
|
51
|
+
return send(action.method_name, method, command, args, &block) if action.reply_transformer.nil?
|
52
|
+
|
53
|
+
reply = send(action.method_name, method, command, args)
|
54
|
+
action.reply_transformer.call(reply).then(&TSF.call(block))
|
71
55
|
rescue ::RedisClient::CircuitBreaker::OpenCircuitError
|
72
56
|
raise
|
73
57
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
@@ -93,7 +77,12 @@ class RedisClient
|
|
93
77
|
end
|
94
78
|
|
95
79
|
# @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
|
96
|
-
def
|
80
|
+
def assign_node_and_send_command(method, command, args, retry_count: 3, &block)
|
81
|
+
node = assign_node(command)
|
82
|
+
send_command_to_node(node, method, command, args, retry_count: retry_count, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def send_command_to_node(node, method, command, args, retry_count: 3, &block)
|
97
86
|
handle_redirection(node, command, retry_count: retry_count) do |on_node|
|
98
87
|
if args.empty?
|
99
88
|
# prevent memory allocation for variable-length args
|
@@ -118,7 +107,7 @@ class RedisClient
|
|
118
107
|
elsif e.message.start_with?('ASK')
|
119
108
|
node = assign_asking_node(e.message)
|
120
109
|
if retry_count >= 0
|
121
|
-
node.call('
|
110
|
+
node.call('asking')
|
122
111
|
retry
|
123
112
|
end
|
124
113
|
elsif e.message.start_with?('CLUSTERDOWN')
|
@@ -268,6 +257,81 @@ class RedisClient
|
|
268
257
|
|
269
258
|
private
|
270
259
|
|
260
|
+
def build_dedicated_actions # rubocop:disable Metrics/AbcSize
|
261
|
+
pick_first = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
|
262
|
+
multiple_key_action = Action.new(method_name: :send_multiple_keys_command)
|
263
|
+
all_node_first_action = Action.new(method_name: :send_command_to_all_nodes, reply_transformer: pick_first)
|
264
|
+
primary_first_action = Action.new(method_name: :send_command_to_primaries, reply_transformer: pick_first)
|
265
|
+
not_supported_action = Action.new(method_name: :fail_not_supported_command)
|
266
|
+
keyless_action = Action.new(method_name: :fail_keyless_command)
|
267
|
+
actions = {
|
268
|
+
'ping' => Action.new(method_name: :send_ping_command, reply_transformer: pick_first),
|
269
|
+
'wait' => Action.new(method_name: :send_wait_command),
|
270
|
+
'keys' => Action.new(method_name: :send_command_to_replicas, reply_transformer: ->(reply) { reply.flatten.sort_by(&:to_s) }),
|
271
|
+
'dbsize' => Action.new(method_name: :send_command_to_replicas, reply_transformer: ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }),
|
272
|
+
'scan' => Action.new(method_name: :send_scan_command),
|
273
|
+
'lastsave' => Action.new(method_name: :send_command_to_all_nodes, reply_transformer: ->(reply) { reply.sort_by(&:to_i) }),
|
274
|
+
'role' => Action.new(method_name: :send_command_to_all_nodes),
|
275
|
+
'config' => Action.new(method_name: :send_config_command),
|
276
|
+
'client' => Action.new(method_name: :send_client_command),
|
277
|
+
'cluster' => Action.new(method_name: :send_cluster_command),
|
278
|
+
'memory' => Action.new(method_name: :send_memory_command),
|
279
|
+
'script' => Action.new(method_name: :send_script_command),
|
280
|
+
'pubsub' => Action.new(method_name: :send_pubsub_command),
|
281
|
+
'watch' => Action.new(method_name: :send_watch_command),
|
282
|
+
'mget' => multiple_key_action,
|
283
|
+
'mset' => multiple_key_action,
|
284
|
+
'del' => multiple_key_action,
|
285
|
+
'acl' => all_node_first_action,
|
286
|
+
'auth' => all_node_first_action,
|
287
|
+
'bgrewriteaof' => all_node_first_action,
|
288
|
+
'bgsave' => all_node_first_action,
|
289
|
+
'quit' => all_node_first_action,
|
290
|
+
'save' => all_node_first_action,
|
291
|
+
'flushall' => primary_first_action,
|
292
|
+
'flushdb' => primary_first_action,
|
293
|
+
'readonly' => not_supported_action,
|
294
|
+
'readwrite' => not_supported_action,
|
295
|
+
'shutdown' => not_supported_action,
|
296
|
+
'discard' => keyless_action,
|
297
|
+
'exec' => keyless_action,
|
298
|
+
'multi' => keyless_action,
|
299
|
+
'unwatch' => keyless_action
|
300
|
+
}.freeze
|
301
|
+
actions.each_with_object({}) do |(k, v), acc|
|
302
|
+
acc[k] = v
|
303
|
+
acc[k.upcase] = v
|
304
|
+
end.freeze
|
305
|
+
end
|
306
|
+
|
307
|
+
def send_command_to_all_nodes(method, command, args, &block)
|
308
|
+
@node.call_all(method, command, args, &block)
|
309
|
+
end
|
310
|
+
|
311
|
+
def send_command_to_primaries(method, command, args, &block)
|
312
|
+
@node.call_primaries(method, command, args, &block)
|
313
|
+
end
|
314
|
+
|
315
|
+
def send_command_to_replicas(method, command, args, &block)
|
316
|
+
@node.call_replicas(method, command, args, &block)
|
317
|
+
end
|
318
|
+
|
319
|
+
def send_ping_command(method, command, args, &block)
|
320
|
+
@node.send_ping(method, command, args, &block)
|
321
|
+
end
|
322
|
+
|
323
|
+
def send_scan_command(_method, command, _args, &_block)
|
324
|
+
scan(command, seed: 1)
|
325
|
+
end
|
326
|
+
|
327
|
+
def fail_not_supported_command(_method, command, _args, &_block)
|
328
|
+
raise ::RedisClient::Cluster::OrchestrationCommandNotSupported.from_command(command.first).with_config(@config)
|
329
|
+
end
|
330
|
+
|
331
|
+
def fail_keyless_command(_method, command, _args, &_block)
|
332
|
+
raise ::RedisClient::Cluster::AmbiguousNodeError.from_command(command.first).with_config(@config)
|
333
|
+
end
|
334
|
+
|
271
335
|
def send_wait_command(method, command, args, retry_count: 1, &block) # rubocop:disable Metrics/AbcSize
|
272
336
|
@node.call_primaries(method, command, args).select { |r| r.is_a?(Integer) }.sum.then(&TSF.call(block))
|
273
337
|
rescue ::RedisClient::Cluster::ErrorCollection => e
|
@@ -280,79 +344,110 @@ class RedisClient
|
|
280
344
|
retry
|
281
345
|
end
|
282
346
|
|
283
|
-
def send_config_command(method, command, args, &block)
|
284
|
-
|
285
|
-
when 'resetstat', 'rewrite', 'set'
|
347
|
+
def send_config_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
|
348
|
+
if command[1].casecmp('resetstat').zero?
|
286
349
|
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
287
|
-
|
350
|
+
elsif command[1].casecmp('rewrite').zero?
|
351
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
352
|
+
elsif command[1].casecmp('set').zero?
|
353
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
354
|
+
else
|
355
|
+
assign_node(command).public_send(method, *args, command, &block)
|
288
356
|
end
|
289
357
|
end
|
290
358
|
|
291
359
|
def send_memory_command(method, command, args, &block)
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
360
|
+
if command[1].casecmp('stats').zero?
|
361
|
+
@node.call_all(method, command, args, &block)
|
362
|
+
elsif command[1].casecmp('purge').zero?
|
363
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
364
|
+
else
|
365
|
+
assign_node(command).public_send(method, *args, command, &block)
|
296
366
|
end
|
297
367
|
end
|
298
368
|
|
299
|
-
def send_client_command(method, command, args, &block)
|
300
|
-
|
301
|
-
|
302
|
-
|
369
|
+
def send_client_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
|
370
|
+
if command[1].casecmp('list').zero?
|
371
|
+
@node.call_all(method, command, args, &block).flatten
|
372
|
+
elsif command[1].casecmp('pause').zero?
|
303
373
|
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
304
|
-
|
374
|
+
elsif command[1].casecmp('reply').zero?
|
375
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
376
|
+
elsif command[1].casecmp('setname').zero?
|
377
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
378
|
+
else
|
379
|
+
assign_node(command).public_send(method, *args, command, &block)
|
305
380
|
end
|
306
381
|
end
|
307
382
|
|
308
|
-
def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
383
|
+
def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
384
|
+
if command[1].casecmp('addslots').zero?
|
385
|
+
fail_not_supported_command(method, command, args, &block)
|
386
|
+
elsif command[1].casecmp('delslots').zero?
|
387
|
+
fail_not_supported_command(method, command, args, &block)
|
388
|
+
elsif command[1].casecmp('failover').zero?
|
389
|
+
fail_not_supported_command(method, command, args, &block)
|
390
|
+
elsif command[1].casecmp('forget').zero?
|
391
|
+
fail_not_supported_command(method, command, args, &block)
|
392
|
+
elsif command[1].casecmp('meet').zero?
|
393
|
+
fail_not_supported_command(method, command, args, &block)
|
394
|
+
elsif command[1].casecmp('replicate').zero?
|
395
|
+
fail_not_supported_command(method, command, args, &block)
|
396
|
+
elsif command[1].casecmp('reset').zero?
|
397
|
+
fail_not_supported_command(method, command, args, &block)
|
398
|
+
elsif command[1].casecmp('set-config-epoch').zero?
|
399
|
+
fail_not_supported_command(method, command, args, &block)
|
400
|
+
elsif command[1].casecmp('setslot').zero?
|
401
|
+
fail_not_supported_command(method, command, args, &block)
|
402
|
+
elsif command[1].casecmp('saveconfig').zero?
|
403
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
404
|
+
elsif command[1].casecmp('getkeysinslot').zero?
|
315
405
|
raise ArgumentError, command.join(' ') if command.size != 4
|
316
406
|
|
317
407
|
handle_node_reload_error do
|
318
408
|
node_key = @node.find_node_key_of_replica(command[2])
|
319
409
|
@node.find_by(node_key).public_send(method, *args, command, &block)
|
320
410
|
end
|
321
|
-
else
|
411
|
+
else
|
412
|
+
assign_node(command).public_send(method, *args, command, &block)
|
322
413
|
end
|
323
414
|
end
|
324
415
|
|
325
|
-
def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
|
326
|
-
|
327
|
-
when 'debug', 'kill'
|
416
|
+
def send_script_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
417
|
+
if command[1].casecmp('debug').zero?
|
328
418
|
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
329
|
-
|
419
|
+
elsif command[1].casecmp('kill').zero?
|
420
|
+
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
421
|
+
elsif command[1].casecmp('flush').zero?
|
422
|
+
@node.call_primaries(method, command, args).first.then(&TSF.call(block))
|
423
|
+
elsif command[1].casecmp('load').zero?
|
330
424
|
@node.call_primaries(method, command, args).first.then(&TSF.call(block))
|
331
|
-
|
425
|
+
elsif command[1].casecmp('exists').zero?
|
332
426
|
@node.call_all(method, command, args).transpose.map { |arr| arr.any?(&:zero?) ? 0 : 1 }.then(&TSF.call(block))
|
333
|
-
else
|
427
|
+
else
|
428
|
+
assign_node(command).public_send(method, *args, command, &block)
|
334
429
|
end
|
335
430
|
end
|
336
431
|
|
337
432
|
def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
338
|
-
|
339
|
-
when 'channels'
|
433
|
+
if command[1].casecmp('channels').zero?
|
340
434
|
@node.call_all(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
|
341
|
-
|
435
|
+
elsif command[1].casecmp('shardchannels').zero?
|
342
436
|
@node.call_replicas(method, command, args).flatten.uniq.sort_by(&:to_s).then(&TSF.call(block))
|
343
|
-
|
437
|
+
elsif command[1].casecmp('numpat').zero?
|
344
438
|
@node.call_all(method, command, args).select { |e| e.is_a?(Integer) }.sum.then(&TSF.call(block))
|
345
|
-
|
439
|
+
elsif command[1].casecmp('numsub').zero?
|
346
440
|
@node.call_all(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
|
347
441
|
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
|
348
|
-
|
442
|
+
elsif command[1].casecmp('shardnumsub').zero?
|
349
443
|
@node.call_replicas(method, command, args).reject(&:empty?).map { |e| Hash[*e] }
|
350
444
|
.reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }.then(&TSF.call(block))
|
351
|
-
else
|
445
|
+
else
|
446
|
+
assign_node(command).public_send(method, *args, command, &block)
|
352
447
|
end
|
353
448
|
end
|
354
449
|
|
355
|
-
def send_watch_command(command)
|
450
|
+
def send_watch_command(_method, command, _args, &_block)
|
356
451
|
unless block_given?
|
357
452
|
msg = 'A block required. And you need to use the block argument as a client for the transaction.'
|
358
453
|
raise ::RedisClient::Cluster::Transaction::ConsistencyError.new(msg).with_config(@config)
|
@@ -367,22 +462,23 @@ class RedisClient
|
|
367
462
|
end
|
368
463
|
end
|
369
464
|
|
370
|
-
def send_multiple_keys_command(
|
465
|
+
def send_multiple_keys_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
371
466
|
# This implementation is prioritized performance rather than readability or so.
|
372
|
-
|
373
|
-
|
467
|
+
cmd = command.first
|
468
|
+
if cmd.casecmp('mget').zero?
|
374
469
|
single_key_cmd = 'get'
|
375
470
|
keys_step = 1
|
376
|
-
|
471
|
+
elsif cmd.casecmp('mset').zero?
|
377
472
|
single_key_cmd = 'set'
|
378
473
|
keys_step = 2
|
379
|
-
|
474
|
+
elsif cmd.casecmp('del').zero?
|
380
475
|
single_key_cmd = 'del'
|
381
476
|
keys_step = 1
|
382
|
-
else
|
477
|
+
else
|
478
|
+
raise NotImplementedError, cmd
|
383
479
|
end
|
384
480
|
|
385
|
-
return
|
481
|
+
return assign_node_and_send_command(method, command, args, &block) if command.size <= keys_step + 1 || ::RedisClient::Cluster::KeySlotConverter.hash_tag_included?(command[1])
|
386
482
|
|
387
483
|
seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
|
388
484
|
pipeline = ::RedisClient::Cluster::Pipeline.new(self, @command_builder, @concurrent_worker, exception: true, seed: seed)
|
@@ -402,10 +498,12 @@ class RedisClient
|
|
402
498
|
end
|
403
499
|
|
404
500
|
replies = pipeline.execute
|
405
|
-
result =
|
406
|
-
|
407
|
-
|
408
|
-
|
501
|
+
result = if cmd.casecmp('mset').zero?
|
502
|
+
replies.first
|
503
|
+
elsif cmd.casecmp('del').zero?
|
504
|
+
replies.sum
|
505
|
+
else
|
506
|
+
replies
|
409
507
|
end
|
410
508
|
block_given? ? yield(result) : result
|
411
509
|
end
|
@@ -93,24 +93,24 @@ class RedisClient
|
|
93
93
|
end
|
94
94
|
|
95
95
|
def prepare_tx
|
96
|
-
@pipeline.call('
|
96
|
+
@pipeline.call('multi')
|
97
97
|
@pending_commands.each(&:call)
|
98
98
|
@pending_commands.clear
|
99
99
|
end
|
100
100
|
|
101
101
|
def commit
|
102
|
-
@pipeline.call('
|
102
|
+
@pipeline.call('exec')
|
103
103
|
settle
|
104
104
|
end
|
105
105
|
|
106
106
|
def cancel
|
107
|
-
@pipeline.call('
|
107
|
+
@pipeline.call('discard')
|
108
108
|
settle
|
109
109
|
end
|
110
110
|
|
111
111
|
def settle
|
112
112
|
# If we needed ASKING on the watch, we need ASKING on the multi as well.
|
113
|
-
@node.call('
|
113
|
+
@node.call('asking') if @asking
|
114
114
|
# Don't handle redirections at this level if we're in a watch (the watcher handles redirections
|
115
115
|
# at the whole-transaction level.)
|
116
116
|
send_transaction(@node, redirect: !!@watching_slot ? 0 : MAX_REDIRECTION)
|
@@ -189,7 +189,7 @@ class RedisClient
|
|
189
189
|
end
|
190
190
|
|
191
191
|
def try_asking(node)
|
192
|
-
node.call('
|
192
|
+
node.call('asking') == 'OK'
|
193
193
|
rescue StandardError
|
194
194
|
false
|
195
195
|
end
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -64,7 +64,7 @@ class RedisClient
|
|
64
64
|
def scan(*args, **kwargs, &block)
|
65
65
|
return to_enum(__callee__, *args, **kwargs) unless block_given?
|
66
66
|
|
67
|
-
command = @command_builder.generate(['
|
67
|
+
command = @command_builder.generate(['scan', ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
68
68
|
seed = Random.new_seed
|
69
69
|
loop do
|
70
70
|
cursor, keys = router.scan(command, seed: seed)
|
@@ -77,21 +77,21 @@ class RedisClient
|
|
77
77
|
def sscan(key, *args, **kwargs, &block)
|
78
78
|
return to_enum(__callee__, key, *args, **kwargs) unless block_given?
|
79
79
|
|
80
|
-
command = @command_builder.generate(['
|
80
|
+
command = @command_builder.generate(['sscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
81
81
|
router.scan_single_key(command, arity: 1, &block)
|
82
82
|
end
|
83
83
|
|
84
84
|
def hscan(key, *args, **kwargs, &block)
|
85
85
|
return to_enum(__callee__, key, *args, **kwargs) unless block_given?
|
86
86
|
|
87
|
-
command = @command_builder.generate(['
|
87
|
+
command = @command_builder.generate(['hscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
88
88
|
router.scan_single_key(command, arity: 2, &block)
|
89
89
|
end
|
90
90
|
|
91
91
|
def zscan(key, *args, **kwargs, &block)
|
92
92
|
return to_enum(__callee__, key, *args, **kwargs) unless block_given?
|
93
93
|
|
94
|
-
command = @command_builder.generate(['
|
94
|
+
command = @command_builder.generate(['zscan', key, ZERO_CURSOR_FOR_SCAN] + args, kwargs)
|
95
95
|
router.scan_single_key(command, arity: 2, &block)
|
96
96
|
end
|
97
97
|
|
@@ -152,8 +152,9 @@ class RedisClient
|
|
152
152
|
end
|
153
153
|
|
154
154
|
def method_missing(name, *args, **kwargs, &block)
|
155
|
-
|
156
|
-
|
155
|
+
cmd = name.respond_to?(:name) ? name.name : name.to_s
|
156
|
+
if router.command_exists?(cmd)
|
157
|
+
args.unshift(cmd)
|
157
158
|
command = @command_builder.generate(args, kwargs)
|
158
159
|
return router.send_command(:call_v, command, &block)
|
159
160
|
end
|
@@ -19,7 +19,7 @@ class RedisClient
|
|
19
19
|
VALID_NODES_KEYS = %i[ssl username password host port db].freeze
|
20
20
|
MERGE_CONFIG_KEYS = %i[ssl username password].freeze
|
21
21
|
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
22
|
-
MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS',
|
22
|
+
MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', -1)) # for backward compatibility
|
23
23
|
# It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
|
24
24
|
SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
|
25
25
|
# It affects to strike a balance between load and stability in initialization or changed states.
|
@@ -47,7 +47,6 @@ class RedisClient
|
|
47
47
|
max_startup_sample: MAX_STARTUP_SAMPLE,
|
48
48
|
**client_config
|
49
49
|
)
|
50
|
-
|
51
50
|
@replica = true & replica
|
52
51
|
@replica_affinity = replica_affinity.to_s.to_sym
|
53
52
|
@fixed_hostname = fixed_hostname.to_s
|
@@ -110,12 +109,16 @@ class RedisClient
|
|
110
109
|
private
|
111
110
|
|
112
111
|
def merge_concurrency_option(option)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
112
|
+
opts = {}
|
113
|
+
|
114
|
+
if MAX_WORKERS.positive?
|
115
|
+
opts[:model] = :on_demand
|
116
|
+
opts[:size] = MAX_WORKERS
|
118
117
|
end
|
118
|
+
|
119
|
+
opts.merge!(option.transform_keys(&:to_sym)) if option.is_a?(Hash)
|
120
|
+
opts[:model] = :none if opts.empty?
|
121
|
+
opts.freeze
|
119
122
|
end
|
120
123
|
|
121
124
|
def build_node_configs(addrs)
|
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.
|
4
|
+
version: 0.13.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taishi Kasuga
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -49,7 +49,6 @@ files:
|
|
49
49
|
- lib/redis_client/cluster/node/random_replica_or_primary.rb
|
50
50
|
- lib/redis_client/cluster/node_key.rb
|
51
51
|
- lib/redis_client/cluster/noop_command_builder.rb
|
52
|
-
- lib/redis_client/cluster/normalized_cmd_name.rb
|
53
52
|
- lib/redis_client/cluster/optimistic_locking.rb
|
54
53
|
- lib/redis_client/cluster/pipeline.rb
|
55
54
|
- lib/redis_client/cluster/pub_sub.rb
|
@@ -1,71 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'singleton'
|
4
|
-
|
5
|
-
class RedisClient
|
6
|
-
class Cluster
|
7
|
-
class NormalizedCmdName
|
8
|
-
include Singleton
|
9
|
-
|
10
|
-
EMPTY_STRING = ''
|
11
|
-
|
12
|
-
private_constant :EMPTY_STRING
|
13
|
-
|
14
|
-
def initialize
|
15
|
-
@cache = {}
|
16
|
-
@mutex = Mutex.new
|
17
|
-
end
|
18
|
-
|
19
|
-
def get_by_command(command)
|
20
|
-
get(command, index: 0)
|
21
|
-
end
|
22
|
-
|
23
|
-
def get_by_subcommand(command)
|
24
|
-
get(command, index: 1)
|
25
|
-
end
|
26
|
-
|
27
|
-
def get_by_name(name)
|
28
|
-
get(name, index: 0)
|
29
|
-
end
|
30
|
-
|
31
|
-
def clear
|
32
|
-
@mutex.synchronize { @cache.clear }
|
33
|
-
true
|
34
|
-
end
|
35
|
-
|
36
|
-
private
|
37
|
-
|
38
|
-
def get(command, index:)
|
39
|
-
name = extract_name(command, index: index)
|
40
|
-
return EMPTY_STRING if name.nil? || name.empty?
|
41
|
-
|
42
|
-
normalize(name)
|
43
|
-
end
|
44
|
-
|
45
|
-
def extract_name(command, index:)
|
46
|
-
case command
|
47
|
-
when String, Symbol then index.zero? ? command : nil
|
48
|
-
when Array then extract_name_from_array(command, index: index)
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def extract_name_from_array(command, index:)
|
53
|
-
return if command.size - 1 < index
|
54
|
-
|
55
|
-
case e = command[index]
|
56
|
-
when String, Symbol then e
|
57
|
-
when Array then e[index]
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def normalize(name)
|
62
|
-
return @cache[name] || name.to_s.downcase if @cache.key?(name)
|
63
|
-
return name.to_s.downcase if @mutex.locked?
|
64
|
-
|
65
|
-
str = name.to_s.downcase
|
66
|
-
@mutex.synchronize { @cache[name] = str }
|
67
|
-
str
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
end
|