redis-cluster-client 0.7.5 → 0.8.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = fetch_cluster_info(@config, @concurrent_worker, pool: @pool, **@client_kwargs)
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) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
77
- if args.empty?
78
- # prevent memory allocation for variable-length args
79
- node.public_send(method, command, &block)
80
- else
81
- node.public_send(method, *args, command, &block)
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
- rescue ::RedisClient::CircuitBreaker::OpenCircuitError
84
- raise
85
- rescue ::RedisClient::CommandError => e
86
- raise if retry_count <= 0
90
+ end
87
91
 
88
- if e.message.start_with?('MOVED')
89
- node = assign_redirection_node(e.message)
90
- retry_count -= 1
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 try_delegate(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize
114
- node.public_send(method, *args, **kwargs, &block)
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 if retry_count <= 0
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
- retry
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
- rescue ::RedisClient::ConnectionError
137
- raise if retry_count <= 0
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 find_node_key(command, seed: nil)
174
- key = @command.extract_first_key(command)
175
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
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.find_node_key_of_replica(slot, seed: seed) || @node.any_replica_node_key(seed: seed)
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
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
187
- @node.find_node_key_of_primary(slot)
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
- def fetch_cluster_info(config, concurrent_worker, pool: nil, **kwargs)
310
- node_info_list = ::RedisClient::Cluster::Node.load_info(config.per_node_key, concurrent_worker, slow_command_timeout: config.slow_command_timeout, **kwargs)
311
- node_addrs = node_info_list.map { |i| ::RedisClient::Cluster::NodeKey.hashify(i.node_key) }
312
- config.update_node(node_addrs)
313
- ::RedisClient::Cluster::Node.new(
314
- config.per_node_key,
315
- concurrent_worker,
316
- node_info_list: node_info_list,
317
- pool: pool,
318
- with_replica: config.use_replica?,
319
- replica_affinity: config.replica_affinity,
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
- return if @mutex.locked?
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
- @node_key = nil
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
- ensure_node_key(command)
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
- ensure_node_key(command)
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
- ensure_node_key(command)
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
- ensure_node_key(command)
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(watch: nil, &block)
37
- yield self
38
- raise ArgumentError, 'empty transaction' if @node_key.nil?
62
+ def execute
63
+ @pending_commands.each(&:call)
39
64
 
40
- node = @router.find_node(@node_key)
41
- @router.try_delegate(node, :multi, watch: watch, &block)
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 ensure_node_key(command)
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
- raise ConsistencyError, "Client couldn't determine the node to be executed the transaction by: #{command}" if node_key.nil?
82
+ return false if node_key.nil?
49
83
 
50
- @node_key ||= node_key
51
- raise ConsistencyError, "The transaction should be done for single node: #{@node_key}, #{node_key}" if node_key != @node_key
84
+ @node = @router.find_node(node_key)
85
+ prepare_tx
86
+ true
87
+ end
52
88
 
53
- nil
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
@@ -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(@router, @command_builder, @concurrent_worker, seed: seed)
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, &block)
93
- ::RedisClient::Cluster::Transaction.new(@router, @command_builder).execute(watch: watch, &block)
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, :connect_with_original_config
26
+ attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
27
+ :connect_with_original_config, :startup_nodes
27
28
 
28
- def initialize( # rubocop:disable Metrics/AbcSize
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
- @node_configs = build_node_configs(nodes.dup)
44
- client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
45
- @command_builder = client_config.fetch(:command_builder, ::RedisClient::CommandBuilder)
46
- @client_config = merge_generic_config(client_config, @node_configs)
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} #{per_node_key.values}>"
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 update_node(addrs)
103
- return if @mutex.locked?
104
-
105
- @mutex.synchronize { @node_configs = build_node_configs(addrs) }
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
- return client_config if node_configs.empty?
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
- cfg = node_configs.first
182
- MERGE_CONFIG_KEYS.each { |k| client_config[k] = cfg[k] if cfg.key?(k) }
183
- client_config
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