redis-cluster-client 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 367df4e02bcd425f14da73b2ff243dfdb7fd44c6cb565bcc46952b939057904f
4
- data.tar.gz: bfdb8033d0695fe0389563ad563c8445f9624e99ef6315e7374ee7f2e440fe40
3
+ metadata.gz: 619becbe41f6cb5c829de9df021a399ac966b1976284e06e7792c0b570061e32
4
+ data.tar.gz: 5b6ddc1c8410bcf8ab03538bf5dd77d0425d4bf34f170ea026ff4d1c1be2ce8e
5
5
  SHA512:
6
- metadata.gz: 278740e8829a1def72a411552ed7c17d0d265930c81dfc2b967cfa325a74cdbdbd0adb832dacc77e4c86c8316648695b94044cb6fdf3dd3267617d5f18b95ec1
7
- data.tar.gz: 007bf587198b7575dabee128c0500700ef0d10c7b699a632023ce4d80fa788d2a88e851748f041ab5f6453350d7de6f9d8c96bcbc798deec7acbf37b2700ea6a
6
+ metadata.gz: 658a78bdd61f7e902b2385c73f23f3fa692a73b77a073afc0cb3d0616726a99cb65884ec4ab7cbae2bdd4574444f1de2f639105eaa4f40e1c07c186d7589dff5
7
+ data.tar.gz: 33b9d88cc33fec6f5a556ae3beb494c9875796088b93a47b647efe02db6f41540d49f3120d5d7114c8757aa78e29b173abd20a8b2a2341861e953a0ab4b0f67a
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_cluster_client'
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_client/cluster/node/replica_mixin'
4
+
5
+ class RedisClient
6
+ class Cluster
7
+ class Node
8
+ class LatencyReplica
9
+ include ::RedisClient::Cluster::Node::ReplicaMixin
10
+
11
+ attr_reader :replica_clients
12
+
13
+ DUMMY_LATENCY_NSEC = 100 * 1000 * 1000 * 1000
14
+ MEASURE_ATTEMPT_COUNT = 10
15
+
16
+ def initialize(replications, options, pool, **kwargs)
17
+ super
18
+
19
+ all_replica_clients = @clients.select { |k, _| @replica_node_keys.include?(k) }
20
+ latencies = measure_latencies(all_replica_clients)
21
+ @replications.each_value { |keys| keys.sort_by! { |k| latencies.fetch(k) } }
22
+ @replica_clients = select_replica_clients(@replications, @clients)
23
+ @clients_for_scanning = select_clients_for_scanning(@replications, @clients)
24
+ end
25
+
26
+ def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
27
+ @clients_for_scanning
28
+ end
29
+
30
+ def find_node_key_of_replica(primary_node_key, seed: nil) # rubocop:disable Lint/UnusedMethodArgument
31
+ @replications.fetch(primary_node_key, EMPTY_ARRAY).first || primary_node_key
32
+ end
33
+
34
+ def any_replica_node_key(seed: nil)
35
+ random = seed.nil? ? Random : Random.new(seed)
36
+ @replications.reject { |_, v| v.empty? }.values.sample(random: random).first
37
+ end
38
+
39
+ private
40
+
41
+ def measure_latencies(clients) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
42
+ latencies = {}
43
+
44
+ clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS) do |chuncked_clients|
45
+ threads = chuncked_clients.map do |k, v|
46
+ Thread.new(k, v) do |node_key, client|
47
+ Thread.pass
48
+
49
+ min = DUMMY_LATENCY_NSEC
50
+ MEASURE_ATTEMPT_COUNT.times do
51
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
52
+ client.send(:call_once, 'PING')
53
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - starting
54
+ min = duration if duration < min
55
+ end
56
+
57
+ latencies[node_key] = min
58
+ rescue StandardError
59
+ latencies[node_key] = DUMMY_LATENCY_NSEC
60
+ end
61
+ end
62
+
63
+ threads.each(&:join)
64
+ end
65
+
66
+ latencies
67
+ end
68
+
69
+ def select_replica_clients(replications, clients)
70
+ keys = replications.values.filter_map(&:first)
71
+ clients.select { |k, _| keys.include?(k) }
72
+ end
73
+
74
+ def select_clients_for_scanning(replications, clients)
75
+ keys = replications.map do |primary_node_key, replica_node_keys|
76
+ replica_node_keys.empty? ? primary_node_key : replica_node_keys.first
77
+ end
78
+
79
+ clients.select { |k, _| keys.include?(k) }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisClient
4
+ class Cluster
5
+ class Node
6
+ class PrimaryOnly
7
+ attr_reader :clients
8
+
9
+ def initialize(replications, options, pool, **kwargs)
10
+ @primary_node_keys = replications.keys.sort
11
+ @clients = build_clients(@primary_node_keys, options, pool, **kwargs)
12
+ end
13
+
14
+ alias primary_clients clients
15
+ alias replica_clients clients
16
+
17
+ def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
18
+ @clients
19
+ end
20
+
21
+ def find_node_key_of_replica(primary_node_key, seed: nil) # rubocop:disable Lint/UnusedMethodArgument
22
+ primary_node_key
23
+ end
24
+
25
+ def any_primary_node_key(seed: nil)
26
+ random = seed.nil? ? Random : Random.new(seed)
27
+ @primary_node_keys.sample(random: random)
28
+ end
29
+
30
+ alias any_replica_node_key any_primary_node_key
31
+
32
+ private
33
+
34
+ def build_clients(primary_node_keys, options, pool, **kwargs)
35
+ options.filter_map do |node_key, option|
36
+ next if !primary_node_keys.empty? && !primary_node_keys.include?(node_key)
37
+
38
+ option = option.merge(kwargs.reject { |k, _| ::RedisClient::Cluster::Node::IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
39
+ config = ::RedisClient::Cluster::Node::Config.new(**option)
40
+ client = pool.nil? ? config.new_client : config.new_pool(**pool)
41
+ [node_key, client]
42
+ end.to_h
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_client/cluster/node/replica_mixin'
4
+
5
+ class RedisClient
6
+ class Cluster
7
+ class Node
8
+ class RandomReplica
9
+ include ::RedisClient::Cluster::Node::ReplicaMixin
10
+
11
+ def replica_clients
12
+ keys = @replications.values.filter_map(&:sample)
13
+ @clients.select { |k, _| keys.include?(k) }
14
+ end
15
+
16
+ def clients_for_scanning(seed: nil)
17
+ random = seed.nil? ? Random : Random.new(seed)
18
+ keys = @replications.map do |primary_node_key, replica_node_keys|
19
+ replica_node_keys.empty? ? primary_node_key : replica_node_keys.sample(random: random)
20
+ end
21
+
22
+ clients.select { |k, _| keys.include?(k) }
23
+ end
24
+
25
+ def find_node_key_of_replica(primary_node_key, seed: nil)
26
+ random = seed.nil? ? Random : Random.new(seed)
27
+ @replications.fetch(primary_node_key, EMPTY_ARRAY).sample(random: random) || primary_node_key
28
+ end
29
+
30
+ def any_replica_node_key(seed: nil)
31
+ random = seed.nil? ? Random : Random.new(seed)
32
+ @replica_node_keys.sample(random: random)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,37 @@
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, **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.filter_map 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.to_h
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -3,6 +3,9 @@
3
3
  require 'redis_client'
4
4
  require 'redis_client/config'
5
5
  require 'redis_client/cluster/errors'
6
+ require 'redis_client/cluster/node/primary_only'
7
+ require 'redis_client/cluster/node/random_replica'
8
+ require 'redis_client/cluster/node/latency_replica'
6
9
 
7
10
  class RedisClient
8
11
  class Cluster
@@ -13,6 +16,7 @@ class RedisClient
13
16
  MIN_SLOT = 0
14
17
  MAX_SLOT = SLOT_SIZE - 1
15
18
  MAX_STARTUP_SAMPLE = 37
19
+ MAX_THREADS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
16
20
  IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
17
21
 
18
22
  ReloadNeeded = Class.new(::RedisClient::Error)
@@ -39,18 +43,22 @@ class RedisClient
39
43
  errors = Array.new(startup_size)
40
44
  startup_options = options.to_a.sample(MAX_STARTUP_SAMPLE).to_h
41
45
  startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, **kwargs)
42
- threads = startup_nodes.each_with_index.map do |raw_client, idx|
43
- Thread.new(raw_client, idx) do |cli, i|
44
- Thread.pass
45
- reply = cli.call('CLUSTER', 'NODES')
46
- node_info_list[i] = parse_node_info(reply)
47
- rescue StandardError => e
48
- errors[i] = e
49
- ensure
50
- cli&.close
46
+ startup_nodes.each_slice(MAX_THREADS).with_index do |chuncked_startup_nodes, chuncked_idx|
47
+ threads = chuncked_startup_nodes.each_with_index.map do |raw_client, idx|
48
+ Thread.new(raw_client, (MAX_THREADS * chuncked_idx) + idx) do |cli, i|
49
+ Thread.pass
50
+ reply = cli.call('CLUSTER', 'NODES')
51
+ node_info_list[i] = parse_node_info(reply)
52
+ rescue StandardError => e
53
+ errors[i] = e
54
+ ensure
55
+ cli&.close
56
+ end
51
57
  end
58
+
59
+ threads.each(&:join)
52
60
  end
53
- threads.each(&:join)
61
+
54
62
  raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.all?(&:nil?)
55
63
 
56
64
  grouped = node_info_list.compact.group_by do |rows|
@@ -88,11 +96,18 @@ class RedisClient
88
96
  end
89
97
  end
90
98
 
91
- def initialize(options, node_info: [], pool: nil, with_replica: false, **kwargs)
92
- @with_replica = with_replica
99
+ def initialize( # rubocop:disable Metrics/ParameterLists
100
+ options,
101
+ node_info: [],
102
+ with_replica: false,
103
+ replica_affinity: :random,
104
+ pool: nil,
105
+ **kwargs
106
+ )
107
+
93
108
  @slots = build_slot_node_mappings(node_info)
94
109
  @replications = build_replication_mappings(node_info)
95
- @clients = build_clients(options, pool: pool, **kwargs)
110
+ @topology = make_topology_class(with_replica, replica_affinity).new(@replications, options, pool, **kwargs)
96
111
  @mutex = Mutex.new
97
112
  end
98
113
 
@@ -101,87 +116,46 @@ class RedisClient
101
116
  end
102
117
 
103
118
  def each(&block)
104
- @clients.each_value(&block)
119
+ @topology.clients.each_value(&block)
105
120
  end
106
121
 
107
122
  def sample
108
- @clients.values.sample
123
+ @topology.clients.values.sample
109
124
  end
110
125
 
111
126
  def node_keys
112
- @clients.keys.sort
113
- end
114
-
115
- def primary_node_keys
116
- @clients.filter_map { |k, _| primary?(k) ? k : nil }.sort
117
- end
118
-
119
- def replica_node_keys
120
- return primary_node_keys if replica_disabled?
121
-
122
- @clients.filter_map { |k, _| replica?(k) ? k : nil }.sort
127
+ @topology.clients.keys.sort
123
128
  end
124
129
 
125
130
  def find_by(node_key)
126
- raise ReloadNeeded if node_key.nil? || !@clients.key?(node_key)
131
+ raise ReloadNeeded if node_key.nil? || !@topology.clients.key?(node_key)
127
132
 
128
- @clients.fetch(node_key)
133
+ @topology.clients.fetch(node_key)
129
134
  end
130
135
 
131
- def call_all(method, *args, **kwargs, &block)
132
- results, errors = try_map do |_, client|
133
- client.send(method, *args, **kwargs, &block)
134
- end
135
-
136
- return results.values if errors.empty?
137
-
138
- raise ::RedisClient::Cluster::ErrorCollection, errors
136
+ def call_all(method, command, args, &block)
137
+ call_multiple_nodes!(@topology.clients, method, command, args, &block)
139
138
  end
140
139
 
141
- def call_primaries(method, *args, **kwargs, &block)
142
- results, errors = try_map do |node_key, client|
143
- next if replica?(node_key)
144
-
145
- client.send(method, *args, **kwargs, &block)
146
- end
147
-
148
- return results.values if errors.empty?
149
-
150
- raise ::RedisClient::Cluster::ErrorCollection, errors
140
+ def call_primaries(method, command, args, &block)
141
+ call_multiple_nodes!(@topology.primary_clients, method, command, args, &block)
151
142
  end
152
143
 
153
- def call_replicas(method, *args, **kwargs, &block)
154
- return call_primaries(method, *args, **kwargs, &block) if replica_disabled?
155
-
156
- replica_node_keys = @replications.values.map(&:sample)
157
- results, errors = try_map do |node_key, client|
158
- next if primary?(node_key) || !replica_node_keys.include?(node_key)
159
-
160
- client.send(method, *args, **kwargs, &block)
161
- end
162
-
163
- return results.values if errors.empty?
164
-
165
- raise ::RedisClient::Cluster::ErrorCollection, errors
144
+ def call_replicas(method, command, args, &block)
145
+ call_multiple_nodes!(@topology.replica_clients, method, command, args, &block)
166
146
  end
167
147
 
168
- def send_ping(method, *args, **kwargs, &block)
169
- results, errors = try_map do |_, client|
170
- client.send(method, *args, **kwargs, &block)
171
- end
172
-
173
- return results.values if errors.empty?
148
+ def send_ping(method, command, args, &block)
149
+ result_values, errors = call_multiple_nodes(@topology.clients, method, command, args, &block)
150
+ return result_values if errors.empty?
174
151
 
175
152
  raise ReloadNeeded if errors.values.any?(::RedisClient::ConnectionError)
176
153
 
177
154
  raise ::RedisClient::Cluster::ErrorCollection, errors
178
155
  end
179
156
 
180
- def scale_reading_clients
181
- keys = replica_disabled? ? @replications.keys : @replications.values.map(&:first)
182
- @clients.select { |k, _| keys.include?(k) }.values.sort_by do |client|
183
- "#{client.config.host}-#{client.config.port}"
184
- end
157
+ def clients_for_scanning(seed: nil)
158
+ @topology.clients_for_scanning(seed: seed).values.sort_by { |c| "#{c.config.host}-#{c.config.port}" }
185
159
  end
186
160
 
187
161
  def find_node_key_of_primary(slot)
@@ -193,41 +167,33 @@ class RedisClient
193
167
  @slots[slot]
194
168
  end
195
169
 
196
- def find_node_key_of_replica(slot)
197
- return if slot.nil?
198
-
199
- slot = Integer(slot)
200
- return if slot < MIN_SLOT || slot > MAX_SLOT
170
+ def find_node_key_of_replica(slot, seed: nil)
171
+ primary_node_key = find_node_key_of_primary(slot)
172
+ @topology.find_node_key_of_replica(primary_node_key, seed: seed)
173
+ end
201
174
 
202
- return @slots[slot] if replica_disabled? || @replications[@slots[slot]].size.zero?
175
+ def any_primary_node_key(seed: nil)
176
+ @topology.any_primary_node_key(seed: seed)
177
+ end
203
178
 
204
- @replications[@slots[slot]].sample
179
+ def any_replica_node_key(seed: nil)
180
+ @topology.any_replica_node_key(seed: seed)
205
181
  end
206
182
 
207
183
  def update_slot(slot, node_key)
208
184
  @mutex.synchronize { @slots[slot] = node_key }
209
185
  end
210
186
 
211
- def replicated?(primary_node_key, replica_node_key)
212
- return false if @replications.nil? || @replications.size.zero?
213
-
214
- @replications.fetch(primary_node_key).include?(replica_node_key)
215
- end
216
-
217
187
  private
218
188
 
219
- def replica_disabled?
220
- !@with_replica
221
- end
222
-
223
- def primary?(node_key)
224
- !replica?(node_key)
225
- end
226
-
227
- def replica?(node_key)
228
- return false if @replications.nil? || @replications.size.zero?
229
-
230
- !@replications.key?(node_key)
189
+ def make_topology_class(with_replica, replica_affinity)
190
+ if with_replica && replica_affinity == :random
191
+ ::RedisClient::Cluster::Node::RandomReplica
192
+ elsif with_replica && replica_affinity == :latency
193
+ ::RedisClient::Cluster::Node::LatencyReplica
194
+ else
195
+ ::RedisClient::Cluster::Node::PrimaryOnly
196
+ end
231
197
  end
232
198
 
233
199
  def build_slot_node_mappings(node_info)
@@ -250,34 +216,38 @@ class RedisClient
250
216
  end
251
217
  end
252
218
 
253
- def build_clients(options, pool: nil, **kwargs)
254
- options.filter_map do |node_key, option|
255
- next if replica_disabled? && replica?(node_key)
219
+ def call_multiple_nodes(clients, method, command, args, &block)
220
+ results, errors = try_map(clients) do |_, client|
221
+ client.send(method, *args, command, &block)
222
+ end
223
+
224
+ [results.values, errors]
225
+ end
256
226
 
257
- config = ::RedisClient::Cluster::Node::Config.new(
258
- scale_read: replica?(node_key),
259
- **option.merge(kwargs.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
260
- )
261
- client = pool.nil? ? config.new_client : config.new_pool(**pool)
227
+ def call_multiple_nodes!(clients, method, command, args, &block)
228
+ result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
229
+ return result_values if errors.empty?
262
230
 
263
- [node_key, client]
264
- end.to_h
231
+ raise ::RedisClient::Cluster::ErrorCollection, errors
265
232
  end
266
233
 
267
- def try_map # rubocop:disable Metrics/MethodLength
234
+ def try_map(clients) # rubocop:disable Metrics/MethodLength
268
235
  results = {}
269
236
  errors = {}
270
- threads = @clients.map do |k, v|
271
- Thread.new(k, v) do |node_key, client|
272
- Thread.pass
273
- reply = yield(node_key, client)
274
- results[node_key] = reply unless reply.nil?
275
- rescue StandardError => e
276
- errors[node_key] = e
237
+ clients.each_slice(MAX_THREADS) do |chuncked_clients|
238
+ threads = chuncked_clients.map do |k, v|
239
+ Thread.new(k, v) do |node_key, client|
240
+ Thread.pass
241
+ reply = yield(node_key, client)
242
+ results[node_key] = reply unless reply.nil?
243
+ rescue StandardError => e
244
+ errors[node_key] = e
245
+ end
277
246
  end
247
+
248
+ threads.each(&:join)
278
249
  end
279
250
 
280
- threads.each(&:join)
281
251
  [results, errors]
282
252
  end
283
253
  end
@@ -7,28 +7,55 @@ class RedisClient
7
7
  class Cluster
8
8
  class Pipeline
9
9
  ReplySizeError = Class.new(::RedisClient::Error)
10
+ MAX_THREADS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
10
11
 
11
- def initialize(router)
12
+ def initialize(router, command_builder)
12
13
  @router = router
14
+ @command_builder = command_builder
13
15
  @grouped = Hash.new([].freeze)
14
16
  @size = 0
17
+ @seed = Random.new_seed
15
18
  end
16
19
 
17
- def call(*command, **kwargs)
18
- node_key = @router.find_node_key(*command, primary_only: true, **kwargs)
19
- @grouped[node_key] += [[@size, :call, command, kwargs]]
20
+ def call(*args, **kwargs, &block)
21
+ command = @command_builder.generate(args, kwargs)
22
+ node_key = @router.find_node_key(command, seed: @seed)
23
+ @grouped[node_key] += [[@size, :call_v, command, block]]
20
24
  @size += 1
21
25
  end
22
26
 
23
- def call_once(*command, **kwargs)
24
- node_key = @router.find_node_key(*command, primary_only: true, **kwargs)
25
- @grouped[node_key] += [[@size, :call_once, command, kwargs]]
27
+ def call_v(args, &block)
28
+ command = @command_builder.generate(args)
29
+ node_key = @router.find_node_key(command, seed: @seed)
30
+ @grouped[node_key] += [[@size, :call_v, command, block]]
26
31
  @size += 1
27
32
  end
28
33
 
29
- def blocking_call(timeout, *command, **kwargs)
30
- node_key = @router.find_node_key(*command, primary_only: true, **kwargs)
31
- @grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
34
+ def call_once(*args, **kwargs, &block)
35
+ command = @command_builder.generate(args, kwargs)
36
+ node_key = @router.find_node_key(command, seed: @seed)
37
+ @grouped[node_key] += [[@size, :call_once_v, command, block]]
38
+ @size += 1
39
+ end
40
+
41
+ def call_once_v(args, &block)
42
+ command = @command_builder.generate(args)
43
+ node_key = @router.find_node_key(command, seed: @seed)
44
+ @grouped[node_key] += [[@size, :call_once_v, command, block]]
45
+ @size += 1
46
+ end
47
+
48
+ def blocking_call(timeout, *args, **kwargs, &block)
49
+ command = @command_builder.generate(args, kwargs)
50
+ node_key = @router.find_node_key(command, seed: @seed)
51
+ @grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
52
+ @size += 1
53
+ end
54
+
55
+ def blocking_call_v(timeout, args, &block)
56
+ command = @command_builder.generate(args)
57
+ node_key = @router.find_node_key(command, seed: @seed)
58
+ @grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
32
59
  @size += 1
33
60
  end
34
61
 
@@ -40,29 +67,27 @@ class RedisClient
40
67
  def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
41
68
  all_replies = Array.new(@size)
42
69
  errors = {}
43
- threads = @grouped.map do |k, v|
44
- Thread.new(@router, k, v) do |router, node_key, rows|
45
- Thread.pass
46
- replies = router.find_node(node_key).pipelined do |pipeline|
47
- rows.each do |row|
48
- case row[1]
49
- when :call then pipeline.call(*row[2], **row[3])
50
- when :call_once then pipeline.call_once(*row[2], **row[3])
51
- when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
52
- else raise NotImplementedError, row[1]
70
+ @grouped.each_slice(MAX_THREADS) do |chuncked_grouped|
71
+ threads = chuncked_grouped.map do |k, v|
72
+ Thread.new(@router, k, v) do |router, node_key, rows|
73
+ Thread.pass
74
+ replies = router.find_node(node_key).pipelined do |pipeline|
75
+ rows.each do |(_size, *row, block)|
76
+ pipeline.send(*row, &block)
53
77
  end
54
78
  end
55
- end
56
79
 
57
- raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
80
+ raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
58
81
 
59
- rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
60
- rescue StandardError => e
61
- errors[node_key] = e
82
+ rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
83
+ rescue StandardError => e
84
+ errors[node_key] = e
85
+ end
62
86
  end
87
+
88
+ threads.each(&:join)
63
89
  end
64
90
 
65
- threads.each(&:join)
66
91
  return all_replies if errors.empty?
67
92
 
68
93
  raise ::RedisClient::Cluster::ErrorCollection, errors
@@ -3,15 +3,24 @@
3
3
  class RedisClient
4
4
  class Cluster
5
5
  class PubSub
6
- def initialize(router)
6
+ def initialize(router, command_builder)
7
7
  @router = router
8
+ @command_builder = command_builder
8
9
  @pubsub = nil
9
10
  end
10
11
 
11
- def call(*command, **kwargs)
12
+ def call(*args, **kwargs)
12
13
  close
13
- @pubsub = @router.assign_node(*command, **kwargs).pubsub
14
- @pubsub.call(*command, **kwargs)
14
+ command = @command_builder.generate(args, kwargs)
15
+ @pubsub = @router.assign_node(command).pubsub
16
+ @pubsub.call_v(command)
17
+ end
18
+
19
+ def call_v(command)
20
+ close
21
+ command = @command_builder.generate(command)
22
+ @pubsub = @router.assign_node(command).pubsub
23
+ @pubsub.call_v(command)
15
24
  end
16
25
 
17
26
  def close
@@ -20,40 +20,37 @@ class RedisClient
20
20
  @client_kwargs = kwargs
21
21
  @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
22
22
  @command = ::RedisClient::Cluster::Command.load(@node)
23
- @command_builder = @config.command_builder
24
23
  @mutex = Mutex.new
24
+ @command_builder = @config.command_builder
25
25
  end
26
26
 
27
- def send_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
28
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
29
- command = @command_builder.generate!(command, kwargs)
30
-
27
+ def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
31
28
  cmd = command.first.to_s.downcase
32
29
  case cmd
33
30
  when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
34
- @node.call_all(method, *args, **kwargs, &block).first
31
+ @node.call_all(method, command, args, &block).first
35
32
  when 'flushall', 'flushdb'
36
- @node.call_primaries(method, *args, **kwargs, &block).first
37
- when 'ping' then @node.send_ping(method, *args, **kwargs, &block).first
38
- when 'wait' then send_wait_command(method, *args, **kwargs, &block)
39
- when 'keys' then @node.call_replicas(method, *args, **kwargs, &block).flatten.sort_by(&:to_s)
40
- when 'dbsize' then @node.call_replicas(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
41
- when 'scan' then scan(*command, **kwargs)
42
- when 'lastsave' then @node.call_all(method, *args, **kwargs, &block).sort_by(&:to_i)
43
- when 'role' then @node.call_all(method, *args, **kwargs, &block)
44
- when 'config' then send_config_command(method, *args, **kwargs, &block)
45
- when 'client' then send_client_command(method, *args, **kwargs, &block)
46
- when 'cluster' then send_cluster_command(method, *args, **kwargs, &block)
33
+ @node.call_primaries(method, command, args, &block).first
34
+ when 'ping' then @node.send_ping(method, command, args, &block).first
35
+ when 'wait' then send_wait_command(method, command, args, &block)
36
+ when 'keys' then @node.call_replicas(method, command, args, &block).flatten.sort_by(&:to_s)
37
+ when 'dbsize' then @node.call_replicas(method, command, args, &block).select { |e| e.is_a?(Integer) }.sum
38
+ when 'scan' then scan(command, seed: 1)
39
+ when 'lastsave' then @node.call_all(method, command, args, &block).sort_by(&:to_i)
40
+ when 'role' then @node.call_all(method, command, args, &block)
41
+ when 'config' then send_config_command(method, command, args, &block)
42
+ when 'client' then send_client_command(method, command, args, &block)
43
+ when 'cluster' then send_cluster_command(method, command, args, &block)
47
44
  when 'readonly', 'readwrite', 'shutdown'
48
45
  raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
49
- when 'memory' then send_memory_command(method, *args, **kwargs, &block)
50
- when 'script' then send_script_command(method, *args, **kwargs, &block)
51
- when 'pubsub' then send_pubsub_command(method, *args, **kwargs, &block)
46
+ when 'memory' then send_memory_command(method, command, args, &block)
47
+ when 'script' then send_script_command(method, command, args, &block)
48
+ when 'pubsub' then send_pubsub_command(method, command, args, &block)
52
49
  when 'discard', 'exec', 'multi', 'unwatch'
53
50
  raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
54
51
  else
55
- node = assign_node(*command)
56
- try_send(node, method, *args, **kwargs, &block)
52
+ node = assign_node(command)
53
+ try_send(node, method, command, args, &block)
57
54
  end
58
55
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
59
56
  update_cluster_info!
@@ -67,7 +64,37 @@ class RedisClient
67
64
 
68
65
  # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
69
66
  # Redirection and resharding
70
- def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
67
+ def try_send(node, method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
68
+ node.send(method, *args, command, &block)
69
+ rescue ::RedisClient::CommandError => e
70
+ raise if retry_count <= 0
71
+
72
+ if e.message.start_with?('MOVED')
73
+ node = assign_redirection_node(e.message)
74
+ retry_count -= 1
75
+ retry
76
+ elsif e.message.start_with?('ASK')
77
+ node = assign_asking_node(e.message)
78
+ node.call('ASKING')
79
+ retry_count -= 1
80
+ retry
81
+ elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
82
+ update_cluster_info!
83
+ retry_count -= 1
84
+ retry
85
+ else
86
+ raise
87
+ end
88
+ rescue ::RedisClient::ConnectionError => e
89
+ raise if method == :blocking_call_v || (method == :blocking_call && e.is_a?(RedisClient::ReadTimeoutError))
90
+ raise if retry_count <= 0
91
+
92
+ update_cluster_info!
93
+ retry_count -= 1
94
+ retry
95
+ end
96
+
97
+ def try_delegate(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
71
98
  node.send(method, *args, **kwargs, &block)
72
99
  rescue ::RedisClient::CommandError => e
73
100
  raise if retry_count <= 0
@@ -96,8 +123,8 @@ class RedisClient
96
123
  retry
97
124
  end
98
125
 
99
- def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
100
- command = @command_builder.generate!(command, kwargs)
126
+ def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
127
+ command = @command_builder.generate(command, kwargs)
101
128
 
102
129
  command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
103
130
  input_cursor = Integer(command[1])
@@ -105,14 +132,14 @@ class RedisClient
105
132
  client_index = input_cursor % 256
106
133
  raw_cursor = input_cursor >> 8
107
134
 
108
- clients = @node.scale_reading_clients
135
+ clients = @node.clients_for_scanning(seed: seed)
109
136
 
110
137
  client = clients[client_index]
111
138
  return [ZERO_CURSOR_FOR_SCAN, []] unless client
112
139
 
113
140
  command[1] = raw_cursor.to_s
114
141
 
115
- result_cursor, result_keys = client.call(*command, **kwargs)
142
+ result_cursor, result_keys = client.call_v(command)
116
143
  result_cursor = Integer(result_cursor)
117
144
 
118
145
  client_index += 1 if result_cursor == 0
@@ -120,21 +147,19 @@ class RedisClient
120
147
  [((result_cursor << 8) + client_index).to_s, result_keys]
121
148
  end
122
149
 
123
- def assign_node(*command, **kwargs)
124
- node_key = find_node_key(*command, **kwargs)
150
+ def assign_node(command)
151
+ node_key = find_node_key(command)
125
152
  find_node(node_key)
126
153
  end
127
154
 
128
- def find_node_key(*command, primary_only: false, **kwargs)
129
- command = @command_builder.generate!(command, kwargs)
130
-
155
+ def find_node_key(command, seed: nil)
131
156
  key = @command.extract_first_key(command)
132
157
  slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
133
158
 
134
- if @command.should_send_to_primary?(command) || primary_only
135
- @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
159
+ if @command.should_send_to_primary?(command)
160
+ @node.find_node_key_of_primary(slot) || @node.any_primary_node_key(seed: seed)
136
161
  else
137
- @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
162
+ @node.find_node_key_of_replica(slot, seed: seed) || @node.any_replica_node_key(seed: seed)
138
163
  end
139
164
  end
140
165
 
@@ -154,8 +179,8 @@ class RedisClient
154
179
 
155
180
  private
156
181
 
157
- def send_wait_command(method, *args, retry_count: 3, **kwargs, &block)
158
- @node.call_primaries(method, *args, **kwargs, &block).select { |r| r.is_a?(Integer) }.sum
182
+ def send_wait_command(method, command, args, retry_count: 3, &block)
183
+ @node.call_primaries(method, command, args, &block).select { |r| r.is_a?(Integer) }.sum
159
184
  rescue ::RedisClient::Cluster::ErrorCollection => e
160
185
  raise if retry_count <= 0
161
186
  raise if e.errors.values.none? do |err|
@@ -167,82 +192,65 @@ class RedisClient
167
192
  retry
168
193
  end
169
194
 
170
- def send_config_command(method, *args, **kwargs, &block)
171
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
172
- command = @command_builder.generate!(command, kwargs)
173
-
195
+ def send_config_command(method, command, args, &block)
174
196
  case command[1].to_s.downcase
175
197
  when 'resetstat', 'rewrite', 'set'
176
- @node.call_all(method, *args, **kwargs, &block).first
177
- else assign_node(*command).send(method, *args, **kwargs, &block)
198
+ @node.call_all(method, command, args, &block).first
199
+ else assign_node(command).send(method, *args, command, &block)
178
200
  end
179
201
  end
180
202
 
181
- def send_memory_command(method, *args, **kwargs, &block)
182
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
183
- command = @command_builder.generate!(command, kwargs)
184
-
203
+ def send_memory_command(method, command, args, &block)
185
204
  case command[1].to_s.downcase
186
- when 'stats' then @node.call_all(method, *args, **kwargs, &block)
187
- when 'purge' then @node.call_all(method, *args, **kwargs, &block).first
188
- else assign_node(*command).send(method, *args, **kwargs, &block)
205
+ when 'stats' then @node.call_all(method, command, args, &block)
206
+ when 'purge' then @node.call_all(method, command, args, &block).first
207
+ else assign_node(command).send(method, *args, command, &block)
189
208
  end
190
209
  end
191
210
 
192
- def send_client_command(method, *args, **kwargs, &block)
193
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
194
- command = @command_builder.generate!(command, kwargs)
195
-
211
+ def send_client_command(method, command, args, &block)
196
212
  case command[1].to_s.downcase
197
- when 'list' then @node.call_all(method, *args, **kwargs, &block).flatten
213
+ when 'list' then @node.call_all(method, command, args, &block).flatten
198
214
  when 'pause', 'reply', 'setname'
199
- @node.call_all(method, *args, **kwargs, &block).first
200
- else assign_node(*command).send(method, *args, **kwargs, &block)
215
+ @node.call_all(method, command, args, &block).first
216
+ else assign_node(command).send(method, *args, command, &block)
201
217
  end
202
218
  end
203
219
 
204
- def send_cluster_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
205
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
206
- command = @command_builder.generate!(command, kwargs)
220
+ def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/MethodLength
207
221
  subcommand = command[1].to_s.downcase
208
222
 
209
223
  case subcommand
210
224
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
211
225
  'reset', 'set-config-epoch', 'setslot'
212
226
  raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
213
- when 'saveconfig' then @node.call_all(method, *args, **kwargs, &block).first
227
+ when 'saveconfig' then @node.call_all(method, command, args, &block).first
214
228
  when 'getkeysinslot'
215
229
  raise ArgumentError, command.join(' ') if command.size != 4
216
230
 
217
- find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, **kwargs, &block)
218
- else assign_node(*command).send(method, *args, **kwargs, &block)
231
+ find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, command, &block)
232
+ else assign_node(command).send(method, *args, command, &block)
219
233
  end
220
234
  end
221
235
 
222
- def send_script_command(method, *args, **kwargs, &block)
223
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
224
- command = @command_builder.generate!(command, kwargs)
225
-
236
+ def send_script_command(method, command, args, &block)
226
237
  case command[1].to_s.downcase
227
238
  when 'debug', 'kill'
228
- @node.call_all(method, *args, **kwargs, &block).first
239
+ @node.call_all(method, command, args, &block).first
229
240
  when 'flush', 'load'
230
- @node.call_primaries(method, *args, **kwargs, &block).first
231
- else assign_node(*command).send(method, *args, **kwargs, &block)
241
+ @node.call_primaries(method, command, args, &block).first
242
+ else assign_node(command).send(method, *args, command, &block)
232
243
  end
233
244
  end
234
245
 
235
- def send_pubsub_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
236
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
237
- command = @command_builder.generate!(command, kwargs)
238
-
246
+ def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
239
247
  case command[1].to_s.downcase
240
- when 'channels' then @node.call_all(method, *args, **kwargs, &block).flatten.uniq.sort_by(&:to_s)
248
+ when 'channels' then @node.call_all(method, command, args, &block).flatten.uniq.sort_by(&:to_s)
241
249
  when 'numsub'
242
- @node.call_all(method, *args, **kwargs, &block).reject(&:empty?).map { |e| Hash[*e] }
250
+ @node.call_all(method, command, args, &block).reject(&:empty?).map { |e| Hash[*e] }
243
251
  .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
244
- when 'numpat' then @node.call_all(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
245
- else assign_node(*command).send(method, *args, **kwargs, &block)
252
+ when 'numpat' then @node.call_all(method, command, args, &block).select { |e| e.is_a?(Integer) }.sum
253
+ else assign_node(command).send(method, *args, command, &block)
246
254
  end
247
255
  end
248
256
 
@@ -258,18 +266,24 @@ class RedisClient
258
266
  find_node(node_key)
259
267
  end
260
268
 
261
- def fetch_cluster_info(config, pool: nil, **kwargs)
269
+ def fetch_cluster_info(config, pool: nil, **kwargs) # rubocop:disable Metrics/MethodLength
262
270
  node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
263
271
  node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
264
272
  config.update_node(node_addrs)
265
- ::RedisClient::Cluster::Node.new(config.per_node_key,
266
- node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
273
+ ::RedisClient::Cluster::Node.new(
274
+ config.per_node_key,
275
+ node_info: node_info,
276
+ pool: pool,
277
+ with_replica: config.use_replica?,
278
+ replica_affinity: config.replica_affinity,
279
+ **kwargs
280
+ )
267
281
  end
268
282
 
269
283
  def update_cluster_info!
270
284
  @mutex.synchronize do
271
285
  begin
272
- @node.call_all(:close)
286
+ @node.each(&:close)
273
287
  rescue ::RedisClient::Cluster::ErrorCollection
274
288
  # ignore
275
289
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'redis_client'
4
3
  require 'redis_client/cluster/pipeline'
5
4
  require 'redis_client/cluster/pub_sub'
6
5
  require 'redis_client/cluster/router'
@@ -9,54 +8,77 @@ class RedisClient
9
8
  class Cluster
10
9
  ZERO_CURSOR_FOR_SCAN = '0'
11
10
 
11
+ attr_reader :config
12
+
12
13
  def initialize(config, pool: nil, **kwargs)
14
+ @config = config
13
15
  @router = ::RedisClient::Cluster::Router.new(config, pool: pool, **kwargs)
16
+ @command_builder = config.command_builder
14
17
  end
15
18
 
16
19
  def inspect
17
20
  "#<#{self.class.name} #{@router.node.node_keys.join(', ')}>"
18
21
  end
19
22
 
20
- def call(*command, **kwargs, &block)
21
- @router.send_command(:call, *command, **kwargs, &block)
23
+ def call(*args, **kwargs, &block)
24
+ command = @command_builder.generate(args, kwargs)
25
+ @router.send_command(:call_v, command, &block)
26
+ end
27
+
28
+ def call_v(command, &block)
29
+ command = @command_builder.generate(command)
30
+ @router.send_command(:call_v, command, &block)
31
+ end
32
+
33
+ def call_once(*args, **kwargs, &block)
34
+ command = @command_builder.generate(args, kwargs)
35
+ @router.send_command(:call_once_v, command, &block)
36
+ end
37
+
38
+ def call_once_v(command, &block)
39
+ command = @command_builder.generate(command)
40
+ @router.send_command(:call_once_v, command, &block)
22
41
  end
23
42
 
24
- def call_once(*command, **kwargs, &block)
25
- @router.send_command(:call_once, *command, **kwargs, &block)
43
+ def blocking_call(timeout, *args, **kwargs, &block)
44
+ command = @command_builder.generate(args, kwargs)
45
+ @router.send_command(:blocking_call_v, command, timeout, &block)
26
46
  end
27
47
 
28
- def blocking_call(timeout, *command, **kwargs, &block)
29
- @router.send_command(:blocking_call, timeout, *command, **kwargs, &block)
48
+ def blocking_call_v(timeout, command, &block)
49
+ command = @command_builder.generate(command)
50
+ @router.send_command(:blocking_call_v, command, timeout, &block)
30
51
  end
31
52
 
32
53
  def scan(*args, **kwargs, &block)
33
54
  raise ArgumentError, 'block required' unless block
34
55
 
56
+ seed = Random.new_seed
35
57
  cursor = ZERO_CURSOR_FOR_SCAN
36
58
  loop do
37
- cursor, keys = @router.scan('SCAN', cursor, *args, **kwargs)
59
+ cursor, keys = @router.scan('SCAN', cursor, *args, seed: seed, **kwargs)
38
60
  keys.each(&block)
39
61
  break if cursor == ZERO_CURSOR_FOR_SCAN
40
62
  end
41
63
  end
42
64
 
43
65
  def sscan(key, *args, **kwargs, &block)
44
- node = @router.assign_node('SSCAN', key)
45
- @router.try_send(node, :sscan, key, *args, **kwargs, &block)
66
+ node = @router.assign_node(['SSCAN', key])
67
+ @router.try_delegate(node, :sscan, key, *args, **kwargs, &block)
46
68
  end
47
69
 
48
70
  def hscan(key, *args, **kwargs, &block)
49
- node = @router.assign_node('HSCAN', key)
50
- @router.try_send(node, :hscan, key, *args, **kwargs, &block)
71
+ node = @router.assign_node(['HSCAN', key])
72
+ @router.try_delegate(node, :hscan, key, *args, **kwargs, &block)
51
73
  end
52
74
 
53
75
  def zscan(key, *args, **kwargs, &block)
54
- node = @router.assign_node('ZSCAN', key)
55
- @router.try_send(node, :zscan, key, *args, **kwargs, &block)
76
+ node = @router.assign_node(['ZSCAN', key])
77
+ @router.try_delegate(node, :zscan, key, *args, **kwargs, &block)
56
78
  end
57
79
 
58
80
  def pipelined
59
- pipeline = ::RedisClient::Cluster::Pipeline.new(@router)
81
+ pipeline = ::RedisClient::Cluster::Pipeline.new(@router, @command_builder)
60
82
  yield pipeline
61
83
  return [] if pipeline.empty? == 0
62
84
 
@@ -64,11 +86,11 @@ class RedisClient
64
86
  end
65
87
 
66
88
  def pubsub
67
- ::RedisClient::Cluster::PubSub.new(@router)
89
+ ::RedisClient::Cluster::PubSub.new(@router, @command_builder)
68
90
  end
69
91
 
70
92
  def close
71
- @router.node.call_all(:close)
93
+ @router.node.each(&:close)
72
94
  nil
73
95
  end
74
96
 
@@ -77,7 +99,8 @@ class RedisClient
77
99
  def method_missing(name, *args, **kwargs, &block)
78
100
  if @router.command_exists?(name)
79
101
  args.unshift(name)
80
- return @router.send_command(:call, *args, **kwargs, &block)
102
+ command = @command_builder.generate(args, kwargs)
103
+ return @router.send_command(:call_v, command, &block)
81
104
  end
82
105
 
83
106
  super
@@ -20,15 +20,25 @@ class RedisClient
20
20
 
21
21
  InvalidClientConfigError = Class.new(::RedisClient::Error)
22
22
 
23
- attr_reader :command_builder
23
+ attr_reader :command_builder, :client_config, :replica_affinity
24
+
25
+ def initialize( # rubocop:disable Metrics/ParameterLists
26
+ nodes: DEFAULT_NODES,
27
+ replica: false,
28
+ replica_affinity: :random,
29
+ fixed_hostname: '',
30
+ client_implementation: Cluster,
31
+ **client_config
32
+ )
24
33
 
25
- def initialize(nodes: DEFAULT_NODES, replica: false, fixed_hostname: '', **client_config)
26
34
  @replica = true & replica
35
+ @replica_affinity = replica_affinity.to_s.to_sym
27
36
  @fixed_hostname = fixed_hostname.to_s
28
37
  @node_configs = build_node_configs(nodes.dup)
29
38
  client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
30
39
  @command_builder = client_config.fetch(:command_builder, ::RedisClient::CommandBuilder)
31
40
  @client_config = merge_generic_config(client_config, @node_configs)
41
+ @client_implementation = client_implementation
32
42
  @mutex = Mutex.new
33
43
  end
34
44
 
@@ -36,12 +46,16 @@ class RedisClient
36
46
  "#<#{self.class.name} #{per_node_key.values}>"
37
47
  end
38
48
 
49
+ def read_timeout
50
+ @client_config[:read_timeout] || @client_config[:timeout] || RedisClient::Config::DEFAULT_TIMEOUT
51
+ end
52
+
39
53
  def new_pool(size: 5, timeout: 5, **kwargs)
40
- ::RedisClient::Cluster.new(self, pool: { size: size, timeout: timeout }, **kwargs)
54
+ @client_implementation.new(self, pool: { size: size, timeout: timeout }, **kwargs)
41
55
  end
42
56
 
43
57
  def new_client(**kwargs)
44
- ::RedisClient::Cluster.new(self, **kwargs)
58
+ @client_implementation.new(self, **kwargs)
45
59
  end
46
60
 
47
61
  def per_node_key
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.1.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-02 00:00:00.000000000 Z
11
+ date: 2022-09-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.5'
19
+ version: '0.6'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.5'
26
+ version: '0.6'
27
27
  description:
28
28
  email:
29
29
  - proxy0721@gmail.com
@@ -31,11 +31,16 @@ executables: []
31
31
  extensions: []
32
32
  extra_rdoc_files: []
33
33
  files:
34
+ - lib/redis-cluster-client.rb
34
35
  - lib/redis_client/cluster.rb
35
36
  - lib/redis_client/cluster/command.rb
36
37
  - lib/redis_client/cluster/errors.rb
37
38
  - lib/redis_client/cluster/key_slot_converter.rb
38
39
  - lib/redis_client/cluster/node.rb
40
+ - lib/redis_client/cluster/node/latency_replica.rb
41
+ - lib/redis_client/cluster/node/primary_only.rb
42
+ - lib/redis_client/cluster/node/random_replica.rb
43
+ - lib/redis_client/cluster/node/replica_mixin.rb
39
44
  - lib/redis_client/cluster/node_key.rb
40
45
  - lib/redis_client/cluster/pipeline.rb
41
46
  - lib/redis_client/cluster/pub_sub.rb