redis-cluster-client 0.7.5 → 0.7.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -13,7 +13,6 @@ class RedisClient
13
13
  class Cluster
14
14
  class Router
15
15
  ZERO_CURSOR_FOR_SCAN = '0'
16
- METHODS_FOR_BLOCKING_CMD = %i[blocking_call_v blocking_call].freeze
17
16
  TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
18
17
 
19
18
  def initialize(config, concurrent_worker, pool: nil, **kwargs)
@@ -23,9 +22,9 @@ class RedisClient
23
22
  @concurrent_worker = concurrent_worker
24
23
  @pool = pool
25
24
  @client_kwargs = kwargs
26
- @node = fetch_cluster_info(@config, @concurrent_worker, pool: @pool, **@client_kwargs)
25
+ @node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
26
+ update_cluster_info!
27
27
  @command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
28
- @mutex = Mutex.new
29
28
  @command_builder = @config.command_builder
30
29
  end
31
30
 
@@ -66,77 +65,61 @@ class RedisClient
66
65
  raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
67
66
 
68
67
  update_cluster_info! if e.errors.values.any? do |err|
69
- err.message.start_with?('CLUSTERDOWN Hash slot not served')
68
+ @node.owns_error?(err) && err.message.start_with?('CLUSTERDOWN Hash slot not served')
70
69
  end
71
70
 
72
71
  raise
73
72
  end
74
73
 
75
74
  # @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)
75
+ def try_send(node, method, command, args, retry_count: 3, &block)
76
+ handle_redirection(node, retry_count: retry_count) do |on_node|
77
+ if args.empty?
78
+ # prevent memory allocation for variable-length args
79
+ on_node.public_send(method, command, &block)
80
+ else
81
+ on_node.public_send(method, *args, command, &block)
82
+ end
82
83
  end
83
- rescue ::RedisClient::CircuitBreaker::OpenCircuitError
84
- raise
85
- rescue ::RedisClient::CommandError => e
86
- raise if retry_count <= 0
84
+ end
87
85
 
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
86
+ def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block)
87
+ handle_redirection(node, retry_count: retry_count) do |on_node|
88
+ on_node.public_send(method, *args, **kwargs, &block)
103
89
  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
90
  end
112
91
 
113
- def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize
114
- node.public_send(method, *args, **kwargs, &block)
92
+ def handle_redirection(node, retry_count:) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
93
+ yield node
115
94
  rescue ::RedisClient::CircuitBreaker::OpenCircuitError
116
95
  raise
117
96
  rescue ::RedisClient::CommandError => e
118
- raise if retry_count <= 0
97
+ raise unless ErrorIdentification.client_owns_error?(e, node)
119
98
 
120
99
  if e.message.start_with?('MOVED')
121
100
  node = assign_redirection_node(e.message)
122
101
  retry_count -= 1
123
- retry
102
+ retry if retry_count >= 0
124
103
  elsif e.message.start_with?('ASK')
125
104
  node = assign_asking_node(e.message)
126
- node.call('ASKING')
127
105
  retry_count -= 1
128
- retry
106
+ if retry_count >= 0
107
+ node.call('ASKING')
108
+ retry
109
+ end
129
110
  elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
130
111
  update_cluster_info!
131
112
  retry_count -= 1
132
- retry
133
- else
134
- raise
113
+ retry if retry_count >= 0
135
114
  end
136
- rescue ::RedisClient::ConnectionError
137
- raise if retry_count <= 0
115
+ raise
116
+ rescue ::RedisClient::ConnectionError => e
117
+ raise unless ErrorIdentification.client_owns_error?(e, node)
138
118
 
139
119
  update_cluster_info!
120
+
121
+ raise if retry_count <= 0
122
+
140
123
  retry_count -= 1
141
124
  retry
142
125
  end
@@ -170,21 +153,30 @@ class RedisClient
170
153
  find_node(node_key)
171
154
  end
172
155
 
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)
156
+ def find_node_key_by_key(key, seed: nil, primary: false)
157
+ if key && !key.empty?
158
+ slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
159
+ primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
179
160
  else
180
- @node.find_node_key_of_replica(slot, seed: seed) || @node.any_replica_node_key(seed: seed)
161
+ primary ? @node.any_primary_node_key(seed: seed) : @node.any_replica_node_key(seed: seed)
181
162
  end
182
163
  end
183
164
 
165
+ def find_primary_node_by_slot(slot)
166
+ node_key = @node.find_node_key_of_primary(slot)
167
+ find_node(node_key)
168
+ end
169
+
170
+ def find_node_key(command, seed: nil)
171
+ key = @command.extract_first_key(command)
172
+ find_node_key_by_key(key, seed: seed, primary: @command.should_send_to_primary?(command))
173
+ end
174
+
184
175
  def find_primary_node_key(command)
185
176
  key = @command.extract_first_key(command)
186
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
187
- @node.find_node_key_of_primary(slot)
177
+ return nil unless key&.size&.> 0
178
+
179
+ find_node_key_by_key(key, primary: true)
188
180
  end
189
181
 
190
182
  def find_node(node_key, retry_count: 3)
@@ -306,34 +298,8 @@ class RedisClient
306
298
  end
307
299
  end
308
300
 
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
- )
322
- end
323
-
324
301
  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
302
+ @node.reload!
337
303
  end
338
304
  end
339
305
  end
@@ -1,56 +1,182 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis_client'
4
+ require 'redis_client/cluster/pipeline'
5
+ require 'redis_client/cluster/node_key'
4
6
 
5
7
  class RedisClient
6
8
  class Cluster
7
9
  class Transaction
8
10
  ConsistencyError = Class.new(::RedisClient::Error)
9
11
 
10
- def initialize(router, command_builder)
12
+ def initialize(router, command_builder, node = nil)
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?
14
20
  end
15
21
 
16
- def call(*command, **kwargs, &_)
22
+ def call(*command, **kwargs, &block)
17
23
  command = @command_builder.generate(command, kwargs)
18
- ensure_node_key(command)
24
+ if prepare(command)
25
+ @pipeline.call_v(command, &block)
26
+ else
27
+ defer { @pipeline.call_v(command, &block) }
28
+ end
19
29
  end
20
30
 
21
- def call_v(command, &_)
31
+ def call_v(command, &block)
22
32
  command = @command_builder.generate(command)
23
- ensure_node_key(command)
33
+ if prepare(command)
34
+ @pipeline.call_v(command, &block)
35
+ else
36
+ defer { @pipeline.call_v(command, &block) }
37
+ end
24
38
  end
25
39
 
26
- def call_once(*command, **kwargs, &_)
40
+ def call_once(*command, **kwargs, &block)
41
+ @retryable = false
27
42
  command = @command_builder.generate(command, kwargs)
28
- ensure_node_key(command)
43
+ if prepare(command)
44
+ @pipeline.call_once_v(command, &block)
45
+ else
46
+ defer { @pipeline.call_once_v(command, &block) }
47
+ end
29
48
  end
30
49
 
31
- def call_once_v(command, &_)
50
+ def call_once_v(command, &block)
51
+ @retryable = false
32
52
  command = @command_builder.generate(command)
33
- ensure_node_key(command)
53
+ if prepare(command)
54
+ @pipeline.call_once_v(command, &block)
55
+ else
56
+ defer { @pipeline.call_once_v(command, &block) }
57
+ end
34
58
  end
35
59
 
36
- def execute(watch: nil, &block)
37
- yield self
38
- raise ArgumentError, 'empty transaction' if @node_key.nil?
60
+ def execute
61
+ @pending_commands.each(&:call)
39
62
 
40
- node = @router.find_node(@node_key)
41
- @router.try_delegate(node, :multi, watch: watch, &block)
63
+ raise ArgumentError, 'empty transaction' if @pipeline._empty?
64
+ raise ConsistencyError, "couldn't determine the node: #{@pipeline._commands}" if @node.nil?
65
+
66
+ settle
42
67
  end
43
68
 
44
69
  private
45
70
 
46
- def ensure_node_key(command)
71
+ def defer(&block)
72
+ @pending_commands << block
73
+ nil
74
+ end
75
+
76
+ def prepare(command)
77
+ return true unless @node.nil?
78
+
47
79
  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?
80
+ return false if node_key.nil?
49
81
 
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
82
+ @node = @router.find_node(node_key)
83
+ prepare_tx
84
+ true
85
+ end
52
86
 
53
- nil
87
+ def prepare_tx
88
+ @pipeline.call('MULTI')
89
+ @pending_commands.each(&:call)
90
+ @pending_commands.clear
91
+ end
92
+
93
+ def settle
94
+ @pipeline.call('EXEC')
95
+ send_transaction(@node, redirect: true)
96
+ end
97
+
98
+ def send_transaction(client, redirect:)
99
+ case client
100
+ when ::RedisClient then send_pipeline(client, redirect: redirect)
101
+ when ::RedisClient::Pooled then client.with { |c| send_pipeline(c, redirect: redirect) }
102
+ else raise NotImplementedError, "#{client.class.name}#multi for cluster client"
103
+ end
104
+ end
105
+
106
+ def send_pipeline(client, redirect:)
107
+ replies = client.ensure_connected_cluster_scoped(retryable: @retryable) do |connection|
108
+ commands = @pipeline._commands
109
+ client.middlewares.call_pipelined(commands, client.config) do
110
+ connection.call_pipelined(commands, nil)
111
+ rescue ::RedisClient::CommandError => e
112
+ return handle_command_error!(commands, e) if redirect
113
+
114
+ raise
115
+ end
116
+ end
117
+
118
+ return if replies.last.nil?
119
+
120
+ coerce_results!(replies.last)
121
+ end
122
+
123
+ def coerce_results!(results, offset: 1)
124
+ results.each_with_index do |result, index|
125
+ if result.is_a?(::RedisClient::CommandError)
126
+ result._set_command(@pipeline._commands[index + offset])
127
+ raise result
128
+ end
129
+
130
+ next if @pipeline._blocks.nil?
131
+
132
+ block = @pipeline._blocks[index + offset]
133
+ next if block.nil?
134
+
135
+ results[index] = block.call(result)
136
+ end
137
+
138
+ results
139
+ end
140
+
141
+ def handle_command_error!(commands, err)
142
+ if err.message.start_with?('CROSSSLOT')
143
+ raise ConsistencyError, "#{err.message}: #{err.command}"
144
+ elsif err.message.start_with?('MOVED', 'ASK')
145
+ ensure_the_same_node!(commands)
146
+ handle_redirection(err)
147
+ else
148
+ raise err
149
+ end
150
+ end
151
+
152
+ def ensure_the_same_node!(commands)
153
+ expected_node_key = NodeKey.build_from_client(@node)
154
+
155
+ commands.each do |command|
156
+ node_key = @router.find_primary_node_key(command)
157
+ next if node_key.nil?
158
+ next if node_key == expected_node_key
159
+
160
+ raise ConsistencyError, "the transaction should be executed to a slot in a node: #{commands}"
161
+ end
162
+ end
163
+
164
+ def handle_redirection(err)
165
+ if err.message.start_with?('MOVED')
166
+ node = @router.assign_redirection_node(err.message)
167
+ send_transaction(node, redirect: false)
168
+ elsif err.message.start_with?('ASK')
169
+ node = @router.assign_asking_node(err.message)
170
+ try_asking(node) ? send_transaction(node, redirect: false) : err
171
+ else
172
+ raise err
173
+ end
174
+ end
175
+
176
+ def try_asking(node)
177
+ node.call('ASKING') == 'OK'
178
+ rescue StandardError
179
+ false
54
180
  end
55
181
  end
56
182
  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
@@ -89,14 +91,34 @@ class RedisClient
89
91
  pipeline.execute
90
92
  end
91
93
 
92
- def multi(watch: nil, &block)
93
- ::RedisClient::Cluster::Transaction.new(@router, @command_builder).execute(watch: watch, &block)
94
+ def multi(watch: nil)
95
+ if watch.nil? || watch.empty?
96
+ transaction = ::RedisClient::Cluster::Transaction.new(@router, @command_builder)
97
+ yield transaction
98
+ transaction.execute
99
+ else
100
+ locking = ::RedisClient::Cluster::OptimisticLocking.new(watch, @router)
101
+ locking.watch do |c|
102
+ transaction = ::RedisClient::Cluster::Transaction.new(@router, @command_builder, c)
103
+ yield transaction
104
+ transaction.execute
105
+ end
106
+ end
94
107
  end
95
108
 
96
109
  def pubsub
97
110
  ::RedisClient::Cluster::PubSub.new(@router, @command_builder)
98
111
  end
99
112
 
113
+ # TODO: This isn't an official public interface yet. Don't use in your production environment.
114
+ # @see https://github.com/redis-rb/redis-cluster-client/issues/299
115
+ def with(key: nil, hashtag: nil, write: true)
116
+ key = process_with_arguments(key, hashtag)
117
+ node_key = @router.find_node_key_by_key(key, primary: write)
118
+ node = @router.find_node(node_key)
119
+ node.with { |c| yield ::RedisClient::Cluster::PinningNode.new(c) }
120
+ end
121
+
100
122
  def close
101
123
  @concurrent_worker.close
102
124
  @router.close
@@ -105,6 +127,19 @@ class RedisClient
105
127
 
106
128
  private
107
129
 
130
+ def process_with_arguments(key, hashtag) # rubocop:disable Metrics/CyclomaticComplexity
131
+ raise ArgumentError, 'Only one of key or hashtag may be provided' if key && hashtag
132
+
133
+ if hashtag
134
+ # The documentation says not to wrap your hashtag in {}, but people will probably
135
+ # do it anyway and it's easy for us to fix here.
136
+ key = hashtag&.match?(/^{.*}$/) ? hashtag : "{#{hashtag}}"
137
+ end
138
+ raise ArgumentError, 'One of key or hashtag must be provided' if key.nil? || key.empty?
139
+
140
+ key
141
+ end
142
+
108
143
  def method_missing(name, *args, **kwargs, &block)
109
144
  if @router.command_exists?(name)
110
145
  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
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.7.5
4
+ version: 0.7.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-10-20 00:00:00.000000000 Z
11
+ date: 2024-02-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -38,16 +38,19 @@ files:
38
38
  - lib/redis_client/cluster/concurrent_worker/none.rb
39
39
  - lib/redis_client/cluster/concurrent_worker/on_demand.rb
40
40
  - lib/redis_client/cluster/concurrent_worker/pooled.rb
41
+ - lib/redis_client/cluster/error_identification.rb
41
42
  - lib/redis_client/cluster/errors.rb
42
43
  - lib/redis_client/cluster/key_slot_converter.rb
43
44
  - lib/redis_client/cluster/node.rb
45
+ - lib/redis_client/cluster/node/base_topology.rb
44
46
  - lib/redis_client/cluster/node/latency_replica.rb
45
47
  - lib/redis_client/cluster/node/primary_only.rb
46
48
  - lib/redis_client/cluster/node/random_replica.rb
47
49
  - lib/redis_client/cluster/node/random_replica_or_primary.rb
48
- - lib/redis_client/cluster/node/replica_mixin.rb
49
50
  - lib/redis_client/cluster/node_key.rb
50
51
  - lib/redis_client/cluster/normalized_cmd_name.rb
52
+ - lib/redis_client/cluster/optimistic_locking.rb
53
+ - lib/redis_client/cluster/pinning_node.rb
51
54
  - lib/redis_client/cluster/pipeline.rb
52
55
  - lib/redis_client/cluster/pub_sub.rb
53
56
  - lib/redis_client/cluster/router.rb
@@ -75,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
78
  - !ruby/object:Gem::Version
76
79
  version: '0'
77
80
  requirements: []
78
- rubygems_version: 3.4.19
81
+ rubygems_version: 3.5.3
79
82
  signing_key:
80
83
  specification_version: 4
81
84
  summary: A Redis cluster client for Ruby
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class RedisClient
4
- class Cluster
5
- class Node
6
- module ReplicaMixin
7
- attr_reader :clients, :primary_clients
8
-
9
- EMPTY_ARRAY = [].freeze
10
-
11
- def initialize(replications, options, pool, _concurrent_worker, **kwargs)
12
- @replications = replications
13
- @primary_node_keys = @replications.keys.sort
14
- @replica_node_keys = @replications.values.flatten.sort
15
- @clients = build_clients(@primary_node_keys, options, pool, **kwargs)
16
- @primary_clients = @clients.select { |k, _| @primary_node_keys.include?(k) }
17
- end
18
-
19
- def any_primary_node_key(seed: nil)
20
- random = seed.nil? ? Random : Random.new(seed)
21
- @primary_node_keys.sample(random: random)
22
- end
23
-
24
- private
25
-
26
- def build_clients(primary_node_keys, options, pool, **kwargs)
27
- options.to_h do |node_key, option|
28
- option = option.merge(kwargs.reject { |k, _| ::RedisClient::Cluster::Node::IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
29
- config = ::RedisClient::Cluster::Node::Config.new(scale_read: !primary_node_keys.include?(node_key), **option)
30
- client = pool.nil? ? config.new_client : config.new_pool(**pool)
31
- [node_key, client]
32
- end
33
- end
34
- end
35
- end
36
- end
37
- end