redis-cluster-client 0.7.5 → 0.8.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 +53 -17
- data/lib/redis_client/cluster/error_identification.rb +55 -0
- data/lib/redis_client/cluster/key_slot_converter.rb +18 -0
- data/lib/redis_client/cluster/node/base_topology.rb +60 -0
- data/lib/redis_client/cluster/node/latency_replica.rb +13 -17
- data/lib/redis_client/cluster/node/primary_only.rb +7 -19
- data/lib/redis_client/cluster/node/random_replica.rb +2 -4
- data/lib/redis_client/cluster/node/random_replica_or_primary.rb +2 -4
- data/lib/redis_client/cluster/node.rb +160 -107
- data/lib/redis_client/cluster/node_key.rb +4 -0
- data/lib/redis_client/cluster/optimistic_locking.rb +72 -0
- data/lib/redis_client/cluster/pinning_node.rb +35 -0
- data/lib/redis_client/cluster/pipeline.rb +22 -14
- data/lib/redis_client/cluster/router.rb +75 -80
- data/lib/redis_client/cluster/transaction.rb +140 -20
- data/lib/redis_client/cluster.rb +47 -4
- data/lib/redis_client/cluster_config.rb +30 -45
- metadata +9 -6
- data/lib/redis_client/cluster/node/replica_mixin.rb +0 -37
@@ -8,12 +8,14 @@ require 'redis_client/cluster/key_slot_converter'
|
|
8
8
|
require 'redis_client/cluster/node'
|
9
9
|
require 'redis_client/cluster/node_key'
|
10
10
|
require 'redis_client/cluster/normalized_cmd_name'
|
11
|
+
require 'redis_client/cluster/transaction'
|
12
|
+
require 'redis_client/cluster/optimistic_locking'
|
13
|
+
require 'redis_client/cluster/error_identification'
|
11
14
|
|
12
15
|
class RedisClient
|
13
16
|
class Cluster
|
14
17
|
class Router
|
15
18
|
ZERO_CURSOR_FOR_SCAN = '0'
|
16
|
-
METHODS_FOR_BLOCKING_CMD = %i[blocking_call_v blocking_call].freeze
|
17
19
|
TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
|
18
20
|
|
19
21
|
def initialize(config, concurrent_worker, pool: nil, **kwargs)
|
@@ -23,9 +25,9 @@ class RedisClient
|
|
23
25
|
@concurrent_worker = concurrent_worker
|
24
26
|
@pool = pool
|
25
27
|
@client_kwargs = kwargs
|
26
|
-
@node =
|
28
|
+
@node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
|
29
|
+
update_cluster_info!
|
27
30
|
@command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
|
28
|
-
@mutex = Mutex.new
|
29
31
|
@command_builder = @config.command_builder
|
30
32
|
end
|
31
33
|
|
@@ -45,6 +47,7 @@ class RedisClient
|
|
45
47
|
when 'memory' then send_memory_command(method, command, args, &block)
|
46
48
|
when 'script' then send_script_command(method, command, args, &block)
|
47
49
|
when 'pubsub' then send_pubsub_command(method, command, args, &block)
|
50
|
+
when 'watch' then send_watch_command(command, &block)
|
48
51
|
when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
|
49
52
|
@node.call_all(method, command, args).first.then(&TSF.call(block))
|
50
53
|
when 'flushall', 'flushdb'
|
@@ -66,6 +69,8 @@ class RedisClient
|
|
66
69
|
raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
|
67
70
|
|
68
71
|
update_cluster_info! if e.errors.values.any? do |err|
|
72
|
+
next false if ::RedisClient::Cluster::ErrorIdentification.identifiable?(err) && @node.none? { |c| ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(err, c) }
|
73
|
+
|
69
74
|
err.message.start_with?('CLUSTERDOWN Hash slot not served')
|
70
75
|
end
|
71
76
|
|
@@ -73,70 +78,54 @@ class RedisClient
|
|
73
78
|
end
|
74
79
|
|
75
80
|
# @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
|
76
|
-
def try_send(node, method, command, args, retry_count: 3, &block)
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
81
|
+
def try_send(node, method, command, args, retry_count: 3, &block)
|
82
|
+
handle_redirection(node, retry_count: retry_count) do |on_node|
|
83
|
+
if args.empty?
|
84
|
+
# prevent memory allocation for variable-length args
|
85
|
+
on_node.public_send(method, command, &block)
|
86
|
+
else
|
87
|
+
on_node.public_send(method, *args, command, &block)
|
88
|
+
end
|
82
89
|
end
|
83
|
-
|
84
|
-
raise
|
85
|
-
rescue ::RedisClient::CommandError => e
|
86
|
-
raise if retry_count <= 0
|
90
|
+
end
|
87
91
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
retry
|
92
|
-
elsif e.message.start_with?('ASK')
|
93
|
-
node = assign_asking_node(e.message)
|
94
|
-
node.call('ASKING')
|
95
|
-
retry_count -= 1
|
96
|
-
retry
|
97
|
-
elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
|
98
|
-
update_cluster_info!
|
99
|
-
retry_count -= 1
|
100
|
-
retry
|
101
|
-
else
|
102
|
-
raise
|
92
|
+
def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block)
|
93
|
+
handle_redirection(node, retry_count: retry_count) do |on_node|
|
94
|
+
on_node.public_send(method, *args, **kwargs, &block)
|
103
95
|
end
|
104
|
-
rescue ::RedisClient::ConnectionError => e
|
105
|
-
raise if METHODS_FOR_BLOCKING_CMD.include?(method) && e.is_a?(RedisClient::ReadTimeoutError)
|
106
|
-
raise if retry_count <= 0
|
107
|
-
|
108
|
-
update_cluster_info!
|
109
|
-
retry_count -= 1
|
110
|
-
retry
|
111
96
|
end
|
112
97
|
|
113
|
-
def
|
114
|
-
node
|
98
|
+
def handle_redirection(node, retry_count:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
99
|
+
yield node
|
115
100
|
rescue ::RedisClient::CircuitBreaker::OpenCircuitError
|
116
101
|
raise
|
117
102
|
rescue ::RedisClient::CommandError => e
|
118
|
-
raise
|
103
|
+
raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
|
119
104
|
|
120
105
|
if e.message.start_with?('MOVED')
|
121
106
|
node = assign_redirection_node(e.message)
|
122
107
|
retry_count -= 1
|
123
|
-
retry
|
108
|
+
retry if retry_count >= 0
|
124
109
|
elsif e.message.start_with?('ASK')
|
125
110
|
node = assign_asking_node(e.message)
|
126
|
-
node.call('ASKING')
|
127
111
|
retry_count -= 1
|
128
|
-
|
112
|
+
if retry_count >= 0
|
113
|
+
node.call('ASKING')
|
114
|
+
retry
|
115
|
+
end
|
129
116
|
elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
|
130
117
|
update_cluster_info!
|
131
118
|
retry_count -= 1
|
132
|
-
retry
|
133
|
-
else
|
134
|
-
raise
|
119
|
+
retry if retry_count >= 0
|
135
120
|
end
|
136
|
-
|
137
|
-
|
121
|
+
raise
|
122
|
+
rescue ::RedisClient::ConnectionError => e
|
123
|
+
raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
|
138
124
|
|
139
125
|
update_cluster_info!
|
126
|
+
|
127
|
+
raise if retry_count <= 0
|
128
|
+
|
140
129
|
retry_count -= 1
|
141
130
|
retry
|
142
131
|
end
|
@@ -170,21 +159,40 @@ class RedisClient
|
|
170
159
|
find_node(node_key)
|
171
160
|
end
|
172
161
|
|
173
|
-
def
|
174
|
-
key
|
175
|
-
|
176
|
-
|
177
|
-
if @command.should_send_to_primary?(command)
|
178
|
-
@node.find_node_key_of_primary(slot) || @node.any_primary_node_key(seed: seed)
|
162
|
+
def find_node_key_by_key(key, seed: nil, primary: false)
|
163
|
+
if key && !key.empty?
|
164
|
+
slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
165
|
+
primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
|
179
166
|
else
|
180
|
-
@node.
|
167
|
+
primary ? @node.any_primary_node_key(seed: seed) : @node.any_replica_node_key(seed: seed)
|
181
168
|
end
|
182
169
|
end
|
183
170
|
|
171
|
+
def find_primary_node_by_slot(slot)
|
172
|
+
node_key = @node.find_node_key_of_primary(slot)
|
173
|
+
find_node(node_key)
|
174
|
+
end
|
175
|
+
|
176
|
+
def find_node_key(command, seed: nil)
|
177
|
+
key = @command.extract_first_key(command)
|
178
|
+
find_node_key_by_key(key, seed: seed, primary: @command.should_send_to_primary?(command))
|
179
|
+
end
|
180
|
+
|
184
181
|
def find_primary_node_key(command)
|
185
182
|
key = @command.extract_first_key(command)
|
186
|
-
|
187
|
-
|
183
|
+
return nil unless key&.size&.> 0
|
184
|
+
|
185
|
+
find_node_key_by_key(key, primary: true)
|
186
|
+
end
|
187
|
+
|
188
|
+
def find_slot(command)
|
189
|
+
find_slot_by_key(@command.extract_first_key(command))
|
190
|
+
end
|
191
|
+
|
192
|
+
def find_slot_by_key(key)
|
193
|
+
return if key.empty?
|
194
|
+
|
195
|
+
::RedisClient::Cluster::KeySlotConverter.convert(key)
|
188
196
|
end
|
189
197
|
|
190
198
|
def find_node(node_key, retry_count: 3)
|
@@ -306,34 +314,21 @@ class RedisClient
|
|
306
314
|
end
|
307
315
|
end
|
308
316
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
::RedisClient::Cluster::
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
**kwargs
|
321
|
-
)
|
317
|
+
# for redis-rb
|
318
|
+
def send_watch_command(command)
|
319
|
+
raise ::RedisClient::Cluster::Transaction::ConsistencyError, 'A block required. And you need to use the block argument as a client for the transaction.' unless block_given?
|
320
|
+
|
321
|
+
::RedisClient::Cluster::OptimisticLocking.new(self).watch(command[1..]) do |c, slot, asking|
|
322
|
+
transaction = ::RedisClient::Cluster::Transaction.new(
|
323
|
+
self, @command_builder, node: c, slot: slot, asking: asking
|
324
|
+
)
|
325
|
+
yield transaction
|
326
|
+
transaction.execute
|
327
|
+
end
|
322
328
|
end
|
323
329
|
|
324
330
|
def update_cluster_info!
|
325
|
-
|
326
|
-
|
327
|
-
@mutex.synchronize do
|
328
|
-
begin
|
329
|
-
@node.each(&:close)
|
330
|
-
rescue ::RedisClient::Cluster::ErrorCollection
|
331
|
-
# ignore
|
332
|
-
end
|
333
|
-
|
334
|
-
@config = @original_config.dup if @connect_with_original_config
|
335
|
-
@node = fetch_cluster_info(@config, @concurrent_worker, pool: @pool, **@client_kwargs)
|
336
|
-
end
|
331
|
+
@node.reload!
|
337
332
|
end
|
338
333
|
end
|
339
334
|
end
|
@@ -1,56 +1,176 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
|
+
require 'redis_client/cluster/pipeline'
|
4
5
|
|
5
6
|
class RedisClient
|
6
7
|
class Cluster
|
7
8
|
class Transaction
|
8
9
|
ConsistencyError = Class.new(::RedisClient::Error)
|
10
|
+
MAX_REDIRECTION = 2
|
9
11
|
|
10
|
-
def initialize(router, command_builder)
|
12
|
+
def initialize(router, command_builder, node: nil, slot: nil, asking: false)
|
11
13
|
@router = router
|
12
14
|
@command_builder = command_builder
|
13
|
-
@
|
15
|
+
@retryable = true
|
16
|
+
@pipeline = ::RedisClient::Pipeline.new(@command_builder)
|
17
|
+
@pending_commands = []
|
18
|
+
@node = node
|
19
|
+
prepare_tx unless @node.nil?
|
20
|
+
@watching_slot = slot
|
21
|
+
@asking = asking
|
14
22
|
end
|
15
23
|
|
16
|
-
def call(*command, **kwargs, &
|
24
|
+
def call(*command, **kwargs, &block)
|
17
25
|
command = @command_builder.generate(command, kwargs)
|
18
|
-
|
26
|
+
if prepare(command)
|
27
|
+
@pipeline.call_v(command, &block)
|
28
|
+
else
|
29
|
+
defer { @pipeline.call_v(command, &block) }
|
30
|
+
end
|
19
31
|
end
|
20
32
|
|
21
|
-
def call_v(command, &
|
33
|
+
def call_v(command, &block)
|
22
34
|
command = @command_builder.generate(command)
|
23
|
-
|
35
|
+
if prepare(command)
|
36
|
+
@pipeline.call_v(command, &block)
|
37
|
+
else
|
38
|
+
defer { @pipeline.call_v(command, &block) }
|
39
|
+
end
|
24
40
|
end
|
25
41
|
|
26
|
-
def call_once(*command, **kwargs, &
|
42
|
+
def call_once(*command, **kwargs, &block)
|
43
|
+
@retryable = false
|
27
44
|
command = @command_builder.generate(command, kwargs)
|
28
|
-
|
45
|
+
if prepare(command)
|
46
|
+
@pipeline.call_once_v(command, &block)
|
47
|
+
else
|
48
|
+
defer { @pipeline.call_once_v(command, &block) }
|
49
|
+
end
|
29
50
|
end
|
30
51
|
|
31
|
-
def call_once_v(command, &
|
52
|
+
def call_once_v(command, &block)
|
53
|
+
@retryable = false
|
32
54
|
command = @command_builder.generate(command)
|
33
|
-
|
55
|
+
if prepare(command)
|
56
|
+
@pipeline.call_once_v(command, &block)
|
57
|
+
else
|
58
|
+
defer { @pipeline.call_once_v(command, &block) }
|
59
|
+
end
|
34
60
|
end
|
35
61
|
|
36
|
-
def execute
|
37
|
-
|
38
|
-
raise ArgumentError, 'empty transaction' if @node_key.nil?
|
62
|
+
def execute
|
63
|
+
@pending_commands.each(&:call)
|
39
64
|
|
40
|
-
|
41
|
-
|
65
|
+
raise ArgumentError, 'empty transaction' if @pipeline._empty?
|
66
|
+
raise ConsistencyError, "couldn't determine the node: #{@pipeline._commands}" if @node.nil?
|
67
|
+
|
68
|
+
settle
|
42
69
|
end
|
43
70
|
|
44
71
|
private
|
45
72
|
|
46
|
-
def
|
73
|
+
def defer(&block)
|
74
|
+
@pending_commands << block
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare(command)
|
79
|
+
return true unless @node.nil?
|
80
|
+
|
47
81
|
node_key = @router.find_primary_node_key(command)
|
48
|
-
|
82
|
+
return false if node_key.nil?
|
49
83
|
|
50
|
-
@
|
51
|
-
|
84
|
+
@node = @router.find_node(node_key)
|
85
|
+
prepare_tx
|
86
|
+
true
|
87
|
+
end
|
52
88
|
|
53
|
-
|
89
|
+
def prepare_tx
|
90
|
+
@pipeline.call('MULTI')
|
91
|
+
@pending_commands.each(&:call)
|
92
|
+
@pending_commands.clear
|
93
|
+
end
|
94
|
+
|
95
|
+
def settle
|
96
|
+
@pipeline.call('EXEC')
|
97
|
+
# If we needed ASKING on the watch, we need ASKING on the multi as well.
|
98
|
+
@node.call('ASKING') if @asking
|
99
|
+
# Don't handle redirections at this level if we're in a watch (the watcher handles redirections
|
100
|
+
# at the whole-transaction level.)
|
101
|
+
send_transaction(@node, redirect: !!@watching_slot ? 0 : MAX_REDIRECTION)
|
102
|
+
end
|
103
|
+
|
104
|
+
def send_transaction(client, redirect:)
|
105
|
+
case client
|
106
|
+
when ::RedisClient then send_pipeline(client, redirect: redirect)
|
107
|
+
when ::RedisClient::Pooled then client.with { |c| send_pipeline(c, redirect: redirect) }
|
108
|
+
else raise NotImplementedError, "#{client.class.name}#multi for cluster client"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def send_pipeline(client, redirect:)
|
113
|
+
replies = client.ensure_connected_cluster_scoped(retryable: @retryable) do |connection|
|
114
|
+
commands = @pipeline._commands
|
115
|
+
client.middlewares.call_pipelined(commands, client.config) do
|
116
|
+
connection.call_pipelined(commands, nil)
|
117
|
+
rescue ::RedisClient::CommandError => e
|
118
|
+
ensure_the_same_slot!(commands)
|
119
|
+
return handle_command_error!(e, redirect: redirect) unless redirect.zero?
|
120
|
+
|
121
|
+
raise
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
return if replies.last.nil?
|
126
|
+
|
127
|
+
coerce_results!(replies.last)
|
128
|
+
end
|
129
|
+
|
130
|
+
def coerce_results!(results, offset: 1)
|
131
|
+
results.each_with_index do |result, index|
|
132
|
+
if result.is_a?(::RedisClient::CommandError)
|
133
|
+
result._set_command(@pipeline._commands[index + offset])
|
134
|
+
raise result
|
135
|
+
end
|
136
|
+
|
137
|
+
next if @pipeline._blocks.nil?
|
138
|
+
|
139
|
+
block = @pipeline._blocks[index + offset]
|
140
|
+
next if block.nil?
|
141
|
+
|
142
|
+
results[index] = block.call(result)
|
143
|
+
end
|
144
|
+
|
145
|
+
results
|
146
|
+
end
|
147
|
+
|
148
|
+
def handle_command_error!(err, redirect:) # rubocop:disable Metrics/AbcSize
|
149
|
+
if err.message.start_with?('CROSSSLOT')
|
150
|
+
raise ConsistencyError, "#{err.message}: #{err.command}"
|
151
|
+
elsif err.message.start_with?('MOVED')
|
152
|
+
node = @router.assign_redirection_node(err.message)
|
153
|
+
send_transaction(node, redirect: redirect - 1)
|
154
|
+
elsif err.message.start_with?('ASK')
|
155
|
+
node = @router.assign_asking_node(err.message)
|
156
|
+
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
|
157
|
+
else
|
158
|
+
raise err
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def ensure_the_same_slot!(commands)
|
163
|
+
slots = commands.map { |command| @router.find_slot(command) }.compact.uniq
|
164
|
+
return if slots.size == 1 && @watching_slot.nil?
|
165
|
+
return if slots.size == 1 && @watching_slot == slots.first
|
166
|
+
|
167
|
+
raise(ConsistencyError, "the transaction should be executed to a slot in a node: #{commands}")
|
168
|
+
end
|
169
|
+
|
170
|
+
def try_asking(node)
|
171
|
+
node.call('ASKING') == 'OK'
|
172
|
+
rescue StandardError
|
173
|
+
false
|
54
174
|
end
|
55
175
|
end
|
56
176
|
end
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -5,6 +5,8 @@ require 'redis_client/cluster/pipeline'
|
|
5
5
|
require 'redis_client/cluster/pub_sub'
|
6
6
|
require 'redis_client/cluster/router'
|
7
7
|
require 'redis_client/cluster/transaction'
|
8
|
+
require 'redis_client/cluster/pinning_node'
|
9
|
+
require 'redis_client/cluster/optimistic_locking'
|
8
10
|
|
9
11
|
class RedisClient
|
10
12
|
class Cluster
|
@@ -80,23 +82,51 @@ class RedisClient
|
|
80
82
|
@router.try_delegate(node, :zscan, key, *args, **kwargs, &block)
|
81
83
|
end
|
82
84
|
|
83
|
-
def pipelined
|
85
|
+
def pipelined(exception: true)
|
84
86
|
seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
|
85
|
-
pipeline = ::RedisClient::Cluster::Pipeline.new(
|
87
|
+
pipeline = ::RedisClient::Cluster::Pipeline.new(
|
88
|
+
@router,
|
89
|
+
@command_builder,
|
90
|
+
@concurrent_worker,
|
91
|
+
exception: exception,
|
92
|
+
seed: seed
|
93
|
+
)
|
94
|
+
|
86
95
|
yield pipeline
|
87
96
|
return [] if pipeline.empty?
|
88
97
|
|
89
98
|
pipeline.execute
|
90
99
|
end
|
91
100
|
|
92
|
-
def multi(watch: nil
|
93
|
-
|
101
|
+
def multi(watch: nil)
|
102
|
+
if watch.nil? || watch.empty?
|
103
|
+
transaction = ::RedisClient::Cluster::Transaction.new(@router, @command_builder)
|
104
|
+
yield transaction
|
105
|
+
return transaction.execute
|
106
|
+
end
|
107
|
+
|
108
|
+
::RedisClient::Cluster::OptimisticLocking.new(@router).watch(watch) do |c, slot, asking|
|
109
|
+
transaction = ::RedisClient::Cluster::Transaction.new(
|
110
|
+
@router, @command_builder, node: c, slot: slot, asking: asking
|
111
|
+
)
|
112
|
+
yield transaction
|
113
|
+
transaction.execute
|
114
|
+
end
|
94
115
|
end
|
95
116
|
|
96
117
|
def pubsub
|
97
118
|
::RedisClient::Cluster::PubSub.new(@router, @command_builder)
|
98
119
|
end
|
99
120
|
|
121
|
+
# TODO: This isn't an official public interface yet. Don't use in your production environment.
|
122
|
+
# @see https://github.com/redis-rb/redis-cluster-client/issues/299
|
123
|
+
def with(key: nil, hashtag: nil, write: true)
|
124
|
+
key = process_with_arguments(key, hashtag)
|
125
|
+
node_key = @router.find_node_key_by_key(key, primary: write)
|
126
|
+
node = @router.find_node(node_key)
|
127
|
+
node.with { |c| yield ::RedisClient::Cluster::PinningNode.new(c) }
|
128
|
+
end
|
129
|
+
|
100
130
|
def close
|
101
131
|
@concurrent_worker.close
|
102
132
|
@router.close
|
@@ -105,6 +135,19 @@ class RedisClient
|
|
105
135
|
|
106
136
|
private
|
107
137
|
|
138
|
+
def process_with_arguments(key, hashtag) # rubocop:disable Metrics/CyclomaticComplexity
|
139
|
+
raise ArgumentError, 'Only one of key or hashtag may be provided' if key && hashtag
|
140
|
+
|
141
|
+
if hashtag
|
142
|
+
# The documentation says not to wrap your hashtag in {}, but people will probably
|
143
|
+
# do it anyway and it's easy for us to fix here.
|
144
|
+
key = hashtag&.match?(/^{.*}$/) ? hashtag : "{#{hashtag}}"
|
145
|
+
end
|
146
|
+
raise ArgumentError, 'One of key or hashtag must be provided' if key.nil? || key.empty?
|
147
|
+
|
148
|
+
key
|
149
|
+
end
|
150
|
+
|
108
151
|
def method_missing(name, *args, **kwargs, &block)
|
109
152
|
if @router.command_exists?(name)
|
110
153
|
args.unshift(name)
|
@@ -23,9 +23,10 @@ class RedisClient
|
|
23
23
|
|
24
24
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
25
25
|
|
26
|
-
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
26
|
+
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
27
|
+
:connect_with_original_config, :startup_nodes
|
27
28
|
|
28
|
-
def initialize(
|
29
|
+
def initialize(
|
29
30
|
nodes: DEFAULT_NODES,
|
30
31
|
replica: false,
|
31
32
|
replica_affinity: :random,
|
@@ -34,39 +35,26 @@ class RedisClient
|
|
34
35
|
connect_with_original_config: false,
|
35
36
|
client_implementation: ::RedisClient::Cluster, # for redis gem
|
36
37
|
slow_command_timeout: SLOW_COMMAND_TIMEOUT,
|
38
|
+
command_builder: ::RedisClient::CommandBuilder,
|
37
39
|
**client_config
|
38
40
|
)
|
39
41
|
|
40
42
|
@replica = true & replica
|
41
43
|
@replica_affinity = replica_affinity.to_s.to_sym
|
42
44
|
@fixed_hostname = fixed_hostname.to_s
|
43
|
-
@
|
44
|
-
|
45
|
-
@
|
46
|
-
|
45
|
+
@command_builder = command_builder
|
46
|
+
node_configs = build_node_configs(nodes.dup)
|
47
|
+
@client_config = merge_generic_config(client_config, node_configs)
|
48
|
+
# Keep tabs on the original startup nodes we were constructed with
|
49
|
+
@startup_nodes = build_startup_nodes(node_configs)
|
47
50
|
@concurrency = merge_concurrency_option(concurrency)
|
48
51
|
@connect_with_original_config = connect_with_original_config
|
49
52
|
@client_implementation = client_implementation
|
50
53
|
@slow_command_timeout = slow_command_timeout
|
51
|
-
@mutex = Mutex.new
|
52
|
-
end
|
53
|
-
|
54
|
-
def dup
|
55
|
-
self.class.new(
|
56
|
-
nodes: @node_configs,
|
57
|
-
replica: @replica,
|
58
|
-
replica_affinity: @replica_affinity,
|
59
|
-
fixed_hostname: @fixed_hostname,
|
60
|
-
concurrency: @concurrency,
|
61
|
-
connect_with_original_config: @connect_with_original_config,
|
62
|
-
client_implementation: @client_implementation,
|
63
|
-
slow_command_timeout: @slow_command_timeout,
|
64
|
-
**@client_config
|
65
|
-
)
|
66
54
|
end
|
67
55
|
|
68
56
|
def inspect
|
69
|
-
"#<#{self.class.name} #{
|
57
|
+
"#<#{self.class.name} #{startup_nodes.values}>"
|
70
58
|
end
|
71
59
|
|
72
60
|
def read_timeout
|
@@ -86,29 +74,14 @@ class RedisClient
|
|
86
74
|
@client_implementation.new(self, concurrency: @concurrency, **kwargs)
|
87
75
|
end
|
88
76
|
|
89
|
-
def per_node_key
|
90
|
-
@node_configs.to_h do |config|
|
91
|
-
node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
|
92
|
-
config = @client_config.merge(config)
|
93
|
-
config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
|
94
|
-
[node_key, config]
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
77
|
def use_replica?
|
99
78
|
@replica
|
100
79
|
end
|
101
80
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
def add_node(host, port)
|
109
|
-
return if @mutex.locked?
|
110
|
-
|
111
|
-
@mutex.synchronize { @node_configs << { host: host, port: port } }
|
81
|
+
def client_config_for_node(node_key)
|
82
|
+
config = ::RedisClient::Cluster::NodeKey.hashify(node_key)
|
83
|
+
config[:port] = ensure_integer(config[:port])
|
84
|
+
augment_client_config(config)
|
112
85
|
end
|
113
86
|
|
114
87
|
private
|
@@ -176,11 +149,23 @@ class RedisClient
|
|
176
149
|
end
|
177
150
|
|
178
151
|
def merge_generic_config(client_config, node_configs)
|
179
|
-
|
152
|
+
cfg = node_configs.first || {}
|
153
|
+
client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
|
154
|
+
.merge(cfg.slice(*MERGE_CONFIG_KEYS))
|
155
|
+
end
|
156
|
+
|
157
|
+
def build_startup_nodes(configs)
|
158
|
+
configs.to_h do |config|
|
159
|
+
node_key = ::RedisClient::Cluster::NodeKey.build_from_host_port(config[:host], config[:port])
|
160
|
+
config = augment_client_config(config)
|
161
|
+
[node_key, config]
|
162
|
+
end
|
163
|
+
end
|
180
164
|
|
181
|
-
|
182
|
-
|
183
|
-
|
165
|
+
def augment_client_config(config)
|
166
|
+
config = @client_config.merge(config)
|
167
|
+
config = config.merge(host: @fixed_hostname) unless @fixed_hostname.empty?
|
168
|
+
config
|
184
169
|
end
|
185
170
|
end
|
186
171
|
end
|