redis-cluster-client 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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