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.
@@ -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